/ tests / WikiParserTest.php
<?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 &lt;script&gt;x&lt;/script&gt; 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 &lt;nowiki&gt; 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>&lt;math&gt;</nowiki>",
            false, false, 0);
        $this->assertTrue(strpos($html, "&lt;") === false,
            "an already-escaped entity is not escaped again");
        $this->assertTrue(strpos($html, "&lt;math&gt;") !== 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\">&nbsp;</span>x</p>\n",
            "one colon is indent level one");
        $this->assertEqual($parser->parse("::::: z"),
            "<p><span class=\"indent4\">&nbsp;</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, "&lt;b&gt;y&lt;/b&gt;") !==
            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&lt;/pre&gt;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=&quot;nb&quot;\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, "&lt;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>&lt;pre&gt;lalala&lt;/pre&gt;</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&lt;pre&gt;\nlalala\n&lt;/pre&gt;\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 &lt; 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("![a](/i.png)"),
            "<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("![a](Public@i.png)"),
            "<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>&lt;script&gt;x&lt;/script&gt;</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 &lt; 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\">&#8617;</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");
    }
}
X