Make WordPress Core


Ignore:
Timestamp:
08/08/2024 07:23:53 AM (15 months ago)
Author:
dmsnell
Message:

HTML API: Add support for SVG and MathML (Foreign content)

As part of work to add more spec support to the HTML API, this patch adds
support for SVG and MathML elements, or more generally, "foreign content."

The rules in foreign content are a mix of XML and HTML parsing rules and
introduce additional complexity into the processor, but is important in
order to avoid getting lost when inside these elements.

Developed in https://github.com/wordpress/wordpress-develop/pull/6006
Discussed in https://core.trac.wordpress.org/ticket/61576

Props: dmsnell, jonsurrell, westonruter.
See #61576.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/html-api/class-wp-html-tag-processor.php

    r58866 r58867  
    513513
    514514    /**
     515     * Indicates whether the parser is inside foreign content,
     516     * e.g. inside an SVG or MathML element.
     517     *
     518     * One of 'html', 'svg', or 'math'.
     519     *
     520     * Several parsing rules change based on whether the parser
     521     * is inside foreign content, including whether CDATA sections
     522     * are allowed and whether a self-closing flag indicates that
     523     * an element has no content.
     524     *
     525     * @since 6.7.0
     526     *
     527     * @var string
     528     */
     529    private $parsing_namespace = 'html';
     530
     531    /**
    515532     * What kind of syntax token became an HTML comment.
    516533     *
     
    779796    public function __construct( $html ) {
    780797        $this->html = $html;
     798    }
     799
     800    /**
     801     * Switches parsing mode into a new namespace, such as when
     802     * encountering an SVG tag and entering foreign content.
     803     *
     804     * @since 6.7.0
     805     *
     806     * @param string $new_namespace One of 'html', 'svg', or 'math' indicating into what
     807     *                              namespace the next tokens will be processed.
     808     * @return bool Whether the namespace was valid and changed.
     809     */
     810    public function change_parsing_namespace( string $new_namespace ): bool {
     811        if ( ! in_array( $new_namespace, array( 'html', 'math', 'svg' ), true ) ) {
     812            return false;
     813        }
     814
     815        $this->parsing_namespace = $new_namespace;
     816        return true;
    781817    }
    782818
     
    844880     *
    845881     * @since 6.5.0
     882     * @since 6.7.0 Recognizes CDATA sections within foreign content.
    846883     *
    847884     * @return bool Whether a token was parsed.
     
    957994        if (
    958995            $this->is_closing_tag ||
     996            'html' !== $this->parsing_namespace ||
    959997            1 !== strspn( $this->html, 'iIlLnNpPsStTxX', $this->tag_name_starts_at, 1 )
    960998        ) {
     
    9971035
    9981036        // Find the closing tag if necessary.
    999         $found_closer = false;
    10001037        switch ( $tag_name ) {
    10011038            case 'SCRIPT':
     
    17571794                    $this->text_length          = $closer_at - $this->text_starts_at;
    17581795                    $this->bytes_already_parsed = $closer_at + 1;
     1796                    return true;
     1797                }
     1798
     1799                if (
     1800                    'html' !== $this->parsing_namespace &&
     1801                    strlen( $html ) > $at + 8 &&
     1802                    '[' === $html[ $at + 2 ] &&
     1803                    'C' === $html[ $at + 3 ] &&
     1804                    'D' === $html[ $at + 4 ] &&
     1805                    'A' === $html[ $at + 5 ] &&
     1806                    'T' === $html[ $at + 6 ] &&
     1807                    'A' === $html[ $at + 7 ] &&
     1808                    '[' === $html[ $at + 8 ]
     1809                ) {
     1810                    $closer_at = strpos( $html, ']]>', $at + 9 );
     1811                    if ( false === $closer_at ) {
     1812                        $this->parser_state = self::STATE_INCOMPLETE_INPUT;
     1813
     1814                        return false;
     1815                    }
     1816
     1817                    $this->parser_state         = self::STATE_CDATA_NODE;
     1818                    $this->text_starts_at       = $at + 9;
     1819                    $this->text_length          = $closer_at - $this->text_starts_at;
     1820                    $this->token_length         = $closer_at + 3 - $this->token_starts_at;
     1821                    $this->bytes_already_parsed = $closer_at + 3;
    17591822                    return true;
    17601823                }
     
    26552718
    26562719    /**
     2720     * Returns the namespace of the matched token.
     2721     *
     2722     * @since 6.7.0
     2723     *
     2724     * @return string One of 'html', 'math', or 'svg'.
     2725     */
     2726    public function get_namespace(): string {
     2727        return $this->parsing_namespace;
     2728    }
     2729
     2730    /**
    26572731     * Returns the uppercase name of the matched tag.
    26582732     *
     
    26892763
    26902764        return null;
     2765    }
     2766
     2767    /**
     2768     * Returns the adjusted tag name for a given token, taking into
     2769     * account the current parsing context, whether HTML, SVG, or MathML.
     2770     *
     2771     * @since 6.7.0
     2772     *
     2773     * @return string|null Name of current tag name.
     2774     */
     2775    public function get_qualified_tag_name(): ?string {
     2776        $tag_name = $this->get_tag();
     2777        if ( null === $tag_name ) {
     2778            return null;
     2779        }
     2780
     2781        if ( 'html' === $this->get_namespace() ) {
     2782            return $tag_name;
     2783        }
     2784
     2785        $lower_tag_name = strtolower( $tag_name );
     2786        if ( 'math' === $this->get_namespace() ) {
     2787            return $lower_tag_name;
     2788        }
     2789
     2790        if ( 'svg' === $this->get_namespace() ) {
     2791            switch ( $lower_tag_name ) {
     2792                case 'altglyph':
     2793                    return 'altGlyph';
     2794
     2795                case 'altglyphdef':
     2796                    return 'altGlyphDef';
     2797
     2798                case 'altglyphitem':
     2799                    return 'altGlyphItem';
     2800
     2801                case 'animatecolor':
     2802                    return 'animateColor';
     2803
     2804                case 'animatemotion':
     2805                    return 'animateMotion';
     2806
     2807                case 'animatetransform':
     2808                    return 'animateTransform';
     2809
     2810                case 'clippath':
     2811                    return 'clipPath';
     2812
     2813                case 'feblend':
     2814                    return 'feBlend';
     2815
     2816                case 'fecolormatrix':
     2817                    return 'feColorMatrix';
     2818
     2819                case 'fecomponenttransfer':
     2820                    return 'feComponentTransfer';
     2821
     2822                case 'fecomposite':
     2823                    return 'feComposite';
     2824
     2825                case 'feconvolvematrix':
     2826                    return 'feConvolveMatrix';
     2827
     2828                case 'fediffuselighting':
     2829                    return 'feDiffuseLighting';
     2830
     2831                case 'fedisplacementmap':
     2832                    return 'feDisplacementMap';
     2833
     2834                case 'fedistantlight':
     2835                    return 'feDistantLight';
     2836
     2837                case 'fedropshadow':
     2838                    return 'feDropShadow';
     2839
     2840                case 'feflood':
     2841                    return 'feFlood';
     2842
     2843                case 'fefunca':
     2844                    return 'feFuncA';
     2845
     2846                case 'fefuncb':
     2847                    return 'feFuncB';
     2848
     2849                case 'fefuncg':
     2850                    return 'feFuncG';
     2851
     2852                case 'fefuncr':
     2853                    return 'feFuncR';
     2854
     2855                case 'fegaussianblur':
     2856                    return 'feGaussianBlur';
     2857
     2858                case 'feimage':
     2859                    return 'feImage';
     2860
     2861                case 'femerge':
     2862                    return 'feMerge';
     2863
     2864                case 'femergenode':
     2865                    return 'feMergeNode';
     2866
     2867                case 'femorphology':
     2868                    return 'feMorphology';
     2869
     2870                case 'feoffset':
     2871                    return 'feOffset';
     2872
     2873                case 'fepointlight':
     2874                    return 'fePointLight';
     2875
     2876                case 'fespecularlighting':
     2877                    return 'feSpecularLighting';
     2878
     2879                case 'fespotlight':
     2880                    return 'feSpotLight';
     2881
     2882                case 'fetile':
     2883                    return 'feTile';
     2884
     2885                case 'feturbulence':
     2886                    return 'feTurbulence';
     2887
     2888                case 'foreignobject':
     2889                    return 'foreignObject';
     2890
     2891                case 'glyphref':
     2892                    return 'glyphRef';
     2893
     2894                case 'lineargradient':
     2895                    return 'linearGradient';
     2896
     2897                case 'radialgradient':
     2898                    return 'radialGradient';
     2899
     2900                case 'textpath':
     2901                    return 'textPath';
     2902
     2903                default:
     2904                    return $lower_tag_name;
     2905            }
     2906        }
     2907    }
     2908
     2909    /**
     2910     * Returns the adjusted attribute name for a given attribute, taking into
     2911     * account the current parsing context, whether HTML, SVG, or MathML.
     2912     *
     2913     * @since 6.7.0
     2914     *
     2915     * @param string $attribute_name Which attribute to adjust.
     2916     *
     2917     * @return string|null
     2918     */
     2919    public function get_qualified_attribute_name( $attribute_name ): ?string {
     2920        if ( self::STATE_MATCHED_TAG !== $this->parser_state ) {
     2921            return null;
     2922        }
     2923
     2924        $namespace = $this->get_namespace();
     2925        $lower_name = strtolower( $attribute_name );
     2926
     2927        if ( 'math' === $namespace && 'definitionurl' === $lower_name ) {
     2928            return 'definitionURL';
     2929        }
     2930
     2931        if ( 'svg' === $this->get_namespace() ) {
     2932            switch ( $lower_name ) {
     2933                case 'attributename':
     2934                    return 'attributeName';
     2935
     2936                case 'attributetype':
     2937                    return 'attributeType';
     2938
     2939                case 'basefrequency':
     2940                    return 'baseFrequency';
     2941
     2942                case 'baseprofile':
     2943                    return 'baseProfile';
     2944
     2945                case 'calcmode':
     2946                    return 'calcMode';
     2947
     2948                case 'clippathunits':
     2949                    return 'clipPathUnits';
     2950
     2951                case 'diffuseconstant':
     2952                    return 'diffuseConstant';
     2953
     2954                case 'edgemode':
     2955                    return 'edgeMode';
     2956
     2957                case 'filterunits':
     2958                    return 'filterUnits';
     2959
     2960                case 'glyphref':
     2961                    return 'glyphRef';
     2962
     2963                case 'gradienttransform':
     2964                    return 'gradientTransform';
     2965
     2966                case 'gradientunits':
     2967                    return 'gradientUnits';
     2968
     2969                case 'kernelmatrix':
     2970                    return 'kernelMatrix';
     2971
     2972                case 'kernelunitlength':
     2973                    return 'kernelUnitLength';
     2974
     2975                case 'keypoints':
     2976                    return 'keyPoints';
     2977
     2978                case 'keysplines':
     2979                    return 'keySplines';
     2980
     2981                case 'keytimes':
     2982                    return 'keyTimes';
     2983
     2984                case 'lengthadjust':
     2985                    return 'lengthAdjust';
     2986
     2987                case 'limitingconeangle':
     2988                    return 'limitingConeAngle';
     2989
     2990                case 'markerheight':
     2991                    return 'markerHeight';
     2992
     2993                case 'markerunits':
     2994                    return 'markerUnits';
     2995
     2996                case 'markerwidth':
     2997                    return 'markerWidth';
     2998
     2999                case 'maskcontentunits':
     3000                    return 'maskContentUnits';
     3001
     3002                case 'maskunits':
     3003                    return 'maskUnits';
     3004
     3005                case 'numoctaves':
     3006                    return 'numOctaves';
     3007
     3008                case 'pathlength':
     3009                    return 'pathLength';
     3010
     3011                case 'patterncontentunits':
     3012                    return 'patternContentUnits';
     3013
     3014                case 'patterntransform':
     3015                    return 'patternTransform';
     3016
     3017                case 'patternunits':
     3018                    return 'patternUnits';
     3019
     3020                case 'pointsatx':
     3021                    return 'pointsAtX';
     3022
     3023                case 'pointsaty':
     3024                    return 'pointsAtY';
     3025
     3026                case 'pointsatz':
     3027                    return 'pointsAtZ';
     3028
     3029                case 'preservealpha':
     3030                    return 'preserveAlpha';
     3031
     3032                case 'preserveaspectratio':
     3033                    return 'preserveAspectRatio';
     3034
     3035                case 'primitiveunits':
     3036                    return 'primitiveUnits';
     3037
     3038                case 'refx':
     3039                    return 'refX';
     3040
     3041                case 'refy':
     3042                    return 'refY';
     3043
     3044                case 'repeatcount':
     3045                    return 'repeatCount';
     3046
     3047                case 'repeatdur':
     3048                    return 'repeatDur';
     3049
     3050                case 'requiredextensions':
     3051                    return 'requiredExtensions';
     3052
     3053                case 'requiredfeatures':
     3054                    return 'requiredFeatures';
     3055
     3056                case 'specularconstant':
     3057                    return 'specularConstant';
     3058
     3059                case 'specularexponent':
     3060                    return 'specularExponent';
     3061
     3062                case 'spreadmethod':
     3063                    return 'spreadMethod';
     3064
     3065                case 'startoffset':
     3066                    return 'startOffset';
     3067
     3068                case 'stddeviation':
     3069                    return 'stdDeviation';
     3070
     3071                case 'stitchtiles':
     3072                    return 'stitchTiles';
     3073
     3074                case 'surfacescale':
     3075                    return 'surfaceScale';
     3076
     3077                case 'systemlanguage':
     3078                    return 'systemLanguage';
     3079
     3080                case 'tablevalues':
     3081                    return 'tableValues';
     3082
     3083                case 'targetx':
     3084                    return 'targetX';
     3085
     3086                case 'targety':
     3087                    return 'targetY';
     3088
     3089                case 'textlength':
     3090                    return 'textLength';
     3091
     3092                case 'viewbox':
     3093                    return 'viewBox';
     3094
     3095                case 'viewtarget':
     3096                    return 'viewTarget';
     3097
     3098                case 'xchannelselector':
     3099                    return 'xChannelSelector';
     3100
     3101                case 'ychannelselector':
     3102                    return 'yChannelSelector';
     3103
     3104                case 'zoomandpan':
     3105                    return 'zoomAndPan';
     3106            }
     3107        }
     3108
     3109        if ( 'html' !== $namespace ) {
     3110            switch ( $lower_name ) {
     3111                case 'xlink:actuate':
     3112                    return 'xlink actuate';
     3113
     3114                case 'xlink:arcrole':
     3115                    return 'xlink arcrole';
     3116
     3117                case 'xlink:href':
     3118                    return 'xlink href';
     3119
     3120                case 'xlink:role':
     3121                    return 'xlink role';
     3122
     3123                case 'xlink:show':
     3124                    return 'xlink show';
     3125
     3126                case 'xlink:title':
     3127                    return 'xlink title';
     3128
     3129                case 'xlink:type':
     3130                    return 'xlink type';
     3131
     3132                case 'xml:lang':
     3133                    return 'xml lang';
     3134
     3135                case 'xml:space':
     3136                    return 'xml space';
     3137
     3138                case 'xmlns':
     3139                    return 'xmlns';
     3140
     3141                case 'xmlns:xlink':
     3142                    return 'xmlns xlink';
     3143            }
     3144        }
     3145
     3146        return $attribute_name;
    26913147    }
    26923148
     
    29643420         * for security reasons (to avoid joining together strings that were safe
    29653421         * when separated, but not when joined).
     3422         *
     3423         * @todo Inside HTML integration points and MathML integration points, the
     3424         *       text is processed according to the insertion mode, not according
     3425         *       to the foreign content rules. This should strip the NULL bytes.
    29663426         */
    2967         return '#text' === $tag_name
     3427        return ( '#text' === $tag_name && 'html' === $this->get_namespace() )
    29683428            ? str_replace( "\x00", '', $decoded )
    29693429            : str_replace( "\x00", "\u{FFFD}", $decoded );
Note: See TracChangeset for help on using the changeset viewer.