/ tests / WikiParserJavascriptTest.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\library\JavascriptUnitTest;
use seekquarry\yioop\library\WikiParser;

/**
 * Checks that the Javascript wiki parser in scripts/wiki_parser.js renders wiki
 * inputs to the same html the server-side WikiParser produces. The two
 * parsers are a port of one another and are meant to agree character for
 * character, so the server parser renders each input to get the html the
 * Javascript parser is expected to match. There are two groups of cases:
 * the code block cases Chris reported, where a multi-line code example used
 * to leak its close code tag and a code block wrapping a pre tag used to
 * break on its line breaks, and a broader group covering headings,
 * emphasis, links, lists, tables, and other everyday markup. In a browser
 * each group shows a pass or fail table; from the command line the
 * JavascriptUnitTest runner replays the same check under node.
 *
 * @author Chris Pollett
 */
class WikiParserJavascriptTest extends JavascriptUnitTest
{
    /**
     * Checks the code and pre block cases Chris reported plus the plain
     * inline code and pre cases they grew out of.
     *
     * @return string html and Javascript that checks each case in a
     *      browser or, from the command line, under node
     */
    public function codeBlockTestCase()
    {
        $inputs = [
            ["inline code", "see <code>foo</code> here\n"],
            ["pre block", "<pre>x</pre>\n"],
            ["user agent code block",
                "<code>\n Mozilla/5.0 (compatible; NAME_FROM_THIS_FIELD; " .
                "YOUR_SITES_URL/bot)\n</code>\n"],
            ["code around pre on one line",
                "<code><pre>lalala</pre></code>\n"],
            ["code around pre on many lines",
                "<code>\n<pre>\nlalala\n</pre>\n</code>\n"],
            ["text after a pre close", "<pre>foo</pre> and more\n"],
        ];
        return $this->renderCases("wikiParserCodeBlock",
            "codeBlockTestCase", $inputs);
    }
    /**
     * Checks a broad set of everyday wiki markup so the port is exercised
     * beyond the code block cases.
     *
     * @return string html and Javascript that checks each case in a
     *      browser or, from the command line, under node
     */
    public function parseCorpusTestCase()
    {
        $inputs = [
            ["heading", "== Sub =="],
            ["italic", "''i''"],
            ["bold", "'''b'''"],
            ["internal link", "[[Page|text]]"],
            ["external link", "[[http://x/|e]]"],
            ["nested list", "* a\n** b\n"],
            ["ordered list", "# one\n# two\n"],
            ["definition list", "; term : def\n"],
            ["table", "{|\n! H\n|-\n| c\n|}\n"],
            ["form text field", "{{textfield|Your Name|name}}"],
            ["form field with a break after it",
                "{{r-textfield|Email:|Email}}<br>"],
            ["form field that must be filled",
                "{{r-textfield|Your Name|name}}"],
            ["form writing box", "{{textarea|Your Story|story}}"],
            ["form check box", "{{checkbox|Agree|agree}}"],
            ["form choice", "{{radio|Yes|choice|yes}}"],
            ["form send button", "{{submit|Send it|send}}"],
            ["form list to pick from",
                "{{dropdown|Pick one|choice}}\n{{option|First|1}}\n" .
                "{{option|Second|2}}\n{{end-dropdown}}"],
            ["form list that must be picked from",
                "{{r-dropdown|Pick one|choice}}\n{{option|First|1}}\n" .
                "{{end-r-dropdown}}"],
            ["form list to put in order",
                "{{sorter|Order these|order}}\n{{option|First|1}}\n" .
                "{{end-sorter}}"],
            ["form list to pick some of",
                "{{choosek|Pick two|picks|2}}\n{{option|First|1}}\n" .
                "{{end-choosek}}"],
            ["form block of kept words",
                "{{data-block|notes}}\nsome words\n{{end-data-block}}"],
            ["form check box with words after",
                "{{lcheckbox|Agree|agree}}"],
            ["form choice with words after", "{{lradio|Yes|choice|yes}}"],
            ["form asking for a signed-in reader", "{{require-signin}}"],
            ["form kept secret", "{{secret-ballot}}"],
            ["form checked before sending", "{{presubmit|checkThem}}"],
            ["form acted on when sent", "{{onsubmit|sendThem}}"],
            ["form shared once edited", "{{share-edited-form}}"],
            ["form keyed to the reader", "{{user-record-form}}"],
            ["form keyed by a hash", "{{hash-record-form}}"],
            ["table holding a named file",
                "{|\n|-\n! Head !! Head\n|-\n" .
                "| ((resource-thumb:a.pdf|Some words)) || Example\n|}"],
            ["nowiki", "<nowiki><b>x</b></nowiki>\n"],
            ["indent", ": in\n"],
            ["horizontal rule", "----\n"],
            ["multibyte", "café ⚙\n"],
            ["block class", "{{block-class=\"nav\"\n* one\n* two\n}}"],
            ["block id", "{{block-id=\"top\"\nsome words\n}}"],
            ["block style",
                "{{block-style=\"color:red\"\nsome words\n}}"],
            ["plain class", "{{class=\"nav\"\n* one\n* two\n}}"],
            ["centered block", "{{center|middle words}}"],
            ["block class with a stored quote",
                "{{block-class=&quot;nav&quot;\n* one\n* two\n}}"],
        ];
        return $this->renderCases("wikiParserCorpus",
            "parseCorpusTestCase", $inputs);
    }
    /**
     * Checks how a file kept with a page is drawn in the preview of a
     * page written in wiki markup. The site draws such a file after it
     * has read the page rather than while reading it, so the parser
     * class leaves the markup alone and the drawing is done by
     * parseWikiContent. Each case here holds one form and what it
     * should become: a picture, a picture standing in a link of its
     * own, a picture drawn without any link, a document in a frame, and
     * anything else as a link to it.
     *
     * @return string html and Javascript that checks the drawing in a
     *      browser or, from the command line, under node
     */
    public function drawsFilesKeptWithPageTestCase()
    {
        ob_start();
        ?>
        <div id="wikiResourceTest"></div>
        <script src="../scripts/basic.js" ></script>
        <script src="../scripts/wiki_parser.js" ></script>
        <script>
        var unit_test_results = [];
        var drawnAs = function (name, source, wanted) {
            var made = parseWikiContent(source, 7, 42, "group",
                "YIOOP_TOKEN", "abc", true, "/");
            return {name: name, pass: made.indexOf(wanted) >= 0 &&
                made.indexOf("((resource") < 0};
        };
        unit_test_results.push(drawnAs("a picture is drawn as a picture",
            "((resource:photo.webp|A photo))",
            '<img src="/wd/resources/YIOOP_TOKEN=abc/7/42/photo.webp"'));
        unit_test_results.push(drawnAs(
            "a thumb stands in a link of its own",
            "((resource-thumb:photo.webp|A photo))",
            '<a class="image-list" href='));
        unit_test_results.push(drawnAs(
            "a thumb draws the picture kept beside the file",
            "((resource-thumb:notes.pdf|The notes))",
            '<img src="/wd/thumbs/YIOOP_TOKEN=abc/7/42/notes.pdf"'));
        unit_test_results.push(drawnAs(
            "a picture without a link is drawn on its own",
            "((resource-nolink:photo.webp|A photo))",
            'class="photo"'));
        unit_test_results.push(drawnAs(
            "a document is shown in a frame of its own",
            "((resource:notes.pdf|The notes))",
            '<iframe class="wiki-resource-object"'));
        unit_test_results.push(drawnAs(
            "a named file inside a table cell is drawn there",
            "{|\n|-\n! Head !! Head\n|-\n" +
            "| ((resource-thumb:a.pdf|Some words)) || Example\n|}",
            '<td><a class="image-list"'));
        /* A preview shows what a reader would see, so a fresh form
           comes up empty rather than showing the marks the site fills
           in when it draws the page. */
        unit_test_results.push(drawnAs(
            "a form field comes up empty for a reader",
            "{{r-textfield|First Name:|first_name}}",
            "value=''"));
        unit_test_results.push({name:
            "no standing-in marks are left in a drawn form",
            pass: parseWikiContent("{{r-textfield|A|a}}\n{{checkbox|B|b}}",
                7, 42, "group", "YIOOP_TOKEN", "abc", true,
                "/").indexOf("[{") < 0});
        /* The date, the reader's name and the moment ride along with a
           form without being shown, as the site sends them. */
        var carried = parseWikiContent("{{date}}", 7, 42, "group",
            "YIOOP_TOKEN", "abc", true, "/");
        unit_test_results.push({name:
            "the date rides along without being shown",
            pass: carried.indexOf("type='hidden' name='date'") >= 0 &&
                carried.indexOf(new Date().getFullYear()) >= 0});
        unit_test_results.push({name:
            "the reader's name rides along under its own name",
            pass: parseWikiContent("{{username|who}}", 7, 42, "group",
                "YIOOP_TOKEN", "abc", true, "/").indexOf(
                "name='who' value='[{username}]'") >= 0});
        unit_test_results.push({name:
            "the moment rides along as a count of seconds",
            pass: (/name='at' value='\d{10}'/).test(parseWikiContent(
                "{{timestamp|at}}", 7, 42, "group", "YIOOP_TOKEN", "abc",
                true, "/"))});
        /* A list takes its choices from a block of kept words named
           beside it, which the page fills in once it is drawn. */
        unit_test_results.push({name:
            "a list names the block its choices come from",
            pass: parseWikiContent("{{r-dropdown|Pick|YesOrNo}}\n" +
                "{{end-r-dropdown}}", 7, 42, "group", "YIOOP_TOKEN",
                "abc", true, "/").indexOf("data-block='YesOrNo'") >= 0});
        unit_test_results.push({name:
            "a field with words after it keeps them",
            pass: parseWikiContent("{{r-textfield|Email:|Email}}<br>", 7,
                42, "group", "YIOOP_TOKEN", "abc", true,
                "/").indexOf("<br>") >= 0});
        unit_test_results.push(drawnAs(
            "anything else is a link to it",
            "((resource-link:notes.txt|The notes))",
            "<a href="));
        </script>
        <?php
        $out_data = ob_get_contents();
        ob_end_clean();
        return $out_data;
    }
    /**
     * Renders the server parser output for each input and builds the html
     * and Javascript block that, in a browser or under node, renders each
     * input again with the Javascript parser and reports whether it matches
     * the server output. Each input is a name paired with a wiki source.
     *
     * @param string $container_id id of the div the result table is added
     *      under
     * @param string $test_name name shown for this group of cases
     * @param array $inputs list of name and wiki source pairs to check
     * @return string the html and Javascript block for this group of cases
     */
    private function renderCases($container_id, $test_name, $inputs)
    {
        $parser = new WikiParser("/b/");
        $cases = [];
        foreach ($inputs as $input) {
            $cases[] = ["name" => $input[0], "source" => $input[1],
                "expected" => $parser->parse($input[1], false, false, 0)];
        }
        $wiki_cases = json_encode($cases);
        ob_start();
        ?>
        <div id="<?= $container_id ?>">
        </div>
        <script src="../scripts/basic.js" ></script>
        <script src="../scripts/wiki_parser.js" ></script>
        <script src="../scripts/javascript_unit_test.js" ></script>
        <script>
        var wiki_cases = <?= $wiki_cases ?>;
        var results = [];
        for (let index = 0; index < wiki_cases.length; index++) {
            let parser = new WikiParser("/b/", false);
            let rendered = parser.parse(wiki_cases[index].source, false);
            results.push({ name: wiki_cases[index].name,
                pass: rendered === wiki_cases[index].expected });
        }
        reportUnitTestResults("<?= $container_id ?>", "<?= $test_name ?>",
            results);
        </script>
        <?php
        $out_data = ob_get_contents();
        ob_end_clean();
        return $out_data;
    }
}
X