/ 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;

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