<?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\controllers\SearchController;
use seekquarry\yioop\library as L;
use seekquarry\yioop\library\UnitTest;
/**
* Locks in what the wiki parser produces today for a broad set of wiki
* source, so the coming tokenizer-and-parser rewrite can be checked for
* parity page by page before it ships. Each corpus case has a name, an
* engine (0 for the mediawiki-style syntax, 1 for the markdown syntax),
* and its source. Running the parser on the source is compared against a
* saved reference file under tests/test_files/wiki_corpus.
*
* A few cases are marked known-bad in their comment: the parser gets them
* wrong today (a link inside a heading, a fourth list level, a table
* inside a list item), and the reference simply records that wrong output
* so the rewrite's improvement shows up as a reviewed change rather than a
* silent one.
*
* To regenerate the reference files after an intended parser change, run
* this file's writeReferences method once and review the git diff.
*
* @author Chris Pollett
*/
class WikiParserCorpusTest extends UnitTest
{
/**
* Folder holding the saved reference output, one file per case.
*/
const CORPUS_DIR = __DIR__ . "/test_files/wiki_corpus";
/**
* No set up being done for the time being
*/
public function setUp()
{
}
/**
* No tear down being done for the time being
*/
public function tearDown()
{
}
/**
* The corpus: a name-keyed list of cases, each giving the render
* engine to use and the wiki source to feed the parser. Kept as a
* static method so the reference-writing tool can read the same list
* the test reads.
*
* @return array the corpus cases keyed by name
*/
public static function corpus()
{
return [
"mw_headings" => ["engine" => 0, "source" =>
"= One =\n== Two ==\n=== Three ===\n==== Four ====\n" .
"===== Five =====\n====== Six ======\n"],
"mw_format" => ["engine" => 0, "source" =>
"''italic'' and '''bold''' and '''''both'''''\n"],
"mw_htmltags" => ["engine" => 0, "source" =>
"<u>u</u> <sub>sub</sub> <sup>sup</sup> <del>del</del> " .
"<ins>ins</ins> <s>s</s> <tt>tt</tt>\n"],
"mw_ulist" => ["engine" => 0, "source" =>
"* a\n** b\n** c\n*** d\n* e\n"],
"mw_olist" => ["engine" => 0, "source" =>
"# a\n## b\n## c\n# d\n"],
"mw_dlist" => ["engine" => 0, "source" =>
";Term 1: Definition 1\n;Term 2: Definition 2\n"],
"mw_table" => ["engine" => 0, "source" =>
"{| class=\"wikitable\"\n|+ Caption\n!a!!b\n|-\n" .
"|c||d\n|}\n"],
"mw_links" => ["engine" => 0, "source" =>
"[[Page|text]] and [[http://example.com/|ext]] and " .
"[[group@Page|inter]] and [[#anchor|jump]]\n"],
"mw_style" => ["engine" => 0, "source" =>
"{{center|middle}}\n\n{{style=\"color:red\" red text}}\n"],
"mw_pre_nowiki" => ["engine" => 0, "source" =>
"<pre>preformatted '''not bold'''</pre>\n"],
/* known-bad: the fourth list level renders as a literal star */
"mw_nest_list4" => ["engine" => 0, "source" =>
"* a\n** b\n*** c\n**** d\n"],
/* known-bad: a table inside a list item stays literal text */
"mw_nest_table_in_list" => ["engine" => 0, "source" =>
"* before\n* {|\n|a||b\n|}\n* after\n"],
/* known-bad: a link inside a heading is not stripped for the
table of contents and the heading fails to render */
"mw_toc_link" => ["engine" => 0, "source" =>
"== [[Page|Linked Heading]] ==\nb1\n\n== Second ==\nb2\n" .
"\n== Third ==\nb3\n\n== Fourth ==\nb4\n"],
"md_basic" => ["engine" => 1, "source" =>
"# One\n## Two\n\n**bold** and *italic*\n\n- a\n- b\n"],
"md_fence" => ["engine" => 1, "source" =>
"text\n\n```php\n\$x = 1;\n```\n\nmore `inline` code\n"],
"md_blockquote" => ["engine" => 1, "source" =>
"> quoted line one\n> quoted line two\n"],
];
}
/**
* Parses one corpus case the way the wiki controller would show it. A
* wiki case is cleaned and then parsed, the way stored wiki text is; a
* markdown case is parsed straight from its source, the way a git readme
* blob is, since that path does no cleaning of the text beforehand.
*
* @param array $case one corpus entry with engine and source keys
* @return string the parser's html output for that case
*/
public static function renderCase($case)
{
$parser = new L\WikiParser();
if ($case["engine"] == 1) {
return $parser->parse($case["source"], false, false, 1);
}
$controller = new SearchController();
$cleaned = $controller->clean($case["source"], "string");
return $parser->parse($cleaned, false, false, $case["engine"]);
}
/**
* Writes the reference file for every corpus case. This is a tool for
* regenerating the saved output after an intended parser change, not a
* test; the test reads what this writes. Review the git diff of the
* reference folder afterward.
*
* @return void the reference files are written to CORPUS_DIR
*/
public static function writeReferences()
{
if (!is_dir(self::CORPUS_DIR)) {
mkdir(self::CORPUS_DIR, 0777, true);
}
foreach (self::corpus() as $name => $case) {
file_put_contents(self::CORPUS_DIR . "/" . $name . ".html",
self::renderCase($case));
}
}
/**
* Checks that every corpus case still parses to its saved reference
* output. A difference means the parser's output changed for that
* case, which is exactly what the coming rewrite must account for.
*/
public function corpusParityTestCase()
{
foreach (self::corpus() as $name => $case) {
$reference_file = self::CORPUS_DIR . "/" . $name . ".html";
$expected = is_file($reference_file) ?
file_get_contents($reference_file) : "MISSING REFERENCE";
$actual = self::renderCase($case);
$this->assertEqual($actual, $expected,
"corpus case '$name' matches its saved reference");
}
}
}