<?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;
use seekquarry\yioop\library\wiki as LW;
/**
* 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
{
/**
* A form field a page marks as required writes that down for
* itself, whichever kind of field it is. A required ordering or
* choose-some field left that off, so the site read such a form
* back as though nothing had been asked for and took an empty
* answer as a filled-in one.
*/
public function requiredFieldsSayTheyAreRequiredTestCase()
{
$parser = new LW\WikiParser("/b/");
$marks = ["{{r-textfield|Your name|who|40}}" => "r-textfield",
"{{r-textarea|Why|why}}" => "r-textarea",
"{{r-dropdown|pick}}{{option|One|1}}{{end-r-dropdown}}" =>
"r-textfield",
"{{r-sorter|order|block}}{{option|One|1}}{{end-r-sorter}}" =>
"r-sorter",
"{{r-choosek|some|2|block}}{{option|One|1}}" .
"{{end-r-choosek}}" => "r-choosek"];
foreach ($marks as $mark => $kind) {
$drawn = $parser->parse($mark);
$this->assertTrue(strpos($drawn, "value='$kind'") !== false,
"a form asking for $kind writes down that it is asked");
$this->assertTrue(strpos($drawn, "csv-star") !== false,
"and shows the star that says so on the page");
}
}
/**
* 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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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",
"a table keeps only its class and style 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 LW\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 LW\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 LW\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,
"the drawn page holds no carriage return");
}
/**
* 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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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 LW\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");
}
/**
* Leading-hash headings map to their level and are given an id down to
* level three; seven hashes are too many to be a heading and stay a
* paragraph, and an underline of equals signs makes a setext heading.
*/
public function headingsTestCase()
{
$parser = new LW\WikiParser("/b/");
$this->assertEqual($parser->parseMarkdown("# One\n## Two"),
"<h1 id=\"One\">One</h1>\n<h2 id=\"Two\">Two</h2>",
"leading hashes set the heading level and an id");
$this->assertEqual($parser->parseMarkdown("###### Six"),
"<h6 id=\"Six\">Six</h6>",
"a level six heading is given an id like the rest");
$this->assertEqual($parser->parseMarkdown("####### G"),
"<p>####### G</p>",
"seven hashes are too many to be a heading");
$this->assertEqual($parser->parseMarkdown("Title\n====="),
"<h1 id=\"Title\">Title</h1>",
"an equals underline makes a setext heading");
}
/**
* Stars and underscores give italics and bold, double tildes give
* strike-through, and back-ticks give inline code with its contents
* shown verbatim.
*/
public function emphasisTestCase()
{
$parser = new LW\WikiParser("/b/");
$this->assertEqual(
$parser->parseMarkdown("**b** *i* ~~s~~ `c`"),
"<p><strong>b</strong> <em>i</em> <del>s</del> " .
"<code>c</code></p>",
"bold, italic, strike, and code render inline");
}
/**
* A fenced code block keeps its lines verbatim, escapes html-special
* characters, and names its language in a class from the info word.
*/
public function fencedCodeTestCase()
{
$parser = new LW\WikiParser("/b/");
$this->assertEqual(
$parser->parseMarkdown("```php\n\$x = 1 < 2;\n```"),
"<pre><code class=\"language-php\">\$x = 1 < 2;" .
"</code></pre>",
"a fence keeps code verbatim and escapes it");
}
/**
* Links and images carry their destination, an angle-bracket url or
* email becomes a link, a destination whose scheme could run script is
* dropped to an empty destination, and a group@page link destination
* becomes a cross-group marker while an image's is not.
*/
public function linksAndImagesTestCase()
{
$parser = new LW\WikiParser("/b/");
$this->assertEqual(
$parser->parseMarkdown("[t](https://x.com)"),
"<p><a href=\"https://x.com\">t</a></p>",
"a link carries its destination");
$this->assertEqual($parser->parseMarkdown(""),
"<p><img src=\"/i.png\" alt=\"a\"></p>",
"an image carries its source and alt text");
$this->assertEqual($parser->parseMarkdown("<https://a.org>"),
"<p><a href=\"https://a.org\">https://a.org</a></p>",
"an angle-bracket url becomes a link");
$this->assertEqual($parser->parseMarkdown("<me@x.com>"),
"<p><a href=\"mailto:me@x.com\">me@x.com</a></p>",
"an angle-bracket email becomes a mailto link");
$this->assertEqual(
$parser->parseMarkdown("[x](javascript:alert(1))"),
"<p><a href=\"\">x</a></p>",
"an unsafe scheme is dropped to an empty destination");
$this->assertEqual(
$parser->parseMarkdown("[Syntax](Public@Syntax)"),
"<p><a href=\"@@Public@Syntax@@\">Syntax</a></p>",
"a group@page link destination becomes a cross-group marker");
$this->assertEqual(
$parser->parseMarkdown(""),
"<p><img src=\"Public@i.png\" alt=\"a\"></p>",
"an image destination is not turned into a marker");
}
/**
* Bullets make an unordered list, numbers an ordered one, and a more
* indented bullet nests a list inside the item above it.
*/
public function listsTestCase()
{
$parser = new LW\WikiParser("/b/");
$this->assertEqual($parser->parseMarkdown("- a\n- b"),
"<ul>\n<li>a</li>\n<li>b</li>\n</ul>",
"bullets make an unordered list");
$this->assertEqual($parser->parseMarkdown("1. a\n2. b"),
"<ol>\n<li>a</li>\n<li>b</li>\n</ol>",
"numbers make an ordered list");
$this->assertEqual(
$parser->parseMarkdown("- a\n - a1\n- b"),
"<ul>\n<li>a\n<ul>\n<li>a1</li>\n</ul></li>\n<li>b</li>\n</ul>",
"an indented bullet nests a list in the item above");
}
/**
* A pipe table reads its header, takes each column's alignment from the
* divider row's colons, and fills its body rows.
*/
public function tableTestCase()
{
$parser = new LW\WikiParser("/b/");
$this->assertEqual(
$parser->parseMarkdown("| A | B |\n|:--|--:|\n| 1 | 2 |"),
"<table>\n<thead>\n<tr><th style=\"text-align:left\">A</th>" .
"<th style=\"text-align:right\">B</th></tr>\n</thead>\n" .
"<tbody>\n<tr><td style=\"text-align:left\">1</td>" .
"<td style=\"text-align:right\">2</td></tr>\n</tbody>\n</table>",
"a pipe table takes alignment from the divider colons");
}
/**
* A greater-than sign quotes a line, and a run of dashes on its own
* makes a horizontal rule between blocks.
*/
public function blockQuoteAndRuleTestCase()
{
$parser = new LW\WikiParser("/b/");
$this->assertEqual($parser->parseMarkdown("> q1\n> q2"),
"<blockquote><p>q1\nq2</p></blockquote>",
"greater-than signs make a block quote");
$this->assertEqual($parser->parseMarkdown("a\n\n---\n\nb"),
"<p>a</p>\n<hr>\n<p>b</p>",
"a run of dashes makes a horizontal rule");
}
/**
* The wiki brace templates render the same in markdown as on a wiki
* page: an inline toggle, a centered block, and a wrapping block whose
* inner text is itself read as markdown.
*/
public function templateReuseTestCase()
{
$parser = new LW\WikiParser("/b/");
$this->assertEqual(
$parser->parseMarkdown("{{toggle|b1|show}}"),
"<p><a href=\"javascript:toggleDisplay('b1')\">show</a></p>",
"an inline toggle template renders in markdown");
$this->assertEqual($parser->parseMarkdown("{{center|x}}"),
"<div class=\"center\">\n<p>x</p></div>",
"a centered block template renders in markdown");
$this->assertEqual($parser->parseMarkdown(
"{{block|myid|color:red}}\ninside **bold**\n{{end-block}}"),
"<div id=\"myid\" style=\"color:red\">\n" .
"<p>inside <strong>bold</strong></p></div>",
"a wrapping block reads its inner text as markdown");
}
/**
* A safe inline tag passes through, an unsafe tag is escaped so it shows
* as text, a backslash escapes a markup character, and a dollar span
* becomes inline code.
*/
public function inlineTagAndEscapeTestCase()
{
$parser = new LW\WikiParser("/b/");
$this->assertEqual($parser->parseMarkdown("<sub>2</sub>"),
"<p><sub>2</sub></p>",
"a safe inline tag passes through");
$this->assertEqual(
$parser->parseMarkdown("<script>x</script>"),
"<p><script>x</script></p>",
"an unsafe tag is escaped to text");
$this->assertEqual($parser->parseMarkdown("\\*x\\*"),
"<p>*x*</p>",
"a backslash escapes a markup character");
$this->assertEqual($parser->parseMarkdown("\$a^2\$"),
"<p><code>a^2</code></p>",
"a dollar span becomes inline code");
}
/**
* A run of lines indented four spaces is an indented code block, its
* text escaped and shown verbatim, and such indentation does not break
* a paragraph it follows without a blank line.
*/
public function indentedCodeTestCase()
{
$parser = new LW\WikiParser("/b/");
$this->assertEqual(
$parser->parseMarkdown("text\n\n code < x\n\nmore"),
"<p>text</p>\n<pre><code>code < x</code></pre>\n<p>more</p>",
"four-space indent makes an escaped code block");
}
/**
* A reference-style link resolves against a definition given elsewhere
* on the page, whether named in full, collapsed, or by shortcut, and a
* reference image works the same way.
*/
public function referenceLinkTestCase()
{
$parser = new LW\WikiParser("/b/");
$this->assertEqual($parser->parseMarkdown(
"[the docs][docs]\n\n[docs]: https://x.com \"Docs\""),
"<p><a href=\"https://x.com\" title=\"Docs\">the docs</a></p>",
"a full reference link resolves with its title");
$this->assertEqual($parser->parseMarkdown(
"[docs]\n\n[docs]: https://x.com"),
"<p><a href=\"https://x.com\">docs</a></p>",
"a shortcut reference link resolves");
$this->assertEqual($parser->parseMarkdown(
"![logo][img]\n\n[img]: /logo.png"),
"<p><img src=\"/logo.png\" alt=\"logo\"></p>",
"a reference image resolves");
}
/**
* A footnote reference becomes a small numbered link and the footnote's
* text is listed at the foot of the page with a link back up; numbers
* follow the order footnotes are first used.
*/
public function footnoteTestCase()
{
$parser = new LW\WikiParser("/b/");
$this->assertEqual($parser->parseMarkdown("a[^1]\n\n[^1]: note"),
"<p>a<sup class=\"footnote-ref\"><a href=\"#fn-1\" " .
"id=\"fnref-1\">1</a></sup></p>\n<section class=\"footnotes\">" .
"<ol>\n<li id=\"fn-1\">note <a href=\"#fnref-1\">↩</a>" .
"</li>\n</ol></section>",
"a footnote links to a numbered note at the foot");
}
/**
* Emphasis follows the flanking rules: underscores inside a word do not
* emphasize, stars with a space on each side do not, but a star inside a
* word still does.
*/
public function emphasisFlankingTestCase()
{
$parser = new LW\WikiParser("/b/");
$this->assertEqual(
$parser->parseMarkdown("snake_case_word here"),
"<p>snake_case_word here</p>",
"underscores inside a word do not emphasize");
$this->assertEqual($parser->parseMarkdown("a * b * c"),
"<p>a * b * c</p>",
"stars with spaces on both sides do not emphasize");
$this->assertEqual($parser->parseMarkdown("foo*bar*baz"),
"<p>foo<em>bar</em>baz</p>",
"a star inside a word still emphasizes");
}
/**
* The delimiter-stack pass nests and splits emphasis the way GitHub's
* markdown does: a triple run nests emphasis inside strong, and the rule
* of three keeps adjacent runs from pairing across a boundary.
*/
public function emphasisNestingTestCase()
{
$parser = new LW\WikiParser("/b/");
$this->assertEqual($parser->parseMarkdown("***foo***"),
"<p><em><strong>foo</strong></em></p>",
"a triple run nests emphasis inside strong");
$this->assertEqual($parser->parseMarkdown("**foo**bar**baz**"),
"<p><strong>foo</strong>bar<strong>baz</strong></p>",
"the rule of three keeps two strong runs apart");
$this->assertEqual($parser->parseMarkdown("*foo**bar**baz*"),
"<p><em>foo<strong>bar</strong>baz</em></p>",
"strong nests inside emphasis");
}
/**
* Deeply nested markdown does not run the call stack out: the parser
* bails to escaped text once it has nested past its depth limit and
* still returns a result.
*/
public function depthGuardTestCase()
{
$parser = new LW\WikiParser("/b/");
$quotes = str_repeat("> ", 60) . "deep";
$this->assertTrue(
strlen($parser->parseMarkdown($quotes)) > 0,
"deeply nested quotes return without a stack overflow");
$templates = str_repeat("{{center|", 60) . "x" .
str_repeat("}}", 60);
$this->assertTrue(
strlen($parser->parseMarkdown($templates)) > 0,
"deeply nested templates return without a stack overflow");
}
/**
* A block-class directive always makes a div, in a block or inline. A
* link splits on its first top-level bar, so a resource that carries
* its own bar stays whole in the shown text while the three field
* relationship form still points at its middle field. A heading may
* run over several lines, closing on a later line, and still holds the
* inline links and spans written across those lines.
*/
public function blockClassAndMultilineHeadingTestCase()
{
$parser = new LW\WikiParser("/b/");
$this->assertEqual(
$parser->parse("{{block-class=\"box\"\nhi there\n}}", false),
"<div class=\"box\">\n<p>hi there</p>\n</div>\n",
"a block-class directive wraps its block in a div");
$this->assertEqual(
$parser->parse("a {{block-class=\"tag\" b}} c", false),
"<p>a <div class=\"tag\">b</div> c</p>\n",
"a block-class directive inline is still a div");
$this->assertEqual(
$parser->parse("[[https://www.frise.org/| " .
"((resource-nolink:FriseGuy.png|FRISE))]]", false),
"<p><a href=\"https://www.frise.org/\"> " .
"((resource-nolink:FriseGuy.png|FRISE))</a></p>\n",
"a bar inside a resource does not split the link");
$this->assertEqual(
$parser->parse("[[isa|Dog|the dog]]", false),
"<p><a href=\"/b/Dog\">the dog</a></p>\n",
"the three field link points at its middle field");
$this->assertEqual(
$parser->parse("=one\ntwo=", false),
"<h1 id=\"one two\">one\ntwo</h1>\n",
"a heading may close on a later line");
$title = "{{block-class=\"title-stack\" \n" .
"=[[https://www.frise.org/|FRISE]] - the\n" .
"{{class=\"outline\" FR}}ee\n" .
"{{class=\"outline\" I}}teractive\n" .
"{{class=\"outline\" S}}tory\n" .
"{{class=\"outline\" E}}ngine\n" .
"=\n}}";
$this->assertEqual($parser->parse($title, false),
"<div class=\"title-stack\">\n" .
"<h1 id=\"FRISE - the FRee Iteractive Story Engine \">" .
"<a href=\"https://www.frise.org/\">FRISE</a> - the\n" .
"<span class=\"outline\">FR</span>ee\n" .
"<span class=\"outline\">I</span>teractive\n" .
"<span class=\"outline\">S</span>tory\n" .
"<span class=\"outline\">E</span>ngine\n" .
"</h1>\n</div>\n",
"a multi-line heading of links and spans sits in a div");
}
/**
* Folder holding the saved reference output, one file per case.
*/
const CORPUS_DIR = __DIR__ . "/test_files/wiki_corpus";
/**
* The corpus: a name-keyed list of cases, each giving the render
* engine to use and the wiki source to feed the parser. Kept as a
* static method so the reference-writing tool can read the same list
* the test reads.
*
* @return array the corpus cases keyed by name
*/
public static function corpus()
{
return [
"mw_headings" => ["engine" => 0, "source" =>
"= One =\n== Two ==\n=== Three ===\n==== Four ====\n" .
"===== Five =====\n====== Six ======\n"],
"mw_format" => ["engine" => 0, "source" =>
"''italic'' and '''bold''' and '''''both'''''\n"],
"mw_htmltags" => ["engine" => 0, "source" =>
"<u>u</u> <sub>sub</sub> <sup>sup</sup> <del>del</del> " .
"<ins>ins</ins> <s>s</s> <tt>tt</tt>\n"],
"mw_ulist" => ["engine" => 0, "source" =>
"* a\n** b\n** c\n*** d\n* e\n"],
"mw_olist" => ["engine" => 0, "source" =>
"# a\n## b\n## c\n# d\n"],
"mw_dlist" => ["engine" => 0, "source" =>
";Term 1: Definition 1\n;Term 2: Definition 2\n"],
"mw_table" => ["engine" => 0, "source" =>
"{| class=\"wikitable\"\n|+ Caption\n!a!!b\n|-\n" .
"|c||d\n|}\n"],
"mw_links" => ["engine" => 0, "source" =>
"[[Page|text]] and [[http://example.com/|ext]] and " .
"[[group@Page|inter]] and [[#anchor|jump]]\n"],
"mw_style" => ["engine" => 0, "source" =>
"{{center|middle}}\n\n{{style=\"color:red\" red text}}\n"],
"mw_pre_nowiki" => ["engine" => 0, "source" =>
"<pre>preformatted '''not bold'''</pre>\n"],
/* known-bad: the fourth list level renders as a literal star */
"mw_nest_list4" => ["engine" => 0, "source" =>
"* a\n** b\n*** c\n**** d\n"],
/* known-bad: a table inside a list item stays literal text */
"mw_nest_table_in_list" => ["engine" => 0, "source" =>
"* before\n* {|\n|a||b\n|}\n* after\n"],
/* known-bad: a link inside a heading is not stripped for the
table of contents and the heading fails to render */
"mw_toc_link" => ["engine" => 0, "source" =>
"== [[Page|Linked Heading]] ==\nb1\n\n== Second ==\nb2\n" .
"\n== Third ==\nb3\n\n== Fourth ==\nb4\n"],
"md_basic" => ["engine" => 1, "source" =>
"# One\n## Two\n\n**bold** and *italic*\n\n- a\n- b\n"],
"md_fence" => ["engine" => 1, "source" =>
"text\n\n```php\n\$x = 1;\n```\n\nmore `inline` code\n"],
"md_blockquote" => ["engine" => 1, "source" =>
"> quoted line one\n> quoted line two\n"],
];
}
/**
* Parses one corpus case the way the wiki controller would show it. A
* wiki case is cleaned and then parsed, the way stored wiki text is; a
* markdown case is parsed straight from its source, the way a git readme
* blob is, since that path does no cleaning of the text beforehand.
*
* @param array $case one corpus entry with engine and source keys
* @return string the parser's html output for that case
*/
public static function renderCase($case)
{
$parser = new LW\WikiParser();
if ($case["engine"] == 1) {
return $parser->parse($case["source"], false, false, 1);
}
$controller = new SearchController();
$cleaned = $controller->clean($case["source"], "string");
return $parser->parse($cleaned, false, false, $case["engine"]);
}
/**
* Writes the reference file for every corpus case. This is a tool for
* regenerating the saved output after an intended parser change, not a
* test; the test reads what this writes. Review the git diff of the
* reference folder afterward.
*
* @return void the reference files are written to CORPUS_DIR
*/
public static function writeReferences()
{
if (!is_dir(self::CORPUS_DIR)) {
mkdir(self::CORPUS_DIR, 0777, true);
}
foreach (self::corpus() as $name => $case) {
file_put_contents(self::CORPUS_DIR . "/" . $name . ".html",
self::renderCase($case));
}
}
/**
* Checks that every corpus case still parses to its saved reference
* output. A difference means the parser's output changed for that
* case, which is exactly what the coming rewrite must account for.
*/
public function corpusParityTestCase()
{
foreach (self::corpus() as $name => $case) {
$reference_file = self::CORPUS_DIR . "/" . $name . ".html";
$expected = is_file($reference_file) ?
file_get_contents($reference_file) : "MISSING REFERENCE";
$actual = self::renderCase($case);
$this->assertEqual($actual, $expected,
"corpus case '$name' matches its saved reference");
}
}
/**
* linkMayNameALanguageTestCase checks that a wiki link saying
* which language of a page it wants reaches that language. A writer
* writes the language after a question mark, as in
* Endorsements?vi-VN, and a page has a copy in each language. Read
* as part of the name, the whole of it named no page at all, and
* where the address it joined was already a query the mark opened a
* second one.
*/
public function linkMayNameALanguageTestCase()
{
$parser = new LW\WikiParser(
"/group/9?a=wiki&page_name=", [], true);
$drawn = $parser->parse("[[Endorsements?vi-VN|Ung Ho]]",
false, true);
$this->assertTrue(str_contains($drawn,
"page_name=Endorsements[{locale}]vi-VN"),
"the language is carried as a stand-in after the name");
$parser = new LW\WikiParser("/group/9/", [], true);
$drawn = $parser->parse("[[Endorsements?l=vi-VN|Ung Ho]]",
false, true);
$this->assertTrue(str_contains($drawn,
"Endorsements[{locale}]vi-VN"),
"the older spelling is read the same way");
$drawn = $parser->parse("[[What?|A mark in the name]]",
false, true);
$this->assertTrue(str_contains($drawn, "/group/9/What?"),
"a name ending in a mark is left as it stands");
}
/**
* presentationIsReadAsSlidesTestCase checks that a page whose kind
* is a presentation comes back wrapped in the marks a slide deck is
* drawn from, with one slide for each stretch between the four-dot
* separators the edit bar writes. Each slide's own text goes through
* the ordinary block parse, so a heading and a list inside a slide
* are drawn the way they are anywhere else.
*/
public function presentationIsReadAsSlidesTestCase()
{
$parser = new LW\WikiParser("http://localhost/");
$drawn = $parser->parse("page_type=presentation\n\n" .
"END_HEAD_VARS=First=\n* Point one\n* Point two\n....\n" .
"=Second=\n# Ordered one\n");
$this->assertEqual(1, substr_count($drawn, "<x-slides>"),
"the whole deck is wrapped once");
$this->assertEqual(2, substr_count($drawn, "<x-slide>"),
"each stretch between separators is its own slide");
$this->assertTrue(!str_contains($drawn, "...."),
"the separator itself is not drawn");
$this->assertTrue(str_contains($drawn, "<li>Point two</li>"),
"a list running up to a separator ends at it");
}
/**
* pageThatIsNotAPresentationKeepsNoSlideMarksTestCase checks that a
* page of any other kind is drawn as it always was, so a four-dot
* line on an ordinary page stays ordinary text and no deck marks are
* added around it.
*/
public function pageThatIsNotAPresentationKeepsNoSlideMarksTestCase()
{
$parser = new LW\WikiParser("http://localhost/");
$drawn = $parser->parse("=Head=\n* a\n....\n* b\n");
$this->assertEqual(0, substr_count($drawn, "<x-slides>"),
"an ordinary page is not wrapped as a deck");
$this->assertEqual(0, substr_count($drawn, "<x-slide>"),
"and holds no slides");
}
/**
* markdownPresentationIsReadAsSlidesTestCase checks the markdown
* engine reads the same separator into the same marks, so a writer
* moving between the two engines writes slides one way.
*/
public function markdownPresentationIsReadAsSlidesTestCase()
{
$parser = new LW\WikiParser("http://localhost/");
$drawn = $parser->parseMarkdown("# First\n\n- one\n\n" .
"....\n\n# Second\n\n1. two\n", false, "presentation");
$this->assertEqual(1, substr_count($drawn, "<x-slides>"),
"the whole deck is wrapped once");
$this->assertEqual(2, substr_count($drawn, "<x-slide>"),
"each stretch between separators is its own slide");
$this->assertTrue(!str_contains($drawn, "...."),
"the separator itself is not drawn");
}
}