protected function processEndTag(Tag $tag) { $tagName = $tag->getName(); if (empty($this->cntOpen[$tagName])) { return; } $closeTags = array(); $i = \count($this->openTags); while (--$i >= 0) { $openTag = $this->openTags[$i]; if ($tag->canClose($openTag)) { break; } $closeTags[] = $openTag; ++$this->currentFixingCost; } if ($i < 0) { $this->logger->debug('Skipping end tag with no start tag', array('tag' => $tag)); return; } $keepReopening = (bool) ($this->currentFixingCost < $this->maxFixingCost); $reopenTags = array(); foreach ($closeTags as $openTag) { $openTagName = $openTag->getName(); if ($keepReopening) { if ($openTag->getFlags() & self::RULE_AUTO_REOPEN) { $reopenTags[] = $openTag; } else { $keepReopening = \false; } } $tagPos = $tag->getPos(); if ($openTag->getFlags() & self::RULE_IGNORE_WHITESPACE) { $tagPos = $this->getMagicPos($tagPos); } $endTag = new Tag(Tag::END_TAG, $openTagName, $tagPos, 0); $endTag->setFlags($openTag->getFlags()); $this->outputTag($endTag); $this->popContext(); } $this->outputTag($tag); $this->popContext(); if (!empty($closeTags) && $this->currentFixingCost < $this->maxFixingCost) { $ignorePos = $this->pos; $i = \count($this->tagStack); while (--$i >= 0 && ++$this->currentFixingCost < $this->maxFixingCost) { $upcomingTag = $this->tagStack[$i]; if ($upcomingTag->getPos() > $ignorePos || $upcomingTag->isStartTag()) { break; } $j = \count($closeTags); while (--$j >= 0 && ++$this->currentFixingCost < $this->maxFixingCost) { if ($upcomingTag->canClose($closeTags[$j])) { \array_splice($closeTags, $j, 1); if (isset($reopenTags[$j])) { \array_splice($reopenTags, $j, 1); } $ignorePos = \max($ignorePos, $upcomingTag->getPos() + $upcomingTag->getLen()); break; } } } if ($ignorePos > $this->pos) { $this->outputIgnoreTag(new Tag(Tag::SELF_CLOSING_TAG, 'i', $this->pos, $ignorePos - $this->pos)); } } foreach ($reopenTags as $startTag) { $newTag = $this->addCopyTag($startTag, $this->pos, 0); $endTag = $startTag->getEndTag(); if ($endTag) { $newTag->pairWith($endTag); } } }
/** * @testdox canClose() returns TRUE if the tags are paired together */ public function testCanClosePaired() { $startTag = new Tag(Tag::START_TAG, 'X', 0, 0); $endTag = new Tag(Tag::END_TAG, 'X', 1, 0); $endTag->pairWith($startTag); $this->assertTrue($endTag->canClose($startTag)); }
/** * Process given end tag at current position * * @param Tag $tag end tag * @return void */ protected function processEndTag(Tag $tag) { $tagName = $tag->getName(); if (empty($this->cntOpen[$tagName])) { // This is an end tag with no start tag return; } /** * @var array List of tags need to be closed before given tag */ $closeTags = []; // Iterate through all open tags from last to first to find a match for our tag $i = count($this->openTags); while (--$i >= 0) { $openTag = $this->openTags[$i]; if ($tag->canClose($openTag)) { break; } $closeTags[] = $openTag; ++$this->currentFixingCost; } if ($i < 0) { // Did not find a matching tag $this->logger->debug('Skipping end tag with no start tag', ['tag' => $tag]); return; } // Only reopen tags if we haven't exceeded our "fixing" budget $keepReopening = (bool) ($this->currentFixingCost < $this->maxFixingCost); // Iterate over tags that are being closed, output their end tag and collect tags to be // reopened $reopenTags = []; foreach ($closeTags as $openTag) { $openTagName = $openTag->getName(); // Test whether this tag should be reopened automatically if ($keepReopening) { if ($openTag->getFlags() & self::RULE_AUTO_REOPEN) { $reopenTags[] = $openTag; } else { $keepReopening = false; } } // Find the earliest position we can close this open tag $tagPos = $tag->getPos(); if ($openTag->getFlags() & self::RULE_IGNORE_WHITESPACE) { $tagPos = $this->getMagicPos($tagPos); } // Output an end tag to close this start tag, then update the context $endTag = new Tag(Tag::END_TAG, $openTagName, $tagPos, 0); $endTag->setFlags($openTag->getFlags()); $this->outputTag($endTag); $this->popContext(); } // Output our tag, moving the cursor past it, then update the context $this->outputTag($tag); $this->popContext(); // If our fixing budget allows it, peek at upcoming tags and remove end tags that would // close tags that are already being closed now. Also, filter our list of tags being // reopened by removing those that would immediately be closed if (!empty($closeTags) && $this->currentFixingCost < $this->maxFixingCost) { /** * @var integer Rightmost position of the portion of text to ignore */ $ignorePos = $this->pos; $i = count($this->tagStack); while (--$i >= 0 && ++$this->currentFixingCost < $this->maxFixingCost) { $upcomingTag = $this->tagStack[$i]; // Test whether the upcoming tag is positioned at current "ignore" position and it's // strictly an end tag (not a start tag or a self-closing tag) if ($upcomingTag->getPos() > $ignorePos || $upcomingTag->isStartTag()) { break; } // Test whether this tag would close any of the tags we're about to reopen $j = count($closeTags); while (--$j >= 0 && ++$this->currentFixingCost < $this->maxFixingCost) { if ($upcomingTag->canClose($closeTags[$j])) { // Remove the tag from the lists and reset the keys array_splice($closeTags, $j, 1); if (isset($reopenTags[$j])) { array_splice($reopenTags, $j, 1); } // Extend the ignored text to cover this tag $ignorePos = max($ignorePos, $upcomingTag->getPos() + $upcomingTag->getLen()); break; } } } if ($ignorePos > $this->pos) { /** * @todo have a method that takes (pos,len) rather than a Tag */ $this->outputIgnoreTag(new Tag(Tag::SELF_CLOSING_TAG, 'i', $this->pos, $ignorePos - $this->pos)); } } // Re-add tags that need to be reopened, at current cursor position foreach ($reopenTags as $startTag) { $newTag = $this->addCopyTag($startTag, $this->pos, 0); // Re-pair the new tag $endTag = $startTag->getEndTag(); if ($endTag) { $newTag->pairWith($endTag); } } }