<?php
/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* END LICENSE
*
* @author Chris Pollett chris@pollett.org
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*/
namespace seekquarry\yioop\tests;
use seekquarry\yioop\controllers\SearchController;
use seekquarry\yioop\library as L;
use seekquarry\yioop\library\UnitTest;
/**
* Tests the functionality of WikiParser used when processing Wikipedia dumps
* and used for Yioop's internal wiki infrastructure
*
* @author Chris Pollett
*/
class WikiParserTest extends UnitTest
{
/**
* No set up being done for the time being
*/
public function setUp()
{
}
/**
* No tear down being done for the time being
*/
public function tearDown()
{
}
/**
* Checks that the basic WikiParser substitutions are done correctly
*/
/**
* Heading markup maps to the matching html level, clamped at six.
*/
public function parseHeadingsTestCase()
{
$parser = new L\WikiParser("/b/");
$this->assertEqual($parser->parse("= A ="),
"<h1 id=\"A\">A</h1>\n",
"single equals is a level one heading");
$this->assertEqual($parser->parse("======= G ======="),
"<h6 id=\"G\">G</h6>\n",
"beyond six equals clamps to level six");
}
/**
* A four-level list nests four lists deep, which the regex parser
* this replaces could not do.
*/
public function parseDeepListTestCase()
{
$parser = new L\WikiParser("/b/");
$html = $parser->parse("* a\n** b\n*** c\n**** d\n");
$expected = "<ul>\n<li>a\n<ul>\n<li>b\n<ul>\n<li>c\n" .
"<ul>\n<li>d</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n" .
"</li>\n</ul>\n";
$this->assertEqual($html, $expected,
"four star levels nest four unordered lists deep");
$this->assertEqual(substr_count($html, "<ul>"), 4,
"the fourth level is a real nested list, not literal text");
}
/**
* A mix of ordered and unordered markers nests each level as the
* marker at that position asks.
*/
public function parseMixedListTestCase()
{
$parser = new L\WikiParser("/b/");
$html = $parser->parse("# one\n#* sub a\n#* sub b\n# two\n");
$expected = "<ol>\n<li>one\n<ul>\n<li>sub a</li>\n" .
"<li>sub b</li>\n</ul>\n</li>\n<li>two</li>\n</ol>\n";
$this->assertEqual($html, $expected,
"an ordered list carries an unordered sublist");
}
/**
* Bold, italic, and bold-italic emphasis render to the matching
* tags, and a link body is parsed for inline markup in turn.
*/
public function parseEmphasisTestCase()
{
$parser = new L\WikiParser("/b/");
$this->assertEqual($parser->parse("''i''"),
"<p><i>i</i></p>\n", "two quotes make italic");
$this->assertEqual($parser->parse("'''b'''"),
"<p><b>b</b></p>\n", "three quotes make bold");
$this->assertEqual($parser->parse("'''''bi'''''"),
"<p><b><i>bi</i></b></p>\n",
"five quotes make bold italic");
}
/**
* A page link is joined to the base address, an allowed scheme is
* kept as written, and a disallowed scheme such as javascript is
* joined to the base too so it cannot become an active url. A
* three-part link points at its middle page part, not the leading
* relationship, and a group@page target becomes a cross-group marker
* the view resolves at display time.
*/
public function parseLinkTestCase()
{
$parser = new L\WikiParser("/b/");
$this->assertEqual($parser->parse("[[Page|text]]"),
"<p><a href=\"/b/Page\">text</a></p>\n",
"a page link joins the base address");
$this->assertEqual($parser->parse("[[http://x/|e]]"),
"<p><a href=\"http://x/\">e</a></p>\n",
"an http link is kept as written");
$this->assertEqual($parser->parse("[[javascript:x|c]]"),
"<p><a href=\"/b/javascript:x\">c</a></p>\n",
"a javascript target is treated as a page name");
$this->assertEqual(
$parser->parse("[[Daughter|Page Name|shown text]]"),
"<p><a href=\"/b/Page Name\">shown text</a></p>\n",
"a relationship link points at its middle page part");
$this->assertEqual($parser->parse("[[grp@Page Name|shown]]"),
"<p><a href=\"@@grp@Page Name@@\">shown</a></p>\n",
"a group@page link becomes a cross-group marker");
}
/**
* A link inside a heading renders as a link inside the heading, the
* case the regex parser mangles.
*/
public function parseLinkInHeadingTestCase()
{
$parser = new L\WikiParser("/b/");
$this->assertEqual($parser->parse("== [[Page|Linked]] =="),
"<h2 id=\"Linked\"><a href=\"/b/Page\">Linked</a></h2>\n",
"a heading keeps a link inside it");
}
/**
* Angle brackets in leaf text are escaped so they are not read as
* html.
*/
public function parseEscapingTestCase()
{
$parser = new L\WikiParser("/b/");
$this->assertEqual(
$parser->parse("a <script>x</script> b"),
"<p>a <script>x</script> b</p>\n",
"angle brackets in text are escaped");
}
/**
* An allowed inline tag passes through and the text inside it is
* still parsed for wiki markup.
*/
public function parseHtmlTagsTestCase()
{
$parser = new L\WikiParser("/b/");
$this->assertEqual($parser->parse("<u>x</u>"),
"<p><u>x</u></p>\n", "an allowed tag passes through");
$this->assertEqual($parser->parse("<u>'''b'''</u>"),
"<p><u><b>b</b></u></p>\n",
"markup inside an allowed tag is still parsed");
}
/**
* Content wrapped in nowiki is shown as typed, with its wiki markup
* left inert.
*/
public function parseNowikiTestCase()
{
$parser = new L\WikiParser("/b/");
$this->assertEqual(
$parser->parse("x <nowiki>[[y]]</nowiki> z"),
"<p>x [[y]] z</p>\n",
"nowiki leaves its link markup inert");
$this->assertEqual(
$parser->parse("x <nowiki><nowiki></nowiki> z"),
"<p>x <nowiki> z</p>\n",
"a nowiki-wrapped nowiki tag prints as a literal open tag");
}
/**
* A nowiki span that runs across several lines, wrapping markup that
* would otherwise start its own blocks, is kept whole and shown as
* literal text: the block markup inside is not parsed and the nowiki
* markers themselves do not appear.
*/
public function parseSpanningNowikiTestCase()
{
$parser = new L\WikiParser("/b/");
$html = $parser->parse("lead\n<nowiki>\n{|\n!a\n|}\n</nowiki>\n" .
"tail\n", false, false, 0);
$this->assertTrue(strpos($html, "<table") === false,
"table markup inside a spanning nowiki is not parsed");
$this->assertTrue(strpos($html, "nowiki") === false,
"the nowiki markers do not appear");
$this->assertTrue(strpos($html, "{|") !== false,
"the wrapped markup shows as literal characters");
}
/**
* Text that already carries an escaped entity, such as one written by
* hand to show a tag as an example, is not escaped a second time, so it
* shows the tag it stands for rather than the raw entity text.
*/
public function parseNoDoubleEscapeTestCase()
{
$parser = new L\WikiParser("/b/");
$html = $parser->parse("<nowiki><math></nowiki>",
false, false, 0);
$this->assertTrue(strpos($html, "<") === false,
"an already-escaped entity is not escaped again");
$this->assertTrue(strpos($html, "<math>") !== false,
"the entity shows the tag it stands for");
}
/**
* Content between backticks is math for the math renderer, not wiki
* markup, so a matrix written with brackets is kept whole rather than
* being read as a link.
*/
public function parseBacktickMathTestCase()
{
$parser = new L\WikiParser("/b/");
$html = $parser->parse("`[[1, -2],[3,4]]`", false, false, 0);
$this->assertTrue(strpos($html, "<a href") === false,
"brackets inside backtick math are not read as a link");
$this->assertTrue(strpos($html, "[[1, -2],[3,4]]") !== false,
"the backtick math content is kept whole");
}
/**
* A table cell line can pair a header cell with the data beside it: a
* bang opens a header cell, a double bar switches to a data cell, and a
* line that opens with neither continues the cell above it.
*/
public function parseTableMixedCellsTestCase()
{
$parser = new L\WikiParser("/b/");
$html = $parser->parse("{|\n!name|| meaning\nsecond line\n|}\n",
false, false, 0);
$this->assertTrue(strpos($html,
"<th>name</th><td>meaning\nsecond line</td>") !== false,
"a header cell pairs with the data cell and its continuation");
}
/**
* A single top-level heading is the page title, so it is left out of
* the contents box while the deeper headings still appear.
*/
public function parseLoneTitleNotInContentsTestCase()
{
$parser = new L\WikiParser("/b/");
$html = $parser->parse("= Title =\n== A ==\nx\n\n== B ==\nx\n\n" .
"== C ==\nx\n\n== D ==\nx\n", false, false, 0);
$box = substr($html, 0, strpos($html, "</div>"));
$this->assertTrue(strpos($box, "#Title") === false,
"the lone title is not a contents entry");
$this->assertTrue(strpos($box, "#A") !== false,
"the deeper headings still appear");
}
/**
* Lines that open with a space are shown preformatted, gathered into
* one pre block that ends at a line which does not open with a space.
*/
public function parseSpacePreTestCase()
{
$parser = new L\WikiParser("/b/");
$html = $parser->parse(" line one\n line two\nafter\n", false,
false, 0);
$this->assertTrue(strpos($html, "<pre>line one\nline two</pre>") !==
false, "spaced lines become one pre block, the space dropped");
$this->assertTrue(strpos($html, "<p>after</p>") !== false,
"a non-spaced line ends the pre block");
}
/**
* A heading marked with a notoc marker is kept out of the contents box
* but still shows as a heading, with the marker dropped.
*/
public function parseNotocHeadingTestCase()
{
$parser = new L\WikiParser("/b/");
$html = $parser->parse("== <notoc>Hidden</notoc> ==\nx\n\n" .
"== A ==\nx\n\n== B ==\nx\n\n== C ==\nx\n\n== D ==\nx\n",
false, false, 0);
$this->assertTrue(strpos($html, "<h2 id=\"Hidden\">Hidden</h2>") !==
false, "the marked heading still renders");
$this->assertTrue(strpos($html, "#Hidden") === false,
"the marked heading is not a contents entry");
}
/**
* Form templates become what a form page needs: an action form becomes
* its bracketed marker, a field form emits its input and the hidden
* CSVFORM input as a block, and a category form is dropped.
*/
public function parseFormTemplateTestCase()
{
$parser = new L\WikiParser("/b/");
$marker = $parser->parse("{{require-signin|/login}}", false,
false, 0);
$this->assertTrue(strpos($marker, "[{require-signin|/login}]") !==
false, "an action form becomes its bracketed marker");
$field = $parser->parse("{{textfield|Name|fname|20}}", false,
false, 0);
$this->assertTrue(strpos($field, "name='fname'") !== false &&
strpos($field, "CSVFORM[fname]") !== false,
"a field form emits its input and its CSVFORM hidden input");
$this->assertTrue(strpos($field, "<p>") === false,
"a standalone form is a block, not wrapped in a paragraph");
$this->assertTrue(trim($parser->parse("{{category|x}}", false,
false, 0)) === "", "a category form is dropped");
}
/**
* The search-box tag becomes a deferred token the view fills in later,
* and a tag shown as an example inside nowiki is left as plain text so a
* help page can describe the syntax without drawing a real search box.
*/
public function parseSearchWidgetTestCase()
{
$parser = new L\WikiParser("/b/");
$out = $parser->parse(
"{{search:default|size:small|placeholder:Find here}}");
$this->assertTrue(strpos($out,
"[{search|default|small|Find here}]") !== false,
"a search tag becomes its deferred token");
$example = $parser->parse("<nowiki>" .
"{{search:default|size:small|placeholder:Find}}</nowiki>");
$this->assertTrue(strpos($example, "[{search|") === false,
"a search tag shown inside nowiki stays plain text");
}
/**
* A display-time token such as [{recent_places}] shown as an example
* inside nowiki has its opening broken so it is drawn as text, while the
* same token in ordinary text is left whole for the view to fill in.
*/
public function parseVerbatimTokenTestCase()
{
$parser = new L\WikiParser("/b/");
$in_nowiki = $parser->parse("<nowiki>[{recent_places}]</nowiki>");
$this->assertTrue(strpos($in_nowiki, "[{recent_places}]") === false,
"a token inside nowiki no longer reads as the live token");
$plain = $parser->parse("[{recent_places}]");
$this->assertTrue(strpos($plain, "[{recent_places}]") !== false,
"a token in ordinary text is left whole for the view");
}
/**
* A single bar divides a row into cells too, and a bar inside a link or
* a nowiki span stays part of its cell rather than dividing it.
*/
public function parseTableSingleBarCellsTestCase()
{
$parser = new L\WikiParser("/b/");
$html = $parser->parse("{|\n|<nowiki>''a''</nowiki>|''a''\n|}\n",
false, false, 0);
$this->assertEqual(substr_count($html, "<td>"), 2,
"a single bar splits the row into two cells");
$this->assertTrue(strpos($html, "<i>a</i>") !== false,
"the cell outside nowiki still renders its emphasis");
$link = $parser->parse("{|\n|[[P|T]]|next\n|}\n", false, false, 0);
$this->assertEqual(substr_count($link, "<td>"), 2,
"a bar inside a link is not a cell divider");
}
/**
* Headings inside a notoc region render as headings but are kept out of
* the contents box, while headings outside it still appear.
*/
public function parseNotocRegionTestCase()
{
$parser = new L\WikiParser("/b/");
$html = $parser->parse("<notoc>\n== A ==\nx\n</notoc>\n== C ==\n" .
"z\n\n== D ==\nw\n\n== E ==\nv\n\n== F ==\nu\n", false,
false, 0);
$box = substr($html, 0, strpos($html, "</div>"));
$this->assertTrue(strpos($html, "<h2 id=\"A\">A</h2>") !== false,
"a heading in a notoc region still renders");
$this->assertTrue(strpos($box, "#A") === false,
"a heading in a notoc region is not a contents entry");
$this->assertTrue(strpos($box, "#C") !== false,
"a heading outside the region still appears");
}
/**
* A class, id, or style template inside text wraps just its content in
* a span, leaving the surrounding text alone.
*/
public function parseInlineAttributeTemplateTestCase()
{
$parser = new L\WikiParser("/b/");
$html = $parser->parse("hi {{class=\"red\" b}} yo\n", false,
false, 0);
$this->assertTrue(strpos($html,
"<span class=\"red\">b</span>") !== false,
"an inline class template wraps its content in a span");
$style = $parser->parse("{{style=\"color:red\" x}} tail\n", false,
false, 0);
$this->assertTrue(strpos($style,
"<span style=\"color:red\">x</span>") !== false,
"an inline style template wraps its content in a span");
}
/**
* A pre block keeps its content exactly, without parsing the wiki
* markup inside it.
*/
public function parsePreTestCase()
{
$parser = new L\WikiParser("/b/");
$this->assertEqual($parser->parse("<pre>a [[b]]</pre>"),
"<pre>a [[b]]</pre>\n",
"a pre block keeps its content literal");
}
/**
* Colon-prefixed lines become indent levels, clamped at four.
*/
public function parseIndentTestCase()
{
$parser = new L\WikiParser("/b/");
$this->assertEqual($parser->parse(": x"),
"<p><span class=\"indent1\"> </span>x</p>\n",
"one colon is indent level one");
$this->assertEqual($parser->parse("::::: z"),
"<p><span class=\"indent4\"> </span>z</p>\n",
"five colons clamp to indent level four");
}
/**
* A single newline continues the current block: lines under a list
* item that do not start a new block fold into that item, so a list
* whose items have wrapped text stays one list rather than breaking
* into separate lists with paragraphs between.
*/
public function parseBlockContinuationTestCase()
{
$parser = new L\WikiParser("/b/");
$html = $parser->parse("# one\ntwo\nthree\n# four", false, false,
0);
$this->assertEqual(substr_count($html, "<ol>"), 1,
"wrapped item text does not split the list");
$this->assertEqual(substr_count($html, "<li>"), 2,
"the list has exactly two items");
$this->assertTrue(strpos($html, "<li>one\ntwo\nthree</li>") !==
false, "the wrapped lines fold into the first item");
}
/**
* A table renders its caption, header cells, and data cells; only
* class and style survive from the table's attributes, so an event
* handler cannot ride along.
*/
public function parseTableTestCase()
{
$parser = new L\WikiParser("/b/");
$html = $parser->parse(
"{| class=\"wikitable\"\n|+ Caption\n!a!!b\n|-\n|c||d\n|}");
$expected = "<table class=\"wikitable\">\n" .
"<caption>Caption</caption>\n" .
"<tr><th>a</th><th>b</th></tr>\n" .
"<tr><td>c</td><td>d</td></tr>\n</table>\n";
$this->assertEqual($html, $expected,
"a table renders caption, headers, and cells");
$this->assertEqual(
$parser->parse("{| onmouseover=\"x()\" class=\"ok\"\n" .
"|a\n|}"),
"<table class=\"ok\">\n<tr><td>a</td></tr>\n</table>\n",
"only class and style survive a table's attributes");
}
/**
* The styling templates wrap their parsed content in a div: center,
* left, and right set the alignment class, style sets an inline
* style, they nest, and an unrecognized template is left as text.
*/
public function parseTemplateTestCase()
{
$parser = new L\WikiParser("/b/");
$this->assertEqual(
$parser->parse("{{center|hello '''world'''}}"),
"<div class=\"center\">\n<p>hello <b>world</b></p>\n" .
"</div>\n", "center wraps its parsed content");
$this->assertEqual(
$parser->parse("{{center|{{left|inner}}}}"),
"<div class=\"center\">\n<div class=\"align-left\">\n" .
"<p>inner</p>\n</div>\n</div>\n",
"a template nests inside another");
$this->assertEqual(
$parser->parse("{{style='color:red' red text}}"),
"<div style=\"color:red\">\n<p>red text</p>\n</div>\n",
"style sets an inline style on the div");
$this->assertEqual($parser->parse("{{unknown thing}}"),
"<p>{{unknown thing}}</p>\n",
"an unrecognized template stays as text");
}
/**
* A block template on the line right after non-blank text, with no
* blank line between, is still read as a template rather than being
* swallowed into the preceding paragraph.
*/
public function parseTemplateAfterTextTestCase()
{
$parser = new L\WikiParser("/b/");
$html = $parser->parse("some text\n{{center|x}}", false, false,
0);
$this->assertTrue(strpos($html, "<div class=\"center\">") !==
false, "a template just after text is still a template");
$this->assertTrue(strpos($html, "{{center") === false,
"the template markup is not left literal");
}
/**
* Windows line endings, which a browser textarea submits when a page
* is saved, carry a carriage return that has to be normalized away.
* Left in, the return rides at the end of a heading line and stops it
* being read as a heading, so each of these lines must still come out
* as its own heading and no carriage return may reach the output.
*/
public function parseCarriageReturnLineEndingsTestCase()
{
$parser = new L\WikiParser("/b/");
$html = $parser->parse("= One =\r\n== Two ==\r\n=== Three ===\r\n",
false, false, 0);
$this->assertTrue(strpos($html, "<h1 id=\"One\">One</h1>") !==
false, "a carriage return does not stop a heading parsing");
$this->assertTrue(strpos($html, "<h3 id=\"Three\">Three</h3>") !==
false, "the last carriage-return heading parses too");
$this->assertTrue(strpos($html, "\r") === false,
"no carriage return survives into the output");
}
/**
* A page with enough headings gets a contents box whose entries show
* each heading's plain text, so a link inside a heading reads as its
* words rather than as raw link markup, while the heading itself keeps
* the link and gains a matching anchor id. Fewer headings draw no box.
*/
public function parseTableOfContentsTestCase()
{
$parser = new L\WikiParser("/b/");
$html = $parser->parse(
"== [[Page|Linked Heading]] ==\nb\n\n== Second ==\nb\n\n" .
"=== Sub ===\nb\n\n== Third ==\nb\n");
$this->assertTrue(strpos($html,
"<li><a href=\"#Linked Heading\">Linked Heading</a>") !==
false, "the contents entry shows the heading's plain text");
$this->assertTrue(strpos($html, "[[") === false,
"the contents strips link markup rather than showing it");
$this->assertTrue(strpos($html,
"<h2 id=\"Linked Heading\"><a href=\"/b/Page\">" .
"Linked Heading</a></h2>") !== false,
"the heading keeps its link and gains an anchor id");
$this->assertTrue(strpos($parser->parse(
"== One ==\nx\n\n== Two ==\ny\n"), "class=\"toc\"") ===
false, "no contents box below the heading threshold");
}
/**
* The contents box goes just before the first second-level heading, so
* a lead paragraph and a top-level heading above that point stay above
* the box rather than being pushed below it.
*/
public function parseTableOfContentsPlacementTestCase()
{
$parser = new L\WikiParser("/b/");
$html = $parser->parse("= Top =\nlead\n\n== A ==\nx\n\n== B ==" .
"\nx\n\n== C ==\nx\n\n== D ==\nx\n", false, false, 0);
$top = strpos($html, "<h1");
$box = strpos($html, "class=\"toc");
$second = strpos($html, "<h2");
$this->assertTrue($top !== false && $box !== false &&
$top < $box, "the top-level heading stays above the box");
$this->assertTrue($box < $second,
"the box sits before the first second-level heading");
}
/**
* A nowiki marker inside a pre block is a directive not to read its
* contents as wiki markup; since a pre already shows text literally,
* the marker is dropped and its contents kept, so example markup shows
* as plain characters and the nowiki tags do not appear.
*/
public function parseNowikiInsidePreTestCase()
{
$parser = new L\WikiParser("/b/");
$html = $parser->parse("<pre><nowiki>= Level 1 =</nowiki></pre>\n",
false, false, 0);
$this->assertTrue(strpos($html, "<pre>= Level 1 =</pre>") !==
false, "the wrapped markup shows literally inside the pre");
$this->assertTrue(strpos($html, "nowiki") === false,
"the nowiki marker itself does not appear");
$this->assertTrue(strpos($html, "<h1") === false,
"the wrapped heading is not read as a heading");
}
/**
* A run of definition lines, each a term then a colon then its
* meaning, becomes one description list with the terms and meanings
* paired, rather than being left as a paragraph.
*/
public function parseDefinitionListTestCase()
{
$parser = new L\WikiParser("/b/");
$html = $parser->parse(";Term 1: Meaning 1\n;Term 2: Meaning 2\n",
false, false, 0);
$this->assertEqual(substr_count($html, "<dl>"), 1,
"consecutive definition lines share one list");
$this->assertTrue(strpos($html,
"<dt>Term 1</dt>\n<dd>Meaning 1</dd>") !== false,
"a term and its meaning are paired");
$this->assertEqual(substr_count($html, "<dt>"), 2,
"both terms are present");
}
/**
* The named templates: Main and See also make an indented link, and
* Hatnote parenthesizes its parsed content.
*/
public function parseNamedTemplateTestCase()
{
$parser = new L\WikiParser("/b/");
$this->assertEqual($parser->parse("{{Main|Some Page}}"),
"<div class=\"indent\">(<a href=\"/b/Some Page\">" .
"Some Page</a>)</div>\n", "Main makes an indented link");
$this->assertEqual($parser->parse("{{Hatnote|see '''X'''}}"),
"(see <b>X</b>)\n", "Hatnote parenthesizes parsed content");
}
/**
* A toggle makes a show-or-hide link, and a block wraps its parsed
* content in a div with the given id and style.
*/
public function parseToggleTestCase()
{
$parser = new L\WikiParser("/b/");
$this->assertEqual($parser->parse("{{toggle|sec|label}}"),
"<p><a href=\"javascript:toggleDisplay('sec')\">label</a>" .
"</p>\n", "toggle makes a show-or-hide link");
$this->assertEqual($parser->parse(
"{{block|sec|color:red}}\nx\n{{end-block}}"),
"<div id=\"sec\" style=\"color:red\">\n<p>x</p>\n</div>\n",
"block wraps its content in an identified div");
}
/**
* Math is wrapped in backticks for the page's math renderer to pick
* up, and its content is left as written.
*/
public function parseMathTestCase()
{
$parser = new L\WikiParser("/b/");
$this->assertEqual($parser->parse("say <math>e=mc^2</math>"),
"<p>say `e=mc^2`</p>\n", "math is wrapped in backticks");
}
/**
* A citation becomes a numbered footnote marker, and the citations
* gather into a reference list at the foot of the page.
*/
public function parseReferencesTestCase()
{
$parser = new L\WikiParser("/b/");
$html = $parser->parse(
"A claim{{cite|title=The Book|author=Jane}}.");
$this->assertTrue(strpos($html,
"<sup id=\"cite_1\"><a href=\"#ref_1\">[1]</a></sup>") !==
false, "a citation becomes a numbered footnote marker");
$this->assertTrue(strpos($html,
"<li id=\"ref_1\"><a href=\"#cite_1\">^</a> The Book, Jane") !==
false, "the citation gathers into the reference list");
}
/**
* Tests that a nowiki span inside a pre block, whether the block comes
* from pre tags or from leading spaces, has its tags dropped and its
* characters kept, and that a pre close inside such a span does not end
* the block.
*/
public function parseNowikiInPreTestCase()
{
$parser = new L\WikiParser("/b/");
$tagged = $parser->parse(
"<pre>foo <nowiki>bar</nowiki> baz</pre>\n", false, false, 0);
$this->assertTrue(strpos($tagged, "<pre>foo bar baz</pre>") !==
false, "nowiki tags drop inside a pre block");
$escaped = $parser->parse(
"<pre>x <nowiki><b>y</b></nowiki> z</pre>\n", false, false, 0);
$this->assertTrue(strpos($escaped, "<b>y</b>") !==
false, "nowiki contents stay literal inside a pre block");
$inside = $parser->parse(
"<pre>a <nowiki>k</pre>m</nowiki> b</pre>\ntail\n", false,
false, 0);
$this->assertTrue(strpos($inside, "k</pre>m b</pre>") !==
false, "a pre close inside nowiki does not end the block");
$spaced = $parser->parse(" foo <nowiki>bar</nowiki> baz\n", false,
false, 0);
$this->assertTrue(strpos($spaced, "<pre>foo bar baz</pre>") !==
false, "nowiki tags drop inside a space-led pre block");
}
/**
* A nowiki span that opens on a space-led preformatted line but closes
* on a later line, even one that does not open with a space, keeps the
* run going to the close: the tags drop, the gathered lines show
* verbatim in one pre block, and text after the close returns to
* normal parsing rather than the opener being left literal.
*/
public function parseSpacePreMultilineNowikiTestCase()
{
$parser = new L\WikiParser("/b/");
$document = " <nowiki>{{block-class=\"nb\"\n" .
"line two\n}}</nowiki>\nafter\n";
$html = $parser->parse($document, false, false, 0);
$this->assertTrue(strpos($html,
"<pre>{{block-class="nb"\nline two\n}}</pre>") !==
false, "a multi-line nowiki in a space pre stays one pre block");
$this->assertTrue(strpos($html, "<p>after</p>") !== false,
"text after the nowiki close returns to normal parsing");
}
/**
* Writing class or id on its own wraps text in a span so it keeps
* flowing, while the block- forms of either wrap it in a block of
* its own wherever they are written.
*/
public function blockIdTestCase()
{
$parser = new L\WikiParser("");
$html = $parser->parse('{{block-id="some_css_id" a block}}',
false, true);
$this->assertTrue(strpos($html,
'<div id="some_css_id">') !== false,
"block-id wraps its text in a div carrying that id");
$html = $parser->parse('words {{block-id="mid" a block}} words',
false, true);
$this->assertTrue(strpos($html, '<div id="mid">') !== false,
"block-id makes a block even written in the middle of a line");
$html = $parser->parse('words {{id="sid" a span}} words', false,
true);
$this->assertTrue(strpos($html, '<span id="sid">') !== false,
"id on its own keeps its text flowing within the line");
$html = $parser->parse('words {{block-class="bc" a block}} words',
false, true);
$this->assertTrue(strpos($html, '<div class="bc">') !== false,
"block-class still makes a block carrying that class");
$html = $parser->parse(
'words {{block-style="color:red" a block}} words', false, true);
$this->assertTrue(strpos($html, '<div style="color:red">') !== false,
"block-style wraps its text in a block carrying that style");
$html = $parser->parse('words {{style="color:red" a span}} words',
false, true);
$this->assertTrue(strpos($html, '<span style="color:red">') !== false,
"style on its own keeps its text flowing within the line");
}
/**
* Tests that a safe run of attributes at a table cell's wall, before
* the bar that ends it, is kept as that cell's attributes, that text
* left over after the attributes stays as cell content rather than
* being dropped, that a plain row is still split into cells, and that
* an unsafe attribute at the wall is dropped.
*/
public function parseCellAttributesTestCase()
{
$parser = new L\WikiParser("/b/");
$styled = $parser->parse(
"{|\n|style=\"color:red\"|content\n|}\n", false, false, 0);
$this->assertTrue(strpos($styled,
"<td style=\"color:red\">content</td>") !== false,
"a safe attribute run at the wall sets the cell attributes");
$leftover = $parser->parse(
"{|\n|style=\"text-align:right;\" a| hi yo\n|}\n", false,
false, 0);
$this->assertTrue(strpos($leftover,
"<td style=\"text-align:right;\">a hi yo</td>") !== false,
"text after the attributes stays as cell content");
$plain = $parser->parse("{|\n| one | two\n|}\n", false, false, 0);
$this->assertTrue(strpos($plain, "<td>one</td><td>two</td>") !==
false, "a plain row is still split into cells");
$unsafe = $parser->parse(
"{|\n|style=\"x\"onmouseover=\"e()\"|c\n|}\n", false, false, 0);
$this->assertTrue(strpos($unsafe, "onmouseover") === false,
"an unsafe attribute at the wall is dropped");
}
/**
* Tests that a rule added to the parser is tried both within a line and
* at the start of a block, and that the html it returns reaches the
* output as html rather than being escaped as text.
*/
public function parseExtraRuleTestCase()
{
$parser = new L\WikiParser("/b/");
$parser->addRule(function ($text, $index) {
$trigger = "[[Pic:";
if (substr_compare($text, $trigger, $index,
strlen($trigger)) !== 0) {
return null;
}
$end = strpos($text, "]]", $index);
if ($end === false) {
return null;
}
$name = substr($text, $index + strlen($trigger),
$end - $index - strlen($trigger));
return ["html" => "<a href=\"/p/$name\">$name</a>",
"next" => $end + 2];
});
$inline = $parser->parse("see [[Pic:Cat]] here\n", false, false, 0);
$this->assertTrue(strpos($inline, "<a href=\"/p/Cat\">Cat</a>") !==
false, "an added rule fires within a line and its html stays");
$block = $parser->parse("[[Pic:Dog]]\n", false, false, 0);
$this->assertTrue(strpos($block, "<a href=\"/p/Dog\">Dog</a>") !==
false, "an added rule fires at the start of a block");
$bare = new L\WikiParser("/b/");
$raw = $bare->parse("raw <a href=\"x\">y</a>\n", false, false, 0);
$this->assertTrue(strpos($raw, "<a href") !== false,
"without a rule an attribute-bearing tag is escaped");
}
/**
* Tests that a form template followed on its line by inline content,
* such as a line break, is emitted as its own block with that content
* kept after it rather than wrapped as a paragraph, so a dropdown's
* select and a data block's option list are not split apart by a stray
* paragraph tag landing inside them.
*/
public function parseFormTrailingTestCase()
{
$parser = new L\WikiParser("/b/");
$dropdown = $parser->parse(
"{{r-dropdown|Units|unit-list}}\n{{end-r-dropdown}}<br>\n");
$this->assertTrue(strpos($dropdown, "<p></select>") === false,
"a dropdown close with a trailing break keeps the select whole");
$this->assertTrue(strpos($dropdown, "</select>") !== false &&
strpos($dropdown, "</div><br>") !== false,
"the trailing break is kept after the dropdown block");
$block = $parser->parse(
"{{data-block|x}}\n{{option|1|1}}\n{{end-data-block}}<br>\n");
$this->assertTrue(strpos($block, "<p></x-data-block>") === false,
"a data block close with a trailing break stays whole");
$field = $parser->parse("{{r-textfield|Name:|Name}}<br>\n");
$this->assertTrue(
strpos($field, "<p><div class='csv-form-field'>") === false,
"a field with a trailing break is not wrapped in a paragraph");
}
/**
* Tests that markup nested past the parser's depth limit is passed
* through as escaped literal text rather than driving the block and
* inline readers to call themselves until the call stack is spent.
*/
public function parseDepthGuardTestCase()
{
$parser = new L\WikiParser("/b/");
$open = "";
$close = "";
for ($level = 0; $level < 40; $level++) {
$open .= "{{block|n$level|}}\n";
$close = "\n{{end-block}}" . $close;
}
$deep_blocks = $parser->parse($open . "core" . $close,
false, false, 0);
$this->assertTrue(strlen($deep_blocks) > 0,
"deeply nested blocks return output instead of exhausting " .
"the call stack");
$this->assertTrue(strpos($deep_blocks, "{{end-block}}") !== false,
"blocks nested past the depth limit are left as literal text");
$deep_links = $parser->parse(str_repeat("[[a|", 40) . "core" .
str_repeat("]]", 40), false, false, 0);
$this->assertTrue(strlen($deep_links) > 0,
"deeply nested links return output instead of exhausting " .
"the call stack");
}
/**
* Checks that a code block is read as one verbatim unit whether it
* holds a user-agent line laid out over several lines or a pre tag,
* so its opening and closing code tags are kept and never left as
* stray text, and a line break inside does not tear the block apart.
* This covers two reported bugs: a multi-line code example that leaked
* a close code tag, and a code block wrapping a pre tag that broke on
* its line breaks. The last case checks that text after a pre or code
* close on the same line becomes a paragraph, not a second block.
*
* @return void asserts the reported inputs render as whole blocks
*/
public function parseCodeBlockTestCase()
{
$parser = new L\WikiParser("/b/");
$agent = $parser->parse("<code>\n Mozilla/5.0 (compatible; " .
"NAME_FROM_THIS_FIELD; YOUR_SITES_URL/bot)\n</code>\n", false,
false, 0);
$this->assertEqual($agent, "<code>\n Mozilla/5.0 (compatible; " .
"NAME_FROM_THIS_FIELD; YOUR_SITES_URL/bot)\n</code>\n",
"a multi-line code block keeps its tags and leaks no close tag");
$this->assertTrue(strpos($agent, "<pre>") === false,
"an indented line inside a code block is not read as a pre");
$wrapped = $parser->parse("<code><pre>lalala</pre></code>\n",
false, false, 0);
$this->assertEqual($wrapped,
"<code><pre>lalala</pre></code>\n",
"a pre tag inside a code block is shown as literal text");
$multiline = $parser->parse(
"<code>\n<pre>\nlalala\n</pre>\n</code>\n", false, false, 0);
$this->assertEqual($multiline,
"<code>\n<pre>\nlalala\n</pre>\n</code>\n",
"a code block wrapping a pre tag survives its line breaks");
$trailing = $parser->parse("<pre>foo</pre> and more\n", false,
false, 0);
$this->assertEqual($trailing, "<pre>foo</pre>\n<p> and more</p>\n",
"text after a pre close is a paragraph, not a second pre");
}
}