/ tests / WikiMarkdownTest.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 as L;
use seekquarry\yioop\library\UnitTest;

/**
 * Tests the GitHub-flavored markdown reader of WikiParser, the path used to
 * show a code repository's readme. Each case feeds markdown to parseMarkdown
 * and checks the html a reader would see, so the scanner-and-parser reader
 * can be held to the flavor it aims at.
 *
 * @author Chris Pollett
 */
class WikiMarkdownTest 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()
    {
    }
    /**
     * 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 L\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 L\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 L\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 L\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 L\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 L\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 L\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 L\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 L\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 L\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 L\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 L\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 L\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 L\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 L\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 L\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");
    }
}
X