/ tests / MarkdownJavascriptTest.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;

/**
 * Checks that the markdown reader written for the browser makes of a
 * page what the site itself makes of it. A preview that disagreed with
 * what saving gives would be worse than none, so each case here holds a
 * piece of markdown beside the html the site produced for it.
 *
 * @author Chris Pollett
 */
class MarkdownJavascriptTest extends JavascriptUnitTest
{
    /**
     * Checks the reader against the site on a spread of markdown: words,
     * headings of both kinds, lists plain and nested, quotations, rules,
     * fenced code, links written both ways, pictures, tables, marks
     * within a line, and marks a writer has escaped.
     *
     * @return string html and Javascript that checks the reader in a
     *      browser or, from the command line, under node
     */
    public function readerAgreesWithTheSiteTestCase()
    {
        ob_start();
        ?>
        <div id="markdownReaderTest"></div>
        <script src="../scripts/basic.js" ></script>
        <script src="../scripts/markdown_parser.js" ></script>
        <script>
        var unit_test_results = [];
        var tidy = function (html) {
            return String(html).replace(/\s+/g, " ").trim();
        };
        var sameHtml = function (name, source, wanted) {
            return {name: name + " reads as the site reads it",
                pass: tidy(parseMarkdownContent(source)) === tidy(wanted)};
        };
        unit_test_results.push(sameHtml("paragraph",
            "A plain paragraph with *emphasis*, **strong**, `code` and a\ns" +
            "econd line folded into the same paragraph.",
            "<p>A plain paragraph with <em>emphasis</em>, <strong>strong</s" +
            "trong>, <code>code</code> and a\nsecond line folded into the s" +
            "ame paragraph.</p>"));
        unit_test_results.push(sameHtml("headings",
            "# One\n## Two\n###### Six\nSetext One\n==========\nSetext Two" +
            "\n----------",
            "<h1 id=\"One\">One</h1>\n<h2 id=\"Two\">Two</h2>\n<h6 id=\"Six" +
            "\">Six</h6>\n<h1 id=\"Setext One\">Setext One</h1>\n<h2 id=\"S" +
            "etext Two\">Setext Two</h2>"));
        unit_test_results.push(sameHtml("lists",
            "* first\n* second\n* third\n\n1. one\n2. two\n3. three",
            "<ul>\n<li>first</li>\n<li>second</li>\n<li>third</li>\n</ul>\n" +
            "<ol>\n<li>one</li>\n<li>two</li>\n<li>three</li>\n</ol>"));
        unit_test_results.push(sameHtml("nestedlist",
            "* outer\n    * inner one\n    * inner two\n* outer again",
            "<ul>\n<li>outer\n<ul>\n<li>inner one</li>\n<li>inner two</li>" +
            "\n</ul></li>\n<li>outer again</li>\n</ul>"));
        unit_test_results.push(sameHtml("quote",
            "> a quotation\n> over two lines\n\nafter the quotation",
            "<blockquote><p>a quotation\nover two lines</p></blockquote>\n<" +
            "p>after the quotation</p>"));
        unit_test_results.push(sameHtml("rule",
            "before\n\n---\n\nafter",
            "<p>before</p>\n<hr>\n<p>after</p>"));
        unit_test_results.push(sameHtml("fence",
            "```\ncode line one\ncode line two\n```",
            "<pre><code>code line one\ncode line two</code></pre>"));
        unit_test_results.push(sameHtml("links",
            "A [link](https://example.com) and an ![image](picture.png) and" +
            "\nan autolink <https://example.com/plain>.",
            "<p>A <a href=\"https://example.com\">link</a> and an <img src=" +
            "\"picture.png\" alt=\"image\"> and\nan autolink <a href=\"http" +
            "s://example.com/plain\">https://example.com/plain</a>.</p>"));
        unit_test_results.push(sameHtml("reference",
            "A [reference link][one] here.\n\n[one]: https://example.com/re" +
            "f \"Titled\"",
            "<p>A <a href=\"https://example.com/ref\" title=\"Titled\">refe" +
            "rence link</a> here.</p>"));
        unit_test_results.push(sameHtml("table",
            "| head one | head two |\n| -------- | -------- |\n| cell one |" +
            " cell two |\n| cell three | cell four |",
            "<table>\n<thead>\n<tr><th>head one</th><th>head two</th></tr>" +
            "\n</thead>\n<tbody>\n<tr><td>cell one</td><td>cell two</td></t" +
            "r>\n<tr><td>cell three</td><td>cell four</td></tr>\n</tbody>\n" +
            "</table>"));
        unit_test_results.push(sameHtml("inlinemix",
            "Text with **bold *and italic* inside** and `a code span with *" +
            "stars*`.",
            "<p>Text with <strong>bold <em>and italic</em> inside</strong> " +
            "and <code>a code span with *stars*</code>.</p>"));
        unit_test_results.push(sameHtml("escapes",
            "Not \\*emphasis\\* and not \\# a heading.",
            "<p>Not *emphasis* and not # a heading.</p>"));
        unit_test_results.push(sameHtml("footnote",
            "Words with a note[^one] in them.\n\n[^one]: The note itsel" +
            "f.",
            "<p>Words with a note<sup class=\"footnote-ref\"><a href=\"" +
            "#fn-one\" id=\"fnref-one\">1</a></sup> in them.</p>\n<sect" +
            "ion class=\"footnotes\"><ol>\n<li id=\"fn-one\">The note i" +
            "tself. <a href=\"#fnref-one\">&#8617;</a></li>\n</ol></sec" +
            "tion>"));
        unit_test_results.push(sameHtml("twoFootnotes",
            "First[^a] and second[^b].\n\n[^a]: Note A.\n[^b]: Note B.",
            "<p>First<sup class=\"footnote-ref\"><a href=\"#fn-a\" id=" +
            "\"fnref-a\">1</a></sup> and second<sup class=\"footnote-re" +
            "f\"><a href=\"#fn-b\" id=\"fnref-b\">2</a></sup>.</p>\n<se" +
            "ction class=\"footnotes\"><ol>\n<li id=\"fn-a\">Note A. <a" +
            " href=\"#fnref-a\">&#8617;</a></li>\n<li id=\"fn-b\">Note " +
            "B. <a href=\"#fnref-b\">&#8617;</a></li>\n</ol></section>"));
        unit_test_results.push(sameHtml("blockTemplateClass",
            "{{class=\"nav\"\n* one\n* two\n}}",
            "<div class=\"nav\">\n<ul>\n<li>one</li>\n<li>two</li>\n</u" +
            "l></div>"));
        unit_test_results.push(sameHtml("blockTemplateBlockClass",
            "{{block-class=\"nav\"\n* one\n* two\n}}",
            "<div class=\"nav\">\n<ul>\n<li>one</li>\n<li>two</li>\n</u" +
            "l></div>"));
        unit_test_results.push(sameHtml("blockTemplateCenter",
            "{{center|middle words}}",
            "<div class=\"center\">\n<p>middle words</p></div>"));
        unit_test_results.push(sameHtml("wrappedBlock",
            "{{block|top|color:red}}\nsome words\n{{end-block}}",
            "<div id=\"top\" style=\"color:red\">\n<p>some words</p></d" +
            "iv>"));
        unit_test_results.push(sameHtml("wrappedInblock",
            "{{inblock|mid|font-weight:bold}}\nwords\n{{end-inblock}}",
            "<span id=\"mid\" style=\"font-weight:bold\">\n<p>words</p>" +
            "</span>"));
        unit_test_results.push(sameHtml("spreadBlockClass",
            "{{block-class=\"red\"\nyo\n}}",
            "<div class=\"red\">\n<p>yo</p></div>"));
        unit_test_results.push(sameHtml("spreadClassList",
            "{{class=\"nav\"\n* one\n* two\n}}",
            "<div class=\"nav\">\n<ul>\n<li>one</li>\n<li>two</li>\n</u" +
            "l></div>"));
        unit_test_results.push(sameHtml("spreadBlockStyle",
            "{{block-style=\"color:red\"\nsome words\n}}",
            "<div style=\"color:red\">\n<p>some words</p></div>"));
        unit_test_results.push(sameHtml("spreadBlockId",
            "{{block-id=\"top\"\nsome words\n}}",
            "<div id=\"top\">\n<p>some words</p></div>"));
        /* What names a file kept with the page becomes the picture,
           the player or the link that stands for it, asked for by the
           group and page the preview was told. */
        var drawsFile = function (name, source, tag) {
            var made = parseMarkdownContent(source, 7, 42, "YIOOP_TOKEN",
                "abc", false);
            return {name: name + " is drawn as what it stands for",
                pass: made.indexOf("<" + tag) >= 0 &&
                    made.indexOf("g=7&p=42") >= 0 &&
                    made.indexOf("((resource") < 0};
        };
        /* A picture asked for without a link is drawn on its own, as
           the site draws it, where the plain form is wrapped in a link
           to itself. */
        unit_test_results.push({name:
            "a picture asked for without a link is drawn on its own",
            pass: parseMarkdownContent(
                "((resource-nolink:photo.webp|A photo))", 7, 42,
                "YIOOP_TOKEN", "abc", false).indexOf('class="photo"') >= 0});
        unit_test_results.push({name:
            "the plain form is still drawn the way it was",
            pass: parseMarkdownContent("((resource:photo.webp|A photo))",
                7, 42, "YIOOP_TOKEN", "abc", false).indexOf(
                'class="wiki-resource-image"') >= 0});
        unit_test_results.push(sameHtml("form textfield",
            "{{textfield|Your Name|name}}",
            "<div class='csv-form-field'><label for='name-id'>Your Name" +
            "</label><input id='name-id' type='text' name='name' maxlen" +
            "gth='64' value=''><input type='hidden' name='CSVFORM[name]" +
            "' value='textfield' ><span class='gray'  id='name-ctr' dat" +
            "a-ctr='name-id'></span></div>"));
        unit_test_results.push(sameHtml("form textarea",
            "{{textarea|Your Story|story}}",
            "<div class='csv-form-field'><label for='story-id'>Your Sto" +
            "ry</label><br><textarea id='story-id' name='story' data-co" +
            "unt-field='true' class='short-text-area' maxlength='4096'>" +
            "</textarea><div class='gray align-opposite'  id='story-ctr" +
            "' data-ctr='story-id'></div><input type='hidden' name='CSV" +
            "FORM[story]' value='textarea' ></div>"));
        unit_test_results.push(sameHtml("form checkbox",
            "{{checkbox|Agree|agree}}",
            "<div class='csv-form-field'><label for='agree-id'>Agree</l" +
            "abel><input id='agree-id' type='checkbox' name='agree'  ><" +
            "input type='hidden' name='CSVFORM[agree]' value='checkbox'" +
            " ></div>"));
        unit_test_results.push(sameHtml("form radio",
            "{{radio|Yes|choice|yes}}",
            "<div class='csv-form-field'><label for='choice-yes-id'>Yes" +
            "</label><input id='choice-yes-id' type='radio' name='choic" +
            "e' value='yes'  ><input type='hidden' name='CSVFORM[choice" +
            "]' value='radio' ></div>"));
        unit_test_results.push(sameHtml("form submit",
            "{{submit|Send it|send}}",
            "<div class='csv-form-field'><input id='Send it-id' type='s" +
            "ubmit' name='send' value='Send it' ><input type='hidden' n" +
            "ame='CSVFORM[Send it]' value='submit' ></div>"));
        unit_test_results.push(sameHtml("form dropdown",
            "{{dropdown|Pick one|choice}}\n{{option|First|1}}\n{{option" +
            "|Second|2}}\n{{end-dropdown}}",
            "<div class='csv-form-field'><input type='hidden' name='CSV" +
            "FORM[Pick one]' value='textfield' ><select name='Pick one'" +
            " data-value='' data-block='choice'>\n<option value='1'>Fir" +
            "st</option>\n<option value='2'>Second</option>\n</select><" +
            "/div>"));
        unit_test_results.push(sameHtml("form sorter",
            "{{sorter|Order these|order}}\n{{option|First|1}}\n{{option" +
            "|Second|2}}\n{{end-sorter}}",
            "<div class='csv-form-field'><input type='hidden' name='CSV" +
            "FORM[Order these]' value='sorter' ><select name='Order the" +
            "se' data-sortable='true' data-value='' data-block='order'>" +
            "\n<option value='1'>First</option>\n<option value='2'>Seco" +
            "nd</option>\n</select></div>"));
        unit_test_results.push(sameHtml("form choosek",
            "{{choosek|Pick two|picks|2}}\n{{option|First|1}}\n{{option" +
            "|Second|2}}\n{{end-choosek}}",
            "<div class='csv-form-field'><input type='hidden' name='CSV" +
            "FORM[Pick two]' value='choosek' ><select name='Pick two' d" +
            "ata-sortable='true' data-value='' data-cutoff='picks' data" +
            "-block='2'>\n<option value='1'>First</option>\n<option val" +
            "ue='2'>Second</option>\n</select></div>"));
        unit_test_results.push(sameHtml("form dataBlock",
            "{{data-block|notes}}\nsome words\n{{end-data-block}}",
            "<x-data-block style='display:none' id='notes' >\n<p>some w" +
            "ords</p>\n</x-data-block>"));
        unit_test_results.push(sameHtml("form lcheckbox",
            "{{lcheckbox|Agree|agree}}",
            "<div class='csv-form-field'><input id='agree-id' type='che" +
            "ckbox' name='agree'  ><label for='agree-id'>Agree</label><" +
            "input type='hidden' name='CSVFORM[agree]' value='checkbox'" +
            " ></div>"));
        unit_test_results.push(sameHtml("form lradio",
            "{{lradio|Yes|choice|yes}}",
            "<div class='csv-form-field'><input id='choice-yes-id' type" +
            "='radio' name='choice' value='yes'  ><label for='choice-ye" +
            "s-id'>Yes</label><input type='hidden' name='CSVFORM[choice" +
            "]' value='radio' ></div>"));
        unit_test_results.push(sameHtml("form requireSignin",
            "{{require-signin}}",
            ""));
        unit_test_results.push(sameHtml("form shareEditedForm",
            "{{share-edited-form}}",
            ""));
        /* A file asked for as a thumb stands as the small picture kept
           beside it, so a document shows its front page. */
        unit_test_results.push({name:
            "a thumb draws the picture kept beside the file",
            pass: parseMarkdownContent(
                "((resource-thumb:notes.pdf|The notes))", 7, 42,
                "YIOOP_TOKEN", "abc", true, "/").indexOf(
                '<img src="/wd/thumbs/YIOOP_TOKEN=abc/7/42/notes.pdf"')
                >= 0});
        unit_test_results.push({name:
            "a file asked for as a link is a link to it",
            pass: parseMarkdownContent(
                "((resource-link:notes.txt|The notes))", 7, 42,
                "YIOOP_TOKEN", "abc", true, "/").indexOf("<a href=") >= 0});
        /* A document is shown in a frame of its own rather than as a
           link, as the site shows one. */
        unit_test_results.push({name:
            "a document is shown in a frame of its own",
            pass: parseMarkdownContent("((resource:notes.pdf|The notes))",
                7, 42, "YIOOP_TOKEN", "abc", false).indexOf(
                '<iframe class="wiki-resource-object"') >= 0});
        /* The site stores a quote as the name that stands for it, so a
           block written with quotes must read the same after a save as
           it did before one. */
        unit_test_results.push({name:
            "a block reads the same with a stored quote",
            pass: parseMarkdownContent(
                "{{block-class=&quot;red&quot;\nyo\n}}") ===
                parseMarkdownContent("{{block-class=\"red\"\nyo\n}}")});
        /* Where the site serves pretty addresses the same file is asked
           for as a path, with the mark guarding it in the path or a dash
           where none is needed. */
        var drawsPath = function (name, source, wanted) {
            var made = parseMarkdownContent(source, 7, 42, "YIOOP_TOKEN",
                "abc", true, "/");
            return {name: name, pass: made.indexOf(wanted) >= 0};
        };
        unit_test_results.push(drawsPath(
            "a file is asked for as a path where addresses are pretty",
            "((resource:photo.webp|A photo))",
            "/wd/resources/YIOOP_TOKEN=abc/7/42/photo.webp"));
        unit_test_results.push({name:
            "a dash stands where no mark is needed",
            pass: parseMarkdownContent("((resource:photo.webp|A photo))",
                7, 42, "YIOOP_TOKEN", "", true, "/").indexOf(
                "/wd/resources/-/7/42/photo.webp") >= 0});
        /* A site living under a folder of its own keeps that folder in
           front of the file's address. */
        unit_test_results.push({name:
            "a site under a folder keeps that folder in the address",
            pass: parseMarkdownContent("((resource:photo.webp|A photo))",
                7, 42, "YIOOP_TOKEN", "abc", true, "/yioop/").indexOf(
                "/yioop/wd/resources/YIOOP_TOKEN=abc/7/42/photo.webp")
                >= 0});
        unit_test_results.push(drawsFile("a picture",
            "((resource:photo.webp|A photo))", "img"));
        unit_test_results.push(drawsFile("a picture with no words",
            "((resource:photo.jpg|))", "img"));
        unit_test_results.push(drawsFile("a moving picture",
            "((resource:talk.mp4|What was said))", "video"));
        unit_test_results.push(drawsFile("any other file",
            "((resource:notes.txt|Some notes))", "a"));
        unit_test_results.push(drawsFile("one inside a paragraph",
            "before ((resource:photo.png|A photo)) after", "img"));

        </script>
        <?php
        $out_data = ob_get_contents();
        ob_end_clean();
        return $out_data;
    }
}
X