|
|
@ -294,16 +294,14 @@ class TreeBuilder { |
|
|
|
$this->debugLog .= "EMITTED: ".constant(get_class($token)."::NAME")."\n"; |
|
|
|
return true; |
|
|
|
})()); |
|
|
|
$iterations = 0; |
|
|
|
$insertionMode = $this->insertionMode; |
|
|
|
|
|
|
|
// If element name coercison has occurred at some earlier point, |
|
|
|
// we must coerce all end tag names to match mangled start tags |
|
|
|
if ($token instanceof EndTagToken && $this->DOM->mangledElements) { |
|
|
|
$token->name = $this->coerceName($token->name); |
|
|
|
} |
|
|
|
ProcessToken: |
|
|
|
$adjustedCurrentNode = $this->stack->adjustedCurrentNode; |
|
|
|
$adjustedCurrentNodeName = $this->stack->adjustedCurrentNodeName; |
|
|
|
$adjustedCurrentNodeNamespace = $this->stack->adjustedCurrentNodeNamespace; |
|
|
|
|
|
|
|
# 13.2.6 Tree construction |
|
|
|
# |
|
|
@ -315,49 +313,30 @@ class TreeBuilder { |
|
|
|
# If the adjusted current node is an element in the HTML namespace |
|
|
|
// DEVIATION: For the purposes of this implementation the HTML namespace is null |
|
|
|
// rather than the XHTML namespace |
|
|
|
|| $adjustedCurrentNodeNamespace === null |
|
|
|
|| $this->stack->adjustedCurrentNodeNamespace === null |
|
|
|
# If the adjusted current node is a MathML text integration |
|
|
|
# point and the token is a start tag whose tag name is |
|
|
|
# neither "mglyph" nor "malignmark" |
|
|
|
# If the adjusted current node is a MathML text integration |
|
|
|
# point and the token is a character token |
|
|
|
|| ($adjustedCurrentNode->isMathMLTextIntegrationPoint() && (($token instanceof StartTagToken && ($token->name !== 'mglyph' && $token->name !== 'malignmark') || $token instanceof CharacterToken))) |
|
|
|
|| ($this->stack->adjustedCurrentNode->isMathMLTextIntegrationPoint() && (($token instanceof StartTagToken && ($token->name !== 'mglyph' && $token->name !== 'malignmark') || $token instanceof CharacterToken))) |
|
|
|
# If the adjusted current node is an annotation-xml element |
|
|
|
# in the MathML namespace and the token is a start tag |
|
|
|
# whose tag name is "svg" |
|
|
|
|| ($adjustedCurrentNodeNamespace === Parser::MATHML_NAMESPACE && $adjustedCurrentNodeName === 'annotation-xml' && $token instanceof StartTagToken && $token->name === 'svg') |
|
|
|
|| ($this->stack->adjustedCurrentNodeNamespace === Parser::MATHML_NAMESPACE && $this->stack->adjustedCurrentNodeName === 'annotation-xml' && $token instanceof StartTagToken && $token->name === 'svg') |
|
|
|
# If the adjusted current node is an HTML integration point |
|
|
|
# and the token is a start tag |
|
|
|
# If the adjusted current node is an HTML integration point |
|
|
|
# and the token is a character token |
|
|
|
|| ($adjustedCurrentNode->isHTMLIntegrationPoint() && ($token instanceof StartTagToken || $token instanceof CharacterToken)) |
|
|
|
|| ($this->stack->adjustedCurrentNode->isHTMLIntegrationPoint() && ($token instanceof StartTagToken || $token instanceof CharacterToken)) |
|
|
|
# If the token is an end-of-file token |
|
|
|
|| $token instanceof EOFToken |
|
|
|
) { |
|
|
|
# Process the token according to the rules given in the section |
|
|
|
# corresponding to the current insertion mode in HTML content. |
|
|
|
$this->parseTokenInHTMLContent($token); |
|
|
|
} |
|
|
|
# Otherwise |
|
|
|
else { |
|
|
|
# Process the token according to the rules given in the section |
|
|
|
# for parsing tokens in foreign content. |
|
|
|
$this->parseTokenInForeignContent($token); |
|
|
|
} |
|
|
|
# When a start tag token is emitted with its self-closing flag set, if the flag |
|
|
|
# is not acknowledged when it is processed by the tree construction stage, that |
|
|
|
# is a non-void-html-element-start-tag-with-trailing-solidus parse error. |
|
|
|
if ($token instanceof StartTagToken && $token->selfClosing && !$token->selfClosingAcknowledged) { |
|
|
|
$this->error(ParseError::NON_VOID_HTML_ELEMENT_START_TAG_WITH_TRAILING_SOLIDUS, $token->name); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
protected function parseTokenInHTMLContent(Token $token, int $insertionMode = null): bool { |
|
|
|
$iterations = 0; |
|
|
|
ProcessToken: |
|
|
|
assert($iterations++ < 50, new LoopException("Probable infinite loop detected in HTML content handling (inner reprocessing)")); |
|
|
|
$insertionMode = $insertionMode ?? $this->insertionMode; |
|
|
|
|
|
|
|
assert((function() use ($insertionMode) { |
|
|
|
$mode = self::INSERTION_MODE_NAMES[$insertionMode] ?? $insertionMode; |
|
|
|
$this->debugLog .= " Mode: $mode (".(string) $this->stack.")\n"; |
|
|
@ -1230,7 +1209,7 @@ class TreeBuilder { |
|
|
|
// Character tokens in this implementation can have more than one character in |
|
|
|
// them. |
|
|
|
if (strlen($nextToken->data) === 1 && $nextToken->data === "\n") { |
|
|
|
return true; |
|
|
|
continue; |
|
|
|
} elseif (strpos($nextToken->data, "\n") === 0) { |
|
|
|
$nextToken->data = substr($nextToken->data, 1); |
|
|
|
} |
|
|
@ -1527,7 +1506,7 @@ class TreeBuilder { |
|
|
|
// Character tokens in this implementation can have more than one character in |
|
|
|
// them. |
|
|
|
if (strlen($nextToken->data) === 1 && $nextToken->data === "\n") { |
|
|
|
return true; |
|
|
|
continue; |
|
|
|
} elseif (strpos($nextToken->data, "\n") === 0) { |
|
|
|
$nextToken->data = substr($nextToken->data, 1); |
|
|
|
} |
|
|
@ -1760,7 +1739,7 @@ class TreeBuilder { |
|
|
|
# scope, then this is a parse error; return and ignore the token. |
|
|
|
if (!$node || !$this->stack->hasElementInScope($node)) { |
|
|
|
$this->error(ParseError::UNEXPECTED_END_TAG, $token->name); |
|
|
|
return true; |
|
|
|
continue; |
|
|
|
} |
|
|
|
# 4. Generate implied end tags. |
|
|
|
$this->stack->generateImpliedEndTags(); |
|
|
@ -1778,7 +1757,7 @@ class TreeBuilder { |
|
|
|
# this is a parse error; return and ignore the token. |
|
|
|
if ($this->stack->hasElementInScope('form')) { |
|
|
|
$this->error(ParseError::UNEXPECTED_END_TAG, $token->name); |
|
|
|
return true; |
|
|
|
continue; |
|
|
|
} |
|
|
|
# 2. Generate implied end tags. |
|
|
|
$this->stack->generateImpliedEndTags(); |
|
|
@ -1929,13 +1908,13 @@ class TreeBuilder { |
|
|
|
} |
|
|
|
# Pop all the nodes from the current node up to node, including node, then stop these steps. |
|
|
|
$this->stack->popUntilSame($node); |
|
|
|
return true; |
|
|
|
continue 2; |
|
|
|
} |
|
|
|
# Otherwise, if node is in the special category, then |
|
|
|
# this is a parse error; ignore the token, and return. |
|
|
|
elseif ($this->isElementSpecial($node)) { |
|
|
|
$this->error(ParseError::UNEXPECTED_END_TAG, $token->name); |
|
|
|
return true; |
|
|
|
continue 2; |
|
|
|
} |
|
|
|
# Set node to the previous entry in the stack of open elements. |
|
|
|
# Return to the step labeled loop. |
|
|
@ -1961,7 +1940,7 @@ class TreeBuilder { |
|
|
|
} |
|
|
|
|
|
|
|
# 2. Stop parsing. |
|
|
|
return true; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
# 13.2.6.4.8 The "text" insertion mode |
|
|
@ -3124,7 +3103,7 @@ class TreeBuilder { |
|
|
|
# An end-of-file token |
|
|
|
elseif ($token instanceof EOFToken) { |
|
|
|
# Stop parsing. |
|
|
|
return true; |
|
|
|
return; |
|
|
|
} |
|
|
|
# Anything else |
|
|
|
else { |
|
|
@ -3227,7 +3206,7 @@ class TreeBuilder { |
|
|
|
$this->error(ParseError::UNEXPECTED_EOF); |
|
|
|
} |
|
|
|
# Stop parsing. |
|
|
|
return true; |
|
|
|
return; |
|
|
|
} |
|
|
|
# Anything else |
|
|
|
else { |
|
|
@ -3289,7 +3268,7 @@ class TreeBuilder { |
|
|
|
# An end-of-file token |
|
|
|
elseif ($token instanceof EOFToken) { |
|
|
|
# Stop parsing. |
|
|
|
return true; |
|
|
|
return; |
|
|
|
} |
|
|
|
# Anything else |
|
|
|
else { |
|
|
@ -3331,7 +3310,7 @@ class TreeBuilder { |
|
|
|
# An end-of-file token |
|
|
|
elseif ($token instanceof EOFToken) { |
|
|
|
# Stop parsing. |
|
|
|
return true; |
|
|
|
return; |
|
|
|
} |
|
|
|
# Anything else |
|
|
|
else { |
|
|
@ -3371,7 +3350,7 @@ class TreeBuilder { |
|
|
|
# An end-of-file token |
|
|
|
elseif ($token instanceof EOFToken) { |
|
|
|
# Stop parsing. |
|
|
|
return true; |
|
|
|
return; |
|
|
|
} |
|
|
|
# A start tag whose tag name is "noframes" |
|
|
|
elseif ($token instanceof StartTagToken && $token->name === "noframes") { |
|
|
@ -3397,223 +3376,12 @@ class TreeBuilder { |
|
|
|
else { |
|
|
|
throw new \Exception("UNREACHABLE CODE"); |
|
|
|
} |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
protected function adopt(TagToken $token): void { |
|
|
|
# The adoption agency algorithm, which takes as its only argument a |
|
|
|
# token 'token' for which the algorithm is being run, consists of |
|
|
|
# the following steps: |
|
|
|
|
|
|
|
assert((function() { |
|
|
|
$this->debugLog .= " Adoption agency (".(string) $this->stack.")\n"; |
|
|
|
return true; |
|
|
|
})()); |
|
|
|
|
|
|
|
# Let subject be token's tag name. |
|
|
|
$subject = $token->name; |
|
|
|
$errorCode = $token instanceof StartTagToken ? ParseError::UNEXPECTED_START_TAG : ParseError::UNEXPECTED_END_TAG; |
|
|
|
# If the current node is an HTML element whose tag name is subject, |
|
|
|
# and the current node is not in the list of active formatting elements, |
|
|
|
# then pop the current node off the stack of open elements, and return. |
|
|
|
$currentNode = $this->stack->currentNode; |
|
|
|
if ( |
|
|
|
$currentNode->namespaceURI === null |
|
|
|
&& $currentNode->nodeName === $subject |
|
|
|
&& $this->activeFormattingElementsList->findSame($currentNode) === -1 |
|
|
|
) { |
|
|
|
$this->stack->pop(); |
|
|
|
return; |
|
|
|
} |
|
|
|
# Let outer loop counter be zero. |
|
|
|
$outerLoopCounter = 0; |
|
|
|
# Outer loop: If outer loop counter is greater than or equal to eight, then return. |
|
|
|
OuterLoop: |
|
|
|
if ($outerLoopCounter >= 8) { |
|
|
|
return; |
|
|
|
} |
|
|
|
# Increment outer loop counter by one. |
|
|
|
$outerLoopCounter++; |
|
|
|
# Let formatting element be the last element in the list of active |
|
|
|
# formatting elements that: |
|
|
|
# 1. is between the end of the list and the last marker in the list, |
|
|
|
# if any, or the start of the list otherwise, and |
|
|
|
# 2. has the tag name subject. |
|
|
|
$formattingElementIndex = $this->activeFormattingElementsList->findToMarker($subject); |
|
|
|
if ($formattingElementIndex > -1) { |
|
|
|
$formattingElement = $this->activeFormattingElementsList[$formattingElementIndex]['element']; |
|
|
|
$formattingToken = $this->activeFormattingElementsList[$formattingElementIndex]['token']; |
|
|
|
} else { |
|
|
|
$formattingElement = null; |
|
|
|
} |
|
|
|
# If there is no such element, then return and instead act as |
|
|
|
# described in the "any other end tag" entry above. |
|
|
|
if (!$formattingElement) { |
|
|
|
// NOTE: The "entry above" refers to the "in body" insertion mode |
|
|
|
// Changes here should be mirrored there |
|
|
|
foreach ($this->stack as $node) { |
|
|
|
if ($node->nodeName === $token->name && $node->namespaceURI === null) { |
|
|
|
$this->stack->generateImpliedEndTags($token->name); |
|
|
|
if (!$node->isSameNode($this->stack->currentNode)) { |
|
|
|
$this->error($errorCode, $token->name); |
|
|
|
} |
|
|
|
$this->stack->popUntilSame($node); |
|
|
|
return; |
|
|
|
} elseif ($this->isElementSpecial($node)) { |
|
|
|
$this->error($errorCode, $token->name); |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
# If formatting element is not in the stack of open elements, |
|
|
|
# then this is a parse error; remove the element from the |
|
|
|
# list, and return. |
|
|
|
if (($stackIndex = $this->stack->findSame($formattingElement)) === -1) { |
|
|
|
$this->error($errorCode, $token->name); |
|
|
|
unset($this->activeFormattingElementsList[$formattingElementIndex]); |
|
|
|
return; |
|
|
|
} |
|
|
|
# If formatting element is in the stack of open elements, but |
|
|
|
# the element is not in scope, then this is a parse error; return. |
|
|
|
if (!$this->stack->hasElementInScope($formattingElement)) { |
|
|
|
$this->error($errorCode, $token->name); |
|
|
|
return; |
|
|
|
} |
|
|
|
# If formatting element is not the current node, this is a |
|
|
|
# parse error. (But do not return.) |
|
|
|
if (!$formattingElement->isSameNode($this->stack->currentNode)) { |
|
|
|
$this->error($errorCode, $token->name); |
|
|
|
} |
|
|
|
# Let furthest block be the topmost node in the stack of open elements that |
|
|
|
# is lower in the stack than formatting element, and is an element in the |
|
|
|
# special category. There might not be one. |
|
|
|
$furthestBlock = null; |
|
|
|
for ($k = ($stackIndex + 1); $k < count($this->stack); $k++) { |
|
|
|
if ($this->isElementSpecial($this->stack[$k])) { |
|
|
|
$furthestBlockIndex = $k; |
|
|
|
$furthestBlock = $this->stack[$k]; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
# If there is no furthest block, then the UA must first pop all the nodes |
|
|
|
# from the bottom of the stack of open elements, from the current node up |
|
|
|
# to and including formatting element, then remove formatting element from |
|
|
|
# the list of active formatting elements, and finally return. |
|
|
|
if (!$furthestBlock) { |
|
|
|
$this->stack->popUntilSame($formattingElement); |
|
|
|
$this->activeFormattingElementsList->removeSame($formattingElement); |
|
|
|
return; |
|
|
|
} |
|
|
|
# Let common ancestor be the element immediately above formatting element |
|
|
|
# in the stack of open elements. |
|
|
|
$commonAncestor = $this->stack[$stackIndex - 1] ?? null; |
|
|
|
# Let a bookmark note the position of formatting element in the list of |
|
|
|
# active formatting elements relative to the elements on either side |
|
|
|
# of it in the list. |
|
|
|
$bookmark = $formattingElementIndex; |
|
|
|
# Let node and last node be furthest block. Follow these steps: |
|
|
|
$node = $furthestBlock; |
|
|
|
$nodeIndex = $furthestBlockIndex; |
|
|
|
$lastNode = $furthestBlock; |
|
|
|
# Let inner loop counter be zero. |
|
|
|
$innerLoopCounter = 0; |
|
|
|
# Inner loop: Increment inner loop counter by one. |
|
|
|
InnerLoop: |
|
|
|
$innerLoopCounter++; |
|
|
|
# Let node be the element immediately above node in the stack of open |
|
|
|
# elements, or if node is no longer in the stack of open elements |
|
|
|
# (e.g. because it got removed by this algorithm), the element that |
|
|
|
# was immediately above node in the stack of open elements before |
|
|
|
# node was removed. |
|
|
|
$node = $this->stack[--$nodeIndex]; |
|
|
|
# If node is formatting element, then go to the next step in the |
|
|
|
# overall algorithm. |
|
|
|
if ($node->isSameNode($formattingElement)) { |
|
|
|
$nodeListPos = $formattingElementIndex; |
|
|
|
goto AfterInnerLoop; |
|
|
|
} |
|
|
|
# If inner loop counter is greater than three and node is in the |
|
|
|
# list of active formatting elements, then remove node from the |
|
|
|
# list of active formatting elements. |
|
|
|
$nodeListPos = $this->activeFormattingElementsList->findSame($node); |
|
|
|
if ($innerLoopCounter > 3 && $nodeListPos > -1) { |
|
|
|
$this->activeFormattingElementsList->removeSame($node); |
|
|
|
if ($bookmark > $nodeListPos) { |
|
|
|
$bookmark--; |
|
|
|
} |
|
|
|
$nodeListPos = -1; |
|
|
|
} |
|
|
|
# If node is not in the list of active formatting elements, then |
|
|
|
# remove node from the stack of open elements and then go back to |
|
|
|
# the step labeled inner loop. |
|
|
|
if ($nodeListPos === -1) { |
|
|
|
$this->stack->removeSame($node); |
|
|
|
goto InnerLoop; |
|
|
|
} |
|
|
|
# Create an element for the token for which the element node was |
|
|
|
# created, in the HTML namespace, with common ancestor as the |
|
|
|
# intended parent; replace the entry for node in the list of |
|
|
|
# active formatting elements with an entry for the new element, |
|
|
|
# replace the entry for node in the stack of open elements with |
|
|
|
# an entry for the new element, and let node be the new element. |
|
|
|
$nodeToken = $this->activeFormattingElementsList[$nodeListPos]['token']; |
|
|
|
$element = $this->createElementForToken($nodeToken, null, $commonAncestor); |
|
|
|
$this->activeFormattingElementsList[$nodeListPos] = ['token' => $nodeToken, 'element' => $element]; |
|
|
|
$this->stack[$nodeIndex] = $element; |
|
|
|
$node = $element; |
|
|
|
# If last node is furthest block, then move the aforementioned |
|
|
|
# bookmark to be immediately after the new node in the list of |
|
|
|
# active formatting elements. |
|
|
|
if ($lastNode->isSameNode($furthestBlock)) { |
|
|
|
$bookmark = $nodeListPos + 1; |
|
|
|
} |
|
|
|
# Insert last node into node, first removing it from its previous |
|
|
|
# parent node if any. |
|
|
|
if ($lastNode->parentNode) { |
|
|
|
$lastNode->parentNode->removeChild($lastNode); |
|
|
|
} |
|
|
|
$node->appendChild($lastNode); |
|
|
|
# Let last node be node. |
|
|
|
$lastNode = $node; |
|
|
|
# Return to the step labeled inner loop. |
|
|
|
goto InnerLoop; |
|
|
|
# Insert whatever last node ended up being in the previous step |
|
|
|
# at the appropriate place for inserting a node, but using |
|
|
|
# common ancestor as the override target. |
|
|
|
AfterInnerLoop: |
|
|
|
$place = $this->appropriatePlaceForInsertingNode($commonAncestor); |
|
|
|
if ($place['insert before']) { |
|
|
|
$place['node']->parentNode->insertBefore($lastNode, $place['node']); |
|
|
|
} else { |
|
|
|
$place['node']->appendChild($lastNode); |
|
|
|
} |
|
|
|
# Create an element for the token for which formatting element was |
|
|
|
# created, in the HTML namespace, with furthest block as the |
|
|
|
# intended parent. |
|
|
|
$element = $this->createElementForToken($formattingToken, null, $furthestBlock); |
|
|
|
# Take all of the child nodes of furthest block and append them to |
|
|
|
# the element created in the last step. |
|
|
|
while ($furthestBlock->hasChildNodes()) { |
|
|
|
$element->appendChild($furthestBlock->firstChild); |
|
|
|
} |
|
|
|
# Append that new element to furthest block. |
|
|
|
$furthestBlock->appendChild($element); |
|
|
|
# Remove formatting element from the list of active formatting |
|
|
|
# elements, and insert the new element into the list of active |
|
|
|
# formatting elements at the position of the aforementioned bookmark. |
|
|
|
$this->activeFormattingElementsList->insert($formattingToken, $element, $bookmark); |
|
|
|
$this->activeFormattingElementsList->removeSame($formattingElement); |
|
|
|
# Remove formatting element from the stack of open elements, and |
|
|
|
# insert the new element into the stack of open elements |
|
|
|
# immediately below the position of furthest block in that stack. |
|
|
|
assert($stackIndex > 0, new \Exception("Attempting to delete root element from stack")); |
|
|
|
$this->stack->removeSame($formattingElement); |
|
|
|
$this->stack->insert($element, $this->stack->findSame($furthestBlock) + 1); |
|
|
|
# Jump back to the step labeled outer loop. |
|
|
|
goto OuterLoop; |
|
|
|
} |
|
|
|
# Otherwise |
|
|
|
else { |
|
|
|
# Process the token according to the rules given in the section |
|
|
|
# for parsing tokens in foreign content. |
|
|
|
|
|
|
|
protected function parseTokenInForeignContent(Token $token): bool { |
|
|
|
assert((function() { |
|
|
|
$this->debugLog .= " Mode: Foreign content (".(string) $this->stack.")\n"; |
|
|
|
return true; |
|
|
@ -3729,7 +3497,7 @@ class TreeBuilder { |
|
|
|
# "in body" insertion mode. |
|
|
|
// DEVIATION: Spec bug |
|
|
|
// See https://github.com/whatwg/html/issues/6439 |
|
|
|
return $this->parseTokenInHTMLContent($token); |
|
|
|
goto ProcessToken; |
|
|
|
} |
|
|
|
# Any other start tag |
|
|
|
else { |
|
|
@ -3812,7 +3580,7 @@ class TreeBuilder { |
|
|
|
if (strtolower($this->stack->currentNodeName) !== $token->name) { |
|
|
|
$this->error(ParseError::UNEXPECTED_END_TAG, $token->name); |
|
|
|
} |
|
|
|
return true; |
|
|
|
continue 2; |
|
|
|
} |
|
|
|
# If node's tag name, converted to ASCII lowercase, is the same as the |
|
|
|
# tag name of the token, pop elements from the stack of open elements until node |
|
|
@ -3822,7 +3590,7 @@ class TreeBuilder { |
|
|
|
$this->error(ParseError::UNEXPECTED_END_TAG, $token->name); |
|
|
|
} |
|
|
|
$this->stack->popUntilSame($node); |
|
|
|
return true; |
|
|
|
continue 2; |
|
|
|
} |
|
|
|
# Set node to the previous entry in the stack of open elements. |
|
|
|
$node = $this->stack[--$pos]; |
|
|
@ -3831,9 +3599,229 @@ class TreeBuilder { |
|
|
|
} while ($node->namespaceURI !== null); |
|
|
|
# Otherwise, process the token according to the rules given in the section |
|
|
|
# corresponding to the current insertion mode in HTML content. |
|
|
|
return $this->parseTokenInHTMLContent($token, $this->insertionMode); |
|
|
|
goto ProcessToken; |
|
|
|
} |
|
|
|
} |
|
|
|
# When a start tag token is emitted with its self-closing flag set, if the flag |
|
|
|
# is not acknowledged when it is processed by the tree construction stage, that |
|
|
|
# is a non-void-html-element-start-tag-with-trailing-solidus parse error. |
|
|
|
if ($token instanceof StartTagToken && $token->selfClosing && !$token->selfClosingAcknowledged) { |
|
|
|
$this->error(ParseError::NON_VOID_HTML_ELEMENT_START_TAG_WITH_TRAILING_SOLIDUS, $token->name); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
protected function adopt(TagToken $token): void { |
|
|
|
# The adoption agency algorithm, which takes as its only argument a |
|
|
|
# token 'token' for which the algorithm is being run, consists of |
|
|
|
# the following steps: |
|
|
|
|
|
|
|
assert((function() { |
|
|
|
$this->debugLog .= " Adoption agency (".(string) $this->stack.")\n"; |
|
|
|
return true; |
|
|
|
})()); |
|
|
|
|
|
|
|
# Let subject be token's tag name. |
|
|
|
$subject = $token->name; |
|
|
|
$errorCode = $token instanceof StartTagToken ? ParseError::UNEXPECTED_START_TAG : ParseError::UNEXPECTED_END_TAG; |
|
|
|
# If the current node is an HTML element whose tag name is subject, |
|
|
|
# and the current node is not in the list of active formatting elements, |
|
|
|
# then pop the current node off the stack of open elements, and return. |
|
|
|
$currentNode = $this->stack->currentNode; |
|
|
|
if ( |
|
|
|
$currentNode->namespaceURI === null |
|
|
|
&& $currentNode->nodeName === $subject |
|
|
|
&& $this->activeFormattingElementsList->findSame($currentNode) === -1 |
|
|
|
) { |
|
|
|
$this->stack->pop(); |
|
|
|
return; |
|
|
|
} |
|
|
|
# Let outer loop counter be zero. |
|
|
|
$outerLoopCounter = 0; |
|
|
|
# Outer loop: If outer loop counter is greater than or equal to eight, then return. |
|
|
|
OuterLoop: |
|
|
|
if ($outerLoopCounter >= 8) { |
|
|
|
return; |
|
|
|
} |
|
|
|
# Increment outer loop counter by one. |
|
|
|
$outerLoopCounter++; |
|
|
|
# Let formatting element be the last element in the list of active |
|
|
|
# formatting elements that: |
|
|
|
# 1. is between the end of the list and the last marker in the list, |
|
|
|
# if any, or the start of the list otherwise, and |
|
|
|
# 2. has the tag name subject. |
|
|
|
$formattingElementIndex = $this->activeFormattingElementsList->findToMarker($subject); |
|
|
|
if ($formattingElementIndex > -1) { |
|
|
|
$formattingElement = $this->activeFormattingElementsList[$formattingElementIndex]['element']; |
|
|
|
$formattingToken = $this->activeFormattingElementsList[$formattingElementIndex]['token']; |
|
|
|
} else { |
|
|
|
$formattingElement = null; |
|
|
|
} |
|
|
|
# If there is no such element, then return and instead act as |
|
|
|
# described in the "any other end tag" entry above. |
|
|
|
if (!$formattingElement) { |
|
|
|
// NOTE: The "entry above" refers to the "in body" insertion mode |
|
|
|
// Changes here should be mirrored there |
|
|
|
foreach ($this->stack as $node) { |
|
|
|
if ($node->nodeName === $token->name && $node->namespaceURI === null) { |
|
|
|
$this->stack->generateImpliedEndTags($token->name); |
|
|
|
if (!$node->isSameNode($this->stack->currentNode)) { |
|
|
|
$this->error($errorCode, $token->name); |
|
|
|
} |
|
|
|
$this->stack->popUntilSame($node); |
|
|
|
return; |
|
|
|
} elseif ($this->isElementSpecial($node)) { |
|
|
|
$this->error($errorCode, $token->name); |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
# If formatting element is not in the stack of open elements, |
|
|
|
# then this is a parse error; remove the element from the |
|
|
|
# list, and return. |
|
|
|
if (($stackIndex = $this->stack->findSame($formattingElement)) === -1) { |
|
|
|
$this->error($errorCode, $token->name); |
|
|
|
unset($this->activeFormattingElementsList[$formattingElementIndex]); |
|
|
|
return; |
|
|
|
} |
|
|
|
# If formatting element is in the stack of open elements, but |
|
|
|
# the element is not in scope, then this is a parse error; return. |
|
|
|
if (!$this->stack->hasElementInScope($formattingElement)) { |
|
|
|
$this->error($errorCode, $token->name); |
|
|
|
return; |
|
|
|
} |
|
|
|
# If formatting element is not the current node, this is a |
|
|
|
# parse error. (But do not return.) |
|
|
|
if (!$formattingElement->isSameNode($this->stack->currentNode)) { |
|
|
|
$this->error($errorCode, $token->name); |
|
|
|
} |
|
|
|
# Let furthest block be the topmost node in the stack of open elements that |
|
|
|
# is lower in the stack than formatting element, and is an element in the |
|
|
|
# special category. There might not be one. |
|
|
|
$furthestBlock = null; |
|
|
|
for ($k = ($stackIndex + 1); $k < count($this->stack); $k++) { |
|
|
|
if ($this->isElementSpecial($this->stack[$k])) { |
|
|
|
$furthestBlockIndex = $k; |
|
|
|
$furthestBlock = $this->stack[$k]; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
# If there is no furthest block, then the UA must first pop all the nodes |
|
|
|
# from the bottom of the stack of open elements, from the current node up |
|
|
|
# to and including formatting element, then remove formatting element from |
|
|
|
# the list of active formatting elements, and finally return. |
|
|
|
if (!$furthestBlock) { |
|
|
|
$this->stack->popUntilSame($formattingElement); |
|
|
|
$this->activeFormattingElementsList->removeSame($formattingElement); |
|
|
|
return; |
|
|
|
} |
|
|
|
# Let common ancestor be the element immediately above formatting element |
|
|
|
# in the stack of open elements. |
|
|
|
$commonAncestor = $this->stack[$stackIndex - 1] ?? null; |
|
|
|
# Let a bookmark note the position of formatting element in the list of |
|
|
|
# active formatting elements relative to the elements on either side |
|
|
|
# of it in the list. |
|
|
|
$bookmark = $formattingElementIndex; |
|
|
|
# Let node and last node be furthest block. Follow these steps: |
|
|
|
$node = $furthestBlock; |
|
|
|
$nodeIndex = $furthestBlockIndex; |
|
|
|
$lastNode = $furthestBlock; |
|
|
|
# Let inner loop counter be zero. |
|
|
|
$innerLoopCounter = 0; |
|
|
|
# Inner loop: Increment inner loop counter by one. |
|
|
|
InnerLoop: |
|
|
|
$innerLoopCounter++; |
|
|
|
# Let node be the element immediately above node in the stack of open |
|
|
|
# elements, or if node is no longer in the stack of open elements |
|
|
|
# (e.g. because it got removed by this algorithm), the element that |
|
|
|
# was immediately above node in the stack of open elements before |
|
|
|
# node was removed. |
|
|
|
$node = $this->stack[--$nodeIndex]; |
|
|
|
# If node is formatting element, then go to the next step in the |
|
|
|
# overall algorithm. |
|
|
|
if ($node->isSameNode($formattingElement)) { |
|
|
|
$nodeListPos = $formattingElementIndex; |
|
|
|
goto AfterInnerLoop; |
|
|
|
} |
|
|
|
# If inner loop counter is greater than three and node is in the |
|
|
|
# list of active formatting elements, then remove node from the |
|
|
|
# list of active formatting elements. |
|
|
|
$nodeListPos = $this->activeFormattingElementsList->findSame($node); |
|
|
|
if ($innerLoopCounter > 3 && $nodeListPos > -1) { |
|
|
|
$this->activeFormattingElementsList->removeSame($node); |
|
|
|
if ($bookmark > $nodeListPos) { |
|
|
|
$bookmark--; |
|
|
|
} |
|
|
|
$nodeListPos = -1; |
|
|
|
} |
|
|
|
# If node is not in the list of active formatting elements, then |
|
|
|
# remove node from the stack of open elements and then go back to |
|
|
|
# the step labeled inner loop. |
|
|
|
if ($nodeListPos === -1) { |
|
|
|
$this->stack->removeSame($node); |
|
|
|
goto InnerLoop; |
|
|
|
} |
|
|
|
# Create an element for the token for which the element node was |
|
|
|
# created, in the HTML namespace, with common ancestor as the |
|
|
|
# intended parent; replace the entry for node in the list of |
|
|
|
# active formatting elements with an entry for the new element, |
|
|
|
# replace the entry for node in the stack of open elements with |
|
|
|
# an entry for the new element, and let node be the new element. |
|
|
|
$nodeToken = $this->activeFormattingElementsList[$nodeListPos]['token']; |
|
|
|
$element = $this->createElementForToken($nodeToken, null, $commonAncestor); |
|
|
|
$this->activeFormattingElementsList[$nodeListPos] = ['token' => $nodeToken, 'element' => $element]; |
|
|
|
$this->stack[$nodeIndex] = $element; |
|
|
|
$node = $element; |
|
|
|
# If last node is furthest block, then move the aforementioned |
|
|
|
# bookmark to be immediately after the new node in the list of |
|
|
|
# active formatting elements. |
|
|
|
if ($lastNode->isSameNode($furthestBlock)) { |
|
|
|
$bookmark = $nodeListPos + 1; |
|
|
|
} |
|
|
|
# Insert last node into node, first removing it from its previous |
|
|
|
# parent node if any. |
|
|
|
if ($lastNode->parentNode) { |
|
|
|
$lastNode->parentNode->removeChild($lastNode); |
|
|
|
} |
|
|
|
$node->appendChild($lastNode); |
|
|
|
# Let last node be node. |
|
|
|
$lastNode = $node; |
|
|
|
# Return to the step labeled inner loop. |
|
|
|
goto InnerLoop; |
|
|
|
# Insert whatever last node ended up being in the previous step |
|
|
|
# at the appropriate place for inserting a node, but using |
|
|
|
# common ancestor as the override target. |
|
|
|
AfterInnerLoop: |
|
|
|
$place = $this->appropriatePlaceForInsertingNode($commonAncestor); |
|
|
|
if ($place['insert before']) { |
|
|
|
$place['node']->parentNode->insertBefore($lastNode, $place['node']); |
|
|
|
} else { |
|
|
|
$place['node']->appendChild($lastNode); |
|
|
|
} |
|
|
|
# Create an element for the token for which formatting element was |
|
|
|
# created, in the HTML namespace, with furthest block as the |
|
|
|
# intended parent. |
|
|
|
$element = $this->createElementForToken($formattingToken, null, $furthestBlock); |
|
|
|
# Take all of the child nodes of furthest block and append them to |
|
|
|
# the element created in the last step. |
|
|
|
while ($furthestBlock->hasChildNodes()) { |
|
|
|
$element->appendChild($furthestBlock->firstChild); |
|
|
|
} |
|
|
|
# Append that new element to furthest block. |
|
|
|
$furthestBlock->appendChild($element); |
|
|
|
# Remove formatting element from the list of active formatting |
|
|
|
# elements, and insert the new element into the list of active |
|
|
|
# formatting elements at the position of the aforementioned bookmark. |
|
|
|
$this->activeFormattingElementsList->insert($formattingToken, $element, $bookmark); |
|
|
|
$this->activeFormattingElementsList->removeSame($formattingElement); |
|
|
|
# Remove formatting element from the stack of open elements, and |
|
|
|
# insert the new element into the stack of open elements |
|
|
|
# immediately below the position of furthest block in that stack. |
|
|
|
assert($stackIndex > 0, new \Exception("Attempting to delete root element from stack")); |
|
|
|
$this->stack->removeSame($formattingElement); |
|
|
|
$this->stack->insert($element, $this->stack->findSame($furthestBlock) + 1); |
|
|
|
# Jump back to the step labeled outer loop. |
|
|
|
goto OuterLoop; |
|
|
|
} |
|
|
|
|
|
|
|
protected function appropriatePlaceForInsertingNode(\DOMNode $overrideTarget = null): array { |
|
|
|