Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
97.14% |
68 / 70 |
|
60.00% |
3 / 5 |
CRAP | |
0.00% |
0 / 1 |
InForeignContent | |
97.14% |
68 / 70 |
|
60.00% |
3 / 5 |
25 | |
0.00% |
0 / 1 |
characters | |
100.00% |
27 / 27 |
|
100.00% |
1 / 1 |
5 | |||
isIntegrationPoint | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
startTag | |
96.00% |
24 / 25 |
|
0.00% |
0 / 1 |
11 | |||
endTag | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
5 | |||
endDocument | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace Wikimedia\RemexHtml\TreeBuilder; |
4 | |
5 | use Wikimedia\RemexHtml\HTMLData; |
6 | use 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 | */ |
14 | class 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 | } |