Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.14% covered (success)
97.14%
68 / 70
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
InForeignContent
97.14% covered (success)
97.14%
68 / 70
60.00% covered (warning)
60.00%
3 / 5
25
0.00% covered (danger)
0.00%
0 / 1
 characters
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
5
 isIntegrationPoint
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 startTag
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
11
 endTag
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 endDocument
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Wikimedia\RemexHtml\TreeBuilder;
4
5use Wikimedia\RemexHtml\HTMLData;
6use Wikimedia\RemexHtml\Tokenizer\Attributes;
7
8/**
9 * The rules for parsing tokens in foreign content.
10 *
11 * This is not referred to as an insertion mode in the spec, but is
12 * sufficiently similar to one that we can inherit from InsertionMode here.
13 */
14class InForeignContent extends InsertionMode {
15    /**
16     * The list of tag names which unconditionally generate a parse error when
17     * seen in foreign content.
18     * @var array<string,bool>
19     */
20    private static $notAllowed = [
21        'b' => true,
22        'big' => true,
23        'blockquote' => true,
24        'body' => true,
25        'br' => true,
26        'center' => true,
27        'code' => true,
28        'dd' => true,
29        'div' => true,
30        'dl' => true,
31        'dt' => true,
32        'em' => true,
33        'embed' => true,
34        'h1' => true,
35        'h2' => true,
36        'h3' => true,
37        'h4' => true,
38        'h5' => true,
39        'h6' => true,
40        'head' => true,
41        'hr' => true,
42        'i' => true,
43        'img' => true,
44        'li' => true,
45        'listing' => true,
46        'menu' => true,
47        'meta' => true,
48        'nobr' => true,
49        'ol' => true,
50        'p' => true,
51        'pre' => true,
52        'ruby' => true,
53        's' => true,
54        'small' => true,
55        'span' => true,
56        'strong' => true,
57        'strike' => true,
58        'sub' => true,
59        'sup' => true,
60        'table' => true,
61        'tt' => true,
62        'u' => true,
63        'ul' => true,
64        'var' => true,
65    ];
66
67    /**
68     * The table for correcting the tag names of SVG elements, given in the
69     * "Any other start tag" section of the spec.
70     * @var array<string,string>
71     */
72    private static $svgElementCase = [
73        'altglyph' => 'altGlyph',
74        'altglyphdef' => 'altGlyphDef',
75        'altglyphitem' => 'altGlyphItem',
76        'animatecolor' => 'animateColor',
77        'animatemotion' => 'animateMotion',
78        'animatetransform' => 'animateTransform',
79        'clippath' => 'clipPath',
80        'feblend' => 'feBlend',
81        'fecolormatrix' => 'feColorMatrix',
82        'fecomponenttransfer' => 'feComponentTransfer',
83        'fecomposite' => 'feComposite',
84        'feconvolvematrix' => 'feConvolveMatrix',
85        'fediffuselighting' => 'feDiffuseLighting',
86        'fedisplacementmap' => 'feDisplacementMap',
87        'fedistantlight' => 'feDistantLight',
88        'fedropshadow' => 'feDropShadow',
89        'feflood' => 'feFlood',
90        'fefunca' => 'feFuncA',
91        'fefuncb' => 'feFuncB',
92        'fefuncg' => 'feFuncG',
93        'fefuncr' => 'feFuncR',
94        'fegaussianblur' => 'feGaussianBlur',
95        'feimage' => 'feImage',
96        'femerge' => 'feMerge',
97        'femergenode' => 'feMergeNode',
98        'femorphology' => 'feMorphology',
99        'feoffset' => 'feOffset',
100        'fepointlight' => 'fePointLight',
101        'fespecularlighting' => 'feSpecularLighting',
102        'fespotlight' => 'feSpotLight',
103        'fetile' => 'feTile',
104        'feturbulence' => 'feTurbulence',
105        'foreignobject' => 'foreignObject',
106        'glyphref' => 'glyphRef',
107        'lineargradient' => 'linearGradient',
108        'radialgradient' => 'radialGradient',
109        'textpath' => 'textPath',
110    ];
111
112    public function characters( $text, $start, $length, $sourceStart, $sourceLength ) {
113        $builder = $this->builder;
114
115        while ( $length ) {
116            $normalLength = strcspn( $text, "\0\t\n\f\r ", $start, $length );
117            if ( $normalLength ) {
118                $builder->framesetOK = false;
119                $builder->insertCharacters( $text, $start, $normalLength,
120                    $sourceStart, $sourceLength );
121            }
122            $start += $normalLength;
123            $length -= $normalLength;
124            $sourceStart += $normalLength;
125            $sourceLength -= $normalLength;
126            if ( !$length ) {
127                break;
128            }
129
130            $char = $text[$start];
131            if ( $char === "\0" ) {
132                $builder->error( "replaced null character", $sourceStart );
133                $builder->insertCharacters( "\xef\xbf\xbd", 0, 3, $sourceStart, $sourceLength );
134                $start++;
135                $length--;
136                $sourceStart++;
137                $sourceLength--;
138            } else {
139                // Whitespace
140                $wsLength = strspn( $text, "\t\n\f\r ", $start, $length );
141                $builder->insertCharacters( $text, $start, $wsLength, $sourceStart, $wsLength );
142                $start += $wsLength;
143                $length -= $wsLength;
144                $sourceStart += $wsLength;
145                $sourceLength -= $wsLength;
146            }
147        }
148    }
149
150    private function isIntegrationPoint( Element $element ) {
151        return $element->namespace === HTMLData::NS_HTML
152            || $element->isMathmlTextIntegration()
153            || $element->isHtmlIntegration();
154    }
155
156    public function startTag( $name, Attributes $attrs, $selfClose, $sourceStart, $sourceLength ) {
157        $builder = $this->builder;
158        $stack = $builder->stack;
159        $dispatcher = $this->dispatcher;
160
161        if ( isset( self::$notAllowed[$name] ) ) {
162            $allowed = false;
163        } elseif ( $name === 'font' && (
164            isset( $attrs['color'] ) || isset( $attrs['face'] ) || isset( $attrs['size'] ) )
165        ) {
166            $allowed = false;
167        } else {
168            $allowed = true;
169        }
170
171        if ( !$allowed ) {
172            $builder->error( "unexpected <$name> tag in foreign content", $sourceStart );
173            if ( !$builder->isFragment ) {
174                do {
175                    $builder->pop( $sourceStart, 0 );
176                } while ( $stack->current && !$this->isIntegrationPoint( $stack->current ) );
177                $dispatcher->startTag( $name, $attrs, $selfClose, $sourceStart, $sourceLength );
178                return;
179            }
180        }
181
182        $acnNs = $builder->adjustedCurrentNode()->namespace;
183        if ( $acnNs === HTMLData::NS_MATHML ) {
184            $attrs = new ForeignAttributes( $attrs, 'math' );
185        } elseif ( $acnNs === HTMLData::NS_SVG ) {
186            $attrs = new ForeignAttributes( $attrs, 'svg' );
187            $name = self::$svgElementCase[$name] ?? $name;
188        } else {
189            $attrs = new ForeignAttributes( $attrs, 'other' );
190        }
191        $dispatcher->ack = true;
192        $builder->insertForeign( $acnNs, $name, $attrs, $selfClose, $sourceStart, $sourceLength );
193    }
194
195    public function endTag( $name, $sourceStart, $sourceLength ) {
196        $builder = $this->builder;
197        $stack = $builder->stack;
198        $dispatcher = $this->dispatcher;
199
200        $node = $stack->current;
201        if ( strcasecmp( $node->name, $name ) !== 0 ) {
202            $builder->error( "mismatched end tag in foreign content", $sourceStart );
203        }
204        for ( $idx = $stack->length() - 1; $idx > 0; $idx-- ) {
205            if ( strcasecmp( $node->name, $name ) === 0 ) {
206                $builder->popAllUpToElement( $node, $sourceStart, $sourceLength );
207                break;
208            }
209            $node = $stack->item( $idx - 1 );
210            if ( $node->namespace === HTMLData::NS_HTML ) {
211                $dispatcher->getHandler()->endTag( $name, $sourceStart, $sourceLength );
212                break;
213            }
214        }
215    }
216
217    public function endDocument( $pos ) {
218        // @phan-suppress-previous-line PhanPluginNeverReturnMethod
219        throw new TreeBuilderError( "unspecified, presumed unreachable" );
220    }
221}