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