<?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  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\">↩</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\">↩</a></li>\n<li id=\"fn-b\">Note " +
"B. <a href=\"#fnref-b\">↩</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="red"\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;
}
}