mirror of
https://github.com/sbrl/Pepperminty-Wiki.git
synced 2024-11-10 12:33:00 +00:00
Starbeamrainbowlabs
26f5838ce0
This is transparently handled by a wrapper around inlineLink, which conditionally bails by returning the parent if parsing fails. It then ~~ab~~uses inlineInternalLink to provide proper internal link support. Fixes #190.
1614 lines
63 KiB
PHP
1614 lines
63 KiB
PHP
<?php
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
register_module([
|
|
"name" => "Parsedown",
|
|
"version" => "0.11.3",
|
|
"author" => "Emanuil Rusev & Starbeamrainbowlabs",
|
|
"description" => "An upgraded (now default!) parser based on Emanuil Rusev's Parsedown Extra PHP library (https://github.com/erusev/parsedown-extra), which is licensed MIT. Please be careful, as this module adds some weight to your installation.",
|
|
"extra_data" => [
|
|
/********** Parsedown versions **********
|
|
* Parsedown Core: 1.8.0-beta-7 *
|
|
* Parsedown Extra: 0.8.0-beta-1 *
|
|
* Parsedown Extreme: 0.1.6 *removed* *
|
|
****************************************/
|
|
"Parsedown.php" => "https://raw.githubusercontent.com/erusev/parsedown/1610e4747c88a53676f94f752b447f4eff03c28d/Parsedown.php",
|
|
// "ParsedownExtra.php" => "https://raw.githubusercontent.com/erusev/parsedown-extra/91ac3ff98f0cea243bdccc688df43810f044dcef/ParsedownExtra.php",
|
|
// "Parsedown.php" => "https://raw.githubusercontent.com/erusev/parsedown/3825db53a2be5d9ce54436a9cc557c6bdce1808a/Parsedown.php",
|
|
"ParsedownExtra.php" => "https://raw.githubusercontent.com/erusev/parsedown-extra/352d03d941fc801724e82e49424ff409175261fd/ParsedownExtra.php"
|
|
// Parsedown Extreme is causing PHP 7.4+ errors, and isn't rendering correctly with the security features we have turned on.
|
|
// "ParsedownExtended.php" => "https://raw.githubusercontent.com/BenjaminHoegh/ParsedownExtended/8e1224e61a199cb513c47398353a27f6ba822da6/ParsedownExtended.php"
|
|
// "ParsedownExtreme.php" => "https://raw.githubusercontent.com/BenjaminHoegh/parsedown-extreme/adae4136534ad1e4159fe04c74c4683681855b84/ParsedownExtreme.php"
|
|
],
|
|
"id" => "parser-parsedown",
|
|
"code" => function() {
|
|
global $settings;
|
|
|
|
$parser = new PeppermintParsedown();
|
|
$parser->setInternalLinkBase("?page=%s");
|
|
add_parser("parsedown", function($source, $untrusted) use ($parser) {
|
|
global $settings;
|
|
$parser->setsafeMode($untrusted || $settings->all_untrusted);
|
|
$parser->setMarkupEscaped($settings->clean_raw_html);
|
|
$result = $parser->text($source);
|
|
|
|
return $result;
|
|
}, function($source) {
|
|
global $version, $settings, $pageindex;
|
|
$id_text = "$version|$settings->parser|$source";
|
|
|
|
// Find template includes
|
|
preg_match_all(
|
|
'/\{\{\s*([^|]+)\s*(?:\|[^}]*)?\}\}/',
|
|
$source, $includes
|
|
);
|
|
foreach($includes[1] as $include_pagename) {
|
|
if(empty($pageindex->$include_pagename))
|
|
continue;
|
|
$id_text .= "|$include_pagename:" . parsedown_pagename_resolve(
|
|
$pageindex->$include_pagename->lastmodified
|
|
);
|
|
}
|
|
|
|
return str_replace(["+","/"], ["-","_"], base64_encode(hash(
|
|
"sha256",
|
|
$id_text,
|
|
true
|
|
)));
|
|
});
|
|
|
|
add_action("parsedown-render-ext", function() {
|
|
global $settings, $env, $paths;
|
|
|
|
if(!$settings->parser_ext_renderers_enabled) {
|
|
http_response_code(403);
|
|
header("content-type: image/png");
|
|
imagepng(errorimage("Error: External diagram renderer support\nhas been disabled on $settings->sitename.\nTry contacting {$settings->admindetails_name}, $settings->sitename's administrator."));
|
|
exit();
|
|
}
|
|
|
|
if(!isset($_GET["source"])) {
|
|
http_response_code(400);
|
|
header("content-type: image/png");
|
|
imagepng(errorimage("Error: No source text \nspecified."));
|
|
exit();
|
|
}
|
|
|
|
if(!isset($_GET["language"])) {
|
|
http_response_code(400);
|
|
header("content-type: image/png");
|
|
imagepng(errorimage("Error: No external renderer \nlanguage specified."));
|
|
exit();
|
|
}
|
|
|
|
$source = $_GET["source"];
|
|
$language = $_GET["language"];
|
|
|
|
if(!isset($settings->parser_ext_renderers->$language)) {
|
|
$message = "Error: Unknown language {$_GET["language"]}.\nSupported languages:\n";
|
|
foreach($settings->parser_ext_renderers as $language => $spec)
|
|
$message .= "$spec->name ($language)\n";
|
|
|
|
http_response_code(400);
|
|
header("content-type: image/png");
|
|
imagepng(errorimage(trim($message)));
|
|
exit();
|
|
}
|
|
|
|
$renderer = $settings->parser_ext_renderers->$language;
|
|
|
|
$cache_id = hash("sha256",
|
|
hash("sha256", $language) .
|
|
hash("sha256", $source) .
|
|
($_GET["immutable_key"] ?? "")
|
|
);
|
|
|
|
$cache_file_location = "{$paths->cache_directory}/render_ext/$cache_id." . system_mime_type_extension($renderer->output_format);
|
|
|
|
// If it exists on disk already, then serve that instead
|
|
if(file_exists($cache_file_location)) {
|
|
header("cache-control: public, max-age=31536000, immutable");
|
|
header("content-type: $renderer->output_format");
|
|
header("content-length: " . filesize($cache_file_location));
|
|
header("x-cache: render_ext/hit");
|
|
readfile($cache_file_location);
|
|
exit();
|
|
}
|
|
|
|
if(!$settings->parser_ext_allow_anon && !$env->is_logged_in) {
|
|
http_response_code(401);
|
|
header("content-type: image/png");
|
|
imagepng(errorimage(wordwrap("Error: You aren't logged in, that image hasn't yet been cached, and $settings->sitename does not allow anonymous users to invoke external renderers, so that image can't be generated right now. Try contacting $settings->admindetails_name, $settings->sitename's administrator (their details can be found at the bottom of every page).")));
|
|
exit();
|
|
}
|
|
|
|
// Create the cache directory if doesn't exist already
|
|
if(!file_exists(dirname($cache_file_location)))
|
|
mkdir(dirname($cache_file_location), 0750, true);
|
|
|
|
$cli_to_execute = $renderer->cli;
|
|
|
|
$descriptors = [
|
|
0 => null, // stdin
|
|
1 => null, // stdout
|
|
2 => tmpfile() // stderr
|
|
];
|
|
|
|
switch ($renderer->cli_mode) {
|
|
case "pipe":
|
|
// Fill stdin with the input text
|
|
$descriptors[0] = tmpfile();
|
|
fwrite($descriptors[0], $source);
|
|
fseek($descriptors[0], 0);
|
|
|
|
// Pipe the output to be the cache file
|
|
$descriptors[1] = fopen($cache_file_location, "wb+");
|
|
break;
|
|
|
|
case "substitution_pipe":
|
|
// Update the command that we're going to execute
|
|
$cli_to_execute = str_replace(
|
|
"{input_text}",
|
|
escapeshellarg($source),
|
|
$cli_to_execute
|
|
);
|
|
|
|
// Set the descriptors
|
|
$descriptors[0] = tmpfile();
|
|
$descriptors[1] = fopen($cache_file_location, "wb+");
|
|
break;
|
|
|
|
case "file":
|
|
$descriptors[0] = tmpfile();
|
|
fwrite($descriptors[0], $source);
|
|
$descriptors[1] = tmpfile();
|
|
|
|
$cli_to_execute = str_replace(
|
|
[ "{input_file}", "{output_file}" ],
|
|
[
|
|
escapeshellarg(stream_get_meta_data($descriptors[0])["uri"]),
|
|
escapeshellarg($cache_file_location)
|
|
],
|
|
$cli_to_execute
|
|
);
|
|
break;
|
|
|
|
default:
|
|
http_response_code(503);
|
|
header("cache-control: no-cache, no-store, must-revalidate");
|
|
header("content-type: image/png");
|
|
imagepng(errorimage("Error: Unknown external renderer mode '$renderer->cli_mode'.\nPlease contact $settings->admindetails_name, $settings->sitename's administrator."));
|
|
exit();
|
|
break;
|
|
}
|
|
|
|
if('\\' !== DIRECTORY_SEPARATOR) {
|
|
// We're not on Windows, so we can use timeout to force-kill if it takes too long
|
|
$cli_to_execute = "timeout {$settings->parser_ext_time_limit} $cli_to_execute";
|
|
}
|
|
|
|
$start_time = microtime(true);
|
|
$process_handle = proc_open(
|
|
$cli_to_execute,
|
|
$descriptors,
|
|
$pipes,
|
|
null, // working directory
|
|
null // environment variables
|
|
);
|
|
if(!is_resource($process_handle)) {
|
|
fclose($descriptors[0]);
|
|
fclose($descriptors[1]);
|
|
fclose($descriptors[2]);
|
|
|
|
if(file_exists($cache_file_location)) unlink($cache_file_location);
|
|
|
|
http_response_code(503);
|
|
header("cache-control: no-cache, no-store, must-revalidate");
|
|
header("content-type: image/png");
|
|
imagepng(errorimage("Error: Failed to start external renderer.\nIs $renderer->name installed?"));
|
|
exit();
|
|
}
|
|
// Wait for it to exit
|
|
$exit_code = proc_close($process_handle);
|
|
|
|
fclose($descriptors[0]);
|
|
fclose($descriptors[1]);
|
|
|
|
$time_taken = round((microtime(true) - $start_time) * 1000, 2);
|
|
|
|
if($exit_code !== 0 || !file_exists($cache_file_location)) {
|
|
fseek($descriptors[2], 0);
|
|
$error_details = stream_get_contents($descriptors[2]);
|
|
// Delete the cache file, which is guaranteed to exist because
|
|
// we pre-emptively create it above
|
|
if(file_exists($cache_file_location)) unlink($cache_file_location);
|
|
|
|
http_response_code(503);
|
|
header("content-type: image/png");
|
|
imagepng(errorimage(
|
|
"Error: The external renderer ($renderer->name)\nexited with code $exit_code,\nor potentially did not create the output file.\nDetails:\n" . wordwrap($error_details)
|
|
));
|
|
exit();
|
|
}
|
|
|
|
header("cache-control: public, max-age=31536000, immutable");
|
|
header("content-type: $renderer->output_format");
|
|
header("content-length: " . filesize($cache_file_location));
|
|
header("x-cache: render_ext/miss, renderer took {$time_taken}ms");
|
|
readfile($cache_file_location);
|
|
});
|
|
|
|
/*
|
|
* ███████ ████████ █████ ████████ ██ ███████ ████████ ██ ██████ ███████
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ███████ ██ ███████ ██ ██ ███████ ██ ██ ██ ███████
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ███████ ██ ██ ██ ██ ██ ███████ ██ ██ ██████ ███████
|
|
*/
|
|
statistic_add([
|
|
"id" => "wanted-pages",
|
|
"name" => "Wanted Pages",
|
|
"type" => "page",
|
|
"update" => function($old_stats) {
|
|
global $pageindex, $env;
|
|
|
|
$result = new stdClass(); // completed, value, state
|
|
$pages = [];
|
|
foreach($pageindex as $pagename => $pagedata) {
|
|
if(!file_exists($env->storage_prefix . $pagedata->filename))
|
|
continue;
|
|
$page_content = file_get_contents($env->storage_prefix . $pagedata->filename);
|
|
|
|
$page_links = PeppermintParsedown::extract_page_names($page_content);
|
|
|
|
foreach($page_links as $linked_page) {
|
|
// We're only interested in pages that don't exist
|
|
if(!empty($pageindex->$linked_page)) continue;
|
|
|
|
if(empty($pages[$linked_page]))
|
|
$pages[$linked_page] = 0;
|
|
$pages[$linked_page]++;
|
|
}
|
|
}
|
|
|
|
arsort($pages);
|
|
|
|
$result->value = $pages;
|
|
$result->completed = true;
|
|
return $result;
|
|
},
|
|
"render" => function($stats_data) {
|
|
$result = "<h2>$stats_data->name</h2>\n";
|
|
$result .= "<table class='wanted-pages'>\n";
|
|
$result .= "\t<tr><th>Page Name</th><th>Linking Pages</th></tr>\n";
|
|
foreach($stats_data->value as $pagename => $linking_pages) {
|
|
$result .= "\t<tr><td>$pagename</td><td>$linking_pages</td></tr>\n";
|
|
}
|
|
$result .= "</table>\n";
|
|
return $result;
|
|
}
|
|
]);
|
|
statistic_add([
|
|
"id" => "orphan-pages",
|
|
"name" => "Orphan Pages",
|
|
"type" => "page-list",
|
|
"update" => function($old_stats) {
|
|
global $pageindex, $env;
|
|
|
|
$result = new stdClass(); // completed, value, state
|
|
$pages = [];
|
|
foreach($pageindex as $pagename => $pagedata) {
|
|
if(!file_exists($env->storage_prefix . $pagedata->filename))
|
|
continue;
|
|
$page_content = file_get_contents($env->storage_prefix . $pagedata->filename);
|
|
|
|
$page_links = PeppermintParsedown::extract_page_names($page_content);
|
|
|
|
foreach($page_links as $linked_page) {
|
|
// We're only interested in pages that exist
|
|
if(empty($pageindex->$linked_page)) continue;
|
|
|
|
$pages[$linked_page] = true;
|
|
}
|
|
}
|
|
|
|
$orphaned_pages = [];
|
|
foreach($pageindex as $pagename => $page_data) {
|
|
if(empty($pages[$pagename]))
|
|
$orphaned_pages[] = $pagename;
|
|
}
|
|
|
|
$sorter = new Collator("");
|
|
$sorter->sort($orphaned_pages);
|
|
|
|
$result->value = $orphaned_pages;
|
|
$result->completed = true;
|
|
return $result;
|
|
}
|
|
]);
|
|
statistic_add([
|
|
"id" => "most-linked-to-pages",
|
|
"name" => "Most Linked-To Pages",
|
|
"type" => "page",
|
|
"update" => function($old_stats) {
|
|
global $pageindex, $env;
|
|
|
|
$result = new stdClass(); // completed, value, state
|
|
$pages = [];
|
|
foreach($pageindex as $pagename => $pagedata) {
|
|
if(!file_exists($env->storage_prefix . $pagedata->filename))
|
|
continue;
|
|
$page_content = file_get_contents($env->storage_prefix . $pagedata->filename);
|
|
|
|
$page_links = PeppermintParsedown::extract_page_names($page_content);
|
|
|
|
foreach($page_links as $linked_page) {
|
|
// We're only interested in pages that exist
|
|
if(empty($pageindex->$linked_page)) continue;
|
|
|
|
if(empty($pages[$linked_page]))
|
|
$pages[$linked_page] = 0;
|
|
$pages[$linked_page]++;
|
|
}
|
|
}
|
|
|
|
arsort($pages);
|
|
|
|
$result->value = $pages;
|
|
$result->completed = true;
|
|
return $result;
|
|
},
|
|
"render" => function($stats_data) {
|
|
global $pageindex;
|
|
$result = "<h2>$stats_data->name</h2>\n";
|
|
$result .= "<table class='most-linked-to-pages'>\n";
|
|
$result .= "\t<tr><th>Page Name</th><th>Linking Pages</th></tr>\n";
|
|
foreach($stats_data->value as $pagename => $link_count) {
|
|
$pagename_display = !empty($pageindex->$pagename->redirect) && $pageindex->$pagename->redirect ? "<em>$pagename</em>" : $pagename;
|
|
$result .= "\t<tr><td><a href='?page=" . rawurlencode($pagename) . "'>$pagename_display</a></td><td>$link_count</td></tr>\n";
|
|
}
|
|
$result .= "</table>\n";
|
|
return $result;
|
|
}
|
|
]);
|
|
|
|
add_help_section("20-parser-default", "Editor Syntax",
|
|
"<p>$settings->sitename's editor uses an extended version of <a href='http://parsedown.org/'>Parsedown</a> to render pages, which is a fantastic open source Github flavoured markdown parser. You can find a quick reference guide on Github flavoured markdown <a href='https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet'>here</a> by <a href='https://github.com/adam-p/'>adam-p</a>, or if you prefer a book <a href='https://www.gitbook.com/book/roachhd/master-markdown/details'>Mastering Markdown</a> by KB is a good read, and free too!</p>
|
|
<h3>Tips</h3>
|
|
<ul>
|
|
<li>Put 2 spaces at the end of a line to add a soft line break. Leave a blank line to add a head line break (i.e. a new paragraph).</li>
|
|
<li>If you don't like the default id given to a header, you can add a custom one instead. Put it in curly braces after the heading name like this: <code># Heading Name {#HeadingId}</code>. Then you can link to like like this: <code>[[Page name#HeadingId}]]</code>. You can also link to a heading id on the current page by omitting the page name: <code>[[#HeadingId]]</code>. Finally, a heading id is automatically generated for every heading by default. Take the heading name, make it lowercase, and replace the spaces with dashes <code>.</code>, and that's the heading ID that you can link to (although sometimes some special characters are removed).</li>
|
|
</ul>
|
|
<h3>Extra Syntax</h3>
|
|
<p>$settings->sitename's editor also supports some extra custom syntax, some of which is inspired by <a href='https://mediawiki.org/'>Mediawiki</a>.
|
|
<table>
|
|
<tr><th style='width: 40%'>Type this</th><th style='width: 20%'>To get this</th><th>Comments</th></th>
|
|
<tr><td><code>[[Internal link]]</code></td><td><a href='?page=Internal%20link'>Internal Link</a></td><td>An internal link.</td></tr>
|
|
<tr><td><code>[[Internal link|Display Text]]</code></td><td><a href='?page=Internal%20link'>Display Text</a></td><td>An internal link with some display text.</td></tr>
|
|
<tr><td><code>![Alt text](http://example.com/path/to/image.png | 256x256 | right)</code></td><td><img src='http://example.com/path/to/image.png' alt='Alt text' style='float: right; max-width: 256px; max-height: 256px;' /></td><td>An image floating to the right of the page that fits inside a 256px x 256px box, preserving aspect ratio.</td></tr>
|
|
<tr><td><code>![Alt text](http://example.com/path/to/image.png | 256x256 | caption)</code></td><td><figure><img src='http://example.com/path/to/image.png' alt='Alt text' style='max-width: 256px; max-height: 256px;' /><figcaption>Alt text</figcaption></figure></td><td>An image with a caption that fits inside a 256px x 256px box, preserving aspect ratio. The presence of the word <code>caption</code> in the regular braces causes the alt text to be taken and displayed below the image itself.</td></tr>
|
|
<tr><td><code>![Alt text](Files/Cheese.png)</code></td><td><img src='index.php?action=preview&page=Files/Cheese.png' alt='Alt text' style='' /></td><td>An example of the short url syntax for images. Simply enter the page name of an image (or video / audio file), and Pepperminty Wiki will sort out the url for you.</td></tr>
|
|
<tr><td><code>Some text ==marked text== more text</code></td><td>Some text <mark>marked text</mark> more text</td><td>Marked / highlighted text</td></tr>
|
|
<tr><td><code>Some text^superscript^ more text</code></td><td>Some text<sup>superscript</sup> more text</td><td>Superscript</td></tr>
|
|
<tr><td><code>Some text~subscript~ more text</code></td><td>Some text<sub>subscript</sub> more text</td><td>Subscript (note that we use a single tilda <code>~</code> here - a double results in strikethrough text instead)</td></tr>
|
|
<tr><td><code>[ ] Unticked checkbox</code></td><td><input type='checkbox' disabled /> Unticked checkbox</td><td>An unticked checkbox. Must be at the beginning of a line or directly after a list item (e.g. <code> - </code> or <code>1. </code>).</td></tr>
|
|
<tr><td><code>[x] Ticked checkbox</code></td><td><input type='checkbox' checked='checked' disabled /> Ticked checkbox</td><td>An ticked checkbox. The same rules as unticked checkboxes apply here too.</td></tr>
|
|
<tr><td><code>some text >!spoiler text!< more text</code></td><td>some text <a class='spoiler' href='#spoiler-example' id='spoiler-example'>spoiler text</a> more text</td><td>A spoiler. Users must click it to reveal the content hidden beneath.</td></tr>
|
|
<tr><td><code>some text ||spoiler text|| more text</code></td><td>some text <a class='spoiler' href='#spoiler-example-2' id='spoiler-example-2'>spoiler text</a> more text</td><td>Alternative spoiler syntax inspired by <a href='https://support.discord.com/hc/en-us/articles/360022320632-Spoiler-Tags-'>Discord</a>.</td></tr>
|
|
<tr><td><code>[__TOC__]</code></td><td></td><td>An automatic table of contents. Note that this must be on a line by itself with no text before or after it on that line for it to work.</td></tr>
|
|
</table>
|
|
<p>Note that the all image image syntax above can be mixed and matched to your liking. The <code>caption</code> option in particular must come last or next to last.</p>
|
|
<h4>Templating</h4>
|
|
<p>$settings->sitename also supports including one page in another page as a <em>template</em>. The syntax is very similar to that of Mediawiki. For example, <code>{{Announcement banner}}</code> will include the contents of the \"Announcement banner\" page, assuming it exists.</p>
|
|
<p>You can also use variables. Again, the syntax here is very similar to that of Mediawiki - they can be referenced in the included page by surrrounding the variable name in triple curly braces (e.g. <code>{{{Announcement text}}}</code>), and set when including a page with the bar syntax (e.g. <code>{{Announcement banner | importance = high | text = Maintenance has been planned for tonight.}}</code>). Currently the only restriction in templates and variables is that you may not include a closing curly brace (<code>}</code>) in the page name, variable name, or value.</p>
|
|
<h5>Special Variables</h5>
|
|
<p>$settings->sitename also supports a number of special built-in variables. Their syntax and function are described below:</p>
|
|
<table>
|
|
<tr><th>Type this</th><th>To get this</th></tr>
|
|
<tr><td><code>{{{@}}}</code></td><td>Lists all variables and their values in a table.</td></tr>
|
|
<tr><td><code>{{{#}}}</code></td><td>Shows a 'stack trace', outlining all the parent includes of the current page being parsed.</td></tr>
|
|
<tr><td><code>{{{~}}}</code></td><td>Outputs the requested page's name.</td></tr>
|
|
<tr><td><code>{{{*}}}</code></td><td>Outputs a comma separated list of all the subpages of the current page.</td></tr>
|
|
<tr><td><code>{{{+}}}</code></td><td>Shows a gallery containing all the files that are sub pages of the current page.</td></tr>
|
|
</table>
|
|
<p>Note that a page <em>doesn't not</em> need to be included as a template to use these variables.");
|
|
if($settings->parser_ext_renderers_enabled) {
|
|
$doc_help = "<p>$settings->sitename supports external renderers. External renderers take the content of a code fence block, like this:</p>
|
|
<pre><code>```language_code
|
|
Insert text here
|
|
```</code></pre>
|
|
<p>...and render it to an image. This is based on the <code>language_code</code> specified, as is done in the above example. Precisely what the output of a external renderer is depends on the external renderers defined, but $settings->sitename currently has the following external renderers registered:</p>
|
|
<table>
|
|
<tr><th>Name</th><th>Language code</th><th>Description</th><th>Reference Link</th></tr>
|
|
";
|
|
|
|
foreach($settings->parser_ext_renderers as $code => $renderer) {
|
|
$row = array_map(function($value) { return htmlentities($value, ENT_COMPAT|ENT_HTML5); }, [
|
|
$renderer->name,
|
|
$code,
|
|
$renderer->description,
|
|
$renderer->url
|
|
]);
|
|
$row[3] = "<a href='$row[3]'>🔗</a>";
|
|
$doc_help .= "<tr><td>".implode("</td><td>", $row)."</td></tr>\n";
|
|
}
|
|
$doc_help .= "</table>
|
|
$settings->admindetails_name can register more external renderers - see the <a href='https://starbeamrainbowlabs.com/labs/peppermint/__nightdocs/06.8-External-Renderers.html'>documentation</a> for more information.</p>";
|
|
|
|
add_help_section("24-external-renderers", "External Renderers", $doc_help);
|
|
}
|
|
}
|
|
]);
|
|
|
|
require_once("$paths->extra_data_directory/parser-parsedown/Parsedown.php");
|
|
require_once("$paths->extra_data_directory/parser-parsedown/ParsedownExtra.php");
|
|
// require_once("$paths->extra_data_directory/parser-parsedown/ParsedownExtended.php");
|
|
|
|
/**
|
|
* Attempts to 'auto-correct' a page name by trying different capitalisation
|
|
* combinations.
|
|
*
|
|
* @param string $pagename The page name to auto-correct.
|
|
* @return string The auto-corrected page name.
|
|
*/
|
|
function parsedown_pagename_resolve($pagename) {
|
|
global $pageindex;
|
|
|
|
// If the page doesn't exist, check varying different
|
|
// capitalisations to see if it exists under some variant.
|
|
if(!empty($pageindex->$pagename))
|
|
return $pagename;
|
|
|
|
$pagename = ucfirst($pagename);
|
|
if(!empty($pageindex->$pagename))
|
|
return $pagename;
|
|
|
|
$pagename = ucwords($pagename);
|
|
return $pagename;
|
|
}
|
|
|
|
/*
|
|
* ██████ █████ ██████ ███████ ███████ ██████ ██████ ██ ██ ███ ██
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██
|
|
* ██████ ███████ ██████ ███████ █████ ██ ██ ██ ██ ██ █ ██ ██ ██ ██
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███ ██ ██ ██ ██
|
|
* ██ ██ ██ ██ ██ ███████ ███████ ██████ ██████ ███ ███ ██ ████
|
|
*
|
|
* ███████ ██ ██ ████████ ███████ ███ ██ ███████ ██ ██████ ███ ██ ███████
|
|
* ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ████ ██ ██
|
|
* █████ ███ ██ █████ ██ ██ ██ ███████ ██ ██ ██ ██ ██ ██ ███████
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ███████ ██ ██ ██ ███████ ██ ████ ███████ ██ ██████ ██ ████ ███████
|
|
*/
|
|
/**
|
|
* The Peppermint-flavoured Parsedown parser.
|
|
*/
|
|
class PeppermintParsedown extends ParsedownExtra
|
|
{
|
|
/**
|
|
* A long random and extremely unlikely string to identify where we want
|
|
* to put a table of contents.
|
|
* Hopefully nobody is unlucky enough to include this in a page...!
|
|
* @var string
|
|
*/
|
|
private const TOC_ID = "█yyZiy9c9oHVExhVummYZId_dO9-fvaGFvgQirEapxOtaL-s7WnK34lF9ObBoQ0EH2kvtd6VKcAL2█";
|
|
|
|
/**
|
|
* The base directory with which internal links will be resolved.
|
|
* @var string
|
|
*/
|
|
private $internalLinkBase = "./%s";
|
|
|
|
/**
|
|
* The parameter stack. Used for recursive templating.
|
|
* @var array
|
|
*/
|
|
protected $paramStack = [];
|
|
|
|
/**
|
|
* Creates a new Peppermint Parsedown instance.
|
|
*/
|
|
function __construct()
|
|
{
|
|
parent::__construct();
|
|
|
|
array_unshift($this->BlockTypes["["], "TableOfContents");
|
|
|
|
// Prioritise our internal link parsing over the regular link parsing
|
|
$this->addInlineType("[", "InternalLink", true);
|
|
// Prioritise the checkbox handling - this is fine 'cause it doesn't step on InternalLink's toes
|
|
$this->addInlineType("[", "Checkbox", true);
|
|
// Prioritise our image parser over the regular image parser
|
|
$this->addInlineType("!", "ExtendedImage", true);
|
|
|
|
$this->addInlineType("{", "Template");
|
|
$this->addInlineType("=", "Mark");
|
|
$this->addInlineType("^", "Superscript");
|
|
$this->addInlineType("~", "Subscript");
|
|
|
|
$this->addInlineType(">", "Spoiler", true);
|
|
$this->addInlineType("|", "Spoiler", true);
|
|
}
|
|
|
|
/**
|
|
* Helper method to add an inline type.
|
|
* @param string $char The char to match against.
|
|
* @param string $function_id The name bit of the function to call.
|
|
* @param boolean $before_others Whether to prioritise this function over other existing ones (default: false).
|
|
*/
|
|
protected function addInlineType(string $char, string $function_id, bool $before_others = false) {
|
|
if(mb_strlen($char) > 1)
|
|
throw new Exception("Error: '$char' is longer than a single character.");
|
|
if(!isset($this->InlineTypes[$char]) or !is_array($this->InlineTypes[$char]))
|
|
$this->InlineTypes[$char] = [];
|
|
|
|
if(strpos($this->inlineMarkerList, $char) === false)
|
|
$this->inlineMarkerList .= $char;
|
|
|
|
if(!$before_others)
|
|
$this->InlineTypes[$char][] = $function_id;
|
|
else
|
|
array_unshift($this->InlineTypes[$char], $function_id);
|
|
}
|
|
|
|
/*
|
|
* Override the text method here to insert the table of contents after
|
|
* rendering has been completed
|
|
*/
|
|
public function text($text) {
|
|
$result = parent::text($text);
|
|
$toc_html = $this->generateTableOfContents();
|
|
$result = str_replace(self::TOC_ID, $toc_html, $result);
|
|
|
|
return $result;
|
|
}
|
|
|
|
|
|
/*
|
|
* ████████ ███████ ███ ███ ██████ ██ █████ ████████ ██ ███ ██ ██████
|
|
* ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██
|
|
* ██ █████ ██ ████ ██ ██████ ██ ███████ ██ ██ ██ ██ ██ ██ ███
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ██ ███████ ██ ██ ██ ███████ ██ ██ ██ ██ ██ ████ ██████
|
|
* %templating
|
|
*/
|
|
/**
|
|
* Parses templating definitions.
|
|
* @param string $fragment The fragment to parse it out from.
|
|
*/
|
|
protected function inlineTemplate($fragment)
|
|
{
|
|
global $env, $pageindex;
|
|
|
|
// Variable parsing
|
|
if(preg_match("/\{\{\{([^}]+)\}\}\}/", $fragment["text"], $matches))
|
|
{
|
|
$params = [];
|
|
if(!empty($this->paramStack))
|
|
{
|
|
$stackEntry = array_slice($this->paramStack, -1)[0];
|
|
$params = !empty($stackEntry) ? $stackEntry["params"] : false;
|
|
}
|
|
|
|
$variableKey = trim($matches[1]);
|
|
|
|
$variableValue = false;
|
|
switch ($variableKey) {
|
|
case "@": // Lists all variables and their values
|
|
if(!empty($params)) {
|
|
$variableValue = "<table>
|
|
<tr><th>Key</th><th>Value</th></tr>\n";
|
|
foreach($params as $key => $value) {
|
|
$variableValue .= "\t<tr><td>" . $this->escapeText($key) . "</td><td>" . $this->escapeText($value) . "</td></tr>\n";
|
|
}
|
|
$variableValue .= "</table>";
|
|
}
|
|
else {
|
|
$variableValue = "<em>(no variables are currently defined)</em>";
|
|
}
|
|
break;
|
|
case "#": // Shows a stack trace
|
|
$variableValue = "<ol start=\"0\">\n";
|
|
$variableValue .= "\t<li>$env->page</li>\n";
|
|
foreach($this->paramStack as $curStackEntry) {
|
|
$variableValue .= "\t<li>" . $curStackEntry["pagename"] . "</li>\n";
|
|
}
|
|
$variableValue .= "</ol>\n";
|
|
break;
|
|
case "~": // Show requested page's name
|
|
if(!empty($this->paramStack))
|
|
$variableValue = $this->escapeText($env->page);
|
|
break;
|
|
case "*": // Lists subpages
|
|
$subpages = get_subpages($pageindex, $env->page);
|
|
$variableValue = [];
|
|
foreach($subpages as $pagename => $depth)
|
|
{
|
|
$variableValue[] = $pagename;
|
|
}
|
|
$variableValue = implode(", ", $variableValue);
|
|
if(strlen($variableValue) === 0)
|
|
$variableValue = "<em>(none yet!)</em>";
|
|
break;
|
|
case "+": // Shows a file gallery for subpages with files
|
|
// If the upload module isn't present, then there's no point
|
|
// in checking for uploaded files
|
|
if(!module_exists("feature-upload"))
|
|
break;
|
|
|
|
$variableValue = [];
|
|
$subpages = get_subpages($pageindex, $env->page);
|
|
foreach($subpages as $pagename => $depth)
|
|
{
|
|
// Make sure that this is an uploaded file
|
|
if(!$pageindex->$pagename->uploadedfile)
|
|
continue;
|
|
|
|
$mime_type = $pageindex->$pagename->uploadedfilemime;
|
|
|
|
$previewSize = 300;
|
|
$previewUrl = "?action=preview&size=$previewSize&page=" . rawurlencode($pagename);
|
|
|
|
$previewType = substr($mime_type, 0, strpos($mime_type, "/"));
|
|
if($mime_type == "application/pdf")
|
|
$previewType = "pdf";
|
|
|
|
$previewHtml = "";
|
|
switch($previewType)
|
|
{
|
|
case "video":
|
|
$previewHtml .= "<video src='$previewUrl' controls preload='metadata'>".$this->escapeText($pagename)."</video>\n";
|
|
break;
|
|
case "audio":
|
|
$previewHtml .= "<audio src='$previewUrl' controls preload='metadata'>".$this->escapeText($pagename)."</audio>\n";
|
|
break;
|
|
case "pdf":
|
|
$previewHtml .= "<object type='application/pdf' data='$previewUrl' style='width: {$previewStyle}px;'></object>";
|
|
break;
|
|
case "application":
|
|
case "image":
|
|
default:
|
|
$previewHtml .= "<img src='$previewUrl' />\n";
|
|
break;
|
|
}
|
|
$previewHtml = "<a href='?page=" . rawurlencode($pagename) . "'>$previewHtml$pagename</a>";
|
|
|
|
$variableValue[$pagename] = "<li style='min-width: $previewSize" . "px; min-height: $previewSize" . "px;'>$previewHtml</li>";
|
|
}
|
|
|
|
if(count($variableValue) === 0)
|
|
$variableValue["default"] = "<li><em>(No files found)</em></li>\n";
|
|
$variableValue = implode("\n", $variableValue);
|
|
$variableValue = "<ul class='file-gallery'>$variableValue</ul>";
|
|
break;
|
|
}
|
|
if(isset($params[$variableKey]))
|
|
$variableValue = $this->escapeText($params[$variableKey]);
|
|
|
|
if($variableValue !== false)
|
|
{
|
|
return [
|
|
"extent" => strlen($matches[0]),
|
|
"element" => [
|
|
"name" => "span",
|
|
"attributes" => [
|
|
"class" => "template-var-value"
|
|
],
|
|
// rawHtml is fine here 'cause we escape above
|
|
// Note also that we *must* return some sort of element here: we can't just return rawHtml directly. It needs to be a property of an element.
|
|
"rawHtml" => $variableValue
|
|
]
|
|
];
|
|
}
|
|
}
|
|
else if(preg_match("/\{\{([^}]+)\}\}/", $fragment["text"], $matches))
|
|
{
|
|
$templateElement = $this->templateHandler($matches[1]);
|
|
|
|
if(!empty($templateElement))
|
|
{
|
|
return [
|
|
"extent" => strlen($matches[0]),
|
|
"element" => $templateElement
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles parsing out templates - recursively - and the parameter stack associated with it.
|
|
* @param string $source The source string to process.
|
|
* @return array The parsed result
|
|
*/
|
|
protected function templateHandler($source)
|
|
{
|
|
global $pageindex, $env;
|
|
|
|
|
|
$parts = preg_split("/\\||¦/", trim($source, "{}"));
|
|
$parts = array_map("trim", $parts);
|
|
|
|
// Extract the name of the template page
|
|
$templatePagename = array_shift($parts);
|
|
// If the page that we are supposed to use as the tempalte doesn't
|
|
// exist, then there's no point in continuing.
|
|
if(empty($pageindex->$templatePagename))
|
|
return false;
|
|
|
|
// Parse the parameters
|
|
$params = [];
|
|
$i = 0;
|
|
foreach($parts as $part)
|
|
{
|
|
if(strpos($part, "=") !== false)
|
|
{
|
|
// This param contains an equals sign, so it's a named parameter
|
|
$keyValuePair = explode("=", $part, 2);
|
|
$keyValuePair = array_map("trim", $keyValuePair);
|
|
$params[$keyValuePair[0]] = $keyValuePair[1];
|
|
}
|
|
else
|
|
{
|
|
// This isn't a named parameter
|
|
$params["$i"] = trim($part);
|
|
|
|
$i++;
|
|
}
|
|
}
|
|
// Add the parsed parameters to the parameter stack
|
|
$this->paramStack[] = [
|
|
"pagename" => $templatePagename,
|
|
"params" => $params
|
|
];
|
|
|
|
$templateFilePath = $env->storage_prefix . $pageindex->$templatePagename->filename;
|
|
|
|
// We use linesElements here to avoid resetting $this->DefinitionData (which is done in ->textElements)
|
|
// Consequently, we have to do what textElements does directly
|
|
$parsedTemplateSource = $this->linesElements(explode("\n",
|
|
trim(str_replace(["\r\n", "\r"], "\n", file_get_contents($templateFilePath)), "\n")
|
|
));
|
|
|
|
// Remove the parsed parameters from the stack
|
|
array_pop($this->paramStack);
|
|
|
|
return [
|
|
"name" => "div",
|
|
"elements" => $parsedTemplateSource,
|
|
"attributes" => [
|
|
"class" => "template"
|
|
]
|
|
];
|
|
}
|
|
|
|
|
|
/*
|
|
* ██████ ██ ██ ███████ ██████ ██ ██ ██████ ██████ ██ ██
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ██ ███████ █████ ██ █████ ██████ ██ ██ ███
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ██████ ██ ██ ███████ ██████ ██ ██ ██████ ██████ ██ ██
|
|
* %checkbox
|
|
*/
|
|
protected function inlineCheckbox($fragment) {
|
|
// We're not interested if it's not at the beginning of a line
|
|
if(strpos($fragment["context"], $fragment["text"]) !== 0)
|
|
return;
|
|
|
|
// If it doesn't match, then we're not interested
|
|
if(preg_match('/\[([ x])\]/u', $fragment["text"], $matches) !== 1)
|
|
return;
|
|
|
|
$checkbox_content = $matches[1];
|
|
|
|
$result = [
|
|
"extent" => 3,
|
|
"element" => [
|
|
"name" => "input",
|
|
"attributes" => [
|
|
"type" => "checkbox",
|
|
"disabled" => "disabled"
|
|
]
|
|
]
|
|
];
|
|
|
|
if($checkbox_content === "x")
|
|
$result["element"]["attributes"]["checked"] = "checked";
|
|
|
|
return $result;
|
|
}
|
|
|
|
|
|
/*
|
|
* ███ ███ █████ ██████ ██ ██ ███████ ██████
|
|
* ████ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ██ ████ ██ ███████ ██████ █████ █████ ██ ██
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ███████ ██████
|
|
*
|
|
* ████████ ███████ ██ ██ ████████
|
|
* ██ ██ ██ ██ ██
|
|
* ██ █████ ███ ██
|
|
* ██ ██ ██ ██ ██
|
|
* ██ ███████ ██ ██ ██
|
|
* %markedtext
|
|
*/
|
|
protected function inlineMark($fragment) {
|
|
// A question mark makes the PCRE 1-or-more + there lazy instead of greedy.
|
|
// Ref https://www.rexegg.com/regex-quantifiers.html#greedytrap
|
|
if(preg_match('/==([^=]+?)==/', $fragment["text"], $matches) !== 1)
|
|
return;
|
|
|
|
$marked_text = $matches[1];
|
|
|
|
$result = [
|
|
"extent" => strlen($matches[0]),
|
|
"element" => [
|
|
"name" => "mark",
|
|
"handler" => [
|
|
"function" => "lineElements",
|
|
"argument" => $marked_text,
|
|
"destination" => "elements"
|
|
]
|
|
]
|
|
];
|
|
return $result;
|
|
}
|
|
|
|
|
|
/*
|
|
* ██████ ██ ██ ██████ ██ ██████ ██ ██ ██████ ███████ ██████
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ███████ ██ ██ ██████ ██ ███████ ██ ██ ██████ █████ ██████
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ██████ ██████ ██████ ██ ██████ ██████ ██ ███████ ██ ██
|
|
*
|
|
* ██████ ██████ ██████ ██ ██████ ████████
|
|
* ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ███████ ██ ██████ ██ ██████ ██
|
|
* ██ ██ ██ ██ ██ ██ ██
|
|
* ██████ ██████ ██ ██ ██ ██ ██
|
|
* %subsuperscript
|
|
*/
|
|
protected function inlineSuperscript($fragment) {
|
|
if(preg_match('/\^([^^]+?)\^/', $fragment["text"], $matches) !== 1)
|
|
return;
|
|
|
|
$superscript_text = $matches[1];
|
|
|
|
$result = [
|
|
"extent" => strlen($matches[0]),
|
|
"element" => [
|
|
"name" => "sup",
|
|
"handler" => [
|
|
"function" => "lineElements",
|
|
"argument" => $superscript_text,
|
|
"destination" => "elements"
|
|
]
|
|
]
|
|
];
|
|
return $result;
|
|
}
|
|
protected function inlineSubscript($fragment) {
|
|
if(preg_match('/~([^~]+?)~/', $fragment["text"], $matches) !== 1)
|
|
return;
|
|
|
|
$subscript_text = $matches[1];
|
|
|
|
$result = [
|
|
"extent" => strlen($matches[0]),
|
|
"element" => [
|
|
"name" => "sub",
|
|
"handler" => [
|
|
"function" => "lineElements",
|
|
"argument" => $subscript_text,
|
|
"destination" => "elements"
|
|
]
|
|
]
|
|
];
|
|
return $result;
|
|
}
|
|
|
|
|
|
/*
|
|
* ███████ ██████ ██████ ██ ██ ███████ ██████
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ███████ ██████ ██ ██ ██ ██ █████ ██████
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ███████ ██ ██████ ██ ███████ ███████ ██ ██
|
|
* %spoiler
|
|
*/
|
|
protected function inlineSpoiler($fragment) {
|
|
if(preg_match('/(?:\|\||>!)([^|]+?)(?:\|\||!<)/', $fragment["text"], $matches) !== 1)
|
|
return;
|
|
|
|
$spoiler_text = $matches[1];
|
|
$id = "spoiler-".crypto_id(24);
|
|
|
|
$result = [
|
|
"extent" => strlen($matches[0]),
|
|
"element" => [
|
|
"name" => "a",
|
|
"attributes" => [
|
|
"id" => $id,
|
|
"class" => "spoiler",
|
|
"href" => "#$id"
|
|
],
|
|
"handler" => [
|
|
"function" => "lineElements",
|
|
"argument" => $spoiler_text,
|
|
"destination" => "elements"
|
|
]
|
|
]
|
|
];
|
|
return $result;
|
|
}
|
|
|
|
|
|
/*
|
|
* ██ ███ ██ ████████ ███████ ██████ ███ ██ █████ ██
|
|
* ██ ████ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██
|
|
* ██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ███████ ██
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ██ ██ ████ ██ ███████ ██ ██ ██ ████ ██ ██ ███████
|
|
*
|
|
* ██ ██ ███ ██ ██ ██ ███████
|
|
* ██ ██ ████ ██ ██ ██ ██
|
|
* ██ ██ ██ ██ ██ █████ ███████
|
|
* ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ███████ ██ ██ ████ ██ ██ ███████
|
|
* %internallinks
|
|
*/
|
|
/**
|
|
* Parses internal links
|
|
* @param string $fragment The fragment to parse.
|
|
*/
|
|
protected function inlineInternalLink($fragment)
|
|
{
|
|
global $pageindex, $env;
|
|
|
|
if(preg_match('/^\[\[([^\]]*?)\]\]([^\s!?",;.()\[\]{}*=+\/]*)/u', $fragment["text"], $matches) === 1) {
|
|
// 1: Parse parameters out
|
|
// -------------------------------
|
|
$link_page = str_replace(["\r", "\n"], [" ", " "], trim($matches[1]));
|
|
$display = $link_page . trim($matches[2]);
|
|
if(strpos($matches[1], "|") !== false || strpos($matches[1], "¦") !== false)
|
|
{
|
|
// We have a bar character
|
|
$parts = preg_split("/\\||¦/", $matches[1], 2);
|
|
$link_page = trim($parts[0]); // The page to link to
|
|
$display = trim($parts[1]); // The text to display
|
|
}
|
|
|
|
|
|
// 2: Parse the hash out
|
|
// -------------------------------
|
|
$hash_code = "";
|
|
if(strpos($link_page, "#") !== false)
|
|
{
|
|
// We want to link to a subsection of a page
|
|
$hash_code = substr($link_page, strpos($link_page, "#") + 1);
|
|
$link_page = substr($link_page, 0, strpos($link_page, "#"));
|
|
|
|
// If $link_page is empty then we want to link to the current page
|
|
if(strlen($link_page) === 0)
|
|
$link_page = $env->page;
|
|
}
|
|
|
|
|
|
// 3: Page name auto-correction
|
|
// -------------------------------
|
|
$is_interwiki_link = module_exists("feature-interwiki-links") && is_interwiki_link($link_page);
|
|
// Try different variants on the pagename to try and get it to
|
|
// match something automagically
|
|
if(!$is_interwiki_link && empty($pageindex->$link_page))
|
|
$link_page = parsedown_pagename_resolve($link_page);
|
|
|
|
|
|
// 4: Construct the full url
|
|
// -------------------------------
|
|
$link_url = null;
|
|
// If it's an interwiki link, then handle it as such
|
|
if($is_interwiki_link)
|
|
$link_url = interwiki_get_pagename_url($link_page);
|
|
|
|
// If it isn't (or it failed), then try it as a normal link instead
|
|
if(empty($link_url)) {
|
|
$link_url = str_replace(
|
|
"%s", rawurlencode($link_page),
|
|
$this->internalLinkBase
|
|
);
|
|
// We failed to handle it as an interwiki link, so we should
|
|
// tell everyone that
|
|
$is_interwiki_link = false;
|
|
}
|
|
|
|
// 5: Construct the title
|
|
// -------------------------------
|
|
$title = $link_page;
|
|
if($is_interwiki_link)
|
|
$title = interwiki_pagename_resolve($link_page)->name . ": " . interwiki_pagename_parse($link_page)[1] . " (Interwiki)";
|
|
|
|
if(strlen($hash_code) > 0)
|
|
$link_url .= "#$hash_code";
|
|
|
|
|
|
// 6: Result encoding
|
|
// -------------------------------
|
|
$result = [
|
|
"extent" => strlen($matches[0]),
|
|
"element" => [
|
|
"name" => "a",
|
|
"text" => $display,
|
|
|
|
"attributes" => [
|
|
"href" => $link_url,
|
|
"title" => $title
|
|
]
|
|
]
|
|
];
|
|
|
|
// Attach some useful classes based on how we handled it
|
|
$class_list = [];
|
|
// Interwiki links can never be redlinks
|
|
if(!$is_interwiki_link && empty($pageindex->{makepathsafe($link_page)}))
|
|
$class_list[] = "redlink";
|
|
if($is_interwiki_link)
|
|
$class_list[] = "interwiki_link";
|
|
|
|
$result["element"]["attributes"]["class"] = implode(" ", $class_list);
|
|
|
|
return $result;
|
|
}
|
|
}
|
|
|
|
/*
|
|
███ █████ ███ ██ ██ ██████ ███ ███ ██████ ██
|
|
██ ██ ██ ██ ██ ██ ██ ██ ████ ████ ██ ██ ██
|
|
██ ███████ ██ ██ ██ ██████ ██ ████ ██ ██ ██ ██
|
|
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
███ ██ ██ ███ ██ ██ ██ ██████ ██ ██ ██ ██████ ██
|
|
*/
|
|
// This wraps the native [display text](url) syntax
|
|
protected function inlineLink($fragment) {
|
|
global $settings;
|
|
|
|
// If this feature is disabled, defer to the parent implementation unconditionally
|
|
if(!$settings->parser_mangle_external_links)
|
|
return parent::inlineLink($fragment);
|
|
|
|
// Extract the URL from the internal link. If it fails defer to the parent function
|
|
// 1 = display text, 2 = url
|
|
if(preg_match("/^\[([^[\]]+?)\]\(\s*([^()]+)\s*\)/", $fragment["text"], $matches) !== 1)
|
|
return parent::inlineLink($fragment);
|
|
|
|
// Check the URL. If it doesn't match our *exact* format, then it doesn't happen.
|
|
if(!is_string($matches[2]) || preg_match("/^\.\/(.+)\.md$/", $matches[2], $matches_url) !== 1)
|
|
return parent::inlineLink($fragment);
|
|
|
|
// The page name is made safe when Pepperminty Wiki does initial consistency checks (if it's unsafe it results in a 301 redirect)
|
|
$page_name = parsedown_pagename_resolve($matches_url[1]);
|
|
|
|
$internal_link_text = "[[${page_name}]]";
|
|
if(!empty($matches[1])) // If the display text isn't empty, then respect it
|
|
$internal_link_text = "[[${page_name}¦{$matches[1]}]]";
|
|
|
|
$result = $this->inlineInternalLink([
|
|
"text" => $internal_link_text
|
|
]);
|
|
$result["extent"] = strlen($fragment["text"]); // Parsedown isn't mb_ friendly
|
|
return $result;
|
|
}
|
|
|
|
/*
|
|
* ███████ ██ ██ ████████ ███████ ███ ██ ██████ ███████ ██████
|
|
* ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██
|
|
* █████ ███ ██ █████ ██ ██ ██ ██ ██ █████ ██ ██
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ███████ ██ ██ ██ ███████ ██ ████ ██████ ███████ ██████
|
|
*
|
|
* ██ ███ ███ █████ ██████ ███████ ███████
|
|
* ██ ████ ████ ██ ██ ██ ██ ██
|
|
* ██ ██ ████ ██ ███████ ██ ███ █████ ███████
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ██ ██ ██ ██ ██ ██████ ███████ ███████
|
|
* %extendedimages
|
|
*/
|
|
/**
|
|
* Parses the extended image syntax.
|
|
* @param string $fragment The source fragment to parse.
|
|
*/
|
|
protected function inlineExtendedImage($fragment)
|
|
{
|
|
global $pageindex;
|
|
|
|
if(preg_match('/^!\[(.*)\]\(([^|¦)]+)\s*(?:(?:\||¦)([^|¦)]*))?(?:(?:\||¦)([^|¦)]*))?(?:(?:\||¦)([^)]*))?\)/', $fragment["text"], $matches))
|
|
{
|
|
/*
|
|
* 0 - Everything
|
|
* 1 - Alt text
|
|
* 2 - Url
|
|
* 3 - First param (optional)
|
|
* 4 - Second param (optional)
|
|
* 5 - Third param (optional)
|
|
*/
|
|
$altText = $matches[1];
|
|
$imageUrl = trim(str_replace("&", "&", $matches[2])); // Decode & to allow it in preview urls
|
|
$param1 = empty($matches[3]) ? false : strtolower(trim($matches[3]));
|
|
$param2 = empty($matches[4]) ? false : strtolower(trim($matches[4]));
|
|
$param3 = empty($matches[5]) ? false : strtolower(trim($matches[5]));
|
|
$floatDirection = false;
|
|
$imageSize = false;
|
|
$imageCaption = false;
|
|
$shortImageUrl = false;
|
|
|
|
if($this->isFloatValue($param1))
|
|
{
|
|
// Param 1 is a valid css float: ... value
|
|
$floatDirection = $param1;
|
|
$imageSize = $this->parseSizeSpec($param2);
|
|
}
|
|
else if($this->isFloatValue($param2))
|
|
{
|
|
// Param 2 is a valid css float: ... value
|
|
$floatDirection = $param2;
|
|
$imageSize = $this->parseSizeSpec($param1);
|
|
}
|
|
else if($this->isFloatValue($param3))
|
|
{
|
|
$floatDirection = $param3;
|
|
$imageSize = $this->parseSizeSpec($param1);
|
|
}
|
|
else if($param1 === false and $param2 === false)
|
|
{
|
|
// Neither params were specified
|
|
$floatDirection = false;
|
|
$imageSize = false;
|
|
}
|
|
else
|
|
{
|
|
// Neither of them are floats, but at least one is specified
|
|
// This must mean that the first param is a size spec like
|
|
// 250x128.
|
|
$imageSize = $this->parseSizeSpec($param1);
|
|
}
|
|
|
|
if($param1 !== false && strtolower(trim($param1)) == "caption")
|
|
$imageCaption = true;
|
|
if($param2 !== false && strtolower(trim($param2)) == "caption")
|
|
$imageCaption = true;
|
|
if($param3 !== false && strtolower(trim($param3)) == "caption")
|
|
$imageCaption = true;
|
|
|
|
//echo("Image url: $imageUrl, Pageindex entry: " . var_export(isset($pageindex->$imageUrl), true) . "\n");
|
|
|
|
if(isset($pageindex->$imageUrl) and $pageindex->$imageUrl->uploadedfile)
|
|
{
|
|
// We have a short url! Expand it.
|
|
$shortImageUrl = $imageUrl;
|
|
$imageUrl = "index.php?action=preview";
|
|
if($imageSize !== false) $imageUrl .= "&size=" . max($imageSize["x"], $imageSize["y"]);
|
|
else $imageUrl .= "&size=original";
|
|
// This has to be last in order for the extension auto-detection to work correctly
|
|
$imageUrl .= "&page=" . rawurlencode($shortImageUrl);
|
|
}
|
|
|
|
$style = "";
|
|
if($imageSize !== false)
|
|
$style .= " max-width: " . $imageSize["x"] . "px; max-height: " . $imageSize["y"] . "px;";
|
|
if($floatDirection)
|
|
$style .= " float: $floatDirection;";
|
|
|
|
$urlExtension = pathinfo($imageUrl, PATHINFO_EXTENSION);
|
|
$urlType = system_extension_mime_type($urlExtension);
|
|
$embed_type = substr($urlType, 0, strpos($urlType, "/"));
|
|
if($urlType == "application/pdf") $embed_type = "pdf";
|
|
$result = [];
|
|
switch($embed_type)
|
|
{
|
|
case "pdf":
|
|
$imageUrl = preg_replace("/&size=[0-9]+/", "&size=original", $imageUrl);
|
|
if($imageSize === false) $style .= "width: 100%;";
|
|
else $style = str_replace("max-width", "width", $style);
|
|
|
|
$result = [
|
|
"extent" => strlen($matches[0]),
|
|
"element" => [
|
|
"name" => "object",
|
|
"text" => "",
|
|
"attributes" => [
|
|
"type" => "application/pdf",
|
|
"data" => $imageUrl,
|
|
"style" => trim($style)
|
|
]
|
|
]
|
|
];
|
|
break;
|
|
|
|
case "audio":
|
|
$result = [
|
|
"extent" => strlen($matches[0]),
|
|
"element" => [
|
|
"name" => "audio",
|
|
"text" => $altText,
|
|
"attributes" => [
|
|
"src" => $imageUrl,
|
|
"controls" => "controls",
|
|
"preload" => "metadata",
|
|
"style" => trim($style)
|
|
]
|
|
]
|
|
];
|
|
break;
|
|
case "video":
|
|
$result = [
|
|
"extent" => strlen($matches[0]),
|
|
"element" => [
|
|
"name" => "video",
|
|
"text" => $altText,
|
|
"attributes" => [
|
|
"src" => $imageUrl,
|
|
"controls" => "controls",
|
|
"preload" => "metadata",
|
|
"style" => trim($style)
|
|
]
|
|
]
|
|
];
|
|
break;
|
|
case "image":
|
|
default:
|
|
// If we can't work out what it is, then assume it's an image
|
|
$result = [
|
|
"extent" => strlen($matches[0]),
|
|
"element" => [
|
|
"name" => "img",
|
|
"attributes" => [
|
|
"src" => $imageUrl,
|
|
"alt" => $altText,
|
|
"title" => $altText,
|
|
"style" => trim($style)
|
|
]
|
|
]
|
|
];
|
|
break;
|
|
}
|
|
|
|
// ~ Image linker ~
|
|
|
|
$imageHref = $shortImageUrl !== false ? "?page=" . rawurlencode($shortImageUrl) : $imageUrl;
|
|
if($embed_type !== "pdf") {
|
|
$result["element"] = [
|
|
"name" => "a",
|
|
"attributes" => [
|
|
"href" => $imageHref
|
|
],
|
|
"text" => [$result["element"]],
|
|
"handler" => "elements"
|
|
];
|
|
}
|
|
|
|
// ~
|
|
|
|
if($imageCaption) {
|
|
$rawStyle = $result["element"]["text"][0]["attributes"]["style"];
|
|
$containerStyle = preg_replace('/^.*float/', "float", $rawStyle);
|
|
$mediaStyle = preg_replace('/\s*float.*;/', "", $rawStyle);
|
|
$result["element"] = [
|
|
"name" => "figure",
|
|
"attributes" => [
|
|
"style" => $containerStyle
|
|
],
|
|
"text" => [
|
|
$result["element"],
|
|
[
|
|
"name" => "figcaption",
|
|
// We use lineElements here because of issue #209 - in short it makes it appear as part of the same document to avoid breaking footnotes
|
|
// Specifically ->text() calls ->textElements(), which resets $this->DefinitionData, which is used to hold information about footnotes
|
|
// lineElements = inline text, and linesElements = multiline text
|
|
"handler" => [
|
|
"function" => "lineElements",
|
|
"argument" => $altText,
|
|
"destination" => "elements"
|
|
]
|
|
],
|
|
],
|
|
"handler" => "elements"
|
|
];
|
|
$result["element"]["text"][0]["attributes"]["style"] = $mediaStyle;
|
|
}
|
|
return $result;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* ██████ ██████ ██████ ███████ ██████ ██ ██████ ██████ ██ ██
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ██████ ██████ ██████ ███████ ██████ ███████ ██████ ██████ ██ ██
|
|
*
|
|
* ██ ██ ██████ ██████ ██████ █████ ██████ ███████
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ██ ██ ██████ ██ ███ ██████ ███████ ██ ██ █████
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ██████ ██ ██████ ██ ██ ██ ██ ██████ ███████
|
|
* %codeblockupgrade
|
|
*/
|
|
|
|
protected function blockFencedCodeComplete($block) {
|
|
global $settings;
|
|
$result = parent::blockFencedCodeComplete($block);
|
|
|
|
// Default value: "text"
|
|
$language = preg_replace("/^language-/", "", $block["element"]["element"]["attributes"]["class"] ?? "language-text");
|
|
|
|
if(!isset($settings->parser_ext_renderers->$language))
|
|
return $result;
|
|
|
|
$text = $result["element"]["element"]["text"];
|
|
$renderer = $settings->parser_ext_renderers->$language;
|
|
|
|
$result["element"] = [
|
|
"name" => "p",
|
|
"element" => [
|
|
"name" => "img",
|
|
"attributes" => [
|
|
"alt" => "Diagram rendered by {$renderer->name}",
|
|
"src" => "?action=parsedown-render-ext&language=".rawurlencode($language)."&immutable_key=".hash("crc32b", json_encode($renderer))."&source=".rawurlencode($text)
|
|
]
|
|
]
|
|
];
|
|
if(!empty($renderer->output_classes))
|
|
$result["element"]["element"]["attributes"]["class"] = implode(" ", $renderer->output_classes);
|
|
|
|
return $result;
|
|
}
|
|
|
|
/*
|
|
* ██ ██ ███████ █████ ██████ ███████ ██████
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ███████ █████ ███████ ██ ██ █████ ██████
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ██ ██ ███████ ██ ██ ██████ ███████ ██ ██
|
|
* %header %toc
|
|
*/
|
|
|
|
private $headingIdsUsed = [];
|
|
private $tableOfContents = [];
|
|
|
|
/**
|
|
* Inserts an item into the table of contents.
|
|
* @param int $level The level to insert it at (valid values: 1 - 6)
|
|
* @param string $id The id of the item.
|
|
* @param string $text The text to display.
|
|
*/
|
|
protected function addTableOfContentsEntry(int $level, string $id, string $text) : void {
|
|
$new_obj = (object) [
|
|
"level" => $level,
|
|
"id" => $id,
|
|
"text" => $text,
|
|
"children" => []
|
|
];
|
|
|
|
if(count($this->tableOfContents) == 0) {
|
|
$this->tableOfContents[] = $new_obj;
|
|
return;
|
|
}
|
|
|
|
$lastEntry = end($this->tableOfContents);
|
|
if($level > $lastEntry->level) {
|
|
$this->insertTableOfContentsObject($new_obj, $lastEntry);
|
|
return;
|
|
}
|
|
$this->tableOfContents[] = $new_obj;
|
|
return;
|
|
}
|
|
private function insertTableOfContentsObject(object $obj, object $target) {
|
|
if($obj->level - 1 > $target->level && !empty($target->children)) {
|
|
$this->insertTableOfContentsObject($obj, end($target->children));
|
|
}
|
|
$target->children[] = $obj;
|
|
}
|
|
|
|
protected function generateTableOfContents() : string {
|
|
global $settings;
|
|
$elements = [ $this->generateTableOfContentsElement($this->tableOfContents) ];
|
|
if($settings->parser_toc_heading_level > 1)
|
|
array_unshift(
|
|
$elements,
|
|
[ "name" => "h$settings->parser_toc_heading_level", "text" => "Table of Contents", "attributes" => [ "id" => "table-of-contents" ] ]
|
|
);
|
|
|
|
return trim($this->elements($elements), "\n");
|
|
}
|
|
private function generateTableOfContentsElement($toc) : array {
|
|
$elements = [];
|
|
foreach($toc as $entry) {
|
|
$next = [
|
|
"name" => "li",
|
|
"attributes" => [
|
|
"data-level" => $entry->level
|
|
],
|
|
"elements" => [ [
|
|
"name" => "a",
|
|
"attributes" => [
|
|
"href" => "#$entry->id"
|
|
],
|
|
"handler" => [
|
|
"function" => "lineElements",
|
|
"argument" => $entry->text,
|
|
"destination" => "elements"
|
|
]
|
|
] ]
|
|
];
|
|
if(isset($entry->children))
|
|
$next["elements"][] = $this->generateTableOfContentsElement($entry->children);
|
|
|
|
$elements[] = $next;
|
|
}
|
|
|
|
return [
|
|
"name" => "ul",
|
|
"elements" => $elements
|
|
];
|
|
}
|
|
|
|
protected function blockHeader($line) {
|
|
// This function overrides the header function defined in ParsedownExtra
|
|
$result = parent::blockHeader($line);
|
|
|
|
// If this heading doesn't have an id already, add an automatic one
|
|
if(!isset($result["element"]["attributes"]["id"])) {
|
|
$heading_id = str_replace(" ", "-",
|
|
mb_strtolower(makepathsafe(
|
|
$result["element"]["handler"]["argument"]
|
|
))
|
|
);
|
|
$suffix = "";
|
|
while(in_array($heading_id . $suffix, $this->headingIdsUsed)) {
|
|
$heading_number = intval(str_replace("_", "", $suffix));
|
|
if($heading_number == 0) $heading_number++;
|
|
$suffix = "_" . ($heading_number + 1);
|
|
}
|
|
$result["element"]["attributes"]["id"] = $heading_id . $suffix;
|
|
$this->headingIdsUsed[] = $result["element"]["attributes"]["id"];
|
|
}
|
|
|
|
$this->addTableOfContentsEntry(
|
|
intval(strtr($result["element"]["name"], [ "h" => "" ])),
|
|
$result["element"]["attributes"]["id"],
|
|
$result["element"]["handler"]["argument"]
|
|
);
|
|
|
|
return $result;
|
|
}
|
|
|
|
/*
|
|
* Inserts a special string to identify where we need to put the table of contents later
|
|
*/
|
|
protected function blockTableOfContents($fragment) {
|
|
// Indent? Don't even want to know
|
|
if($fragment["indent"] > 0) return;
|
|
// If it doesn't match, then we're not interested
|
|
if(preg_match('/^\[_*(?:TOC|toc)_*\]$/u', $fragment["text"], $matches) !== 1)
|
|
return;
|
|
|
|
$result = [
|
|
"element" => [ "text" => self::TOC_ID ]
|
|
];
|
|
return $result;
|
|
}
|
|
|
|
# ~
|
|
# Static Methods
|
|
# ~
|
|
|
|
/**
|
|
* Extracts the page names from internal links in a given markdown source.
|
|
* Does not actually _parse_ the source - only extracts via a regex.
|
|
* @param string $page_text The source text to extract a list of page names from.
|
|
* @return array A list of page names that the given source text links to.
|
|
*/
|
|
public static function extract_page_names($page_text) {
|
|
global $pageindex;
|
|
preg_match_all("/\[\[([^\]]+)\]\]/", $page_text, $linked_pages);
|
|
if(count($linked_pages[1]) === 0)
|
|
return []; // No linked pages here
|
|
|
|
$result = [];
|
|
foreach($linked_pages[1] as $linked_page) {
|
|
// Strip everything after the | and the #
|
|
if(strpos($linked_page, "|") !== false)
|
|
$linked_page = substr($linked_page, 0, strpos($linked_page, "|"));
|
|
if(strpos($linked_page, "#") !== false)
|
|
$linked_page = substr($linked_page, 0, strpos($linked_page, "#"));
|
|
if(strlen($linked_page) === 0)
|
|
continue;
|
|
// Make sure we try really hard to find this page in the
|
|
// pageindex
|
|
$altered_linked_page = $linked_page;
|
|
if(!empty($pageindex->{ucfirst($linked_page)}))
|
|
$altered_linked_page = ucfirst($linked_page);
|
|
else if(!empty($pageindex->{ucwords($linked_page)}))
|
|
$altered_linked_page = ucwords($linked_page);
|
|
else // Our efforts were in vain, so reset to the original
|
|
$altered_linked_page = $linked_page;
|
|
|
|
$result[] = $altered_linked_page;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
|
|
/*
|
|
* ██ ██ ████████ ██ ██ ██ ████████ ██ ███████ ███████
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ █████ ███████
|
|
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
* ██████ ██ ██ ███████ ██ ██ ██ ███████ ███████
|
|
* %utilities
|
|
*/
|
|
|
|
/**
|
|
* Returns whether a string is a valid float: XXXXXX; value.
|
|
* Used in parsing the extended image syntax.
|
|
* @param string $value The value check.
|
|
* @return bool Whether it's valid or not.
|
|
*/
|
|
private function isFloatValue(string $value)
|
|
{
|
|
return in_array(strtolower($value), [ "left", "right" ]);
|
|
}
|
|
|
|
/**
|
|
* Parses a size specifier into an array.
|
|
* @param string $text The source text to parse. e.g. "256x128"
|
|
* @return array|bool The parsed size specifier. Example: ["x" => 256, "y" => 128]. Returns false if parsing failed.
|
|
*/
|
|
private function parseSizeSpec(string $text)
|
|
{
|
|
if(strpos($text, "x") === false)
|
|
return false;
|
|
$parts = explode("x", $text, 2);
|
|
|
|
if(count($parts) != 2)
|
|
return false;
|
|
|
|
array_map("trim", $parts);
|
|
array_map("intval", $parts);
|
|
|
|
if(in_array(0, $parts))
|
|
return false;
|
|
|
|
return [
|
|
"x" => $parts[0],
|
|
"y" => $parts[1]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Escapes the source text via htmlentities.
|
|
* @param string $text The text to escape.
|
|
* @return string The escaped string.
|
|
*/
|
|
protected function escapeText($text)
|
|
{
|
|
return htmlentities($text, ENT_COMPAT | ENT_HTML5);
|
|
}
|
|
|
|
/**
|
|
* Sets the base url to be used for internal links. '%s' will be replaced
|
|
* with a URL encoded version of the page name.
|
|
* @param string $url The url to use when parsing internal links.
|
|
*/
|
|
public function setInternalLinkBase($url)
|
|
{
|
|
$this->internalLinkBase = $url;
|
|
}
|
|
}
|
|
|
|
?>
|