PluginProbe
WindPress – Tailwind CSS integration for WordPress / 3.2.87
WindPress – Tailwind CSS integration for WordPress v3.2.87
3.2.89 3.2.88 3.2.87 3.2.86 3.2.85 3.2.84 3.2.83 3.2.82 3.2.81 trunk 3.0.0 3.0.1 3.0.10 3.0.11 3.0.12 3.0.13 3.0.14 3.0.15 3.0.16 3.0.17 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 All 143 releases
windpress / vendor / masterminds / html5 / src / HTML5 / Parser / DOMTreeBuilder.php

DOMTreeBuilder.php in WindPress – Tailwind CSS integration for WordPress 3.2.87, at vendor/masterminds/html5/src/HTML5/Parser/DOMTreeBuilder.php

569 lines 22.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WindPressDeps\Masterminds\HTML5\Parser;
4
5 use WindPressDeps\Masterminds\HTML5\Elements;
6 use WindPressDeps\Masterminds\HTML5\InstructionProcessor;
7 /**
8 * Create an HTML5 DOM tree from events.
9 *
10 * This attempts to create a DOM from events emitted by a parser. This
11 * attempts (but does not guarantee) to up-convert older HTML documents
12 * to HTML5. It does this by applying HTML5's rules, but it will not
13 * change the architecture of the document itself.
14 *
15 * Many of the error correction and quirks features suggested in the specification
16 * are implemented herein; however, not all of them are. Since we do not
17 * assume a graphical user agent, no presentation-specific logic is conducted
18 * during tree building.
19 *
20 * FIXME: The present tree builder does not exactly follow the state machine rules
21 * for insert modes as outlined in the HTML5 spec. The processor needs to be
22 * re-written to accomodate this. See, for example, the Go language HTML5
23 * parser.
24 */
25 class DOMTreeBuilder implements EventHandler
26 {
27 /**
28 * Defined in http://www.w3.org/TR/html51/infrastructure.html#html-namespace-0.
29 */
30 const NAMESPACE_HTML = 'http://www.w3.org/1999/xhtml';
31 const NAMESPACE_MATHML = 'http://www.w3.org/1998/Math/MathML';
32 const NAMESPACE_SVG = 'http://www.w3.org/2000/svg';
33 const NAMESPACE_XLINK = 'http://www.w3.org/1999/xlink';
34 const NAMESPACE_XML = 'http://www.w3.org/XML/1998/namespace';
35 const NAMESPACE_XMLNS = 'http://www.w3.org/2000/xmlns/';
36 const OPT_DISABLE_HTML_NS = 'disable_html_ns';
37 const OPT_TARGET_DOC = 'target_document';
38 const OPT_IMPLICIT_NS = 'implicit_namespaces';
39 /**
40 * Holds the HTML5 element names that causes a namespace switch.
41 *
42 * @var array
43 */
44 protected $nsRoots = array('html' => self::NAMESPACE_HTML, 'svg' => self::NAMESPACE_SVG, 'math' => self::NAMESPACE_MATHML);
45 /**
46 * Holds the always available namespaces (which does not require the XMLNS declaration).
47 *
48 * @var array
49 */
50 protected $implicitNamespaces = array('xml' => self::NAMESPACE_XML, 'xmlns' => self::NAMESPACE_XMLNS, 'xlink' => self::NAMESPACE_XLINK);
51 /**
52 * Holds a stack of currently active namespaces.
53 *
54 * @var array
55 */
56 protected $nsStack = array();
57 /**
58 * Holds the number of namespaces declared by a node.
59 *
60 * @var array
61 */
62 protected $pushes = array();
63 /**
64 * Defined in 8.2.5.
65 */
66 const IM_INITIAL = 0;
67 const IM_BEFORE_HTML = 1;
68 const IM_BEFORE_HEAD = 2;
69 const IM_IN_HEAD = 3;
70 const IM_IN_HEAD_NOSCRIPT = 4;
71 const IM_AFTER_HEAD = 5;
72 const IM_IN_BODY = 6;
73 const IM_TEXT = 7;
74 const IM_IN_TABLE = 8;
75 const IM_IN_TABLE_TEXT = 9;
76 const IM_IN_CAPTION = 10;
77 const IM_IN_COLUMN_GROUP = 11;
78 const IM_IN_TABLE_BODY = 12;
79 const IM_IN_ROW = 13;
80 const IM_IN_CELL = 14;
81 const IM_IN_SELECT = 15;
82 const IM_IN_SELECT_IN_TABLE = 16;
83 const IM_AFTER_BODY = 17;
84 const IM_IN_FRAMESET = 18;
85 const IM_AFTER_FRAMESET = 19;
86 const IM_AFTER_AFTER_BODY = 20;
87 const IM_AFTER_AFTER_FRAMESET = 21;
88 const IM_IN_SVG = 22;
89 const IM_IN_MATHML = 23;
90 protected $options = array();
91 protected $stack = array();
92 protected $current;
93 // Pointer in the tag hierarchy.
94 protected $rules;
95 protected $doc;
96 protected $frag;
97 protected $processor;
98 protected $insertMode = 0;
99 /**
100 * Track if we are in an element that allows only inline child nodes.
101 *
102 * @var string|null
103 */
104 protected $onlyInline;
105 /**
106 * Quirks mode is enabled by default.
107 * Any document that is missing the DT will be considered to be in quirks mode.
108 */
109 protected $quirks = \true;
110 protected $errors = array();
111 public function __construct($isFragment = \false, array $options = array())
112 {
113 $this->options = $options;
114 if (isset($options[self::OPT_TARGET_DOC])) {
115 $this->doc = $options[self::OPT_TARGET_DOC];
116 } else {
117 $impl = new \DOMImplementation();
118 // XXX:
119 // Create the doctype. For now, we are always creating HTML5
120 // documents, and attempting to up-convert any older DTDs to HTML5.
121 $dt = $impl->createDocumentType('html');
122 // $this->doc = \DOMImplementation::createDocument(NULL, 'html', $dt);
123 $this->doc = $impl->createDocument(null, '', $dt);
124 $this->doc->encoding = !empty($options['encoding']) ? $options['encoding'] : 'UTF-8';
125 }
126 $this->errors = array();
127 $this->current = $this->doc;
128 // ->documentElement;
129 // Create a rules engine for tags.
130 $this->rules = new TreeBuildingRules();
131 $implicitNS = array();
132 if (isset($this->options[self::OPT_IMPLICIT_NS])) {
133 $implicitNS = $this->options[self::OPT_IMPLICIT_NS];
134 } elseif (isset($this->options['implicitNamespaces'])) {
135 $implicitNS = $this->options['implicitNamespaces'];
136 }
137 // Fill $nsStack with the defalut HTML5 namespaces, plus the "implicitNamespaces" array taken form $options
138 array_unshift($this->nsStack, $implicitNS + array('' => self::NAMESPACE_HTML) + $this->implicitNamespaces);
139 if ($isFragment) {
140 $this->insertMode = static::IM_IN_BODY;
141 $this->frag = $this->doc->createDocumentFragment();
142 $this->current = $this->frag;
143 }
144 }
145 /**
146 * Get the document.
147 */
148 public function document()
149 {
150 return $this->doc;
151 }
152 /**
153 * Get the DOM fragment for the body.
154 *
155 * This returns a DOMNodeList because a fragment may have zero or more
156 * DOMNodes at its root.
157 *
158 * @see http://www.w3.org/TR/2012/CR-html5-20121217/syntax.html#concept-frag-parse-context
159 *
160 * @return \DOMDocumentFragment
161 */
162 public function fragment()
163 {
164 return $this->frag;
165 }
166 /**
167 * Provide an instruction processor.
168 *
169 * This is used for handling Processor Instructions as they are
170 * inserted. If omitted, PI's are inserted directly into the DOM tree.
171 */
172 public function setInstructionProcessor(InstructionProcessor $proc)
173 {
174 $this->processor = $proc;
175 }
176 public function doctype($name, $idType = 0, $id = null, $quirks = \false)
177 {
178 // This is used solely for setting quirks mode. Currently we don't
179 // try to preserve the inbound DT. We convert it to HTML5.
180 $this->quirks = $quirks;
181 if ($this->insertMode > static::IM_INITIAL) {
182 $this->parseError('Illegal placement of DOCTYPE tag. Ignoring: ' . $name);
183 return;
184 }
185 $this->insertMode = static::IM_BEFORE_HTML;
186 }
187 /**
188 * Process the start tag.
189 *
190 * @todo - XMLNS namespace handling (we need to parse, even if it's not valid)
191 * - XLink, MathML and SVG namespace handling
192 * - Omission rules: 8.1.2.4 Optional tags
193 *
194 * @param string $name
195 * @param array $attributes
196 * @param bool $selfClosing
197 *
198 * @return int
199 */
200 public function startTag($name, $attributes = array(), $selfClosing = \false)
201 {
202 $lname = $this->normalizeTagName($name);
203 // Make sure we have an html element.
204 if (!$this->doc->documentElement && 'html' !== $name && !$this->frag) {
205 $this->startTag('html');
206 }
207 // Set quirks mode if we're at IM_INITIAL with no doctype.
208 if ($this->insertMode === static::IM_INITIAL) {
209 $this->quirks = \true;
210 $this->parseError('No DOCTYPE specified.');
211 }
212 // SPECIAL TAG HANDLING:
213 // Spec says do this, and "don't ask."
214 // find the spec where this is defined... looks problematic
215 if ('image' === $name && !($this->insertMode === static::IM_IN_SVG || $this->insertMode === static::IM_IN_MATHML)) {
216 $name = 'img';
217 }
218 // Autoclose p tags where appropriate.
219 if ($this->insertMode >= static::IM_IN_BODY && Elements::isA($name, Elements::AUTOCLOSE_P)) {
220 $this->autoclose('p');
221 }
222 // Set insert mode:
223 switch ($name) {
224 case 'html':
225 $this->insertMode = static::IM_BEFORE_HEAD;
226 break;
227 case 'head':
228 if ($this->insertMode > static::IM_BEFORE_HEAD) {
229 $this->parseError('Unexpected head tag outside of head context.');
230 } else {
231 $this->insertMode = static::IM_IN_HEAD;
232 }
233 break;
234 case 'body':
235 $this->insertMode = static::IM_IN_BODY;
236 break;
237 case 'svg':
238 $this->insertMode = static::IM_IN_SVG;
239 break;
240 case 'math':
241 $this->insertMode = static::IM_IN_MATHML;
242 break;
243 case 'noscript':
244 if ($this->insertMode === static::IM_IN_HEAD) {
245 $this->insertMode = static::IM_IN_HEAD_NOSCRIPT;
246 }
247 break;
248 }
249 // Special case handling for SVG.
250 if ($this->insertMode === static::IM_IN_SVG) {
251 $lname = Elements::normalizeSvgElement($lname);
252 }
253 $pushes = 0;
254 // when we found a tag thats appears inside $nsRoots, we have to switch the defalut namespace
255 if (isset($this->nsRoots[$lname]) && $this->nsStack[0][''] !== $this->nsRoots[$lname]) {
256 array_unshift($this->nsStack, array('' => $this->nsRoots[$lname]) + $this->nsStack[0]);
257 ++$pushes;
258 }
259 $needsWorkaround = \false;
260 if (isset($this->options['xmlNamespaces']) && $this->options['xmlNamespaces']) {
261 // when xmlNamespaces is true a and we found a 'xmlns' or 'xmlns:*' attribute, we should add a new item to the $nsStack
262 foreach ($attributes as $aName => $aVal) {
263 if ('xmlns' === $aName) {
264 $needsWorkaround = $aVal;
265 array_unshift($this->nsStack, array('' => $aVal) + $this->nsStack[0]);
266 ++$pushes;
267 } elseif ('xmlns' === (($pos = strpos($aName, ':')) ? substr($aName, 0, $pos) : '')) {
268 array_unshift($this->nsStack, array(substr($aName, $pos + 1) => $aVal) + $this->nsStack[0]);
269 ++$pushes;
270 }
271 }
272 }
273 if ($this->onlyInline && Elements::isA($lname, Elements::BLOCK_TAG)) {
274 $this->autoclose($this->onlyInline);
275 $this->onlyInline = null;
276 }
277 // some elements as table related tags might have optional end tags that force us to auto close multiple tags
278 // https://www.w3.org/TR/html401/struct/tables.html
279 if ($this->current instanceof \DOMElement && isset(Elements::$optionalEndElementsParentsToClose[$lname])) {
280 foreach (Elements::$optionalEndElementsParentsToClose[$lname] as $parentElName) {
281 if ($this->current instanceof \DOMElement && $this->current->tagName === $parentElName) {
282 $this->autoclose($parentElName);
283 }
284 }
285 }
286 try {
287 $prefix = ($pos = strpos($lname, ':')) ? substr($lname, 0, $pos) : '';
288 if (\false !== $needsWorkaround) {
289 $xml = "<{$lname} xmlns=\"{$needsWorkaround}\" " . (strlen($prefix) && isset($this->nsStack[0][$prefix]) ? "xmlns:{$prefix}=\"" . $this->nsStack[0][$prefix] . '"' : '') . '/>';
290 $frag = new \DOMDocument('1.0', 'UTF-8');
291 $frag->loadXML($xml);
292 $ele = $this->doc->importNode($frag->documentElement, \true);
293 } else if (!isset($this->nsStack[0][$prefix]) || '' === $prefix && isset($this->options[self::OPT_DISABLE_HTML_NS]) && $this->options[self::OPT_DISABLE_HTML_NS]) {
294 $ele = $this->doc->createElement($lname);
295 } else {
296 $ele = $this->doc->createElementNS($this->nsStack[0][$prefix], $lname);
297 }
298 } catch (\DOMException $e) {
299 $this->parseError("Illegal tag name: <{$lname}>. Replaced with <invalid>.");
300 $ele = $this->doc->createElement('invalid');
301 }
302 if (Elements::isA($lname, Elements::BLOCK_ONLY_INLINE)) {
303 $this->onlyInline = $lname;
304 }
305 // When we add some namespacess, we have to track them. Later, when "endElement" is invoked, we have to remove them.
306 // When we are on a void tag, we do not need to care about namesapce nesting.
307 if ($pushes > 0 && !Elements::isA($name, Elements::VOID_TAG)) {
308 // PHP tends to free the memory used by DOM,
309 // to avoid spl_object_hash collisions whe have to avoid garbage collection of $ele storing it into $pushes
310 // see https://bugs.php.net/bug.php?id=67459
311 $this->pushes[spl_object_hash($ele)] = array($pushes, $ele);
312 }
313 foreach ($attributes as $aName => $aVal) {
314 // xmlns attributes can't be set
315 if ('xmlns' === $aName) {
316 continue;
317 }
318 if ($this->insertMode === static::IM_IN_SVG) {
319 $aName = Elements::normalizeSvgAttribute($aName);
320 } elseif ($this->insertMode === static::IM_IN_MATHML) {
321 $aName = Elements::normalizeMathMlAttribute($aName);
322 }
323 $aVal = (string) $aVal;
324 try {
325 $prefix = ($pos = strpos($aName, ':')) ? substr($aName, 0, $pos) : \false;
326 if ('xmlns' === $prefix) {
327 $ele->setAttributeNS(self::NAMESPACE_XMLNS, $aName, $aVal);
328 } elseif (\false !== $prefix && isset($this->nsStack[0][$prefix])) {
329 $ele->setAttributeNS($this->nsStack[0][$prefix], $aName, $aVal);
330 } else {
331 $ele->setAttribute($aName, $aVal);
332 }
333 } catch (\DOMException $e) {
334 $this->parseError("Illegal attribute name for tag {$name}. Ignoring: {$aName}");
335 continue;
336 }
337 // This is necessary on a non-DTD schema, like HTML5.
338 if ('id' === $aName) {
339 $ele->setIdAttribute('id', \true);
340 }
341 }
342 if ($this->frag !== $this->current && $this->rules->hasRules($name)) {
343 // Some elements have special processing rules. Handle those separately.
344 $this->current = $this->rules->evaluate($ele, $this->current);
345 } else {
346 // Otherwise, it's a standard element.
347 $this->current->appendChild($ele);
348 if (!Elements::isA($name, Elements::VOID_TAG)) {
349 $this->current = $ele;
350 }
351 // Self-closing tags should only be respected on foreign elements
352 // (and are implied on void elements)
353 // See: https://www.w3.org/TR/html5/syntax.html#start-tags
354 if (Elements::isHtml5Element($name)) {
355 $selfClosing = \false;
356 }
357 }
358 // This is sort of a last-ditch attempt to correct for cases where no head/body
359 // elements are provided.
360 if ($this->insertMode <= static::IM_BEFORE_HEAD && 'head' !== $name && 'html' !== $name) {
361 $this->insertMode = static::IM_IN_BODY;
362 }
363 // When we are on a void tag, we do not need to care about namesapce nesting,
364 // but we have to remove the namespaces pushed to $nsStack.
365 if ($pushes > 0 && Elements::isA($name, Elements::VOID_TAG)) {
366 // remove the namespaced definded by current node
367 for ($i = 0; $i < $pushes; ++$i) {
368 array_shift($this->nsStack);
369 }
370 }
371 if ($selfClosing) {
372 $this->endTag($name);
373 }
374 // Return the element mask, which the tokenizer can then use to set
375 // various processing rules.
376 return Elements::element($name);
377 }
378 public function endTag($name)
379 {
380 $lname = $this->normalizeTagName($name);
381 // Special case within 12.2.6.4.7: An end tag whose tag name is "br" should be treated as an opening tag
382 if ('br' === $name) {
383 $this->parseError('Closing tag encountered for void element br.');
384 $this->startTag('br');
385 } elseif (Elements::isA($name, Elements::VOID_TAG)) {
386 return;
387 }
388 if ($this->insertMode <= static::IM_BEFORE_HTML) {
389 // 8.2.5.4.2
390 if (in_array($name, array('html', 'br', 'head', 'title'))) {
391 $this->startTag('html');
392 $this->endTag($name);
393 $this->insertMode = static::IM_BEFORE_HEAD;
394 return;
395 }
396 // Ignore the tag.
397 $this->parseError('Illegal closing tag at global scope.');
398 return;
399 }
400 // Special case handling for SVG.
401 if ($this->insertMode === static::IM_IN_SVG) {
402 $lname = Elements::normalizeSvgElement($lname);
403 }
404 $cid = spl_object_hash($this->current);
405 // XXX: HTML has no parent. What do we do, though,
406 // if this element appears in the wrong place?
407 if ('html' === $lname) {
408 return;
409 }
410 // remove the namespaced definded by current node
411 if (isset($this->pushes[$cid])) {
412 for ($i = 0; $i < $this->pushes[$cid][0]; ++$i) {
413 array_shift($this->nsStack);
414 }
415 unset($this->pushes[$cid]);
416 }
417 if (!$this->autoclose($lname)) {
418 $this->parseError('Could not find closing tag for ' . $lname);
419 }
420 switch ($lname) {
421 case 'head':
422 $this->insertMode = static::IM_AFTER_HEAD;
423 break;
424 case 'body':
425 $this->insertMode = static::IM_AFTER_BODY;
426 break;
427 case 'svg':
428 case 'mathml':
429 $this->insertMode = static::IM_IN_BODY;
430 break;
431 }
432 }
433 public function comment($cdata)
434 {
435 // TODO: Need to handle case where comment appears outside of the HTML tag.
436 $node = $this->doc->createComment($cdata);
437 $this->current->appendChild($node);
438 }
439 public function text($data)
440 {
441 // XXX: Hmmm.... should we really be this strict?
442 if ($this->insertMode < static::IM_IN_HEAD) {
443 // Per '8.2.5.4.3 The "before head" insertion mode' the characters
444 // " \t\n\r\f" should be ignored but no mention of a parse error. This is
445 // practical as most documents contain these characters. Other text is not
446 // expected here so recording a parse error is necessary.
447 $dataTmp = trim($data, " \t\n\r\f");
448 if (!empty($dataTmp)) {
449 // fprintf(STDOUT, "Unexpected insert mode: %d", $this->insertMode);
450 $this->parseError('Unexpected text. Ignoring: ' . $dataTmp);
451 }
452 return;
453 }
454 // fprintf(STDOUT, "Appending text %s.", $data);
455 $node = $this->doc->createTextNode($data);
456 $this->current->appendChild($node);
457 }
458 public function eof()
459 {
460 // If the $current isn't the $root, do we need to do anything?
461 }
462 public function parseError($msg, $line = 0, $col = 0)
463 {
464 $this->errors[] = sprintf('Line %d, Col %d: %s', $line, $col, $msg);
465 }
466 public function getErrors()
467 {
468 return $this->errors;
469 }
470 public function cdata($data)
471 {
472 $node = $this->doc->createCDATASection($data);
473 $this->current->appendChild($node);
474 }
475 public function processingInstruction($name, $data = null)
476 {
477 // XXX: Ignore initial XML declaration, per the spec.
478 if ($this->insertMode === static::IM_INITIAL && 'xml' === strtolower($name)) {
479 return;
480 }
481 // Important: The processor may modify the current DOM tree however it sees fit.
482 if ($this->processor instanceof InstructionProcessor) {
483 $res = $this->processor->process($this->current, $name, $data);
484 if (!empty($res)) {
485 $this->current = $res;
486 }
487 return;
488 }
489 // Otherwise, this is just a dumb PI element.
490 $node = $this->doc->createProcessingInstruction($name, $data);
491 $this->current->appendChild($node);
492 }
493 // ==========================================================================
494 // UTILITIES
495 // ==========================================================================
496 /**
497 * Apply normalization rules to a tag name.
498 * See sections 2.9 and 8.1.2.
499 *
500 * @param string $tagName
501 *
502 * @return string The normalized tag name.
503 */
504 protected function normalizeTagName($tagName)
505 {
506 /*
507 * Section 2.9 suggests that we should not do this. if (strpos($name, ':') !== false) { // We know from the grammar that there must be at least one other // char besides :, since : is not a legal tag start. $parts = explode(':', $name); return array_pop($parts); }
508 */
509 return $tagName;
510 }
511 protected function quirksTreeResolver($name)
512 {
513 throw new \Exception('Not implemented.');
514 }
515 /**
516 * Automatically climb the tree and close the closest node with the matching $tag.
517 *
518 * @param string $tagName
519 *
520 * @return bool
521 */
522 protected function autoclose($tagName)
523 {
524 $working = $this->current;
525 do {
526 if (\XML_ELEMENT_NODE !== $working->nodeType) {
527 return \false;
528 }
529 if ($working->tagName === $tagName) {
530 $this->current = $working->parentNode;
531 return \true;
532 }
533 } while ($working = $working->parentNode);
534 return \false;
535 }
536 /**
537 * Checks if the given tagname is an ancestor of the present candidate.
538 *
539 * If $this->current or anything above $this->current matches the given tag
540 * name, this returns true.
541 *
542 * @param string $tagName
543 *
544 * @return bool
545 */
546 protected function isAncestor($tagName)
547 {
548 $candidate = $this->current;
549 while (\XML_ELEMENT_NODE === $candidate->nodeType) {
550 if ($candidate->tagName === $tagName) {
551 return \true;
552 }
553 $candidate = $candidate->parentNode;
554 }
555 return \false;
556 }
557 /**
558 * Returns true if the immediate parent element is of the given tagname.
559 *
560 * @param string $tagName
561 *
562 * @return bool
563 */
564 protected function isParent($tagName)
565 {
566 return $this->current->tagName === $tagName;
567 }
568 }
569