<?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 < 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(""),
"<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(""),
"<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><script>x</script></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 < 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\">↩</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");
}
}