PluginProbe
WindPress – Tailwind CSS integration for WordPress / 3.0.5
WindPress – Tailwind CSS integration for WordPress v3.0.5
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.0.5, at vendor/masterminds/html5/src/HTML5/Parser/DOMTreeBuilder.php

573 lines 22.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WindPressPackages\Masterminds\HTML5\Parser;
4
5 use WindPressPackages\Masterminds\HTML5\Elements;
6 use WindPressPackages\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 * @param InstructionProcessor $proc
173 */
174 public function setInstructionProcessor(InstructionProcessor $proc)
175 {
176 $this->processor = $proc;
177 }
178 public function doctype($name, $idType = 0, $id = null, $quirks = \false)
179 {
180 // This is used solely for setting quirks mode. Currently we don't
181 // try to preserve the inbound DT. We convert it to HTML5.
182 $this->quirks = $quirks;
183 if ($this->insertMode > static::IM_INITIAL) {
184 $this->parseError('Illegal placement of DOCTYPE tag. Ignoring: ' . $name);
185 return;
186 }
187 $this->insertMode = static::IM_BEFORE_HTML;
188 }
189 /**
190 * Process the start tag.
191 *
192 * @todo - XMLNS namespace handling (we need to parse, even if it's not valid)
193 * - XLink, MathML and SVG namespace handling
194 * - Omission rules: 8.1.2.4 Optional tags
195 *
196 * @param string $name
197 * @param array $attributes
198 * @param bool $selfClosing
199 *
200 * @return int
201 */
202 public function startTag($name, $attributes = array(), $selfClosing = \false)
203 {
204 $lname = $this->normalizeTagName($name);
205 // Make sure we have an html element.
206 if (!$this->doc->documentElement && 'html' !== $name && !$this->frag) {
207 $this->startTag('html');
208 }
209 // Set quirks mode if we're at IM_INITIAL with no doctype.
210 if ($this->insertMode === static::IM_INITIAL) {
211 $this->quirks = \true;
212 $this->parseError('No DOCTYPE specified.');
213 }
214 // SPECIAL TAG HANDLING:
215 // Spec says do this, and "don't ask."
216 // find the spec where this is defined... looks problematic
217 if ('image' === $name && !($this->insertMode === static::IM_IN_SVG || $this->insertMode === static::IM_IN_MATHML)) {
218 $name = 'img';
219 }
220 // Autoclose p tags where appropriate.
221 if ($this->insertMode >= static::IM_IN_BODY && Elements::isA($name, Elements::AUTOCLOSE_P)) {
222 $this->autoclose('p');
223 }
224 // Set insert mode:
225 switch ($name) {
226 case 'html':
227 $this->insertMode = static::IM_BEFORE_HEAD;
228 break;
229 case 'head':
230 if ($this->insertMode > static::IM_BEFORE_HEAD) {
231 $this->parseError('Unexpected head tag outside of head context.');
232 } else {
233 $this->insertMode = static::IM_IN_HEAD;
234 }
235 break;
236 case 'body':
237 $this->insertMode = static::IM_IN_BODY;
238 break;
239 case 'svg':
240 $this->insertMode = static::IM_IN_SVG;
241 break;
242 case 'math':
243 $this->insertMode = static::IM_IN_MATHML;
244 break;
245 case 'noscript':
246 if ($this->insertMode === static::IM_IN_HEAD) {
247 $this->insertMode = static::IM_IN_HEAD_NOSCRIPT;
248 }
249 break;
250 }
251 // Special case handling for SVG.
252 if ($this->insertMode === static::IM_IN_SVG) {
253 $lname = Elements::normalizeSvgElement($lname);
254 }
255 $pushes = 0;
256 // when we found a tag thats appears inside $nsRoots, we have to switch the defalut namespace
257 if (isset($this->nsRoots[$lname]) && $this->nsStack[0][''] !== $this->nsRoots[$lname]) {
258 \array_unshift($this->nsStack, array('' => $this->nsRoots[$lname]) + $this->nsStack[0]);
259 ++$pushes;
260 }
261 $needsWorkaround = \false;
262 if (isset($this->options['xmlNamespaces']) && $this->options['xmlNamespaces']) {
263 // when xmlNamespaces is true a and we found a 'xmlns' or 'xmlns:*' attribute, we should add a new item to the $nsStack
264 foreach ($attributes as $aName => $aVal) {
265 if ('xmlns' === $aName) {
266 $needsWorkaround = $aVal;
267 \array_unshift($this->nsStack, array('' => $aVal) + $this->nsStack[0]);
268 ++$pushes;
269 } elseif ('xmlns' === (($pos = \strpos($aName, ':')) ? \substr($aName, 0, $pos) : '')) {
270 \array_unshift($this->nsStack, array(\substr($aName, $pos + 1) => $aVal) + $this->nsStack[0]);
271 ++$pushes;
272 }
273 }
274 }
275 if ($this->onlyInline && Elements::isA($lname, Elements::BLOCK_TAG)) {
276 $this->autoclose($this->onlyInline);
277 $this->onlyInline = null;
278 }
279 // some elements as table related tags might have optional end tags that force us to auto close multiple tags
280 // https://www.w3.org/TR/html401/struct/tables.html
281 if ($this->current instanceof \DOMElement && isset(Elements::$optionalEndElementsParentsToClose[$lname])) {
282 foreach (Elements::$optionalEndElementsParentsToClose[$lname] as $parentElName) {
283 if ($this->current instanceof \DOMElement && $this->current->tagName === $parentElName) {
284 $this->autoclose($parentElName);
285 }
286 }
287 }
288 try {
289 $prefix = ($pos = \strpos($lname, ':')) ? \substr($lname, 0, $pos) : '';
290 if (\false !== $needsWorkaround) {
291 $xml = "<{$lname} xmlns=\"{$needsWorkaround}\" " . (\strlen($prefix) && isset($this->nsStack[0][$prefix]) ? "xmlns:{$prefix}=\"" . $this->nsStack[0][$prefix] . '"' : '') . '/>';
292 $frag = new \DOMDocument('1.0', 'UTF-8');
293 $frag->loadXML($xml);
294 $ele = $this->doc->importNode($frag->documentElement, \true);
295 } else {
296 if (!isset($this->nsStack[0][$prefix]) || '' === $prefix && isset($this->options[self::OPT_DISABLE_HTML_NS]) && $this->options[self::OPT_DISABLE_HTML_NS]) {
297 $ele = $this->doc->createElement($lname);
298 } else {
299 $ele = $this->doc->createElementNS($this->nsStack[0][$prefix], $lname);
300 }
301 }
302 } catch (\DOMException $e) {
303 $this->parseError("Illegal tag name: <{$lname}>. Replaced with <invalid>.");
304 $ele = $this->doc->createElement('invalid');
305 }
306 if (Elements::isA($lname, Elements::BLOCK_ONLY_INLINE)) {
307 $this->onlyInline = $lname;
308 }
309 // When we add some namespacess, we have to track them. Later, when "endElement" is invoked, we have to remove them.
310 // When we are on a void tag, we do not need to care about namesapce nesting.
311 if ($pushes > 0 && !Elements::isA($name, Elements::VOID_TAG)) {
312 // PHP tends to free the memory used by DOM,
313 // to avoid spl_object_hash collisions whe have to avoid garbage collection of $ele storing it into $pushes
314 // see https://bugs.php.net/bug.php?id=67459
315 $this->pushes[\spl_object_hash($ele)] = array($pushes, $ele);
316 }
317 foreach ($attributes as $aName => $aVal) {
318 // xmlns attributes can't be set
319 if ('xmlns' === $aName) {
320 continue;
321 }
322 if ($this->insertMode === static::IM_IN_SVG) {
323 $aName = Elements::normalizeSvgAttribute($aName);
324 } elseif ($this->insertMode === static::IM_IN_MATHML) {
325 $aName = Elements::normalizeMathMlAttribute($aName);
326 }
327 $aVal = (string) $aVal;
328 try {
329 $prefix = ($pos = \strpos($aName, ':')) ? \substr($aName, 0, $pos) : \false;
330 if ('xmlns' === $prefix) {
331 $ele->setAttributeNS(self::NAMESPACE_XMLNS, $aName, $aVal);
332 } elseif (\false !== $prefix && isset($this->nsStack[0][$prefix])) {
333 $ele->setAttributeNS($this->nsStack[0][$prefix], $aName, $aVal);
334 } else {
335 $ele->setAttribute($aName, $aVal);
336 }
337 } catch (\DOMException $e) {
338 $this->parseError("Illegal attribute name for tag {$name}. Ignoring: {$aName}");
339 continue;
340 }
341 // This is necessary on a non-DTD schema, like HTML5.
342 if ('id' === $aName) {
343 $ele->setIdAttribute('id', \true);
344 }
345 }
346 if ($this->frag !== $this->current && $this->rules->hasRules($name)) {
347 // Some elements have special processing rules. Handle those separately.
348 $this->current = $this->rules->evaluate($ele, $this->current);
349 } else {
350 // Otherwise, it's a standard element.
351 $this->current->appendChild($ele);
352 if (!Elements::isA($name, Elements::VOID_TAG)) {
353 $this->current = $ele;
354 }
355 // Self-closing tags should only be respected on foreign elements
356 // (and are implied on void elements)
357 // See: https://www.w3.org/TR/html5/syntax.html#start-tags
358 if (Elements::isHtml5Element($name)) {
359 $selfClosing = \false;
360 }
361 }
362 // This is sort of a last-ditch attempt to correct for cases where no head/body
363 // elements are provided.
364 if ($this->insertMode <= static::IM_BEFORE_HEAD && 'head' !== $name && 'html' !== $name) {
365 $this->insertMode = static::IM_IN_BODY;
366 }
367 // When we are on a void tag, we do not need to care about namesapce nesting,
368 // but we have to remove the namespaces pushed to $nsStack.
369 if ($pushes > 0 && Elements::isA($name, Elements::VOID_TAG)) {
370 // remove the namespaced definded by current node
371 for ($i = 0; $i < $pushes; ++$i) {
372 \array_shift($this->nsStack);
373 }
374 }
375 if ($selfClosing) {
376 $this->endTag($name);
377 }
378 // Return the element mask, which the tokenizer can then use to set
379 // various processing rules.
380 return Elements::element($name);
381 }
382 public function endTag($name)
383 {
384 $lname = $this->normalizeTagName($name);
385 // Special case within 12.2.6.4.7: An end tag whose tag name is "br" should be treated as an opening tag
386 if ('br' === $name) {
387 $this->parseError('Closing tag encountered for void element br.');
388 $this->startTag('br');
389 } elseif (Elements::isA($name, Elements::VOID_TAG)) {
390 return;
391 }
392 if ($this->insertMode <= static::IM_BEFORE_HTML) {
393 // 8.2.5.4.2
394 if (\in_array($name, array('html', 'br', 'head', 'title'))) {
395 $this->startTag('html');
396 $this->endTag($name);
397 $this->insertMode = static::IM_BEFORE_HEAD;
398 return;
399 }
400 // Ignore the tag.
401 $this->parseError('Illegal closing tag at global scope.');
402 return;
403 }
404 // Special case handling for SVG.
405 if ($this->insertMode === static::IM_IN_SVG) {
406 $lname = Elements::normalizeSvgElement($lname);
407 }
408 $cid = \spl_object_hash($this->current);
409 // XXX: HTML has no parent. What do we do, though,
410 // if this element appears in the wrong place?
411 if ('html' === $lname) {
412 return;
413 }
414 // remove the namespaced definded by current node
415 if (isset($this->pushes[$cid])) {
416 for ($i = 0; $i < $this->pushes[$cid][0]; ++$i) {
417 \array_shift($this->nsStack);
418 }
419 unset($this->pushes[$cid]);
420 }
421 if (!$this->autoclose($lname)) {
422 $this->parseError('Could not find closing tag for ' . $lname);
423 }
424 switch ($lname) {
425 case 'head':
426 $this->insertMode = static::IM_AFTER_HEAD;
427 break;
428 case 'body':
429 $this->insertMode = static::IM_AFTER_BODY;
430 break;
431 case 'svg':
432 case 'mathml':
433 $this->insertMode = static::IM_IN_BODY;
434 break;
435 }
436 }
437 public function comment($cdata)
438 {
439 // TODO: Need to handle case where comment appears outside of the HTML tag.
440 $node = $this->doc->createComment($cdata);
441 $this->current->appendChild($node);
442 }
443 public function text($data)
444 {
445 // XXX: Hmmm.... should we really be this strict?
446 if ($this->insertMode < static::IM_IN_HEAD) {
447 // Per '8.2.5.4.3 The "before head" insertion mode' the characters
448 // " \t\n\r\f" should be ignored but no mention of a parse error. This is
449 // practical as most documents contain these characters. Other text is not
450 // expected here so recording a parse error is necessary.
451 $dataTmp = \trim($data, " \t\n\r\f");
452 if (!empty($dataTmp)) {
453 // fprintf(STDOUT, "Unexpected insert mode: %d", $this->insertMode);
454 $this->parseError('Unexpected text. Ignoring: ' . $dataTmp);
455 }
456 return;
457 }
458 // fprintf(STDOUT, "Appending text %s.", $data);
459 $node = $this->doc->createTextNode($data);
460 $this->current->appendChild($node);
461 }
462 public function eof()
463 {
464 // If the $current isn't the $root, do we need to do anything?
465 }
466 public function parseError($msg, $line = 0, $col = 0)
467 {
468 $this->errors[] = \sprintf('Line %d, Col %d: %s', $line, $col, $msg);
469 }
470 public function getErrors()
471 {
472 return $this->errors;
473 }
474 public function cdata($data)
475 {
476 $node = $this->doc->createCDATASection($data);
477 $this->current->appendChild($node);
478 }
479 public function processingInstruction($name, $data = null)
480 {
481 // XXX: Ignore initial XML declaration, per the spec.
482 if ($this->insertMode === static::IM_INITIAL && 'xml' === \strtolower($name)) {
483 return;
484 }
485 // Important: The processor may modify the current DOM tree however it sees fit.
486 if ($this->processor instanceof InstructionProcessor) {
487 $res = $this->processor->process($this->current, $name, $data);
488 if (!empty($res)) {
489 $this->current = $res;
490 }
491 return;
492 }
493 // Otherwise, this is just a dumb PI element.
494 $node = $this->doc->createProcessingInstruction($name, $data);
495 $this->current->appendChild($node);
496 }
497 // ==========================================================================
498 // UTILITIES
499 // ==========================================================================
500 /**
501 * Apply normalization rules to a tag name.
502 * See sections 2.9 and 8.1.2.
503 *
504 * @param string $tagName
505 *
506 * @return string The normalized tag name.
507 */
508 protected function normalizeTagName($tagName)
509 {
510 /*
511 * 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); }
512 */
513 return $tagName;
514 }
515 protected function quirksTreeResolver($name)
516 {
517 throw new \Exception('Not implemented.');
518 }
519 /**
520 * Automatically climb the tree and close the closest node with the matching $tag.
521 *
522 * @param string $tagName
523 *
524 * @return bool
525 */
526 protected function autoclose($tagName)
527 {
528 $working = $this->current;
529 do {
530 if (\XML_ELEMENT_NODE !== $working->nodeType) {
531 return \false;
532 }
533 if ($working->tagName === $tagName) {
534 $this->current = $working->parentNode;
535 return \true;
536 }
537 } while ($working = $working->parentNode);
538 return \false;
539 }
540 /**
541 * Checks if the given tagname is an ancestor of the present candidate.
542 *
543 * If $this->current or anything above $this->current matches the given tag
544 * name, this returns true.
545 *
546 * @param string $tagName
547 *
548 * @return bool
549 */
550 protected function isAncestor($tagName)
551 {
552 $candidate = $this->current;
553 while (\XML_ELEMENT_NODE === $candidate->nodeType) {
554 if ($candidate->tagName === $tagName) {
555 return \true;
556 }
557 $candidate = $candidate->parentNode;
558 }
559 return \false;
560 }
561 /**
562 * Returns true if the immediate parent element is of the given tagname.
563 *
564 * @param string $tagName
565 *
566 * @return bool
567 */
568 protected function isParent($tagName)
569 {
570 return $this->current->tagName === $tagName;
571 }
572 }
573