/** * Capture all attributes in given string * * @param Tag $tag Target tag * @param string $elName Name of the HTML element * @param string $str String containing the attribute declarations * @return void */ protected function captureAttributes(Tag $tag, $elName, $str) { preg_match_all('/[a-z][-a-z0-9]*(?>\\s*=\\s*(?>"[^"]*"|\'[^\']*\'|[^\\s"\'=<>`]+))?/i', $str, $attrMatches); foreach ($attrMatches[0] as $attrMatch) { $pos = strpos($attrMatch, '='); /** * If there's no equal sign, it's a boolean attribute and we generate a value equal * to the attribute's name, lowercased * * @link http://www.w3.org/html/wg/drafts/html/master/single-page.html#boolean-attributes */ if ($pos === false) { $pos = strlen($attrMatch); $attrMatch .= '=' . strtolower($attrMatch); } // Normalize the attribute name, remove the whitespace around its value to account // for cases like <b title = "foo"/> $attrName = strtolower(trim(substr($attrMatch, 0, $pos))); $attrValue = trim(substr($attrMatch, 1 + $pos)); // Use the attribute's alias if applicable if (isset($this->config['aliases'][$elName][$attrName])) { $attrName = $this->config['aliases'][$elName][$attrName]; } // Remove quotes around the value if ($attrValue[0] === '"' || $attrValue[0] === "'") { $attrValue = substr($attrValue, 1, -1); } $tag->setAttribute($attrName, html_entity_decode($attrValue, ENT_QUOTES, 'UTF-8')); } }
/** * @testdox The [MEDIA] tag transfers its priority to the tag it creates */ public function testTagPriority() { $newTag = $this->getMockBuilder('s9e\\TextFormatter\\Parser\\Tag')->disableOriginalConstructor()->setMethods(['setAttributes', 'setSortPriority'])->getMock(); $newTag->expects($this->once())->method('setSortPriority')->with(123); $tagStack = $this->getMockBuilder('s9e\\TextFormatter\\Parser')->disableOriginalConstructor()->setMethods(['addSelfClosingTag'])->getMock(); $tagStack->expects($this->once())->method('addSelfClosingTag')->will($this->returnValue($newTag)); $tag = new Tag(Tag::START_TAG, 'MEDIA', 0, 0); $tag->setAttribute('media', 'foo'); $tag->setSortPriority(123); Parser::filterTag($tag, $tagStack, ['foo.invalid' => 'foo']); }
protected function captureAttributes(Tag $tag, $elName, $str) { \preg_match_all('/[a-z][-a-z0-9]*(?>\\s*=\\s*(?>"[^"]*"|\'[^\']*\'|[^\\s"\'=<>`]+))?/i', $str, $attrMatches); foreach ($attrMatches[0] as $attrMatch) { $pos = \strpos($attrMatch, '='); if ($pos === \false) { $pos = \strlen($attrMatch); $attrMatch .= '=' . \strtolower($attrMatch); } $attrName = \strtolower(\trim(\substr($attrMatch, 0, $pos))); $attrValue = \trim(\substr($attrMatch, 1 + $pos)); if (isset($this->config['aliases'][$elName][$attrName])) { $attrName = $this->config['aliases'][$elName][$attrName]; } if ($attrValue[0] === '"' || $attrValue[0] === "'") { $attrValue = \substr($attrValue, 1, -1); } $tag->setAttribute($attrName, \html_entity_decode($attrValue, \ENT_QUOTES, 'UTF-8')); } }
protected function setLinkAttributes(Tag $tag, $linkInfo, $attrName) { $url = $linkInfo; $title = ''; $pos = \strpos($linkInfo, ' '); if ($pos !== \false) { $url = \substr($linkInfo, 0, $pos); $title = \substr(\trim(\substr($linkInfo, $pos)), 1, -1); } $tag->setAttribute($attrName, $this->decode($url)); if ($title > '') { $tag->setAttribute('title', $this->decode($title)); } }
/** * Truncate the replacement text set in a LINK_TEXT tag * * @param \s9e\TextFormatter\Parser\Tag $tag LINK_TEXT tag * @return bool Always true to indicate that the tag is valid */ public function truncate_text(\s9e\TextFormatter\Parser\Tag $tag) { $text = $tag->getAttribute('text'); if (utf8_strlen($text) > 55) { $text = utf8_substr($text, 0, 39) . ' ... ' . utf8_substr($text, -10); } $tag->setAttribute('text', $text); return true; }
protected static function tagIsMissingAnyAttribute(Tag $tag, array $attrNames) { foreach ($attrNames as $attrName) { if (!$tag->hasAttribute($attrName)) { return \true; } } return \false; }
/** * @testdox filterAttributes() calls the logger's setAttribute() and unsetAttribute() methods for each attribute with a filterChain */ public function testFilterAttributesCallsLoggerSetAttribute() { $logger = $this->getMock('s9e\\TextFormatter\\Parser\\Logger', ['setAttribute', 'unsetAttribute']); $logger->expects($this->at(0))->method('setAttribute')->with('foo'); $logger->expects($this->at(2))->method('setAttribute')->with('bar'); $logger->expects($this->exactly(2))->method('unsetAttribute'); $tagConfig = new TagConfig(); $tagConfig->attributes->add('foo')->filterChain->append(function () { }); $tagConfig->attributes->add('bar')->filterChain->append(function () { }); $tag = new Tag(Tag::SELF_CLOSING_TAG, 'X', 0, 0); $tag->setAttribute('foo', 'foo'); $tag->setAttribute('bar', 'bar'); Parser::filterAttributes($tag, $tagConfig->asConfig(), [], $logger); }
/** * Set a URL or IMG tag's attributes * * @param Tag $tag URL or IMG tag * @param array $m Regexp captures * @param string[] $attrNames List of attribute names * @return void */ protected function setLinkAttributes(Tag $tag, array $m, array $attrNames) { if (isset($m[3])) { $attrValues = $this->getInlineLinkAttributes($m); } else { $label = isset($m[2]) ? $m[2][0] : $m[1][0]; $attrValues = $this->getReferenceLinkAttributes($label); } foreach ($attrValues as $k => $attrValue) { $tag->setAttribute($attrNames[$k], $attrValue); } }
/** * @testdox gc() removes a paired tag's reference */ public function testGc() { $startTag = new Tag(Tag::START_TAG, 'X', 0, 0); $endTag = new Tag(Tag::END_TAG, 'X', 1, 0); $startTag->pairWith($endTag); $this->assertSame($endTag, $startTag->getEndTag()); $this->assertSame($startTag, $endTag->getStartTag()); $startTag->gc(); $this->assertNull($startTag->getEndTag()); $endTag->gc(); $this->assertNull($endTag->getStartTag()); }
protected static function compareTags(Tag $a, Tag $b) { $aPos = $a->getPos(); $bPos = $b->getPos(); if ($aPos !== $bPos) { return $bPos - $aPos; } if ($a->getSortPriority() !== $b->getSortPriority()) { return $b->getSortPriority() - $a->getSortPriority(); } $aLen = $a->getLen(); $bLen = $b->getLen(); if (!$aLen || !$bLen) { if (!$aLen && !$bLen) { $order = array(Tag::END_TAG => 0, Tag::SELF_CLOSING_TAG => 1, Tag::START_TAG => 2); return $order[$b->getType()] - $order[$a->getType()]; } return $aLen ? -1 : 1; } return $aLen - $bLen; }
public function getData() { return [['foo bar', '<r><X><Y>foo</Y> <Z>bar</Z></X></r>', function ($configurator) { $configurator->tags->add('X'); $configurator->tags->add('Y'); $configurator->tags->add('Z'); }, function ($parser) { $parser->addStartTag('X', 0, 0); $parser->addSelfClosingTag('Y', 0, 3); $parser->addSelfClosingTag('Z', 4, 3); $parser->addEndTag('X', 7, 0); }], ['foo bar', '<r>foo <X>bar</X></r>', function ($configurator) { $configurator->tags->add('X'); }, function ($parser) { $parser->addStartTag('X', 4, 0); }], ['foo bar', '<t>foo bar</t>', null, function ($parser) { $parser->addStartTag('X', 4, 0); }], ['foo bar', '<r><X>fo<Y>o</Y></X> bar</r>', function ($configurator) { $configurator->tags->add('X'); $configurator->tags->add('Y'); }, function ($parser) { $parser->addStartTag('X', 0, 0); $parser->addEndTag('X', 3, 0); $parser->addStartTag('Y', 2, 0); $parser->addEndTag('Y', 4, 1); }], ['foo bar', '<r><X>fo<Y>o</Y></X><Y> b</Y>ar</r>', function ($configurator) { $configurator->tags->add('X'); $configurator->tags->add('Y')->rules->autoReopen(); }, function ($parser) { $parser->addStartTag('X', 0, 0); $parser->addEndTag('X', 3, 0); $parser->addStartTag('Y', 2, 0); $parser->addEndTag('Y', 5, 0); }], ['foo bar', '<r><X>fo<Y attr="foo">o</Y></X><Y attr="foo"> b</Y>ar</r>', function ($configurator) { $configurator->tags->add('X'); $tag = $configurator->tags->add('Y'); $tag->attributes->add('attr')->required = false; $tag->rules->autoReopen(); }, function ($parser) { $parser->addStartTag('X', 0, 0); $parser->addEndTag('X', 3, 0); $parser->addStartTag('Y', 2, 0)->setAttribute('attr', 'foo'); $parser->addEndTag('Y', 5, 0); }], ['x [b][i]...[/b][/i] y', '<r>x <B><s>[b]</s><I><s>[i]</s>...</I><e>[/b]</e></B><i>[/i]</i> y</r>', function ($configurator) { $configurator->tags->add('B'); $configurator->tags->add('I')->rules->autoReopen(); }, function ($parser) { $parser->addStartTag('B', 2, 3); $parser->addStartTag('I', 5, 3); $parser->addEndTag('B', 11, 4); $parser->addEndTag('I', 15, 4); }], ['x [b][i]...[/b]![/i] y', '<r>x <B><s>[b]</s><I><s>[i]</s>...</I><e>[/b]</e></B><I>!<e>[/i]</e></I> y</r>', function ($configurator) { $configurator->tags->add('B'); $configurator->tags->add('I')->rules->autoReopen(); }, function ($parser) { $parser->addStartTag('B', 2, 3); $parser->addStartTag('I', 5, 3); $parser->addEndTag('B', 11, 4); $parser->addEndTag('I', 16, 4); }], ['x [b][i][u]...[/b][/u][/i] y', '<r>x <B><s>[b]</s><I><s>[i]</s><U><s>[u]</s>...</U></I><e>[/b]</e></B><i>[/u][/i]</i> y</r>', function ($configurator) { $configurator->tags->add('B'); $configurator->tags->add('I')->rules->autoReopen(); $configurator->tags->add('U')->rules->autoReopen(); }, function ($parser) { $parser->addStartTag('B', 2, 3); $parser->addStartTag('I', 5, 3); $parser->addStartTag('U', 8, 3); $parser->addEndTag('B', 14, 4); $parser->addEndTag('U', 18, 4); $parser->addEndTag('I', 22, 4); }], ['x [b][i][u]...[/b][/u][/i] y', '<r>x <B><s>[b]</s><I><s>[i]</s><U><s>[u]</s>...</U></I><e>[/b]</e></B><i>[/u][/i]</i> y</r>', function ($configurator) { $configurator->tags->add('B'); $configurator->tags->add('I'); $configurator->tags->add('U'); }, function ($parser) { $parser->addStartTag('B', 2, 3); $parser->addStartTag('I', 5, 3); $parser->addStartTag('U', 8, 3); $parser->addEndTag('B', 14, 4); $parser->addEndTag('U', 18, 4); $parser->addEndTag('I', 22, 4); }], ['x [b][i][u]...[/b][/i][/u] y', '<r>x <B><s>[b]</s><I><s>[i]</s><U><s>[u]</s>...</U></I><e>[/b]</e></B><i>[/i][/u]</i> y</r>', function ($configurator) { $configurator->tags->add('B'); $configurator->tags->add('I')->rules->autoReopen(); $configurator->tags->add('U')->rules->autoReopen(); }, function ($parser) { $parser->addStartTag('B', 2, 3); $parser->addStartTag('I', 5, 3); $parser->addStartTag('U', 8, 3); $parser->addEndTag('B', 14, 4); $parser->addEndTag('I', 18, 4); $parser->addEndTag('U', 22, 4); }], ['x [b][i][u]...[/b][/i][/u] y', '<r>x <B><s>[b]</s><I><s>[i]</s><U><s>[u]</s>...</U></I><e>[/b]</e></B>[/i][/u] y</r>', function ($configurator) { $configurator->tags->add('B'); $configurator->tags->add('I')->rules->autoReopen(); $configurator->tags->add('U')->rules->autoReopen(); }, function ($parser) { // Set maxFixingCost to 2 so that it allows [u] and [i] to be closed, without // spending any efforts on reopening them $parser->maxFixingCost = 2; $parser->addStartTag('B', 2, 3); $parser->addStartTag('I', 5, 3); $parser->addStartTag('U', 8, 3); $parser->addEndTag('B', 14, 4); $parser->addEndTag('I', 18, 4); $parser->addEndTag('U', 22, 4); }], ['x [b][i][u]...[/b][/i]u[/u] y', '<r>x <B><s>[b]</s><I><s>[i]</s><U><s>[u]</s>...</U></I><e>[/b]</e></B><i>[/i]</i><U>u<e>[/u]</e></U> y</r>', function ($configurator) { $configurator->tags->add('B'); $configurator->tags->add('I')->rules->autoReopen(); $configurator->tags->add('U')->rules->autoReopen(); }, function ($parser) { $parser->addStartTag('B', 2, 3); $parser->addStartTag('I', 5, 3); $parser->addStartTag('U', 8, 3); $parser->addEndTag('B', 14, 4); $parser->addEndTag('I', 18, 4); $parser->addEndTag('U', 23, 4); }], ['x [i][b][u]...[/b][/i][/u] y', '<r>x <I><s>[i]</s><B><s>[b]</s><U><s>[u]</s>...</U><e>[/b]</e></B><e>[/i]</e></I><i>[/u]</i> y</r>', function ($configurator) { $configurator->tags->add('B')->rules->autoReopen(); $configurator->tags->add('I')->rules->autoReopen(); $configurator->tags->add('U')->rules->autoReopen(); }, function ($parser) { $parser->addStartTag('I', 2, 3); $parser->addStartTag('B', 5, 3); $parser->addStartTag('U', 8, 3); $parser->addEndTag('B', 14, 4); $parser->addEndTag('I', 18, 4); $parser->addEndTag('U', 22, 4); }], ['foo bar', '<r>foo <X>bar</X></r>', function ($configurator) { $configurator->tags->add('X'); $configurator->tags->add('Y'); }, function ($parser) { $parser->addStartTag('X', 4, 0); $parser->addEndTag('Y', 5, 0); }], ['foo bar', '<r><X>foo</X> bar</r>', function ($configurator) { $configurator->tags->add('X'); }, function ($parser) { $parser->addSelfClosingTag('X', 0, 3); $parser->addSelfClosingTag('X', 0, 3); $parser->addSelfClosingTag('X', 1, 1); }], ['fooo bar', '<r><X>f<X>oo</X>o</X> bar</r>', function ($configurator) { $configurator->tags->add('X')->nestingLimit = 2; }, function ($parser) { $parser->addStartTag('X', 0, 0); $parser->addStartTag('X', 1, 0); $parser->addSelfClosingTag('X', 2, 1); $parser->addEndTag('X', 3, 0); $parser->addEndTag('X', 4, 0); }, [['err', 'Nesting limit exceeded', ['tag' => $this->runClosure(function () { $tag = new Tag(Tag::SELF_CLOSING_TAG, 'X', 2, 1); $tag->invalidate(); return $tag; }), 'tagName' => 'X', 'nestingLimit' => 2]]]], ['foo bar', '<r><X>f</X><X>o</X>o bar</r>', function ($configurator) { $configurator->tags->add('X')->tagLimit = 2; }, function ($parser) { $parser->addSelfClosingTag('X', 0, 1); $parser->addSelfClosingTag('X', 1, 1); $parser->addSelfClosingTag('X', 2, 1); }, [['err', 'Tag limit exceeded', ['tag' => $this->runClosure(function () { $tag = new Tag(Tag::SELF_CLOSING_TAG, 'X', 2, 1); $tag->invalidate(); return $tag; }), 'tagName' => 'X', 'tagLimit' => 2]]]], ['foo bar', '<r><X>foo</X> bar</r>', function ($configurator) { $configurator->tags->add('X'); $configurator->tags->add('Y'); }, function ($parser) { $parser->addSelfClosingTag('X', 0, 3); $parser->addSelfClosingTag('Y', 1, 1)->cascadeInvalidationTo($parser->addSelfClosingTag('Y', 5, 1)); }], ["[pre]foo[b]x\ny[/b]bar[/pre]a\nb", "<r><PRE><s>[pre]</s>foo<B><s>[b]</s>x<br/>\ny<e>[/b]</e></B>bar<e>[/pre]</e></PRE>a<br/>\nb</r>", function ($configurator) { $configurator->rootRules->enableAutoLineBreaks(); $configurator->tags->add('PRE')->rules->suspendAutoLineBreaks(); $configurator->tags->add('B'); }, function ($parser) { $parser->addStartTag('PRE', 0, 5); $parser->addEndTag('PRE', 21, 6); $parser->addStartTag('B', 8, 3); $parser->addEndTag('B', 14, 4); }], ["[pre]foo[b]x\ny[/b]bar[/pre]a\nb", "<r><PRE><s>[pre]</s>foo<B><s>[b]</s>x\ny<e>[/b]</e></B>bar<e>[/pre]</e></PRE>a<br/>\nb</r>", function ($configurator) { $configurator->rootRules->enableAutoLineBreaks(); $configurator->tags->add('PRE')->rules->disableAutoLineBreaks(); $configurator->tags->add('B'); }, function ($parser) { $parser->addStartTag('PRE', 0, 5); $parser->addEndTag('PRE', 21, 6); $parser->addStartTag('B', 8, 3); $parser->addEndTag('B', 14, 4); }], ['foo bar', '<r><X>foo</X> bar</r>', function ($configurator) { $configurator->tags->add('X'); }, function ($parser) { $parser->addSelfClosingTag('X', 0, 3); $parser->addIgnoreTag(2, 1); }], ['foo bar', '<r><X>foo</X><i> b</i>ar</r>', function ($configurator) { $configurator->tags->add('X'); }, function ($parser) { $parser->addSelfClosingTag('X', 0, 3); $parser->addIgnoreTag(2, 3); }], ['foo bar', '<r>foo <X>bar</X></r>', function ($configurator) { $configurator->tags->add('X'); }, function ($parser) { $parser->addSelfClosingTag('X', 4, 3); }], ['foo bar', '<t>foo bar</t>', function ($configurator) { $configurator->tags->add('X'); }, function ($parser) { $parser->addSelfClosingTag('X', 4, 4); }], ['foo bar', '<r><X>foo bar</X></r>', function ($configurator) { $configurator->tags->add('X'); }, function ($parser) { $parser->addSelfClosingTag('X', 0, 7); }], ['foo bar', '<t>foo bar</t>', function ($configurator) { $configurator->tags->add('X'); }, function ($parser) { $parser->addSelfClosingTag('X', 0, 8); }], ['*foo* bar', '<r><X><s>*</s>foo<e>*</e></X> bar</r>', function ($configurator) { $configurator->tags->add('X'); }, function ($parser) { $parser->addStartTag('X', 0, 1)->pairWith($parser->addEndTag('X', 4, 1)); }], ['*foo* bar', '<r><X><s>*</s>foo* bar</X></r>', function ($configurator) { $configurator->tags->add('X'); }, function ($parser) { $parser->addStartTag('X', 0, 1)->pairWith($parser->addEndTag('X', 99, 1)); }], ['*foo* bar', '<r><X><s>*</s>foo* bar</X></r>', function ($configurator) { $configurator->tags->add('X'); }, function ($parser) { $parser->addStartTag('X', 0, 1)->pairWith($parser->addEndTag('X', 99, 1)); $parser->addEndTag('X', 4, 1); }], ['*_foo* bar_', '<r><X><s>*</s><Y><s>_</s>foo</Y><e>*</e></X><Y> bar<e>_</e></Y></r>', function ($configurator) { $configurator->tags->add('X'); $configurator->tags->add('Y')->rules->autoReopen(); }, function ($parser) { $parser->addStartTag('X', 0, 1)->pairWith($parser->addEndTag('X', 5, 1)); $parser->addStartTag('Y', 1, 1)->pairWith($parser->addEndTag('Y', 10, 1)); $parser->addEndTag('Y', 6, 1); }], ['**x**x***', '<r><X><s>**</s>x<Y><s>**</s>x<e>**</e></Y><e>*</e></X></r>', function ($configurator) { $configurator->tags->add('X'); $configurator->tags->add('Y'); }, function ($parser) { $parser->addStartTag('X', 0, 2)->pairWith($parser->addEndTag('X', 7, 2)); $parser->addStartTag('Y', 3, 2)->pairWith($parser->addEndTag('Y', 6, 2)); }], ['**x[**]x', '<r><X><s>**</s>x<Y>[**]</Y></X>x</r>', function ($configurator) { $configurator->tags->add('X'); $configurator->tags->add('Y'); }, function ($parser) { $parser->addStartTag('X', 0, 2)->pairWith($parser->addEndTag('X', 4, 2)); $parser->addSelfClosingTag('Y', 3, 4); }], ['xy', '<t>x<br/>y</t>', null, function ($parser) { $parser->addBrTag(1); }], ['xy', '<t>xy</t>', function ($configurator) { $configurator->rootRules->preventLineBreaks(); }, function ($parser) { $parser->addBrTag(1); }], ['xx', '<r><X>x</X><X>x</X></r>', function ($configurator) { $configurator->tags->add('X')->rules->closeParent('X'); }, function ($parser) { $parser->addStartTag('X', 0, 0); $parser->addStartTag('X', 1, 0); }], ['xx [hr] yy', '<r>xx <HR>[hr]</HR> yy</r>', function ($configurator) { $configurator->tags->add('HR')->rules->autoClose(); }, function ($parser) { $parser->addStartTag('HR', 3, 4); }], ['xx [hr][/hr] yy', '<r>xx <HR><s>[hr]</s><e>[/hr]</e></HR> yy</r>', function ($configurator) { $configurator->tags->add('HR')->rules->autoClose(); }, function ($parser) { $parser->addStartTag('HR', 3, 4); $parser->addEndTag('HR', 7, 5); }], ['xx [img=foo.png] yy', '<r>xx <IMG src="foo.png">[img=foo.png]</IMG> yy</r>', function ($configurator) { $tag = $configurator->tags->add('IMG'); $tag->attributes->add('src'); $tag->rules->autoClose(); }, function ($parser) { $parser->addStartTag('IMG', 3, 13)->setAttribute('src', 'foo.png'); }], ['xx [img]foo.png[/img] yy', '<r>xx <IMG src="foo.png"><s>[img]</s>foo.png<e>[/img]</e></IMG> yy</r>', function ($configurator) { $tag = $configurator->tags->add('IMG'); $tag->attributes->add('src'); $tag->rules->autoClose(); }, function ($parser) { $tag = $parser->addStartTag('IMG', 3, 5); $tag->setAttribute('src', 'foo.png'); $tag->pairWith($parser->addEndTag('IMG', 15, 6)); }], ['XYX', '<r><X><s>X</s><Y>Y</Y><e>X</e></X></r>', function ($configurator) { $configurator->tags->add('X'); $configurator->tags->add('Y'); }, function ($parser) { $parser->addStartTag('X', 0, 1); $parser->addSelfClosingTag('Y', 1, 1); $parser->addEndTag('X', 2, 1); }], ['XYX', '<r><X><s>X</s>Y<e>X</e></X></r>', function ($configurator) { $configurator->tags->add('X')->rules->denyChild('Y'); $configurator->tags->add('Y'); }, function ($parser) { $parser->addStartTag('X', 0, 1); $parser->addSelfClosingTag('Y', 1, 1); $parser->addEndTag('X', 2, 1); }], ['XYZYX', '<r><X><s>X</s><Y><s>Y</s><Z>Z</Z><e>Y</e></Y><e>X</e></X></r>', function ($configurator) { $configurator->tags->add('X')->rules->denyChild('Z'); $configurator->tags->add('Y'); $configurator->tags->add('Z'); }, function ($parser) { $parser->addStartTag('X', 0, 1); $parser->addStartTag('Y', 1, 1); $parser->addSelfClosingTag('Z', 2, 1); $parser->addEndTag('Y', 3, 1); $parser->addEndTag('X', 4, 1); }], ['XYZYX', '<r><X><s>X</s><Y><s>Y</s>Z<e>Y</e></Y><e>X</e></X></r>', function ($configurator) { $configurator->tags->add('X')->rules->denyChild('Z'); $configurator->tags->add('Y')->rules->isTransparent(); $configurator->tags->add('Z'); }, function ($parser) { $parser->addStartTag('X', 0, 1); $parser->addStartTag('Y', 1, 1); $parser->addSelfClosingTag('Z', 2, 1); $parser->addEndTag('Y', 3, 1); $parser->addEndTag('X', 4, 1); }], ['XYX', '<r><X><s>X</s>Y<e>X</e></X></r>', function ($configurator) { $configurator->tags->add('X')->rules->denyDescendant('Y'); $configurator->tags->add('Y'); }, function ($parser) { $parser->addStartTag('X', 0, 1); $parser->addSelfClosingTag('Y', 1, 1); $parser->addEndTag('X', 2, 1); }], ['XYZYX', '<r><X><s>X</s><Y><s>Y</s>Z<e>Y</e></Y><e>X</e></X></r>', function ($configurator) { $configurator->tags->add('X')->rules->denyDescendant('Z'); $configurator->tags->add('Y'); $configurator->tags->add('Z'); }, function ($parser) { $parser->addStartTag('X', 0, 1); $parser->addStartTag('Y', 1, 1); $parser->addSelfClosingTag('Z', 2, 1); $parser->addEndTag('Y', 3, 1); $parser->addEndTag('X', 4, 1); }], ['XYX', '<r><X><s>X</s>Y<e>X</e></X></r>', function ($configurator) { $rules = $configurator->tags->add('X')->rules; $rules->isTransparent(); $rules->denyChild('Y'); $configurator->tags->add('Y'); }, function ($parser) { $parser->addStartTag('X', 0, 1); $parser->addSelfClosingTag('Y', 1, 1); $parser->addEndTag('X', 2, 1); }], ['XYYYX', '<r><X><s>X</s><Y><s>Y</s><Y><s>Y</s><Y><s>Y</s></Y></Y></Y><e>X</e></X></r>', function ($configurator) { $configurator->tags->add('X'); $configurator->tags->add('Y'); }, function ($parser) { $parser->maxFixingCost = 0; $parser->addStartTag('X', 0, 1); $parser->addStartTag('Y', 1, 1); $parser->addStartTag('Y', 2, 1); $parser->addStartTag('Y', 3, 1); $parser->addEndTag('X', 4, 1); }, [['warn', 'Fixing cost limit exceeded']]], ['XYYYYX', '<r><X><s>X</s><Y><s>Y</s><Y>Y</Y><Y>Y</Y><e>Y</e></Y><e>X</e></X></r>', function ($configurator) { $configurator->tags->add('X'); $configurator->tags->add('Y'); }, function ($parser) { // Ensuring that we can still parse well-formed text if we disallow any fixing $parser->maxFixingCost = 0; $parser->addStartTag('X', 0, 1); $parser->addStartTag('Y', 1, 1); $parser->addSelfClosingTag('Y', 2, 1); $parser->addSelfClosingTag('Y', 3, 1); $parser->addEndTag('Y', 4, 1); $parser->addEndTag('X', 5, 1); }], ['..', '<r><X>.</X><X>.</X></r>', function ($configurator) { $configurator->tags->add('X'); }, function ($parser) { // NOTE: the tags are added in order of position, but the stack still needs to // be sorted following the right tiebreakers. This is what we're testing $parser->addEndTag('X', 2, 0); $parser->addEndTag('X', 1, 0); $parser->addStartTag('X', 1, 0); $parser->addStartTag('X', 0, 0); }], ['...', '<r><X>.</X><X>.</X><X>.</X></r>', function ($configurator) { $configurator->tags->add('X')->filterChain->append(function ($tag, $parser) { if ($tag->getPos() === 0) { $parser->addSelfClosingTag('X', 2, 1); } return true; })->addParameterByName('parser'); }, function ($parser) { $parser->addSelfClosingTag('X', 0, 1); $parser->addSelfClosingTag('X', 1, 1); }], ['...', '<r><X>.</X><X>.</X><X>.</X></r>', function ($configurator) { $configurator->tags->add('X')->filterChain->append(function ($tag, $parser) { if ($tag->getPos() === 0) { $parser->addSelfClosingTag('X', 1, 1); } return true; })->addParameterByName('parser'); }, function ($parser) { $parser->addSelfClosingTag('X', 0, 1); $parser->addSelfClosingTag('X', 2, 1); }], ['[UL] [*] foo [*] bar [/UL]', '<r><UL><s>[UL]</s> <LI><s>[*]</s> foo</LI> <LI><s>[*]</s> bar</LI> <e>[/UL]</e></UL></r>', function ($configurator) { $configurator->tags->add('UL'); $rules = $configurator->tags->add('LI')->rules; $rules->closeAncestor('LI'); $rules->ignoreSurroundingWhitespace(); }, function ($parser) { $parser->addStartTag('UL', 0, 4); $parser->addStartTag('LI', 10, 3); $parser->addStartTag('LI', 23, 3); $parser->addEndTag('UL', 35, 5); }], ['[UL] [*] foo [*] bar [/UL]', '<r><UL><s>[UL]</s> <LI><s>[*]</s> foo</LI> <LI><s>[*]</s> bar</LI> <e>[/UL]</e></UL></r>', function ($configurator) { $configurator->tags->add('UL'); $rules = $configurator->tags->add('LI')->rules; $rules->closeParent('LI'); $rules->ignoreSurroundingWhitespace(); }, function ($parser) { $parser->addStartTag('UL', 0, 4); $parser->addStartTag('LI', 10, 3); $parser->addStartTag('LI', 23, 3); $parser->addEndTag('UL', 35, 5); }], ['XX', '<r>X<X>X</X></r>', function ($configurator) { $configurator->tags->add('X')->filterChain->append(function ($tag) { return (bool) $tag->getPos(); }); }, function ($parser) { $parser->addSelfClosingTag('X', 0, 1); $parser->addSelfClosingTag('X', 1, 1); }], ['XYX', '<r><X><s>X</s>Y<e>X</e></X></r>', function ($configurator) { $configurator->tags->add('X')->rules->denyChild('Y'); $configurator->tags->add('Y'); }, function ($parser) { $parser->addStartTag('X', 0, 1); $parser->addSelfClosingTag('Y', 1, 1); $parser->addEndTag('X', 2, 1); }, [['warn', 'Tag is not allowed in this context', ['tagName' => 'Y', 'tag' => $this->runClosure(function () { $tag = new Tag(Tag::SELF_CLOSING_TAG, 'Y', 1, 1); $tag->invalidate(); return $tag; })]]]], ['XX', '<r><X><s>X</s><e>X</e></X></r>', function ($configurator) { $configurator->tags->add('X')->rules->denyChild('Y'); $configurator->tags->add('Y'); }, function ($parser) { $parser->addStartTag('X', 0, 1); $parser->addSelfClosingTag('Y', 1, 0); $parser->addEndTag('X', 1, 1); }, [['debug', 'Tag is not allowed in this context', ['tagName' => 'Y', 'tag' => $this->runClosure(function () { $tag = new Tag(Tag::SELF_CLOSING_TAG, 'Y', 1, 0); $tag->invalidate(); return $tag; })]]]], ['X', '<t>X</t>', function ($configurator) { $configurator->tags->add('X')->rules->requireAncestor('Y'); $configurator->tags->add('Y'); }, function ($parser) { $parser->addSelfClosingTag('X', 0, 1); }], ['.X.', '<r><NOPARSE><s>.</s>X<e>.</e></NOPARSE></r>', function ($configurator) { $configurator->tags->add('NOPARSE')->rules->ignoreTags(); $configurator->tags->add('X'); }, function ($parser) { $parser->addTagPair('NOPARSE', 0, 1, 2, 1); $parser->addSelfClosingTag('X', 1, 1); }], ['.X.', '<r><NOPARSE><s>.</s>X<e>.</e></NOPARSE></r>', function ($configurator) { $configurator->tags->add('NOPARSE')->rules->ignoreTags(); $configurator->tags->add('X')->rules->closeParent('NOPARSE'); }, function ($parser) { $parser->addTagPair('NOPARSE', 0, 1, 2, 1); $parser->addSelfClosingTag('X', 1, 1); }], ['X.X.X', '<r><X><s>X</s><NOPARSE><s>.</s>X<e>.</e></NOPARSE><e>X</e></X></r>', function ($configurator) { $configurator->tags->add('NOPARSE')->rules->ignoreTags(); $configurator->tags->add('X'); }, function ($parser) { $parser->addTagPair('NOPARSE', 1, 1, 3, 1); $parser->addStartTag('X', 0, 1); $parser->addEndTag('X', 2, 1); $parser->addEndTag('X', 4, 1); }], ['.X.', '<r><NOPARSE>.<i>X</i>.</NOPARSE></r>', function ($configurator) { $configurator->tags->add('NOPARSE')->rules->ignoreTags(); }, function ($parser) { $parser->addTagPair('NOPARSE', 0, 0, 3, 0); $parser->addIgnoreTag(1, 1); }], ['.XX.', '<r><NOPARSE><p>.X</p><p>X.</p></NOPARSE></r>', function ($configurator) { $tag = $configurator->tags->add('NOPARSE'); $tag->rules->createParagraphs(); $tag->rules->ignoreTags(); }, function ($parser) { $parser->addTagPair('NOPARSE', 0, 0, 4, 0); $parser->addParagraphBreak(2); }], ['.X.', '<r><NOPARSE>.<br/>X.</NOPARSE></r>', function ($configurator) { $configurator->tags->add('NOPARSE')->rules->ignoreTags(); }, function ($parser) { $parser->addTagPair('NOPARSE', 0, 0, 3, 0); $parser->addBrTag(1); }], ['foobar', '<t><p>foo</p><p>bar</p></t>', function ($configurator) { $configurator->rootRules->createParagraphs(); }, function ($parser) { $parser->addParagraphBreak(3); }], ['foo|bar', '<r><p>foo</p><i>|</i><p>bar</p></r>', function ($configurator) { $configurator->rootRules->createParagraphs(); }, function ($parser) { $parser->addParagraphBreak(3); $parser->addIgnoreTag(3, 1); }], ["\n\n", "<r><X><br/>\n</X><X>\n</X></r>", function ($configurator) { $configurator->rootRules->enableAutoLineBreaks(); $configurator->tags->add('X'); }, function ($parser) { $parser->addTagPair('X', 0, 0, 1, 0); $parser->addTagPair('X', 1, 0, 2, 0)->setFlags(Parser::RULE_SUSPEND_AUTO_BR); }], ['xxx', "<r><T8>x<T8>x</T8>x</T8></r>", function ($configurator) { // Create 9 tags with different rules so that they don't occupy the same bit in // the allowed tags bitfields for ($i = 0; $i <= 8; ++$i) { $j = ($i + 1) % 9; $configurator->tags->add('T' . $i)->rules->denyChild('T' . $j); } }, function ($parser) { $parser->addTagPair('T8', 0, 0, 3, 0); $parser->addSelfClosingTag('T8', 1, 1); }], ['[x][y]..[/y][/x]', '<r><X><s>[x]</s></X><Y><s>[y]</s><X>..</X><e>[/y]</e></Y><i>[/x]</i></r>', function ($configurator) { $configurator->tags->add('X')->rules->autoReopen(); $configurator->tags->add('Y')->rules->closeParent('X')->fosterParent('X'); }, function ($parser) { $parser->addStartTag('X', 0, 3); $parser->addStartTag('Y', 3, 3); $parser->addEndTag('Y', 8, 4); $parser->addEndTag('X', 12, 4); }], ["foo\nbar", "<t>foo\nbar</t>"], ["foo\nbar", "<t>foo<br/>\nbar</t>", function ($configurator) { $configurator->rootRules->enableAutoLineBreaks(); }], ["foo\nbar", "<t>foo\nbar</t>", function ($configurator) { $configurator->rootRules->disableAutoLineBreaks(); $configurator->rootRules->enableAutoLineBreaks(); }], ["[Y]\n[N]\n[Y]\n[N]\n[/N]\n[/Y]\n[/N]\n[/Y]", "<r><Y><s>[Y]</s><br/>\n<N><s>[N]</s>\n<Y><s>[Y]</s><br/>\n<N><s>[N]</s>\n<e>[/N]</e></N><br/>\n<e>[/Y]</e></Y>\n<e>[/N]</e></N><br/>\n<e>[/Y]</e></Y></r>", function ($configurator) { $configurator->tags->add('Y')->rules->enableAutoLineBreaks(); $configurator->tags->add('N')->rules->disableAutoLineBreaks(); }, function ($parser) { $parser->addStartTag('Y', 0, 3); $parser->addStartTag('N', 4, 3); $parser->addStartTag('Y', 8, 3); $parser->addStartTag('N', 12, 3); $parser->addEndTag('N', 16, 4); $parser->addEndTag('Y', 21, 4); $parser->addEndTag('N', 26, 4); $parser->addEndTag('Y', 31, 4); }], ["[Y]\n[S]\n[X]\n[/X]\n[/S]\n[/Y]", "<r><Y><s>[Y]</s><br/>\n<S><s>[S]</s>\n<X><s>[X]</s><br/>\n<e>[/X]</e></X>\n<e>[/S]</e></S><br/>\n<e>[/Y]</e></Y></r>", function ($configurator) { $configurator->tags->add('Y')->rules->enableAutoLineBreaks(); $configurator->tags->add('S')->rules->suspendAutoLineBreaks(); $configurator->tags->add('X'); }, function ($parser) { $parser->addStartTag('Y', 0, 3); $parser->addStartTag('S', 4, 3); $parser->addStartTag('X', 8, 3); $parser->addEndTag('X', 12, 4); $parser->addEndTag('S', 17, 4); $parser->addEndTag('Y', 22, 4); }], ['...', '<t>...</t>', function ($configurator) { $configurator->tags->add('X'); }, function ($parser) { $parser->addSelfClosingTag('X', 1, 1); $parser->addVerbatim(0, 3); }], [".\n.\n", "<t>.\n.<br/>\n</t>", function ($configurator) { $configurator->rootRules->enableAutoLineBreaks(); }, function ($parser) { $parser->addVerbatim(1, 1)->setFlags(0); }], [".\n.\n", "<t>.<br/>\n.\n</t>", function ($configurator) { $configurator->rootRules->disableAutoLineBreaks(); }, function ($parser) { $parser->addVerbatim(1, 1)->setFlags(Parser::RULE_ENABLE_AUTO_BR); }], ['xxx', '<r><i>xxx</i></r>', null, function ($parser) { $parser->addIgnoreTag(0, 99); }]]; }
/** * sortTags() callback * * Tags are stored as a stack, in LIFO order. We sort tags by position _descending_ so that they * are processed in the order they appear in the text. * * @param Tag $a First tag to compare * @param Tag $b Second tag to compare * @return integer */ protected static function compareTags(Tag $a, Tag $b) { $aPos = $a->getPos(); $bPos = $b->getPos(); // First we order by pos descending if ($aPos !== $bPos) { return $bPos - $aPos; } // If the tags start at the same position, we'll use their sortPriority if applicable. Tags // with a lower value get sorted last, which means they'll be processed first. IOW, -10 is // processed before 10 if ($a->getSortPriority() !== $b->getSortPriority()) { return $b->getSortPriority() - $a->getSortPriority(); } // If the tags start at the same position and have the same priority, we'll sort them // according to their length, with special considerations for zero-width tags $aLen = $a->getLen(); $bLen = $b->getLen(); if (!$aLen || !$bLen) { // Zero-width end tags are ordered after zero-width start tags so that a pair that ends // with a zero-width tag has the opportunity to be closed before another pair starts // with a zero-width tag. For example, the pairs that would enclose each of the letters // in the string "XY". Self-closing tags are ordered between end tags and start tags in // an attempt to keep them out of tag pairs if (!$aLen && !$bLen) { $order = [Tag::END_TAG => 0, Tag::SELF_CLOSING_TAG => 1, Tag::START_TAG => 2]; return $order[$b->getType()] - $order[$a->getType()]; } // Here, we know that only one of $a or $b is a zero-width tags. Zero-width tags are // ordered after wider tags so that they have a chance to be processed before the next // character is consumed, which would force them to be skipped return $aLen ? -1 : 1; } // Here we know that both tags start at the same position and have a length greater than 0. // We sort tags by length ascending, so that the longest matches are processed first. If // their length is identical, the order is undefined as PHP's sort isn't stable return $aLen - $bLen; }
/** * @testdox addTagPair() returns the newly-created start tag */ public function testAddTagPairReturn() { $dummyStack = new DummyStack(); $startTag = new Tag(Tag::START_TAG, 'FOO', 1, 2); $endTag = new Tag(Tag::END_TAG, 'FOO', 3, 4); $startTag->pairWith($endTag); $this->assertEquals($startTag, $dummyStack->addTagPair('FOO', 1, 2, 3, 4)); }