Chris@0: self::NAMESPACE_HTML, Chris@0: 'svg' => self::NAMESPACE_SVG, Chris@17: 'math' => self::NAMESPACE_MATHML, Chris@0: ); Chris@0: Chris@0: /** Chris@0: * Holds the always available namespaces (which does not require the XMLNS declaration). Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: protected $implicitNamespaces = array( Chris@0: 'xml' => self::NAMESPACE_XML, Chris@0: 'xmlns' => self::NAMESPACE_XMLNS, Chris@17: 'xlink' => self::NAMESPACE_XLINK, Chris@0: ); Chris@0: Chris@0: /** Chris@0: * Holds a stack of currently active namespaces. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: protected $nsStack = array(); Chris@0: Chris@0: /** Chris@0: * Holds the number of namespaces declared by a node. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: protected $pushes = array(); Chris@0: Chris@0: /** Chris@0: * Defined in 8.2.5. Chris@0: */ Chris@0: const IM_INITIAL = 0; Chris@0: Chris@0: const IM_BEFORE_HTML = 1; Chris@0: Chris@0: const IM_BEFORE_HEAD = 2; Chris@0: Chris@0: const IM_IN_HEAD = 3; Chris@0: Chris@0: const IM_IN_HEAD_NOSCRIPT = 4; Chris@0: Chris@0: const IM_AFTER_HEAD = 5; Chris@0: Chris@0: const IM_IN_BODY = 6; Chris@0: Chris@0: const IM_TEXT = 7; Chris@0: Chris@0: const IM_IN_TABLE = 8; Chris@0: Chris@0: const IM_IN_TABLE_TEXT = 9; Chris@0: Chris@0: const IM_IN_CAPTION = 10; Chris@0: Chris@0: const IM_IN_COLUMN_GROUP = 11; Chris@0: Chris@0: const IM_IN_TABLE_BODY = 12; Chris@0: Chris@0: const IM_IN_ROW = 13; Chris@0: Chris@0: const IM_IN_CELL = 14; Chris@0: Chris@0: const IM_IN_SELECT = 15; Chris@0: Chris@0: const IM_IN_SELECT_IN_TABLE = 16; Chris@0: Chris@0: const IM_AFTER_BODY = 17; Chris@0: Chris@0: const IM_IN_FRAMESET = 18; Chris@0: Chris@0: const IM_AFTER_FRAMESET = 19; Chris@0: Chris@0: const IM_AFTER_AFTER_BODY = 20; Chris@0: Chris@0: const IM_AFTER_AFTER_FRAMESET = 21; Chris@0: Chris@0: const IM_IN_SVG = 22; Chris@0: Chris@0: const IM_IN_MATHML = 23; Chris@0: Chris@0: protected $options = array(); Chris@0: Chris@0: protected $stack = array(); Chris@0: Chris@0: protected $current; // Pointer in the tag hierarchy. Chris@17: protected $rules; Chris@0: protected $doc; Chris@0: Chris@0: protected $frag; Chris@0: Chris@0: protected $processor; Chris@0: Chris@0: protected $insertMode = 0; Chris@0: Chris@0: /** Chris@17: * Track if we are in an element that allows only inline child nodes. Chris@17: * Chris@0: * @var string|null Chris@0: */ Chris@0: protected $onlyInline; Chris@0: Chris@0: /** Chris@0: * Quirks mode is enabled by default. Chris@17: * Any document that is missing the DT will be considered to be in quirks mode. Chris@0: */ Chris@0: protected $quirks = true; Chris@0: Chris@0: protected $errors = array(); Chris@0: Chris@0: public function __construct($isFragment = false, array $options = array()) Chris@0: { Chris@0: $this->options = $options; Chris@0: Chris@0: if (isset($options[self::OPT_TARGET_DOC])) { Chris@0: $this->doc = $options[self::OPT_TARGET_DOC]; Chris@0: } else { Chris@0: $impl = new \DOMImplementation(); Chris@0: // XXX: Chris@0: // Create the doctype. For now, we are always creating HTML5 Chris@0: // documents, and attempting to up-convert any older DTDs to HTML5. Chris@0: $dt = $impl->createDocumentType('html'); Chris@0: // $this->doc = \DOMImplementation::createDocument(NULL, 'html', $dt); Chris@0: $this->doc = $impl->createDocument(null, null, $dt); Chris@0: } Chris@17: Chris@0: $this->errors = array(); Chris@0: Chris@0: $this->current = $this->doc; // ->documentElement; Chris@0: Chris@0: // Create a rules engine for tags. Chris@17: $this->rules = new TreeBuildingRules(); Chris@0: Chris@0: $implicitNS = array(); Chris@0: if (isset($this->options[self::OPT_IMPLICIT_NS])) { Chris@0: $implicitNS = $this->options[self::OPT_IMPLICIT_NS]; Chris@17: } elseif (isset($this->options['implicitNamespaces'])) { Chris@17: $implicitNS = $this->options['implicitNamespaces']; Chris@0: } Chris@0: Chris@0: // Fill $nsStack with the defalut HTML5 namespaces, plus the "implicitNamespaces" array taken form $options Chris@17: array_unshift($this->nsStack, $implicitNS + array('' => self::NAMESPACE_HTML) + $this->implicitNamespaces); Chris@0: Chris@0: if ($isFragment) { Chris@0: $this->insertMode = static::IM_IN_BODY; Chris@0: $this->frag = $this->doc->createDocumentFragment(); Chris@0: $this->current = $this->frag; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Get the document. Chris@0: */ Chris@0: public function document() Chris@0: { Chris@0: return $this->doc; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Get the DOM fragment for the body. Chris@0: * Chris@0: * This returns a DOMNodeList because a fragment may have zero or more Chris@0: * DOMNodes at its root. Chris@0: * Chris@0: * @see http://www.w3.org/TR/2012/CR-html5-20121217/syntax.html#concept-frag-parse-context Chris@0: * Chris@17: * @return \DOMDocumentFragment Chris@0: */ Chris@0: public function fragment() Chris@0: { Chris@0: return $this->frag; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Provide an instruction processor. Chris@0: * Chris@0: * This is used for handling Processor Instructions as they are Chris@0: * inserted. If omitted, PI's are inserted directly into the DOM tree. Chris@17: * Chris@17: * @param InstructionProcessor $proc Chris@0: */ Chris@17: public function setInstructionProcessor(InstructionProcessor $proc) Chris@0: { Chris@0: $this->processor = $proc; Chris@0: } Chris@0: Chris@0: public function doctype($name, $idType = 0, $id = null, $quirks = false) Chris@0: { Chris@0: // This is used solely for setting quirks mode. Currently we don't Chris@0: // try to preserve the inbound DT. We convert it to HTML5. Chris@0: $this->quirks = $quirks; Chris@0: Chris@0: if ($this->insertMode > static::IM_INITIAL) { Chris@17: $this->parseError('Illegal placement of DOCTYPE tag. Ignoring: ' . $name); Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@0: $this->insertMode = static::IM_BEFORE_HTML; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Process the start tag. Chris@0: * Chris@0: * @todo - XMLNS namespace handling (we need to parse, even if it's not valid) Chris@0: * - XLink, MathML and SVG namespace handling Chris@0: * - Omission rules: 8.1.2.4 Optional tags Chris@17: * Chris@17: * @param string $name Chris@17: * @param array $attributes Chris@17: * @param bool $selfClosing Chris@17: * Chris@17: * @return int Chris@0: */ Chris@0: public function startTag($name, $attributes = array(), $selfClosing = false) Chris@0: { Chris@0: $lname = $this->normalizeTagName($name); Chris@0: Chris@0: // Make sure we have an html element. Chris@17: if (!$this->doc->documentElement && 'html' !== $name && !$this->frag) { Chris@0: $this->startTag('html'); Chris@0: } Chris@0: Chris@0: // Set quirks mode if we're at IM_INITIAL with no doctype. Chris@17: if ($this->insertMode === static::IM_INITIAL) { Chris@0: $this->quirks = true; Chris@17: $this->parseError('No DOCTYPE specified.'); Chris@0: } Chris@0: Chris@0: // SPECIAL TAG HANDLING: Chris@0: // Spec says do this, and "don't ask." Chris@0: // find the spec where this is defined... looks problematic Chris@17: if ('image' === $name && !($this->insertMode === static::IM_IN_SVG || $this->insertMode === static::IM_IN_MATHML)) { Chris@0: $name = 'img'; Chris@0: } Chris@0: Chris@0: // Autoclose p tags where appropriate. Chris@0: if ($this->insertMode >= static::IM_IN_BODY && Elements::isA($name, Elements::AUTOCLOSE_P)) { Chris@0: $this->autoclose('p'); Chris@0: } Chris@0: Chris@0: // Set insert mode: Chris@0: switch ($name) { Chris@0: case 'html': Chris@0: $this->insertMode = static::IM_BEFORE_HEAD; Chris@0: break; Chris@0: case 'head': Chris@0: if ($this->insertMode > static::IM_BEFORE_HEAD) { Chris@17: $this->parseError('Unexpected head tag outside of head context.'); Chris@0: } else { Chris@0: $this->insertMode = static::IM_IN_HEAD; Chris@0: } Chris@0: break; Chris@0: case 'body': Chris@0: $this->insertMode = static::IM_IN_BODY; Chris@0: break; Chris@0: case 'svg': Chris@0: $this->insertMode = static::IM_IN_SVG; Chris@0: break; Chris@0: case 'math': Chris@0: $this->insertMode = static::IM_IN_MATHML; Chris@0: break; Chris@0: case 'noscript': Chris@17: if ($this->insertMode === static::IM_IN_HEAD) { Chris@0: $this->insertMode = static::IM_IN_HEAD_NOSCRIPT; Chris@0: } Chris@0: break; Chris@0: } Chris@0: Chris@0: // Special case handling for SVG. Chris@17: if ($this->insertMode === static::IM_IN_SVG) { Chris@0: $lname = Elements::normalizeSvgElement($lname); Chris@0: } Chris@0: Chris@0: $pushes = 0; Chris@0: // when we found a tag thats appears inside $nsRoots, we have to switch the defalut namespace Chris@0: if (isset($this->nsRoots[$lname]) && $this->nsStack[0][''] !== $this->nsRoots[$lname]) { Chris@0: array_unshift($this->nsStack, array( Chris@17: '' => $this->nsRoots[$lname], Chris@0: ) + $this->nsStack[0]); Chris@17: ++$pushes; Chris@0: } Chris@0: $needsWorkaround = false; Chris@17: if (isset($this->options['xmlNamespaces']) && $this->options['xmlNamespaces']) { Chris@0: // when xmlNamespaces is true a and we found a 'xmlns' or 'xmlns:*' attribute, we should add a new item to the $nsStack Chris@0: foreach ($attributes as $aName => $aVal) { Chris@17: if ('xmlns' === $aName) { Chris@0: $needsWorkaround = $aVal; Chris@0: array_unshift($this->nsStack, array( Chris@17: '' => $aVal, Chris@0: ) + $this->nsStack[0]); Chris@17: ++$pushes; Chris@17: } elseif ('xmlns' === (($pos = strpos($aName, ':')) ? substr($aName, 0, $pos) : '')) { Chris@0: array_unshift($this->nsStack, array( Chris@17: substr($aName, $pos + 1) => $aVal, Chris@0: ) + $this->nsStack[0]); Chris@17: ++$pushes; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: if ($this->onlyInline && Elements::isA($lname, Elements::BLOCK_TAG)) { Chris@17: $this->autoclose($this->onlyInline); Chris@17: $this->onlyInline = null; Chris@0: } Chris@0: Chris@0: try { Chris@0: $prefix = ($pos = strpos($lname, ':')) ? substr($lname, 0, $pos) : ''; Chris@0: Chris@17: if (false !== $needsWorkaround) { Chris@17: $xml = "<$lname xmlns=\"$needsWorkaround\" " . (strlen($prefix) && isset($this->nsStack[0][$prefix]) ? ("xmlns:$prefix=\"" . $this->nsStack[0][$prefix] . '"') : '') . '/>'; Chris@0: Chris@0: $frag = new \DOMDocument('1.0', 'UTF-8'); Chris@0: $frag->loadXML($xml); Chris@0: Chris@0: $ele = $this->doc->importNode($frag->documentElement, true); Chris@0: } else { Chris@17: if (!isset($this->nsStack[0][$prefix]) || ('' === $prefix && isset($this->options[self::OPT_DISABLE_HTML_NS]) && $this->options[self::OPT_DISABLE_HTML_NS])) { Chris@0: $ele = $this->doc->createElement($lname); Chris@0: } else { Chris@0: $ele = $this->doc->createElementNS($this->nsStack[0][$prefix], $lname); Chris@0: } Chris@0: } Chris@0: } catch (\DOMException $e) { Chris@0: $this->parseError("Illegal tag name: <$lname>. Replaced with ."); Chris@0: $ele = $this->doc->createElement('invalid'); Chris@0: } Chris@0: Chris@0: if (Elements::isA($lname, Elements::BLOCK_ONLY_INLINE)) { Chris@17: $this->onlyInline = $lname; Chris@0: } Chris@0: Chris@0: // When we add some namespacess, we have to track them. Later, when "endElement" is invoked, we have to remove them. Chris@0: // When we are on a void tag, we do not need to care about namesapce nesting. Chris@0: if ($pushes > 0 && !Elements::isA($name, Elements::VOID_TAG)) { Chris@0: // PHP tends to free the memory used by DOM, Chris@0: // to avoid spl_object_hash collisions whe have to avoid garbage collection of $ele storing it into $pushes Chris@0: // see https://bugs.php.net/bug.php?id=67459 Chris@0: $this->pushes[spl_object_hash($ele)] = array($pushes, $ele); Chris@0: Chris@0: // SEE https://github.com/facebook/hhvm/issues/2962 Chris@0: if (defined('HHVM_VERSION')) { Chris@0: $ele->setAttribute('html5-php-fake-id-attribute', spl_object_hash($ele)); Chris@0: } Chris@0: } Chris@0: Chris@0: foreach ($attributes as $aName => $aVal) { Chris@0: // xmlns attributes can't be set Chris@17: if ('xmlns' === $aName) { Chris@0: continue; Chris@0: } Chris@0: Chris@17: if ($this->insertMode === static::IM_IN_SVG) { Chris@0: $aName = Elements::normalizeSvgAttribute($aName); Chris@17: } elseif ($this->insertMode === static::IM_IN_MATHML) { Chris@0: $aName = Elements::normalizeMathMlAttribute($aName); Chris@0: } Chris@0: Chris@0: try { Chris@0: $prefix = ($pos = strpos($aName, ':')) ? substr($aName, 0, $pos) : false; Chris@0: Chris@17: if ('xmlns' === $prefix) { Chris@17: $ele->setAttributeNS(self::NAMESPACE_XMLNS, $aName, $aVal); Chris@17: } elseif (false !== $prefix && isset($this->nsStack[0][$prefix])) { Chris@17: $ele->setAttributeNS($this->nsStack[0][$prefix], $aName, $aVal); Chris@0: } else { Chris@0: $ele->setAttribute($aName, $aVal); Chris@0: } Chris@0: } catch (\DOMException $e) { Chris@0: $this->parseError("Illegal attribute name for tag $name. Ignoring: $aName"); Chris@0: continue; Chris@0: } Chris@0: Chris@0: // This is necessary on a non-DTD schema, like HTML5. Chris@17: if ('id' === $aName) { Chris@0: $ele->setIdAttribute('id', true); Chris@0: } Chris@0: } Chris@0: Chris@17: if ($this->frag !== $this->current && $this->rules->hasRules($name)) { Chris@17: // Some elements have special processing rules. Handle those separately. Chris@0: $this->current = $this->rules->evaluate($ele, $this->current); Chris@17: } else { Chris@17: // Otherwise, it's a standard element. Chris@0: $this->current->appendChild($ele); Chris@0: Chris@17: if (!Elements::isA($name, Elements::VOID_TAG)) { Chris@0: $this->current = $ele; Chris@0: } Chris@17: Chris@17: // Self-closing tags should only be respected on foreign elements Chris@17: // (and are implied on void elements) Chris@17: // See: https://www.w3.org/TR/html5/syntax.html#start-tags Chris@17: if (Elements::isHtml5Element($name)) { Chris@17: $selfClosing = false; Chris@17: } Chris@0: } Chris@0: Chris@0: // This is sort of a last-ditch attempt to correct for cases where no head/body Chris@0: // elements are provided. Chris@17: if ($this->insertMode <= static::IM_BEFORE_HEAD && 'head' !== $name && 'html' !== $name) { Chris@0: $this->insertMode = static::IM_IN_BODY; Chris@0: } Chris@0: Chris@0: // When we are on a void tag, we do not need to care about namesapce nesting, Chris@0: // but we have to remove the namespaces pushed to $nsStack. Chris@0: if ($pushes > 0 && Elements::isA($name, Elements::VOID_TAG)) { Chris@0: // remove the namespaced definded by current node Chris@17: for ($i = 0; $i < $pushes; ++$i) { Chris@0: array_shift($this->nsStack); Chris@0: } Chris@0: } Chris@17: Chris@17: if ($selfClosing) { Chris@17: $this->endTag($name); Chris@17: } Chris@17: Chris@0: // Return the element mask, which the tokenizer can then use to set Chris@0: // various processing rules. Chris@0: return Elements::element($name); Chris@0: } Chris@0: Chris@0: public function endTag($name) Chris@0: { Chris@0: $lname = $this->normalizeTagName($name); Chris@0: Chris@0: // Ignore closing tags for unary elements. Chris@0: if (Elements::isA($name, Elements::VOID_TAG)) { Chris@0: return; Chris@0: } Chris@0: Chris@0: if ($this->insertMode <= static::IM_BEFORE_HTML) { Chris@0: // 8.2.5.4.2 Chris@0: if (in_array($name, array( Chris@0: 'html', Chris@0: 'br', Chris@0: 'head', Chris@17: 'title', Chris@0: ))) { Chris@0: $this->startTag('html'); Chris@0: $this->endTag($name); Chris@0: $this->insertMode = static::IM_BEFORE_HEAD; Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@0: // Ignore the tag. Chris@17: $this->parseError('Illegal closing tag at global scope.'); Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@0: // Special case handling for SVG. Chris@17: if ($this->insertMode === static::IM_IN_SVG) { Chris@0: $lname = Elements::normalizeSvgElement($lname); Chris@0: } Chris@0: Chris@0: // See https://github.com/facebook/hhvm/issues/2962 Chris@0: if (defined('HHVM_VERSION') && ($cid = $this->current->getAttribute('html5-php-fake-id-attribute'))) { Chris@0: $this->current->removeAttribute('html5-php-fake-id-attribute'); Chris@0: } else { Chris@0: $cid = spl_object_hash($this->current); Chris@0: } Chris@0: Chris@0: // XXX: HTML has no parent. What do we do, though, Chris@0: // if this element appears in the wrong place? Chris@17: if ('html' === $lname) { Chris@0: return; Chris@0: } Chris@0: Chris@0: // remove the namespaced definded by current node Chris@0: if (isset($this->pushes[$cid])) { Chris@17: for ($i = 0; $i < $this->pushes[$cid][0]; ++$i) { Chris@0: array_shift($this->nsStack); Chris@0: } Chris@0: unset($this->pushes[$cid]); Chris@0: } Chris@0: Chris@17: if (!$this->autoclose($lname)) { Chris@0: $this->parseError('Could not find closing tag for ' . $lname); Chris@0: } Chris@0: Chris@0: switch ($lname) { Chris@17: case 'head': Chris@0: $this->insertMode = static::IM_AFTER_HEAD; Chris@0: break; Chris@17: case 'body': Chris@0: $this->insertMode = static::IM_AFTER_BODY; Chris@0: break; Chris@17: case 'svg': Chris@17: case 'mathml': Chris@0: $this->insertMode = static::IM_IN_BODY; Chris@0: break; Chris@0: } Chris@0: } Chris@0: Chris@0: public function comment($cdata) Chris@0: { Chris@0: // TODO: Need to handle case where comment appears outside of the HTML tag. Chris@0: $node = $this->doc->createComment($cdata); Chris@0: $this->current->appendChild($node); Chris@0: } Chris@0: Chris@0: public function text($data) Chris@0: { Chris@0: // XXX: Hmmm.... should we really be this strict? Chris@0: if ($this->insertMode < static::IM_IN_HEAD) { Chris@0: // Per '8.2.5.4.3 The "before head" insertion mode' the characters Chris@0: // " \t\n\r\f" should be ignored but no mention of a parse error. This is Chris@0: // practical as most documents contain these characters. Other text is not Chris@0: // expected here so recording a parse error is necessary. Chris@0: $dataTmp = trim($data, " \t\n\r\f"); Chris@17: if (!empty($dataTmp)) { Chris@0: // fprintf(STDOUT, "Unexpected insert mode: %d", $this->insertMode); Chris@17: $this->parseError('Unexpected text. Ignoring: ' . $dataTmp); Chris@0: } Chris@0: Chris@0: return; Chris@0: } Chris@0: // fprintf(STDOUT, "Appending text %s.", $data); Chris@0: $node = $this->doc->createTextNode($data); Chris@0: $this->current->appendChild($node); Chris@0: } Chris@0: Chris@0: public function eof() Chris@0: { Chris@0: // If the $current isn't the $root, do we need to do anything? Chris@0: } Chris@0: Chris@0: public function parseError($msg, $line = 0, $col = 0) Chris@0: { Chris@17: $this->errors[] = sprintf('Line %d, Col %d: %s', $line, $col, $msg); Chris@0: } Chris@0: Chris@0: public function getErrors() Chris@0: { Chris@0: return $this->errors; Chris@0: } Chris@0: Chris@0: public function cdata($data) Chris@0: { Chris@0: $node = $this->doc->createCDATASection($data); Chris@0: $this->current->appendChild($node); Chris@0: } Chris@0: Chris@0: public function processingInstruction($name, $data = null) Chris@0: { Chris@0: // XXX: Ignore initial XML declaration, per the spec. Chris@17: if ($this->insertMode === static::IM_INITIAL && 'xml' === strtolower($name)) { Chris@0: return; Chris@0: } Chris@0: Chris@17: // Important: The processor may modify the current DOM tree however it sees fit. Chris@17: if ($this->processor instanceof InstructionProcessor) { Chris@0: $res = $this->processor->process($this->current, $name, $data); Chris@17: if (!empty($res)) { Chris@0: $this->current = $res; Chris@0: } Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@0: // Otherwise, this is just a dumb PI element. Chris@0: $node = $this->doc->createProcessingInstruction($name, $data); Chris@0: Chris@0: $this->current->appendChild($node); Chris@0: } Chris@0: Chris@0: // ========================================================================== Chris@0: // UTILITIES Chris@0: // ========================================================================== Chris@0: Chris@0: /** Chris@0: * Apply normalization rules to a tag name. Chris@0: * See sections 2.9 and 8.1.2. Chris@0: * Chris@17: * @param string $tagName Chris@17: * Chris@0: * @return string The normalized tag name. Chris@0: */ Chris@17: protected function normalizeTagName($tagName) Chris@0: { Chris@0: /* Chris@0: * 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); } Chris@0: */ Chris@17: return $tagName; Chris@0: } Chris@0: Chris@0: protected function quirksTreeResolver($name) Chris@0: { Chris@17: throw new \Exception('Not implemented.'); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Automatically climb the tree and close the closest node with the matching $tag. Chris@17: * Chris@17: * @param string $tagName Chris@17: * Chris@17: * @return bool Chris@0: */ Chris@17: protected function autoclose($tagName) Chris@0: { Chris@0: $working = $this->current; Chris@0: do { Chris@17: if (XML_ELEMENT_NODE !== $working->nodeType) { Chris@0: return false; Chris@0: } Chris@17: if ($working->tagName === $tagName) { Chris@0: $this->current = $working->parentNode; Chris@0: Chris@0: return true; Chris@0: } Chris@0: } while ($working = $working->parentNode); Chris@17: Chris@0: return false; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Checks if the given tagname is an ancestor of the present candidate. Chris@0: * Chris@0: * If $this->current or anything above $this->current matches the given tag Chris@0: * name, this returns true. Chris@17: * Chris@17: * @param string $tagName Chris@17: * Chris@17: * @return bool Chris@0: */ Chris@17: protected function isAncestor($tagName) Chris@0: { Chris@0: $candidate = $this->current; Chris@17: while (XML_ELEMENT_NODE === $candidate->nodeType) { Chris@17: if ($candidate->tagName === $tagName) { Chris@0: return true; Chris@0: } Chris@0: $candidate = $candidate->parentNode; Chris@0: } Chris@0: Chris@0: return false; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns true if the immediate parent element is of the given tagname. Chris@17: * Chris@17: * @param string $tagName Chris@17: * Chris@17: * @return bool Chris@0: */ Chris@17: protected function isParent($tagName) Chris@0: { Chris@17: return $this->current->tagName === $tagName; Chris@0: } Chris@0: }