PluginProbe ʕ •ᴥ•ʔ
Independent Analytics – WordPress Analytics Plugin / 1.17
Independent Analytics – WordPress Analytics Plugin v1.17
2.15.0 2.14.10 trunk 1.1 1.10 1.10.1 1.11 1.12 1.13 1.14 1.15 1.16 1.17 1.17.1 1.17.2 1.17.3 1.17.4 1.18 1.18.1 1.19.0 1.19.1 1.2 1.20.0 1.21.0 1.22.0 1.22.1 1.23.0 1.23.1 1.24.0 1.24.1 1.25.0 1.25.1 1.26.0 1.27.0 1.28.0 1.28.1 1.28.2 1.28.3 1.29.0 1.3 1.30.0 1.30.1 1.4 1.5 1.6 1.7 1.8 1.9 2.0.0 2.0.1 2.1.4 2.1.5 2.1.6 2.10.0 2.10.1 2.10.2 2.10.3 2.10.4 2.11.0 2.11.1 2.11.10 2.11.2 2.11.3 2.11.4 2.11.5 2.11.6 2.11.7 2.11.8 2.11.9 2.12.0 2.12.1 2.12.2 2.13.1 2.13.2 2.13.5 2.13.6 2.14.0 2.14.1 2.14.2 2.14.4 2.14.6 2.14.7 2.14.8 2.14.9 2.2.0 2.2.1 2.3.1 2.3.2 2.4.2 2.4.3 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.7.0 2.7.1 2.7.2 2.7.3 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7
independent-analytics / vendor / masterminds / html5 / src / HTML5 / Parser / DOMTreeBuilder.php
independent-analytics / vendor / masterminds / html5 / src / HTML5 / Parser Last commit date
CharacterReference.php 3 years ago DOMTreeBuilder.php 3 years ago EventHandler.php 3 years ago FileInputStream.php 3 years ago InputStream.php 3 years ago ParseError.php 3 years ago Scanner.php 3 years ago StringInputStream.php 3 years ago Tokenizer.php 3 years ago TreeBuildingRules.php 3 years ago UTF8Utils.php 3 years ago
DOMTreeBuilder.php
564 lines
1 <?php
2
3 namespace IAWP\Masterminds\HTML5\Parser;
4
5 use IAWP\Masterminds\HTML5\Elements;
6 use IAWP\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 try {
280 $prefix = ($pos = \strpos($lname, ':')) ? \substr($lname, 0, $pos) : '';
281 if (\false !== $needsWorkaround) {
282 $xml = "<{$lname} xmlns=\"{$needsWorkaround}\" " . (\strlen($prefix) && isset($this->nsStack[0][$prefix]) ? "xmlns:{$prefix}=\"" . $this->nsStack[0][$prefix] . '"' : '') . '/>';
283 $frag = new \DOMDocument('1.0', 'UTF-8');
284 $frag->loadXML($xml);
285 $ele = $this->doc->importNode($frag->documentElement, \true);
286 } else {
287 if (!isset($this->nsStack[0][$prefix]) || '' === $prefix && isset($this->options[self::OPT_DISABLE_HTML_NS]) && $this->options[self::OPT_DISABLE_HTML_NS]) {
288 $ele = $this->doc->createElement($lname);
289 } else {
290 $ele = $this->doc->createElementNS($this->nsStack[0][$prefix], $lname);
291 }
292 }
293 } catch (\DOMException $e) {
294 $this->parseError("Illegal tag name: <{$lname}>. Replaced with <invalid>.");
295 $ele = $this->doc->createElement('invalid');
296 }
297 if (Elements::isA($lname, Elements::BLOCK_ONLY_INLINE)) {
298 $this->onlyInline = $lname;
299 }
300 // When we add some namespacess, we have to track them. Later, when "endElement" is invoked, we have to remove them.
301 // When we are on a void tag, we do not need to care about namesapce nesting.
302 if ($pushes > 0 && !Elements::isA($name, Elements::VOID_TAG)) {
303 // PHP tends to free the memory used by DOM,
304 // to avoid spl_object_hash collisions whe have to avoid garbage collection of $ele storing it into $pushes
305 // see https://bugs.php.net/bug.php?id=67459
306 $this->pushes[\spl_object_hash($ele)] = array($pushes, $ele);
307 }
308 foreach ($attributes as $aName => $aVal) {
309 // xmlns attributes can't be set
310 if ('xmlns' === $aName) {
311 continue;
312 }
313 if ($this->insertMode === static::IM_IN_SVG) {
314 $aName = Elements::normalizeSvgAttribute($aName);
315 } elseif ($this->insertMode === static::IM_IN_MATHML) {
316 $aName = Elements::normalizeMathMlAttribute($aName);
317 }
318 $aVal = (string) $aVal;
319 try {
320 $prefix = ($pos = \strpos($aName, ':')) ? \substr($aName, 0, $pos) : \false;
321 if ('xmlns' === $prefix) {
322 $ele->setAttributeNS(self::NAMESPACE_XMLNS, $aName, $aVal);
323 } elseif (\false !== $prefix && isset($this->nsStack[0][$prefix])) {
324 $ele->setAttributeNS($this->nsStack[0][$prefix], $aName, $aVal);
325 } else {
326 $ele->setAttribute($aName, $aVal);
327 }
328 } catch (\DOMException $e) {
329 $this->parseError("Illegal attribute name for tag {$name}. Ignoring: {$aName}");
330 continue;
331 }
332 // This is necessary on a non-DTD schema, like HTML5.
333 if ('id' === $aName) {
334 $ele->setIdAttribute('id', \true);
335 }
336 }
337 if ($this->frag !== $this->current && $this->rules->hasRules($name)) {
338 // Some elements have special processing rules. Handle those separately.
339 $this->current = $this->rules->evaluate($ele, $this->current);
340 } else {
341 // Otherwise, it's a standard element.
342 $this->current->appendChild($ele);
343 if (!Elements::isA($name, Elements::VOID_TAG)) {
344 $this->current = $ele;
345 }
346 // Self-closing tags should only be respected on foreign elements
347 // (and are implied on void elements)
348 // See: https://www.w3.org/TR/html5/syntax.html#start-tags
349 if (Elements::isHtml5Element($name)) {
350 $selfClosing = \false;
351 }
352 }
353 // This is sort of a last-ditch attempt to correct for cases where no head/body
354 // elements are provided.
355 if ($this->insertMode <= static::IM_BEFORE_HEAD && 'head' !== $name && 'html' !== $name) {
356 $this->insertMode = static::IM_IN_BODY;
357 }
358 // When we are on a void tag, we do not need to care about namesapce nesting,
359 // but we have to remove the namespaces pushed to $nsStack.
360 if ($pushes > 0 && Elements::isA($name, Elements::VOID_TAG)) {
361 // remove the namespaced definded by current node
362 for ($i = 0; $i < $pushes; ++$i) {
363 \array_shift($this->nsStack);
364 }
365 }
366 if ($selfClosing) {
367 $this->endTag($name);
368 }
369 // Return the element mask, which the tokenizer can then use to set
370 // various processing rules.
371 return Elements::element($name);
372 }
373 public function endTag($name)
374 {
375 $lname = $this->normalizeTagName($name);
376 // Special case within 12.2.6.4.7: An end tag whose tag name is "br" should be treated as an opening tag
377 if ('br' === $name) {
378 $this->parseError('Closing tag encountered for void element br.');
379 $this->startTag('br');
380 } elseif (Elements::isA($name, Elements::VOID_TAG)) {
381 return;
382 }
383 if ($this->insertMode <= static::IM_BEFORE_HTML) {
384 // 8.2.5.4.2
385 if (\in_array($name, array('html', 'br', 'head', 'title'))) {
386 $this->startTag('html');
387 $this->endTag($name);
388 $this->insertMode = static::IM_BEFORE_HEAD;
389 return;
390 }
391 // Ignore the tag.
392 $this->parseError('Illegal closing tag at global scope.');
393 return;
394 }
395 // Special case handling for SVG.
396 if ($this->insertMode === static::IM_IN_SVG) {
397 $lname = Elements::normalizeSvgElement($lname);
398 }
399 $cid = \spl_object_hash($this->current);
400 // XXX: HTML has no parent. What do we do, though,
401 // if this element appears in the wrong place?
402 if ('html' === $lname) {
403 return;
404 }
405 // remove the namespaced definded by current node
406 if (isset($this->pushes[$cid])) {
407 for ($i = 0; $i < $this->pushes[$cid][0]; ++$i) {
408 \array_shift($this->nsStack);
409 }
410 unset($this->pushes[$cid]);
411 }
412 if (!$this->autoclose($lname)) {
413 $this->parseError('Could not find closing tag for ' . $lname);
414 }
415 switch ($lname) {
416 case 'head':
417 $this->insertMode = static::IM_AFTER_HEAD;
418 break;
419 case 'body':
420 $this->insertMode = static::IM_AFTER_BODY;
421 break;
422 case 'svg':
423 case 'mathml':
424 $this->insertMode = static::IM_IN_BODY;
425 break;
426 }
427 }
428 public function comment($cdata)
429 {
430 // TODO: Need to handle case where comment appears outside of the HTML tag.
431 $node = $this->doc->createComment($cdata);
432 $this->current->appendChild($node);
433 }
434 public function text($data)
435 {
436 // XXX: Hmmm.... should we really be this strict?
437 if ($this->insertMode < static::IM_IN_HEAD) {
438 // Per '8.2.5.4.3 The "before head" insertion mode' the characters
439 // " \t\n\r\f" should be ignored but no mention of a parse error. This is
440 // practical as most documents contain these characters. Other text is not
441 // expected here so recording a parse error is necessary.
442 $dataTmp = \trim($data, " \t\n\r\f");
443 if (!empty($dataTmp)) {
444 // fprintf(STDOUT, "Unexpected insert mode: %d", $this->insertMode);
445 $this->parseError('Unexpected text. Ignoring: ' . $dataTmp);
446 }
447 return;
448 }
449 // fprintf(STDOUT, "Appending text %s.", $data);
450 $node = $this->doc->createTextNode($data);
451 $this->current->appendChild($node);
452 }
453 public function eof()
454 {
455 // If the $current isn't the $root, do we need to do anything?
456 }
457 public function parseError($msg, $line = 0, $col = 0)
458 {
459 $this->errors[] = \sprintf('Line %d, Col %d: %s', $line, $col, $msg);
460 }
461 public function getErrors()
462 {
463 return $this->errors;
464 }
465 public function cdata($data)
466 {
467 $node = $this->doc->createCDATASection($data);
468 $this->current->appendChild($node);
469 }
470 public function processingInstruction($name, $data = null)
471 {
472 // XXX: Ignore initial XML declaration, per the spec.
473 if ($this->insertMode === static::IM_INITIAL && 'xml' === \strtolower($name)) {
474 return;
475 }
476 // Important: The processor may modify the current DOM tree however it sees fit.
477 if ($this->processor instanceof InstructionProcessor) {
478 $res = $this->processor->process($this->current, $name, $data);
479 if (!empty($res)) {
480 $this->current = $res;
481 }
482 return;
483 }
484 // Otherwise, this is just a dumb PI element.
485 $node = $this->doc->createProcessingInstruction($name, $data);
486 $this->current->appendChild($node);
487 }
488 // ==========================================================================
489 // UTILITIES
490 // ==========================================================================
491 /**
492 * Apply normalization rules to a tag name.
493 * See sections 2.9 and 8.1.2.
494 *
495 * @param string $tagName
496 *
497 * @return string The normalized tag name.
498 */
499 protected function normalizeTagName($tagName)
500 {
501 /*
502 * 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); }
503 */
504 return $tagName;
505 }
506 protected function quirksTreeResolver($name)
507 {
508 throw new \Exception('Not implemented.');
509 }
510 /**
511 * Automatically climb the tree and close the closest node with the matching $tag.
512 *
513 * @param string $tagName
514 *
515 * @return bool
516 */
517 protected function autoclose($tagName)
518 {
519 $working = $this->current;
520 do {
521 if (\XML_ELEMENT_NODE !== $working->nodeType) {
522 return \false;
523 }
524 if ($working->tagName === $tagName) {
525 $this->current = $working->parentNode;
526 return \true;
527 }
528 } while ($working = $working->parentNode);
529 return \false;
530 }
531 /**
532 * Checks if the given tagname is an ancestor of the present candidate.
533 *
534 * If $this->current or anything above $this->current matches the given tag
535 * name, this returns true.
536 *
537 * @param string $tagName
538 *
539 * @return bool
540 */
541 protected function isAncestor($tagName)
542 {
543 $candidate = $this->current;
544 while (\XML_ELEMENT_NODE === $candidate->nodeType) {
545 if ($candidate->tagName === $tagName) {
546 return \true;
547 }
548 $candidate = $candidate->parentNode;
549 }
550 return \false;
551 }
552 /**
553 * Returns true if the immediate parent element is of the given tagname.
554 *
555 * @param string $tagName
556 *
557 * @return bool
558 */
559 protected function isParent($tagName)
560 {
561 return $this->current->tagName === $tagName;
562 }
563 }
564