mikuli.cz

:)
git clone https://git.sr.ht/~ashymad/mikuli.cz
Log | Files | Refs

commit e885d5f321628604c8f90bb01e59fea85a752778
parent 13dad7f1061cd2a10d4ba6fb7c722214cc4d3361
Author: markseu <mark2011@mayberg.se>
Date:   Wed, 29 Apr 2015 09:26:48 +0200

Core update (spring remix)

Diffstat:
M.htaccess | 2+-
MREADME.md | 9+++------
Msystem/config/config.ini | 24++++++++++++------------
Rsystem/config/error401.txt -> system/config/page-error-401.txt | 0
Rsystem/config/error404.txt -> system/config/page-error-404.txt | 0
Rsystem/config/error424.txt -> system/config/page-error-424.txt | 0
Rsystem/config/error500.txt -> system/config/page-error-500.txt | 0
Rsystem/config/newpage.txt -> system/config/page-new-default.txt | 0
Msystem/core/core-commandline.php | 200++++++++++++++++++++++++++++++++++++++++----------------------------------------
Msystem/core/core-webinterface.css | 2+-
Msystem/core/core-webinterface.js | 2+-
Msystem/core/core-webinterface.php | 140+++++++++++++++++++++++++++++++++++++------------------------------------------
Msystem/core/core.php | 1861+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Asystem/plugins/markdown.php | 3330+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsystem/plugins/markdownextra.php | 3330-------------------------------------------------------------------------------
Dsystem/snippets/header.php | 17-----------------
Dsystem/snippets/pagination.php | 11-----------
Dsystem/templates/default.php | 7-------
Dsystem/themes/default.css | 60------------------------------------------------------------
Asystem/themes/flatsite.css | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsystem/snippets/content.php -> system/themes/snippets/content.php | 0
Rsystem/snippets/footer.php -> system/themes/snippets/footer.php | 0
Asystem/themes/snippets/header.php | 17+++++++++++++++++
Rsystem/snippets/navigation.php -> system/themes/snippets/navigation.php | 0
Asystem/themes/snippets/pagination.php | 11+++++++++++
Rsystem/snippets/sitename.php -> system/themes/snippets/sitename.php | 0
Asystem/themes/templates/default.html | 6++++++
27 files changed, 4673 insertions(+), 4458 deletions(-)

diff --git a/.htaccess b/.htaccess @@ -13,5 +13,5 @@ RewriteRule ^(cache|content|system)/ error [L] RewriteCond %{REQUEST_FILENAME} -f RewriteRule ^[^\.]+$ - [T=text/html,L] -ErrorDocument 404 /error404.html +ErrorDocument 404 /error.html </IfModule> diff --git a/README.md b/README.md @@ -1,4 +1,4 @@ -Yellow 0.5.2 +Yellow 0.5.3 ============ [![Yellow](https://raw.githubusercontent.com/wiki/datenstrom/yellow/images/yellow.jpg)](http://datenstrom.se/yellow) @@ -9,11 +9,8 @@ How do I install this? 1. [Download Yellow and unzip it](https://github.com/datenstrom/yellow/archive/master.zip). 2. Copy all files to your web hosting. 3. Open your website in a web browser. - -How do I get started? ---------------------- -Start by editing your website. Just give it a try. -For more information see [Yellow documentation](https://github.com/datenstrom/yellow/wiki). + +Installing is unzipping one file and you are ready to go. [Learn more](https://github.com/datenstrom/yellow/wiki). License ------- diff --git a/system/config/config.ini b/system/config/config.ini @@ -3,8 +3,7 @@ sitename = Yellow author = Yellow language = en -theme = default -template = default +theme = flatsite // timeZone = UTC // serverScheme = http @@ -18,14 +17,14 @@ systemDir = system/ configDir = system/config/ coreDir = system/core/ pluginDir = system/plugins/ -snippetDir = system/snippets/ -templateDir = system/templates/ themeDir = system/themes/ +snippetDir = system/themes/snippets/ +templateDir = system/themes/templates/ mediaDir = media/ imageDir = media/images/ staticDir = cache/ staticDefaultFile = index.html -staticErrorFile = error404.html +staticErrorFile = error.html contentDir = content/ contentRootDir = default/ contentHomeDir = home/ @@ -33,18 +32,19 @@ contentDefaultFile = page.txt contentPagination = page contentExtension = .txt configExtension = .ini -errorPageFile = error(.*).txt -newPageFile = new(.*).txt -textStringFile = language(.*).ini +textFile = language-(.*).ini +errorFile = page-error-(.*).txt robotsTextFile = robots.txt -parser = markdownextra +template = default +parser = markdown parserSafeMode = 0 multiLanguageMode = 0 +commandlineIgnoreLocation = +commandlineSystemFile = .htaccess webinterfaceLocation = /edit/ webinterfaceServerScheme = http webinterfaceUserHashAlgorithm = bcrypt webinterfaceUserHashCost = 10 webinterfaceUserFile = user.ini -webinterfaceFilePrefix = published -commandlineIgnoreLocation = -commandlineSystemFile = .htaccess +webinterfaceNewFile = page-new-(.*).txt +webinterfaceMetaFilePrefix = published diff --git a/system/config/error401.txt b/system/config/page-error-401.txt diff --git a/system/config/error404.txt b/system/config/page-error-404.txt diff --git a/system/config/error424.txt b/system/config/page-error-424.txt diff --git a/system/config/error500.txt b/system/config/page-error-500.txt diff --git a/system/config/newpage.txt b/system/config/page-new-default.txt diff --git a/system/core/core-commandline.php b/system/core/core-commandline.php @@ -1,20 +1,20 @@ <?php -// Copyright (c) 2013-2014 Datenstrom, http://datenstrom.se +// Copyright (c) 2013-2015 Datenstrom, http://datenstrom.se // This file may be used and distributed under the terms of the public license. // Command line core plugin class YellowCommandline { - const Version = "0.5.1"; - var $yellow; //access to API - var $content; //number of content pages - var $media; //number of media files - var $system; //number of system files - var $error; //number of build errors - var $locationsArguments; //locations with arguments detected - var $locationsPagination; //locations with pagination detected + const Version = "0.5.3"; + var $yellow; //access to API + var $content; //number of content pages + var $media; //number of media files + var $system; //number of system files + var $error; //number of build errors + var $locationsArgs; //locations with location arguments detected + var $locationsArgsPagination; //locations with pagination arguments detected - // Handle plugin initialisation + // Handle initialisation function onLoad($yellow) { $this->yellow = $yellow; @@ -22,15 +22,6 @@ class YellowCommandline $this->yellow->config->setDefault("commandlineSystemFile", ".htaccess"); } - // Handle command help - function onCommandHelp() - { - $help .= "version\n"; - $help .= "build [DIRECTORY LOCATION]\n"; - $help .= "clean [DIRECTORY LOCATION]\n"; - return $help; - } - // Handle command function onCommand($args) { @@ -51,6 +42,15 @@ class YellowCommandline return $statusCode; } + // Handle command help + function onCommandHelp() + { + $help .= "version\n"; + $help .= "build [DIRECTORY LOCATION]\n"; + $help .= "clean [DIRECTORY LOCATION]\n"; + return $help; + } + // Show available commands function helpCommand() { @@ -93,34 +93,38 @@ class YellowCommandline return $statusCode; } - // Build static directories and files + // Build static pages and files function buildStatic($path, $location) { $this->yellow->toolbox->timerStart($time); $path = rtrim(empty($path) ? $this->yellow->config->get("staticDir") : $path, '/'); $this->content = $this->media = $this->system = $this->error = $statusCode = 0; - $this->locationsArguments = $this->locationsPagination = array(); + $this->locationsArgs = $this->locationsArgsPagination = array(); if(empty($location)) { $statusCode = $this->cleanStatic($path, $location); foreach($this->getStaticLocations() as $location) { - $statusCode = max($statusCode, $this->buildStaticRequest($path, $location, true)); + $statusCode = max($statusCode, $this->buildStaticPage($path, $location, true)); } - foreach($this->locationsArguments as $location) + foreach($this->locationsArgs as $location) { - $statusCode = max($statusCode, $this->buildStaticRequest($path, $location, true)); + $statusCode = max($statusCode, $this->buildStaticPage($path, $location, true)); } - foreach($this->locationsPagination as $location) + foreach($this->locationsArgsPagination as $location) { + if(substru($location, -1) != ':') + { + $statusCode = max($statusCode, $this->buildStaticPage($path, $location, false, true)); + } for($pageNumber=2; $pageNumber<=999; ++$pageNumber) { - $statusCodeLocation = $this->buildStaticRequest($path, $location.$pageNumber, false, true); + $statusCodeLocation = $this->buildStaticPage($path, $location.$pageNumber, false, true); $statusCode = max($statusCode, $statusCodeLocation); if($statusCodeLocation == 0) break; } } - $statusCode = max($statusCode, $this->buildStaticError($path, 404)); + $statusCode = max($statusCode, $this->buildStaticPage($path, "/error", false, false, true)); foreach($this->getStaticFilesMedia($path) as $fileNameSource=>$fileNameDest) { $statusCode = max($statusCode, $this->buildStaticFile($fileNameSource, $fileNameDest, true)); @@ -130,66 +134,33 @@ class YellowCommandline $statusCode = max($statusCode, $this->buildStaticFile($fileNameSource, $fileNameDest, false)); } } else { - $statusCode = $this->buildStaticRequest($path, $location); + $statusCode = $this->buildStaticPage($path, $location); } $this->yellow->toolbox->timerStop($time); if(defined("DEBUG") && DEBUG>=1) echo "YellowCommandline::buildStatic time:$time ms\n"; return $statusCode; } - // Build static request - function buildStaticRequest($path, $location, $analyse = false, $probe = false) + // Build static page + function buildStaticPage($path, $location, $analyse = false, $probe = false, $error = false) { ob_start(); $_SERVER["SERVER_PROTOCOL"] = "HTTP/1.1"; $_SERVER["SERVER_NAME"] = $this->yellow->config->get("serverName"); $_SERVER["REQUEST_URI"] = $this->yellow->config->get("serverBase").$location; - $_SERVER["SCRIPT_NAME"] = $this->yellow->config->get("serverBase")."yellow.php"; + $_SERVER["SCRIPT_NAME"] = $this->yellow->config->get("serverBase")."/yellow.php"; $_REQUEST = array(); $statusCode = $this->yellow->request(); - if($statusCode < 400) + if($statusCode<400 || $error) { - $fileName = $this->yellow->toolbox->findStaticFileFromLocation($location, $path, - $this->yellow->config->get("staticDefaultFile")); $fileData = ob_get_contents(); $modified = strtotime($this->yellow->page->getHeader("Last-Modified")); - if(!$this->yellow->toolbox->createFile($fileName, $fileData, true) || - !$this->yellow->toolbox->modifyFile($fileName, $modified)) + if($statusCode>=301 && $statusCode<=303) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + $fileData = $this->getStaticRedirect($this->yellow->page->getHeader("Location")); + $modified = time(); } - } - ob_end_clean(); - if($statusCode==200 && $analyse) $this->analyseStaticContent($fileData); - if($statusCode==404 && $probe) $statusCode = 0; - if($statusCode != 0) ++$this->content; - if($statusCode >= 400) - { - ++$this->error; - echo "ERROR building content location '$location', ".$this->yellow->page->getStatusCode(true)."\n"; - } - if(defined("DEBUG") && DEBUG>=3) echo $fileData; - if(defined("DEBUG") && DEBUG>=1) echo "YellowCommandline::buildStaticRequest status:$statusCode location:$location\n"; - return $statusCode; - } - - // Build static error - function buildStaticError($path, $statusCodeRequest) - { - ob_start(); - $_SERVER["SERVER_PROTOCOL"] = "HTTP/1.1"; - $_SERVER["SERVER_NAME"] = $this->yellow->config->get("serverName"); - $_SERVER["REQUEST_URI"] = $this->yellow->config->get("serverBase")."/"; - $_SERVER["SCRIPT_NAME"] = $this->yellow->config->get("serverBase")."yellow.php"; - $_REQUEST = array(); - $fileName = "$path/".$this->yellow->config->get("staticErrorFile"); - $statusCode = $this->yellow->request($statusCodeRequest); - if($statusCode == $statusCodeRequest) - { - $statusCode = 200; - $fileData = ob_get_contents(); - $modified = strtotime($this->yellow->page->getHeader("Last-Modified")); + $fileName = $this->getStaticFile($path, $location, $statusCode); if(!$this->yellow->toolbox->createFile($fileName, $fileData, true) || !$this->yellow->toolbox->modifyFile($fileName, $modified)) { @@ -199,13 +170,17 @@ class YellowCommandline } } ob_end_clean(); - ++$this->system; + if($statusCode==200 && $analyse) $this->analyseStaticPage($fileData); + if($statusCode==404 && $error) $statusCode = 200; + if($statusCode==404 && $probe) $statusCode = 0; + if($statusCode != 0) ++$this->content; if($statusCode >= 400) { ++$this->error; - echo "ERROR building error file, ".$this->yellow->page->getStatusCode(true)."\n"; + echo "ERROR building content location '$location', ".$this->yellow->page->getStatusCode(true)."\n"; } - if(defined("DEBUG") && DEBUG>=1) echo "YellowCommandline::buildStaticError status:$statusCode file:$fileName\n"; + if(defined("DEBUG") && DEBUG>=3) echo $fileData; + if(defined("DEBUG") && DEBUG>=1) echo "YellowCommandline::buildStaticPage status:$statusCode location:$location\n"; return $statusCode; } @@ -226,8 +201,8 @@ class YellowCommandline return $statusCode; } - // Analyse static content, detect locations with arguments and pagination - function analyseStaticContent($text) + // Analyse static page, detect locations with arguments + function analyseStaticPage($text) { $serverName = $this->yellow->config->get("serverName"); $serverBase = $this->yellow->config->get("serverBase"); @@ -243,20 +218,20 @@ class YellowCommandline if(!$this->yellow->toolbox->isLocationArgs($match)) continue; if(substru($match, 0, strlenu($serverBase)) != $serverBase) continue; $location = rawurldecode(substru($match, strlenu($serverBase))); - if(!$this->yellow->toolbox->isPaginationLocation($location, $pagination)) + if(!$this->yellow->toolbox->isLocationArgsPagination($location, $pagination)) { $location = rtrim($location, '/').'/'; - if(is_null($this->locationsArguments[$location])) + if(is_null($this->locationsArgs[$location])) { - $this->locationsArguments[$location] = $location; - if(defined("DEBUG") && DEBUG>=2) echo "YellowCommandline::analyseStaticContent type:arguments location:$location\n"; + $this->locationsArgs[$location] = $location; + if(defined("DEBUG") && DEBUG>=2) echo "YellowCommandline::analyseStaticPage detected location:$location\n"; } } else { $location = rtrim($location, "0..9"); - if(is_null($this->locationsPagination[$location])) + if(is_null($this->locationsArgsPagination[$location])) { - $this->locationsPagination[$location] = $location; - if(defined("DEBUG") && DEBUG>=2) echo "YellowCommandline::analyseStaticContent type:pagination location:$location\n"; + $this->locationsArgsPagination[$location] = $location; + if(defined("DEBUG") && DEBUG>=2) echo "YellowCommandline::analyseStaticPage detected location:$location\n"; } } } @@ -297,9 +272,9 @@ class YellowCommandline function cleanStaticDirectory($path) { $statusCode = 200; - if($this->isYellowDirectory($path)) + if(is_dir($path)) { - if(is_file("$path/yellow.php") || $path=="/" || !$this->yellow->toolbox->deleteDirectory($path, true)) + if(!$this->checkStaticDirectory($path) || !$this->yellow->toolbox->deleteDirectory($path, true)) { $statusCode = 500; echo "ERROR cleaning pages: Can't delete directory '$path'!\n"; @@ -312,18 +287,13 @@ class YellowCommandline function cleanStaticFile($path, $location) { $statusCode = 200; - $fileName = $this->yellow->toolbox->findStaticFileFromLocation($location, $path, - $this->yellow->config->get("staticDefaultFile")); - if($this->isYellowDirectory($path) && is_file($fileName)) + $fileName = $this->getStaticFile($path, $location, $statusCode); + if(is_file($fileName)) { - $entry = basename($fileName); - if($entry!="yellow.php" && substru($entry, 0, 1)!=".") + if(!$this->checkStaticDirectory($path) || !$this->yellow->toolbox->deleteFile($fileName)) { - if(!$this->yellow->toolbox->deleteFile($fileName)) - { - $statusCode = 500; - echo "ERROR cleaning pages: Can't delete file '$fileName'!\n"; - } + $statusCode = 500; + echo "ERROR cleaning pages: Can't delete file '$fileName'!\n"; } } return $statusCode; @@ -352,7 +322,20 @@ class YellowCommandline $serverName = $this->yellow->config->get("serverName"); $serverBase = $this->yellow->config->get("serverBase"); return !empty($serverScheme) && !empty($serverName) && - $this->yellow->toolbox->isValidLocation($serverBase) && $serverBase!="/"; + $this->yellow->lookup->isValidLocation($serverBase) && $serverBase!="/"; + } + + // Check static directory + function checkStaticDirectory($path) + { + $ok = false; + if(!empty($path)) + { + if($path == rtrim($this->yellow->config->get("staticDir"), '/')) $ok = true; + if(is_file("$path/".$this->yellow->config->get("commandlineSystemFile"))) $ok = true; + if(is_file("$path/yellow.php")) $ok = false; + } + return $ok; } // Return static locations from file system @@ -409,6 +392,29 @@ class YellowCommandline return $files; } + // Return static file + function getStaticFile($path, $location, $statusCode) + { + if($statusCode < 400) + { + $fileName = $path.$location; + if(!$this->yellow->lookup->isFileLocation($location)) $fileName .= $this->yellow->config->get("staticDefaultFile"); + } else if($statusCode == 404) { + $fileName = $path."/".$this->yellow->config->get("staticErrorFile"); + } + return $fileName; + } + + // Return static redirect + function getStaticRedirect($location) + { + $output = "<!DOCTYPE html><html>\n<head>\n"; + $output .= "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" />\n"; + $output .= "<meta http-equiv=\"refresh\" content=\"0;url=".htmlspecialchars($location)."\" />\n"; + $output .= "</head>\n</html>"; + return $output; + } + // Return command help function getCommandHelp() { @@ -436,12 +442,6 @@ class YellowCommandline usort($data, strnatcasecmp); return $data; } - - // Check if directory contains Yellow files - function isYellowDirectory($path) - { - return is_file("$path/yellow.php") || is_file("$path/".$this->yellow->config->get("commandlineSystemFile")); - } } $yellow->plugins->register("commandline", "YellowCommandline", YellowCommandline::Version); diff --git a/system/core/core-webinterface.css b/system/core/core-webinterface.css @@ -1,4 +1,4 @@ -/* Yellow web interface 0.5.1 */ +/* Yellow web interface 0.5.3 */ .yellow-bar { position:relative; overflow:hidden; line-height:2em; margin-bottom:10px; } .yellow-bar-left { display:block; float:left; } diff --git a/system/core/core-webinterface.js b/system/core/core-webinterface.js @@ -4,7 +4,7 @@ // Yellow main API var yellow = { - version: "0.5.1", + version: "0.5.3", action: function(text) { yellow.webinterface.action(text); }, onClick: function(e) { yellow.webinterface.hidePanesOnClick(yellow.toolbox.getEventElement(e)); }, onKeydown: function(e) { yellow.webinterface.hidePanesOnKeydown(yellow.toolbox.getEventKeycode(e)); }, diff --git a/system/core/core-webinterface.php b/system/core/core-webinterface.php @@ -5,7 +5,7 @@ // Web interface core plugin class YellowWebinterface { - const Version = "0.5.1"; + const Version = "0.5.3"; var $yellow; //access to API var $active; //web interface is active? (boolean) var $userLoginFailed; //web interface login failed? (boolean) @@ -15,7 +15,7 @@ class YellowWebinterface var $rawDataSource; //raw data of page for comparison var $rawDataEdit; //raw data of page for editing - // Handle plugin initialisation + // Handle initialisation function onLoad($yellow) { $this->yellow = $yellow; @@ -27,7 +27,8 @@ class YellowWebinterface $this->yellow->config->setDefault("webinterfaceUserHashAlgorithm", "bcrypt"); $this->yellow->config->setDefault("webinterfaceUserHashCost", "10"); $this->yellow->config->setDefault("webinterfaceUserFile", "user.ini"); - $this->yellow->config->setDefault("webinterfaceFilePrefix", "published"); + $this->yellow->config->setDefault("webinterfaceNewFile", "page-new-(.*).txt"); + $this->yellow->config->setDefault("webinterfaceMetaFilePrefix", "published"); $this->users->load($this->yellow->config->get("configDir").$this->yellow->config->get("webinterfaceUserFile")); } @@ -44,17 +45,17 @@ class YellowWebinterface if(rtrim($location, '/') == rtrim($activeLocation, '/')) { $statusCode = 301; - $locationHeader = $this->yellow->toolbox->getLocationHeader( + $location = $this->yellow->lookup->normaliseUrl( $this->yellow->config->get("webinterfaceServerScheme"), $this->yellow->config->get("webinterfaceServerName"), $base, $activeLocation); - $this->yellow->sendStatus($statusCode, $locationHeader, false); + $this->yellow->sendStatus($statusCode, $location); } } return $statusCode; } // Handle page meta data parsing - function onParseMeta($page, $text) + function onParseMeta($page) { if($this->isActive() && $this->isUser()) { @@ -71,41 +72,35 @@ class YellowWebinterface } } - // Handle page extra header - function onHeaderExtra($page) + // Handle page extra HTML data + function onExtra() { - $header = ""; + $output = ""; if($this->isActive()) { $location = $this->yellow->config->getHtml("serverBase").$this->yellow->config->getHtml("pluginLocation"); - $header .= "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"{$location}core-webinterface.css\" />\n"; - $header .= "<script type=\"text/javascript\" src=\"{$location}core-webinterface.js\"></script>\n"; - $header .= "<script type=\"text/javascript\">\n"; - $header .= "// <![CDATA[\n"; + $output .= "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"{$location}core-webinterface.css\" />\n"; + $output .= "<script type=\"text/javascript\" src=\"{$location}core-webinterface.js\"></script>\n"; + $output .= "<script type=\"text/javascript\">\n"; + $output .= "// <![CDATA[\n"; if($this->isUser()) { - $header .= "yellow.page.userPermission = ".json_encode($this->userPermission).";\n"; - $header .= "yellow.page.rawDataSource = ".json_encode($this->rawDataSource).";\n"; - $header .= "yellow.page.rawDataEdit = ".json_encode($this->rawDataEdit).";\n"; - $header .= "yellow.page.rawDataNew = ".json_encode($this->getDataNew()).";\n"; - $header .= "yellow.page.parserSafeMode = ".json_encode($page->parserSafeMode).";\n"; - $header .= "yellow.page.statusCode = ".json_encode($page->statusCode).";\n"; + $output .= "yellow.page.userPermission = ".json_encode($this->userPermission).";\n"; + $output .= "yellow.page.rawDataSource = ".json_encode($this->rawDataSource).";\n"; + $output .= "yellow.page.rawDataEdit = ".json_encode($this->rawDataEdit).";\n"; + $output .= "yellow.page.rawDataNew = ".json_encode($this->getDataNew()).";\n"; + $output .= "yellow.page.parserSafeMode = ".json_encode($this->yellow->page->parserSafeMode).";\n"; + $output .= "yellow.page.statusCode = ".json_encode($this->yellow->page->statusCode).";\n"; } - $header .= "yellow.config = ".json_encode($this->getDataConfig()).";\n"; - $language = $this->isUser() ? $this->users->getLanguage() : $page->get("language"); + $output .= "yellow.config = ".json_encode($this->getDataConfig()).";\n"; + $language = $this->isUser() ? $this->users->getLanguage() : $this->yellow->page->get("language"); if(!$this->yellow->text->isLanguage($language)) $language = $this->yellow->config->get("language"); - $header .= "yellow.text = ".json_encode($this->yellow->text->getData("webinterface", $language)).";\n"; - if(defined("DEBUG")) $header .= "yellow.debug = ".json_encode(DEBUG).";\n"; - $header .= "// ]]>\n"; - $header .= "</script>\n"; + $output .= "yellow.text = ".json_encode($this->yellow->text->getData("webinterface", $language)).";\n"; + if(defined("DEBUG")) $output .= "yellow.debug = ".json_encode(DEBUG).";\n"; + $output .= "// ]]>\n"; + $output .= "</script>\n"; } - return $header; - } - - // Handle command help - function onCommandHelp() - { - return "user EMAIL PASSWORD [NAME LANGUAGE HOME]\n"; + return $output; } // Handle command @@ -120,6 +115,12 @@ class YellowWebinterface return $statusCode; } + // Handle command help + function onCommandHelp() + { + return "user EMAIL PASSWORD [NAME LANGUAGE HOME]\n"; + } + // Create or update user account function userCommand($args) { @@ -180,11 +181,12 @@ class YellowWebinterface { $statusCode = $this->yellow->processRequest($serverScheme, $serverName, $base, $location, $fileName, false); } else { - if($this->yellow->toolbox->isFileLocation($location) && $this->yellow->isContentDirectory("$location/")) + if($this->yellow->isRequestContentDirectory($location)) { $statusCode = 301; - $locationHeader = $this->yellow->toolbox->getLocationHeader($serverScheme, $serverName, $base, "$location/"); - $this->yellow->sendStatus($statusCode, $locationHeader, false); + $location = $this->yellow->lookup->isFileLocation($location) ? "$location/" : "/".$this->yellow->getRequestLanguage()."/"; + $location = $this->yellow->lookup->normaliseUrl($serverScheme, $serverName, $base, $location); + $this->yellow->sendStatus($statusCode, $location); } else { $statusCode = $this->userPermission ? 424 : 404; $this->yellow->processRequest($serverScheme, $serverName, $base, $location, $fileName, false); @@ -207,8 +209,8 @@ class YellowWebinterface if($this->yellow->toolbox->createFile($page->fileName, $page->rawData)) { $statusCode = 303; - $locationHeader = $this->yellow->toolbox->getLocationHeader($serverScheme, $serverName, $base, $page->location); - $this->yellow->sendStatus($statusCode, $locationHeader, false); + $location = $this->yellow->lookup->normaliseUrl($serverScheme, $serverName, $base, $page->location); + $this->yellow->sendStatus($statusCode, $location); } else { $statusCode = 500; $this->yellow->processRequest($serverScheme, $serverName, $base, $location, $fileName, false); @@ -239,8 +241,8 @@ class YellowWebinterface $this->yellow->toolbox->createFile($page->fileName, $page->rawData)) { $statusCode = 303; - $locationHeader = $this->yellow->toolbox->getLocationHeader($serverScheme, $serverName, $base, $page->location); - $this->yellow->sendStatus($statusCode, $locationHeader, false); + $location = $this->yellow->lookup->normaliseUrl($serverScheme, $serverName, $base, $page->location); + $this->yellow->sendStatus($statusCode, $location); } else { $statusCode = 500; $this->yellow->processRequest($serverScheme, $serverName, $base, $location, $fileName, false); @@ -265,8 +267,8 @@ class YellowWebinterface if(!is_file($fileName) || $this->yellow->toolbox->deleteFile($fileName)) { $statusCode = 303; - $locationHeader = $this->yellow->toolbox->getLocationHeader($serverScheme, $serverName, $base, $location); - $this->yellow->sendStatus($statusCode, $locationHeader, false); + $location = $this->yellow->lookup->normaliseUrl($serverScheme, $serverName, $base, $location); + $this->yellow->sendStatus($statusCode, $location); } else { $statusCode = 500; $this->yellow->processRequest($serverScheme, $serverName, $base, $location, $fileName, false); @@ -284,12 +286,12 @@ class YellowWebinterface if(substru($location, 0, strlenu($home)) == $home) { $statusCode = 303; - $locationHeader = $this->yellow->toolbox->getLocationHeader($serverScheme, $serverName, $base, $location); - $this->yellow->sendStatus($statusCode, $locationHeader, false); + $location = $this->yellow->lookup->normaliseUrl($serverScheme, $serverName, $base, $location); + $this->yellow->sendStatus($statusCode, $location); } else { $statusCode = 302; - $locationHeader = $this->yellow->toolbox->getLocationHeader($serverScheme, $serverName, $base, $home); - $this->yellow->sendStatus($statusCode, $locationHeader, false); + $location = $this->yellow->lookup->normaliseUrl($serverScheme, $serverName, $base, $home); + $this->yellow->sendStatus($statusCode, $location); } return $statusCode; } @@ -300,11 +302,11 @@ class YellowWebinterface $statusCode = 302; $this->users->destroyCookie("login"); $this->users->email = ""; - $locationHeader = $this->yellow->toolbox->getLocationHeader( + $location = $this->yellow->lookup->normaliseUrl( $this->yellow->config->get("serverScheme"), $this->yellow->config->get("serverName"), $this->yellow->config->get("serverBase"), $location); - $this->yellow->sendStatus($statusCode, $locationHeader, false); + $this->yellow->sendStatus($statusCode, $location); return $statusCode; } @@ -390,14 +392,11 @@ class YellowWebinterface { $page = new YellowPage($this->yellow); $page->setRequestInformation($serverScheme, $serverName, $base, $location, $fileName); - $page->parseData($rawData, false); - $page->fileName = $this->yellow->toolbox->findFileFromTitle( - $page->get($this->yellow->config->get("webinterfaceFilePrefix")), $page->get("title"), $fileName, - $this->yellow->config->get("contentDefaultFile"), $this->yellow->config->get("contentExtension")); - $page->location = $this->yellow->toolbox->findLocationFromFile( - $page->fileName, $this->yellow->config->get("contentDir"), - $this->yellow->config->get("contentRootDir"), $this->yellow->config->get("contentHomeDir"), + $page->parseData($rawData, false, 0); + $page->fileName = $this->yellow->lookup->findFileFromTitle( + $page->get($this->yellow->config->get("webinterfaceMetaFilePrefix")), $page->get("title"), $fileName, $this->yellow->config->get("contentDefaultFile"), $this->yellow->config->get("contentExtension")); + $page->location = $this->yellow->lookup->findLocationFromFile($page->fileName); if($this->yellow->pages->find($page->location)) { preg_match("/^(.*?)(\d*)$/", $page->get("title"), $matches); @@ -407,13 +406,10 @@ class YellowWebinterface for(; $titleNumber<=999; ++$titleNumber) { $page->rawData = $this->updateDataTitle($rawData, $titleText.$titleNumber); - $page->fileName = $this->yellow->toolbox->findFileFromTitle( - $page->get($this->yellow->config->get("webinterfaceFilePrefix")), $titleText.$titleNumber, $fileName, - $this->yellow->config->get("contentDefaultFile"), $this->yellow->config->get("contentExtension")); - $page->location = $this->yellow->toolbox->findLocationFromFile( - $page->fileName, $this->yellow->config->get("contentDir"), - $this->yellow->config->get("contentRootDir"), $this->yellow->config->get("contentHomeDir"), + $page->fileName = $this->yellow->lookup->findFileFromTitle( + $page->get($this->yellow->config->get("webinterfaceMetaFilePrefix")), $titleText.$titleNumber, $fileName, $this->yellow->config->get("contentDefaultFile"), $this->yellow->config->get("contentExtension")); + $page->location = $this->yellow->lookup->findLocationFromFile($page->fileName); if(!$this->yellow->pages->find($page->location)) { $ok = true; break; } } if(!$ok) $page->error(500, "Page '".$page->get("title")."' can not be created!"); @@ -427,23 +423,20 @@ class YellowWebinterface { $page = new YellowPage($this->yellow); $page->setRequestInformation($serverScheme, $serverName, $base, $location, $fileName); - $page->parseData($this->merge->merge($rawDataSource, $rawDataEdit, $rawDataFile), false); + $page->parseData($this->merge->merge($rawDataSource, $rawDataEdit, $rawDataFile), false, 0); if(empty($page->rawData)) $page->error(500, "Page has been modified by someone else!"); - if($this->yellow->toolbox->isFileLocation($location) && !$page->isError()) + if($this->yellow->lookup->isFileLocation($location) && !$page->isError()) { $pageSource = new YellowPage($this->yellow); $pageSource->setRequestInformation($serverScheme, $serverName, $base, $location, $fileName); - $pageSource->parseData($rawDataSource, false); - $prefix = $this->yellow->config->get("webinterfaceFilePrefix"); + $pageSource->parseData($rawDataSource, false, 0); + $prefix = $this->yellow->config->get("webinterfaceMetaFilePrefix"); if($pageSource->get($prefix)!=$page->get($prefix) || $pageSource->get("title")!=$page->get("title")) { - $page->fileName = $this->yellow->toolbox->findFileFromTitle( + $page->fileName = $this->yellow->lookup->findFileFromTitle( $page->get($prefix), $page->get("title"), $fileName, $this->yellow->config->get("contentDefaultFile"), $this->yellow->config->get("contentExtension")); - $page->location = $this->yellow->toolbox->findLocationFromFile( - $page->fileName, $this->yellow->config->get("contentDir"), - $this->yellow->config->get("contentRootDir"), $this->yellow->config->get("contentHomeDir"), - $this->yellow->config->get("contentDefaultFile"), $this->yellow->config->get("contentExtension")); + $page->location = $this->yellow->lookup->findLocationFromFile($page->fileName); if($pageSource->location!=$page->location && $this->yellow->pages->find($page->location)) { $page->error(500, "Page '".$page->get("title")."' already exists!"); @@ -457,12 +450,9 @@ class YellowWebinterface // Return content data for new page function getDataNew($title = "") { - $fileName = $this->yellow->toolbox->findFileFromLocation( - $this->yellow->page->location, $this->yellow->config->get("contentDir"), - $this->yellow->config->get("contentRootDir"), $this->yellow->config->get("contentHomeDir"), - $this->yellow->config->get("contentDefaultFile"), $this->yellow->config->get("contentExtension")); - $fileName = $this->yellow->toolbox->findFileNew($fileName, - $this->yellow->config->get("configDir"), $this->yellow->config->get("newPageFile"), + $fileName = $this->yellow->lookup->findFileFromLocation($this->yellow->page->location); + $fileName = $this->yellow->lookup->findFileNew($fileName, + $this->yellow->config->get("configDir"), $this->yellow->config->get("webinterfaceNewFile"), $this->yellow->config->get("contentDefaultFile")); $fileData = $this->yellow->toolbox->getFileData($fileName); $fileData = preg_replace("/@datetime/i", date("Y-m-d H:i:s"), $fileData); diff --git a/system/core/core.php b/system/core/core.php @@ -5,27 +5,30 @@ // Yellow main class class Yellow { - const Version = "0.5.2"; + const Version = "0.5.3"; var $page; //current page var $pages; //pages from file system + var $files; //files from file system + var $plugins; //plugins var $config; //configuration var $text; //text strings - var $plugins; //plugins + var $lookup; //location and file lookup var $toolbox; //toolbox with helpers function __construct() { $this->page = new YellowPage($this); $this->pages = new YellowPages($this); + $this->files = new YellowFiles($this); + $this->plugins = new YellowPlugins($this); $this->config = new YellowConfig($this); $this->text = new YellowText($this); + $this->lookup = new YellowLookup($this); $this->toolbox = new YellowToolbox(); - $this->plugins = new YellowPlugins(); $this->config->setDefault("sitename", "Yellow"); $this->config->setDefault("author", "Yellow"); $this->config->setDefault("language", "en"); $this->config->setDefault("theme", "default"); - $this->config->setDefault("template", "default"); $this->config->setDefault("timeZone", $this->toolbox->getTimeZone()); $this->config->setDefault("serverScheme", $this->toolbox->getServerScheme()); $this->config->setDefault("serverName", $this->toolbox->getServerName()); @@ -35,16 +38,16 @@ class Yellow $this->config->setDefault("themeLocation", "/media/themes/"); $this->config->setDefault("systemDir", "system/"); $this->config->setDefault("configDir", "system/config/"); - $this->config->setDefault("staticDir", "cache/"); - $this->config->setDefault("staticDefaultFile", "index.html"); - $this->config->setDefault("staticErrorFile", "error404.html"); $this->config->setDefault("coreDir", "system/core/"); $this->config->setDefault("pluginDir", "system/plugins/"); - $this->config->setDefault("snippetDir", "system/snippets/"); - $this->config->setDefault("templateDir", "system/templates/"); $this->config->setDefault("themeDir", "system/themes/"); + $this->config->setDefault("snippetDir", "system/themes/snippets/"); + $this->config->setDefault("templateDir", "system/themes/templates/"); $this->config->setDefault("mediaDir", "media/"); $this->config->setDefault("imageDir", "media/images/"); + $this->config->setDefault("staticDir", "cache/"); + $this->config->setDefault("staticDefaultFile", "index.html"); + $this->config->setDefault("staticErrorFile", "error.html"); $this->config->setDefault("contentDir", "content/"); $this->config->setDefault("contentRootDir", "default/"); $this->config->setDefault("contentHomeDir", "home/"); @@ -53,24 +56,24 @@ class Yellow $this->config->setDefault("contentExtension", ".txt"); $this->config->setDefault("configExtension", ".ini"); $this->config->setDefault("configFile", "config.ini"); - $this->config->setDefault("errorPageFile", "error(.*).txt"); - $this->config->setDefault("newPageFile", "new(.*).txt"); - $this->config->setDefault("textStringFile", "text(.*).ini"); + $this->config->setDefault("textFile", "language-(.*).ini"); + $this->config->setDefault("errorFile", "page-error-(.*).txt"); $this->config->setDefault("robotsTextFile", "robots.txt"); - $this->config->setDefault("parser", "markdownextra"); + $this->config->setDefault("template", "default"); + $this->config->setDefault("parser", "markdown"); $this->config->setDefault("parserSafeMode", "0"); $this->config->setDefault("multiLanguageMode", "0"); $this->config->load($this->config->get("configDir").$this->config->get("configFile")); - $this->text->load($this->config->get("configDir").$this->config->get("textStringFile")); + $this->text->load($this->config->get("configDir").$this->config->get("textFile")); $this->updateConfig(); } // Handle request - function request($statusCodeRequest = 0) + function request() { - $this->toolbox->timerStart($time); ob_start(); $statusCode = 0; + $this->toolbox->timerStart($time); list($serverScheme, $serverName, $base, $location, $fileName) = $this->getRequestInformation(); $this->page->setRequestInformation($serverScheme, $serverName, $base, $location, $fileName); foreach($this->plugins->plugins as $key=>$value) @@ -87,10 +90,9 @@ class Yellow $this->pages->requestHandler = "core"; $statusCode = $this->processRequest($serverScheme, $serverName, $base, $location, $fileName, true); } - if($statusCodeRequest >= 400) $this->page->error($statusCodeRequest, "Request error"); if($this->page->isError()) $statusCode = $this->processRequestError(); - ob_end_flush(); $this->toolbox->timerStop($time); + ob_end_flush(); if(defined("DEBUG") && DEBUG>=1) echo "Yellow::request status:$statusCode location:$location<br/>\n"; if(defined("DEBUG") && DEBUG>=1) echo "Yellow::request time:$time ms<br/>\n"; return $statusCode; @@ -99,37 +101,33 @@ class Yellow // Process request function processRequest($serverScheme, $serverName, $base, $location, $fileName, $cacheable) { + $statusCode = 0; if(is_readable($fileName)) { - if($cacheable) $fileName = $this->getStaticFile($location, $fileName); if($this->toolbox->isRequestCleanUrl($location)) { $statusCode = 303; $locationArgs = $this->toolbox->getLocationArgsCleanUrl($this->config->get("contentPagination")); - $locationHeader = $this->toolbox->getLocationHeader($serverScheme, $serverName, $base, $location.$locationArgs); - $this->sendStatus($statusCode, $locationHeader, false); - } else if($this->isStaticFile($fileName)) { - $statusCode = 200; - $statusCode = $this->sendFile($statusCode, $fileName, $cacheable); - } else { - $statusCode = 200; - $fileName = $this->readPage($serverScheme, $serverName, $base, $location, $fileName, $cacheable, $statusCode); - $statusCode = $this->sendPage(); + $location = $this->lookup->normaliseUrl($serverScheme, $serverName, $base, $location.$locationArgs); + $this->sendStatus($statusCode, $location); } } else { - if($cacheable) $fileName = $this->getStaticFile("", ""); - if(($this->toolbox->isFileLocation($location) && $this->isContentDirectory("$location/")) || - ($location=="/" && $this->config->get("multiLanguageMode"))) + if($this->isRequestContentDirectory($location)) { $statusCode = 301; - $location = $this->toolbox->isFileLocation($location) ? "$location/" : "/".$this->getRequestLanguage()."/"; - $locationHeader = $this->toolbox->getLocationHeader($serverScheme, $serverName, $base, $location); - $this->sendStatus($statusCode, $locationHeader, false); - } else if($this->isStaticFile($fileName)) { - $statusCode = 404; + $location = $this->lookup->isFileLocation($location) ? "$location/" : "/".$this->getRequestLanguage()."/"; + $location = $this->lookup->normaliseUrl($serverScheme, $serverName, $base, $location); + $this->sendStatus($statusCode, $location); + } + } + if($statusCode == 0) + { + $statusCode = is_readable($fileName) ? 200 : 404; + $fileName = $this->getStaticFileFromCache($location, $fileName, $cacheable, $statusCode); + if($this->isStaticFile($fileName)) + { $statusCode = $this->sendFile($statusCode, $fileName, $cacheable); } else { - $statusCode = 404; $fileName = $this->readPage($serverScheme, $serverName, $base, $location, $fileName, $cacheable, $statusCode); $statusCode = $this->sendPage(); } @@ -137,7 +135,7 @@ class Yellow if(defined("DEBUG") && DEBUG>=1) { $handler = $this->getRequestHandler(); - echo "Yellow::processRequest handler:$handler file:$fileName<br/>\n"; + echo "Yellow::processRequest file:$fileName handler:$handler<br/>\n"; } return $statusCode; } @@ -153,17 +151,17 @@ class Yellow if(defined("DEBUG") && DEBUG>=1) { $handler = $this->getRequestHandler(); - echo "Yellow::processRequestError handler:$handler file:$fileName<br/>\n"; + echo "Yellow::processRequestError file:$fileName handler:$handler<br/>\n"; } return $statusCode; } - // Read page from file + // Read page function readPage($serverScheme, $serverName, $base, $location, $fileName, $cacheable, $statusCode, $pageError = "") { if($statusCode >= 400) { - $fileName = $this->config->get("configDir").$this->config->get("errorPageFile"); + $fileName = $this->config->get("configDir").$this->config->get("errorFile"); $fileName = strreplaceu("(.*)", $statusCode, $fileName); $cacheable = false; } @@ -178,29 +176,25 @@ class Yellow // Send page response function sendPage() { - $this->page->parseResponse(); + $this->page->parsePage(); $statusCode = $this->page->statusCode; $lastModifiedFormatted = $this->page->getHeader("Last-Modified"); - if($statusCode==200 && $this->page->isCacheable() && $this->toolbox->isFileNotModified($lastModifiedFormatted)) + if($statusCode==200 && $this->page->isCacheable() && $this->toolbox->isRequestNotModified($lastModifiedFormatted)) { $statusCode = 304; - $this->page->clean($statusCode); - } - if($this->page->isExisting("pageClean")) ob_clean(); - if(PHP_SAPI != "cli") - { @header($this->toolbox->getHttpStatusFormatted($statusCode)); - foreach($this->page->headerData as $key=>$value) @header("$key: $value"); } else { - if($statusCode>=301 && $statusCode<=303) $this->sendStaticRedirect(); + @header($this->toolbox->getHttpStatusFormatted($statusCode)); + foreach($this->page->headerData as $key=>$value) @header("$key: $value"); + if(!is_null($this->page->outputData)) echo $this->page->outputData; } if(defined("DEBUG") && DEBUG>=1) { foreach($this->page->headerData as $key=>$value) echo "Yellow::sendPage $key: $value<br/>\n"; - $fileNameTemplate = $this->config->get("templateDir").$this->page->get("template").".php"; $fileNameTheme = $this->config->get("themeDir").$this->page->get("theme").".css"; + $templateName = $this->page->get("template"); $parserName = $this->page->get("parser"); - echo "Yellow::sendPage template:$fileNameTemplate theme:$fileNameTheme parser:$parserName<br/>\n"; + echo "Yellow::sendPage theme:$fileNameTheme template:$templateName parser:$parserName<br/>\n"; } return $statusCode; } @@ -209,7 +203,7 @@ class Yellow function sendFile($statusCode, $fileName, $cacheable) { $lastModifiedFormatted = $this->toolbox->getHttpDateFormatted(filemtime($fileName)); - if($statusCode==200 && $cacheable && $this->toolbox->isFileNotModified($lastModifiedFormatted)) + if($statusCode==200 && $cacheable && $this->toolbox->isRequestNotModified($lastModifiedFormatted)) { $statusCode = 304; @header($this->toolbox->getHttpStatusFormatted($statusCode)); @@ -224,29 +218,16 @@ class Yellow } // Send status response - function sendStatus($statusCode, $responseHeader, $cacheable) + function sendStatus($statusCode, $location = "") { - if(PHP_SAPI != "cli") + if(!empty($location)) $this->page->clean($statusCode, $location); + @header($this->toolbox->getHttpStatusFormatted($statusCode)); + foreach($this->page->headerData as $key=>$value) @header("$key: $value"); + if(defined("DEBUG") && DEBUG>=1) { - @header($this->toolbox->getHttpStatusFormatted($statusCode)); - if(!$cacheable) @header("Cache-Control: no-cache, must-revalidate"); - if(!empty($responseHeader)) @header($responseHeader); - } else { - if(!empty($responseHeader)) $this->page->header($responseHeader); - if($statusCode>=301 && $statusCode<=303) $this->sendStaticRedirect(); + foreach($this->page->headerData as $key=>$value) echo "Yellow::sendStatus $key: $value<br/>\n"; } } - - // Send static redirect response - function sendStaticRedirect() - { - $lastModifiedFormatted = $this->toolbox->getHttpDateFormatted(time()); - if(!$this->page->isHeader("Last-Modified")) $this->page->setHeader("Last-Modified", $lastModifiedFormatted); - echo "<!DOCTYPE html><html>\n<head>\n"; - echo "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" />\n"; - echo "<meta http-equiv=\"refresh\" content=\"0;url=".htmlspecialchars($this->page->getHeader("Location"))."\" />\n"; - echo "</head>\n</html>"; - } // Return request information function getRequestInformation($serverScheme = "", $serverName = "", $base = "") @@ -271,20 +252,14 @@ class Yellow $fileName = $this->config->get("configDir").$this->config->get("robotsTextFile"); } } - if(empty($fileName)) - { - $fileName = $this->toolbox->findFileFromLocation($location, $this->config->get("contentDir"), - $this->config->get("contentRootDir"), $this->config->get("contentHomeDir"), - $this->config->get("contentDefaultFile"), $this->config->get("contentExtension")); - } + if(empty($fileName)) $fileName = $this->lookup->findFileFromLocation($location); return array($serverScheme, $serverName, $base, $location, $fileName); } // Return request language function getRequestLanguage() { - $languages = $this->toolbox->findRootLanguages($this->config->get("contentDir"), $this->config->get("contentRootDir")); - return $this->toolbox->detectBrowserLanguage($languages, $this->config->get("language")); + return $this->toolbox->detectBrowserLanguage($this->pages->getLanguages(), $this->config->get("language")); } // Return request handler @@ -293,16 +268,17 @@ class Yellow return $this->pages->requestHandler; } - // Return static file - function getStaticFile($location, $fileName) + // Return static file from cache if available + function getStaticFileFromCache($location, $fileName, $cacheable, $statusCode) { - if(PHP_SAPI != "cli") + if(PHP_SAPI != "cli" && $cacheable) { - if(!empty($location)) + if($statusCode == 200) { - $fileNameStatic = $this->toolbox->findStaticFileFromLocation($location, - $this->config->get("staticDir"), $this->config->get("staticDefaultFile")); - } else { + $location .= $this->toolbox->getLocationArgs(); + $fileNameStatic = rtrim($this->config->get("staticDir"), '/').$location; + if(!$this->lookup->isFileLocation($location)) $fileNameStatic .= $this->config->get("staticDefaultFile"); + } else if($statusCode == 404) { $fileNameStatic = $this->config->get("staticDir").$this->config->get("staticErrorFile"); } if(is_readable($fileNameStatic)) $fileName = $fileNameStatic; @@ -316,26 +292,30 @@ class Yellow $staticDirLength = strlenu($this->config->get("staticDir")); $systemDirLength = strlenu($this->config->get("systemDir")); return substru($fileName, 0, $staticDirLength) == $this->config->get("staticDir") || - substru($fileName, 0, $systemDirLength) == $this->config->get("systemDir"); + substru($fileName, 0, $systemDirLength) == $this->config->get("systemDir"); } - // Check if content directory - function isContentDirectory($location) + // Check if request can be redirected into content directory + function isRequestContentDirectory($location) { - $path = $this->toolbox->findFileFromLocation($location, $this->config->get("contentDir"), - $this->config->get("contentRootDir"), $this->config->get("contentHomeDir"), "", ""); - return is_dir($path); + $ok = false; + if($this->lookup->isFileLocation($location)) + { + $path = $this->lookup->findFileFromLocation("$location/", true); + $ok = is_dir($path); + } else if($location=="/") { + $ok = $this->config->get("multiLanguageMode"); + } + return $ok; } // Update configuration function updateConfig() { - date_default_timezone_set($this->config->get("timeZone")); - list($pathRoot, $pathHome) = $this->toolbox->findRootConfig($this->config->get("contentDir"), - $this->config->get("contentRootDir"), $this->config->get("contentHomeDir"), - $this->config->get("multiLanguageMode")); + list($pathRoot, $pathHome) = $this->lookup->getContentInformation(); $this->config->set("contentRootDir", $pathRoot); $this->config->set("contentHomeDir", $pathHome); + date_default_timezone_set($this->config->get("timeZone")); } // Execute command @@ -356,7 +336,7 @@ class Yellow // Execute template function template($name) { - $fileNameTemplate = $this->config->get("templateDir")."$name.php"; + $fileNameTemplate = $this->config->get("templateDir")."$name.html"; if(is_file($fileNameTemplate)) { $this->page->setLastModified(filemtime($fileNameTemplate)); @@ -364,6 +344,7 @@ class Yellow require($fileNameTemplate); } else { $this->page->error(500, "Template '$name' does not exist!"); + echo "Template error<br/>\n"; } } @@ -379,6 +360,7 @@ class Yellow require($fileNameSnippet); } else { $this->page->error(500, "Snippet '$name' does not exist!"); + echo "Snippet error<br/>\n"; } } @@ -403,11 +385,14 @@ class YellowPage var $metaDataOffsetBytes; //meta data offset var $metaData; //meta data var $headerData; //response header - var $parserData; //content data of page + var $outputData; //response output + var $pages; //page collection var $parser; //content parser + var $parserData; //content data of page var $parserSafeMode; //page is parsed in safe mode? (boolean) - var $active; //page is active location? (boolean) + var $available; //page is available? (boolean) var $visible; //page is visible location? (boolean) + var $active; //page is active location? (boolean) var $cacheable; //page is cacheable? (boolean) var $statusCode; //status code @@ -416,6 +401,7 @@ class YellowPage $this->yellow = $yellow; $this->metaData = array(); $this->headerData = array(); + $this->pages = new YellowPageCollection($yellow); } // Set request information @@ -429,17 +415,16 @@ class YellowPage } // Parse page data - function parseData($rawData, $cacheable, $statusCode = 0, $pageError = "") + function parseData($rawData, $cacheable, $statusCode, $pageError = "") { $this->lastModified = 0; $this->rawData = $rawData; - $this->parserData = ""; $this->parser = NULL; + $this->parserData = ""; $this->parserSafeMode = $this->yellow->config->get("parserSafeMode"); - $this->active = $this->yellow->toolbox->isActiveLocation($this->location, $this->yellow->page->location, - $this->yellow->pages->getHomeLocation($this->yellow->page->location)); - $this->visible = $this->yellow->toolbox->isVisibleLocation($this->location, $this->fileName, - $this->yellow->config->get("contentDir")); + $this->available = true; + $this->visible = $this->yellow->lookup->isVisibleLocation($this->location, $this->fileName); + $this->active = $this->yellow->lookup->isActiveLocation($this->location, $this->yellow->page->location); $this->cacheable = $cacheable; $this->statusCode = $statusCode; $this->parseMeta($pageError); @@ -465,48 +450,59 @@ class YellowPage function parseMeta($pageError = "") { $this->metaData = array(); - $this->set("title", $this->yellow->toolbox->createTextTitle($this->location)); - $this->set("sitename", $this->yellow->config->get("sitename")); - $this->set("author", $this->yellow->config->get("author")); - $this->set("language", $this->yellow->toolbox->findLanguageFromFile($this->fileName, - $this->yellow->config->get("contentDir"), $this->yellow->config->get("contentRootDir"), - $this->yellow->config->get("language"))); - $this->set("theme", $this->yellow->toolbox->findNameFromFile($this->fileName, - $this->yellow->config->get("themeDir"), $this->yellow->config->get("theme"), ".css")); - $this->set("template", $this->yellow->toolbox->findNameFromFile($this->fileName, - $this->yellow->config->get("templateDir"), $this->yellow->config->get("template"), ".php")); - $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->findModifiedFromFile($this->fileName))); - $this->set("parser", $this->yellow->config->get("parser")); - - if(preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)[\r\n]+\-\-\-[\r\n]+/s", $this->rawData, $parsed)) - { - $this->metaDataOffsetBytes = strlenb($parsed[0]); - foreach(preg_split("/[\r\n]+/", $parsed[2]) as $line) + if(!is_null($this->rawData)) + { + $this->set("title", $this->yellow->toolbox->createTextTitle($this->location)); + $this->set("sitename", $this->yellow->config->get("sitename")); + $this->set("author", $this->yellow->config->get("author")); + $this->set("language", $this->yellow->lookup->findLanguageFromFile($this->fileName, + $this->yellow->config->get("language"))); + $this->set("theme", $this->yellow->lookup->findNameFromFile($this->fileName, + $this->yellow->config->get("themeDir"), $this->yellow->config->get("theme"), ".css")); + $this->set("template", $this->yellow->lookup->findNameFromFile($this->fileName, + $this->yellow->config->get("templateDir"), $this->yellow->config->get("template"), ".html")); + $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName))); + $this->set("parser", $this->yellow->config->get("parser")); + + if(preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)[\r\n]+\-\-\-[\r\n]+/s", $this->rawData, $parts)) { - preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); - if(!empty($matches[1]) && !strempty($matches[2])) $this->set(lcfirst($matches[1]), $matches[2]); + $this->metaDataOffsetBytes = strlenb($parts[0]); + foreach(preg_split("/[\r\n]+/", $parts[2]) as $line) + { + preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); + if(!empty($matches[1]) && !strempty($matches[2])) $this->set(lcfirst($matches[1]), $matches[2]); + } + } else if(preg_match("/^(\xEF\xBB\xBF)?([^\r\n]+)[\r\n]+=+[\r\n]+/", $this->rawData, $parts)) { + $this->metaDataOffsetBytes = strlenb($parts[0]); + $this->set("title", $parts[2]); } - } else if(preg_match("/^(\xEF\xBB\xBF)?([^\r\n]+)[\r\n]+=+[\r\n]+/", $this->rawData, $parsed)) { - $this->metaDataOffsetBytes = strlenb($parsed[0]); - $this->set("title", $parsed[2]); - } - - $titleHeader = ($this->location == $this->yellow->pages->getHomeLocation($this->location)) ? - $this->get("sitename") : $this->get("title")." - ".$this->get("sitename"); - if($this->get("titleContent") == "-") $this->set("titleContent", ""); - if(!$this->isExisting("titleContent")) $this->set("titleContent", $this->get("title")); - if(!$this->isExisting("titleHeader")) $this->set("titleHeader", $titleHeader); - if(!$this->isExisting("titleNavigation")) $this->set("titleNavigation", $this->get("title")); - $this->set("pageRead", $this->yellow->toolbox->getUrl( - $this->yellow->config->get("serverScheme"), $this->yellow->config->get("serverName"), - $this->yellow->config->get("serverBase"), $this->location)); - $this->set("pageEdit", $this->yellow->toolbox->getUrl( - $this->yellow->config->get("webinterfaceServerScheme"), $this->yellow->config->get("webinterfaceServerName"), - $this->yellow->config->get("serverBase"), rtrim($this->yellow->config->get("webinterfaceLocation"), '/').$this->location)); + + $titleHeader = ($this->location == $this->yellow->pages->getHomeLocation($this->location)) ? + $this->get("sitename") : $this->get("title")." - ".$this->get("sitename"); + if($this->get("titleContent") == "-") $this->set("titleContent", ""); + if(!$this->isExisting("titleContent")) $this->set("titleContent", $this->get("title")); + if(!$this->isExisting("titleHeader")) $this->set("titleHeader", $titleHeader); + if(!$this->isExisting("titleNavigation")) $this->set("titleNavigation", $this->get("title")); + $this->set("pageRead", $this->yellow->lookup->normaliseUrl( + $this->yellow->config->get("serverScheme"), + $this->yellow->config->get("serverName"), + $this->yellow->config->get("serverBase"), $this->location)); + $this->set("pageEdit", $this->yellow->lookup->normaliseUrl( + $this->yellow->config->get("webinterfaceServerScheme"), + $this->yellow->config->get("webinterfaceServerName"), + $this->yellow->config->get("serverBase"), + rtrim($this->yellow->config->get("webinterfaceLocation"), '/').$this->location)); + $this->set("pageFile", $this->yellow->lookup->normaliseFile($this->fileName)); + if($this->get("status") == "hidden") $this->available = false; + } else { + $this->set("type", $this->yellow->toolbox->getFileExtension($this->fileName)); + $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName))); + $this->set("pageFile", $this->yellow->lookup->normaliseFile($this->fileName, true)); + } if(!empty($pageError)) $this->set("pageError", $pageError); foreach($this->yellow->plugins->plugins as $key=>$value) { - if(method_exists($value["obj"], "onParseMeta")) $value["obj"]->onParseMeta($this, $this->rawData); + if(method_exists($value["obj"], "onParseMeta")) $value["obj"]->onParseMeta($this); } } @@ -518,52 +514,69 @@ class YellowPage if($this->yellow->plugins->isExisting($this->get("parser"))) { $plugin = $this->yellow->plugins->plugins[$this->get("parser")]; - if(method_exists($plugin["obj"], "onParseContentText")) + if(method_exists($plugin["obj"], "onParseContentRaw")) { $this->parser = $plugin["obj"]; - $this->parserData = $this->parser->onParseContentText($this, $this->getContent(true)); + $this->parserData = $this->parser->onParseContentRaw($this, $this->getContent(true)); foreach($this->yellow->plugins->plugins as $key=>$value) { - if(method_exists($value["obj"], "onParseContent")) + if(method_exists($value["obj"], "onParseContentText")) { - $output = $value["obj"]->onParseContent($this, $this->parserData); + $output = $value["obj"]->onParseContentText($this, $this->parserData); if(!is_null($output)) { $this->parserData = $output; } } } - if(!$this->isExisting("description")) - { - $this->set("description", $this->yellow->toolbox->createTextDescription($this->parserData, 150)); - } - if(!$this->isExisting("keywords")) - { - $this->set("keywords", $this->yellow->toolbox->createTextKeywords($this->get("title"), 10)); - } } + } else { + $this->parserData = $this->getContent(true); + $this->parserData = preg_replace("/@pageError/i", $this->get("pageError"), $this->parserData); + } + if(!$this->isExisting("description")) + { + $this->set("description", $this->yellow->toolbox->createTextDescription($this->parserData, 150)); + } + if(!$this->isExisting("keywords")) + { + $this->set("keywords", $this->yellow->toolbox->createTextKeywords($this->get("title"), 10)); } - if(defined("DEBUG") && DEBUG>=2) echo "YellowPage::parseContent location:".$this->location."<br/>\n"; + if(defined("DEBUG") && DEBUG>=3) echo "YellowPage::parseContent location:".$this->location."<br/>\n"; } } - // Parse page custom type - function parseType($name, $text, $typeShortcut) + // Parse page content block + function parseContentBlock($name, $text, $typeShortcut) { $output = NULL; foreach($this->yellow->plugins->plugins as $key=>$value) { - if(method_exists($value["obj"], "onParseType")) + if(method_exists($value["obj"], "onParseContentBlock")) { - $output = $value["obj"]->onParseType($this, $name, $text, $typeShortcut); + $output = $value["obj"]->onParseContentBlock($this, $name, $text, $typeShortcut); if(!is_null($output)) break; } } - if(defined("DEBUG") && DEBUG>=3 && !empty($name)) echo "YellowPage::parseType name:$name shortcut:$typeShortcut<br/>\n"; + if(defined("DEBUG") && DEBUG>=3 && !empty($name)) echo "YellowPage::parseContentBlock name:$name shortcut:$typeShortcut<br/>\n"; return $output; } - // Parse page response - function parseResponse() + // Parse page + function parsePage() { - $this->yellow->template($this->get("template")); + $this->outputData = NULL; + if(!$this->isError()) + { + foreach($this->yellow->plugins->plugins as $key=>$value) + { + if(method_exists($value["obj"], "onParsePage")) $value["obj"]->onParsePage(); + } + } + if(is_null($this->outputData)) + { + ob_start(); + $this->yellow->template($this->get("template")); + $this->outputData = ob_get_contents(); + ob_end_clean(); + } if(!$this->isCacheable()) $this->setHeader("Cache-Control", "no-cache, must-revalidate"); if(!$this->isHeader("Content-Type")) $this->setHeader("Content-Type", "text/html; charset=utf-8"); if(!$this->isHeader("Content-Modified")) $this->setHeader("Content-Modified", $this->getModified(true)); @@ -580,30 +593,17 @@ class YellowPage { $this->error(500, "Parser '".$this->get("parser")."' does not exist!"); } - if($this->statusCode==200 && - !$this->yellow->toolbox->isValidContentType($this->getHeader("Content-Type"), $this->getLocation())) + if($this->yellow->getRequestHandler()=="core" && $this->isExisting("redirect") && $this->statusCode==200) { - $this->error(500, "Type '".$this->getHeader("Content-Type")."' does not match file name!"); + $location = $this->yellow->lookup->normaliseLocation($this->get("redirect"), $this->base, $this->location); + $location = $this->yellow->lookup->normaliseUrl($this->serverScheme, $this->serverName, "", $location); + $this->clean(301, $location); } - if($this->statusCode==200 && $this->yellow->getRequestHandler()=="core" && $this->isExisting("redirect")) + if($this->yellow->getRequestHandler()=="core" && !$this->isAvailable() && $this->statusCode==200) { - $location = $this->yellow->toolbox->normaliseLocation($this->get("redirect"), $this->base, $this->location); - $locationHeader = $this->yellow->toolbox->getLocationHeader($this->serverScheme, $this->serverName, "", $location); - $this->clean(301, $locationHeader); - $this->setHeader("Cache-Control", "no-cache, must-revalidate"); + $this->error(404); } - } - - // Set page response header - function setHeader($key, $value) - { - $this->headerData[$key] = $value; - } - - // Return page response header - function getHeader($key) - { - return $this->isHeader($key) ? $this->headerData[$key] : ""; + if($this->isExisting("pageClean")) $this->page->outputData = NULL; } // Set page meta data @@ -624,10 +624,10 @@ class YellowPage return htmlspecialchars($this->get($key)); } - // Return page meta data as human readable date + // Return page meta data as language specific date function getDate($key, $dateFormat = "") { - if($this->yellow->text->isExisting($dateFormat)) + if(!empty($dateFormat)) { $format = $this->yellow->text->get($dateFormat); } else { @@ -650,17 +650,6 @@ class YellowPage return $text; } - // Return page extra header, HTML encoded - function getHeaderExtra() - { - $header = ""; - foreach($this->yellow->plugins->plugins as $key=>$value) - { - if(method_exists($value["obj"], "onHeaderExtra")) $header .= $value["obj"]->onHeaderExtra($this); - } - return $header; - } - // Return parent page relative to current page, NULL if none function getParent() { @@ -683,13 +672,31 @@ class YellowPage function getSiblings($showInvisible = false) { $parentLocation = $this->yellow->pages->getParentLocation($this->location); - return $this->yellow->pages->findChildren($parentLocation, $showInvisible); + return $this->yellow->pages->getChildren($parentLocation, $showInvisible); } // Return page collection with child pages relative to current page function getChildren($showInvisible = false) { - return $this->yellow->pages->findChildren($this->location, $showInvisible); + return $this->yellow->pages->getChildren($this->location, $showInvisible); + } + + // Return page collection with media files for current page + function getFiles($showInvisible = false) + { + return $this->yellow->files->index($showInvisible, true)->filter("pageFile", $this->get("pageFile")); + } + + // Set page collection with additional pages for current page + function setPages($pages) + { + $this->pages = $pages; + } + + // Return page collection with additional pages for current page + function getPages() + { + return $this->pages; } // Return absolute page location @@ -701,7 +708,36 @@ class YellowPage // Return page URL with server scheme and server name function getUrl() { - return $this->yellow->toolbox->getUrl($this->serverScheme, $this->serverName, $this->base, $this->location); + return $this->yellow->lookup->normaliseUrl($this->serverScheme, $this->serverName, $this->base, $this->location); + } + + // Return page extra HTML data + function getExtra() + { + $output = ""; + foreach($this->yellow->plugins->plugins as $key=>$value) + { + if(method_exists($value["obj"], "onExtra")) $output .= $value["obj"]->onExtra(); + } + return $output; + } + + // Set page response output + function setOutput($output) + { + $this->outputData = $output; + } + + // Set page response header + function setHeader($key, $value) + { + $this->headerData[$key] = $value; + } + + // Return page response header + function getHeader($key) + { + return $this->isHeader($key) ? $this->headerData[$key] : ""; } // Return page content modification date, Unix time or HTTP format @@ -748,59 +784,62 @@ class YellowPage } // Respond with status code, no page content - function clean($statusCode, $responseHeader = "") + function clean($statusCode, $location = "") { if(!$this->isExisting("pageClean") && $statusCode>0) { $this->statusCode = $statusCode; $this->lastModified = 0; $this->headerData = array(); - if(!empty($responseHeader)) $this->header($responseHeader); + if(!empty($location)) + { + $this->setHeader("Location", $location); + $this->setHeader("Cache-Control", "no-cache, must-revalidate"); + } $this->set("pageClean", (string)$statusCode); } } - // Add page response header, HTTP format - function header($responseHeader) + // Check if page is available + function isAvailable() { - $tokens = explode(':', $responseHeader, 2); - $this->setHeader(trim($tokens[0]), trim($tokens[1])); + return $this->available; } - // Check if response header exists - function isHeader($key) + // Check if page is visible + function isVisible() { - return !is_null($this->headerData[$key]); + return $this->visible; } - // Check if page meta data exists - function isExisting($key) + // Check if page is within current request + function isActive() { - return !is_null($this->metaData[$key]); + return $this->active; } + // Check if page is cacheable + function isCacheable() + { + return $this->cacheable; + } + // Check if page with error function isError() { return $this->isExisting("pageError"); } - // Check if page is within current HTTP request - function isActive() - { - return $this->active; - } - - // Check if page is visible in navigation - function isVisible() + // Check if response header exists + function isHeader($key) { - return $this->visible; + return !is_null($this->headerData[$key]); } - // Check if page is cacheable - function isCacheable() + // Check if page meta data exists + function isExisting($key) { - return $this->cacheable; + return !is_null($this->metaData[$key]); } } @@ -809,7 +848,7 @@ class YellowPageCollection extends ArrayObject { var $yellow; //access to API var $filterValue; //current page filter value - var $paginationPage; //current page number in pagination + var $paginationNumber; //current page number in pagination var $paginationCount; //highest page number in pagination function __construct($yellow) @@ -861,6 +900,18 @@ class YellowPageCollection extends ArrayObject return $this; } + // Filter page collection by file + function match($regex = "/.*/") + { + $array = array(); + foreach($this->getArrayCopy() as $page) + { + if(preg_match($regex, basename($page->fileName))) array_push($array, $page); + } + $this->exchangeArray($array); + return $this; + } + // Merge page collection function merge($input) { @@ -910,23 +961,24 @@ class YellowPageCollection extends ArrayObject // Paginate page collection function pagination($limit, $reverse = true) { - $array = $this->getArrayCopy(); - if($reverse) $array = array_reverse($array); - $this->paginationPage = 1; + $this->paginationNumber = 1; $this->paginationCount = ceil($this->count() / $limit); - if($limit < $this->count()) + $pagination = $this->yellow->config->get("contentPagination"); + if(isset($_REQUEST[$pagination])) $this->paginationNumber = intval($_REQUEST[$pagination]); + if($this->paginationNumber > $this->paginationCount) $this->paginationNumber = 0; + if($this->paginationNumber >= 1) { - $pagination = $this->yellow->config->get("contentPagination"); - if(isset($_REQUEST[$pagination])) $this->paginationPage = max(1, $_REQUEST[$pagination]); + $array = $this->getArrayCopy(); + if($reverse) $array = array_reverse($array); + $this->exchangeArray(array_slice($array, ($this->paginationNumber - 1) * $limit, $limit)); } - $this->exchangeArray(array_slice($array, ($this->paginationPage - 1) * $limit, $limit)); return $this; } // Return current page number in pagination - function getPaginationPage() + function getPaginationNumber() { - return $this->paginationPage; + return $this->paginationNumber; } // Return highest page number in pagination @@ -936,7 +988,7 @@ class YellowPageCollection extends ArrayObject } // Return absolute location for a page in pagination - function getLocationPage($pageNumber) + function getPaginationLocation($pageNumber) { if($pageNumber>=1 && $pageNumber<=$this->paginationCount) { @@ -949,19 +1001,19 @@ class YellowPageCollection extends ArrayObject } // Return absolute location for previous page in pagination - function getLocationPrevious() + function getPaginationPrevious() { - $pageNumber = $this->paginationPage; + $pageNumber = $this->paginationNumber; $pageNumber = ($pageNumber>1 && $pageNumber<=$this->paginationCount) ? $pageNumber-1 : 0; - return $this->getLocationPage($pageNumber); + return $this->getPaginationLocation($pageNumber); } // Return absolute location for next page in pagination - function getLocationNext() + function getPaginationNext() { - $pageNumber = $this->paginationPage; + $pageNumber = $this->paginationNumber; $pageNumber = ($pageNumber>=1 && $pageNumber<$this->paginationCount) ? $pageNumber+1 : 0; - return $this->getLocationPage($pageNumber); + return $this->getPaginationLocation($pageNumber); } // Return current page filter @@ -978,26 +1030,14 @@ class YellowPageCollection extends ArrayObject return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($modified) : $modified; } - // Return first page in page collection - function first() - { - return $this->offsetGet(0); - } - - // Return last page in page collection - function last() - { - return $this->offsetGet($this->count()-1); - } - - // Check if there is an active pagination + // Check if there is a pagination function isPagination() { return $this->paginationCount > 1; } } -// Yellow pages from file system +// Yellow pages class YellowPages { var $yellow; //access to API @@ -1011,25 +1051,86 @@ class YellowPages $this->pages = array(); } - // Return page collection with all pages from file system - function index($showInvisible = false, $showMulti = false, $levelMax = 0) + // Scan file system on demand + function scanLocation($location) + { + if(is_null($this->pages[$location])) + { + if(defined("DEBUG") && DEBUG>=2) echo "YellowPages::scanLocation location:$location<br/>\n"; + $this->pages[$location] = array(); + $serverScheme = $this->yellow->page->serverScheme; + $serverName = $this->yellow->page->serverName; + $base = $this->yellow->page->base; + if(empty($location)) + { + $rootLocations = $this->yellow->lookup->findRootLocations(); + foreach($rootLocations as $rootLocation) + { + list($rootLocation, $fileName) = explode(' ', $rootLocation, 2); + $page = new YellowPage($this->yellow); + $page->setRequestInformation($serverScheme, $serverName, $base, $rootLocation, $fileName); + $page->parseData("", false, 0); + array_push($this->pages[$location], $page); + } + } else { + $fileNames = $this->yellow->lookup->findChildrenFromLocation($location); + foreach($fileNames as $fileName) + { + $fileHandle = @fopen($fileName, "r"); + if($fileHandle) + { + $fileData = fread($fileHandle, 4096); + $statusCode = filesize($fileName) <= 4096 ? 200 : 0; + fclose($fileHandle); + } else { + $fileData = ""; + $statusCode = 0; + } + $page = new YellowPage($this->yellow); + $page->setRequestInformation($serverScheme, $serverName, $base, + $this->yellow->lookup->findLocationFromFile($fileName), $fileName); + $page->parseData($fileData, false, $statusCode); + array_push($this->pages[$location], $page); + } + } + } + return $this->pages[$location]; + } + + // Return page from file system, NULL if not found + function find($location, $absoluteLocation = false) + { + if($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base)); + foreach($this->scanLocation($this->getParentLocation($location)) as $page) + { + if($page->location == $location) + { + if(!$this->yellow->lookup->isRootLocation($page->location)) { $found = true; break; } + } + } + return $found ? $page : NULL; + } + + // Return page collection with all pages + function index($showInvisible = false, $showMultiLanguage = false, $levelMax = 0) { - $rootLocation = $showMulti ? "" : $this->getRootLocation($this->yellow->page->location); - return $this->findChildrenRecursive($rootLocation, $showInvisible, $levelMax); + $rootLocation = $showMultiLanguage ? "" : $this->getRootLocation($this->yellow->page->location); + return $this->getChildrenRecursive($rootLocation, $showInvisible, $levelMax); } // Return page collection with top-level navigation function top($showInvisible = false) { $rootLocation = $this->getRootLocation($this->yellow->page->location); - return $this->findChildren($rootLocation, $showInvisible); + return $this->getChildren($rootLocation, $showInvisible); } // Return page collection with path ancestry function path($location, $absoluteLocation = false) { $pages = new YellowPageCollection($this->yellow); - if($page = $this->find($location, $absoluteLocation)) + if($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base)); + if($page = $this->find($location)) { $pages->prepend($page); for(; $parent = $page->getParent(); $page=$parent) $pages->prepend($parent); @@ -1044,130 +1145,80 @@ class YellowPages { $pages = new YellowPageCollection($this->yellow); if($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base)); - $postfix = substru($location, strlenu($this->getRootLocation($location)) - 4); - $this->scanChildren(""); - foreach($this->pages[""] as $page) + $locationEnd = substru($location, strlenu($this->getRootLocation($location)) - 4); + foreach($this->scanLocation("") as $page) { - if($content = $this->find(substru($page->location, 4).$postfix)) + if($content = $this->find(substru($page->location, 4).$locationEnd)) { - if($content->isVisible() || $showInvisible) $pages->append($content); + if($content->isAvailable() && ($content->isVisible() || $showInvisible)) + { + if(!$this->yellow->lookup->isRootLocation($content->location)) $pages->append($content); + } } } return $pages; } - // Return empty page collection - function create() + // Return page collection that's empty + function clean() { return new YellowPageCollection($this->yellow); } - // Return one page from file system, NULL if not found - function find($location, $absoluteLocation = false) + // Return available languages + function getLanguages($showInvisible = false) { - if(!$this->yellow->toolbox->isRootLocation($location)) + $languages = array(); + foreach($this->scanLocation("") as $page) { - if($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base)); - $parentLocation = $this->getParentLocation($location); - $this->scanChildren($parentLocation); - foreach($this->pages[$parentLocation] as $page) if($page->location == $location) { $found = true; break; } + if($page->isAvailable() && ($page->isVisible() || $showInvisible)) + { + array_push($languages, $page->get("language")); + } } - return $found ? $page : NULL; + return $languages; } - // Find child pages - function findChildren($location, $showInvisible = false) + // Return child pages + function getChildren($location, $showInvisible = false) { $pages = new YellowPageCollection($this->yellow); - $this->scanChildren($location); - foreach($this->pages[$location] as $page) + foreach($this->scanLocation($location) as $page) { - if($page->isVisible() || $showInvisible) + if($page->isAvailable() && ($page->isVisible() || $showInvisible)) { - if(!$this->yellow->toolbox->isRootLocation($page->location)) $pages->append($page); + if(!$this->yellow->lookup->isRootLocation($page->location)) $pages->append($page); } } return $pages; } - // Find child pages recursively - function findChildrenRecursive($location, $showInvisible = false, $levelMax = 0) + // Return child pages recursively + function getChildrenRecursive($location, $showInvisible = false, $levelMax = 0) { --$levelMax; $pages = new YellowPageCollection($this->yellow); - $this->scanChildren($location); - foreach($this->pages[$location] as $page) + foreach($this->scanLocation($location) as $page) { - if($page->isVisible() || $showInvisible) + if($page->isAvailable() && ($page->isVisible() || $showInvisible)) { - if(!$this->yellow->toolbox->isRootLocation($page->location)) $pages->append($page); - if(!$this->yellow->toolbox->isFileLocation($page->location) && $levelMax!=0) + if(!$this->yellow->lookup->isRootLocation($page->location)) $pages->append($page); + if(!$this->yellow->lookup->isFileLocation($page->location) && $levelMax!=0) { - $pages->merge($this->findChildrenRecursive($page->location, $showInvisible, $levelMax)); + $pages->merge($this->getChildrenRecursive($page->location, $showInvisible, $levelMax)); } } } return $pages; } - // Scan child pages on demand - function scanChildren($location) + // Return root location + function getRootLocation($location) { - if(is_null($this->pages[$location])) + $rootLocation = "root/"; + if($this->yellow->config->get("multiLanguageMode")) { - if(defined("DEBUG") && DEBUG>=2) echo "YellowPages::scanChildren location:$location<br/>\n"; - $this->pages[$location] = array(); - $serverScheme = $this->yellow->page->serverScheme; - $serverName = $this->yellow->page->serverName; - $base = $this->yellow->page->base; - if(empty($location)) - { - $rootLocations = $this->yellow->toolbox->findRootLocations($this->yellow->config->get("contentDir"), - $this->yellow->config->get("contentRootDir")); - foreach($rootLocations as $rootLocation) - { - $page = new YellowPage($this->yellow); - $page->setRequestInformation($serverScheme, $serverName, $base, $rootLocation, ""); - $page->parseData("", false); - array_push($this->pages[$location], $page); - } - } else { - $fileNames = $this->yellow->toolbox->findChildrenFromLocation($location, $this->yellow->config->get("contentDir"), - $this->yellow->config->get("contentRootDir"), $this->yellow->config->get("contentHomeDir"), - $this->yellow->config->get("contentDefaultFile"), $this->yellow->config->get("contentExtension")); - foreach($fileNames as $fileName) - { - $fileHandle = @fopen($fileName, "r"); - if($fileHandle) - { - $fileData = fread($fileHandle, 4096); - $statusCode = filesize($fileName) <= 4096 ? 200 : 0; - fclose($fileHandle); - } else { - $fileData = ""; - $statusCode = 0; - } - $page = new YellowPage($this->yellow); - $page->setRequestInformation($serverScheme, $serverName, $base, - $this->yellow->toolbox->findLocationFromFile($fileName, $this->yellow->config->get("contentDir"), - $this->yellow->config->get("contentRootDir"), $this->yellow->config->get("contentHomeDir"), - $this->yellow->config->get("contentDefaultFile"), $this->yellow->config->get("contentExtension")), - $fileName); - $page->parseData($fileData, false, $statusCode); - array_push($this->pages[$location], $page); - } - } - } - } - - // Return root location - function getRootLocation($location) - { - $rootLocation = "root/"; - if($this->yellow->config->get("multiLanguageMode")) - { - $this->scanChildren(""); - foreach($this->pages[""] as $page) + foreach($this->scanLocation("") as $page) { $token = substru($page->location, 4); if($token!="/" && substru($location, 0, strlenu($token))==$token) { $rootLocation = "root$token"; break; } @@ -1185,25 +1236,226 @@ class YellowPages // Return parent location function getParentLocation($location) { - $prefix = rtrim(substru($this->getRootLocation($location), 4), '/'); - if(preg_match("#^($prefix.*\/).+?$#", $location, $matches)) + $token = rtrim(substru($this->getRootLocation($location), 4), '/'); + if(preg_match("#^($token.*\/).+?$#", $location, $matches)) + { + if($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1]; + } + if(empty($parentLocation)) $parentLocation = "root$token/"; + return $parentLocation; + } + + // Return top-level location + function getParentTopLocation($location) + { + $token = rtrim(substru($this->getRootLocation($location), 4), '/'); + if(preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1]; + if(empty($parentTopLocation)) $parentTopLocation = "$token/"; + return $parentTopLocation; + } +} + +// Yellow files +class YellowFiles +{ + var $yellow; //access to API + var $files; //scanned files + + function __construct($yellow) + { + $this->yellow = $yellow; + $this->files = array(); + } + + // Scan file system on demand + function scanLocation($location) + { + if(is_null($this->files[$location])) + { + if(defined("DEBUG") && DEBUG>=2) echo "YellowFiles::scanLocation location:$location<br/>\n"; + $this->files[$location] = array(); + $serverScheme = $this->yellow->page->serverScheme; + $serverName = $this->yellow->page->serverName; + $base = $this->yellow->page->base; + if(empty($location)) + { + $fileNames = array($this->yellow->config->get("mediaDir")); + } else { + $fileNames = array(); + $path = substru($location, 1); + foreach($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, true) as $entry) + { + array_push($fileNames, $entry."/"); + } + foreach($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, false, true) as $entry) + { + array_push($fileNames, $entry); + } + } + foreach($fileNames as $fileName) + { + $file = new YellowPage($this->yellow); + $file->setRequestInformation($serverScheme, $serverName, $base, "/".$fileName, $fileName); + $file->parseData(NULL, false, 0); + array_push($this->files[$location], $file); + } + } + return $this->files[$location]; + } + + // Return page with media file information, NULL if not found + function find($location, $absoluteLocation = false) + { + if($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base)); + foreach($this->scanLocation($this->getParentLocation($location)) as $file) + { + if($file->location == $location) { $found = true; break; } + } + return $found ? $file : NULL; + } + + // Return page collection with all media files + function index($showInvisible = false, $showMultiPass = false, $levelMax = 0) + { + return $this->getChildrenRecursive("", $showInvisible, $levelMax); + } + + // Return page collection that's empty + function clean() + { + return new YellowPageCollection($this->yellow); + } + + // Return child files + function getChildren($location, $showInvisible = false) + { + $files = new YellowPageCollection($this->yellow); + foreach($this->scanLocation($location) as $file) + { + if($file->isAvailable() && ($file->isVisible() || $showInvisible)) + { + $files->append($file); + } + } + return $files; + } + + // Return child files recursively + function getChildrenRecursive($location, $showInvisible = false, $levelMax = 0) + { + --$levelMax; + $files = new YellowPageCollection($this->yellow); + foreach($this->scanLocation($location) as $file) + { + if($file->isAvailable() && ($file->isVisible() || $showInvisible)) + { + $files->append($file); + if(!$this->yellow->lookup->isFileLocation($file->location) && $levelMax!=0) + { + $files->merge($this->getChildrenRecursive($file->location, $showInvisible, $levelMax)); + } + } + } + return $files; + } + + // Return home location + function getHomeLocation($location) + { + return "/".$this->yellow->config->get("mediaDir"); + } + + // Return parent location + function getParentLocation($location) + { + $token = rtrim("/".$this->yellow->config->get("mediaDir"), '/'); + if(preg_match("#^($token.*\/).+?$#", $location, $matches)) { - if($matches[1]!="$prefix/" || $this->yellow->toolbox->isFileLocation($location)) $parentLocation = $matches[1]; + if($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1]; } - if(empty($parentLocation)) $parentLocation = "root$prefix/"; + if(empty($parentLocation)) $parentLocation = ""; return $parentLocation; } // Return top-level location function getParentTopLocation($location) { - $prefix = rtrim(substru($this->getRootLocation($location), 4), '/'); - if(preg_match("#^($prefix.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1]; - if(empty($parentTopLocation)) $parentTopLocation = "$prefix/"; + $token = rtrim("/".$this->yellow->config->get("mediaDir"), '/'); + if(preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1]; + if(empty($parentTopLocation)) $parentTopLocation = "$token/"; return $parentTopLocation; } } +// Yellow plugins +class YellowPlugins +{ + var $yellow; //access to API + var $plugins; //registered plugins + var $modified; //plugin modification date + + function __construct($yellow) + { + $this->yellow = $yellow; + $this->plugins = array(); + $this->modified = 0; + } + + // Load plugins + function load() + { + $path = $this->yellow->config->get("coreDir"); + foreach($this->yellow->toolbox->getDirectoryEntries($path, "/^core-.*\.php$/", true, false) as $entry) + { + $this->modified = max($this->modified, filemtime($entry)); + global $yellow; + require_once($entry); + } + $path = $this->yellow->config->get("pluginDir"); + foreach($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.php$/", true, false) as $entry) + { + $this->modified = max($this->modified, filemtime($entry)); + global $yellow; + require_once($entry); + } + foreach($this->plugins as $key=>$value) + { + $this->plugins[$key]["obj"] = new $value["class"]; + if(defined("DEBUG") && DEBUG>=3) echo "YellowPlugins::load class:$value[class] $value[version]<br/>\n"; + if(method_exists($this->plugins[$key]["obj"], "onLoad")) $this->plugins[$key]["obj"]->onLoad($yellow); + } + } + + // Register plugin + function register($name, $class, $version) + { + if(!$this->isExisting($name)) + { + $this->plugins[$name] = array(); + $this->plugins[$name]["class"] = $class; + $this->plugins[$name]["version"] = $version; + } + } + + // Return plugin + function get($name) + { + return $this->plugins[$name]["obj"]; + } + + // Return plugins modification date, Unix time or HTTP format + function getModified($httpFormat = false) + { + return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; + } + + // Check if plugin exists + function isExisting($name) + { + return !is_null($this->plugins[$name]); + } +} + // Yellow configuration class YellowConfig { @@ -1455,316 +1707,317 @@ class YellowText return !is_null($this->text[$language]) && !is_null($this->text[$language][$key]); } } - -// Yellow plugins -class YellowPlugins -{ - var $plugins; //registered plugins - var $modified; //plugin modification date - function __construct() +// Yellow location and file lookup +class YellowLookup +{ + var $yellow; //access to API + + function __construct($yellow) { - $this->plugins = array(); - $this->modified = 0; + $this->yellow = $yellow; } - // Load plugins - function load() + // Return root locations + function findRootLocations($includePath = true) { - global $yellow; - $path = $yellow->config->get("coreDir"); - foreach($yellow->toolbox->getDirectoryEntries($path, "/^core-.*\.php$/", true, false) as $entry) require_once($entry); - $path = $yellow->config->get("pluginDir"); - foreach($yellow->toolbox->getDirectoryEntries($path, "/^.*\.php$/", true, false) as $entry) - { - $this->modified = max($this->modified, filemtime($entry)); - require_once($entry); - } - foreach($this->plugins as $key=>$value) + $locations = array(); + $pathBase = $this->yellow->config->get("contentDir"); + $pathRoot = $this->yellow->config->get("contentRootDir"); + if(!empty($pathRoot)) { - $this->plugins[$key]["obj"] = new $value["class"]; - if(defined("DEBUG") && DEBUG>=3) echo "YellowPlugins::load class:$value[class] $value[version]<br/>\n"; - if(method_exists($this->plugins[$key]["obj"], "onLoad")) $this->plugins[$key]["obj"]->onLoad($yellow); + foreach($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) + { + $token = $this->normaliseName($entry)."/"; + if($token == $pathRoot) $token = ""; + array_push($locations, $includePath ? "root/$token $pathBase$entry/" : "root/$token"); + if(defined("DEBUG") && DEBUG>=2) echo "YellowLookup::findRootLocations root/$token<br/>\n"; + } + } else { + array_push($locations, $includePath ? "root/ $pathBase" : "root/"); } + return $locations; } - // Register plugin - function register($name, $class, $version) + // Return location from file path + function findLocationFromFile($fileName) { - if(!$this->isExisting($name)) + $location = "/"; + $pathBase = $this->yellow->config->get("contentDir"); + $pathRoot = $this->yellow->config->get("contentRootDir"); + $pathHome = $this->yellow->config->get("contentHomeDir"); + $fileDefault = $this->yellow->config->get("contentDefaultFile"); + $fileExtension = $this->yellow->config->get("contentExtension"); + if(substru($fileName, 0, strlenu($pathBase)) == $pathBase) { - $this->plugins[$name] = array(); - $this->plugins[$name]["class"] = $class; - $this->plugins[$name]["version"] = $version; + $fileName = substru($fileName, strlenu($pathBase)); + $tokens = explode('/', $fileName); + if(!empty($pathRoot)) + { + $token = $this->normaliseName($tokens[0]).'/'; + if($token!=$pathRoot) $location .= $token; + array_shift($tokens); + } + for($i=0; $i<count($tokens)-1; ++$i) + { + $token = $this->normaliseName($tokens[$i]).'/'; + if($i || $token!=$pathHome) $location .= $token; + } + $token = $this->normaliseName($tokens[$i]); + $fileFolder = $this->normaliseName($tokens[$i-1]).$fileExtension; + if($token!=$fileDefault && $token!=$fileFolder) $location .= $this->normaliseName($tokens[$i], true, true); + $extension = ($pos = strrposu($fileName, '.')) ? substru($fileName, $pos) : ""; + if($extension != $fileExtension) $invalid = true; + } else { + $invalid = true; } - } - - // Return plugin object - function get($name) - { - return $this->plugins[$name]["obj"]; - } - - // Return plugins modification date, Unix time or HTTP format - function getModified($httpFormat = false) - { - return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; - } - - // Check if plugin exists - function isExisting($name) - { - return !is_null($this->plugins[$name]); - } -} - -// Yellow toolbox with helpers -class YellowToolbox -{ - // Return server scheme from current HTTP request - function getServerScheme() - { - $serverScheme = ""; - if(preg_match("/^HTTP\//", $_SERVER["SERVER_PROTOCOL"])) + if(defined("DEBUG") && DEBUG>=2) { - $secure = isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"]!="off"; - $serverScheme = $secure ? "https" : "http"; + $debug = ($invalid ? "INVALID" : $location)." <- $pathBase$fileName"; + echo "YellowLookup::findLocationFromFile $debug<br/>\n"; } - return $serverScheme; - } - - // Return server name from current HTTP request - function getServerName() - { - return $_SERVER["SERVER_NAME"]; + return $invalid ? "" : $location; } - // Return server base from current HTTP request - function getServerBase() + // Return file path from location + function findFileFromLocation($location, $directory = false) { - $serverBase = ""; - if(preg_match("/^(.*)\//", $_SERVER["SCRIPT_NAME"], $matches)) $serverBase = $matches[1]; - return $serverBase; - } - - // Return location from current HTTP request - function getLocation() - { - $uri = $_SERVER["REQUEST_URI"]; - return rawurldecode(($pos = strposu($uri, '?')) ? substru($uri, 0, $pos) : $uri); - } - - // Return location from current HTTP request, remove unwanted path tokens - function getLocationClean() - { - $string = $this->getLocation(); - $location = ($string[0]=='/') ? '' : '/'; - for($pos=0; $pos<strlenb($string); ++$pos) + $path = $pathBase = $this->yellow->config->get("contentDir"); + $pathRoot = $this->yellow->config->get("contentRootDir"); + $pathHome = $this->yellow->config->get("contentHomeDir"); + $fileDefault = $this->yellow->config->get("contentDefaultFile"); + $fileExtension = $this->yellow->config->get("contentExtension"); + $tokens = explode('/', $location); + if($this->isRootLocation($location)) { - if($string[$pos] == '/') + if(!empty($pathRoot)) { - if($string[$pos+1] == '/') continue; - if($string[$pos+1] == '.') + $token = (count($tokens) > 2) ? $tokens[1] : rtrim($pathRoot, '/'); + $path .= $this->findFileDirectory($path, $token, true, true, $found, $invalid); + } + } else { + if(!empty($pathRoot)) + { + if(count($tokens) > 2) { - $posNew = $pos+1; while($string[$posNew] == '.') ++$posNew; - if($string[$posNew]=='/' || $string[$posNew]=='') - { - $pos = $posNew-1; - continue; - } + if($this->normaliseName($tokens[1]) == $this->normaliseName($pathRoot)) $invalid = true; + $path .= $this->findFileDirectory($path, $tokens[1], false, true, $found, $invalid); + if($found) array_shift($tokens); } + if(!$found) $path .= $this->findFileDirectory($path, rtrim($pathRoot, '/'), true, true, $found, $invalid); + } - $location .= $string[$pos]; - } - if(preg_match("/^(.*?\/)([^\/]+:.*)$/", $location, $matches)) - { - $location = $matches[1]; - foreach(explode('/', $matches[2]) as $token) + if(count($tokens) > 2) { - preg_match("/^(.*?):(.*)$/", $token, $matches); - if(!empty($matches[1]) && !strempty($matches[2])) + if($this->normaliseName($tokens[1]) == $this->normaliseName($pathHome)) $invalid = true; + for($i=1; $i<count($tokens)-1; ++$i) { - $matches[1] = strreplaceu(array("\x1c", "\x1d"), array('/', ':'), $matches[1]); - $matches[2] = strreplaceu(array("\x1c", "\x1d"), array('/', ':'), $matches[2]); - $_REQUEST[$matches[1]] = $matches[2]; + $path .= $this->findFileDirectory($path, $tokens[$i], true, true, $found, $invalid); } + } else { + $i = 1; + $tokens[0] = rtrim($pathHome, '/'); + $path .= $this->findFileDirectory($path, $tokens[0], true, true, $found, $invalid); } - } - if(!preg_match("/^HTTP\//", $_SERVER["SERVER_PROTOCOL"])) $_SERVER["SERVER_PROTOCOL"] = "HTTP/1.1"; - return $location; - } - - // Return location arguments from current HTTP request - function getLocationArgs() - { - if(preg_match("/^(.*?\/)([^\/]+:.*)$/", $this->getLocation(), $matches)) $locationArgs = $matches[2]; - return $locationArgs; - } - - // Return location arguments from current HTTP request, modify an argument - function getLocationArgsNew($arg, $pagination) - { - preg_match("/^(.*?):(.*)$/", $arg, $args); - if(preg_match("/^(.*?\/)([^\/]+:.*)$/", $this->getLocation(), $matches)) - { - foreach(explode('/', $matches[2]) as $token) + if(!$directory) { - preg_match("/^(.*?):(.*)$/", $token, $matches); - if($matches[1] == $args[1]) { $matches[2] = $args[2]; $found = true; } - if(!empty($matches[1]) && !strempty($matches[2])) + $fileFolder = $tokens[$i-1].$fileExtension; + if(!empty($tokens[$i])) { - if(!empty($locationArgs)) $locationArgs .= '/'; - $locationArgs .= "$matches[1]:$matches[2]"; + $token = $tokens[$i].$fileExtension; + if($token==$fileDefault || $token==$fileFolder) $invalid = true; + $path .= $this->findFileDirectory($path, $token, true, false, $found, $invalid); + } else { + $path .= $this->findFileDefault($path, $fileDefault, $fileFolder); + } + if(defined("DEBUG") && DEBUG>=2) + { + $debug = "$location -> ".($invalid ? "INVALID" : $path); + echo "YellowLookup::findFileFromLocation $debug<br/>\n"; } } } - if(!$found && !empty($args[1]) && !strempty($args[2])) - { - if(!empty($locationArgs)) $locationArgs .= '/'; - $locationArgs .= "$args[1]:$args[2]"; - } - if(!empty($locationArgs)) - { - if(!$this->isPaginationLocation($locationArgs, $pagination)) $locationArgs .= '/'; - $locationArgs = $this->normaliseArgs($locationArgs, false, false); - } - return $locationArgs; + return $invalid ? "" : $path; } - // Return location arguments from current HTTP request, convert form into clean URL - function getLocationArgsCleanUrl($pagination) + // Return file or directory that matches token + function findFileDirectory($path, $token, $tokenFailback, $directory, &$found, &$invalid) { - foreach(array_merge($_GET, $_POST) as $key=>$value) + if($this->normaliseName($token) != $token) $invalid = true; + if(!$invalid) { - if(!empty($key) && !strempty($value)) + $regex = "/^[\d\-\_\.]*".strreplaceu('-', '.', $token)."$/"; + foreach($this->yellow->toolbox->getDirectoryEntries($path, $regex, false, $directory, false) as $entry) { - if(!empty($locationArgs)) $locationArgs .= '/'; - $key = strreplaceu(array('/', ':'), array("\x1c", "\x1d"), $key); - $value = strreplaceu(array('/', ':'), array("\x1c", "\x1d"), $value); - $locationArgs .= "$key:$value"; + if($this->normaliseName($entry) == $token) { $token = $entry; $found = true; break; } } } - if(!empty($locationArgs)) - { - if(!$this->isPaginationLocation($locationArgs, $pagination)) $locationArgs .= '/'; - $locationArgs = $this->normaliseArgs($locationArgs, false, false); - } - return $locationArgs; - } - - // Return location header with URL - function getLocationHeader($serverScheme, $serverName, $base, $location) - { - return "Location: ".$this->getUrl($serverScheme, $serverName, $base, $location); + if($directory) $token .= '/'; + return ($tokenFailback || $found) ? $token : ""; } - // Check if location contains location arguments - function isLocationArgs($location) + // Return default file in directory + function findFileDefault($path, $fileDefault, $fileFolder) { - return preg_match("/[^\/]+:.*$/", $location); + $token = $fileDefault; + if(!is_file($path."/".$fileDefault)) + { + $regex = "/^[\d\-\_\.]*($fileDefault|$fileFolder)$/"; + foreach($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) + { + if($this->normaliseName($entry) == $fileDefault) { $token = $entry; break; } + if($this->normaliseName($entry) == $fileFolder) { $token = $entry; break; } + } + } + return $token; } - // Check if location contains pagination - function isPaginationLocation($location, $pagination) + // Return children from location + function findChildrenFromLocation($location) { - return preg_match("/^(.*\/)?$pagination:\d*$/", $location); + $fileNames = array(); + $fileDefault = $this->yellow->config->get("contentDefaultFile"); + $fileExtension = $this->yellow->config->get("contentExtension"); + if(!$this->isFileLocation($location)) + { + $path = $this->findFileFromLocation($location, true); + foreach($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false) as $entry) + { + $fileFolder = $this->normaliseName($entry).$fileExtension; + $token = $this->findFileDefault($path.$entry, $fileDefault, $fileFolder); + array_push($fileNames, $path.$entry."/".$token); + } + if(!$this->isRootLocation($location)) + { + $fileFolder = $this->normaliseName(basename($path)).$fileExtension; + $regex = "/^.*\\".$fileExtension."$/"; + foreach($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) + { + if($this->normaliseName($entry) == $fileDefault) continue; + if($this->normaliseName($entry) == $fileFolder) continue; + if($this->normaliseName($entry, true, true) == "") continue; + array_push($fileNames, $path.$entry); + } + } + } + return $fileNames; } - // Check if location is specifying root - function isRootLocation($location) + // Return language from file path + function findLanguageFromFile($fileName, $languageDefault) { - return $location[0] != "/"; + $language = $languageDefault; + $pathBase = $this->yellow->config->get("contentDir"); + $pathRoot = $this->yellow->config->get("contentRootDir"); + if(!empty($pathRoot)) + { + $fileName = substru($fileName, strlenu($pathBase)); + if(preg_match("/^(.+?)\//", $fileName, $matches)) $name = $this->normaliseName($matches[1]); + if(strlenu($name) == 2) $language = $name; + } + return $language; } - // Check if location is specifying file or directory - function isFileLocation($location) + // Return theme/template name from file path + function findNameFromFile($fileName, $pathBase, $nameDefault, $fileExtension) { - return substru($location, -1, 1) != "/"; + $name = ""; + if(preg_match("/^.*\/(.+?)$/", dirname($fileName), $matches)) $name = $this->normaliseName($matches[1]); + if(!is_file("$pathBase$name$fileExtension")) $name = $this->normaliseName($nameDefault); + return $name; } - // Check if file is unmodified since last HTTP request - function isFileNotModified($lastModifiedFormatted) + // Return file path for new page + function findFileNew($fileName, $pathBase, $fileNew, $fileDefault) { - return isset($_SERVER["HTTP_IF_MODIFIED_SINCE"]) && $_SERVER["HTTP_IF_MODIFIED_SINCE"]==$lastModifiedFormatted; + if(preg_match("/^.*\/(.+?)$/", dirname($fileName), $matches)) $name = $this->normaliseName($matches[1]); + $fileName = strreplaceu("(.*)", $name, $pathBase.$fileNew); + if(!is_file($fileName)) + { + $name = $this->normaliseName($fileDefault, true, true); + $fileName = strreplaceu("(.*)", $name, $pathBase.$fileNew); + } + return $fileName; } - // Check if clean URL is requested - function isRequestCleanUrl($location) + // Return file path from title + function findFileFromTitle($titlePrefix, $titleText, $fileName, $fileDefault, $fileExtension) { - return (isset($_GET["clean-url"]) || isset($_POST["clean-url"])) && !$this->isFileLocation($location); + preg_match("/^([\d\-\_\.]*)(.*)$/", $titlePrefix, $matches); + if(preg_match("/\d$/", $matches[1])) $matches[1] .= '-'; + $titleText = $this->normaliseName($titleText, false, false, true); + preg_match("/^([\d\-\_\.]*)(.*)$/", $matches[1].$titleText, $matches); + $fileNamePrefix = $matches[1]; + $fileNameText = empty($matches[2]) ? $fileDefault : $matches[2].$fileExtension; + return dirname($fileName)."/".$fileNamePrefix.$fileNameText; } - // Check if content type is valid for location - function isValidContentType($contentType, $location) + // Normalise file/directory/other name + function normaliseName($text, $removePrefix = true, $removeExtension = false, $filterStrict = false) { - $ok = false; - $extension = ($pos = strrposu($location, '.')) ? substru($location, $pos) : ""; - $tokens = explode(';', $contentType); - if($tokens[0] == "text/html") - { - if($this->isFileLocation($location)) - { - if(empty($extension) || preg_match("/^\.(html|md)$/", $extension)) $ok = true; - } else { - $ok = true; - } - } else { - if($this->isFileLocation($location)) - { - if(!empty($extension) && preg_match("/^.*$extension$/", $tokens[0])) $ok = true; - } - } - return $ok; + if($removeExtension) $text = ($pos = strrposu($text, '.')) ? substru($text, 0, $pos) : $text; + if($removePrefix) if(preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches)) $text = $matches[1]; + if($filterStrict) $text = strreplaceu('.', '-', strtoloweru($text)); + return preg_replace("/[^\pL\d\-\_\.]/u", "-", rtrim($text, '/')); } - // Check if location is valid - function isValidLocation($location) + // Normalise content/media file name + function normaliseFile($fileName, $convertExtension = false) { - $string = ""; - $tokens = explode('/', $location); - for($i=1; $i<count($tokens); ++$i) $string .= '/'.$this->normaliseName($tokens[$i]); - return $location == $string; + $fileName = basename($fileName); + if($convertExtension) + { + $fileName = ($pos = strposu($fileName, '.')) ? substru($fileName, 0, $pos) : $fileName; + $fileName .= $this->yellow->config->get("contentExtension"); + } + return $fileName; } - // Check if location is within current HTTP request - function isActiveLocation($location, $currentLocation, $homeLocation) + // Normalise location, make absolute location + function normaliseLocation($location, $pageBase, $pageLocation, $staticLocation = "", $filterStrict = true) { - if($location != $homeLocation) + if(!preg_match("/^\w+:/", trim(html_entity_decode($location, ENT_QUOTES, "UTF-8")))) { - $active = substru($currentLocation, 0, strlenu($location))==$location; + if(empty($staticLocation) || !preg_match("#^$staticLocation#", $location)) + { + if(preg_match("/^\#/", $location)) + { + $location = $pageBase.$pageLocation.$location; + } else if(!preg_match("/^\//", $location)) { + $location = $this->getDirectoryLocation($pageBase.$pageLocation).$location; + } else if(!preg_match("#^$pageBase#", $location)) { + $location = $pageBase.$location; + } + } } else { - $active = $this->getDirectoryLocation($currentLocation)==$location; + if($filterStrict && !preg_match("/^(http|https|ftp|mailto):/", $location)) $location = "error-xss-filter"; } - return $active; + return $location; } - // Check if location is visible in navigation - function isVisibleLocation($location, $fileName, $pathBase) + // Normalise URL, make absolute URL + function normaliseUrl($serverScheme, $serverName, $base, $location) { - $visible = true; - if(substru($fileName, 0, strlenu($pathBase)) == $pathBase) + if(!preg_match("/^\w+:/", $location)) { - $fileName = substru($fileName, strlenu($pathBase)); - $tokens = explode('/', $fileName); - for($i=0; $i<count($tokens)-1; ++$i) - { - if(!preg_match("/^[\d\-\_\.]+(.*)$/", $tokens[$i])) { $visible = false; break; } - } + $url = "$serverScheme://$serverName$base$location"; } else { - $visible = false; + $url = $location; } - return $visible; + return $url; } - // Return root configuration - function findRootConfig($pathBase, $pathRoot, $pathHome, $multiLanguageMode) + // Return content information + function getContentInformation() { - $path = $pathBase; - if(!$multiLanguageMode) $pathRoot = ""; + $path = $pathBase = $this->yellow->config->get("contentDir"); + $pathRoot = $this->yellow->config->get("contentRootDir"); + $pathHome = $this->yellow->config->get("contentHomeDir"); + if(!$this->yellow->config->get("multiLanguageMode")) $pathRoot = ""; if(!empty($pathRoot)) { $token = $root = rtrim($pathRoot, '/'); - foreach($this->getDirectoryEntries($path, "/.*/", true, true, false) as $entry) + foreach($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false) as $entry) { if(empty($firstRoot)) { $firstRoot = $token = $entry; } if($this->normaliseName($entry) == $root) { $token = $entry; break; } @@ -1775,7 +2028,7 @@ class YellowToolbox if(!empty($pathHome)) { $token = $home = rtrim($pathHome, '/'); - foreach($this->getDirectoryEntries($path, "/.*/", true, true, false) as $entry) + foreach($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false) as $entry) { if(empty($firstHome)) { $firstHome = $token = $entry; } if($this->normaliseName($entry) == $home) { $token = $entry; break; } @@ -1784,281 +2037,219 @@ class YellowToolbox } return array($pathRoot, $pathHome); } + + // Return directory location + function getDirectoryLocation($location) + { + return ($pos = strrposu($location, '/')) ? substru($location, 0, $pos+1) : "/"; + } - // Return root languages - function findRootLanguages($pathBase, $pathRoot, $includeInvisible = false) + // Check if location is specifying root + function isRootLocation($location) { - $languages = array(); - if(!empty($pathRoot)) - { - foreach($this->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) - { - if($includeInvisible || preg_match("/^[\d\-\_\.]+(.*)$/", $entry)) - { - $token = $this->normaliseName($entry); - if(strlenu($token) == 2) array_push($languages, $token); - } - } - } - return $languages; + return $location[0] != "/"; } - // Return root locations - function findRootLocations($pathBase, $pathRoot) + // Check if location is specifying file or directory + function isFileLocation($location) { - $locations = array("root/"); - if(!empty($pathRoot)) - { - $root = rtrim($pathRoot, '/'); - foreach($this->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) - { - $token = $this->normaliseName($entry); - if($token != $root) array_push($locations, "root/$token/"); - } - } - return $locations; + return substru($location, -1, 1) != "/"; } - // Return location from file path - function findLocationFromFile($fileName, $pathBase, $pathRoot, $pathHome, $fileDefault, $fileExtension) + // Check if location is visible + function isVisibleLocation($location, $fileName) { - $location = "/"; + $visible = true; + $pathBase = $this->yellow->config->get("contentDir"); if(substru($fileName, 0, strlenu($pathBase)) == $pathBase) { $fileName = substru($fileName, strlenu($pathBase)); $tokens = explode('/', $fileName); - if(!empty($pathRoot)) - { - $token = $this->normaliseName($tokens[0]).'/'; - if($token!=$pathRoot) $location .= $token; - array_shift($tokens); - } for($i=0; $i<count($tokens)-1; ++$i) { - $token = $this->normaliseName($tokens[$i]).'/'; - if($i || $token!=$pathHome) $location .= $token; + if(!preg_match("/^[\d\-\_\.]+(.*)$/", $tokens[$i])) { $visible = false; break; } } - $token = $this->normaliseName($tokens[$i]); - $fileFolder = $this->normaliseName($tokens[$i-1]).$fileExtension; - if($token!=$fileDefault && $token!=$fileFolder) $location .= $this->normaliseName($tokens[$i], true, true); - $extension = ($pos = strrposu($fileName, '.')) ? substru($fileName, $pos) : ""; - if($extension != $fileExtension) $invalid = true; } else { - $invalid = true; + $visible = false; } - if(defined("DEBUG") && DEBUG>=2) + return $visible; + } + + // Check if location is within current request + function isActiveLocation($location, $currentLocation) + { + if($location != $this->yellow->pages->getHomeLocation($currentLocation)) { - $debug = ($invalid ? "INVALID" : $location)." <- $pathBase$fileName"; - echo "YellowToolbox::findLocationFromFile $debug<br/>\n"; + $active = substru($currentLocation, 0, strlenu($location))==$location; + } else { + $active = $this->getDirectoryLocation($currentLocation)==$location; } - return $invalid ? "" : $location; + return $active; } - // Return file path from location - function findFileFromLocation($location, $pathBase, $pathRoot, $pathHome, $fileDefault, $fileExtension) + // Check if location is valid + function isValidLocation($location) { - $path = $pathBase; + $string = ""; $tokens = explode('/', $location); - if($this->isRootLocation($location)) + for($i=1; $i<count($tokens); ++$i) $string .= '/'.$this->normaliseName($tokens[$i]); + return $location == $string; + } +} + +// Yellow toolbox with helpers +class YellowToolbox +{ + // Return server scheme from current HTTP request + function getServerScheme() + { + $serverScheme = ""; + if(preg_match("/^HTTP\//", $_SERVER["SERVER_PROTOCOL"])) { - if(!empty($pathRoot)) - { - $token = (count($tokens) > 2) ? $tokens[1] : rtrim($pathRoot, '/'); - $path .= $this->findFileDirectory($path, $token, true, true, $found, $invalid); - } - } else { - if(!empty($pathRoot)) - { - if(count($tokens) > 2) - { - if($this->normaliseName($tokens[1]) == $this->normaliseName($pathRoot)) $invalid = true; - $path .= $this->findFileDirectory($path, $tokens[1], false, true, $found, $invalid); - if($found) array_shift($tokens); - } - if(!$found) $path .= $this->findFileDirectory($path, rtrim($pathRoot, '/'), true, true, $found, $invalid); - - } - if(count($tokens) > 2) + $secure = isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"]!="off"; + $serverScheme = $secure ? "https" : "http"; + } + return $serverScheme; + } + + // Return server name from current HTTP request + function getServerName() + { + return $_SERVER["SERVER_NAME"]; + } + + // Return server base from current HTTP request + function getServerBase() + { + $serverBase = ""; + if(preg_match("/^(.*)\//", $_SERVER["SCRIPT_NAME"], $matches)) $serverBase = $matches[1]; + return $serverBase; + } + + // Return location from current HTTP request + function getLocation() + { + $uri = $_SERVER["REQUEST_URI"]; + return rawurldecode(($pos = strposu($uri, '?')) ? substru($uri, 0, $pos) : $uri); + } + + // Return location from current HTTP request, remove unwanted path tokens + function getLocationClean() + { + $string = $this->getLocation(); + $location = ($string[0]=='/') ? '' : '/'; + for($pos=0; $pos<strlenb($string); ++$pos) + { + if($string[$pos] == '/') { - if($this->normaliseName($tokens[1]) == $this->normaliseName($pathHome)) $invalid = true; - for($i=1; $i<count($tokens)-1; ++$i) + if($string[$pos+1] == '/') continue; + if($string[$pos+1] == '.') { - $path .= $this->findFileDirectory($path, $tokens[$i], true, true, $found, $invalid); + $posNew = $pos+1; while($string[$posNew] == '.') ++$posNew; + if($string[$posNew]=='/' || $string[$posNew]=='') + { + $pos = $posNew-1; + continue; + } } - } else { - $i = 1; - $tokens[0] = rtrim($pathHome, '/'); - $path .= $this->findFileDirectory($path, $tokens[0], true, true, $found, $invalid); } - if(!empty($fileDefault) && !empty($fileExtension)) + $location .= $string[$pos]; + } + if(preg_match("/^(.*?\/)([^\/]+:.*)$/", $location, $matches)) + { + $_SERVER["LOCATION"] = $location = $matches[1]; + $_SERVER["LOCATION_ARGS"] = $matches[2]; + foreach(explode('/', $matches[2]) as $token) { - $fileFolder = $tokens[$i-1].$fileExtension; - if(!empty($tokens[$i])) - { - $token = $tokens[$i].$fileExtension; - if($token==$fileDefault || $token==$fileFolder) $invalid = true; - $path .= $this->findFileDirectory($path, $token, true, false, $found, $invalid); - } else { - $path .= $this->findFileDefault($path, $fileDefault, $fileFolder); - } - if(defined("DEBUG") && DEBUG>=2) + preg_match("/^(.*?):(.*)$/", $token, $matches); + if(!empty($matches[1]) && !strempty($matches[2])) { - $debug = "$location -> ".($invalid ? "INVALID" : $path); - echo "YellowToolbox::findFileFromLocation $debug<br/>\n"; + $matches[1] = strreplaceu(array("\x1c", "\x1d"), array('/', ':'), $matches[1]); + $matches[2] = strreplaceu(array("\x1c", "\x1d"), array('/', ':'), $matches[2]); + $_REQUEST[$matches[1]] = $matches[2]; } } } - return $invalid ? "" : $path; + if(!preg_match("/^HTTP\//", $_SERVER["SERVER_PROTOCOL"])) $_SERVER["SERVER_PROTOCOL"] = "HTTP/1.1"; + return $location; } - // Return static file path from location - function findStaticFileFromLocation($location, $path, $fileDefault) + // Return location arguments from current HTTP request + function getLocationArgs() { - $path = rtrim($path, '/').$location; - if(!$this->isFileLocation($location)) $path .= $fileDefault; - return $path; + return $_SERVER["LOCATION_ARGS"]; } - - // Return file or directory that matches token - function findFileDirectory($path, $token, $tokenFailback, $directory, &$found, &$invalid) + + // Return location arguments from current HTTP request, modify an argument + function getLocationArgsNew($arg, $pagination) { - if($this->normaliseName($token) != $token) $invalid = true; - if(!$invalid) + preg_match("/^(.*?):(.*)$/", $arg, $args); + foreach(explode('/', $_SERVER["LOCATION_ARGS"]) as $token) { - $regex = "/^[\d\-\_\.]*".strreplaceu('-', '.', $token)."$/"; - foreach($this->getDirectoryEntries($path, $regex, false, $directory, false) as $entry) + preg_match("/^(.*?):(.*)$/", $token, $matches); + if($matches[1] == $args[1]) { $matches[2] = $args[2]; $found = true; } + if(!empty($matches[1]) && !strempty($matches[2])) { - if($this->normaliseName($entry) == $token) { $token = $entry; $found = true; break; } + if(!empty($locationArgs)) $locationArgs .= '/'; + $locationArgs .= "$matches[1]:$matches[2]"; } } - if($directory) $token .= '/'; - return ($tokenFailback || $found) ? $token : ""; - } - - // Return default file in directory - function findFileDefault($path, $fileDefault, $fileFolder) - { - $token = $fileDefault; - if(!is_file($path."/".$fileDefault)) + if(!$found && !empty($args[1]) && !strempty($args[2])) { - $regex = "/^[\d\-\_\.]*($fileDefault|$fileFolder)$/"; - foreach($this->getDirectoryEntries($path, $regex, true, false, false) as $entry) - { - if($this->normaliseName($entry) == $fileDefault) { $token = $entry; break; } - if($this->normaliseName($entry) == $fileFolder) { $token = $entry; break; } - } + if(!empty($locationArgs)) $locationArgs .= '/'; + $locationArgs .= "$args[1]:$args[2]"; } - return $token; + if(!empty($locationArgs)) + { + if(!$this->isLocationArgsPagination($locationArgs, $pagination)) $locationArgs .= '/'; + $locationArgs = $this->normaliseArgs($locationArgs, false, false); + } + return $locationArgs; } - // Return children from location - function findChildrenFromLocation($location, $pathBase, $pathRoot, $pathHome, $fileDefault, $fileExtension) + // Return location arguments from current HTTP request, convert form into clean URL + function getLocationArgsCleanUrl($pagination) { - $fileNames = array(); - if(!$this->isFileLocation($location)) + foreach(array_merge($_GET, $_POST) as $key=>$value) { - $path = $this->findFileFromLocation($location, $pathBase, $pathRoot, $pathHome, "", ""); - foreach($this->getDirectoryEntries($path, "/.*/", true, true, false) as $entry) - { - $fileFolder = $this->normaliseName($entry).$fileExtension; - $token = $this->findFileDefault($path.$entry, $fileDefault, $fileFolder); - array_push($fileNames, $path.$entry."/".$token); - } - if(!$this->isRootLocation($location)) + if(!empty($key) && !strempty($value)) { - $fileFolder = $this->normaliseName(basename($path)).$fileExtension; - $regex = "/^.*\\".$fileExtension."$/"; - foreach($this->getDirectoryEntries($path, $regex, true, false, false) as $entry) - { - if($this->normaliseName($entry) == $fileDefault) continue; - if($this->normaliseName($entry) == $fileFolder) continue; - if($this->normaliseName($entry, true, true) == "") continue; - array_push($fileNames, $path.$entry); - } + if(!empty($locationArgs)) $locationArgs .= '/'; + $key = strreplaceu(array('/', ':'), array("\x1c", "\x1d"), $key); + $value = strreplaceu(array('/', ':'), array("\x1c", "\x1d"), $value); + $locationArgs .= "$key:$value"; } } - return $fileNames; - } - - // Return modification date from file path, Unix time - function findModifiedFromFile($fileName) - { - $modified = is_readable($fileName) ? filemtime($fileName) : 0; - if($modified == 0) + if(!empty($locationArgs)) { - $path = dirname($fileName); - $modified = is_readable($path) ? filemtime($path) : 0; + if(!$this->isLocationArgsPagination($locationArgs, $pagination)) $locationArgs .= '/'; + $locationArgs = $this->normaliseArgs($locationArgs, false, false); } - return $modified; + return $locationArgs; } - // Return theme/template name from file path - function findNameFromFile($fileName, $pathBase, $nameDefault, $fileExtension) - { - $name = ""; - if(preg_match("/^.*\/(.+?)$/", dirname($fileName), $matches)) $name = $this->normaliseName($matches[1]); - if(!is_file("$pathBase$name$fileExtension")) $name = $this->normaliseName($nameDefault); - return $name; - } - - // Return language from file path - function findLanguageFromFile($fileName, $pathBase, $pathRoot, $languageDefault) + // Check if location contains location arguments + function isLocationArgs($location) { - $language = $languageDefault; - if(!empty($pathRoot)) - { - $fileName = substru($fileName, strlenu($pathBase)); - if(preg_match("/^(.+?)\//", $fileName, $matches)) $name = $this->normaliseName($matches[1]); - if(strlenu($name) == 2) $language = $name; - } - return $language; + return preg_match("/[^\/]+:.*$/", $location); } - // Return file path for new page - function findFileNew($fileName, $pathBase, $fileNew, $fileDefault) + // Check if location contains pagination arguments + function isLocationArgsPagination($location, $pagination) { - if(preg_match("/^.*\/(.+?)$/", dirname($fileName), $matches)) $name = $this->normaliseName($matches[1]); - $fileName = strreplaceu("(.*)", $name, $pathBase.$fileNew); - if(!is_file($fileName)) - { - $name = $this->normaliseName($fileDefault, true, true); - $fileName = strreplaceu("(.*)", $name, $pathBase.$fileNew); - } - return $fileName; + return preg_match("/^(.*\/)?$pagination:.*$/", $location); } - - // Return file path from title - function findFileFromTitle($titlePrefix, $titleText, $fileName, $fileDefault, $fileExtension) + + // Check if clean URL is requested + function isRequestCleanUrl($location) { - preg_match("/^([\d\-\_\.]*)(.*)$/", $titlePrefix, $matches); - if(preg_match("/\d$/", $matches[1])) $matches[1] .= '-'; - $titleText = $this->normaliseName($titleText, false, false, true); - preg_match("/^([\d\-\_\.]*)(.*)$/", $matches[1].$titleText, $matches); - $fileNamePrefix = $matches[1]; - $fileNameText = empty($matches[2]) ? $fileDefault : $matches[2].$fileExtension; - return dirname($fileName)."/".$fileNamePrefix.$fileNameText; + return (isset($_GET["clean-url"]) || isset($_POST["clean-url"])) && substru($location, -1, 1)=="/"; } - - // Normalise location, make absolute location - function normaliseLocation($location, $pageBase, $pageLocation, $staticLocation = "", $filterStrict = true) + + // Check if unmodified since last HTTP request + function isRequestNotModified($lastModifiedFormatted) { - if(!preg_match("/^\w+:/", trim(html_entity_decode($location, ENT_QUOTES, "UTF-8")))) - { - if(empty($staticLocation) || !preg_match("#^$staticLocation#", $location)) - { - if(preg_match("/^\#/", $location)) - { - $location = $pageBase.$pageLocation.$location; - } else if(!preg_match("/^\//", $location)) { - $location = $this->getDirectoryLocation($pageBase.$pageLocation).$location; - } else if(!preg_match("#^$pageBase#", $location)) { - $location = $pageBase.$location; - } - } - } else { - if($filterStrict && !preg_match("/^(http|https|ftp|mailto):/", $location)) $location = "error-xss-filter"; - } - return $location; + return isset($_SERVER["HTTP_IF_MODIFIED_SINCE"]) && $_SERVER["HTTP_IF_MODIFIED_SINCE"]==$lastModifiedFormatted; } // Normalise location arguments @@ -2069,15 +2260,6 @@ class YellowToolbox return strreplaceu(array('%3A','%2F'), array(':','/'), rawurlencode($text)); } - // Normalise file/directory/other name - function normaliseName($text, $removePrefix = true, $removeExtension = false, $filterStrict = false) - { - if($removeExtension) $text = ($pos = strrposu($text, '.')) ? substru($text, 0, $pos) : $text; - if($removePrefix) if(preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches)) $text = $matches[1]; - if($filterStrict) $text = strreplaceu('.', '-', strtoloweru($text)); - return preg_replace("/[^\pL\d\-\_\.]/u", "-", rtrim($text, '/')); - } - // Normalise text into UTF-8 NFC function normaliseUnicode($text) { @@ -2136,31 +2318,14 @@ class YellowToolbox "jpg" => "image/jpeg", "png" => "image/png", "txt" => "text/plain", - "woff" => "application/font-woff"); + "woff" => "application/font-woff", + "xml" => "text/xml; charset=utf-8"); $contentType = "text/html; charset=utf-8"; - $extension = ($pos = strrposu($fileName, '.')) ? substru($fileName, $pos+1) : ""; + $extension = $this->getFileExtension($fileName); if(array_key_exists(strtoloweru($extension), $mimeTypes)) $contentType = $mimeTypes[$extension]; return $contentType; } - // Return URL - function getUrl($serverScheme, $serverName, $base, $location) - { - if(!preg_match("/^\w+:/", $location)) - { - $url = "$serverScheme://$serverName$base$location"; - } else { - $url = $location; - } - return $url; - } - - // Return directory location - function getDirectoryLocation($location) - { - return ($pos = strrposu($location, '/')) ? substru($location, 0, $pos+1) : "/"; - } - // Return files and directories function getDirectoryEntries($path, $regex = "/.*/", $sort = true, $directories = true, $includePath = true) { @@ -2230,6 +2395,24 @@ class YellowToolbox return is_readable($fileName) ? file_get_contents($fileName) : ""; } + // Return file extension + function getFileExtension($fileName) + { + return strtoloweru(($pos = strrposu($fileName, '.')) ? substru($fileName, $pos+1) : ""); + } + + // Return file modification date, Unix time + function getFileModified($fileName) + { + $modified = is_readable($fileName) ? filemtime($fileName) : 0; + if($modified == 0) + { + $path = dirname($fileName); + $modified = is_readable($path) ? filemtime($path) : 0; + } + return $modified; + } + // Create file function createFile($fileName, $fileData, $mkdir = false) { @@ -2282,7 +2465,17 @@ class YellowToolbox { return @unlink($fileName); } - + + // Return lines from text string + function getTextLines($text) + { + $lines = array(); + $split = preg_split("/(\R)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE); + for($i=0; $i<count($split)-1; $i+=2) array_push($lines, $split[$i].$split[$i+1]); + if($split[$i] != '') array_push($lines, $split[$i]); + return $lines; + } + // Return arguments from text string function getTextArgs($text, $optional = "-") { @@ -2366,16 +2559,6 @@ class YellowToolbox return implode(", ", array_slice(array_unique($tokens), 0, $keywordsMax)); } - // Return lines from text string - function getTextLines($text) - { - $lines = array(); - $split = preg_split("/(\R)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE); - for($i=0; $i<count($split)-1; $i+=2) array_push($lines, $split[$i].$split[$i+1]); - if($split[$i] != '') array_push($lines, $split[$i]); - return $lines; - } - // Create title from text string function createTextTitle($text) { @@ -2437,7 +2620,11 @@ class YellowToolbox $hashCalculated = ""; switch($algorithm) { - case "bcrypt": if(substrb($hash, 0, 4) == "$2y$") $hashCalculated = crypt($text, $hash); break; + case "bcrypt": if(substrb($hash, 0, 4)=="$2y$" || substrb($hash, 0, 4)=="$2a$") + { + $hashCalculated = crypt($text, $hash); + } + break; case "sha256": if(substrb($hash, 0, 4) == "$5y$") { $prefix = substrb($hash, 0, 4); diff --git a/system/plugins/markdown.php b/system/plugins/markdown.php @@ -0,0 +1,3329 @@ +<?php +// Copyright (c) 2013-2015 Datenstrom, http://datenstrom.se +// This file may be used and distributed under the terms of the public license. + +// Markdown plugin +class YellowMarkdown +{ + const Version = "0.5.1"; + var $yellow; //access to API + + // Handle initialisation + function onLoad($yellow) + { + $this->yellow = $yellow; + } + + // Handle page content parsing of raw format + function onParseContentRaw($page, $text) + { + $markdown = new YellowMarkdownParser($this->yellow, $page); + return $markdown->transform($text); + } +} + +// Markdown parser +class YellowMarkdownParser extends MarkdownExtraParser +{ + var $yellow; //access to API + var $page; //access to page + var $idAttributes; //id attributes + + function __construct($yellow, $page) + { + $this->yellow = $yellow; + $this->page = $page; + $this->idAttributes = array(); + $this->no_markup = $page->parserSafeMode; + $this->no_entities = $page->parserSafeMode; + $this->url_filter_func = function($url) use ($yellow, $page) + { + return $yellow->lookup->normaliseLocation($url, $page->base, $page->location, + $yellow->config->get("serverBase").$yellow->config->get("imageLocation"), + $page->parserSafeMode && $page->statusCode==200); + }; + parent::__construct(); + } + + // Transform text + function transform($text) + { + $text = preg_replace("/@pageRead/i", $this->page->get("pageRead"), $text); + $text = preg_replace("/@pageEdit/i", $this->page->get("pageEdit"), $text); + $text = preg_replace("/@pageError/i", $this->page->get("pageError"), $text); + return parent::transform($text); + } + + // Return unique id attribute + function getIdAttribute($text) + { + $text = $this->yellow->lookup->normaliseName($text, true, false, true); + $text = trim(preg_replace("/-+/", "-", $text), "-"); + if(is_null($this->idAttributes[$text])) + { + $this->idAttributes[$text] = $text; + $attr = " id=\"$text\""; + } + return $attr; + } + + // Handle links + function doAutoLinks($text) + { + $text = preg_replace_callback("/<(\w+:[^\'\">\s]+)>/", array(&$this, "_doAutoLinks_url_callback"), $text); + $text = preg_replace_callback("/<([\w\-\.]+@[\w\-\.]+)>/", array(&$this, "_doAutoLinks_email_callback"), $text); + $text = preg_replace_callback("/\[(\w+)\s+(.*?)\]/", array(&$this, "_doAutoLinks_shortcut_callback"), $text); + $text = preg_replace_callback("/\[\-\-(.*?)\-\-\]/", array(&$this, "_doAutoLinks_comment_callback"), $text); + $text = preg_replace_callback("/((http|https|ftp):\/\/\S+[^\'\"\,\.\;\:\s]+)/", array(&$this, "_doAutoLinks_url_callback"), $text); + $text = preg_replace_callback("/([\w\-\.]+@[\w\-\.]+\.[\w]{2,4})/", array(&$this, "_doAutoLinks_email_callback"), $text); + return $text; + } + + // Handle shortcuts + function _doAutoLinks_shortcut_callback($matches) + { + $text = preg_replace("/\s+/s", " ", $matches[2]); + $output = $this->page->parseContentBlock($matches[1], $text, true); + if(is_null($output)) $output = htmlspecialchars($matches[0], ENT_NOQUOTES); + return substr($output, 0, 4)=="<div" ? $this->hashBlock($output) : $this->hashPart($output); + } + + // Handle comments + function _doAutoLinks_comment_callback($matches) + { + $text = $matches[1]; + $output = "<!--".htmlspecialchars($text, ENT_NOQUOTES)."-->"; + if($text[0] == '-') $output = ""; + return $this->hashBlock($output); + } + + // Handle fenced code blocks + function _doFencedCodeBlocks_callback($matches) + { + $text = $matches[4]; + $output = $this->page->parseContentBlock($matches[2], $text, false); + if(is_null($output)) + { + $attr = $this->doExtraAttributes("pre", $dummy =& $matches[3]); + $output = "<pre$attr><code>".htmlspecialchars($text, ENT_NOQUOTES)."</code></pre>"; + } + return "\n\n".$this->hashBlock($output)."\n\n"; + } + + // Handle headers, text style + function _doHeaders_callback_setext($matches) + { + if ($matches[3] == '-' && preg_match('{^- }', $matches[1])) return $matches[0]; + $text = $matches[1]; + $level = $matches[3]{0} == '=' ? 1 : 2; + $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2]); + if(empty($attr) && $level>=2 && $level<=3) $attr = $this->getIdAttribute($text); + $output = "<h$level$attr>".$this->runSpanGamut($text)."</h$level>"; + return "\n".$this->hashBlock($output)."\n\n"; + } + + // Handle headers, atx style + function _doHeaders_callback_atx($matches) + { + $text = $matches[2]; + $level = strlen($matches[1]); + $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[3]); + if(empty($attr) && $level>=2 && $level<=3) $attr = $this->getIdAttribute($text); + $output = "<h$level$attr>".$this->runSpanGamut($text)."</h$level>"; + return "\n".$this->hashBlock($output)."\n\n"; + } + + // Handle inline links + function _doAnchors_inline_callback($matches) + { + $url = $matches[3]=="" ? $matches[4] : $matches[3]; + $text = $matches[2]; + $title = $matches[7]; + $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); + $output = "<a href=\"".$this->encodeURLAttribute($url)."\""; + if(!empty($title)) $output .= " title=\"".$this->encodeAttribute($title)."\""; + $output .= $attr; + $output .= ">".$this->runSpanGamut($text)."</a>"; + return $this->hashPart($output); + } + + // Handle inline images + function _doImages_inline_callback($matches) + { + $width = $height = 0; + $src = $matches[3]=="" ? $matches[4] : $matches[3]; + if(!preg_match("/^\w+:/", $src)) + { + list($width, $height) = $this->yellow->toolbox->detectImageInfo($this->yellow->config->get("imageDir").$src); + $src = $this->yellow->config->get("serverBase").$this->yellow->config->get("imageLocation").$src; + } + $alt = $matches[2]; + $title = $matches[7]=="" ? $matches[2] : $matches[7]; + $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]); + $output = "<img src=\"".$this->encodeURLAttribute($src)."\""; + if($width && $height) $output .= " width=\"$width\" height=\"$height\""; + if(!empty($alt)) $output .= " alt=\"".$this->encodeAttribute($alt)."\""; + if(!empty($title)) $output .= " title=\"".$this->encodeAttribute($title)."\""; + $output .= $attr; + $output .= $this->empty_element_suffix; + return $this->hashPart($output); + } +} + +// PHP Markdown Lib +// Copyright (c) 2004-2013 Michel Fortin +// <http://michelf.com/projects/php-markdown/> +// +// Original Markdown +// Copyright (c) 2004-2006 John Gruber +// <http://daringfireball.net/projects/markdown/> +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name "Markdown" nor the names of its contributors may +// be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// This software is provided by the copyright holders and contributors "as +// is" and any express or implied warranties, including, but not limited +// to, the implied warranties of merchantability and fitness for a +// particular purpose are disclaimed. In no event shall the copyright owner +// or contributors be liable for any direct, indirect, incidental, special, +// exemplary, or consequential damages (including, but not limited to, +// procurement of substitute goods or services; loss of use, data, or +// profits; or business interruption) however caused and on any theory of +// liability, whether in contract, strict liability, or tort (including +// negligence or otherwise) arising in any way out of the use of this +// software, even if advised of the possibility of such damage. + +class MarkdownParser { + + ### Version ### + + const MARKDOWNLIB_VERSION = "1.4.1"; + + ### Simple Function Interface ### + + public static function defaultTransform($text) { + # + # Initialize the parser and return the result of its transform method. + # This will work fine for derived classes too. + # + # Take parser class on which this function was called. + $parser_class = \get_called_class(); + + # try to take parser from the static parser list + static $parser_list; + $parser =& $parser_list[$parser_class]; + + # create the parser it not already set + if (!$parser) + $parser = new $parser_class; + + # Transform text using parser. + return $parser->transform($text); + } + + ### Configuration Variables ### + + # Change to ">" for HTML output. + public $empty_element_suffix = " />"; + public $tab_width = 4; + + # Change to `true` to disallow markup or entities. + public $no_markup = false; + public $no_entities = false; + + # Predefined urls and titles for reference links and images. + public $predef_urls = array(); + public $predef_titles = array(); + + # Optional filter function for URLs + public $url_filter_func = null; + + + ### Parser Implementation ### + + # Regex to match balanced [brackets]. + # Needed to insert a maximum bracked depth while converting to PHP. + protected $nested_brackets_depth = 6; + protected $nested_brackets_re; + + protected $nested_url_parenthesis_depth = 4; + protected $nested_url_parenthesis_re; + + # Table of hash values for escaped characters: + protected $escape_chars = '\`*_{}[]()>#+-.!'; + protected $escape_chars_re; + + + public function __construct() { + # + # Constructor function. Initialize appropriate member variables. + # + $this->_initDetab(); + $this->prepareItalicsAndBold(); + + $this->nested_brackets_re = + str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth). + str_repeat('\])*', $this->nested_brackets_depth); + + $this->nested_url_parenthesis_re = + str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth). + str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth); + + $this->escape_chars_re = '['.preg_quote($this->escape_chars).']'; + + # Sort document, block, and span gamut in ascendent priority order. + asort($this->document_gamut); + asort($this->block_gamut); + asort($this->span_gamut); + } + + + # Internal hashes used during transformation. + protected $urls = array(); + protected $titles = array(); + protected $html_hashes = array(); + + # Status flag to avoid invalid nesting. + protected $in_anchor = false; + + + protected function setup() { + # + # Called before the transformation process starts to setup parser + # states. + # + # Clear global hashes. + $this->urls = $this->predef_urls; + $this->titles = $this->predef_titles; + $this->html_hashes = array(); + + $this->in_anchor = false; + } + + protected function teardown() { + # + # Called after the transformation process to clear any variable + # which may be taking up memory unnecessarly. + # + $this->urls = array(); + $this->titles = array(); + $this->html_hashes = array(); + } + + + public function transform($text) { + # + # Main function. Performs some preprocessing on the input text + # and pass it through the document gamut. + # + $this->setup(); + + # Remove UTF-8 BOM and marker character in input, if present. + $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text); + + # Standardize line endings: + # DOS to Unix and Mac to Unix + $text = preg_replace('{\r\n?}', "\n", $text); + + # Make sure $text ends with a couple of newlines: + $text .= "\n\n"; + + # Convert all tabs to spaces. + $text = $this->detab($text); + + # Turn block-level HTML blocks into hash entries + $text = $this->hashHTMLBlocks($text); + + # Strip any lines consisting only of spaces and tabs. + # This makes subsequent regexen easier to write, because we can + # match consecutive blank lines with /\n+/ instead of something + # contorted like /[ ]*\n+/ . + $text = preg_replace('/^[ ]+$/m', '', $text); + + # Run document gamut methods. + foreach ($this->document_gamut as $method => $priority) { + $text = $this->$method($text); + } + + $this->teardown(); + + return $text . "\n"; + } + + protected $document_gamut = array( + # Strip link definitions, store in hashes. + "stripLinkDefinitions" => 20, + + "runBasicBlockGamut" => 30, + ); + + + protected function stripLinkDefinitions($text) { + # + # Strips link definitions from text, stores the URLs and titles in + # hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: ^[id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 + [ ]* + \n? # maybe *one* newline + [ ]* + (?: + <(.+?)> # url = $2 + | + (\S+?) # url = $3 + ) + [ ]* + \n? # maybe one newline + [ ]* + (?: + (?<=\s) # lookbehind for whitespace + ["(] + (.*?) # title = $4 + [")] + [ ]* + )? # title is optional + (?:\n+|\Z) + }xm', + array($this, '_stripLinkDefinitions_callback'), + $text); + return $text; + } + protected function _stripLinkDefinitions_callback($matches) { + $link_id = strtolower($matches[1]); + $url = $matches[2] == '' ? $matches[3] : $matches[2]; + $this->urls[$link_id] = $url; + $this->titles[$link_id] =& $matches[4]; + return ''; # String that will replace the block + } + + + protected function hashHTMLBlocks($text) { + if ($this->no_markup) return $text; + + $less_than_tab = $this->tab_width - 1; + + # Hashify HTML blocks: + # We only want to do this for block-level HTML tags, such as headers, + # lists, and tables. That's because we still want to wrap <p>s around + # "paragraphs" that are wrapped in non-block-level tags, such as anchors, + # phrase emphasis, and spans. The list of tags we're looking for is + # hard-coded: + # + # * List "a" is made of tags which can be both inline or block-level. + # These will be treated block-level when the start tag is alone on + # its line, otherwise they're not matched here and will be taken as + # inline later. + # * List "b" is made of tags which are always block-level; + # + $block_tags_a_re = 'ins|del'; + $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'. + 'script|noscript|style|form|fieldset|iframe|math|svg|'. + 'article|section|nav|aside|hgroup|header|footer|'. + 'figure'; + + # Regular expression for the content of a block tag. + $nested_tags_level = 4; + $attr = ' + (?> # optional tag attributes + \s # starts with whitespace + (?> + [^>"/]+ # text outside quotes + | + /+(?!>) # slash not followed by ">" + | + "[^"]*" # text inside double quotes (tolerate ">") + | + \'[^\']*\' # text inside single quotes (tolerate ">") + )* + )? + '; + $content = + str_repeat(' + (?> + [^<]+ # content without tag + | + <\2 # nested opening tag + '.$attr.' # attributes + (?> + /> + | + >', $nested_tags_level). # end of opening tag + '.*?'. # last level nested tag content + str_repeat(' + </\2\s*> # closing nested tag + ) + | + <(?!/\2\s*> # other tags with a different name + ) + )*', + $nested_tags_level); + $content2 = str_replace('\2', '\3', $content); + + # First, look for nested blocks, e.g.: + # <div> + # <div> + # tags for inner block must be indented. + # </div> + # </div> + # + # The outermost tags must start at the left margin for this to match, and + # the inner nested divs must be indented. + # We need to do this before the next, more liberal match, because the next + # match will start at the first `<div>` and stop at the first `</div>`. + $text = preg_replace_callback('{(?> + (?> + (?<=\n) # Starting on its own line + | # or + \A\n? # the at beginning of the doc + ) + ( # save in $1 + + # Match from `\n<tag>` to `</tag>\n`, handling nested tags + # in between. + + [ ]{0,'.$less_than_tab.'} + <('.$block_tags_b_re.')# start tag = $2 + '.$attr.'> # attributes followed by > and \n + '.$content.' # content, support nesting + </\2> # the matching end tag + [ ]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + + | # Special version for tags of group a. + + [ ]{0,'.$less_than_tab.'} + <('.$block_tags_a_re.')# start tag = $3 + '.$attr.'>[ ]*\n # attributes followed by > + '.$content2.' # content, support nesting + </\3> # the matching end tag + [ ]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + + | # Special case just for <hr />. It was easier to make a special + # case than to make the other regex more complicated. + + [ ]{0,'.$less_than_tab.'} + <(hr) # start tag = $2 + '.$attr.' # attributes + /?> # the matching end tag + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + | # Special case for standalone HTML comments: + + [ ]{0,'.$less_than_tab.'} + (?s: + <!-- .*? --> + ) + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + | # PHP and ASP-style processor instructions (<? and <%) + + [ ]{0,'.$less_than_tab.'} + (?s: + <([?%]) # $2 + .*? + \2> + ) + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + ) + )}Sxmi', + array($this, '_hashHTMLBlocks_callback'), + $text); + + return $text; + } + protected function _hashHTMLBlocks_callback($matches) { + $text = $matches[1]; + $key = $this->hashBlock($text); + return "\n\n$key\n\n"; + } + + + protected function hashPart($text, $boundary = 'X') { + # + # Called whenever a tag must be hashed when a function insert an atomic + # element in the text stream. Passing $text to through this function gives + # a unique text-token which will be reverted back when calling unhash. + # + # The $boundary argument specify what character should be used to surround + # the token. By convension, "B" is used for block elements that needs not + # to be wrapped into paragraph tags at the end, ":" is used for elements + # that are word separators and "X" is used in the general case. + # + # Swap back any tag hash found in $text so we do not have to `unhash` + # multiple times at the end. + $text = $this->unhash($text); + + # Then hash the block. + static $i = 0; + $key = "$boundary\x1A" . ++$i . $boundary; + $this->html_hashes[$key] = $text; + return $key; # String that will replace the tag. + } + + + protected function hashBlock($text) { + # + # Shortcut function for hashPart with block-level boundaries. + # + return $this->hashPart($text, 'B'); + } + + + protected $block_gamut = array( + # + # These are all the transformations that form block-level + # tags like paragraphs, headers, and list items. + # + "doHeaders" => 10, + "doHorizontalRules" => 20, + + "doLists" => 40, + "doCodeBlocks" => 50, + "doBlockQuotes" => 60, + ); + + protected function runBlockGamut($text) { + # + # Run block gamut tranformations. + # + # We need to escape raw HTML in Markdown source before doing anything + # else. This need to be done for each block, and not only at the + # begining in the Markdown function since hashed blocks can be part of + # list items and could have been indented. Indented blocks would have + # been seen as a code block in a previous pass of hashHTMLBlocks. + $text = $this->hashHTMLBlocks($text); + + return $this->runBasicBlockGamut($text); + } + + protected function runBasicBlockGamut($text) { + # + # Run block gamut tranformations, without hashing HTML blocks. This is + # useful when HTML blocks are known to be already hashed, like in the first + # whole-document pass. + # + foreach ($this->block_gamut as $method => $priority) { + $text = $this->$method($text); + } + + # Finally form paragraph and restore hashed blocks. + $text = $this->formParagraphs($text); + + return $text; + } + + + protected function doHorizontalRules($text) { + # Do Horizontal Rules: + return preg_replace( + '{ + ^[ ]{0,3} # Leading space + ([-*_]) # $1: First marker + (?> # Repeated marker group + [ ]{0,2} # Zero, one, or two spaces. + \1 # Marker character + ){2,} # Group repeated at least twice + [ ]* # Tailing spaces + $ # End of line. + }mx', + "\n".$this->hashBlock("<hr$this->empty_element_suffix")."\n", + $text); + } + + + protected $span_gamut = array( + # + # These are all the transformations that occur *within* block-level + # tags like paragraphs, headers, and list items. + # + # Process character escapes, code spans, and inline HTML + # in one shot. + "parseSpan" => -30, + + # Process anchor and image tags. Images must come first, + # because ![foo][f] looks like an anchor. + "doImages" => 10, + "doAnchors" => 20, + + # Make links out of things like `<http://example.com/>` + # Must come after doAnchors, because you can use < and > + # delimiters in inline links like [this](<url>). + "doAutoLinks" => 30, + "encodeAmpsAndAngles" => 40, + + "doItalicsAndBold" => 50, + "doHardBreaks" => 60, + ); + + protected function runSpanGamut($text) { + # + # Run span gamut tranformations. + # + foreach ($this->span_gamut as $method => $priority) { + $text = $this->$method($text); + } + + return $text; + } + + + protected function doHardBreaks($text) { + # Do hard breaks: + return preg_replace_callback('/ {2,}\n/', + array($this, '_doHardBreaks_callback'), $text); + } + protected function _doHardBreaks_callback($matches) { + return $this->hashPart("<br$this->empty_element_suffix\n"); + } + + + protected function doAnchors($text) { + # + # Turn Markdown link shortcuts into XHTML <a> tags. + # + if ($this->in_anchor) return $text; + $this->in_anchor = true; + + # + # First, handle reference-style links: [link text] [id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + ) + }xs', + array($this, '_doAnchors_reference_callback'), $text); + + # + # Next, inline-style links: [link text](url "optional title") + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + \( # literal paren + [ \n]* + (?: + <(.+?)> # href = $3 + | + ('.$this->nested_url_parenthesis_re.') # href = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # Title = $7 + \6 # matching quote + [ \n]* # ignore any spaces/tabs between closing quote and ) + )? # title is optional + \) + ) + }xs', + array($this, '_doAnchors_inline_callback'), $text); + + # + # Last, handle reference-style shortcuts: [link text] + # These must come last in case you've also got [link text][1] + # or [link text](/foo) + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ([^\[\]]+) # link text = $2; can\'t contain [ or ] + \] + ) + }xs', + array($this, '_doAnchors_reference_callback'), $text); + + $this->in_anchor = false; + return $text; + } + protected function _doAnchors_reference_callback($matches) { + $whole_match = $matches[1]; + $link_text = $matches[2]; + $link_id =& $matches[3]; + + if ($link_id == "") { + # for shortcut links like [this][] or [this]. + $link_id = $link_text; + } + + # lower-case and turn embedded newlines into spaces + $link_id = strtolower($link_id); + $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); + + if (isset($this->urls[$link_id])) { + $url = $this->urls[$link_id]; + $url = $this->encodeURLAttribute($url); + + $result = "<a href=\"$url\""; + if ( isset( $this->titles[$link_id] ) ) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text</a>"; + $result = $this->hashPart($result); + } + else { + $result = $whole_match; + } + return $result; + } + protected function _doAnchors_inline_callback($matches) { + $whole_match = $matches[1]; + $link_text = $this->runSpanGamut($matches[2]); + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + // if the URL was of the form <s p a c e s> it got caught by the HTML + // tag parser and hashed. Need to reverse the process before using the URL. + $unhashed = $this->unhash($url); + if ($unhashed != $url) + $url = preg_replace('/^<(.*)>$/', '\1', $unhashed); + + $url = $this->encodeURLAttribute($url); + + $result = "<a href=\"$url\""; + if (isset($title)) { + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text</a>"; + + return $this->hashPart($result); + } + + + protected function doImages($text) { + # + # Turn Markdown image shortcuts into <img> tags. + # + # + # First, handle reference-style labeled images: ![alt text][id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + + ) + }xs', + array($this, '_doImages_reference_callback'), $text); + + # + # Next, handle inline images: ![alt text](url "optional title") + # Don't forget: encode * and _ + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + \s? # One optional whitespace character + \( # literal paren + [ \n]* + (?: + <(\S*)> # src url = $3 + | + ('.$this->nested_url_parenthesis_re.') # src url = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # title = $7 + \6 # matching quote + [ \n]* + )? # title is optional + \) + ) + }xs', + array($this, '_doImages_inline_callback'), $text); + + return $text; + } + protected function _doImages_reference_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $link_id = strtolower($matches[3]); + + if ($link_id == "") { + $link_id = strtolower($alt_text); # for shortcut links like ![this][]. + } + + $alt_text = $this->encodeAttribute($alt_text); + if (isset($this->urls[$link_id])) { + $url = $this->encodeURLAttribute($this->urls[$link_id]); + $result = "<img src=\"$url\" alt=\"$alt_text\""; + if (isset($this->titles[$link_id])) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + $result .= $this->empty_element_suffix; + $result = $this->hashPart($result); + } + else { + # If there's no such link ID, leave intact: + $result = $whole_match; + } + + return $result; + } + protected function _doImages_inline_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + $alt_text = $this->encodeAttribute($alt_text); + $url = $this->encodeURLAttribute($url); + $result = "<img src=\"$url\" alt=\"$alt_text\""; + if (isset($title)) { + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; # $title already quoted + } + $result .= $this->empty_element_suffix; + + return $this->hashPart($result); + } + + + protected function doHeaders($text) { + # Setext-style headers: + # Header 1 + # ======== + # + # Header 2 + # -------- + # + $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', + array($this, '_doHeaders_callback_setext'), $text); + + # atx-style headers: + # # Header 1 + # ## Header 2 + # ## Header 2 with closing hashes ## + # ... + # ###### Header 6 + # + $text = preg_replace_callback('{ + ^(\#{1,6}) # $1 = string of #\'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #\'s (not counted) + \n+ + }xm', + array($this, '_doHeaders_callback_atx'), $text); + + return $text; + } + protected function _doHeaders_callback_setext($matches) { + # Terrible hack to check we haven't found an empty list item. + if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1])) + return $matches[0]; + + $level = $matches[2]{0} == '=' ? 1 : 2; + $block = "<h$level>".$this->runSpanGamut($matches[1])."</h$level>"; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + protected function _doHeaders_callback_atx($matches) { + $level = strlen($matches[1]); + $block = "<h$level>".$this->runSpanGamut($matches[2])."</h$level>"; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + + protected function doLists($text) { + # + # Form HTML ordered (numbered) and unordered (bulleted) lists. + # + $less_than_tab = $this->tab_width - 1; + + # Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[\.]'; + + $markers_relist = array( + $marker_ul_re => $marker_ol_re, + $marker_ol_re => $marker_ul_re, + ); + + foreach ($markers_relist as $marker_re => $other_marker_re) { + # Re-usable pattern to match any entirel ul or ol list: + $whole_list_re = ' + ( # $1 = whole list + ( # $2 + ([ ]{0,'.$less_than_tab.'}) # $3 = number of spaces + ('.$marker_re.') # $4 = first list item marker + [ ]+ + ) + (?s:.+?) + ( # $5 + \z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another list item marker + [ ]* + '.$marker_re.'[ ]+ + ) + | + (?= # Lookahead for another kind of list + \n + \3 # Must have the same indentation + '.$other_marker_re.'[ ]+ + ) + ) + ) + '; // mx + + # We use a different prefix before nested lists than top-level lists. + # See extended comment in _ProcessListItems(). + + if ($this->list_level) { + $text = preg_replace_callback('{ + ^ + '.$whole_list_re.' + }mx', + array($this, '_doLists_callback'), $text); + } + else { + $text = preg_replace_callback('{ + (?:(?<=\n)\n|\A\n?) # Must eat the newline + '.$whole_list_re.' + }mx', + array($this, '_doLists_callback'), $text); + } + } + + return $text; + } + protected function _doLists_callback($matches) { + # Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[\.]'; + $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; + + $list = $matches[1]; + $list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol"; + + $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re ); + + $list .= "\n"; + $result = $this->processListItems($list, $marker_any_re); + + $result = $this->hashBlock("<$list_type>\n" . $result . "</$list_type>"); + return "\n". $result ."\n\n"; + } + + protected $list_level = 0; + + protected function processListItems($list_str, $marker_any_re) { + # + # Process the contents of a single ordered or unordered list, splitting it + # into individual list items. + # + # The $this->list_level global keeps track of when we're inside a list. + # Each time we enter a list, we increment it; when we leave a list, + # we decrement. If it's zero, we're not in a list anymore. + # + # We do this because when we're not inside a list, we want to treat + # something like this: + # + # I recommend upgrading to version + # 8. Oops, now this line is treated + # as a sub-list. + # + # As a single paragraph, despite the fact that the second line starts + # with a digit-period-space sequence. + # + # Whereas when we're inside a list (or sub-list), that line will be + # treated as the start of a sub-list. What a kludge, huh? This is + # an aspect of Markdown's syntax that's hard to parse perfectly + # without resorting to mind-reading. Perhaps the solution is to + # change the syntax rules such that sub-lists must start with a + # starting cardinal number; e.g. "1." or "a.". + + $this->list_level++; + + # trim trailing blank lines: + $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); + + $list_str = preg_replace_callback('{ + (\n)? # leading line = $1 + (^[ ]*) # leading whitespace = $2 + ('.$marker_any_re.' # list marker and space = $3 + (?:[ ]+|(?=\n)) # space only required if item is not empty + ) + ((?s:.*?)) # list item text = $4 + (?:(\n+(?=\n))|\n) # tailing blank line = $5 + (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n)))) + }xm', + array($this, '_processListItems_callback'), $list_str); + + $this->list_level--; + return $list_str; + } + protected function _processListItems_callback($matches) { + $item = $matches[4]; + $leading_line =& $matches[1]; + $leading_space =& $matches[2]; + $marker_space = $matches[3]; + $tailing_blank_line =& $matches[5]; + + if ($leading_line || $tailing_blank_line || + preg_match('/\n{2,}/', $item)) + { + # Replace marker with the appropriate whitespace indentation + $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item; + $item = $this->runBlockGamut($this->outdent($item)."\n"); + } + else { + # Recursion for sub-lists: + $item = $this->doLists($this->outdent($item)); + $item = preg_replace('/\n+$/', '', $item); + $item = $this->runSpanGamut($item); + } + + return "<li>" . $item . "</li>\n"; + } + + + protected function doCodeBlocks($text) { + # + # Process Markdown `<pre><code>` blocks. + # + $text = preg_replace_callback('{ + (?:\n\n|\A\n?) + ( # $1 = the code block -- one or more lines, starting with a space/tab + (?> + [ ]{'.$this->tab_width.'} # Lines must start with a tab or a tab-width of spaces + .*\n+ + )+ + ) + ((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z) # Lookahead for non-space at line-start, or end of doc + }xm', + array($this, '_doCodeBlocks_callback'), $text); + + return $text; + } + protected function _doCodeBlocks_callback($matches) { + $codeblock = $matches[1]; + + $codeblock = $this->outdent($codeblock); + $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); + + # trim leading newlines and trailing newlines + $codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock); + + $codeblock = "<pre><code>$codeblock\n</code></pre>"; + return "\n\n".$this->hashBlock($codeblock)."\n\n"; + } + + + protected function makeCodeSpan($code) { + # + # Create a code span markup for $code. Called from handleSpanToken. + # + $code = htmlspecialchars(trim($code), ENT_NOQUOTES); + return $this->hashPart("<code>$code</code>"); + } + + + protected $em_relist = array( + '' => '(?:(?<!\*)\*(?!\*)|(?<!_)_(?!_))(?![\.,:;]?\s)', + '*' => '(?<![\s*])\*(?!\*)', + '_' => '(?<![\s_])_(?!_)', + ); + protected $strong_relist = array( + '' => '(?:(?<!\*)\*\*(?!\*)|(?<!_)__(?!_))(?![\.,:;]?\s)', + '**' => '(?<![\s*])\*\*(?!\*)', + '__' => '(?<![\s_])__(?!_)', + ); + protected $em_strong_relist = array( + '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<!_)___(?!_))(?![\.,:;]?\s)', + '***' => '(?<![\s*])\*\*\*(?!\*)', + '___' => '(?<![\s_])___(?!_)', + ); + protected $em_strong_prepared_relist; + + protected function prepareItalicsAndBold() { + # + # Prepare regular expressions for searching emphasis tokens in any + # context. + # + foreach ($this->em_relist as $em => $em_re) { + foreach ($this->strong_relist as $strong => $strong_re) { + # Construct list of allowed token expressions. + $token_relist = array(); + if (isset($this->em_strong_relist["$em$strong"])) { + $token_relist[] = $this->em_strong_relist["$em$strong"]; + } + $token_relist[] = $em_re; + $token_relist[] = $strong_re; + + # Construct master expression from list. + $token_re = '{('. implode('|', $token_relist) .')}'; + $this->em_strong_prepared_relist["$em$strong"] = $token_re; + } + } + } + + protected function doItalicsAndBold($text) { + $token_stack = array(''); + $text_stack = array(''); + $em = ''; + $strong = ''; + $tree_char_em = false; + + while (1) { + # + # Get prepared regular expression for seraching emphasis tokens + # in current context. + # + $token_re = $this->em_strong_prepared_relist["$em$strong"]; + + # + # Each loop iteration search for the next emphasis token. + # Each token is then passed to handleSpanToken. + # + $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + $text_stack[0] .= $parts[0]; + $token =& $parts[1]; + $text =& $parts[2]; + + if (empty($token)) { + # Reached end of text span: empty stack without emitting. + # any more emphasis. + while ($token_stack[0]) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + } + break; + } + + $token_len = strlen($token); + if ($tree_char_em) { + # Reached closing marker while inside a three-char emphasis. + if ($token_len == 3) { + # Three-char closing marker, close em and strong. + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<strong><em>$span</em></strong>"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + $strong = ''; + } else { + # Other closing marker: close one em or strong and + # change current token state to match the other + $token_stack[0] = str_repeat($token{0}, 3-$token_len); + $tag = $token_len == 2 ? "strong" : "em"; + $span = $text_stack[0]; + $span = $this->runSpanGamut($span); + $span = "<$tag>$span</$tag>"; + $text_stack[0] = $this->hashPart($span); + $$tag = ''; # $$tag stands for $em or $strong + } + $tree_char_em = false; + } else if ($token_len == 3) { + if ($em) { + # Reached closing marker for both em and strong. + # Closing strong marker: + for ($i = 0; $i < 2; ++$i) { + $shifted_token = array_shift($token_stack); + $tag = strlen($shifted_token) == 2 ? "strong" : "em"; + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<$tag>$span</$tag>"; + $text_stack[0] .= $this->hashPart($span); + $$tag = ''; # $$tag stands for $em or $strong + } + } else { + # Reached opening three-char emphasis marker. Push on token + # stack; will be handled by the special condition above. + $em = $token{0}; + $strong = "$em$em"; + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $tree_char_em = true; + } + } else if ($token_len == 2) { + if ($strong) { + # Unwind any dangling emphasis marker: + if (strlen($token_stack[0]) == 1) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + } + # Closing strong marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<strong>$span</strong>"; + $text_stack[0] .= $this->hashPart($span); + $strong = ''; + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $strong = $token; + } + } else { + # Here $token_len == 1 + if ($em) { + if (strlen($token_stack[0]) == 1) { + # Closing emphasis marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<em>$span</em>"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + } else { + $text_stack[0] .= $token; + } + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $em = $token; + } + } + } + return $text_stack[0]; + } + + + protected function doBlockQuotes($text) { + $text = preg_replace_callback('/ + ( # Wrap whole match in $1 + (?> + ^[ ]*>[ ]? # ">" at the start of a line + .+\n # rest of the first line + (.+\n)* # subsequent consecutive lines + \n* # blanks + )+ + ) + /xm', + array($this, '_doBlockQuotes_callback'), $text); + + return $text; + } + protected function _doBlockQuotes_callback($matches) { + $bq = $matches[1]; + # trim one level of quoting - trim whitespace-only lines + $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq); + $bq = $this->runBlockGamut($bq); # recurse + + $bq = preg_replace('/^/m', " ", $bq); + # These leading spaces cause problem with <pre> content, + # so we need to fix that: + $bq = preg_replace_callback('{(\s*<pre>.+?</pre>)}sx', + array($this, '_doBlockQuotes_callback2'), $bq); + + return "\n". $this->hashBlock("<blockquote>\n$bq\n</blockquote>")."\n\n"; + } + protected function _doBlockQuotes_callback2($matches) { + $pre = $matches[1]; + $pre = preg_replace('/^ /m', '', $pre); + return $pre; + } + + + protected function formParagraphs($text) { + # + # Params: + # $text - string to process with html <p> tags + # + # Strip leading and trailing lines: + $text = preg_replace('/\A\n+|\n+\z/', '', $text); + + $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); + + # + # Wrap <p> tags and unhashify HTML blocks + # + foreach ($grafs as $key => $value) { + if (!preg_match('/^B\x1A[0-9]+B$/', $value)) { + # Is a paragraph. + $value = $this->runSpanGamut($value); + $value = preg_replace('/^([ ]*)/', "<p>", $value); + $value .= "</p>"; + $grafs[$key] = $this->unhash($value); + } + else { + # Is a block. + # Modify elements of @grafs in-place... + $graf = $value; + $block = $this->html_hashes[$graf]; + $graf = $block; +// if (preg_match('{ +// \A +// ( # $1 = <div> tag +// <div \s+ +// [^>]* +// \b +// markdown\s*=\s* ([\'"]) # $2 = attr quote char +// 1 +// \2 +// [^>]* +// > +// ) +// ( # $3 = contents +// .* +// ) +// (</div>) # $4 = closing tag +// \z +// }xs', $block, $matches)) +// { +// list(, $div_open, , $div_content, $div_close) = $matches; +// +// # We can't call Markdown(), because that resets the hash; +// # that initialization code should be pulled into its own sub, though. +// $div_content = $this->hashHTMLBlocks($div_content); +// +// # Run document gamut methods on the content. +// foreach ($this->document_gamut as $method => $priority) { +// $div_content = $this->$method($div_content); +// } +// +// $div_open = preg_replace( +// '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open); +// +// $graf = $div_open . "\n" . $div_content . "\n" . $div_close; +// } + $grafs[$key] = $graf; + } + } + + return implode("\n\n", $grafs); + } + + + protected function encodeAttribute($text) { + # + # Encode text for a double-quoted HTML attribute. This function + # is *not* suitable for attributes enclosed in single quotes. + # + $text = $this->encodeAmpsAndAngles($text); + $text = str_replace('"', '&quot;', $text); + return $text; + } + + + protected function encodeURLAttribute($url, &$text = null) { + # + # Encode text for a double-quoted HTML attribute containing a URL, + # applying the URL filter if set. Also generates the textual + # representation for the URL (removing mailto: or tel:) storing it in $text. + # This function is *not* suitable for attributes enclosed in single quotes. + # + if ($this->url_filter_func) + $url = call_user_func($this->url_filter_func, $url); + + if (preg_match('{^mailto:}i', $url)) + $url = $this->encodeEntityObfuscatedAttribute($url, $text, 7); + else if (preg_match('{^tel:}i', $url)) + { + $url = $this->encodeAttribute($url); + $text = substr($url, 4); + } + else + { + $url = $this->encodeAttribute($url); + $text = $url; + } + + return $url; + } + + + protected function encodeAmpsAndAngles($text) { + # + # Smart processing for ampersands and angle brackets that need to + # be encoded. Valid character entities are left alone unless the + # no-entities mode is set. + # + if ($this->no_entities) { + $text = str_replace('&', '&amp;', $text); + } else { + # Ampersand-encoding based entirely on Nat Irons's Amputator + # MT plugin: <http://bumppo.net/projects/amputator/> + $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', + '&amp;', $text); + } + # Encode remaining <'s + $text = str_replace('<', '&lt;', $text); + + return $text; + } + + + protected function doAutoLinks($text) { + $text = preg_replace_callback('{<((https?|ftp|dict|tel):[^\'">\s]+)>}i', + array($this, '_doAutoLinks_url_callback'), $text); + + # Email addresses: <address@domain.foo> + $text = preg_replace_callback('{ + < + (?:mailto:)? + ( + (?: + [-!#$%&\'*+/=?^_`.{|}~\w\x80-\xFF]+ + | + ".*?" + ) + \@ + (?: + [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+ + | + \[[\d.a-fA-F:]+\] # IPv4 & IPv6 + ) + ) + > + }xi', + array($this, '_doAutoLinks_email_callback'), $text); + + return $text; + } + protected function _doAutoLinks_url_callback($matches) { + $url = $this->encodeURLAttribute($matches[1], $text); + $link = "<a href=\"$url\">$text</a>"; + return $this->hashPart($link); + } + protected function _doAutoLinks_email_callback($matches) { + $addr = $matches[1]; + $url = $this->encodeURLAttribute("mailto:$addr", $text); + $link = "<a href=\"$url\">$text</a>"; + return $this->hashPart($link); + } + + + protected function encodeEntityObfuscatedAttribute($text, &$tail = null, $head_length = 0) { + # + # Input: some text to obfuscate, e.g. "mailto:foo@example.com" + # + # Output: the same text but with most characters encoded as either a + # decimal or hex entity, in the hopes of foiling most address + # harvesting spam bots. E.g.: + # + # &#109;&#x61;&#105;&#x6c;&#116;&#x6f;&#58;&#x66;o&#111; + # &#x40;&#101;&#x78;&#97;&#x6d;&#112;&#x6c;&#101;&#46;&#x63;&#111; + # &#x6d; + # + # Note: the additional output $tail is assigned the same value as the + # ouput, minus the number of characters specified by $head_length. + # + # Based by a filter by Matthew Wickline, posted to BBEdit-Talk. + # With some optimizations by Milian Wolff. Forced encoding of HTML + # attribute special characters by Allan Odgaard. + # + if ($text == "") return $tail = ""; + + $chars = preg_split('/(?<!^)(?!$)/', $text); + $seed = (int)abs(crc32($text) / strlen($text)); # Deterministic seed. + + foreach ($chars as $key => $char) { + $ord = ord($char); + # Ignore non-ascii chars. + if ($ord < 128) { + $r = ($seed * (1 + $key)) % 100; # Pseudo-random function. + # roughly 10% raw, 45% hex, 45% dec + # '@' *must* be encoded. I insist. + # '"' and '>' have to be encoded inside the attribute + if ($r > 90 && strpos('@"&>', $char) === false) /* do nothing */; + else if ($r < 45) $chars[$key] = '&#x'.dechex($ord).';'; + else $chars[$key] = '&#'.$ord.';'; + } + } + + $text = implode('', $chars); + $tail = $head_length ? implode('', array_slice($chars, $head_length)) : $text; + + return $text; + } + + + protected function parseSpan($str) { + # + # Take the string $str and parse it into tokens, hashing embeded HTML, + # escaped characters and handling code spans. + # + $output = ''; + + $span_re = '{ + ( + \\\\'.$this->escape_chars_re.' + | + (?<![`\\\\]) + `+ # code span marker + '.( $this->no_markup ? '' : ' + | + <!-- .*? --> # comment + | + <\?.*?\?> | <%.*?%> # processing instruction + | + <[!$]?[-a-zA-Z0-9:_]+ # regular tags + (?> + \s + (?>[^"\'>]+|"[^"]*"|\'[^\']*\')* + )? + > + | + <[-a-zA-Z0-9:_]+\s*/> # xml-style empty tag + | + </[-a-zA-Z0-9:_]+\s*> # closing tag + ').' + ) + }xs'; + + while (1) { + # + # Each loop iteration seach for either the next tag, the next + # openning code span marker, or the next escaped character. + # Each token is then passed to handleSpanToken. + # + $parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE); + + # Create token from text preceding tag. + if ($parts[0] != "") { + $output .= $parts[0]; + } + + # Check if we reach the end. + if (isset($parts[1])) { + $output .= $this->handleSpanToken($parts[1], $parts[2]); + $str = $parts[2]; + } + else { + break; + } + } + + return $output; + } + + + protected function handleSpanToken($token, &$str) { + # + # Handle $token provided by parseSpan by determining its nature and + # returning the corresponding value that should replace it. + # + switch ($token{0}) { + case "\\": + return $this->hashPart("&#". ord($token{1}). ";"); + case "`": + # Search for end marker in remaining text. + if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm', + $str, $matches)) + { + $str = $matches[2]; + $codespan = $this->makeCodeSpan($matches[1]); + return $this->hashPart($codespan); + } + return $token; // return as text since no ending marker found. + default: + return $this->hashPart($token); + } + } + + + protected function outdent($text) { + # + # Remove one level of line-leading tabs or spaces + # + return preg_replace('/^(\t|[ ]{1,'.$this->tab_width.'})/m', '', $text); + } + + + # String length function for detab. `_initDetab` will create a function to + # hanlde UTF-8 if the default function does not exist. + protected $utf8_strlen = 'mb_strlen'; + + protected function detab($text) { + # + # Replace tabs with the appropriate amount of space. + # + # For each line we separate the line in blocks delemited by + # tab characters. Then we reconstruct every line by adding the + # appropriate number of space between each blocks. + + $text = preg_replace_callback('/^.*\t.*$/m', + array($this, '_detab_callback'), $text); + + return $text; + } + protected function _detab_callback($matches) { + $line = $matches[0]; + $strlen = $this->utf8_strlen; # strlen function for UTF-8. + + # Split in blocks. + $blocks = explode("\t", $line); + # Add each blocks to the line. + $line = $blocks[0]; + unset($blocks[0]); # Do not add first block twice. + foreach ($blocks as $block) { + # Calculate amount of space, insert spaces, insert block. + $amount = $this->tab_width - + $strlen($line, 'UTF-8') % $this->tab_width; + $line .= str_repeat(" ", $amount) . $block; + } + return $line; + } + protected function _initDetab() { + # + # Check for the availability of the function in the `utf8_strlen` property + # (initially `mb_strlen`). If the function is not available, create a + # function that will loosely count the number of UTF-8 characters with a + # regular expression. + # + if (function_exists($this->utf8_strlen)) return; + $this->utf8_strlen = create_function('$text', 'return preg_match_all( + "/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/", + $text, $m);'); + } + + + protected function unhash($text) { + # + # Swap back in all the tags hashed by _HashHTMLBlocks. + # + return preg_replace_callback('/(.)\x1A[0-9]+\1/', + array($this, '_unhash_callback'), $text); + } + protected function _unhash_callback($matches) { + return $this->html_hashes[$matches[0]]; + } + +} + +class MarkdownExtraParser extends MarkdownParser { + + ### Configuration Variables ### + + # Prefix for footnote ids. + public $fn_id_prefix = ""; + + # Optional title attribute for footnote links and backlinks. + public $fn_link_title = ""; + public $fn_backlink_title = ""; + + # Optional class attribute for footnote links and backlinks. + public $fn_link_class = "footnote-ref"; + public $fn_backlink_class = "footnote-backref"; + + # Class name for table cell alignment (%% replaced left/center/right) + # For instance: 'go-%%' becomes 'go-left' or 'go-right' or 'go-center' + # If empty, the align attribute is used instead of a class name. + public $table_align_class_tmpl = ''; + + # Optional class prefix for fenced code block. + public $code_class_prefix = ""; + # Class attribute for code blocks goes on the `code` tag; + # setting this to true will put attributes on the `pre` tag instead. + public $code_attr_on_pre = false; + + # Predefined abbreviations. + public $predef_abbr = array(); + + + ### Parser Implementation ### + + public function __construct() { + # + # Constructor function. Initialize the parser object. + # + # Add extra escapable characters before parent constructor + # initialize the table. + $this->escape_chars .= ':|'; + + # Insert extra document, block, and span transformations. + # Parent constructor will do the sorting. + $this->document_gamut += array( + "doFencedCodeBlocks" => 5, + "stripFootnotes" => 15, + "stripAbbreviations" => 25, + "appendFootnotes" => 50, + ); + $this->block_gamut += array( + "doFencedCodeBlocks" => 5, + "doTables" => 15, + "doDefLists" => 45, + ); + $this->span_gamut += array( + "doFootnotes" => 5, + "doAbbreviations" => 70, + ); + + parent::__construct(); + } + + + # Extra variables used during extra transformations. + protected $footnotes = array(); + protected $footnotes_ordered = array(); + protected $footnotes_ref_count = array(); + protected $footnotes_numbers = array(); + protected $abbr_desciptions = array(); + protected $abbr_word_re = ''; + + # Give the current footnote number. + protected $footnote_counter = 1; + + + protected function setup() { + # + # Setting up Extra-specific variables. + # + parent::setup(); + + $this->footnotes = array(); + $this->footnotes_ordered = array(); + $this->footnotes_ref_count = array(); + $this->footnotes_numbers = array(); + $this->abbr_desciptions = array(); + $this->abbr_word_re = ''; + $this->footnote_counter = 1; + + foreach ($this->predef_abbr as $abbr_word => $abbr_desc) { + if ($this->abbr_word_re) + $this->abbr_word_re .= '|'; + $this->abbr_word_re .= preg_quote($abbr_word); + $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); + } + } + + protected function teardown() { + # + # Clearing Extra-specific variables. + # + $this->footnotes = array(); + $this->footnotes_ordered = array(); + $this->footnotes_ref_count = array(); + $this->footnotes_numbers = array(); + $this->abbr_desciptions = array(); + $this->abbr_word_re = ''; + + parent::teardown(); + } + + + ### Extra Attribute Parser ### + + # Expression to use to catch attributes (includes the braces) + protected $id_class_attr_catch_re = '\{((?:[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,})[ ]*\}'; + # Expression to use when parsing in a context when no capture is desired + protected $id_class_attr_nocatch_re = '\{(?:[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,}[ ]*\}'; + + protected function doExtraAttributes($tag_name, $attr) { + # + # Parse attributes caught by the $this->id_class_attr_catch_re expression + # and return the HTML-formatted list of attributes. + # + # Currently supported attributes are .class and #id. + # + if (empty($attr)) return ""; + + # Split on components + preg_match_all('/[#.a-z][-_:a-zA-Z0-9=]+/', $attr, $matches); + $elements = $matches[0]; + + # handle classes and ids (only first id taken into account) + $classes = array(); + $attributes = array(); + $id = false; + foreach ($elements as $element) { + if ($element{0} == '.') { + $classes[] = substr($element, 1); + } else if ($element{0} == '#') { + if ($id === false) $id = substr($element, 1); + } else if (strpos($element, '=') > 0) { + $parts = explode('=', $element, 2); + $attributes[] = $parts[0] . '="' . $parts[1] . '"'; + } + } + + # compose attributes as string + $attr_str = ""; + if (!empty($id)) { + $attr_str .= ' id="'.$id.'"'; + } + if (!empty($classes)) { + $attr_str .= ' class="'.implode(" ", $classes).'"'; + } + if (!$this->no_markup && !empty($attributes)) { + $attr_str .= ' '.implode(" ", $attributes); + } + return $attr_str; + } + + + protected function stripLinkDefinitions($text) { + # + # Strips link definitions from text, stores the URLs and titles in + # hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: ^[id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 + [ ]* + \n? # maybe *one* newline + [ ]* + (?: + <(.+?)> # url = $2 + | + (\S+?) # url = $3 + ) + [ ]* + \n? # maybe one newline + [ ]* + (?: + (?<=\s) # lookbehind for whitespace + ["(] + (.*?) # title = $4 + [")] + [ ]* + )? # title is optional + (?:[ ]* '.$this->id_class_attr_catch_re.' )? # $5 = extra id & class attr + (?:\n+|\Z) + }xm', + array($this, '_stripLinkDefinitions_callback'), + $text); + return $text; + } + protected function _stripLinkDefinitions_callback($matches) { + $link_id = strtolower($matches[1]); + $url = $matches[2] == '' ? $matches[3] : $matches[2]; + $this->urls[$link_id] = $url; + $this->titles[$link_id] =& $matches[4]; + $this->ref_attr[$link_id] = $this->doExtraAttributes("", $dummy =& $matches[5]); + return ''; # String that will replace the block + } + + + ### HTML Block Parser ### + + # Tags that are always treated as block tags: + protected $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption|figure'; + + # Tags treated as block tags only if the opening tag is alone on its line: + protected $context_block_tags_re = 'script|noscript|style|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video'; + + # Tags where markdown="1" default to span mode: + protected $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address'; + + # Tags which must not have their contents modified, no matter where + # they appear: + protected $clean_tags_re = 'script|style|math|svg'; + + # Tags that do not need to be closed. + protected $auto_close_tags_re = 'hr|img|param|source|track'; + + + protected function hashHTMLBlocks($text) { + # + # Hashify HTML Blocks and "clean tags". + # + # We only want to do this for block-level HTML tags, such as headers, + # lists, and tables. That's because we still want to wrap <p>s around + # "paragraphs" that are wrapped in non-block-level tags, such as anchors, + # phrase emphasis, and spans. The list of tags we're looking for is + # hard-coded. + # + # This works by calling _HashHTMLBlocks_InMarkdown, which then calls + # _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1" + # attribute is found within a tag, _HashHTMLBlocks_InHTML calls back + # _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag. + # These two functions are calling each other. It's recursive! + # + if ($this->no_markup) return $text; + + # + # Call the HTML-in-Markdown hasher. + # + list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text); + + return $text; + } + protected function _hashHTMLBlocks_inMarkdown($text, $indent = 0, + $enclosing_tag_re = '', $span = false) + { + # + # Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags. + # + # * $indent is the number of space to be ignored when checking for code + # blocks. This is important because if we don't take the indent into + # account, something like this (which looks right) won't work as expected: + # + # <div> + # <div markdown="1"> + # Hello World. <-- Is this a Markdown code block or text? + # </div> <-- Is this a Markdown code block or a real tag? + # <div> + # + # If you don't like this, just don't indent the tag on which + # you apply the markdown="1" attribute. + # + # * If $enclosing_tag_re is not empty, stops at the first unmatched closing + # tag with that name. Nested tags supported. + # + # * If $span is true, text inside must treated as span. So any double + # newline will be replaced by a single newline so that it does not create + # paragraphs. + # + # Returns an array of that form: ( processed text , remaining text ) + # + if ($text === '') return array('', ''); + + # Regex to check for the presense of newlines around a block tag. + $newline_before_re = '/(?:^\n?|\n\n)*$/'; + $newline_after_re = + '{ + ^ # Start of text following the tag. + (?>[ ]*<!--.*?-->)? # Optional comment. + [ ]*\n # Must be followed by newline. + }xs'; + + # Regex to match any tag. + $block_tag_re = + '{ + ( # $2: Capture whole tag. + </? # Any opening or closing tag. + (?> # Tag name. + '.$this->block_tags_re.' | + '.$this->context_block_tags_re.' | + '.$this->clean_tags_re.' | + (?!\s)'.$enclosing_tag_re.' + ) + (?: + (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name. + (?> + ".*?" | # Double quotes (can contain `>`) + \'.*?\' | # Single quotes (can contain `>`) + .+? # Anything but quotes and `>`. + )*? + )? + > # End of tag. + | + <!-- .*? --> # HTML Comment + | + <\?.*?\?> | <%.*?%> # Processing instruction + | + <!\[CDATA\[.*?\]\]> # CData Block + '. ( !$span ? ' # If not in span. + | + # Indented code block + (?: ^[ ]*\n | ^ | \n[ ]*\n ) + [ ]{'.($indent+4).'}[^\n]* \n + (?> + (?: [ ]{'.($indent+4).'}[^\n]* | [ ]* ) \n + )* + | + # Fenced code block marker + (?<= ^ | \n ) + [ ]{0,'.($indent+3).'}(?:~{3,}|`{3,}) + [ ]* + (?: + \.?[-_:a-zA-Z0-9]+ # standalone class name + | + '.$this->id_class_attr_nocatch_re.' # extra attributes + )? + [ ]* + (?= \n ) + ' : '' ). ' # End (if not is span). + | + # Code span marker + # Note, this regex needs to go after backtick fenced + # code blocks but it should also be kept outside of the + # "if not in span" condition adding backticks to the parser + `+ + ) + }xs'; + + + $depth = 0; # Current depth inside the tag tree. + $parsed = ""; # Parsed text that will be returned. + + # + # Loop through every tag until we find the closing tag of the parent + # or loop until reaching the end of text if no parent tag specified. + # + do { + # + # Split the text using the first $tag_match pattern found. + # Text before pattern will be first in the array, text after + # pattern will be at the end, and between will be any catches made + # by the pattern. + # + $parts = preg_split($block_tag_re, $text, 2, + PREG_SPLIT_DELIM_CAPTURE); + + # If in Markdown span mode, add a empty-string span-level hash + # after each newline to prevent triggering any block element. + if ($span) { + $void = $this->hashPart("", ':'); + $newline = "$void\n"; + $parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void; + } + + $parsed .= $parts[0]; # Text before current tag. + + # If end of $text has been reached. Stop loop. + if (count($parts) < 3) { + $text = ""; + break; + } + + $tag = $parts[1]; # Tag to handle. + $text = $parts[2]; # Remaining text after current tag. + $tag_re = preg_quote($tag); # For use in a regular expression. + + # + # Check for: Fenced code block marker. + # Note: need to recheck the whole tag to disambiguate backtick + # fences from code spans + # + if (preg_match('{^\n?([ ]{0,'.($indent+3).'})(~{3,}|`{3,})[ ]*(?:\.?[-_:a-zA-Z0-9]+|'.$this->id_class_attr_nocatch_re.')?[ ]*\n?$}', $tag, $capture)) { + # Fenced code block marker: find matching end marker. + $fence_indent = strlen($capture[1]); # use captured indent in re + $fence_re = $capture[2]; # use captured fence in re + if (preg_match('{^(?>.*\n)*?[ ]{'.($fence_indent).'}'.$fence_re.'[ ]*(?:\n|$)}', $text, + $matches)) + { + # End marker found: pass text unchanged until marker. + $parsed .= $tag . $matches[0]; + $text = substr($text, strlen($matches[0])); + } + else { + # No end marker: just skip it. + $parsed .= $tag; + } + } + # + # Check for: Indented code block. + # + else if ($tag{0} == "\n" || $tag{0} == " ") { + # Indented code block: pass it unchanged, will be handled + # later. + $parsed .= $tag; + } + # + # Check for: Code span marker + # Note: need to check this after backtick fenced code blocks + # + else if ($tag{0} == "`") { + # Find corresponding end marker. + $tag_re = preg_quote($tag); + if (preg_match('{^(?>.+?|\n(?!\n))*?(?<!`)'.$tag_re.'(?!`)}', + $text, $matches)) + { + # End marker found: pass text unchanged until marker. + $parsed .= $tag . $matches[0]; + $text = substr($text, strlen($matches[0])); + } + else { + # Unmatched marker: just skip it. + $parsed .= $tag; + } + } + # + # Check for: Opening Block level tag or + # Opening Context Block tag (like ins and del) + # used as a block tag (tag is alone on it's line). + # + else if (preg_match('{^<(?:'.$this->block_tags_re.')\b}', $tag) || + ( preg_match('{^<(?:'.$this->context_block_tags_re.')\b}', $tag) && + preg_match($newline_before_re, $parsed) && + preg_match($newline_after_re, $text) ) + ) + { + # Need to parse tag and following text using the HTML parser. + list($block_text, $text) = + $this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true); + + # Make sure it stays outside of any paragraph by adding newlines. + $parsed .= "\n\n$block_text\n\n"; + } + # + # Check for: Clean tag (like script, math) + # HTML Comments, processing instructions. + # + else if (preg_match('{^<(?:'.$this->clean_tags_re.')\b}', $tag) || + $tag{1} == '!' || $tag{1} == '?') + { + # Need to parse tag and following text using the HTML parser. + # (don't check for markdown attribute) + list($block_text, $text) = + $this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false); + + $parsed .= $block_text; + } + # + # Check for: Tag with same name as enclosing tag. + # + else if ($enclosing_tag_re !== '' && + # Same name as enclosing tag. + preg_match('{^</?(?:'.$enclosing_tag_re.')\b}', $tag)) + { + # + # Increase/decrease nested tag count. + # + if ($tag{1} == '/') $depth--; + else if ($tag{strlen($tag)-2} != '/') $depth++; + + if ($depth < 0) { + # + # Going out of parent element. Clean up and break so we + # return to the calling function. + # + $text = $tag . $text; + break; + } + + $parsed .= $tag; + } + else { + $parsed .= $tag; + } + } while ($depth >= 0); + + return array($parsed, $text); + } + protected function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) { + # + # Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags. + # + # * Calls $hash_method to convert any blocks. + # * Stops when the first opening tag closes. + # * $md_attr indicate if the use of the `markdown="1"` attribute is allowed. + # (it is not inside clean tags) + # + # Returns an array of that form: ( processed text , remaining text ) + # + if ($text === '') return array('', ''); + + # Regex to match `markdown` attribute inside of a tag. + $markdown_attr_re = ' + { + \s* # Eat whitespace before the `markdown` attribute + markdown + \s*=\s* + (?> + (["\']) # $1: quote delimiter + (.*?) # $2: attribute value + \1 # matching delimiter + | + ([^\s>]*) # $3: unquoted attribute value + ) + () # $4: make $3 always defined (avoid warnings) + }xs'; + + # Regex to match any tag. + $tag_re = '{ + ( # $2: Capture whole tag. + </? # Any opening or closing tag. + [\w:$]+ # Tag name. + (?: + (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name. + (?> + ".*?" | # Double quotes (can contain `>`) + \'.*?\' | # Single quotes (can contain `>`) + .+? # Anything but quotes and `>`. + )*? + )? + > # End of tag. + | + <!-- .*? --> # HTML Comment + | + <\?.*?\?> | <%.*?%> # Processing instruction + | + <!\[CDATA\[.*?\]\]> # CData Block + ) + }xs'; + + $original_text = $text; # Save original text in case of faliure. + + $depth = 0; # Current depth inside the tag tree. + $block_text = ""; # Temporary text holder for current text. + $parsed = ""; # Parsed text that will be returned. + + # + # Get the name of the starting tag. + # (This pattern makes $base_tag_name_re safe without quoting.) + # + if (preg_match('/^<([\w:$]*)\b/', $text, $matches)) + $base_tag_name_re = $matches[1]; + + # + # Loop through every tag until we find the corresponding closing tag. + # + do { + # + # Split the text using the first $tag_match pattern found. + # Text before pattern will be first in the array, text after + # pattern will be at the end, and between will be any catches made + # by the pattern. + # + $parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + + if (count($parts) < 3) { + # + # End of $text reached with unbalenced tag(s). + # In that case, we return original text unchanged and pass the + # first character as filtered to prevent an infinite loop in the + # parent function. + # + return array($original_text{0}, substr($original_text, 1)); + } + + $block_text .= $parts[0]; # Text before current tag. + $tag = $parts[1]; # Tag to handle. + $text = $parts[2]; # Remaining text after current tag. + + # + # Check for: Auto-close tag (like <hr/>) + # Comments and Processing Instructions. + # + if (preg_match('{^</?(?:'.$this->auto_close_tags_re.')\b}', $tag) || + $tag{1} == '!' || $tag{1} == '?') + { + # Just add the tag to the block as if it was text. + $block_text .= $tag; + } + else { + # + # Increase/decrease nested tag count. Only do so if + # the tag's name match base tag's. + # + if (preg_match('{^</?'.$base_tag_name_re.'\b}', $tag)) { + if ($tag{1} == '/') $depth--; + else if ($tag{strlen($tag)-2} != '/') $depth++; + } + + # + # Check for `markdown="1"` attribute and handle it. + # + if ($md_attr && + preg_match($markdown_attr_re, $tag, $attr_m) && + preg_match('/^1|block|span$/', $attr_m[2] . $attr_m[3])) + { + # Remove `markdown` attribute from opening tag. + $tag = preg_replace($markdown_attr_re, '', $tag); + + # Check if text inside this tag must be parsed in span mode. + $this->mode = $attr_m[2] . $attr_m[3]; + $span_mode = $this->mode == 'span' || $this->mode != 'block' && + preg_match('{^<(?:'.$this->contain_span_tags_re.')\b}', $tag); + + # Calculate indent before tag. + if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) { + $strlen = $this->utf8_strlen; + $indent = $strlen($matches[1], 'UTF-8'); + } else { + $indent = 0; + } + + # End preceding block with this tag. + $block_text .= $tag; + $parsed .= $this->$hash_method($block_text); + + # Get enclosing tag name for the ParseMarkdown function. + # (This pattern makes $tag_name_re safe without quoting.) + preg_match('/^<([\w:$]*)\b/', $tag, $matches); + $tag_name_re = $matches[1]; + + # Parse the content using the HTML-in-Markdown parser. + list ($block_text, $text) + = $this->_hashHTMLBlocks_inMarkdown($text, $indent, + $tag_name_re, $span_mode); + + # Outdent markdown text. + if ($indent > 0) { + $block_text = preg_replace("/^[ ]{1,$indent}/m", "", + $block_text); + } + + # Append tag content to parsed text. + if (!$span_mode) $parsed .= "\n\n$block_text\n\n"; + else $parsed .= "$block_text"; + + # Start over with a new block. + $block_text = ""; + } + else $block_text .= $tag; + } + + } while ($depth > 0); + + # + # Hash last block text that wasn't processed inside the loop. + # + $parsed .= $this->$hash_method($block_text); + + return array($parsed, $text); + } + + + protected function hashClean($text) { + # + # Called whenever a tag must be hashed when a function inserts a "clean" tag + # in $text, it passes through this function and is automaticaly escaped, + # blocking invalid nested overlap. + # + return $this->hashPart($text, 'C'); + } + + + protected function doAnchors($text) { + # + # Turn Markdown link shortcuts into XHTML <a> tags. + # + if ($this->in_anchor) return $text; + $this->in_anchor = true; + + # + # First, handle reference-style links: [link text] [id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + ) + }xs', + array($this, '_doAnchors_reference_callback'), $text); + + # + # Next, inline-style links: [link text](url "optional title") + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + \( # literal paren + [ \n]* + (?: + <(.+?)> # href = $3 + | + ('.$this->nested_url_parenthesis_re.') # href = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # Title = $7 + \6 # matching quote + [ \n]* # ignore any spaces/tabs between closing quote and ) + )? # title is optional + \) + (?:[ ]? '.$this->id_class_attr_catch_re.' )? # $8 = id/class attributes + ) + }xs', + array($this, '_doAnchors_inline_callback'), $text); + + # + # Last, handle reference-style shortcuts: [link text] + # These must come last in case you've also got [link text][1] + # or [link text](/foo) + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ([^\[\]]+) # link text = $2; can\'t contain [ or ] + \] + ) + }xs', + array($this, '_doAnchors_reference_callback'), $text); + + $this->in_anchor = false; + return $text; + } + protected function _doAnchors_reference_callback($matches) { + $whole_match = $matches[1]; + $link_text = $matches[2]; + $link_id =& $matches[3]; + + if ($link_id == "") { + # for shortcut links like [this][] or [this]. + $link_id = $link_text; + } + + # lower-case and turn embedded newlines into spaces + $link_id = strtolower($link_id); + $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); + + if (isset($this->urls[$link_id])) { + $url = $this->urls[$link_id]; + $url = $this->encodeURLAttribute($url); + + $result = "<a href=\"$url\""; + if ( isset( $this->titles[$link_id] ) ) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + if (isset($this->ref_attr[$link_id])) + $result .= $this->ref_attr[$link_id]; + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text</a>"; + $result = $this->hashPart($result); + } + else { + $result = $whole_match; + } + return $result; + } + protected function _doAnchors_inline_callback($matches) { + $whole_match = $matches[1]; + $link_text = $this->runSpanGamut($matches[2]); + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); + + // if the URL was of the form <s p a c e s> it got caught by the HTML + // tag parser and hashed. Need to reverse the process before using the URL. + $unhashed = $this->unhash($url); + if ($unhashed != $url) + $url = preg_replace('/^<(.*)>$/', '\1', $unhashed); + + $url = $this->encodeURLAttribute($url); + + $result = "<a href=\"$url\""; + if (isset($title)) { + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + $result .= $attr; + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text</a>"; + + return $this->hashPart($result); + } + + + protected function doImages($text) { + # + # Turn Markdown image shortcuts into <img> tags. + # + # + # First, handle reference-style labeled images: ![alt text][id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + + ) + }xs', + array($this, '_doImages_reference_callback'), $text); + + # + # Next, handle inline images: ![alt text](url "optional title") + # Don't forget: encode * and _ + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + \s? # One optional whitespace character + \( # literal paren + [ \n]* + (?: + <(\S*)> # src url = $3 + | + ('.$this->nested_url_parenthesis_re.') # src url = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # title = $7 + \6 # matching quote + [ \n]* + )? # title is optional + \) + (?:[ ]? '.$this->id_class_attr_catch_re.' )? # $8 = id/class attributes + ) + }xs', + array($this, '_doImages_inline_callback'), $text); + + return $text; + } + protected function _doImages_reference_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $link_id = strtolower($matches[3]); + + if ($link_id == "") { + $link_id = strtolower($alt_text); # for shortcut links like ![this][]. + } + + $alt_text = $this->encodeAttribute($alt_text); + if (isset($this->urls[$link_id])) { + $url = $this->encodeURLAttribute($this->urls[$link_id]); + $result = "<img src=\"$url\" alt=\"$alt_text\""; + if (isset($this->titles[$link_id])) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + if (isset($this->ref_attr[$link_id])) + $result .= $this->ref_attr[$link_id]; + $result .= $this->empty_element_suffix; + $result = $this->hashPart($result); + } + else { + # If there's no such link ID, leave intact: + $result = $whole_match; + } + + return $result; + } + protected function _doImages_inline_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]); + + $alt_text = $this->encodeAttribute($alt_text); + $url = $this->encodeURLAttribute($url); + $result = "<img src=\"$url\" alt=\"$alt_text\""; + if (isset($title)) { + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; # $title already quoted + } + $result .= $attr; + $result .= $this->empty_element_suffix; + + return $this->hashPart($result); + } + + + protected function doHeaders($text) { + # + # Redefined to add id and class attribute support. + # + # Setext-style headers: + # Header 1 {#header1} + # ======== + # + # Header 2 {#header2 .class1 .class2} + # -------- + # + $text = preg_replace_callback( + '{ + (^.+?) # $1: Header text + (?:[ ]+ '.$this->id_class_attr_catch_re.' )? # $3 = id/class attributes + [ ]*\n(=+|-+)[ ]*\n+ # $3: Header footer + }mx', + array($this, '_doHeaders_callback_setext'), $text); + + # atx-style headers: + # # Header 1 {#header1} + # ## Header 2 {#header2} + # ## Header 2 with closing hashes ## {#header3.class1.class2} + # ... + # ###### Header 6 {.class2} + # + $text = preg_replace_callback('{ + ^(\#{1,6}) # $1 = string of #\'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #\'s (not counted) + (?:[ ]+ '.$this->id_class_attr_catch_re.' )? # $3 = id/class attributes + [ ]* + \n+ + }xm', + array($this, '_doHeaders_callback_atx'), $text); + + return $text; + } + protected function _doHeaders_callback_setext($matches) { + if ($matches[3] == '-' && preg_match('{^- }', $matches[1])) + return $matches[0]; + $level = $matches[3]{0} == '=' ? 1 : 2; + $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2]); + $block = "<h$level$attr>".$this->runSpanGamut($matches[1])."</h$level>"; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + protected function _doHeaders_callback_atx($matches) { + $level = strlen($matches[1]); + $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[3]); + $block = "<h$level$attr>".$this->runSpanGamut($matches[2])."</h$level>"; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + + protected function doTables($text) { + # + # Form HTML tables. + # + $less_than_tab = $this->tab_width - 1; + # + # Find tables with leading pipe. + # + # | Header 1 | Header 2 + # | -------- | -------- + # | Cell 1 | Cell 2 + # | Cell 3 | Cell 4 + # + $text = preg_replace_callback(' + { + ^ # Start of a line + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + [|] # Optional leading pipe (present) + (.+) \n # $1: Header row (at least one pipe) + + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + [|] ([ ]*[-:]+[-| :]*) \n # $2: Header underline + + ( # $3: Cells + (?> + [ ]* # Allowed whitespace. + [|] .* \n # Row content. + )* + ) + (?=\n|\Z) # Stop at final double newline. + }xm', + array($this, '_doTable_leadingPipe_callback'), $text); + + # + # Find tables without leading pipe. + # + # Header 1 | Header 2 + # -------- | -------- + # Cell 1 | Cell 2 + # Cell 3 | Cell 4 + # + $text = preg_replace_callback(' + { + ^ # Start of a line + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + (\S.*[|].*) \n # $1: Header row (at least one pipe) + + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + ([-:]+[ ]*[|][-| :]*) \n # $2: Header underline + + ( # $3: Cells + (?> + .* [|] .* \n # Row content + )* + ) + (?=\n|\Z) # Stop at final double newline. + }xm', + array($this, '_DoTable_callback'), $text); + + return $text; + } + protected function _doTable_leadingPipe_callback($matches) { + $head = $matches[1]; + $underline = $matches[2]; + $content = $matches[3]; + + # Remove leading pipe for each row. + $content = preg_replace('/^ *[|]/m', '', $content); + + return $this->_doTable_callback(array($matches[0], $head, $underline, $content)); + } + protected function _doTable_makeAlignAttr($alignname) + { + if (empty($this->table_align_class_tmpl)) + return " align=\"$alignname\""; + + $classname = str_replace('%%', $alignname, $this->table_align_class_tmpl); + return " class=\"$classname\""; + } + protected function _doTable_callback($matches) { + $head = $matches[1]; + $underline = $matches[2]; + $content = $matches[3]; + + # Remove any tailing pipes for each line. + $head = preg_replace('/[|] *$/m', '', $head); + $underline = preg_replace('/[|] *$/m', '', $underline); + $content = preg_replace('/[|] *$/m', '', $content); + + # Reading alignement from header underline. + $separators = preg_split('/ *[|] */', $underline); + foreach ($separators as $n => $s) { + if (preg_match('/^ *-+: *$/', $s)) + $attr[$n] = $this->_doTable_makeAlignAttr('right'); + else if (preg_match('/^ *:-+: *$/', $s)) + $attr[$n] = $this->_doTable_makeAlignAttr('center'); + else if (preg_match('/^ *:-+ *$/', $s)) + $attr[$n] = $this->_doTable_makeAlignAttr('left'); + else + $attr[$n] = ''; + } + + # Parsing span elements, including code spans, character escapes, + # and inline HTML tags, so that pipes inside those gets ignored. + $head = $this->parseSpan($head); + $headers = preg_split('/ *[|] */', $head); + $col_count = count($headers); + $attr = array_pad($attr, $col_count, ''); + + # Write column headers. + $text = "<table>\n"; + $text .= "<thead>\n"; + $text .= "<tr>\n"; + foreach ($headers as $n => $header) + $text .= " <th$attr[$n]>".$this->runSpanGamut(trim($header))."</th>\n"; + $text .= "</tr>\n"; + $text .= "</thead>\n"; + + # Split content by row. + $rows = explode("\n", trim($content, "\n")); + + $text .= "<tbody>\n"; + foreach ($rows as $row) { + # Parsing span elements, including code spans, character escapes, + # and inline HTML tags, so that pipes inside those gets ignored. + $row = $this->parseSpan($row); + + # Split row by cell. + $row_cells = preg_split('/ *[|] */', $row, $col_count); + $row_cells = array_pad($row_cells, $col_count, ''); + + $text .= "<tr>\n"; + foreach ($row_cells as $n => $cell) + $text .= " <td$attr[$n]>".$this->runSpanGamut(trim($cell))."</td>\n"; + $text .= "</tr>\n"; + } + $text .= "</tbody>\n"; + $text .= "</table>"; + + return $this->hashBlock($text) . "\n"; + } + + + protected function doDefLists($text) { + # + # Form HTML definition lists. + # + $less_than_tab = $this->tab_width - 1; + + # Re-usable pattern to match any entire dl list: + $whole_list_re = '(?> + ( # $1 = whole list + ( # $2 + [ ]{0,'.$less_than_tab.'} + ((?>.*\S.*\n)+) # $3 = defined term + \n? + [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition + ) + (?s:.+?) + ( # $4 + \z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another term + [ ]{0,'.$less_than_tab.'} + (?: \S.*\n )+? # defined term + \n? + [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition + ) + (?! # Negative lookahead for another definition + [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition + ) + ) + ) + )'; // mx + + $text = preg_replace_callback('{ + (?>\A\n?|(?<=\n\n)) + '.$whole_list_re.' + }mx', + array($this, '_doDefLists_callback'), $text); + + return $text; + } + protected function _doDefLists_callback($matches) { + # Re-usable patterns to match list item bullets and number markers: + $list = $matches[1]; + + # Turn double returns into triple returns, so that we can make a + # paragraph for the last item in a list, if necessary: + $result = trim($this->processDefListItems($list)); + $result = "<dl>\n" . $result . "\n</dl>"; + return $this->hashBlock($result) . "\n\n"; + } + + + protected function processDefListItems($list_str) { + # + # Process the contents of a single definition list, splitting it + # into individual term and definition list items. + # + $less_than_tab = $this->tab_width - 1; + + # trim trailing blank lines: + $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); + + # Process definition terms. + $list_str = preg_replace_callback('{ + (?>\A\n?|\n\n+) # leading line + ( # definition terms = $1 + [ ]{0,'.$less_than_tab.'} # leading whitespace + (?!\:[ ]|[ ]) # negative lookahead for a definition + # mark (colon) or more whitespace. + (?> \S.* \n)+? # actual term (not whitespace). + ) + (?=\n?[ ]{0,3}:[ ]) # lookahead for following line feed + # with a definition mark. + }xm', + array($this, '_processDefListItems_callback_dt'), $list_str); + + # Process actual definitions. + $list_str = preg_replace_callback('{ + \n(\n+)? # leading line = $1 + ( # marker space = $2 + [ ]{0,'.$less_than_tab.'} # whitespace before colon + \:[ ]+ # definition mark (colon) + ) + ((?s:.+?)) # definition text = $3 + (?= \n+ # stop at next definition mark, + (?: # next term or end of text + [ ]{0,'.$less_than_tab.'} \:[ ] | + <dt> | \z + ) + ) + }xm', + array($this, '_processDefListItems_callback_dd'), $list_str); + + return $list_str; + } + protected function _processDefListItems_callback_dt($matches) { + $terms = explode("\n", trim($matches[1])); + $text = ''; + foreach ($terms as $term) { + $term = $this->runSpanGamut(trim($term)); + $text .= "\n<dt>" . $term . "</dt>"; + } + return $text . "\n"; + } + protected function _processDefListItems_callback_dd($matches) { + $leading_line = $matches[1]; + $marker_space = $matches[2]; + $def = $matches[3]; + + if ($leading_line || preg_match('/\n{2,}/', $def)) { + # Replace marker with the appropriate whitespace indentation + $def = str_repeat(' ', strlen($marker_space)) . $def; + $def = $this->runBlockGamut($this->outdent($def . "\n\n")); + $def = "\n". $def ."\n"; + } + else { + $def = rtrim($def); + $def = $this->runSpanGamut($this->outdent($def)); + } + + return "\n<dd>" . $def . "</dd>\n"; + } + + + protected function doFencedCodeBlocks($text) { + # + # Adding the fenced code block syntax to regular Markdown: + # + # ~~~ + # Code block + # ~~~ + # + $less_than_tab = $this->tab_width; + + $text = preg_replace_callback('{ + (?:\n|\A) + # 1: Opening marker + ( + (?:~{3,}|`{3,}) # 3 or more tildes/backticks. + ) + [ ]* + (?: + \.?([-_:a-zA-Z0-9]+) # 2: standalone class name + | + '.$this->id_class_attr_catch_re.' # 3: Extra attributes + )? + [ ]* \n # Whitespace and newline following marker. + + # 4: Content + ( + (?> + (?!\1 [ ]* \n) # Not a closing marker. + .*\n+ + )+ + ) + + # Closing marker. + \1 [ ]* (?= \n ) + }xm', + array($this, '_doFencedCodeBlocks_callback'), $text); + + return $text; + } + protected function _doFencedCodeBlocks_callback($matches) { + $classname =& $matches[2]; + $attrs =& $matches[3]; + $codeblock = $matches[4]; + $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); + $codeblock = preg_replace_callback('/^\n+/', + array($this, '_doFencedCodeBlocks_newlines'), $codeblock); + + if ($classname != "") { + if ($classname{0} == '.') + $classname = substr($classname, 1); + $attr_str = ' class="'.$this->code_class_prefix.$classname.'"'; + } else { + $attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs); + } + $pre_attr_str = $this->code_attr_on_pre ? $attr_str : ''; + $code_attr_str = $this->code_attr_on_pre ? '' : $attr_str; + $codeblock = "<pre$pre_attr_str><code$code_attr_str>$codeblock</code></pre>"; + + return "\n\n".$this->hashBlock($codeblock)."\n\n"; + } + protected function _doFencedCodeBlocks_newlines($matches) { + return str_repeat("<br$this->empty_element_suffix", + strlen($matches[0])); + } + + + # + # Redefining emphasis markers so that emphasis by underscore does not + # work in the middle of a word. + # + protected $em_relist = array( + '' => '(?:(?<!\*)\*(?!\*)|(?<![a-zA-Z0-9_])_(?!_))(?![\.,:;]?\s)', + '*' => '(?<![\s*])\*(?!\*)', + '_' => '(?<![\s_])_(?![a-zA-Z0-9_])', + ); + protected $strong_relist = array( + '' => '(?:(?<!\*)\*\*(?!\*)|(?<![a-zA-Z0-9_])__(?!_))(?![\.,:;]?\s)', + '**' => '(?<![\s*])\*\*(?!\*)', + '__' => '(?<![\s_])__(?![a-zA-Z0-9_])', + ); + protected $em_strong_relist = array( + '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<![a-zA-Z0-9_])___(?!_))(?![\.,:;]?\s)', + '***' => '(?<![\s*])\*\*\*(?!\*)', + '___' => '(?<![\s_])___(?![a-zA-Z0-9_])', + ); + + + protected function formParagraphs($text) { + # + # Params: + # $text - string to process with html <p> tags + # + # Strip leading and trailing lines: + $text = preg_replace('/\A\n+|\n+\z/', '', $text); + + $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); + + # + # Wrap <p> tags and unhashify HTML blocks + # + foreach ($grafs as $key => $value) { + $value = trim($this->runSpanGamut($value)); + + # Check if this should be enclosed in a paragraph. + # Clean tag hashes & block tag hashes are left alone. + $is_p = !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value); + + if ($is_p) { + $value = "<p>$value</p>"; + } + $grafs[$key] = $value; + } + + # Join grafs in one text, then unhash HTML tags. + $text = implode("\n\n", $grafs); + + # Finish by removing any tag hashes still present in $text. + $text = $this->unhash($text); + + return $text; + } + + + ### Footnotes + + protected function stripFootnotes($text) { + # + # Strips link definitions from text, stores the URLs and titles in + # hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: [^id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[\^(.+?)\][ ]?: # note_id = $1 + [ ]* + \n? # maybe *one* newline + ( # text = $2 (no blank lines allowed) + (?: + .+ # actual text + | + \n # newlines but + (?!\[.+?\][ ]?:\s)# negative lookahead for footnote or link definition marker. + (?!\n+[ ]{0,3}\S)# ensure line is not blank and followed + # by non-indented content + )* + ) + }xm', + array($this, '_stripFootnotes_callback'), + $text); + return $text; + } + protected function _stripFootnotes_callback($matches) { + $note_id = $this->fn_id_prefix . $matches[1]; + $this->footnotes[$note_id] = $this->outdent($matches[2]); + return ''; # String that will replace the block + } + + + protected function doFootnotes($text) { + # + # Replace footnote references in $text [^id] with a special text-token + # which will be replaced by the actual footnote marker in appendFootnotes. + # + if (!$this->in_anchor) { + $text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text); + } + return $text; + } + + + protected function appendFootnotes($text) { + # + # Append footnote list to text. + # + $text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', + array($this, '_appendFootnotes_callback'), $text); + + if (!empty($this->footnotes_ordered)) { + $text .= "\n\n"; + $text .= "<div class=\"footnotes\">\n"; + $text .= "<hr". $this->empty_element_suffix ."\n"; + $text .= "<ol>\n\n"; + + $attr = ""; + if ($this->fn_backlink_class != "") { + $class = $this->fn_backlink_class; + $class = $this->encodeAttribute($class); + $attr .= " class=\"$class\""; + } + if ($this->fn_backlink_title != "") { + $title = $this->fn_backlink_title; + $title = $this->encodeAttribute($title); + $attr .= " title=\"$title\""; + } + $num = 0; + + while (!empty($this->footnotes_ordered)) { + $footnote = reset($this->footnotes_ordered); + $note_id = key($this->footnotes_ordered); + unset($this->footnotes_ordered[$note_id]); + $ref_count = $this->footnotes_ref_count[$note_id]; + unset($this->footnotes_ref_count[$note_id]); + unset($this->footnotes[$note_id]); + + $footnote .= "\n"; # Need to append newline before parsing. + $footnote = $this->runBlockGamut("$footnote\n"); + $footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', + array($this, '_appendFootnotes_callback'), $footnote); + + $attr = str_replace("%%", ++$num, $attr); + $note_id = $this->encodeAttribute($note_id); + + # Prepare backlink, multiple backlinks if multiple references + $backlink = "<a href=\"#fnref:$note_id\"$attr>&#8617;</a>"; + for ($ref_num = 2; $ref_num <= $ref_count; ++$ref_num) { + $backlink .= " <a href=\"#fnref$ref_num:$note_id\"$attr>&#8617;</a>"; + } + # Add backlink to last paragraph; create new paragraph if needed. + if (preg_match('{</p>$}', $footnote)) { + $footnote = substr($footnote, 0, -4) . "&#160;$backlink</p>"; + } else { + $footnote .= "\n\n<p>$backlink</p>"; + } + + $text .= "<li id=\"fn:$note_id\">\n"; + $text .= $footnote . "\n"; + $text .= "</li>\n\n"; + } + + $text .= "</ol>\n"; + $text .= "</div>"; + } + return $text; + } + protected function _appendFootnotes_callback($matches) { + $node_id = $this->fn_id_prefix . $matches[1]; + + # Create footnote marker only if it has a corresponding footnote *and* + # the footnote hasn't been used by another marker. + if (isset($this->footnotes[$node_id])) { + $num =& $this->footnotes_numbers[$node_id]; + if (!isset($num)) { + # Transfer footnote content to the ordered list and give it its + # number + $this->footnotes_ordered[$node_id] = $this->footnotes[$node_id]; + $this->footnotes_ref_count[$node_id] = 1; + $num = $this->footnote_counter++; + $ref_count_mark = ''; + } else { + $ref_count_mark = $this->footnotes_ref_count[$node_id] += 1; + } + + $attr = ""; + if ($this->fn_link_class != "") { + $class = $this->fn_link_class; + $class = $this->encodeAttribute($class); + $attr .= " class=\"$class\""; + } + if ($this->fn_link_title != "") { + $title = $this->fn_link_title; + $title = $this->encodeAttribute($title); + $attr .= " title=\"$title\""; + } + + $attr = str_replace("%%", $num, $attr); + $node_id = $this->encodeAttribute($node_id); + + return + "<sup id=\"fnref$ref_count_mark:$node_id\">". + "<a href=\"#fn:$node_id\"$attr>$num</a>". + "</sup>"; + } + + return "[^".$matches[1]."]"; + } + + + ### Abbreviations ### + + protected function stripAbbreviations($text) { + # + # Strips abbreviations from text, stores titles in hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: [id]*: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\*\[(.+?)\][ ]?: # abbr_id = $1 + (.*) # text = $2 (no blank lines allowed) + }xm', + array($this, '_stripAbbreviations_callback'), + $text); + return $text; + } + protected function _stripAbbreviations_callback($matches) { + $abbr_word = $matches[1]; + $abbr_desc = $matches[2]; + if ($this->abbr_word_re) + $this->abbr_word_re .= '|'; + $this->abbr_word_re .= preg_quote($abbr_word); + $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); + return ''; # String that will replace the block + } + + + protected function doAbbreviations($text) { + # + # Find defined abbreviations in text and wrap them in <abbr> elements. + # + if ($this->abbr_word_re) { + // cannot use the /x modifier because abbr_word_re may + // contain significant spaces: + $text = preg_replace_callback('{'. + '(?<![\w\x1A])'. + '(?:'.$this->abbr_word_re.')'. + '(?![\w\x1A])'. + '}', + array($this, '_doAbbreviations_callback'), $text); + } + return $text; + } + protected function _doAbbreviations_callback($matches) { + $abbr = $matches[0]; + if (isset($this->abbr_desciptions[$abbr])) { + $desc = $this->abbr_desciptions[$abbr]; + if (empty($desc)) { + return $this->hashPart("<abbr>$abbr</abbr>"); + } else { + $desc = $this->encodeAttribute($desc); + return $this->hashPart("<abbr title=\"$desc\">$abbr</abbr>"); + } + } else { + return $matches[0]; + } + } + +} + +$yellow->plugins->register("markdown", "YellowMarkdown", YellowMarkdown::Version); +?> +\ No newline at end of file diff --git a/system/plugins/markdownextra.php b/system/plugins/markdownextra.php @@ -1,3329 +0,0 @@ -<?php -// Copyright (c) 2013-2015 Datenstrom, http://datenstrom.se -// This file may be used and distributed under the terms of the public license. - -// Markdown extra plugin -class YellowMarkdownExtra -{ - const Version = "0.1.6"; - var $yellow; //access to API - - // Handle plugin initialisation - function onLoad($yellow) - { - $this->yellow = $yellow; - } - - // Handle page content parsing of raw format - function onParseContentText($page, $text) - { - $markdown = new YellowMarkdownExtraParser($this->yellow, $page); - return $markdown->transform($text); - } -} - -// Markdown extra parser -class YellowMarkdownExtraParser extends MarkdownExtraParser -{ - var $yellow; //access to API - var $page; //access to page - var $idAttributes; //id attributes - - function __construct($yellow, $page) - { - $this->yellow = $yellow; - $this->page = $page; - $this->idAttributes = array(); - $this->no_markup = $page->parserSafeMode; - $this->no_entities = $page->parserSafeMode; - $this->url_filter_func = function($url) use ($yellow, $page) - { - return $yellow->toolbox->normaliseLocation($url, $page->base, $page->location, - $yellow->config->get("serverBase").$yellow->config->get("imageLocation"), - $page->parserSafeMode && $page->statusCode==200); - }; - parent::__construct(); - } - - // Transform text - function transform($text) - { - $text = preg_replace("/@pageRead/i", $this->page->get("pageRead"), $text); - $text = preg_replace("/@pageEdit/i", $this->page->get("pageEdit"), $text); - $text = preg_replace("/@pageError/i", $this->page->get("pageError"), $text); - return parent::transform($text); - } - - // Return unique id attribute - function getIdAttribute($text) - { - $text = $this->yellow->toolbox->normaliseName($text, true, false, true); - $text = trim(preg_replace("/-+/", "-", $text), "-"); - if(is_null($this->idAttributes[$text])) - { - $this->idAttributes[$text] = $text; - $attr = " id=\"$text\""; - } - return $attr; - } - - // Handle links - function doAutoLinks($text) - { - $text = preg_replace_callback("/<(\w+:[^\'\">\s]+)>/", array(&$this, "_doAutoLinks_url_callback"), $text); - $text = preg_replace_callback("/<([\w\-\.]+@[\w\-\.]+)>/", array(&$this, "_doAutoLinks_email_callback"), $text); - $text = preg_replace_callback("/\[(\w+)\s+(.*?)\]/", array(&$this, "_doAutoLinks_shortcut_callback"), $text); - $text = preg_replace_callback("/\[\-\-(.*?)\-\-\]/", array(&$this, "_doAutoLinks_comment_callback"), $text); - $text = preg_replace_callback("/((http|https|ftp):\/\/\S+[^\'\"\,\.\;\:\s]+)/", array(&$this, "_doAutoLinks_url_callback"), $text); - $text = preg_replace_callback("/([\w\-\.]+@[\w\-\.]+\.[\w]{2,4})/", array(&$this, "_doAutoLinks_email_callback"), $text); - return $text; - } - - // Handle shortcuts - function _doAutoLinks_shortcut_callback($matches) - { - $text = preg_replace("/\s+/s", " ", $matches[2]); - $output = $this->page->parseType($matches[1], $text, true); - if(is_null($output)) $output = htmlspecialchars($matches[0], ENT_NOQUOTES); - return $this->hashBlock($output); - } - - // Handle comments - function _doAutoLinks_comment_callback($matches) - { - $text = $matches[1]; - $output = "<!--".htmlspecialchars($text, ENT_NOQUOTES)."-->"; - if($text[0] == '-') $output = ""; - return $this->hashBlock($output); - } - - // Handle fenced code blocks - function _doFencedCodeBlocks_callback($matches) - { - $text = $matches[4]; - $output = $this->page->parseType($matches[2], $text, false); - if(is_null($output)) - { - $attr = $this->doExtraAttributes("pre", $dummy =& $matches[3]); - $output = "<pre$attr><code>".htmlspecialchars($text, ENT_NOQUOTES)."</code></pre>"; - } - return "\n\n".$this->hashBlock($output)."\n\n"; - } - - // Handle headers, text style - function _doHeaders_callback_setext($matches) - { - if ($matches[3] == '-' && preg_match('{^- }', $matches[1])) return $matches[0]; - $text = $matches[1]; - $level = $matches[3]{0} == '=' ? 1 : 2; - $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2]); - if(empty($attr) && $level>=2 && $level<=3) $attr = $this->getIdAttribute($text); - $output = "<h$level$attr>".$this->runSpanGamut($text)."</h$level>"; - return "\n".$this->hashBlock($output)."\n\n"; - } - - // Handle headers, atx style - function _doHeaders_callback_atx($matches) - { - $text = $matches[2]; - $level = strlen($matches[1]); - $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[3]); - if(empty($attr) && $level>=2 && $level<=3) $attr = $this->getIdAttribute($text); - $output = "<h$level$attr>".$this->runSpanGamut($text)."</h$level>"; - return "\n".$this->hashBlock($output)."\n\n"; - } - - // Handle inline links - function _doAnchors_inline_callback($matches) - { - $url = $matches[3]=="" ? $matches[4] : $matches[3]; - $text = $matches[2]; - $title = $matches[7]; - $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); - $output = "<a href=\"".$this->encodeURLAttribute($url)."\""; - if(!empty($title)) $output .= " title=\"".$this->encodeAttribute($title)."\""; - $output .= $attr; - $output .= ">".$this->runSpanGamut($text)."</a>"; - return $this->hashPart($output); - } - - // Handle inline images - function _doImages_inline_callback($matches) - { - $width = $height = 0; - $src = $matches[3]=="" ? $matches[4] : $matches[3]; - if(!preg_match("/^\w+:/", $src)) - { - list($width, $height) = $this->yellow->toolbox->detectImageInfo($this->yellow->config->get("imageDir").$src); - $src = $this->yellow->config->get("serverBase").$this->yellow->config->get("imageLocation").$src; - } - $alt = $matches[2]; - $title = $matches[7]=="" ? $matches[2] : $matches[7]; - $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]); - $output = "<img src=\"".$this->encodeURLAttribute($src)."\""; - if($width && $height) $output .= " width=\"$width\" height=\"$height\""; - if(!empty($alt)) $output .= " alt=\"".$this->encodeAttribute($alt)."\""; - if(!empty($title)) $output .= " title=\"".$this->encodeAttribute($title)."\""; - $output .= $attr; - $output .= $this->empty_element_suffix; - return $this->hashPart($output); - } -} - -// PHP Markdown Lib -// Copyright (c) 2004-2013 Michel Fortin -// <http://michelf.com/projects/php-markdown/> -// -// Original Markdown -// Copyright (c) 2004-2006 John Gruber -// <http://daringfireball.net/projects/markdown/> -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// -// * Neither the name "Markdown" nor the names of its contributors may -// be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// This software is provided by the copyright holders and contributors "as -// is" and any express or implied warranties, including, but not limited -// to, the implied warranties of merchantability and fitness for a -// particular purpose are disclaimed. In no event shall the copyright owner -// or contributors be liable for any direct, indirect, incidental, special, -// exemplary, or consequential damages (including, but not limited to, -// procurement of substitute goods or services; loss of use, data, or -// profits; or business interruption) however caused and on any theory of -// liability, whether in contract, strict liability, or tort (including -// negligence or otherwise) arising in any way out of the use of this -// software, even if advised of the possibility of such damage. - -class MarkdownParser { - - ### Version ### - - const MARKDOWNLIB_VERSION = "1.4.1"; - - ### Simple Function Interface ### - - public static function defaultTransform($text) { - # - # Initialize the parser and return the result of its transform method. - # This will work fine for derived classes too. - # - # Take parser class on which this function was called. - $parser_class = \get_called_class(); - - # try to take parser from the static parser list - static $parser_list; - $parser =& $parser_list[$parser_class]; - - # create the parser it not already set - if (!$parser) - $parser = new $parser_class; - - # Transform text using parser. - return $parser->transform($text); - } - - ### Configuration Variables ### - - # Change to ">" for HTML output. - public $empty_element_suffix = " />"; - public $tab_width = 4; - - # Change to `true` to disallow markup or entities. - public $no_markup = false; - public $no_entities = false; - - # Predefined urls and titles for reference links and images. - public $predef_urls = array(); - public $predef_titles = array(); - - # Optional filter function for URLs - public $url_filter_func = null; - - - ### Parser Implementation ### - - # Regex to match balanced [brackets]. - # Needed to insert a maximum bracked depth while converting to PHP. - protected $nested_brackets_depth = 6; - protected $nested_brackets_re; - - protected $nested_url_parenthesis_depth = 4; - protected $nested_url_parenthesis_re; - - # Table of hash values for escaped characters: - protected $escape_chars = '\`*_{}[]()>#+-.!'; - protected $escape_chars_re; - - - public function __construct() { - # - # Constructor function. Initialize appropriate member variables. - # - $this->_initDetab(); - $this->prepareItalicsAndBold(); - - $this->nested_brackets_re = - str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth). - str_repeat('\])*', $this->nested_brackets_depth); - - $this->nested_url_parenthesis_re = - str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth). - str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth); - - $this->escape_chars_re = '['.preg_quote($this->escape_chars).']'; - - # Sort document, block, and span gamut in ascendent priority order. - asort($this->document_gamut); - asort($this->block_gamut); - asort($this->span_gamut); - } - - - # Internal hashes used during transformation. - protected $urls = array(); - protected $titles = array(); - protected $html_hashes = array(); - - # Status flag to avoid invalid nesting. - protected $in_anchor = false; - - - protected function setup() { - # - # Called before the transformation process starts to setup parser - # states. - # - # Clear global hashes. - $this->urls = $this->predef_urls; - $this->titles = $this->predef_titles; - $this->html_hashes = array(); - - $this->in_anchor = false; - } - - protected function teardown() { - # - # Called after the transformation process to clear any variable - # which may be taking up memory unnecessarly. - # - $this->urls = array(); - $this->titles = array(); - $this->html_hashes = array(); - } - - - public function transform($text) { - # - # Main function. Performs some preprocessing on the input text - # and pass it through the document gamut. - # - $this->setup(); - - # Remove UTF-8 BOM and marker character in input, if present. - $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text); - - # Standardize line endings: - # DOS to Unix and Mac to Unix - $text = preg_replace('{\r\n?}', "\n", $text); - - # Make sure $text ends with a couple of newlines: - $text .= "\n\n"; - - # Convert all tabs to spaces. - $text = $this->detab($text); - - # Turn block-level HTML blocks into hash entries - $text = $this->hashHTMLBlocks($text); - - # Strip any lines consisting only of spaces and tabs. - # This makes subsequent regexen easier to write, because we can - # match consecutive blank lines with /\n+/ instead of something - # contorted like /[ ]*\n+/ . - $text = preg_replace('/^[ ]+$/m', '', $text); - - # Run document gamut methods. - foreach ($this->document_gamut as $method => $priority) { - $text = $this->$method($text); - } - - $this->teardown(); - - return $text . "\n"; - } - - protected $document_gamut = array( - # Strip link definitions, store in hashes. - "stripLinkDefinitions" => 20, - - "runBasicBlockGamut" => 30, - ); - - - protected function stripLinkDefinitions($text) { - # - # Strips link definitions from text, stores the URLs and titles in - # hash references. - # - $less_than_tab = $this->tab_width - 1; - - # Link defs are in the form: ^[id]: url "optional title" - $text = preg_replace_callback('{ - ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 - [ ]* - \n? # maybe *one* newline - [ ]* - (?: - <(.+?)> # url = $2 - | - (\S+?) # url = $3 - ) - [ ]* - \n? # maybe one newline - [ ]* - (?: - (?<=\s) # lookbehind for whitespace - ["(] - (.*?) # title = $4 - [")] - [ ]* - )? # title is optional - (?:\n+|\Z) - }xm', - array($this, '_stripLinkDefinitions_callback'), - $text); - return $text; - } - protected function _stripLinkDefinitions_callback($matches) { - $link_id = strtolower($matches[1]); - $url = $matches[2] == '' ? $matches[3] : $matches[2]; - $this->urls[$link_id] = $url; - $this->titles[$link_id] =& $matches[4]; - return ''; # String that will replace the block - } - - - protected function hashHTMLBlocks($text) { - if ($this->no_markup) return $text; - - $less_than_tab = $this->tab_width - 1; - - # Hashify HTML blocks: - # We only want to do this for block-level HTML tags, such as headers, - # lists, and tables. That's because we still want to wrap <p>s around - # "paragraphs" that are wrapped in non-block-level tags, such as anchors, - # phrase emphasis, and spans. The list of tags we're looking for is - # hard-coded: - # - # * List "a" is made of tags which can be both inline or block-level. - # These will be treated block-level when the start tag is alone on - # its line, otherwise they're not matched here and will be taken as - # inline later. - # * List "b" is made of tags which are always block-level; - # - $block_tags_a_re = 'ins|del'; - $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'. - 'script|noscript|style|form|fieldset|iframe|math|svg|'. - 'article|section|nav|aside|hgroup|header|footer|'. - 'figure'; - - # Regular expression for the content of a block tag. - $nested_tags_level = 4; - $attr = ' - (?> # optional tag attributes - \s # starts with whitespace - (?> - [^>"/]+ # text outside quotes - | - /+(?!>) # slash not followed by ">" - | - "[^"]*" # text inside double quotes (tolerate ">") - | - \'[^\']*\' # text inside single quotes (tolerate ">") - )* - )? - '; - $content = - str_repeat(' - (?> - [^<]+ # content without tag - | - <\2 # nested opening tag - '.$attr.' # attributes - (?> - /> - | - >', $nested_tags_level). # end of opening tag - '.*?'. # last level nested tag content - str_repeat(' - </\2\s*> # closing nested tag - ) - | - <(?!/\2\s*> # other tags with a different name - ) - )*', - $nested_tags_level); - $content2 = str_replace('\2', '\3', $content); - - # First, look for nested blocks, e.g.: - # <div> - # <div> - # tags for inner block must be indented. - # </div> - # </div> - # - # The outermost tags must start at the left margin for this to match, and - # the inner nested divs must be indented. - # We need to do this before the next, more liberal match, because the next - # match will start at the first `<div>` and stop at the first `</div>`. - $text = preg_replace_callback('{(?> - (?> - (?<=\n) # Starting on its own line - | # or - \A\n? # the at beginning of the doc - ) - ( # save in $1 - - # Match from `\n<tag>` to `</tag>\n`, handling nested tags - # in between. - - [ ]{0,'.$less_than_tab.'} - <('.$block_tags_b_re.')# start tag = $2 - '.$attr.'> # attributes followed by > and \n - '.$content.' # content, support nesting - </\2> # the matching end tag - [ ]* # trailing spaces/tabs - (?=\n+|\Z) # followed by a newline or end of document - - | # Special version for tags of group a. - - [ ]{0,'.$less_than_tab.'} - <('.$block_tags_a_re.')# start tag = $3 - '.$attr.'>[ ]*\n # attributes followed by > - '.$content2.' # content, support nesting - </\3> # the matching end tag - [ ]* # trailing spaces/tabs - (?=\n+|\Z) # followed by a newline or end of document - - | # Special case just for <hr />. It was easier to make a special - # case than to make the other regex more complicated. - - [ ]{0,'.$less_than_tab.'} - <(hr) # start tag = $2 - '.$attr.' # attributes - /?> # the matching end tag - [ ]* - (?=\n{2,}|\Z) # followed by a blank line or end of document - - | # Special case for standalone HTML comments: - - [ ]{0,'.$less_than_tab.'} - (?s: - <!-- .*? --> - ) - [ ]* - (?=\n{2,}|\Z) # followed by a blank line or end of document - - | # PHP and ASP-style processor instructions (<? and <%) - - [ ]{0,'.$less_than_tab.'} - (?s: - <([?%]) # $2 - .*? - \2> - ) - [ ]* - (?=\n{2,}|\Z) # followed by a blank line or end of document - - ) - )}Sxmi', - array($this, '_hashHTMLBlocks_callback'), - $text); - - return $text; - } - protected function _hashHTMLBlocks_callback($matches) { - $text = $matches[1]; - $key = $this->hashBlock($text); - return "\n\n$key\n\n"; - } - - - protected function hashPart($text, $boundary = 'X') { - # - # Called whenever a tag must be hashed when a function insert an atomic - # element in the text stream. Passing $text to through this function gives - # a unique text-token which will be reverted back when calling unhash. - # - # The $boundary argument specify what character should be used to surround - # the token. By convension, "B" is used for block elements that needs not - # to be wrapped into paragraph tags at the end, ":" is used for elements - # that are word separators and "X" is used in the general case. - # - # Swap back any tag hash found in $text so we do not have to `unhash` - # multiple times at the end. - $text = $this->unhash($text); - - # Then hash the block. - static $i = 0; - $key = "$boundary\x1A" . ++$i . $boundary; - $this->html_hashes[$key] = $text; - return $key; # String that will replace the tag. - } - - - protected function hashBlock($text) { - # - # Shortcut function for hashPart with block-level boundaries. - # - return $this->hashPart($text, 'B'); - } - - - protected $block_gamut = array( - # - # These are all the transformations that form block-level - # tags like paragraphs, headers, and list items. - # - "doHeaders" => 10, - "doHorizontalRules" => 20, - - "doLists" => 40, - "doCodeBlocks" => 50, - "doBlockQuotes" => 60, - ); - - protected function runBlockGamut($text) { - # - # Run block gamut tranformations. - # - # We need to escape raw HTML in Markdown source before doing anything - # else. This need to be done for each block, and not only at the - # begining in the Markdown function since hashed blocks can be part of - # list items and could have been indented. Indented blocks would have - # been seen as a code block in a previous pass of hashHTMLBlocks. - $text = $this->hashHTMLBlocks($text); - - return $this->runBasicBlockGamut($text); - } - - protected function runBasicBlockGamut($text) { - # - # Run block gamut tranformations, without hashing HTML blocks. This is - # useful when HTML blocks are known to be already hashed, like in the first - # whole-document pass. - # - foreach ($this->block_gamut as $method => $priority) { - $text = $this->$method($text); - } - - # Finally form paragraph and restore hashed blocks. - $text = $this->formParagraphs($text); - - return $text; - } - - - protected function doHorizontalRules($text) { - # Do Horizontal Rules: - return preg_replace( - '{ - ^[ ]{0,3} # Leading space - ([-*_]) # $1: First marker - (?> # Repeated marker group - [ ]{0,2} # Zero, one, or two spaces. - \1 # Marker character - ){2,} # Group repeated at least twice - [ ]* # Tailing spaces - $ # End of line. - }mx', - "\n".$this->hashBlock("<hr$this->empty_element_suffix")."\n", - $text); - } - - - protected $span_gamut = array( - # - # These are all the transformations that occur *within* block-level - # tags like paragraphs, headers, and list items. - # - # Process character escapes, code spans, and inline HTML - # in one shot. - "parseSpan" => -30, - - # Process anchor and image tags. Images must come first, - # because ![foo][f] looks like an anchor. - "doImages" => 10, - "doAnchors" => 20, - - # Make links out of things like `<http://example.com/>` - # Must come after doAnchors, because you can use < and > - # delimiters in inline links like [this](<url>). - "doAutoLinks" => 30, - "encodeAmpsAndAngles" => 40, - - "doItalicsAndBold" => 50, - "doHardBreaks" => 60, - ); - - protected function runSpanGamut($text) { - # - # Run span gamut tranformations. - # - foreach ($this->span_gamut as $method => $priority) { - $text = $this->$method($text); - } - - return $text; - } - - - protected function doHardBreaks($text) { - # Do hard breaks: - return preg_replace_callback('/ {2,}\n/', - array($this, '_doHardBreaks_callback'), $text); - } - protected function _doHardBreaks_callback($matches) { - return $this->hashPart("<br$this->empty_element_suffix\n"); - } - - - protected function doAnchors($text) { - # - # Turn Markdown link shortcuts into XHTML <a> tags. - # - if ($this->in_anchor) return $text; - $this->in_anchor = true; - - # - # First, handle reference-style links: [link text] [id] - # - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - \[ - ('.$this->nested_brackets_re.') # link text = $2 - \] - - [ ]? # one optional space - (?:\n[ ]*)? # one optional newline followed by spaces - - \[ - (.*?) # id = $3 - \] - ) - }xs', - array($this, '_doAnchors_reference_callback'), $text); - - # - # Next, inline-style links: [link text](url "optional title") - # - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - \[ - ('.$this->nested_brackets_re.') # link text = $2 - \] - \( # literal paren - [ \n]* - (?: - <(.+?)> # href = $3 - | - ('.$this->nested_url_parenthesis_re.') # href = $4 - ) - [ \n]* - ( # $5 - ([\'"]) # quote char = $6 - (.*?) # Title = $7 - \6 # matching quote - [ \n]* # ignore any spaces/tabs between closing quote and ) - )? # title is optional - \) - ) - }xs', - array($this, '_doAnchors_inline_callback'), $text); - - # - # Last, handle reference-style shortcuts: [link text] - # These must come last in case you've also got [link text][1] - # or [link text](/foo) - # - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - \[ - ([^\[\]]+) # link text = $2; can\'t contain [ or ] - \] - ) - }xs', - array($this, '_doAnchors_reference_callback'), $text); - - $this->in_anchor = false; - return $text; - } - protected function _doAnchors_reference_callback($matches) { - $whole_match = $matches[1]; - $link_text = $matches[2]; - $link_id =& $matches[3]; - - if ($link_id == "") { - # for shortcut links like [this][] or [this]. - $link_id = $link_text; - } - - # lower-case and turn embedded newlines into spaces - $link_id = strtolower($link_id); - $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); - - if (isset($this->urls[$link_id])) { - $url = $this->urls[$link_id]; - $url = $this->encodeURLAttribute($url); - - $result = "<a href=\"$url\""; - if ( isset( $this->titles[$link_id] ) ) { - $title = $this->titles[$link_id]; - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; - } - - $link_text = $this->runSpanGamut($link_text); - $result .= ">$link_text</a>"; - $result = $this->hashPart($result); - } - else { - $result = $whole_match; - } - return $result; - } - protected function _doAnchors_inline_callback($matches) { - $whole_match = $matches[1]; - $link_text = $this->runSpanGamut($matches[2]); - $url = $matches[3] == '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - - // if the URL was of the form <s p a c e s> it got caught by the HTML - // tag parser and hashed. Need to reverse the process before using the URL. - $unhashed = $this->unhash($url); - if ($unhashed != $url) - $url = preg_replace('/^<(.*)>$/', '\1', $unhashed); - - $url = $this->encodeURLAttribute($url); - - $result = "<a href=\"$url\""; - if (isset($title)) { - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; - } - - $link_text = $this->runSpanGamut($link_text); - $result .= ">$link_text</a>"; - - return $this->hashPart($result); - } - - - protected function doImages($text) { - # - # Turn Markdown image shortcuts into <img> tags. - # - # - # First, handle reference-style labeled images: ![alt text][id] - # - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - !\[ - ('.$this->nested_brackets_re.') # alt text = $2 - \] - - [ ]? # one optional space - (?:\n[ ]*)? # one optional newline followed by spaces - - \[ - (.*?) # id = $3 - \] - - ) - }xs', - array($this, '_doImages_reference_callback'), $text); - - # - # Next, handle inline images: ![alt text](url "optional title") - # Don't forget: encode * and _ - # - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - !\[ - ('.$this->nested_brackets_re.') # alt text = $2 - \] - \s? # One optional whitespace character - \( # literal paren - [ \n]* - (?: - <(\S*)> # src url = $3 - | - ('.$this->nested_url_parenthesis_re.') # src url = $4 - ) - [ \n]* - ( # $5 - ([\'"]) # quote char = $6 - (.*?) # title = $7 - \6 # matching quote - [ \n]* - )? # title is optional - \) - ) - }xs', - array($this, '_doImages_inline_callback'), $text); - - return $text; - } - protected function _doImages_reference_callback($matches) { - $whole_match = $matches[1]; - $alt_text = $matches[2]; - $link_id = strtolower($matches[3]); - - if ($link_id == "") { - $link_id = strtolower($alt_text); # for shortcut links like ![this][]. - } - - $alt_text = $this->encodeAttribute($alt_text); - if (isset($this->urls[$link_id])) { - $url = $this->encodeURLAttribute($this->urls[$link_id]); - $result = "<img src=\"$url\" alt=\"$alt_text\""; - if (isset($this->titles[$link_id])) { - $title = $this->titles[$link_id]; - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; - } - $result .= $this->empty_element_suffix; - $result = $this->hashPart($result); - } - else { - # If there's no such link ID, leave intact: - $result = $whole_match; - } - - return $result; - } - protected function _doImages_inline_callback($matches) { - $whole_match = $matches[1]; - $alt_text = $matches[2]; - $url = $matches[3] == '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - - $alt_text = $this->encodeAttribute($alt_text); - $url = $this->encodeURLAttribute($url); - $result = "<img src=\"$url\" alt=\"$alt_text\""; - if (isset($title)) { - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; # $title already quoted - } - $result .= $this->empty_element_suffix; - - return $this->hashPart($result); - } - - - protected function doHeaders($text) { - # Setext-style headers: - # Header 1 - # ======== - # - # Header 2 - # -------- - # - $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', - array($this, '_doHeaders_callback_setext'), $text); - - # atx-style headers: - # # Header 1 - # ## Header 2 - # ## Header 2 with closing hashes ## - # ... - # ###### Header 6 - # - $text = preg_replace_callback('{ - ^(\#{1,6}) # $1 = string of #\'s - [ ]* - (.+?) # $2 = Header text - [ ]* - \#* # optional closing #\'s (not counted) - \n+ - }xm', - array($this, '_doHeaders_callback_atx'), $text); - - return $text; - } - protected function _doHeaders_callback_setext($matches) { - # Terrible hack to check we haven't found an empty list item. - if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1])) - return $matches[0]; - - $level = $matches[2]{0} == '=' ? 1 : 2; - $block = "<h$level>".$this->runSpanGamut($matches[1])."</h$level>"; - return "\n" . $this->hashBlock($block) . "\n\n"; - } - protected function _doHeaders_callback_atx($matches) { - $level = strlen($matches[1]); - $block = "<h$level>".$this->runSpanGamut($matches[2])."</h$level>"; - return "\n" . $this->hashBlock($block) . "\n\n"; - } - - - protected function doLists($text) { - # - # Form HTML ordered (numbered) and unordered (bulleted) lists. - # - $less_than_tab = $this->tab_width - 1; - - # Re-usable patterns to match list item bullets and number markers: - $marker_ul_re = '[*+-]'; - $marker_ol_re = '\d+[\.]'; - - $markers_relist = array( - $marker_ul_re => $marker_ol_re, - $marker_ol_re => $marker_ul_re, - ); - - foreach ($markers_relist as $marker_re => $other_marker_re) { - # Re-usable pattern to match any entirel ul or ol list: - $whole_list_re = ' - ( # $1 = whole list - ( # $2 - ([ ]{0,'.$less_than_tab.'}) # $3 = number of spaces - ('.$marker_re.') # $4 = first list item marker - [ ]+ - ) - (?s:.+?) - ( # $5 - \z - | - \n{2,} - (?=\S) - (?! # Negative lookahead for another list item marker - [ ]* - '.$marker_re.'[ ]+ - ) - | - (?= # Lookahead for another kind of list - \n - \3 # Must have the same indentation - '.$other_marker_re.'[ ]+ - ) - ) - ) - '; // mx - - # We use a different prefix before nested lists than top-level lists. - # See extended comment in _ProcessListItems(). - - if ($this->list_level) { - $text = preg_replace_callback('{ - ^ - '.$whole_list_re.' - }mx', - array($this, '_doLists_callback'), $text); - } - else { - $text = preg_replace_callback('{ - (?:(?<=\n)\n|\A\n?) # Must eat the newline - '.$whole_list_re.' - }mx', - array($this, '_doLists_callback'), $text); - } - } - - return $text; - } - protected function _doLists_callback($matches) { - # Re-usable patterns to match list item bullets and number markers: - $marker_ul_re = '[*+-]'; - $marker_ol_re = '\d+[\.]'; - $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; - - $list = $matches[1]; - $list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol"; - - $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re ); - - $list .= "\n"; - $result = $this->processListItems($list, $marker_any_re); - - $result = $this->hashBlock("<$list_type>\n" . $result . "</$list_type>"); - return "\n". $result ."\n\n"; - } - - protected $list_level = 0; - - protected function processListItems($list_str, $marker_any_re) { - # - # Process the contents of a single ordered or unordered list, splitting it - # into individual list items. - # - # The $this->list_level global keeps track of when we're inside a list. - # Each time we enter a list, we increment it; when we leave a list, - # we decrement. If it's zero, we're not in a list anymore. - # - # We do this because when we're not inside a list, we want to treat - # something like this: - # - # I recommend upgrading to version - # 8. Oops, now this line is treated - # as a sub-list. - # - # As a single paragraph, despite the fact that the second line starts - # with a digit-period-space sequence. - # - # Whereas when we're inside a list (or sub-list), that line will be - # treated as the start of a sub-list. What a kludge, huh? This is - # an aspect of Markdown's syntax that's hard to parse perfectly - # without resorting to mind-reading. Perhaps the solution is to - # change the syntax rules such that sub-lists must start with a - # starting cardinal number; e.g. "1." or "a.". - - $this->list_level++; - - # trim trailing blank lines: - $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); - - $list_str = preg_replace_callback('{ - (\n)? # leading line = $1 - (^[ ]*) # leading whitespace = $2 - ('.$marker_any_re.' # list marker and space = $3 - (?:[ ]+|(?=\n)) # space only required if item is not empty - ) - ((?s:.*?)) # list item text = $4 - (?:(\n+(?=\n))|\n) # tailing blank line = $5 - (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n)))) - }xm', - array($this, '_processListItems_callback'), $list_str); - - $this->list_level--; - return $list_str; - } - protected function _processListItems_callback($matches) { - $item = $matches[4]; - $leading_line =& $matches[1]; - $leading_space =& $matches[2]; - $marker_space = $matches[3]; - $tailing_blank_line =& $matches[5]; - - if ($leading_line || $tailing_blank_line || - preg_match('/\n{2,}/', $item)) - { - # Replace marker with the appropriate whitespace indentation - $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item; - $item = $this->runBlockGamut($this->outdent($item)."\n"); - } - else { - # Recursion for sub-lists: - $item = $this->doLists($this->outdent($item)); - $item = preg_replace('/\n+$/', '', $item); - $item = $this->runSpanGamut($item); - } - - return "<li>" . $item . "</li>\n"; - } - - - protected function doCodeBlocks($text) { - # - # Process Markdown `<pre><code>` blocks. - # - $text = preg_replace_callback('{ - (?:\n\n|\A\n?) - ( # $1 = the code block -- one or more lines, starting with a space/tab - (?> - [ ]{'.$this->tab_width.'} # Lines must start with a tab or a tab-width of spaces - .*\n+ - )+ - ) - ((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z) # Lookahead for non-space at line-start, or end of doc - }xm', - array($this, '_doCodeBlocks_callback'), $text); - - return $text; - } - protected function _doCodeBlocks_callback($matches) { - $codeblock = $matches[1]; - - $codeblock = $this->outdent($codeblock); - $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); - - # trim leading newlines and trailing newlines - $codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock); - - $codeblock = "<pre><code>$codeblock\n</code></pre>"; - return "\n\n".$this->hashBlock($codeblock)."\n\n"; - } - - - protected function makeCodeSpan($code) { - # - # Create a code span markup for $code. Called from handleSpanToken. - # - $code = htmlspecialchars(trim($code), ENT_NOQUOTES); - return $this->hashPart("<code>$code</code>"); - } - - - protected $em_relist = array( - '' => '(?:(?<!\*)\*(?!\*)|(?<!_)_(?!_))(?![\.,:;]?\s)', - '*' => '(?<![\s*])\*(?!\*)', - '_' => '(?<![\s_])_(?!_)', - ); - protected $strong_relist = array( - '' => '(?:(?<!\*)\*\*(?!\*)|(?<!_)__(?!_))(?![\.,:;]?\s)', - '**' => '(?<![\s*])\*\*(?!\*)', - '__' => '(?<![\s_])__(?!_)', - ); - protected $em_strong_relist = array( - '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<!_)___(?!_))(?![\.,:;]?\s)', - '***' => '(?<![\s*])\*\*\*(?!\*)', - '___' => '(?<![\s_])___(?!_)', - ); - protected $em_strong_prepared_relist; - - protected function prepareItalicsAndBold() { - # - # Prepare regular expressions for searching emphasis tokens in any - # context. - # - foreach ($this->em_relist as $em => $em_re) { - foreach ($this->strong_relist as $strong => $strong_re) { - # Construct list of allowed token expressions. - $token_relist = array(); - if (isset($this->em_strong_relist["$em$strong"])) { - $token_relist[] = $this->em_strong_relist["$em$strong"]; - } - $token_relist[] = $em_re; - $token_relist[] = $strong_re; - - # Construct master expression from list. - $token_re = '{('. implode('|', $token_relist) .')}'; - $this->em_strong_prepared_relist["$em$strong"] = $token_re; - } - } - } - - protected function doItalicsAndBold($text) { - $token_stack = array(''); - $text_stack = array(''); - $em = ''; - $strong = ''; - $tree_char_em = false; - - while (1) { - # - # Get prepared regular expression for seraching emphasis tokens - # in current context. - # - $token_re = $this->em_strong_prepared_relist["$em$strong"]; - - # - # Each loop iteration search for the next emphasis token. - # Each token is then passed to handleSpanToken. - # - $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); - $text_stack[0] .= $parts[0]; - $token =& $parts[1]; - $text =& $parts[2]; - - if (empty($token)) { - # Reached end of text span: empty stack without emitting. - # any more emphasis. - while ($token_stack[0]) { - $text_stack[1] .= array_shift($token_stack); - $text_stack[0] .= array_shift($text_stack); - } - break; - } - - $token_len = strlen($token); - if ($tree_char_em) { - # Reached closing marker while inside a three-char emphasis. - if ($token_len == 3) { - # Three-char closing marker, close em and strong. - array_shift($token_stack); - $span = array_shift($text_stack); - $span = $this->runSpanGamut($span); - $span = "<strong><em>$span</em></strong>"; - $text_stack[0] .= $this->hashPart($span); - $em = ''; - $strong = ''; - } else { - # Other closing marker: close one em or strong and - # change current token state to match the other - $token_stack[0] = str_repeat($token{0}, 3-$token_len); - $tag = $token_len == 2 ? "strong" : "em"; - $span = $text_stack[0]; - $span = $this->runSpanGamut($span); - $span = "<$tag>$span</$tag>"; - $text_stack[0] = $this->hashPart($span); - $$tag = ''; # $$tag stands for $em or $strong - } - $tree_char_em = false; - } else if ($token_len == 3) { - if ($em) { - # Reached closing marker for both em and strong. - # Closing strong marker: - for ($i = 0; $i < 2; ++$i) { - $shifted_token = array_shift($token_stack); - $tag = strlen($shifted_token) == 2 ? "strong" : "em"; - $span = array_shift($text_stack); - $span = $this->runSpanGamut($span); - $span = "<$tag>$span</$tag>"; - $text_stack[0] .= $this->hashPart($span); - $$tag = ''; # $$tag stands for $em or $strong - } - } else { - # Reached opening three-char emphasis marker. Push on token - # stack; will be handled by the special condition above. - $em = $token{0}; - $strong = "$em$em"; - array_unshift($token_stack, $token); - array_unshift($text_stack, ''); - $tree_char_em = true; - } - } else if ($token_len == 2) { - if ($strong) { - # Unwind any dangling emphasis marker: - if (strlen($token_stack[0]) == 1) { - $text_stack[1] .= array_shift($token_stack); - $text_stack[0] .= array_shift($text_stack); - } - # Closing strong marker: - array_shift($token_stack); - $span = array_shift($text_stack); - $span = $this->runSpanGamut($span); - $span = "<strong>$span</strong>"; - $text_stack[0] .= $this->hashPart($span); - $strong = ''; - } else { - array_unshift($token_stack, $token); - array_unshift($text_stack, ''); - $strong = $token; - } - } else { - # Here $token_len == 1 - if ($em) { - if (strlen($token_stack[0]) == 1) { - # Closing emphasis marker: - array_shift($token_stack); - $span = array_shift($text_stack); - $span = $this->runSpanGamut($span); - $span = "<em>$span</em>"; - $text_stack[0] .= $this->hashPart($span); - $em = ''; - } else { - $text_stack[0] .= $token; - } - } else { - array_unshift($token_stack, $token); - array_unshift($text_stack, ''); - $em = $token; - } - } - } - return $text_stack[0]; - } - - - protected function doBlockQuotes($text) { - $text = preg_replace_callback('/ - ( # Wrap whole match in $1 - (?> - ^[ ]*>[ ]? # ">" at the start of a line - .+\n # rest of the first line - (.+\n)* # subsequent consecutive lines - \n* # blanks - )+ - ) - /xm', - array($this, '_doBlockQuotes_callback'), $text); - - return $text; - } - protected function _doBlockQuotes_callback($matches) { - $bq = $matches[1]; - # trim one level of quoting - trim whitespace-only lines - $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq); - $bq = $this->runBlockGamut($bq); # recurse - - $bq = preg_replace('/^/m', " ", $bq); - # These leading spaces cause problem with <pre> content, - # so we need to fix that: - $bq = preg_replace_callback('{(\s*<pre>.+?</pre>)}sx', - array($this, '_doBlockQuotes_callback2'), $bq); - - return "\n". $this->hashBlock("<blockquote>\n$bq\n</blockquote>")."\n\n"; - } - protected function _doBlockQuotes_callback2($matches) { - $pre = $matches[1]; - $pre = preg_replace('/^ /m', '', $pre); - return $pre; - } - - - protected function formParagraphs($text) { - # - # Params: - # $text - string to process with html <p> tags - # - # Strip leading and trailing lines: - $text = preg_replace('/\A\n+|\n+\z/', '', $text); - - $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); - - # - # Wrap <p> tags and unhashify HTML blocks - # - foreach ($grafs as $key => $value) { - if (!preg_match('/^B\x1A[0-9]+B$/', $value)) { - # Is a paragraph. - $value = $this->runSpanGamut($value); - $value = preg_replace('/^([ ]*)/', "<p>", $value); - $value .= "</p>"; - $grafs[$key] = $this->unhash($value); - } - else { - # Is a block. - # Modify elements of @grafs in-place... - $graf = $value; - $block = $this->html_hashes[$graf]; - $graf = $block; -// if (preg_match('{ -// \A -// ( # $1 = <div> tag -// <div \s+ -// [^>]* -// \b -// markdown\s*=\s* ([\'"]) # $2 = attr quote char -// 1 -// \2 -// [^>]* -// > -// ) -// ( # $3 = contents -// .* -// ) -// (</div>) # $4 = closing tag -// \z -// }xs', $block, $matches)) -// { -// list(, $div_open, , $div_content, $div_close) = $matches; -// -// # We can't call Markdown(), because that resets the hash; -// # that initialization code should be pulled into its own sub, though. -// $div_content = $this->hashHTMLBlocks($div_content); -// -// # Run document gamut methods on the content. -// foreach ($this->document_gamut as $method => $priority) { -// $div_content = $this->$method($div_content); -// } -// -// $div_open = preg_replace( -// '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open); -// -// $graf = $div_open . "\n" . $div_content . "\n" . $div_close; -// } - $grafs[$key] = $graf; - } - } - - return implode("\n\n", $grafs); - } - - - protected function encodeAttribute($text) { - # - # Encode text for a double-quoted HTML attribute. This function - # is *not* suitable for attributes enclosed in single quotes. - # - $text = $this->encodeAmpsAndAngles($text); - $text = str_replace('"', '&quot;', $text); - return $text; - } - - - protected function encodeURLAttribute($url, &$text = null) { - # - # Encode text for a double-quoted HTML attribute containing a URL, - # applying the URL filter if set. Also generates the textual - # representation for the URL (removing mailto: or tel:) storing it in $text. - # This function is *not* suitable for attributes enclosed in single quotes. - # - if ($this->url_filter_func) - $url = call_user_func($this->url_filter_func, $url); - - if (preg_match('{^mailto:}i', $url)) - $url = $this->encodeEntityObfuscatedAttribute($url, $text, 7); - else if (preg_match('{^tel:}i', $url)) - { - $url = $this->encodeAttribute($url); - $text = substr($url, 4); - } - else - { - $url = $this->encodeAttribute($url); - $text = $url; - } - - return $url; - } - - - protected function encodeAmpsAndAngles($text) { - # - # Smart processing for ampersands and angle brackets that need to - # be encoded. Valid character entities are left alone unless the - # no-entities mode is set. - # - if ($this->no_entities) { - $text = str_replace('&', '&amp;', $text); - } else { - # Ampersand-encoding based entirely on Nat Irons's Amputator - # MT plugin: <http://bumppo.net/projects/amputator/> - $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', - '&amp;', $text); - } - # Encode remaining <'s - $text = str_replace('<', '&lt;', $text); - - return $text; - } - - - protected function doAutoLinks($text) { - $text = preg_replace_callback('{<((https?|ftp|dict|tel):[^\'">\s]+)>}i', - array($this, '_doAutoLinks_url_callback'), $text); - - # Email addresses: <address@domain.foo> - $text = preg_replace_callback('{ - < - (?:mailto:)? - ( - (?: - [-!#$%&\'*+/=?^_`.{|}~\w\x80-\xFF]+ - | - ".*?" - ) - \@ - (?: - [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+ - | - \[[\d.a-fA-F:]+\] # IPv4 & IPv6 - ) - ) - > - }xi', - array($this, '_doAutoLinks_email_callback'), $text); - - return $text; - } - protected function _doAutoLinks_url_callback($matches) { - $url = $this->encodeURLAttribute($matches[1], $text); - $link = "<a href=\"$url\">$text</a>"; - return $this->hashPart($link); - } - protected function _doAutoLinks_email_callback($matches) { - $addr = $matches[1]; - $url = $this->encodeURLAttribute("mailto:$addr", $text); - $link = "<a href=\"$url\">$text</a>"; - return $this->hashPart($link); - } - - - protected function encodeEntityObfuscatedAttribute($text, &$tail = null, $head_length = 0) { - # - # Input: some text to obfuscate, e.g. "mailto:foo@example.com" - # - # Output: the same text but with most characters encoded as either a - # decimal or hex entity, in the hopes of foiling most address - # harvesting spam bots. E.g.: - # - # &#109;&#x61;&#105;&#x6c;&#116;&#x6f;&#58;&#x66;o&#111; - # &#x40;&#101;&#x78;&#97;&#x6d;&#112;&#x6c;&#101;&#46;&#x63;&#111; - # &#x6d; - # - # Note: the additional output $tail is assigned the same value as the - # ouput, minus the number of characters specified by $head_length. - # - # Based by a filter by Matthew Wickline, posted to BBEdit-Talk. - # With some optimizations by Milian Wolff. Forced encoding of HTML - # attribute special characters by Allan Odgaard. - # - if ($text == "") return $tail = ""; - - $chars = preg_split('/(?<!^)(?!$)/', $text); - $seed = (int)abs(crc32($text) / strlen($text)); # Deterministic seed. - - foreach ($chars as $key => $char) { - $ord = ord($char); - # Ignore non-ascii chars. - if ($ord < 128) { - $r = ($seed * (1 + $key)) % 100; # Pseudo-random function. - # roughly 10% raw, 45% hex, 45% dec - # '@' *must* be encoded. I insist. - # '"' and '>' have to be encoded inside the attribute - if ($r > 90 && strpos('@"&>', $char) === false) /* do nothing */; - else if ($r < 45) $chars[$key] = '&#x'.dechex($ord).';'; - else $chars[$key] = '&#'.$ord.';'; - } - } - - $text = implode('', $chars); - $tail = $head_length ? implode('', array_slice($chars, $head_length)) : $text; - - return $text; - } - - - protected function parseSpan($str) { - # - # Take the string $str and parse it into tokens, hashing embeded HTML, - # escaped characters and handling code spans. - # - $output = ''; - - $span_re = '{ - ( - \\\\'.$this->escape_chars_re.' - | - (?<![`\\\\]) - `+ # code span marker - '.( $this->no_markup ? '' : ' - | - <!-- .*? --> # comment - | - <\?.*?\?> | <%.*?%> # processing instruction - | - <[!$]?[-a-zA-Z0-9:_]+ # regular tags - (?> - \s - (?>[^"\'>]+|"[^"]*"|\'[^\']*\')* - )? - > - | - <[-a-zA-Z0-9:_]+\s*/> # xml-style empty tag - | - </[-a-zA-Z0-9:_]+\s*> # closing tag - ').' - ) - }xs'; - - while (1) { - # - # Each loop iteration seach for either the next tag, the next - # openning code span marker, or the next escaped character. - # Each token is then passed to handleSpanToken. - # - $parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE); - - # Create token from text preceding tag. - if ($parts[0] != "") { - $output .= $parts[0]; - } - - # Check if we reach the end. - if (isset($parts[1])) { - $output .= $this->handleSpanToken($parts[1], $parts[2]); - $str = $parts[2]; - } - else { - break; - } - } - - return $output; - } - - - protected function handleSpanToken($token, &$str) { - # - # Handle $token provided by parseSpan by determining its nature and - # returning the corresponding value that should replace it. - # - switch ($token{0}) { - case "\\": - return $this->hashPart("&#". ord($token{1}). ";"); - case "`": - # Search for end marker in remaining text. - if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm', - $str, $matches)) - { - $str = $matches[2]; - $codespan = $this->makeCodeSpan($matches[1]); - return $this->hashPart($codespan); - } - return $token; // return as text since no ending marker found. - default: - return $this->hashPart($token); - } - } - - - protected function outdent($text) { - # - # Remove one level of line-leading tabs or spaces - # - return preg_replace('/^(\t|[ ]{1,'.$this->tab_width.'})/m', '', $text); - } - - - # String length function for detab. `_initDetab` will create a function to - # hanlde UTF-8 if the default function does not exist. - protected $utf8_strlen = 'mb_strlen'; - - protected function detab($text) { - # - # Replace tabs with the appropriate amount of space. - # - # For each line we separate the line in blocks delemited by - # tab characters. Then we reconstruct every line by adding the - # appropriate number of space between each blocks. - - $text = preg_replace_callback('/^.*\t.*$/m', - array($this, '_detab_callback'), $text); - - return $text; - } - protected function _detab_callback($matches) { - $line = $matches[0]; - $strlen = $this->utf8_strlen; # strlen function for UTF-8. - - # Split in blocks. - $blocks = explode("\t", $line); - # Add each blocks to the line. - $line = $blocks[0]; - unset($blocks[0]); # Do not add first block twice. - foreach ($blocks as $block) { - # Calculate amount of space, insert spaces, insert block. - $amount = $this->tab_width - - $strlen($line, 'UTF-8') % $this->tab_width; - $line .= str_repeat(" ", $amount) . $block; - } - return $line; - } - protected function _initDetab() { - # - # Check for the availability of the function in the `utf8_strlen` property - # (initially `mb_strlen`). If the function is not available, create a - # function that will loosely count the number of UTF-8 characters with a - # regular expression. - # - if (function_exists($this->utf8_strlen)) return; - $this->utf8_strlen = create_function('$text', 'return preg_match_all( - "/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/", - $text, $m);'); - } - - - protected function unhash($text) { - # - # Swap back in all the tags hashed by _HashHTMLBlocks. - # - return preg_replace_callback('/(.)\x1A[0-9]+\1/', - array($this, '_unhash_callback'), $text); - } - protected function _unhash_callback($matches) { - return $this->html_hashes[$matches[0]]; - } - -} - -class MarkdownExtraParser extends MarkdownParser { - - ### Configuration Variables ### - - # Prefix for footnote ids. - public $fn_id_prefix = ""; - - # Optional title attribute for footnote links and backlinks. - public $fn_link_title = ""; - public $fn_backlink_title = ""; - - # Optional class attribute for footnote links and backlinks. - public $fn_link_class = "footnote-ref"; - public $fn_backlink_class = "footnote-backref"; - - # Class name for table cell alignment (%% replaced left/center/right) - # For instance: 'go-%%' becomes 'go-left' or 'go-right' or 'go-center' - # If empty, the align attribute is used instead of a class name. - public $table_align_class_tmpl = ''; - - # Optional class prefix for fenced code block. - public $code_class_prefix = ""; - # Class attribute for code blocks goes on the `code` tag; - # setting this to true will put attributes on the `pre` tag instead. - public $code_attr_on_pre = false; - - # Predefined abbreviations. - public $predef_abbr = array(); - - - ### Parser Implementation ### - - public function __construct() { - # - # Constructor function. Initialize the parser object. - # - # Add extra escapable characters before parent constructor - # initialize the table. - $this->escape_chars .= ':|'; - - # Insert extra document, block, and span transformations. - # Parent constructor will do the sorting. - $this->document_gamut += array( - "doFencedCodeBlocks" => 5, - "stripFootnotes" => 15, - "stripAbbreviations" => 25, - "appendFootnotes" => 50, - ); - $this->block_gamut += array( - "doFencedCodeBlocks" => 5, - "doTables" => 15, - "doDefLists" => 45, - ); - $this->span_gamut += array( - "doFootnotes" => 5, - "doAbbreviations" => 70, - ); - - parent::__construct(); - } - - - # Extra variables used during extra transformations. - protected $footnotes = array(); - protected $footnotes_ordered = array(); - protected $footnotes_ref_count = array(); - protected $footnotes_numbers = array(); - protected $abbr_desciptions = array(); - protected $abbr_word_re = ''; - - # Give the current footnote number. - protected $footnote_counter = 1; - - - protected function setup() { - # - # Setting up Extra-specific variables. - # - parent::setup(); - - $this->footnotes = array(); - $this->footnotes_ordered = array(); - $this->footnotes_ref_count = array(); - $this->footnotes_numbers = array(); - $this->abbr_desciptions = array(); - $this->abbr_word_re = ''; - $this->footnote_counter = 1; - - foreach ($this->predef_abbr as $abbr_word => $abbr_desc) { - if ($this->abbr_word_re) - $this->abbr_word_re .= '|'; - $this->abbr_word_re .= preg_quote($abbr_word); - $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); - } - } - - protected function teardown() { - # - # Clearing Extra-specific variables. - # - $this->footnotes = array(); - $this->footnotes_ordered = array(); - $this->footnotes_ref_count = array(); - $this->footnotes_numbers = array(); - $this->abbr_desciptions = array(); - $this->abbr_word_re = ''; - - parent::teardown(); - } - - - ### Extra Attribute Parser ### - - # Expression to use to catch attributes (includes the braces) - protected $id_class_attr_catch_re = '\{((?:[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,})[ ]*\}'; - # Expression to use when parsing in a context when no capture is desired - protected $id_class_attr_nocatch_re = '\{(?:[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,}[ ]*\}'; - - protected function doExtraAttributes($tag_name, $attr) { - # - # Parse attributes caught by the $this->id_class_attr_catch_re expression - # and return the HTML-formatted list of attributes. - # - # Currently supported attributes are .class and #id. - # - if (empty($attr)) return ""; - - # Split on components - preg_match_all('/[#.a-z][-_:a-zA-Z0-9=]+/', $attr, $matches); - $elements = $matches[0]; - - # handle classes and ids (only first id taken into account) - $classes = array(); - $attributes = array(); - $id = false; - foreach ($elements as $element) { - if ($element{0} == '.') { - $classes[] = substr($element, 1); - } else if ($element{0} == '#') { - if ($id === false) $id = substr($element, 1); - } else if (strpos($element, '=') > 0) { - $parts = explode('=', $element, 2); - $attributes[] = $parts[0] . '="' . $parts[1] . '"'; - } - } - - # compose attributes as string - $attr_str = ""; - if (!empty($id)) { - $attr_str .= ' id="'.$id.'"'; - } - if (!empty($classes)) { - $attr_str .= ' class="'.implode(" ", $classes).'"'; - } - if (!$this->no_markup && !empty($attributes)) { - $attr_str .= ' '.implode(" ", $attributes); - } - return $attr_str; - } - - - protected function stripLinkDefinitions($text) { - # - # Strips link definitions from text, stores the URLs and titles in - # hash references. - # - $less_than_tab = $this->tab_width - 1; - - # Link defs are in the form: ^[id]: url "optional title" - $text = preg_replace_callback('{ - ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 - [ ]* - \n? # maybe *one* newline - [ ]* - (?: - <(.+?)> # url = $2 - | - (\S+?) # url = $3 - ) - [ ]* - \n? # maybe one newline - [ ]* - (?: - (?<=\s) # lookbehind for whitespace - ["(] - (.*?) # title = $4 - [")] - [ ]* - )? # title is optional - (?:[ ]* '.$this->id_class_attr_catch_re.' )? # $5 = extra id & class attr - (?:\n+|\Z) - }xm', - array($this, '_stripLinkDefinitions_callback'), - $text); - return $text; - } - protected function _stripLinkDefinitions_callback($matches) { - $link_id = strtolower($matches[1]); - $url = $matches[2] == '' ? $matches[3] : $matches[2]; - $this->urls[$link_id] = $url; - $this->titles[$link_id] =& $matches[4]; - $this->ref_attr[$link_id] = $this->doExtraAttributes("", $dummy =& $matches[5]); - return ''; # String that will replace the block - } - - - ### HTML Block Parser ### - - # Tags that are always treated as block tags: - protected $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption|figure'; - - # Tags treated as block tags only if the opening tag is alone on its line: - protected $context_block_tags_re = 'script|noscript|style|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video'; - - # Tags where markdown="1" default to span mode: - protected $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address'; - - # Tags which must not have their contents modified, no matter where - # they appear: - protected $clean_tags_re = 'script|style|math|svg'; - - # Tags that do not need to be closed. - protected $auto_close_tags_re = 'hr|img|param|source|track'; - - - protected function hashHTMLBlocks($text) { - # - # Hashify HTML Blocks and "clean tags". - # - # We only want to do this for block-level HTML tags, such as headers, - # lists, and tables. That's because we still want to wrap <p>s around - # "paragraphs" that are wrapped in non-block-level tags, such as anchors, - # phrase emphasis, and spans. The list of tags we're looking for is - # hard-coded. - # - # This works by calling _HashHTMLBlocks_InMarkdown, which then calls - # _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1" - # attribute is found within a tag, _HashHTMLBlocks_InHTML calls back - # _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag. - # These two functions are calling each other. It's recursive! - # - if ($this->no_markup) return $text; - - # - # Call the HTML-in-Markdown hasher. - # - list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text); - - return $text; - } - protected function _hashHTMLBlocks_inMarkdown($text, $indent = 0, - $enclosing_tag_re = '', $span = false) - { - # - # Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags. - # - # * $indent is the number of space to be ignored when checking for code - # blocks. This is important because if we don't take the indent into - # account, something like this (which looks right) won't work as expected: - # - # <div> - # <div markdown="1"> - # Hello World. <-- Is this a Markdown code block or text? - # </div> <-- Is this a Markdown code block or a real tag? - # <div> - # - # If you don't like this, just don't indent the tag on which - # you apply the markdown="1" attribute. - # - # * If $enclosing_tag_re is not empty, stops at the first unmatched closing - # tag with that name. Nested tags supported. - # - # * If $span is true, text inside must treated as span. So any double - # newline will be replaced by a single newline so that it does not create - # paragraphs. - # - # Returns an array of that form: ( processed text , remaining text ) - # - if ($text === '') return array('', ''); - - # Regex to check for the presense of newlines around a block tag. - $newline_before_re = '/(?:^\n?|\n\n)*$/'; - $newline_after_re = - '{ - ^ # Start of text following the tag. - (?>[ ]*<!--.*?-->)? # Optional comment. - [ ]*\n # Must be followed by newline. - }xs'; - - # Regex to match any tag. - $block_tag_re = - '{ - ( # $2: Capture whole tag. - </? # Any opening or closing tag. - (?> # Tag name. - '.$this->block_tags_re.' | - '.$this->context_block_tags_re.' | - '.$this->clean_tags_re.' | - (?!\s)'.$enclosing_tag_re.' - ) - (?: - (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name. - (?> - ".*?" | # Double quotes (can contain `>`) - \'.*?\' | # Single quotes (can contain `>`) - .+? # Anything but quotes and `>`. - )*? - )? - > # End of tag. - | - <!-- .*? --> # HTML Comment - | - <\?.*?\?> | <%.*?%> # Processing instruction - | - <!\[CDATA\[.*?\]\]> # CData Block - '. ( !$span ? ' # If not in span. - | - # Indented code block - (?: ^[ ]*\n | ^ | \n[ ]*\n ) - [ ]{'.($indent+4).'}[^\n]* \n - (?> - (?: [ ]{'.($indent+4).'}[^\n]* | [ ]* ) \n - )* - | - # Fenced code block marker - (?<= ^ | \n ) - [ ]{0,'.($indent+3).'}(?:~{3,}|`{3,}) - [ ]* - (?: - \.?[-_:a-zA-Z0-9]+ # standalone class name - | - '.$this->id_class_attr_nocatch_re.' # extra attributes - )? - [ ]* - (?= \n ) - ' : '' ). ' # End (if not is span). - | - # Code span marker - # Note, this regex needs to go after backtick fenced - # code blocks but it should also be kept outside of the - # "if not in span" condition adding backticks to the parser - `+ - ) - }xs'; - - - $depth = 0; # Current depth inside the tag tree. - $parsed = ""; # Parsed text that will be returned. - - # - # Loop through every tag until we find the closing tag of the parent - # or loop until reaching the end of text if no parent tag specified. - # - do { - # - # Split the text using the first $tag_match pattern found. - # Text before pattern will be first in the array, text after - # pattern will be at the end, and between will be any catches made - # by the pattern. - # - $parts = preg_split($block_tag_re, $text, 2, - PREG_SPLIT_DELIM_CAPTURE); - - # If in Markdown span mode, add a empty-string span-level hash - # after each newline to prevent triggering any block element. - if ($span) { - $void = $this->hashPart("", ':'); - $newline = "$void\n"; - $parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void; - } - - $parsed .= $parts[0]; # Text before current tag. - - # If end of $text has been reached. Stop loop. - if (count($parts) < 3) { - $text = ""; - break; - } - - $tag = $parts[1]; # Tag to handle. - $text = $parts[2]; # Remaining text after current tag. - $tag_re = preg_quote($tag); # For use in a regular expression. - - # - # Check for: Fenced code block marker. - # Note: need to recheck the whole tag to disambiguate backtick - # fences from code spans - # - if (preg_match('{^\n?([ ]{0,'.($indent+3).'})(~{3,}|`{3,})[ ]*(?:\.?[-_:a-zA-Z0-9]+|'.$this->id_class_attr_nocatch_re.')?[ ]*\n?$}', $tag, $capture)) { - # Fenced code block marker: find matching end marker. - $fence_indent = strlen($capture[1]); # use captured indent in re - $fence_re = $capture[2]; # use captured fence in re - if (preg_match('{^(?>.*\n)*?[ ]{'.($fence_indent).'}'.$fence_re.'[ ]*(?:\n|$)}', $text, - $matches)) - { - # End marker found: pass text unchanged until marker. - $parsed .= $tag . $matches[0]; - $text = substr($text, strlen($matches[0])); - } - else { - # No end marker: just skip it. - $parsed .= $tag; - } - } - # - # Check for: Indented code block. - # - else if ($tag{0} == "\n" || $tag{0} == " ") { - # Indented code block: pass it unchanged, will be handled - # later. - $parsed .= $tag; - } - # - # Check for: Code span marker - # Note: need to check this after backtick fenced code blocks - # - else if ($tag{0} == "`") { - # Find corresponding end marker. - $tag_re = preg_quote($tag); - if (preg_match('{^(?>.+?|\n(?!\n))*?(?<!`)'.$tag_re.'(?!`)}', - $text, $matches)) - { - # End marker found: pass text unchanged until marker. - $parsed .= $tag . $matches[0]; - $text = substr($text, strlen($matches[0])); - } - else { - # Unmatched marker: just skip it. - $parsed .= $tag; - } - } - # - # Check for: Opening Block level tag or - # Opening Context Block tag (like ins and del) - # used as a block tag (tag is alone on it's line). - # - else if (preg_match('{^<(?:'.$this->block_tags_re.')\b}', $tag) || - ( preg_match('{^<(?:'.$this->context_block_tags_re.')\b}', $tag) && - preg_match($newline_before_re, $parsed) && - preg_match($newline_after_re, $text) ) - ) - { - # Need to parse tag and following text using the HTML parser. - list($block_text, $text) = - $this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true); - - # Make sure it stays outside of any paragraph by adding newlines. - $parsed .= "\n\n$block_text\n\n"; - } - # - # Check for: Clean tag (like script, math) - # HTML Comments, processing instructions. - # - else if (preg_match('{^<(?:'.$this->clean_tags_re.')\b}', $tag) || - $tag{1} == '!' || $tag{1} == '?') - { - # Need to parse tag and following text using the HTML parser. - # (don't check for markdown attribute) - list($block_text, $text) = - $this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false); - - $parsed .= $block_text; - } - # - # Check for: Tag with same name as enclosing tag. - # - else if ($enclosing_tag_re !== '' && - # Same name as enclosing tag. - preg_match('{^</?(?:'.$enclosing_tag_re.')\b}', $tag)) - { - # - # Increase/decrease nested tag count. - # - if ($tag{1} == '/') $depth--; - else if ($tag{strlen($tag)-2} != '/') $depth++; - - if ($depth < 0) { - # - # Going out of parent element. Clean up and break so we - # return to the calling function. - # - $text = $tag . $text; - break; - } - - $parsed .= $tag; - } - else { - $parsed .= $tag; - } - } while ($depth >= 0); - - return array($parsed, $text); - } - protected function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) { - # - # Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags. - # - # * Calls $hash_method to convert any blocks. - # * Stops when the first opening tag closes. - # * $md_attr indicate if the use of the `markdown="1"` attribute is allowed. - # (it is not inside clean tags) - # - # Returns an array of that form: ( processed text , remaining text ) - # - if ($text === '') return array('', ''); - - # Regex to match `markdown` attribute inside of a tag. - $markdown_attr_re = ' - { - \s* # Eat whitespace before the `markdown` attribute - markdown - \s*=\s* - (?> - (["\']) # $1: quote delimiter - (.*?) # $2: attribute value - \1 # matching delimiter - | - ([^\s>]*) # $3: unquoted attribute value - ) - () # $4: make $3 always defined (avoid warnings) - }xs'; - - # Regex to match any tag. - $tag_re = '{ - ( # $2: Capture whole tag. - </? # Any opening or closing tag. - [\w:$]+ # Tag name. - (?: - (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name. - (?> - ".*?" | # Double quotes (can contain `>`) - \'.*?\' | # Single quotes (can contain `>`) - .+? # Anything but quotes and `>`. - )*? - )? - > # End of tag. - | - <!-- .*? --> # HTML Comment - | - <\?.*?\?> | <%.*?%> # Processing instruction - | - <!\[CDATA\[.*?\]\]> # CData Block - ) - }xs'; - - $original_text = $text; # Save original text in case of faliure. - - $depth = 0; # Current depth inside the tag tree. - $block_text = ""; # Temporary text holder for current text. - $parsed = ""; # Parsed text that will be returned. - - # - # Get the name of the starting tag. - # (This pattern makes $base_tag_name_re safe without quoting.) - # - if (preg_match('/^<([\w:$]*)\b/', $text, $matches)) - $base_tag_name_re = $matches[1]; - - # - # Loop through every tag until we find the corresponding closing tag. - # - do { - # - # Split the text using the first $tag_match pattern found. - # Text before pattern will be first in the array, text after - # pattern will be at the end, and between will be any catches made - # by the pattern. - # - $parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); - - if (count($parts) < 3) { - # - # End of $text reached with unbalenced tag(s). - # In that case, we return original text unchanged and pass the - # first character as filtered to prevent an infinite loop in the - # parent function. - # - return array($original_text{0}, substr($original_text, 1)); - } - - $block_text .= $parts[0]; # Text before current tag. - $tag = $parts[1]; # Tag to handle. - $text = $parts[2]; # Remaining text after current tag. - - # - # Check for: Auto-close tag (like <hr/>) - # Comments and Processing Instructions. - # - if (preg_match('{^</?(?:'.$this->auto_close_tags_re.')\b}', $tag) || - $tag{1} == '!' || $tag{1} == '?') - { - # Just add the tag to the block as if it was text. - $block_text .= $tag; - } - else { - # - # Increase/decrease nested tag count. Only do so if - # the tag's name match base tag's. - # - if (preg_match('{^</?'.$base_tag_name_re.'\b}', $tag)) { - if ($tag{1} == '/') $depth--; - else if ($tag{strlen($tag)-2} != '/') $depth++; - } - - # - # Check for `markdown="1"` attribute and handle it. - # - if ($md_attr && - preg_match($markdown_attr_re, $tag, $attr_m) && - preg_match('/^1|block|span$/', $attr_m[2] . $attr_m[3])) - { - # Remove `markdown` attribute from opening tag. - $tag = preg_replace($markdown_attr_re, '', $tag); - - # Check if text inside this tag must be parsed in span mode. - $this->mode = $attr_m[2] . $attr_m[3]; - $span_mode = $this->mode == 'span' || $this->mode != 'block' && - preg_match('{^<(?:'.$this->contain_span_tags_re.')\b}', $tag); - - # Calculate indent before tag. - if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) { - $strlen = $this->utf8_strlen; - $indent = $strlen($matches[1], 'UTF-8'); - } else { - $indent = 0; - } - - # End preceding block with this tag. - $block_text .= $tag; - $parsed .= $this->$hash_method($block_text); - - # Get enclosing tag name for the ParseMarkdown function. - # (This pattern makes $tag_name_re safe without quoting.) - preg_match('/^<([\w:$]*)\b/', $tag, $matches); - $tag_name_re = $matches[1]; - - # Parse the content using the HTML-in-Markdown parser. - list ($block_text, $text) - = $this->_hashHTMLBlocks_inMarkdown($text, $indent, - $tag_name_re, $span_mode); - - # Outdent markdown text. - if ($indent > 0) { - $block_text = preg_replace("/^[ ]{1,$indent}/m", "", - $block_text); - } - - # Append tag content to parsed text. - if (!$span_mode) $parsed .= "\n\n$block_text\n\n"; - else $parsed .= "$block_text"; - - # Start over with a new block. - $block_text = ""; - } - else $block_text .= $tag; - } - - } while ($depth > 0); - - # - # Hash last block text that wasn't processed inside the loop. - # - $parsed .= $this->$hash_method($block_text); - - return array($parsed, $text); - } - - - protected function hashClean($text) { - # - # Called whenever a tag must be hashed when a function inserts a "clean" tag - # in $text, it passes through this function and is automaticaly escaped, - # blocking invalid nested overlap. - # - return $this->hashPart($text, 'C'); - } - - - protected function doAnchors($text) { - # - # Turn Markdown link shortcuts into XHTML <a> tags. - # - if ($this->in_anchor) return $text; - $this->in_anchor = true; - - # - # First, handle reference-style links: [link text] [id] - # - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - \[ - ('.$this->nested_brackets_re.') # link text = $2 - \] - - [ ]? # one optional space - (?:\n[ ]*)? # one optional newline followed by spaces - - \[ - (.*?) # id = $3 - \] - ) - }xs', - array($this, '_doAnchors_reference_callback'), $text); - - # - # Next, inline-style links: [link text](url "optional title") - # - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - \[ - ('.$this->nested_brackets_re.') # link text = $2 - \] - \( # literal paren - [ \n]* - (?: - <(.+?)> # href = $3 - | - ('.$this->nested_url_parenthesis_re.') # href = $4 - ) - [ \n]* - ( # $5 - ([\'"]) # quote char = $6 - (.*?) # Title = $7 - \6 # matching quote - [ \n]* # ignore any spaces/tabs between closing quote and ) - )? # title is optional - \) - (?:[ ]? '.$this->id_class_attr_catch_re.' )? # $8 = id/class attributes - ) - }xs', - array($this, '_doAnchors_inline_callback'), $text); - - # - # Last, handle reference-style shortcuts: [link text] - # These must come last in case you've also got [link text][1] - # or [link text](/foo) - # - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - \[ - ([^\[\]]+) # link text = $2; can\'t contain [ or ] - \] - ) - }xs', - array($this, '_doAnchors_reference_callback'), $text); - - $this->in_anchor = false; - return $text; - } - protected function _doAnchors_reference_callback($matches) { - $whole_match = $matches[1]; - $link_text = $matches[2]; - $link_id =& $matches[3]; - - if ($link_id == "") { - # for shortcut links like [this][] or [this]. - $link_id = $link_text; - } - - # lower-case and turn embedded newlines into spaces - $link_id = strtolower($link_id); - $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); - - if (isset($this->urls[$link_id])) { - $url = $this->urls[$link_id]; - $url = $this->encodeURLAttribute($url); - - $result = "<a href=\"$url\""; - if ( isset( $this->titles[$link_id] ) ) { - $title = $this->titles[$link_id]; - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; - } - if (isset($this->ref_attr[$link_id])) - $result .= $this->ref_attr[$link_id]; - - $link_text = $this->runSpanGamut($link_text); - $result .= ">$link_text</a>"; - $result = $this->hashPart($result); - } - else { - $result = $whole_match; - } - return $result; - } - protected function _doAnchors_inline_callback($matches) { - $whole_match = $matches[1]; - $link_text = $this->runSpanGamut($matches[2]); - $url = $matches[3] == '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); - - // if the URL was of the form <s p a c e s> it got caught by the HTML - // tag parser and hashed. Need to reverse the process before using the URL. - $unhashed = $this->unhash($url); - if ($unhashed != $url) - $url = preg_replace('/^<(.*)>$/', '\1', $unhashed); - - $url = $this->encodeURLAttribute($url); - - $result = "<a href=\"$url\""; - if (isset($title)) { - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; - } - $result .= $attr; - - $link_text = $this->runSpanGamut($link_text); - $result .= ">$link_text</a>"; - - return $this->hashPart($result); - } - - - protected function doImages($text) { - # - # Turn Markdown image shortcuts into <img> tags. - # - # - # First, handle reference-style labeled images: ![alt text][id] - # - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - !\[ - ('.$this->nested_brackets_re.') # alt text = $2 - \] - - [ ]? # one optional space - (?:\n[ ]*)? # one optional newline followed by spaces - - \[ - (.*?) # id = $3 - \] - - ) - }xs', - array($this, '_doImages_reference_callback'), $text); - - # - # Next, handle inline images: ![alt text](url "optional title") - # Don't forget: encode * and _ - # - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - !\[ - ('.$this->nested_brackets_re.') # alt text = $2 - \] - \s? # One optional whitespace character - \( # literal paren - [ \n]* - (?: - <(\S*)> # src url = $3 - | - ('.$this->nested_url_parenthesis_re.') # src url = $4 - ) - [ \n]* - ( # $5 - ([\'"]) # quote char = $6 - (.*?) # title = $7 - \6 # matching quote - [ \n]* - )? # title is optional - \) - (?:[ ]? '.$this->id_class_attr_catch_re.' )? # $8 = id/class attributes - ) - }xs', - array($this, '_doImages_inline_callback'), $text); - - return $text; - } - protected function _doImages_reference_callback($matches) { - $whole_match = $matches[1]; - $alt_text = $matches[2]; - $link_id = strtolower($matches[3]); - - if ($link_id == "") { - $link_id = strtolower($alt_text); # for shortcut links like ![this][]. - } - - $alt_text = $this->encodeAttribute($alt_text); - if (isset($this->urls[$link_id])) { - $url = $this->encodeURLAttribute($this->urls[$link_id]); - $result = "<img src=\"$url\" alt=\"$alt_text\""; - if (isset($this->titles[$link_id])) { - $title = $this->titles[$link_id]; - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; - } - if (isset($this->ref_attr[$link_id])) - $result .= $this->ref_attr[$link_id]; - $result .= $this->empty_element_suffix; - $result = $this->hashPart($result); - } - else { - # If there's no such link ID, leave intact: - $result = $whole_match; - } - - return $result; - } - protected function _doImages_inline_callback($matches) { - $whole_match = $matches[1]; - $alt_text = $matches[2]; - $url = $matches[3] == '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]); - - $alt_text = $this->encodeAttribute($alt_text); - $url = $this->encodeURLAttribute($url); - $result = "<img src=\"$url\" alt=\"$alt_text\""; - if (isset($title)) { - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; # $title already quoted - } - $result .= $attr; - $result .= $this->empty_element_suffix; - - return $this->hashPart($result); - } - - - protected function doHeaders($text) { - # - # Redefined to add id and class attribute support. - # - # Setext-style headers: - # Header 1 {#header1} - # ======== - # - # Header 2 {#header2 .class1 .class2} - # -------- - # - $text = preg_replace_callback( - '{ - (^.+?) # $1: Header text - (?:[ ]+ '.$this->id_class_attr_catch_re.' )? # $3 = id/class attributes - [ ]*\n(=+|-+)[ ]*\n+ # $3: Header footer - }mx', - array($this, '_doHeaders_callback_setext'), $text); - - # atx-style headers: - # # Header 1 {#header1} - # ## Header 2 {#header2} - # ## Header 2 with closing hashes ## {#header3.class1.class2} - # ... - # ###### Header 6 {.class2} - # - $text = preg_replace_callback('{ - ^(\#{1,6}) # $1 = string of #\'s - [ ]* - (.+?) # $2 = Header text - [ ]* - \#* # optional closing #\'s (not counted) - (?:[ ]+ '.$this->id_class_attr_catch_re.' )? # $3 = id/class attributes - [ ]* - \n+ - }xm', - array($this, '_doHeaders_callback_atx'), $text); - - return $text; - } - protected function _doHeaders_callback_setext($matches) { - if ($matches[3] == '-' && preg_match('{^- }', $matches[1])) - return $matches[0]; - $level = $matches[3]{0} == '=' ? 1 : 2; - $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2]); - $block = "<h$level$attr>".$this->runSpanGamut($matches[1])."</h$level>"; - return "\n" . $this->hashBlock($block) . "\n\n"; - } - protected function _doHeaders_callback_atx($matches) { - $level = strlen($matches[1]); - $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[3]); - $block = "<h$level$attr>".$this->runSpanGamut($matches[2])."</h$level>"; - return "\n" . $this->hashBlock($block) . "\n\n"; - } - - - protected function doTables($text) { - # - # Form HTML tables. - # - $less_than_tab = $this->tab_width - 1; - # - # Find tables with leading pipe. - # - # | Header 1 | Header 2 - # | -------- | -------- - # | Cell 1 | Cell 2 - # | Cell 3 | Cell 4 - # - $text = preg_replace_callback(' - { - ^ # Start of a line - [ ]{0,'.$less_than_tab.'} # Allowed whitespace. - [|] # Optional leading pipe (present) - (.+) \n # $1: Header row (at least one pipe) - - [ ]{0,'.$less_than_tab.'} # Allowed whitespace. - [|] ([ ]*[-:]+[-| :]*) \n # $2: Header underline - - ( # $3: Cells - (?> - [ ]* # Allowed whitespace. - [|] .* \n # Row content. - )* - ) - (?=\n|\Z) # Stop at final double newline. - }xm', - array($this, '_doTable_leadingPipe_callback'), $text); - - # - # Find tables without leading pipe. - # - # Header 1 | Header 2 - # -------- | -------- - # Cell 1 | Cell 2 - # Cell 3 | Cell 4 - # - $text = preg_replace_callback(' - { - ^ # Start of a line - [ ]{0,'.$less_than_tab.'} # Allowed whitespace. - (\S.*[|].*) \n # $1: Header row (at least one pipe) - - [ ]{0,'.$less_than_tab.'} # Allowed whitespace. - ([-:]+[ ]*[|][-| :]*) \n # $2: Header underline - - ( # $3: Cells - (?> - .* [|] .* \n # Row content - )* - ) - (?=\n|\Z) # Stop at final double newline. - }xm', - array($this, '_DoTable_callback'), $text); - - return $text; - } - protected function _doTable_leadingPipe_callback($matches) { - $head = $matches[1]; - $underline = $matches[2]; - $content = $matches[3]; - - # Remove leading pipe for each row. - $content = preg_replace('/^ *[|]/m', '', $content); - - return $this->_doTable_callback(array($matches[0], $head, $underline, $content)); - } - protected function _doTable_makeAlignAttr($alignname) - { - if (empty($this->table_align_class_tmpl)) - return " align=\"$alignname\""; - - $classname = str_replace('%%', $alignname, $this->table_align_class_tmpl); - return " class=\"$classname\""; - } - protected function _doTable_callback($matches) { - $head = $matches[1]; - $underline = $matches[2]; - $content = $matches[3]; - - # Remove any tailing pipes for each line. - $head = preg_replace('/[|] *$/m', '', $head); - $underline = preg_replace('/[|] *$/m', '', $underline); - $content = preg_replace('/[|] *$/m', '', $content); - - # Reading alignement from header underline. - $separators = preg_split('/ *[|] */', $underline); - foreach ($separators as $n => $s) { - if (preg_match('/^ *-+: *$/', $s)) - $attr[$n] = $this->_doTable_makeAlignAttr('right'); - else if (preg_match('/^ *:-+: *$/', $s)) - $attr[$n] = $this->_doTable_makeAlignAttr('center'); - else if (preg_match('/^ *:-+ *$/', $s)) - $attr[$n] = $this->_doTable_makeAlignAttr('left'); - else - $attr[$n] = ''; - } - - # Parsing span elements, including code spans, character escapes, - # and inline HTML tags, so that pipes inside those gets ignored. - $head = $this->parseSpan($head); - $headers = preg_split('/ *[|] */', $head); - $col_count = count($headers); - $attr = array_pad($attr, $col_count, ''); - - # Write column headers. - $text = "<table>\n"; - $text .= "<thead>\n"; - $text .= "<tr>\n"; - foreach ($headers as $n => $header) - $text .= " <th$attr[$n]>".$this->runSpanGamut(trim($header))."</th>\n"; - $text .= "</tr>\n"; - $text .= "</thead>\n"; - - # Split content by row. - $rows = explode("\n", trim($content, "\n")); - - $text .= "<tbody>\n"; - foreach ($rows as $row) { - # Parsing span elements, including code spans, character escapes, - # and inline HTML tags, so that pipes inside those gets ignored. - $row = $this->parseSpan($row); - - # Split row by cell. - $row_cells = preg_split('/ *[|] */', $row, $col_count); - $row_cells = array_pad($row_cells, $col_count, ''); - - $text .= "<tr>\n"; - foreach ($row_cells as $n => $cell) - $text .= " <td$attr[$n]>".$this->runSpanGamut(trim($cell))."</td>\n"; - $text .= "</tr>\n"; - } - $text .= "</tbody>\n"; - $text .= "</table>"; - - return $this->hashBlock($text) . "\n"; - } - - - protected function doDefLists($text) { - # - # Form HTML definition lists. - # - $less_than_tab = $this->tab_width - 1; - - # Re-usable pattern to match any entire dl list: - $whole_list_re = '(?> - ( # $1 = whole list - ( # $2 - [ ]{0,'.$less_than_tab.'} - ((?>.*\S.*\n)+) # $3 = defined term - \n? - [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition - ) - (?s:.+?) - ( # $4 - \z - | - \n{2,} - (?=\S) - (?! # Negative lookahead for another term - [ ]{0,'.$less_than_tab.'} - (?: \S.*\n )+? # defined term - \n? - [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition - ) - (?! # Negative lookahead for another definition - [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition - ) - ) - ) - )'; // mx - - $text = preg_replace_callback('{ - (?>\A\n?|(?<=\n\n)) - '.$whole_list_re.' - }mx', - array($this, '_doDefLists_callback'), $text); - - return $text; - } - protected function _doDefLists_callback($matches) { - # Re-usable patterns to match list item bullets and number markers: - $list = $matches[1]; - - # Turn double returns into triple returns, so that we can make a - # paragraph for the last item in a list, if necessary: - $result = trim($this->processDefListItems($list)); - $result = "<dl>\n" . $result . "\n</dl>"; - return $this->hashBlock($result) . "\n\n"; - } - - - protected function processDefListItems($list_str) { - # - # Process the contents of a single definition list, splitting it - # into individual term and definition list items. - # - $less_than_tab = $this->tab_width - 1; - - # trim trailing blank lines: - $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); - - # Process definition terms. - $list_str = preg_replace_callback('{ - (?>\A\n?|\n\n+) # leading line - ( # definition terms = $1 - [ ]{0,'.$less_than_tab.'} # leading whitespace - (?!\:[ ]|[ ]) # negative lookahead for a definition - # mark (colon) or more whitespace. - (?> \S.* \n)+? # actual term (not whitespace). - ) - (?=\n?[ ]{0,3}:[ ]) # lookahead for following line feed - # with a definition mark. - }xm', - array($this, '_processDefListItems_callback_dt'), $list_str); - - # Process actual definitions. - $list_str = preg_replace_callback('{ - \n(\n+)? # leading line = $1 - ( # marker space = $2 - [ ]{0,'.$less_than_tab.'} # whitespace before colon - \:[ ]+ # definition mark (colon) - ) - ((?s:.+?)) # definition text = $3 - (?= \n+ # stop at next definition mark, - (?: # next term or end of text - [ ]{0,'.$less_than_tab.'} \:[ ] | - <dt> | \z - ) - ) - }xm', - array($this, '_processDefListItems_callback_dd'), $list_str); - - return $list_str; - } - protected function _processDefListItems_callback_dt($matches) { - $terms = explode("\n", trim($matches[1])); - $text = ''; - foreach ($terms as $term) { - $term = $this->runSpanGamut(trim($term)); - $text .= "\n<dt>" . $term . "</dt>"; - } - return $text . "\n"; - } - protected function _processDefListItems_callback_dd($matches) { - $leading_line = $matches[1]; - $marker_space = $matches[2]; - $def = $matches[3]; - - if ($leading_line || preg_match('/\n{2,}/', $def)) { - # Replace marker with the appropriate whitespace indentation - $def = str_repeat(' ', strlen($marker_space)) . $def; - $def = $this->runBlockGamut($this->outdent($def . "\n\n")); - $def = "\n". $def ."\n"; - } - else { - $def = rtrim($def); - $def = $this->runSpanGamut($this->outdent($def)); - } - - return "\n<dd>" . $def . "</dd>\n"; - } - - - protected function doFencedCodeBlocks($text) { - # - # Adding the fenced code block syntax to regular Markdown: - # - # ~~~ - # Code block - # ~~~ - # - $less_than_tab = $this->tab_width; - - $text = preg_replace_callback('{ - (?:\n|\A) - # 1: Opening marker - ( - (?:~{3,}|`{3,}) # 3 or more tildes/backticks. - ) - [ ]* - (?: - \.?([-_:a-zA-Z0-9]+) # 2: standalone class name - | - '.$this->id_class_attr_catch_re.' # 3: Extra attributes - )? - [ ]* \n # Whitespace and newline following marker. - - # 4: Content - ( - (?> - (?!\1 [ ]* \n) # Not a closing marker. - .*\n+ - )+ - ) - - # Closing marker. - \1 [ ]* (?= \n ) - }xm', - array($this, '_doFencedCodeBlocks_callback'), $text); - - return $text; - } - protected function _doFencedCodeBlocks_callback($matches) { - $classname =& $matches[2]; - $attrs =& $matches[3]; - $codeblock = $matches[4]; - $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); - $codeblock = preg_replace_callback('/^\n+/', - array($this, '_doFencedCodeBlocks_newlines'), $codeblock); - - if ($classname != "") { - if ($classname{0} == '.') - $classname = substr($classname, 1); - $attr_str = ' class="'.$this->code_class_prefix.$classname.'"'; - } else { - $attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs); - } - $pre_attr_str = $this->code_attr_on_pre ? $attr_str : ''; - $code_attr_str = $this->code_attr_on_pre ? '' : $attr_str; - $codeblock = "<pre$pre_attr_str><code$code_attr_str>$codeblock</code></pre>"; - - return "\n\n".$this->hashBlock($codeblock)."\n\n"; - } - protected function _doFencedCodeBlocks_newlines($matches) { - return str_repeat("<br$this->empty_element_suffix", - strlen($matches[0])); - } - - - # - # Redefining emphasis markers so that emphasis by underscore does not - # work in the middle of a word. - # - protected $em_relist = array( - '' => '(?:(?<!\*)\*(?!\*)|(?<![a-zA-Z0-9_])_(?!_))(?![\.,:;]?\s)', - '*' => '(?<![\s*])\*(?!\*)', - '_' => '(?<![\s_])_(?![a-zA-Z0-9_])', - ); - protected $strong_relist = array( - '' => '(?:(?<!\*)\*\*(?!\*)|(?<![a-zA-Z0-9_])__(?!_))(?![\.,:;]?\s)', - '**' => '(?<![\s*])\*\*(?!\*)', - '__' => '(?<![\s_])__(?![a-zA-Z0-9_])', - ); - protected $em_strong_relist = array( - '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<![a-zA-Z0-9_])___(?!_))(?![\.,:;]?\s)', - '***' => '(?<![\s*])\*\*\*(?!\*)', - '___' => '(?<![\s_])___(?![a-zA-Z0-9_])', - ); - - - protected function formParagraphs($text) { - # - # Params: - # $text - string to process with html <p> tags - # - # Strip leading and trailing lines: - $text = preg_replace('/\A\n+|\n+\z/', '', $text); - - $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); - - # - # Wrap <p> tags and unhashify HTML blocks - # - foreach ($grafs as $key => $value) { - $value = trim($this->runSpanGamut($value)); - - # Check if this should be enclosed in a paragraph. - # Clean tag hashes & block tag hashes are left alone. - $is_p = !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value); - - if ($is_p) { - $value = "<p>$value</p>"; - } - $grafs[$key] = $value; - } - - # Join grafs in one text, then unhash HTML tags. - $text = implode("\n\n", $grafs); - - # Finish by removing any tag hashes still present in $text. - $text = $this->unhash($text); - - return $text; - } - - - ### Footnotes - - protected function stripFootnotes($text) { - # - # Strips link definitions from text, stores the URLs and titles in - # hash references. - # - $less_than_tab = $this->tab_width - 1; - - # Link defs are in the form: [^id]: url "optional title" - $text = preg_replace_callback('{ - ^[ ]{0,'.$less_than_tab.'}\[\^(.+?)\][ ]?: # note_id = $1 - [ ]* - \n? # maybe *one* newline - ( # text = $2 (no blank lines allowed) - (?: - .+ # actual text - | - \n # newlines but - (?!\[.+?\][ ]?:\s)# negative lookahead for footnote or link definition marker. - (?!\n+[ ]{0,3}\S)# ensure line is not blank and followed - # by non-indented content - )* - ) - }xm', - array($this, '_stripFootnotes_callback'), - $text); - return $text; - } - protected function _stripFootnotes_callback($matches) { - $note_id = $this->fn_id_prefix . $matches[1]; - $this->footnotes[$note_id] = $this->outdent($matches[2]); - return ''; # String that will replace the block - } - - - protected function doFootnotes($text) { - # - # Replace footnote references in $text [^id] with a special text-token - # which will be replaced by the actual footnote marker in appendFootnotes. - # - if (!$this->in_anchor) { - $text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text); - } - return $text; - } - - - protected function appendFootnotes($text) { - # - # Append footnote list to text. - # - $text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', - array($this, '_appendFootnotes_callback'), $text); - - if (!empty($this->footnotes_ordered)) { - $text .= "\n\n"; - $text .= "<div class=\"footnotes\">\n"; - $text .= "<hr". $this->empty_element_suffix ."\n"; - $text .= "<ol>\n\n"; - - $attr = ""; - if ($this->fn_backlink_class != "") { - $class = $this->fn_backlink_class; - $class = $this->encodeAttribute($class); - $attr .= " class=\"$class\""; - } - if ($this->fn_backlink_title != "") { - $title = $this->fn_backlink_title; - $title = $this->encodeAttribute($title); - $attr .= " title=\"$title\""; - } - $num = 0; - - while (!empty($this->footnotes_ordered)) { - $footnote = reset($this->footnotes_ordered); - $note_id = key($this->footnotes_ordered); - unset($this->footnotes_ordered[$note_id]); - $ref_count = $this->footnotes_ref_count[$note_id]; - unset($this->footnotes_ref_count[$note_id]); - unset($this->footnotes[$note_id]); - - $footnote .= "\n"; # Need to append newline before parsing. - $footnote = $this->runBlockGamut("$footnote\n"); - $footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', - array($this, '_appendFootnotes_callback'), $footnote); - - $attr = str_replace("%%", ++$num, $attr); - $note_id = $this->encodeAttribute($note_id); - - # Prepare backlink, multiple backlinks if multiple references - $backlink = "<a href=\"#fnref:$note_id\"$attr>&#8617;</a>"; - for ($ref_num = 2; $ref_num <= $ref_count; ++$ref_num) { - $backlink .= " <a href=\"#fnref$ref_num:$note_id\"$attr>&#8617;</a>"; - } - # Add backlink to last paragraph; create new paragraph if needed. - if (preg_match('{</p>$}', $footnote)) { - $footnote = substr($footnote, 0, -4) . "&#160;$backlink</p>"; - } else { - $footnote .= "\n\n<p>$backlink</p>"; - } - - $text .= "<li id=\"fn:$note_id\">\n"; - $text .= $footnote . "\n"; - $text .= "</li>\n\n"; - } - - $text .= "</ol>\n"; - $text .= "</div>"; - } - return $text; - } - protected function _appendFootnotes_callback($matches) { - $node_id = $this->fn_id_prefix . $matches[1]; - - # Create footnote marker only if it has a corresponding footnote *and* - # the footnote hasn't been used by another marker. - if (isset($this->footnotes[$node_id])) { - $num =& $this->footnotes_numbers[$node_id]; - if (!isset($num)) { - # Transfer footnote content to the ordered list and give it its - # number - $this->footnotes_ordered[$node_id] = $this->footnotes[$node_id]; - $this->footnotes_ref_count[$node_id] = 1; - $num = $this->footnote_counter++; - $ref_count_mark = ''; - } else { - $ref_count_mark = $this->footnotes_ref_count[$node_id] += 1; - } - - $attr = ""; - if ($this->fn_link_class != "") { - $class = $this->fn_link_class; - $class = $this->encodeAttribute($class); - $attr .= " class=\"$class\""; - } - if ($this->fn_link_title != "") { - $title = $this->fn_link_title; - $title = $this->encodeAttribute($title); - $attr .= " title=\"$title\""; - } - - $attr = str_replace("%%", $num, $attr); - $node_id = $this->encodeAttribute($node_id); - - return - "<sup id=\"fnref$ref_count_mark:$node_id\">". - "<a href=\"#fn:$node_id\"$attr>$num</a>". - "</sup>"; - } - - return "[^".$matches[1]."]"; - } - - - ### Abbreviations ### - - protected function stripAbbreviations($text) { - # - # Strips abbreviations from text, stores titles in hash references. - # - $less_than_tab = $this->tab_width - 1; - - # Link defs are in the form: [id]*: url "optional title" - $text = preg_replace_callback('{ - ^[ ]{0,'.$less_than_tab.'}\*\[(.+?)\][ ]?: # abbr_id = $1 - (.*) # text = $2 (no blank lines allowed) - }xm', - array($this, '_stripAbbreviations_callback'), - $text); - return $text; - } - protected function _stripAbbreviations_callback($matches) { - $abbr_word = $matches[1]; - $abbr_desc = $matches[2]; - if ($this->abbr_word_re) - $this->abbr_word_re .= '|'; - $this->abbr_word_re .= preg_quote($abbr_word); - $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); - return ''; # String that will replace the block - } - - - protected function doAbbreviations($text) { - # - # Find defined abbreviations in text and wrap them in <abbr> elements. - # - if ($this->abbr_word_re) { - // cannot use the /x modifier because abbr_word_re may - // contain significant spaces: - $text = preg_replace_callback('{'. - '(?<![\w\x1A])'. - '(?:'.$this->abbr_word_re.')'. - '(?![\w\x1A])'. - '}', - array($this, '_doAbbreviations_callback'), $text); - } - return $text; - } - protected function _doAbbreviations_callback($matches) { - $abbr = $matches[0]; - if (isset($this->abbr_desciptions[$abbr])) { - $desc = $this->abbr_desciptions[$abbr]; - if (empty($desc)) { - return $this->hashPart("<abbr>$abbr</abbr>"); - } else { - $desc = $this->encodeAttribute($desc); - return $this->hashPart("<abbr title=\"$desc\">$abbr</abbr>"); - } - } else { - return $matches[0]; - } - } - -} - -$yellow->plugins->register("markdownextra", "YellowMarkdownExtra", YellowMarkdownExtra::Version); -?> -\ No newline at end of file diff --git a/system/snippets/header.php b/system/snippets/header.php @@ -1,17 +0,0 @@ -<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml" lang="<?php echo $yellow->page->getHtml("language") ?>"> -<head> -<meta charset="utf-8" /> -<meta name="description" content="<?php echo $yellow->page->getHtml("description") ?>" /> -<meta name="keywords" content="<?php echo $yellow->page->getHtml("keywords") ?>" /> -<meta name="author" content="<?php echo $yellow->page->getHtml("author") ?>" /> -<meta name="viewport" content="width=device-width, initial-scale=1" /> -<?php if($yellow->page->isExisting("robots")): ?> -<meta name="robots" content="<?php echo $yellow->page->getHtml("robots") ?>" /> -<?php endif ?> -<title><?php echo $yellow->page->getHtml("titleHeader") ?></title> -<link rel="shortcut icon" href="<?php echo $yellow->config->get("serverBase").$yellow->config->get("imageLocation")."icon.png" ?>" /> -<link rel="stylesheet" type="text/css" media="all" href="<?php echo $yellow->config->get("serverBase").$yellow->config->get("themeLocation").$yellow->page->get("theme").".css" ?>" /> -<?php echo $yellow->page->getHeaderExtra() ?> -</head> -<body> -<div class="page"> diff --git a/system/snippets/pagination.php b/system/snippets/pagination.php @@ -1,11 +0,0 @@ -<?php list($name, $pages) = $yellow->getSnippetArgs() ?> -<?php if($pages->isPagination()): ?> -<div class="pagination"> -<?php if($pages->getLocationPrevious()): ?> -<a class="previous" href="<?php echo $pages->getLocationPrevious() ?>"><?php echo $yellow->text->getHtml("paginationPrevious") ?></a> -<?php endif ?> -<?php if($pages->getLocationNext()): ?> -<a class="next" href="<?php echo $pages->getLocationNext() ?>"><?php echo $yellow->text->getHtml("paginationNext") ?></a> -<?php endif ?> -</div> -<?php endif ?> diff --git a/system/templates/default.php b/system/templates/default.php @@ -1,6 +0,0 @@ -<?php /* Default template */ ?> -<?php $yellow->snippet("header") ?> -<?php $yellow->snippet("sitename") ?> -<?php $yellow->snippet("navigation") ?> -<?php $yellow->snippet("content") ?> -<?php $yellow->snippet("footer") ?> -\ No newline at end of file diff --git a/system/themes/default.css b/system/themes/default.css @@ -1,59 +0,0 @@ -/* Default theme 0.5.2 */ -/* Designer: Datenstrom Sweden */ - -html, body, div, form, pre, span, tr, th, td { margin:0; padding:0; border:0; vertical-align:baseline; } -body { - margin:1em; - background-color:#fff; color:#717171; - font-family:"Helvetica Neue",Helvetica,sans-serif; - font-size:1.0em; - font-weight:200; - line-height:1.5; -} -h1, h2, h3, h4, h5, h6 { color:#07d; font-weight:normal; } -hr { height:1px; background:#ddd; border:0; } -strong { font-weight:bold; } -code { font-size:1.1em; } -a { color:#07d; } -a:hover { color:#07d; text-decoration:underline; } -a, img { border:none; text-decoration:none; } -.sitename h1 { margin-top:0.5em; margin-bottom:0em; font-size:2.2em; font-weight:500; } -.sitename h1 a { color:#111; text-decoration:none; } -.navigation { margin-bottom:1em; line-height:2em; } -.navigation a { color:#111; padding:0 0.3em; display:inline-block; } -.navigation a:hover { color:#07d; } -.navigation ul { margin:0 -0.3em; padding:0; list-style:none; } -.navigation li { display:inline; } -.content h1 a:hover { text-decoration:none; } -.content img { max-width:100%; height:auto; } -.content form { margin:1em 0; } -.content table { border-spacing:0; border-collapse:collapse; } -.content th { text-align:left; padding:0.3em; border-bottom:1px solid #ddd;} -.content td { text-align:left; padding:0.3em; border-top:1px solid #ddd;} -.content .flexible { position:relative; padding-bottom:56.25%; padding-top:30px; } -.content .flexible iframe { position:absolute; top:0; left:0; width:100%; height:100%; } -.content .toc { margin:0; padding:0; list-style:none; } -.pagination { margin:1em 0; } -.footer { margin-top:1em; } -.footer a { color:#717171; } -.footer a:hover { color:#07d; text-decoration:underline; } -.left { float:left; margin:0 1em 0 0; } -.center { display:block; margin:0 auto; } -.right { float:right; margin:0 0 0 1em; } - -/* Responsive and print */ - -.page { margin:0 auto; max-width:62em; } - -@media screen and (min-width:62em) { - body { width:60em; margin:1em auto; } -} -@media screen and (max-width:30em) { - body { margin:0.5em; font-size:0.9em; } - .sitename h1, h1, h2 { font-size:1.3em; } - .sitename h1, .sitename, .navigation, .footer, .page { margin:0; padding:0; border:0; } -} -@media print { - body, h1, h2, h3, h4, h5, h6 { background-color:white; color:black; } - .page { border:none !important; } -} -\ No newline at end of file diff --git a/system/themes/flatsite.css b/system/themes/flatsite.css @@ -0,0 +1,101 @@ +/* Flatsite theme 0.1.7 */ +/* Designer: Mark Mayberg */ + +@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,700); +html, body, div, form, pre, span, tr, th, td { margin:0; padding:0; border:0; vertical-align:baseline; } +body { + margin:1em; + background-color:#fff; color:#717171; + font-family:'Open Sans',sans-serif; + font-size:1.0em; + font-weight:300; + line-height:1.5; +} +h1, h2, h3, h4, h5, h6 { color:#07d; font-weight:normal; } +h1 { font-size:2.0em; } +hr { height:1px; background:#ddd; border:0; } +strong { font-weight:bold; } +code { font-size:1.1em; } +a { color:#07d; } +a:hover { color:#07d; text-decoration:underline; } +a, img { border:none; text-decoration:none; } +.sitename { display:block; float:left; } +.sitename h1 { margin:0; } +.sitename h1 a { color:#111; text-decoration:none; } +.navigation { display:block; float:right; } +.navigation { margin-top:0.9em; margin-bottom:0.9em; line-height:2em; } +.navigation a { padding:0 0.3em; display:inline-block; } +.navigation ul { margin:0 -0.3em; padding:0; list-style:none; } +.navigation li { display:inline; } +.content { clear:both; } +.content h1 a:hover { text-decoration:none; } +.content img { max-width:100%; height:auto; } +.content form { margin:1em 0; } +.content table { border-spacing:0; border-collapse:collapse; } +.content th { text-align:left; padding:0.3em; border-bottom:1px solid #ddd;} +.content td { text-align:left; padding:0.3em; border-top:1px solid #ddd;} +.content .flexible { position:relative; padding-bottom:56.25%; padding-top:30px; } +.content .flexible iframe { position:absolute; top:0; left:0; width:100%; height:100%; } +.content .toc { margin:0; padding:0; list-style:none; } +.content code { border:1px solid #ddd; border-radius:3px; padding:0 0.5em; } +.content pre>code { border:none; padding:0; } +.content pre { border:1px solid #ddd; border-radius:3px; padding:1em; overflow:hidden; } +.content .imagelist { margin:0; padding:0; list-style:none; } +.content .themes { margin:0; padding:0; list-style:none; width:100%; } +.content .themes li { padding-bottom:1em; text-align:center; white-space:nowrap; display:inline-block; width:24%; } +.pagination { margin:1em 0; } +.footer { margin-top:2em; } +.footer a { color:#07d; } +.footer a:hover { color:#07d; text-decoration:underline; } +.left { float:left; margin:0 1em 0 0; } +.center { display:block; margin:0 auto; } +.right { float:right; margin:0 0 0 1em; } + +/* Forms and buttons */ + +.form-control { + margin:0; padding:2px 4px; + display:inline-block; + background-color:#fff; color:#555; + background-image:linear-gradient(to bottom, #fff, #fff); + border:1px solid #bbb; + border-radius:4px; + font-size:0.9em; font-family:inherit; font-weight:normal; line-height:1; +} +.btn { + margin:0; padding:4px 22px; + display:inline-block; min-width:8em; + background-color:#eaeaea; color:#333333; + background-image:linear-gradient(to bottom, #f8f8f8, #e1e1e1); + border:1px solid #bbb; + border-color:#c1c1c1 #c1c1c1 #aaaaaa; + border-radius:4px; + outline-offset:-2px; + font-size:0.9em; font-family:inherit; font-weight:normal; line-height:1; + text-align:center; text-decoration:none; +} +.btn:hover, .btn:focus, .btn:active { + color:#333333; + background-image:none; + text-decoration:none; +} +.btn:active { box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.1); } + +/* Responsive and print */ + +.page { margin:0 auto; max-width:1000px; } + +@media screen and (min-width:62em) { + body { width:60em; margin:1em auto; } + .page{ margin:0; max-width:none; } +} +@media screen and (max-width:30em) { + body { margin:0.5em; font-size:0.9em; } + .sitename h1, h1, h2 { font-size:1.2em; } + .sitename h1, .header, .navigation, .footer, .page { margin:0; padding:0; } + .sitename, .navigation { float:none; } +} +@media print { + body, h1, h2, h3, h4, h5, h6 { background-color:white; color:black; } + .page { border:none !important; } +} +\ No newline at end of file diff --git a/system/snippets/content.php b/system/themes/snippets/content.php diff --git a/system/snippets/footer.php b/system/themes/snippets/footer.php diff --git a/system/themes/snippets/header.php b/system/themes/snippets/header.php @@ -0,0 +1,17 @@ +<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml" lang="<?php echo $yellow->page->getHtml("language") ?>"> +<head> +<meta charset="utf-8" /> +<meta name="description" content="<?php echo $yellow->page->getHtml("description") ?>" /> +<meta name="keywords" content="<?php echo $yellow->page->getHtml("keywords") ?>" /> +<meta name="author" content="<?php echo $yellow->page->getHtml("author") ?>" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<?php if($yellow->page->isExisting("robots")): ?> +<meta name="robots" content="<?php echo $yellow->page->getHtml("robots") ?>" /> +<?php endif ?> +<title><?php echo $yellow->page->getHtml("titleHeader") ?></title> +<link rel="shortcut icon" href="<?php echo $yellow->config->get("serverBase").$yellow->config->get("imageLocation")."icon.png" ?>" /> +<link rel="stylesheet" type="text/css" media="all" href="<?php echo $yellow->config->get("serverBase").$yellow->config->get("themeLocation").$yellow->page->get("theme").".css" ?>" /> +<?php echo $yellow->page->getExtra() ?> +</head> +<body> +<div class="page"> diff --git a/system/snippets/navigation.php b/system/themes/snippets/navigation.php diff --git a/system/themes/snippets/pagination.php b/system/themes/snippets/pagination.php @@ -0,0 +1,11 @@ +<?php list($name, $pages) = $yellow->getSnippetArgs() ?> +<?php if($pages->isPagination()): ?> +<div class="pagination"> +<?php if($pages->getPaginationPrevious()): ?> +<a class="previous" href="<?php echo $pages->getPaginationPrevious() ?>"><?php echo $yellow->text->getHtml("paginationPrevious") ?></a> +<?php endif ?> +<?php if($pages->getPaginationNext()): ?> +<a class="next" href="<?php echo $pages->getPaginationNext() ?>"><?php echo $yellow->text->getHtml("paginationNext") ?></a> +<?php endif ?> +</div> +<?php endif ?> diff --git a/system/snippets/sitename.php b/system/themes/snippets/sitename.php diff --git a/system/themes/templates/default.html b/system/themes/templates/default.html @@ -0,0 +1,5 @@ +<?php $yellow->snippet("header") ?> +<?php $yellow->snippet("sitename") ?> +<?php $yellow->snippet("navigation") ?> +<?php $yellow->snippet("content") ?> +<?php $yellow->snippet("footer") ?> +\ No newline at end of file