"Parsedown", "version" => "0.7.2", "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, and also *requires* write access to the disk on first load.", "id" => "parser-parsedown", "code" => function() { global $settings; $parser = new PeppermintParsedown(); $parser->setInternalLinkBase("?page=%s"); add_parser("parsedown", function($source) use ($parser) { global $settings; if($settings->clean_raw_html) $parser->setMarkupEscaped(true); else $parser->setMarkupEscaped(false); $result = $parser->text($source); return $result; }); add_help_section("20-parser-default", "Editor Syntax", "

$settings->sitename's editor uses an extended version of Parsedown to render pages, which is a fantastic open source Github flavoured markdown parser. You can find a quick reference guide on Github flavoured markdown here by adam-p, or if you prefer a book Mastering Markdown by KB is a good read, and free too!

Extra Syntax

$settings->sitename's editor also supports some extra custom syntax, some of which is inspired by Mediawiki.
Type thisTo get thisComments
[[Internal link]]Internal LinkAn internal link.
[[Display Text|Internal link]]Display TextAn internal link with some display text.
![Alt text](http://example.com/path/to/image.png | 256x256 | right)Alt textAn image floating to the right of the page that fits inside a 256px x 256px box, preserving aspect ratio.

Templating

$settings->sitename also supports including one page in another page as a template. The syntax is very similar to that of Mediawiki. For example, {{Announcement banner}} will include the contents of the \"Announcement banner\" page, assuming it exists.

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. {{{Announcement text}}}), and set when including a page with the bar syntax (e.g. {{Announcement banner | importance = high | text = Maintenance has been planned for tonight.}}). Currently the only restriction in templates and variables is that you may not include a closing curly brace (}) in the page name, variable name, or value.

$settings->sitename also supports a number of special built-in variables. Their syntax and function are described below:

Type thisTo get this
{{{@}}}Lists all variables and their values in a table.
{{{#}}}Shows a 'stack trace', outlining all the parent includes of the current page being parsed.
{{{~}}}Outputs the requested pagee's name.
{{{*}}}Outputs a comma separated list of all the subpages of the current page.
{{{+}}}Shows a gallery containing all the files that are sub pages of the current page.
"); } ]); /*** Parsedown versions *** * Parsedown Core: 1.6.0 * * Parsedown Extra: 0.7.0 * **************************/ $env->parsedown_paths = new stdClass(); $env->parsedown_paths->parsedown = "https://cdn.rawgit.com/erusev/parsedown/3ebbd730b5c2cf5ce78bc1bf64071407fc6674b7/Parsedown.php"; $env->parsedown_paths->parsedown_extra = "https://cdn.rawgit.com/erusev/parsedown-extra/11a44e076d02ffcc4021713398a60cd73f78b6f5/ParsedownExtra.php"; // Download parsedown and parsedown extra if they don't already exist if(!file_exists("./Parsedown.php") || filesize("./Parsedown.php") === 0) file_put_contents("./Parsedown.php", fopen($env->parsedown_paths->parsedown, "r")); if(!file_exists("./ParsedownExtra.php") || filesize("./ParsedownExtra.php") === 0) file_put_contents("./ParsedownExtra.php", fopen($env->parsedown_paths->parsedown_extra, "r")); require_once("./Parsedown.php"); require_once("./ParsedownExtra.php"); /* * ██████ █████ ██████ ███████ ███████ ██████ ██████ ██ ██ ███ ██ * ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ * ██████ ███████ ██████ ███████ █████ ██ ██ ██ ██ ██ █ ██ ██ ██ ██ * ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███ ██ ██ ██ ██ * ██ ██ ██ ██ ██ ███████ ███████ ██████ ██████ ███ ███ ██ ████ * * ███████ ██ ██ ████████ ███████ ███ ██ ███████ ██ ██████ ███ ██ ███████ * ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ████ ██ ██ * █████ ███ ██ █████ ██ ██ ██ ███████ ██ ██ ██ ██ ██ ██ ███████ * ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ * ███████ ██ ██ ██ ███████ ██ ████ ███████ ██ ██████ ██ ████ ███████ */ class PeppermintParsedown extends ParsedownExtra { private $internalLinkBase = "./%s"; protected $maxParamDepth = 0; protected $paramStack = []; function __construct() { // Prioritise our internal link parsing over the regular link parsing array_unshift($this->InlineTypes["["], "InternalLink"); // Prioritise our image parser over the regular image parser array_unshift($this->InlineTypes["!"], "ExtendedImage"); $this->inlineMarkerList .= "{"; if(!isset($this->InlineTypes["{"]) or !is_array($this->InlineTypes["{"])) $this->InlineTypes["{"] = []; $this->InlineTypes["{"][] = "Template"; } /* * ████████ ███████ ███ ███ ██████ ██ █████ ████████ ██ ███ ██ ██████ * ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ * ██ █████ ██ ████ ██ ██████ ██ ███████ ██ ██ ██ ██ ██ ██ ███ * ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ * ██ ███████ ██ ██ ██ ███████ ██ ██ ██ ██ ██ ████ ██████ */ 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 = "\n"; foreach($params as $key => $value) { $variableValue .= "\t\n"; } $variableValue .= "
KeyValue
" . $this->escapeText($key) . "" . $this->escapeText($value) . "
"; } break; case "#": // Shows a stack trace $variableValue = "
    \n"; $variableValue .= "\t
  1. $env->page
  2. \n"; foreach($this->paramStack as $curStackEntry) { $variableValue .= "\t
  3. " . $curStackEntry["pagename"] . "
  4. \n"; } $variableValue .= "
\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 = "(none yet!)"; 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); $previewHtml = ""; switch(substr($mime_type, 0, strpos($mime_type, "/"))) { case "video": $previewHtml .= "\n"; break; case "audio": $previewHtml .= "\n"; break; case "application": case "image": default: $previewHtml .= "\n"; break; } $previewHtml = "$previewHtml$pagename"; $variableValue[$pagename] = "
  • $previewHtml
  • "; } if(count($variableValue) === 0) $variableValue["default"] = "
  • (No files found)
  • \n"; $variableValue = implode("\n", $variableValue); $variableValue = ""; break; } if(isset($params[$variableKey])) { $variableValue = $params[$variableKey]; $variableValue = $this->escapeText($variableValue); } if($variableValue !== false) { return [ "extent" => strlen($matches[0]), "markup" => $variableValue ]; } } else if(preg_match("/\{\{([^}]+)\}\}/", $fragment["text"], $matches)) { $templateElement = $this->templateHandler($matches[1]); if(!empty($templateElement)) { return [ "extent" => strlen($matches[0]), "element" => $templateElement ]; } } } protected function templateHandler($source) { global $pageindex, $env; $parts = explode("|", 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 $this->maxParamDepth++; $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; $parsedTemplateSource = $this->text(file_get_contents($templateFilePath)); // Remove the parsed parameters from the stack array_pop($this->paramStack); return [ "name" => "div", "text" => $parsedTemplateSource, "attributes" => [ "class" => "template" ] ]; } protected function inlineInternalLink($fragment) { global $pageindex; if(preg_match('/^\[\[([^\]]*)\]\]/', $fragment["text"], $matches)) { $display = $linkPage = $matches[1]; if(strpos($matches[1], "|")) { // We have a bar character $parts = explode("|", $matches[1], 2); $linkPage = $parts[0]; $display = $parts[1]; } // Construct the full url $linkUrl = str_replace( "%s", rawurlencode($linkPage), $this->internalLinkBase ); $result = [ "extent" => strlen($matches[0]), "element" => [ "name" => "a", "text" => $display, "attributes" => [ "href" => $linkUrl ] ] ]; if(empty($pageindex->$linkPage)) $result["element"]["attributes"]["class"] = "redlink"; return $result; } return; } protected function inlineExtendedImage($fragment) { ///^!\[(.*)\]\(([^ |)]+)\s*(?:\|([^|)]*)(?:\|([^)]*))?)?\)/ 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 = 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; 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($param2 !== false && strtolower(trim($param2)) == "caption") $imageCaption = true; if($param3 !== false && strtolower(trim($param3)) == "caption") $imageCaption = true; $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); $result = []; switch(substr($urlType, 0, strpos($urlType, "/"))) { 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; } if($imageCaption) { $rawStyle = $result["element"]["attributes"]["style"]; $containerStyle = preg_replace('/^.*float/', "float", $rawStyle); $mediaStyle = preg_replace('/\s*float.*;/', "", $rawStyle); $result["element"] = [ "name" => "figure", "text" => [ $result["element"], [ "name" => "figcaption", "text" => htmlentities($altText) ], ], "attributes" => [ "style" => $containerStyle ], "handler" => "elements" ]; $result["element"]["text"][0]["attributes"]["style"] = $mediaStyle; } return $result; } } # ~ # Utility Methods # ~ private function isFloatValue($value) { return in_array(strtolower($value), [ "left", "right" ]); } private function parseSizeSpec($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] ]; } 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; } } ?>