PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.4
Booking for Appointments and Events Calendar – Amelia v2.4.4
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 6 months ago DOMTreeBuilder.php 6 months ago EventHandler.php 6 months ago FileInputStream.php 6 months ago InputStream.php 6 months ago ParseError.php 6 months ago README.md 1 year ago Scanner.php 6 months ago StringInputStream.php 6 months ago Tokenizer.php 6 months ago TreeBuildingRules.php 6 months ago UTF8Utils.php 6 months ago
DOMTreeBuilder.php
714 lines
1 <?php
2
3 namespace AmeliaVendor\Masterminds\HTML5\Parser;
4
5 use AmeliaVendor\Masterminds\HTML5\Elements;
6 use AmeliaVendor\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 public function setInstructionProcessor(InstructionProcessor $proc)
236 {
237 $this->processor = $proc;
238 }
239
240 public function doctype($name, $idType = 0, $id = null, $quirks = false)
241 {
242 // This is used solely for setting quirks mode. Currently we don't
243 // try to preserve the inbound DT. We convert it to HTML5.
244 $this->quirks = $quirks;
245
246 if ($this->insertMode > static::IM_INITIAL) {
247 $this->parseError('Illegal placement of DOCTYPE tag. Ignoring: ' . $name);
248
249 return;
250 }
251
252 $this->insertMode = static::IM_BEFORE_HTML;
253 }
254
255 /**
256 * Process the start tag.
257 *
258 * @todo - XMLNS namespace handling (we need to parse, even if it's not valid)
259 * - XLink, MathML and SVG namespace handling
260 * - Omission rules: 8.1.2.4 Optional tags
261 *
262 * @param string $name
263 * @param array $attributes
264 * @param bool $selfClosing
265 *
266 * @return int
267 */
268 public function startTag($name, $attributes = array(), $selfClosing = false)
269 {
270 $lname = $this->normalizeTagName($name);
271
272 // Make sure we have an html element.
273 if (!$this->doc->documentElement && 'html' !== $name && !$this->frag) {
274 $this->startTag('html');
275 }
276
277 // Set quirks mode if we're at IM_INITIAL with no doctype.
278 if ($this->insertMode === static::IM_INITIAL) {
279 $this->quirks = true;
280 $this->parseError('No DOCTYPE specified.');
281 }
282
283 // SPECIAL TAG HANDLING:
284 // Spec says do this, and "don't ask."
285 // find the spec where this is defined... looks problematic
286 if ('image' === $name && !($this->insertMode === static::IM_IN_SVG || $this->insertMode === static::IM_IN_MATHML)) {
287 $name = 'img';
288 }
289
290 // Autoclose p tags where appropriate.
291 if ($this->insertMode >= static::IM_IN_BODY && Elements::isA($name, Elements::AUTOCLOSE_P)) {
292 $this->autoclose('p');
293 }
294
295 // Set insert mode:
296 switch ($name) {
297 case 'html':
298 $this->insertMode = static::IM_BEFORE_HEAD;
299 break;
300 case 'head':
301 if ($this->insertMode > static::IM_BEFORE_HEAD) {
302 $this->parseError('Unexpected head tag outside of head context.');
303 } else {
304 $this->insertMode = static::IM_IN_HEAD;
305 }
306 break;
307 case 'body':
308 $this->insertMode = static::IM_IN_BODY;
309 break;
310 case 'svg':
311 $this->insertMode = static::IM_IN_SVG;
312 break;
313 case 'math':
314 $this->insertMode = static::IM_IN_MATHML;
315 break;
316 case 'noscript':
317 if ($this->insertMode === static::IM_IN_HEAD) {
318 $this->insertMode = static::IM_IN_HEAD_NOSCRIPT;
319 }
320 break;
321 }
322
323 // Special case handling for SVG.
324 if ($this->insertMode === static::IM_IN_SVG) {
325 $lname = Elements::normalizeSvgElement($lname);
326 }
327
328 $pushes = 0;
329 // when we found a tag thats appears inside $nsRoots, we have to switch the defalut namespace
330 if (isset($this->nsRoots[$lname]) && $this->nsStack[0][''] !== $this->nsRoots[$lname]) {
331 array_unshift($this->nsStack, array(
332 '' => $this->nsRoots[$lname],
333 ) + $this->nsStack[0]);
334 ++$pushes;
335 }
336 $needsWorkaround = false;
337 if (isset($this->options['xmlNamespaces']) && $this->options['xmlNamespaces']) {
338 // when xmlNamespaces is true a and we found a 'xmlns' or 'xmlns:*' attribute, we should add a new item to the $nsStack
339 foreach ($attributes as $aName => $aVal) {
340 if ('xmlns' === $aName) {
341 $needsWorkaround = $aVal;
342 array_unshift($this->nsStack, array(
343 '' => $aVal,
344 ) + $this->nsStack[0]);
345 ++$pushes;
346 } elseif ('xmlns' === (($pos = strpos($aName, ':')) ? substr($aName, 0, $pos) : '')) {
347 array_unshift($this->nsStack, array(
348 substr($aName, $pos + 1) => $aVal,
349 ) + $this->nsStack[0]);
350 ++$pushes;
351 }
352 }
353 }
354
355 if ($this->onlyInline && Elements::isA($lname, Elements::BLOCK_TAG)) {
356 $this->autoclose($this->onlyInline);
357 $this->onlyInline = null;
358 }
359
360 // some elements as table related tags might have optional end tags that force us to auto close multiple tags
361 // https://www.w3.org/TR/html401/struct/tables.html
362 if ($this->current instanceof \DOMElement && isset(Elements::$optionalEndElementsParentsToClose[$lname])) {
363 foreach (Elements::$optionalEndElementsParentsToClose[$lname] as $parentElName) {
364 if ($this->current instanceof \DOMElement && $this->current->tagName === $parentElName) {
365 $this->autoclose($parentElName);
366 }
367 }
368 }
369
370 try {
371 $prefix = ($pos = strpos($lname, ':')) ? substr($lname, 0, $pos) : '';
372
373 if (false !== $needsWorkaround) {
374 $xml = "<$lname xmlns=\"$needsWorkaround\" " . (strlen($prefix) && isset($this->nsStack[0][$prefix]) ? ("xmlns:$prefix=\"" . $this->nsStack[0][$prefix] . '"') : '') . '/>';
375
376 $frag = new \DOMDocument('1.0', 'UTF-8');
377 $frag->loadXML($xml);
378
379 $ele = $this->doc->importNode($frag->documentElement, true);
380 } else {
381 if (!isset($this->nsStack[0][$prefix]) || ('' === $prefix && isset($this->options[self::OPT_DISABLE_HTML_NS]) && $this->options[self::OPT_DISABLE_HTML_NS])) {
382 $ele = $this->doc->createElement($lname);
383 } else {
384 $ele = $this->doc->createElementNS($this->nsStack[0][$prefix], $lname);
385 }
386 }
387 } catch (\DOMException $e) {
388 $this->parseError("Illegal tag name: <$lname>. Replaced with <invalid>.");
389 $ele = $this->doc->createElement('invalid');
390 }
391
392 if (Elements::isA($lname, Elements::BLOCK_ONLY_INLINE)) {
393 $this->onlyInline = $lname;
394 }
395
396 // When we add some namespacess, we have to track them. Later, when "endElement" is invoked, we have to remove them.
397 // When we are on a void tag, we do not need to care about namesapce nesting.
398 if ($pushes > 0 && !Elements::isA($name, Elements::VOID_TAG)) {
399 // PHP tends to free the memory used by DOM,
400 // to avoid spl_object_hash collisions whe have to avoid garbage collection of $ele storing it into $pushes
401 // see https://bugs.php.net/bug.php?id=67459
402 $this->pushes[spl_object_hash($ele)] = array($pushes, $ele);
403 }
404
405 foreach ($attributes as $aName => $aVal) {
406 // xmlns attributes can't be set
407 if ('xmlns' === $aName) {
408 continue;
409 }
410
411 if ($this->insertMode === static::IM_IN_SVG) {
412 $aName = Elements::normalizeSvgAttribute($aName);
413 } elseif ($this->insertMode === static::IM_IN_MATHML) {
414 $aName = Elements::normalizeMathMlAttribute($aName);
415 }
416
417 $aVal = (string) $aVal;
418
419 try {
420 $prefix = ($pos = strpos($aName, ':')) ? substr($aName, 0, $pos) : false;
421
422 if ('xmlns' === $prefix) {
423 $ele->setAttributeNS(self::NAMESPACE_XMLNS, $aName, $aVal);
424 } elseif (false !== $prefix && isset($this->nsStack[0][$prefix])) {
425 $ele->setAttributeNS($this->nsStack[0][$prefix], $aName, $aVal);
426 } else {
427 $ele->setAttribute($aName, $aVal);
428 }
429 } catch (\DOMException $e) {
430 $this->parseError("Illegal attribute name for tag $name. Ignoring: $aName");
431 continue;
432 }
433
434 // This is necessary on a non-DTD schema, like HTML5.
435 if ('id' === $aName) {
436 $ele->setIdAttribute('id', true);
437 }
438 }
439
440 if ($this->frag !== $this->current && $this->rules->hasRules($name)) {
441 // Some elements have special processing rules. Handle those separately.
442 $this->current = $this->rules->evaluate($ele, $this->current);
443 } else {
444 // Otherwise, it's a standard element.
445 $this->current->appendChild($ele);
446
447 if (!Elements::isA($name, Elements::VOID_TAG)) {
448 $this->current = $ele;
449 }
450
451 // Self-closing tags should only be respected on foreign elements
452 // (and are implied on void elements)
453 // See: https://www.w3.org/TR/html5/syntax.html#start-tags
454 if (Elements::isHtml5Element($name)) {
455 $selfClosing = false;
456 }
457 }
458
459 // This is sort of a last-ditch attempt to correct for cases where no head/body
460 // elements are provided.
461 if ($this->insertMode <= static::IM_BEFORE_HEAD && 'head' !== $name && 'html' !== $name) {
462 $this->insertMode = static::IM_IN_BODY;
463 }
464
465 // When we are on a void tag, we do not need to care about namesapce nesting,
466 // but we have to remove the namespaces pushed to $nsStack.
467 if ($pushes > 0 && Elements::isA($name, Elements::VOID_TAG)) {
468 // remove the namespaced definded by current node
469 for ($i = 0; $i < $pushes; ++$i) {
470 array_shift($this->nsStack);
471 }
472 }
473
474 if ($selfClosing) {
475 $this->endTag($name);
476 }
477
478 // Return the element mask, which the tokenizer can then use to set
479 // various processing rules.
480 return Elements::element($name);
481 }
482
483 public function endTag($name)
484 {
485 $lname = $this->normalizeTagName($name);
486
487 // Special case within 12.2.6.4.7: An end tag whose tag name is "br" should be treated as an opening tag
488 if ('br' === $name) {
489 $this->parseError('Closing tag encountered for void element br.');
490
491 $this->startTag('br');
492 }
493 // Ignore closing tags for other unary elements.
494 elseif (Elements::isA($name, Elements::VOID_TAG)) {
495 return;
496 }
497
498 if ($this->insertMode <= static::IM_BEFORE_HTML) {
499 // 8.2.5.4.2
500 if (in_array($name, array(
501 'html',
502 'br',
503 'head',
504 'title',
505 ))) {
506 $this->startTag('html');
507 $this->endTag($name);
508 $this->insertMode = static::IM_BEFORE_HEAD;
509
510 return;
511 }
512
513 // Ignore the tag.
514 $this->parseError('Illegal closing tag at global scope.');
515
516 return;
517 }
518
519 // Special case handling for SVG.
520 if ($this->insertMode === static::IM_IN_SVG) {
521 $lname = Elements::normalizeSvgElement($lname);
522 }
523
524 $cid = spl_object_hash($this->current);
525
526 // XXX: HTML has no parent. What do we do, though,
527 // if this element appears in the wrong place?
528 if ('html' === $lname) {
529 return;
530 }
531
532 // remove the namespaced definded by current node
533 if (isset($this->pushes[$cid])) {
534 for ($i = 0; $i < $this->pushes[$cid][0]; ++$i) {
535 array_shift($this->nsStack);
536 }
537 unset($this->pushes[$cid]);
538 }
539
540 if (!$this->autoclose($lname)) {
541 $this->parseError('Could not find closing tag for ' . $lname);
542 }
543
544 switch ($lname) {
545 case 'head':
546 $this->insertMode = static::IM_AFTER_HEAD;
547 break;
548 case 'body':
549 $this->insertMode = static::IM_AFTER_BODY;
550 break;
551 case 'svg':
552 case 'mathml':
553 $this->insertMode = static::IM_IN_BODY;
554 break;
555 }
556 }
557
558 public function comment($cdata)
559 {
560 // TODO: Need to handle case where comment appears outside of the HTML tag.
561 $node = $this->doc->createComment($cdata);
562 $this->current->appendChild($node);
563 }
564
565 public function text($data)
566 {
567 // XXX: Hmmm.... should we really be this strict?
568 if ($this->insertMode < static::IM_IN_HEAD) {
569 // Per '8.2.5.4.3 The "before head" insertion mode' the characters
570 // " \t\n\r\f" should be ignored but no mention of a parse error. This is
571 // practical as most documents contain these characters. Other text is not
572 // expected here so recording a parse error is necessary.
573 $dataTmp = trim($data, " \t\n\r\f");
574 if (!empty($dataTmp)) {
575 // fprintf(STDOUT, "Unexpected insert mode: %d", $this->insertMode);
576 $this->parseError('Unexpected text. Ignoring: ' . $dataTmp);
577 }
578
579 return;
580 }
581 // fprintf(STDOUT, "Appending text %s.", $data);
582 $node = $this->doc->createTextNode($data);
583 $this->current->appendChild($node);
584 }
585
586 public function eof()
587 {
588 // If the $current isn't the $root, do we need to do anything?
589 }
590
591 public function parseError($msg, $line = 0, $col = 0)
592 {
593 $this->errors[] = sprintf('Line %d, Col %d: %s', $line, $col, $msg);
594 }
595
596 public function getErrors()
597 {
598 return $this->errors;
599 }
600
601 public function cdata($data)
602 {
603 $node = $this->doc->createCDATASection($data);
604 $this->current->appendChild($node);
605 }
606
607 public function processingInstruction($name, $data = null)
608 {
609 // XXX: Ignore initial XML declaration, per the spec.
610 if ($this->insertMode === static::IM_INITIAL && 'xml' === strtolower($name)) {
611 return;
612 }
613
614 // Important: The processor may modify the current DOM tree however it sees fit.
615 if ($this->processor instanceof InstructionProcessor) {
616 $res = $this->processor->process($this->current, $name, $data);
617 if (!empty($res)) {
618 $this->current = $res;
619 }
620
621 return;
622 }
623
624 // Otherwise, this is just a dumb PI element.
625 $node = $this->doc->createProcessingInstruction($name, $data);
626
627 $this->current->appendChild($node);
628 }
629
630 // ==========================================================================
631 // UTILITIES
632 // ==========================================================================
633
634 /**
635 * Apply normalization rules to a tag name.
636 * See sections 2.9 and 8.1.2.
637 *
638 * @param string $tagName
639 *
640 * @return string The normalized tag name.
641 */
642 protected function normalizeTagName($tagName)
643 {
644 /*
645 * 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); }
646 */
647 return $tagName;
648 }
649
650 protected function quirksTreeResolver($name)
651 {
652 throw new \Exception('Not implemented.');
653 }
654
655 /**
656 * Automatically climb the tree and close the closest node with the matching $tag.
657 *
658 * @param string $tagName
659 *
660 * @return bool
661 */
662 protected function autoclose($tagName)
663 {
664 $working = $this->current;
665 do {
666 if (XML_ELEMENT_NODE !== $working->nodeType) {
667 return false;
668 }
669 if ($working->tagName === $tagName) {
670 $this->current = $working->parentNode;
671
672 return true;
673 }
674 } while ($working = $working->parentNode);
675
676 return false;
677 }
678
679 /**
680 * Checks if the given tagname is an ancestor of the present candidate.
681 *
682 * If $this->current or anything above $this->current matches the given tag
683 * name, this returns true.
684 *
685 * @param string $tagName
686 *
687 * @return bool
688 */
689 protected function isAncestor($tagName)
690 {
691 $candidate = $this->current;
692 while (XML_ELEMENT_NODE === $candidate->nodeType) {
693 if ($candidate->tagName === $tagName) {
694 return true;
695 }
696 $candidate = $candidate->parentNode;
697 }
698
699 return false;
700 }
701
702 /**
703 * Returns true if the immediate parent element is of the given tagname.
704 *
705 * @param string $tagName
706 *
707 * @return bool
708 */
709 protected function isParent($tagName)
710 {
711 return $this->current->tagName === $tagName;
712 }
713 }
714