From f8d681efacb10c832d14e77c531646a56942963a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E5=B9=BF=E6=98=8E?= Date: Mon, 6 Jun 2022 13:54:06 +0800 Subject: [PATCH] * Fix bugs. --- doc/LICENSE.EN | 5 + lib/michelf/Michelf/Markdown.inc.php | 10 - lib/michelf/Michelf/Markdown.php | 1889 ---------------- lib/michelf/Michelf/MarkdownExtra.inc.php | 11 - lib/michelf/Michelf/MarkdownExtra.php | 1870 ---------------- lib/michelf/Michelf/MarkdownInterface.inc.php | 9 - lib/michelf/Michelf/MarkdownInterface.php | 38 - lib/michelf/michelf.class.php | 27 - lib/parsedownextraplugin/LICENSE.txt | 20 + lib/parsedownextraplugin/index.html | 0 lib/parsedownextraplugin/parsedown.php | 1992 +++++++++++++++++ lib/parsedownextraplugin/parsedownextra.php | 686 ++++++ .../parsedownextraplugin.class.php | 590 +++++ module/common/model.php | 8 +- 14 files changed, 3298 insertions(+), 3857 deletions(-) delete mode 100644 lib/michelf/Michelf/Markdown.inc.php delete mode 100644 lib/michelf/Michelf/Markdown.php delete mode 100644 lib/michelf/Michelf/MarkdownExtra.inc.php delete mode 100644 lib/michelf/Michelf/MarkdownExtra.php delete mode 100644 lib/michelf/Michelf/MarkdownInterface.inc.php delete mode 100644 lib/michelf/Michelf/MarkdownInterface.php delete mode 100644 lib/michelf/michelf.class.php create mode 100644 lib/parsedownextraplugin/LICENSE.txt create mode 100644 lib/parsedownextraplugin/index.html create mode 100644 lib/parsedownextraplugin/parsedown.php create mode 100644 lib/parsedownextraplugin/parsedownextra.php create mode 100644 lib/parsedownextraplugin/parsedownextraplugin.class.php diff --git a/doc/LICENSE.EN b/doc/LICENSE.EN index fd21d19cac..714eef045c 100644 --- a/doc/LICENSE.EN +++ b/doc/LICENSE.EN @@ -1,3 +1,8 @@ +ZenTao Project Management Software is licensed under the Z PUBLIC LICENSE (ZPL) 1.2 and the Affero General Public License (AGPL). +The ZPL is applied by default, or you can choose to use our software under the AGPL. + +----------------------------------------------------------------------------------------------------------------------------------------------------------------- + Z PUBLIC LICENSE 1.2 Authorization diff --git a/lib/michelf/Michelf/Markdown.inc.php b/lib/michelf/Michelf/Markdown.inc.php deleted file mode 100644 index e2bd3808e8..0000000000 --- a/lib/michelf/Michelf/Markdown.inc.php +++ /dev/null @@ -1,10 +0,0 @@ - - * @copyright 2004-2019 Michel Fortin - * @copyright (Original Markdown) 2004-2006 John Gruber - */ - -namespace Michelf; - -/** - * Markdown Parser Class - */ -class Markdown implements MarkdownInterface { - /** - * Define the package version - * @var string - */ - const MARKDOWNLIB_VERSION = "2.0"; - - /** - * Simple function interface - Initialize the parser and return the result - * of its transform method. This will work fine for derived classes too. - * - * @api - * - * @param string $text - * @return string - */ - public static function defaultTransform($text) { - // Take parser class on which this function was called. - $parser_class = static::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 = " />"; - - /** - * The width of indentation of the output markup - */ - public $tab_width = 4; - - /** - * Change to `true` to disallow markup or entities. - */ - public $no_markup = false; - public $no_entities = false; - - - /** - * Change to `true` to enable line breaks on \n without two trailling spaces - * @var boolean - */ - public $hard_wrap = false; - - /** - * Predefined URLs and titles for reference links and images. - */ - public $predef_urls = array(); - public $predef_titles = array(); - - /** - * Optional filter function for URLs - * @var callable|null - */ - public $url_filter_func = null; - - /** - * Optional header id="" generation callback function. - * @var callable|null - */ - public $header_id_func = null; - - /** - * Optional function for converting code block content to HTML - * @var callable|null - */ - public $code_block_content_func = null; - - /** - * Optional function for converting code span content to HTML. - * @var callable|null - */ - public $code_span_content_func = null; - - /** - * Class attribute to toggle "enhanced ordered list" behaviour - * setting this to true will allow ordered lists to start from the index - * number that is defined first. - * - * For example: - * 2. List item two - * 3. List item three - * - * Becomes: - *
    - *
  1. List item two
  2. - *
  3. List item three
  4. - *
- */ - public $enhanced_ordered_list = false; - - /** - * 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; - - /** - * Constructor function. Initialize appropriate member variables. - * @return void - */ - public function __construct() { - $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; - - /** - * Status flag to avoid invalid nesting. - */ - protected $in_emphasis_processing = false; - - /** - * Called before the transformation process starts to setup parser states. - * @return void - */ - protected function setup() { - // Clear global hashes. - $this->urls = $this->predef_urls; - $this->titles = $this->predef_titles; - $this->html_hashes = array(); - $this->in_anchor = false; - $this->in_emphasis_processing = false; - } - - /** - * Called after the transformation process to clear any variable which may - * be taking up memory unnecessarly. - * @return void - */ - protected function teardown() { - $this->urls = array(); - $this->titles = array(); - $this->html_hashes = array(); - } - - /** - * Main function. Performs some preprocessing on the input text and pass - * it through the document gamut. - * - * @api - * - * @param string $text - * @return string - */ - public function transform($text) { - $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"; - } - - /** - * Define the document gamut - */ - protected $document_gamut = array( - // Strip link definitions, store in hashes. - "stripLinkDefinitions" => 20, - "runBasicBlockGamut" => 30, - ); - - /** - * Strips link definitions from text, stores the URLs and titles in - * hash references - * @param string $text - * @return string - */ - protected function stripLinkDefinitions($text) { - - $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; - } - - /** - * The callback to strip link definitions - * @param array $matches - * @return string - */ - 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 - } - - /** - * Hashify HTML blocks - * @param string $text - * @return string - */ - 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

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|details|summary'; - - // 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(' - # 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.: - *

- *
- * tags for inner block must be indented. - *
- *
- * - * 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 `
` and stop at the - * first `
`. - */ - $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` to `\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 - # 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 - # the matching end tag - [ ]* # trailing spaces/tabs - (?=\n+|\Z) # followed by a newline or end of document - - | # Special case just for
. 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 ( - ) - [ ]* - (?=\n{2,}|\Z) # followed by a blank line or end of document - - ) - )}Sxmi', - array($this, '_hashHTMLBlocks_callback'), - $text - ); - - return $text; - } - - /** - * The callback for hashing HTML blocks - * @param string $matches - * @return string - */ - protected function _hashHTMLBlocks_callback($matches) { - $text = $matches[1]; - $key = $this->hashBlock($text); - return "\n\n$key\n\n"; - } - - /** - * 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. - * - * @param string $text - * @param string $boundary - * @return string - */ - protected function hashPart($text, $boundary = 'X') { - // 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. - } - - /** - * Shortcut function for hashPart with block-level boundaries. - * @param string $text - * @return string - */ - protected function hashBlock($text) { - return $this->hashPart($text, 'B'); - } - - /** - * Define the block gamut - these are all the transformations that form - * block-level tags like paragraphs, headers, and list items. - */ - protected $block_gamut = array( - "doHeaders" => 10, - "doHorizontalRules" => 20, - "doLists" => 40, - "doCodeBlocks" => 50, - "doBlockQuotes" => 60, - ); - - /** - * 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. - * - * @param string $text - * @return string - */ - protected function runBlockGamut($text) { - $text = $this->hashHTMLBlocks($text); - return $this->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. - * - * @param string $text - * @return string - */ - protected function runBasicBlockGamut($text) { - - foreach ($this->block_gamut as $method => $priority) { - $text = $this->$method($text); - } - - // Finally form paragraph and restore hashed blocks. - $text = $this->formParagraphs($text); - - return $text; - } - - /** - * Convert horizontal rules - * @param string $text - * @return string - */ - protected function doHorizontalRules($text) { - 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("empty_element_suffix")."\n", - $text - ); - } - - /** - * These are all the transformations that occur *within* block-level - * tags like paragraphs, headers, and list items. - */ - protected $span_gamut = array( - // 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 `` - // Must come after doAnchors, because you can use < and > - // delimiters in inline links like [this](). - "doAutoLinks" => 30, - "encodeAmpsAndAngles" => 40, - "doItalicsAndBold" => 50, - "doHardBreaks" => 60, - ); - - /** - * Run span gamut transformations - * @param string $text - * @return string - */ - protected function runSpanGamut($text) { - foreach ($this->span_gamut as $method => $priority) { - $text = $this->$method($text); - } - - return $text; - } - - /** - * Do hard breaks - * @param string $text - * @return string - */ - protected function doHardBreaks($text) { - if ($this->hard_wrap) { - return preg_replace_callback('/ *\n/', - array($this, '_doHardBreaks_callback'), $text); - } else { - return preg_replace_callback('/ {2,}\n/', - array($this, '_doHardBreaks_callback'), $text); - } - } - - /** - * Trigger part hashing for the hard break (callback method) - * @param array $matches - * @return string - */ - protected function _doHardBreaks_callback($matches) { - return $this->hashPart("empty_element_suffix\n"); - } - - /** - * Turn Markdown link shortcuts into XHTML tags. - * @param string $text - * @return string - */ - protected function doAnchors($text) { - 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; - } - - /** - * Callback method to parse referenced anchors - * @param array $matches - * @return string - */ - 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 = "titles[$link_id] ) ) { - $title = $this->titles[$link_id]; - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; - } - - $link_text = $this->runSpanGamut($link_text); - $result .= ">$link_text"; - $result = $this->hashPart($result); - } else { - $result = $whole_match; - } - return $result; - } - - /** - * Callback method to parse inline anchors - * @param array $matches - * @return string - */ - protected function _doAnchors_inline_callback($matches) { - $link_text = $this->runSpanGamut($matches[2]); - $url = $matches[3] === '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - - // If the URL was of the form 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 = "encodeAttribute($title); - $result .= " title=\"$title\""; - } - - $link_text = $this->runSpanGamut($link_text); - $result .= ">$link_text"; - - return $this->hashPart($result); - } - - /** - * Turn Markdown image shortcuts into tags. - * @param string $text - * @return string - */ - protected function doImages($text) { - // 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; - } - - /** - * Callback to parse references image tags - * @param array $matches - * @return string - */ - 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 = "\"$alt_text\"";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; - } - - /** - * Callback to parse inline image tags - * @param array $matches - * @return string - */ - 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 = "\"$alt_text\"";encodeAttribute($title); - $result .= " title=\"$title\""; // $title already quoted - } - $result .= $this->empty_element_suffix; - - return $this->hashPart($result); - } - - /** - * Parse Markdown heading elements to HTML - * @param string $text - * @return string - */ - 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; - } - - /** - * Setext header parsing callback - * @param array $matches - * @return string - */ - 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; - - // ID attribute generation - $idAtt = $this->_generateIdFromHeaderValue($matches[1]); - - $block = "".$this->runSpanGamut($matches[1]).""; - return "\n" . $this->hashBlock($block) . "\n\n"; - } - - /** - * ATX header parsing callback - * @param array $matches - * @return string - */ - protected function _doHeaders_callback_atx($matches) { - // ID attribute generation - $idAtt = $this->_generateIdFromHeaderValue($matches[2]); - - $level = strlen($matches[1]); - $block = "".$this->runSpanGamut($matches[2]).""; - return "\n" . $this->hashBlock($block) . "\n\n"; - } - - /** - * If a header_id_func property is set, we can use it to automatically - * generate an id attribute. - * - * This method returns a string in the form id="foo", or an empty string - * otherwise. - * @param string $headerValue - * @return string - */ - protected function _generateIdFromHeaderValue($headerValue) { - if (!is_callable($this->header_id_func)) { - return ""; - } - - $idValue = call_user_func($this->header_id_func, $headerValue); - if (!$idValue) { - return ""; - } - - return ' id="' . $this->encodeAttribute($idValue) . '"'; - } - - /** - * Form HTML ordered (numbered) and unordered (bulleted) lists. - * @param string $text - * @return string - */ - protected function doLists($text) { - $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; - } - - /** - * List parsing callback - * @param array $matches - * @return string - */ - 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)"; - $marker_ol_start_re = '[0-9]+'; - - $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); - - $ol_start = 1; - if ($this->enhanced_ordered_list) { - // Get the start number for ordered list. - if ($list_type == 'ol') { - $ol_start_array = array(); - $ol_start_check = preg_match("/$marker_ol_start_re/", $matches[4], $ol_start_array); - if ($ol_start_check){ - $ol_start = $ol_start_array[0]; - } - } - } - - if ($ol_start > 1 && $list_type == 'ol'){ - $result = $this->hashBlock("<$list_type start=\"$ol_start\">\n" . $result . ""); - } else { - $result = $this->hashBlock("<$list_type>\n" . $result . ""); - } - return "\n". $result ."\n\n"; - } - - /** - * Nesting tracker for list levels - */ - protected $list_level = 0; - - /** - * Process the contents of a single ordered or unordered list, splitting it - * into individual list items. - * @param string $list_str - * @param string $marker_any_re - * @return string - */ - protected function processListItems($list_str, $marker_any_re) { - /** - * 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; - } - - /** - * List item parsing callback - * @param array $matches - * @return string - */ - 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 = $this->formParagraphs($item, false); - } - - return "
  • " . $item . "
  • \n"; - } - - /** - * Process Markdown `
    ` blocks.
    -	 * @param  string $text
    -	 * @return string
    -	 */
    -	protected function doCodeBlocks($text) {
    -		$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;
    -	}
    -
    -	/**
    -	 * Code block parsing callback
    -	 * @param  array $matches
    -	 * @return string
    -	 */
    -	protected function _doCodeBlocks_callback($matches) {
    -		$codeblock = $matches[1];
    -
    -		$codeblock = $this->outdent($codeblock);
    -		if (is_callable($this->code_block_content_func)) {
    -			$codeblock = call_user_func($this->code_block_content_func, $codeblock, "");
    -		} else {
    -			$codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
    -		}
    -
    -		# trim leading newlines and trailing newlines
    -		$codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock);
    -
    -		$codeblock = "
    $codeblock\n
    "; - return "\n\n" . $this->hashBlock($codeblock) . "\n\n"; - } - - /** - * Create a code span markup for $code. Called from handleSpanToken. - * @param string $code - * @return string - */ - protected function makeCodeSpan($code) { - if (is_callable($this->code_span_content_func)) { - $code = call_user_func($this->code_span_content_func, $code); - } else { - $code = htmlspecialchars(trim($code), ENT_NOQUOTES); - } - return $this->hashPart("$code"); - } - - /** - * Define the emphasis operators with their regex matches - * @var array - */ - protected $em_relist = array( - '' => '(?:(? '(? '(? '(?:(? '(? '(? '(?:(? '(? '(?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; - } - } - } - - /** - * Convert Markdown italics (emphasis) and bold (strong) to HTML - * @param string $text - * @return string - */ - protected function doItalicsAndBold($text) { - if ($this->in_emphasis_processing) { - return $text; // avoid reentrency - } - $this->in_emphasis_processing = true; - - $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 = "$span"; - $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"; - $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"; - $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); - $em = ''; - } - // Closing strong marker: - array_shift($token_stack); - $span = array_shift($text_stack); - $span = $this->runSpanGamut($span); - $span = "$span"; - $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 = "$span"; - $text_stack[0] .= $this->hashPart($span); - $em = ''; - } else { - $text_stack[0] .= $token; - } - } else { - array_unshift($token_stack, $token); - array_unshift($text_stack, ''); - $em = $token; - } - } - } - $this->in_emphasis_processing = false; - return $text_stack[0]; - } - - /** - * Parse Markdown blockquotes to HTML - * @param string $text - * @return string - */ - 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; - } - - /** - * Blockquote parsing callback - * @param array $matches - * @return string - */ - 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
     content,
    -		// so we need to fix that:
    -		$bq = preg_replace_callback('{(\s*
    .+?
    )}sx', - array($this, '_doBlockQuotes_callback2'), $bq); - - return "\n" . $this->hashBlock("
    \n$bq\n
    ") . "\n\n"; - } - - /** - * Blockquote parsing callback - * @param array $matches - * @return string - */ - protected function _doBlockQuotes_callback2($matches) { - $pre = $matches[1]; - $pre = preg_replace('/^ /m', '', $pre); - return $pre; - } - - /** - * Parse paragraphs - * - * @param string $text String to process in paragraphs - * @param boolean $wrap_in_p Whether paragraphs should be wrapped in

    tags - * @return string - */ - protected function formParagraphs($text, $wrap_in_p = true) { - // 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

    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); - if ($wrap_in_p) { - $value = preg_replace('/^([ ]*)/', "

    ", $value); - $value .= "

    "; - } - $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 =
    tag -//
    ]* -// \b -// markdown\s*=\s* ([\'"]) # $2 = attr quote char -// 1 -// \2 -// [^>]* -// > -// ) -// ( # $3 = contents -// .* -// ) -// (
    ) # $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); - } - - /** - * Encode text for a double-quoted HTML attribute. This function - * is *not* suitable for attributes enclosed in single quotes. - * @param string $text - * @return string - */ - protected function encodeAttribute($text) { - $text = $this->encodeAmpsAndAngles($text); - $text = str_replace('"', '"', $text); - return $text; - } - - /** - * 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. - * - * @param string $url - * @param string $text Passed by reference - * @return string URL - */ - protected function encodeURLAttribute($url, &$text = null) { - if (is_callable($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; - } - - /** - * 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. - * @param string $text - * @return string - */ - protected function encodeAmpsAndAngles($text) { - if ($this->no_entities) { - $text = str_replace('&', '&', $text); - } else { - // Ampersand-encoding based entirely on Nat Irons's Amputator - // MT plugin: - $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', - '&', $text); - } - // Encode remaining <'s - $text = str_replace('<', '<', $text); - - return $text; - } - - /** - * Parse Markdown automatic links to anchor HTML tags - * @param string $text - * @return string - */ - protected function doAutoLinks($text) { - $text = preg_replace_callback('{<((https?|ftp|dict|tel):[^\'">\s]+)>}i', - array($this, '_doAutoLinks_url_callback'), $text); - - // Email addresses: - $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; - } - - /** - * Parse URL callback - * @param array $matches - * @return string - */ - protected function _doAutoLinks_url_callback($matches) { - $url = $this->encodeURLAttribute($matches[1], $text); - $link = "$text"; - return $this->hashPart($link); - } - - /** - * Parse email address callback - * @param array $matches - * @return string - */ - protected function _doAutoLinks_email_callback($matches) { - $addr = $matches[1]; - $url = $this->encodeURLAttribute("mailto:$addr", $text); - $link = "$text"; - return $this->hashPart($link); - } - - /** - * 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.: - * - * mailto:foo - * @example.co - * m - * - * 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. - * - * @param string $text - * @param string $tail Passed by reference - * @param integer $head_length - * @return string - */ - protected function encodeEntityObfuscatedAttribute($text, &$tail = null, $head_length = 0) { - if ($text == "") { - return $tail = ""; - } - - $chars = preg_split('/(? $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; - } - - /** - * Take the string $str and parse it into tokens, hashing embeded HTML, - * escaped characters and handling code spans. - * @param string $str - * @return string - */ - protected function parseSpan($str) { - $output = ''; - - $span_re = '{ - ( - \\\\'.$this->escape_chars_re.' - | - (?no_markup ? '' : ' - | - # comment - | - <\?.*?\?> | <%.*?%> # processing instruction - | - <[!$]?[-a-zA-Z0-9:_]+ # regular tags - (?> - \s - (?>[^"\'>]+|"[^"]*"|\'[^\']*\')* - )? - > - | - <[-a-zA-Z0-9:_]+\s*/> # xml-style empty tag - | - # 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; - } - - /** - * Handle $token provided by parseSpan by determining its nature and - * returning the corresponding value that should replace it. - * @param string $token - * @param string $str Passed by reference - * @return string - */ - protected function handleSpanToken($token, &$str) { - 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); - } - } - - /** - * Remove one level of line-leading tabs or spaces - * @param string $text - * @return string - */ - protected function outdent($text) { - return preg_replace('/^(\t|[ ]{1,' . $this->tab_width . '})/m', '', $text); - } - - - /** - * String length function for detab. `_initDetab` will create a function to - * handle UTF-8 if the default function does not exist. - * can be a string or function - */ - protected $utf8_strlen = 'mb_strlen'; - - /** - * Replace tabs with the appropriate amount of spaces. - * - * 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. - * - * @param string $text - * @return string - */ - protected function detab($text) { - $text = preg_replace_callback('/^.*\t.*$/m', - array($this, '_detab_callback'), $text); - - return $text; - } - - /** - * Replace tabs callback - * @param string $matches - * @return string - */ - 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; - } - - /** - * 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. - * @return void - */ - protected function _initDetab() { - - if (function_exists($this->utf8_strlen)) { - return; - } - - //$this->utf8_strlen = fn($text) => preg_match_all('/[\x00-\xBF]|[\xC0-\xFF][\x80-\xBF]*/', $text, $m); - } - - /** - * Swap back in all the tags hashed by _HashHTMLBlocks. - * @param string $text - * @return string - */ - protected function unhash($text) { - return preg_replace_callback('/(.)\x1A[0-9]+\1/', - array($this, '_unhash_callback'), $text); - } - - /** - * Unhashing callback - * @param array $matches - * @return string - */ - protected function _unhash_callback($matches) { - return $this->html_hashes[$matches[0]]; - } -} diff --git a/lib/michelf/Michelf/MarkdownExtra.inc.php b/lib/michelf/Michelf/MarkdownExtra.inc.php deleted file mode 100644 index d09bd7a480..0000000000 --- a/lib/michelf/Michelf/MarkdownExtra.inc.php +++ /dev/null @@ -1,11 +0,0 @@ - - * @copyright 2004-2019 Michel Fortin - * @copyright (Original Markdown) 2004-2006 John Gruber - */ - -namespace Michelf; - -/** - * Markdown Extra Parser Class - */ -class MarkdownExtra extends \Michelf\Markdown { - /** - * Configuration variables - */ - /** - * Prefix for footnote ids. - */ - public $fn_id_prefix = ""; - - /** - * Optional title attribute for footnote links. - */ - public $fn_link_title = ""; - - /** - * Optional class attribute for footnote links and backlinks. - */ - public $fn_link_class = "footnote-ref"; - public $fn_backlink_class = "footnote-backref"; - - /** - * Content to be displayed within footnote backlinks. The default is '↩'; - * the U+FE0E on the end is a Unicode variant selector used to prevent iOS - * from displaying the arrow character as an emoji. - * Optionally use '^^' and '%%' to refer to the footnote number and - * reference number respectively. {@see parseFootnotePlaceholders()} - */ - public $fn_backlink_html = '↩︎'; - - /** - * Optional title and aria-label attributes for footnote backlinks for - * added accessibility (to ensure backlink uniqueness). - * Use '^^' and '%%' to refer to the footnote number and reference number - * respectively. {@see parseFootnotePlaceholders()} - */ - public $fn_backlink_title = ""; - public $fn_backlink_label = ""; - - /** - * 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(); - - /** - * Only convert atx-style headers if there's a space between the header and # - */ - public $hashtag_protection = false; - - /** - * Determines whether footnotes should be appended to the end of the document. - * If true, footnote html can be retrieved from $this->footnotes_assembled. - */ - public $omit_footnotes = false; - - - /** - * After parsing, the HTML for the list of footnotes appears here. - * This is available only if $omit_footnotes == true. - * - * Note: when placing the content of `footnotes_assembled` on the page, - * consider adding the attribute `role="doc-endnotes"` to the `div` or - * `section` that will enclose the list of footnotes so they are - * reachable to accessibility tools the same way they would be with the - * default HTML output. - */ - public $footnotes_assembled = null; - - /** - * Parser implementation - */ - - /** - * Constructor function. Initialize the parser object. - * @return void - */ - public function __construct() { - // 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, - ); - - $this->enhanced_ordered_list = true; - 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; - - /** - * Ref attribute for links - */ - protected $ref_attr = array(); - - /** - * Setting up Extra-specific variables. - */ - protected function setup() { - 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; - $this->footnotes_assembled = null; - - 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); - } - } - - /** - * Clearing Extra-specific variables. - */ - protected function teardown() { - $this->footnotes = array(); - $this->footnotes_ordered = array(); - $this->footnotes_ref_count = array(); - $this->footnotes_numbers = array(); - $this->abbr_desciptions = array(); - $this->abbr_word_re = ''; - - if ( ! $this->omit_footnotes ) - $this->footnotes_assembled = null; - - 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,}[ ]*\}'; - - /** - * 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. - * - * In addition, this method also supports supplying a default Id value, - * which will be used to populate the id attribute in case it was not - * overridden. - * @param string $tag_name - * @param string $attr - * @param mixed $defaultIdValue - * @param array $classes - * @return string - */ - protected function doExtraAttributes($tag_name, $attr, $defaultIdValue = null, $classes = array()) { - if (empty($attr) && !$defaultIdValue && empty($classes)) { - 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) - $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] . '"'; - } - } - - if ($id === false || $id === '') { - $id = $defaultIdValue; - } - - // Compose attributes as string - $attr_str = ""; - if (!empty($id)) { - $attr_str .= ' id="'.$this->encodeAttribute($id) .'"'; - } - if (!empty($classes)) { - $attr_str .= ' class="'. implode(" ", $classes) . '"'; - } - if (!$this->no_markup && !empty($attributes)) { - $attr_str .= ' '.implode(" ", $attributes); - } - return $attr_str; - } - - /** - * Strips link definitions from text, stores the URLs and titles in - * hash references. - * @param string $text - * @return string - */ - protected function stripLinkDefinitions($text) { - $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; - } - - /** - * Strip link definition callback - * @param array $matches - * @return string - */ - 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|details|summary'; - - /** - * 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'; - - /** - * 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

    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! - * @param string $text - * @return string - */ - protected function hashHTMLBlocks($text) { - if ($this->no_markup) { - return $text; - } - - // Call the HTML-in-Markdown hasher. - list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text); - - return $text; - } - - /** - * 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: - * - *

    - *
    - * Hello World. <-- Is this a Markdown code block or text? - *
    <-- Is this a Markdown code block or a real tag? - *
    - * - * 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 ) - * - * @param string $text - * @param integer $indent - * @param string $enclosing_tag_re - * @param boolean $span - * @return array - */ - protected function _hashHTMLBlocks_inMarkdown($text, $indent = 0, - $enclosing_tag_re = '', $span = false) - { - - 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. - # 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 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 = "\n$void"; - $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. - - // 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))*?(?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('{^= 0); - - return array($parsed, $text); - } - - /** - * 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 ) - * @param string $text - * @param string $hash_method - * @param bool $md_attr Handle `markdown="1"` attribute - * @return array - */ - protected function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) { - 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. - - ".*?" | # Double quotes (can contain `>`) - \'.*?\' | # Single quotes (can contain `>`) - .+? # Anything but quotes and `>`. - )*? - )? - > # End of tag. - | - # HTML Comment - | - <\?.*?\?> | <%.*?%> # Processing instruction - | - # 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. - $base_tag_name_re = ''; - - // 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
    ) - // Comments and Processing Instructions. - if (preg_match('{^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('{^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 .= (string) $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); - } - - /** - * 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. - * @param string $text - * @return string - */ - protected function hashClean($text) { - return $this->hashPart($text, 'C'); - } - - /** - * Turn Markdown link shortcuts into XHTML tags. - * @param string $text - * @return string - */ - protected function doAnchors($text) { - 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; - } - - /** - * Callback for reference anchors - * @param array $matches - * @return string - */ - 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 = "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"; - $result = $this->hashPart($result); - } - else { - $result = $whole_match; - } - return $result; - } - - /** - * Callback for inline anchors - * @param array $matches - * @return string - */ - protected function _doAnchors_inline_callback($matches) { - $link_text = $this->runSpanGamut($matches[2]); - $url = $matches[3] === '' ? $matches[4] : $matches[3]; - $title_quote =& $matches[6]; - $title =& $matches[7]; - $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); - - // if the URL was of the form 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 = "encodeAttribute($title); - $result .= " title=\"$title\""; - } - $result .= $attr; - - $link_text = $this->runSpanGamut($link_text); - $result .= ">$link_text"; - - return $this->hashPart($result); - } - - /** - * Turn Markdown image shortcuts into tags. - * @param string $text - * @return string - */ - protected function doImages($text) { - // 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; - } - - /** - * Callback for referenced images - * @param array $matches - * @return string - */ - 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 = "\"$alt_text\"";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; - } - - /** - * Callback for inline images - * @param array $matches - * @return string - */ - protected function _doImages_inline_callback($matches) { - $alt_text = $matches[2]; - $url = $matches[3] === '' ? $matches[4] : $matches[3]; - $title_quote =& $matches[6]; - $title =& $matches[7]; - $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]); - - $alt_text = $this->encodeAttribute($alt_text); - $url = $this->encodeURLAttribute($url); - $result = "\"$alt_text\"";encodeAttribute($title); - $result .= " title=\"$title\""; // $title already quoted - } - $result .= $attr; - $result .= $this->empty_element_suffix; - - return $this->hashPart($result); - } - - /** - * Process markdown headers. Redefined to add ID and class attribute support. - * @param string $text - * @return string - */ - protected function doHeaders($text) { - // 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 - [ ]'.($this->hashtag_protection ? '+' : '*').' - (.+?) # $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; - } - - /** - * Callback for setext headers - * @param array $matches - * @return string - */ - protected function _doHeaders_callback_setext($matches) { - if ($matches[3] === '-' && preg_match('{^- }', $matches[1])) { - return $matches[0]; - } - - $level = $matches[3][0] === '=' ? 1 : 2; - - $defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[1]) : null; - - $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2], $defaultId); - $block = "" . $this->runSpanGamut($matches[1]) . ""; - return "\n" . $this->hashBlock($block) . "\n\n"; - } - - /** - * Callback for atx headers - * @param array $matches - * @return string - */ - protected function _doHeaders_callback_atx($matches) { - $level = strlen($matches[1]); - - $defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[2]) : null; - $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[3], $defaultId); - $block = "" . $this->runSpanGamut($matches[2]) . ""; - return "\n" . $this->hashBlock($block) . "\n\n"; - } - - /** - * Form HTML tables. - * @param string $text - * @return string - */ - protected function doTables($text) { - $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; - } - - /** - * Callback for removing the leading pipe for each row - * @param array $matches - * @return string - */ - protected function _doTable_leadingPipe_callback($matches) { - $head = $matches[1]; - $underline = $matches[2]; - $content = $matches[3]; - - $content = preg_replace('/^ *[|]/m', '', $content); - - return $this->_doTable_callback(array($matches[0], $head, $underline, $content)); - } - - /** - * Make the align attribute in a table - * @param string $alignname - * @return string - */ - 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\""; - } - - /** - * Calback for processing tables - * @param array $matches - * @return string - */ - protected function _doTable_callback($matches) { - $head = $matches[1]; - $underline = $matches[2]; - $content = $matches[3]; - $attr = []; - - // 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 = "\n"; - $text .= "\n"; - $text .= "\n"; - foreach ($headers as $n => $header) { - $text .= " " . $this->runSpanGamut(trim($header)) . "\n"; - } - $text .= "\n"; - $text .= "\n"; - - // Split content by row. - $rows = explode("\n", trim($content, "\n")); - - $text .= "\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 .= "\n"; - foreach ($row_cells as $n => $cell) { - $text .= " " . $this->runSpanGamut(trim($cell)) . "\n"; - } - $text .= "\n"; - } - $text .= "\n"; - $text .= "
    "; - - return $this->hashBlock($text) . "\n"; - } - - /** - * Form HTML definition lists. - * @param string $text - * @return string - */ - protected function doDefLists($text) { - $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; - } - - /** - * Callback for processing definition lists - * @param array $matches - * @return string - */ - 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 = "
    \n" . $result . "\n
    "; - return $this->hashBlock($result) . "\n\n"; - } - - /** - * Process the contents of a single definition list, splitting it - * into individual term and definition list items. - * @param string $list_str - * @return string - */ - protected function processDefListItems($list_str) { - - $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 . '} \:[ ] | -
    | \z - ) - ) - }xm', - array($this, '_processDefListItems_callback_dd'), $list_str); - - return $list_str; - } - - /** - * Callback for
    elements in definition lists - * @param array $matches - * @return string - */ - protected function _processDefListItems_callback_dt($matches) { - $terms = explode("\n", trim($matches[1])); - $text = ''; - foreach ($terms as $term) { - $term = $this->runSpanGamut(trim($term)); - $text .= "\n
    " . $term . "
    "; - } - return $text . "\n"; - } - - /** - * Callback for
    elements in definition lists - * @param array $matches - * @return string - */ - 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
    " . $def . "
    \n"; - } - - /** - * Adding the fenced code block syntax to regular Markdown: - * - * ~~~ - * Code block - * ~~~ - * - * @param string $text - * @return string - */ - protected function doFencedCodeBlocks($text) { - - $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; - } - - /** - * Callback to process fenced code blocks - * @param array $matches - * @return string - */ - protected function _doFencedCodeBlocks_callback($matches) { - $classname =& $matches[2]; - $attrs =& $matches[3]; - $codeblock = $matches[4]; - - if ($this->code_block_content_func) { - $codeblock = call_user_func($this->code_block_content_func, $codeblock, $classname); - } else { - $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); - } - - $codeblock = preg_replace_callback('/^\n+/', - array($this, '_doFencedCodeBlocks_newlines'), $codeblock); - - $classes = array(); - if ($classname !== "") { - if ($classname[0] === '.') { - $classname = substr($classname, 1); - } - $classes[] = $this->code_class_prefix . $classname; - } - $attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs, null, $classes); - $pre_attr_str = $this->code_attr_on_pre ? $attr_str : ''; - $code_attr_str = $this->code_attr_on_pre ? '' : $attr_str; - $codeblock = "$codeblock
    "; - - return "\n\n".$this->hashBlock($codeblock)."\n\n"; - } - - /** - * Replace new lines in fenced code blocks - * @param array $matches - * @return string - */ - protected function _doFencedCodeBlocks_newlines($matches) { - return str_repeat("empty_element_suffix", - strlen($matches[0])); - } - - /** - * Redefining emphasis markers so that emphasis by underscore does not - * work in the middle of a word. - * @var array - */ - protected $em_relist = array( - '' => '(?:(? '(? '(? '(?:(? '(? '(? '(?:(? '(? '(? tags - * @return string HTML output - */ - protected function formParagraphs($text, $wrap_in_p = true) { - // 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

    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 = $wrap_in_p && !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value); - - if ($is_p) { - $value = "

    $value

    "; - } - $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 - Strips link definitions from text, stores the URLs and - * titles in hash references. - * @param string $text - * @return string - */ - protected function stripFootnotes($text) { - $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; - } - - /** - * Callback for stripping footnotes - * @param array $matches - * @return string - */ - 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 - } - - /** - * Replace footnote references in $text [^id] with a special text-token - * which will be replaced by the actual footnote marker in appendFootnotes. - * @param string $text - * @return string - */ - protected function doFootnotes($text) { - if (!$this->in_anchor) { - $text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text); - } - return $text; - } - - /** - * Append footnote list to text - * @param string $text - * @return string - */ - protected function appendFootnotes($text) { - $text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', - array($this, '_appendFootnotes_callback'), $text); - - if ( ! empty( $this->footnotes_ordered ) ) { - $this->_doFootnotes(); - if ( ! $this->omit_footnotes ) { - $text .= "\n\n"; - $text .= "
    \n"; - $text .= "empty_element_suffix . "\n"; - $text .= $this->footnotes_assembled; - $text .= "
    "; - } - } - return $text; - } - - - /** - * Generates the HTML for footnotes. Called by appendFootnotes, even if - * footnotes are not being appended. - * @return void - */ - protected function _doFootnotes() { - $attr = array(); - if ($this->fn_backlink_class !== "") { - $class = $this->fn_backlink_class; - $class = $this->encodeAttribute($class); - $attr['class'] = " class=\"$class\""; - } - $attr['role'] = " role=\"doc-backlink\""; - $num = 0; - - $text = "
      \n\n"; - 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); - - $num++; - $note_id = $this->encodeAttribute($note_id); - - // Prepare backlink, multiple backlinks if multiple references - // Do not create empty backlinks if the html is blank - $backlink = ""; - if (!empty($this->fn_backlink_html)) { - for ($ref_num = 1; $ref_num <= $ref_count; ++$ref_num) { - if (!empty($this->fn_backlink_title)) { - $attr['title'] = ' title="' . $this->encodeAttribute($this->fn_backlink_title) . '"'; - } - if (!empty($this->fn_backlink_label)) { - $attr['label'] = ' aria-label="' . $this->encodeAttribute($this->fn_backlink_label) . '"'; - } - $parsed_attr = $this->parseFootnotePlaceholders( - implode('', $attr), - $num, - $ref_num - ); - $backlink_text = $this->parseFootnotePlaceholders( - $this->fn_backlink_html, - $num, - $ref_num - ); - $ref_count_mark = $ref_num > 1 ? $ref_num : ''; - $backlink .= " $backlink_text"; - } - $backlink = trim($backlink); - } - - // Add backlink to last paragraph; create new paragraph if needed. - if (!empty($backlink)) { - if (preg_match('{

      $}', $footnote)) { - $footnote = substr($footnote, 0, -4) . " $backlink

      "; - } else { - $footnote .= "\n\n

      $backlink

      "; - } - } - - $text .= "
    1. \n"; - $text .= $footnote . "\n"; - $text .= "
    2. \n\n"; - } - $text .= "
    \n"; - - $this->footnotes_assembled = $text; - } - - /** - * Callback for appending footnotes - * @param array $matches - * @return string - */ - 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 .= " role=\"doc-noteref\""; - - $attr = str_replace("%%", $num, $attr); - $node_id = $this->encodeAttribute($node_id); - - return - "". - "$num". - ""; - } - - return "[^" . $matches[1] . "]"; - } - - /** - * Build footnote label by evaluating any placeholders. - * - ^^ footnote number - * - %% footnote reference number (Nth reference to footnote number) - * @param string $label - * @param int $footnote_number - * @param int $reference_number - * @return string - */ - protected function parseFootnotePlaceholders($label, $footnote_number, $reference_number) { - return str_replace( - array('^^', '%%'), - array($footnote_number, $reference_number), - $label - ); - } - - - /** - * Abbreviations - strips abbreviations from text, stores titles in hash - * references. - * @param string $text - * @return string - */ - protected function stripAbbreviations($text) { - $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; - } - - /** - * Callback for stripping abbreviations - * @param array $matches - * @return string - */ - 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 - } - - /** - * Find defined abbreviations in text and wrap them in elements. - * @param string $text - * @return string - */ - protected function doAbbreviations($text) { - if ($this->abbr_word_re) { - // cannot use the /x modifier because abbr_word_re may - // contain significant spaces: - $text = preg_replace_callback('{' . - '(?abbr_word_re . ')' . - '(?![\w\x1A])' . - '}', - array($this, '_doAbbreviations_callback'), $text); - } - return $text; - } - - /** - * Callback for processing abbreviations - * @param array $matches - * @return string - */ - 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"); - } - $desc = $this->encodeAttribute($desc); - return $this->hashPart("$abbr"); - } - return $matches[0]; - } -} diff --git a/lib/michelf/Michelf/MarkdownInterface.inc.php b/lib/michelf/Michelf/MarkdownInterface.inc.php deleted file mode 100644 index c4e9ac7f64..0000000000 --- a/lib/michelf/Michelf/MarkdownInterface.inc.php +++ /dev/null @@ -1,9 +0,0 @@ - - * @copyright 2004-2021 Michel Fortin - * @copyright (Original Markdown) 2004-2006 John Gruber - */ - -namespace Michelf; - -/** - * Markdown Parser Interface - */ -interface MarkdownInterface { - /** - * Initialize the parser and return the result of its transform method. - * This will work fine for derived classes too. - * - * @api - * - * @param string $text - * @return string - */ - public static function defaultTransform($text); - - /** - * Main function. Performs some preprocessing on the input text - * and pass it through the document gamut. - * - * @api - * - * @param string $text - * @return string - */ - public function transform($text); -} diff --git a/lib/michelf/michelf.class.php b/lib/michelf/michelf.class.php deleted file mode 100644 index 6f55684fb5..0000000000 --- a/lib/michelf/michelf.class.php +++ /dev/null @@ -1,27 +0,0 @@ -fn_id_prefix = "post22-"; - $html = $parser->transform($mdCodes); - - return "
    $html
    "; - } -} diff --git a/lib/parsedownextraplugin/LICENSE.txt b/lib/parsedownextraplugin/LICENSE.txt new file mode 100644 index 0000000000..8e7c764d16 --- /dev/null +++ b/lib/parsedownextraplugin/LICENSE.txt @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013-2018 Emanuil Rusev, erusev.com + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/parsedownextraplugin/index.html b/lib/parsedownextraplugin/index.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/parsedownextraplugin/parsedown.php b/lib/parsedownextraplugin/parsedown.php new file mode 100644 index 0000000000..69147dc2ac --- /dev/null +++ b/lib/parsedownextraplugin/parsedown.php @@ -0,0 +1,1992 @@ +textElements($text); + + # convert to markup + $markup = $this->elements($Elements); + + # trim line breaks + $markup = trim($markup, "\n"); + + return $markup; + } + + protected function textElements($text) + { + # make sure no definitions are set + $this->DefinitionData = array(); + + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + # remove surrounding line breaks + $text = trim($text, "\n"); + + # split text into lines + $lines = explode("\n", $text); + + # iterate through lines to identify blocks + return $this->linesElements($lines); + } + + # + # Setters + # + + function setBreaksEnabled($breaksEnabled) + { + $this->breaksEnabled = $breaksEnabled; + + return $this; + } + + protected $breaksEnabled; + + function setMarkupEscaped($markupEscaped) + { + $this->markupEscaped = $markupEscaped; + + return $this; + } + + protected $markupEscaped; + + function setUrlsLinked($urlsLinked) + { + $this->urlsLinked = $urlsLinked; + + return $this; + } + + protected $urlsLinked = true; + + function setSafeMode($safeMode) + { + $this->safeMode = (bool) $safeMode; + + return $this; + } + + protected $safeMode; + + function setStrictMode($strictMode) + { + $this->strictMode = (bool) $strictMode; + + return $this; + } + + protected $strictMode; + + protected $safeLinksWhitelist = array( + 'http://', + 'https://', + 'ftp://', + 'ftps://', + 'mailto:', + 'tel:', + 'data:image/png;base64,', + 'data:image/gif;base64,', + 'data:image/jpeg;base64,', + 'irc:', + 'ircs:', + 'git:', + 'ssh:', + 'news:', + 'steam:', + ); + + # + # Lines + # + + protected $BlockTypes = array( + '#' => array('Header'), + '*' => array('Rule', 'List'), + '+' => array('List'), + '-' => array('SetextHeader', 'Table', 'Rule', 'List'), + '0' => array('List'), + '1' => array('List'), + '2' => array('List'), + '3' => array('List'), + '4' => array('List'), + '5' => array('List'), + '6' => array('List'), + '7' => array('List'), + '8' => array('List'), + '9' => array('List'), + ':' => array('Table'), + '<' => array('Comment', 'Markup'), + '=' => array('SetextHeader'), + '>' => array('Quote'), + '[' => array('Reference'), + '_' => array('Rule'), + '`' => array('FencedCode'), + '|' => array('Table'), + '~' => array('FencedCode'), + ); + + # ~ + + protected $unmarkedBlockTypes = array( + 'Code', + ); + + # + # Blocks + # + + protected function lines(array $lines) + { + return $this->elements($this->linesElements($lines)); + } + + protected function linesElements(array $lines) + { + $Elements = array(); + $CurrentBlock = null; + + foreach ($lines as $line) + { + if (chop($line) === '') + { + if (isset($CurrentBlock)) + { + $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted']) + ? $CurrentBlock['interrupted'] + 1 : 1 + ); + } + + continue; + } + + while (($beforeTab = strstr($line, "\t", true)) !== false) + { + $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4; + + $line = $beforeTab + . str_repeat(' ', $shortage) + . substr($line, strlen($beforeTab) + 1) + ; + } + + $indent = strspn($line, ' '); + + $text = $indent > 0 ? substr($line, $indent) : $line; + + # ~ + + $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); + + # ~ + + if (isset($CurrentBlock['continuable'])) + { + $methodName = 'block' . $CurrentBlock['type'] . 'Continue'; + $Block = $this->$methodName($Line, $CurrentBlock); + + if (isset($Block)) + { + $CurrentBlock = $Block; + + continue; + } + else + { + if ($this->isBlockCompletable($CurrentBlock['type'])) + { + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); + } + } + } + + # ~ + + $marker = $text[0]; + + # ~ + + $blockTypes = $this->unmarkedBlockTypes; + + if (isset($this->BlockTypes[$marker])) + { + foreach ($this->BlockTypes[$marker] as $blockType) + { + $blockTypes []= $blockType; + } + } + + # + # ~ + + foreach ($blockTypes as $blockType) + { + $Block = $this->{"block$blockType"}($Line, $CurrentBlock); + + if (isset($Block)) + { + $Block['type'] = $blockType; + + if ( ! isset($Block['identified'])) + { + if (isset($CurrentBlock)) + { + $Elements[] = $this->extractElement($CurrentBlock); + } + + $Block['identified'] = true; + } + + if ($this->isBlockContinuable($blockType)) + { + $Block['continuable'] = true; + } + + $CurrentBlock = $Block; + + continue 2; + } + } + + # ~ + + if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph') + { + $Block = $this->paragraphContinue($Line, $CurrentBlock); + } + + if (isset($Block)) + { + $CurrentBlock = $Block; + } + else + { + if (isset($CurrentBlock)) + { + $Elements[] = $this->extractElement($CurrentBlock); + } + + $CurrentBlock = $this->paragraph($Line); + + $CurrentBlock['identified'] = true; + } + } + + # ~ + + if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) + { + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); + } + + # ~ + + if (isset($CurrentBlock)) + { + $Elements[] = $this->extractElement($CurrentBlock); + } + + # ~ + + return $Elements; + } + + protected function extractElement(array $Component) + { + if ( ! isset($Component['element'])) + { + if (isset($Component['markup'])) + { + $Component['element'] = array('rawHtml' => $Component['markup']); + } + elseif (isset($Component['hidden'])) + { + $Component['element'] = array(); + } + } + + return $Component['element']; + } + + protected function isBlockContinuable($Type) + { + return method_exists($this, 'block' . $Type . 'Continue'); + } + + protected function isBlockCompletable($Type) + { + return method_exists($this, 'block' . $Type . 'Complete'); + } + + # + # Code + + protected function blockCode($Line, $Block = null) + { + if (isset($Block) and $Block['type'] === 'Paragraph' and ! isset($Block['interrupted'])) + { + return; + } + + if ($Line['indent'] >= 4) + { + $text = substr($Line['body'], 4); + + $Block = array( + 'element' => array( + 'name' => 'pre', + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ), + ); + + return $Block; + } + } + + protected function blockCodeContinue($Line, $Block) + { + if ($Line['indent'] >= 4) + { + if (isset($Block['interrupted'])) + { + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); + + unset($Block['interrupted']); + } + + $Block['element']['element']['text'] .= "\n"; + + $text = substr($Line['body'], 4); + + $Block['element']['element']['text'] .= $text; + + return $Block; + } + } + + protected function blockCodeComplete($Block) + { + return $Block; + } + + # + # Comment + + protected function blockComment($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (strpos($Line['text'], '') !== false) + { + $Block['closed'] = true; + } + + return $Block; + } + } + + protected function blockCommentContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + $Block['element']['rawHtml'] .= "\n" . $Line['body']; + + if (strpos($Line['text'], '-->') !== false) + { + $Block['closed'] = true; + } + + return $Block; + } + + # + # Fenced Code + + protected function blockFencedCode($Line) + { + $marker = $Line['text'][0]; + + $openerLength = strspn($Line['text'], $marker); + + if ($openerLength < 3) + { + return; + } + + $infostring = trim(substr($Line['text'], $openerLength), "\t "); + + if (strpos($infostring, '`') !== false) + { + return; + } + + $Element = array( + 'name' => 'code', + 'text' => '', + ); + + if ($infostring !== '') + { + /** + * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes + * Every HTML element may have a class attribute specified. + * The attribute, if specified, must have a value that is a set + * of space-separated tokens representing the various classes + * that the element belongs to. + * [...] + * The space characters, for the purposes of this specification, + * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), + * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and + * U+000D CARRIAGE RETURN (CR). + */ + $language = substr($infostring, 0, strcspn($infostring, " \t\n\f\r")); + + $Element['attributes'] = array('class' => "language-$language"); + } + + $Block = array( + 'char' => $marker, + 'openerLength' => $openerLength, + 'element' => array( + 'name' => 'pre', + 'element' => $Element, + ), + ); + + return $Block; + } + + protected function blockFencedCodeContinue($Line, $Block) + { + if (isset($Block['complete'])) + { + return; + } + + if (isset($Block['interrupted'])) + { + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); + + unset($Block['interrupted']); + } + + if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength'] + and chop(substr($Line['text'], $len), ' ') === '' + ) { + $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1); + + $Block['complete'] = true; + + return $Block; + } + + $Block['element']['element']['text'] .= "\n" . $Line['body']; + + return $Block; + } + + protected function blockFencedCodeComplete($Block) + { + return $Block; + } + + # + # Header + + protected function blockHeader($Line) + { + $level = strspn($Line['text'], '#'); + + if ($level > 6) + { + return; + } + + $text = trim($Line['text'], '#'); + + if ($this->strictMode and isset($text[0]) and $text[0] !== ' ') + { + return; + } + + $text = trim($text, ' '); + + $Block = array( + 'element' => array( + 'name' => 'h' . $level, + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $text, + 'destination' => 'elements', + ) + ), + ); + + return $Block; + } + + # + # List + + protected function blockList($Line, array $CurrentBlock = null) + { + list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]'); + + if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches)) + { + $contentIndent = strlen($matches[2]); + + if ($contentIndent >= 5) + { + $contentIndent -= 1; + $matches[1] = substr($matches[1], 0, -$contentIndent); + $matches[3] = str_repeat(' ', $contentIndent) . $matches[3]; + } + elseif ($contentIndent === 0) + { + $matches[1] .= ' '; + } + + $markerWithoutWhitespace = strstr($matches[1], ' ', true); + + $Block = array( + 'indent' => $Line['indent'], + 'pattern' => $pattern, + 'data' => array( + 'type' => $name, + 'marker' => $matches[1], + 'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)), + ), + 'element' => array( + 'name' => $name, + 'elements' => array(), + ), + ); + $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/'); + + if ($name === 'ol') + { + $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0'; + + if ($listStart !== '1') + { + if ( + isset($CurrentBlock) + and $CurrentBlock['type'] === 'Paragraph' + and ! isset($CurrentBlock['interrupted']) + ) { + return; + } + + $Block['element']['attributes'] = array('start' => $listStart); + } + } + + $Block['li'] = array( + 'name' => 'li', + 'handler' => array( + 'function' => 'li', + 'argument' => !empty($matches[3]) ? array($matches[3]) : array(), + 'destination' => 'elements' + ) + ); + + $Block['element']['elements'] []= & $Block['li']; + + return $Block; + } + } + + protected function blockListContinue($Line, array $Block) + { + if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument'])) + { + return null; + } + + $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker'])); + + if ($Line['indent'] < $requiredIndent + and ( + ( + $Block['data']['type'] === 'ol' + and preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) or ( + $Block['data']['type'] === 'ul' + and preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) + ) + ) { + if (isset($Block['interrupted'])) + { + $Block['li']['handler']['argument'] []= ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + unset($Block['li']); + + $text = isset($matches[1]) ? $matches[1] : ''; + + $Block['indent'] = $Line['indent']; + + $Block['li'] = array( + 'name' => 'li', + 'handler' => array( + 'function' => 'li', + 'argument' => array($text), + 'destination' => 'elements' + ) + ); + + $Block['element']['elements'] []= & $Block['li']; + + return $Block; + } + elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line)) + { + return null; + } + + if ($Line['text'][0] === '[' and $this->blockReference($Line)) + { + return $Block; + } + + if ($Line['indent'] >= $requiredIndent) + { + if (isset($Block['interrupted'])) + { + $Block['li']['handler']['argument'] []= ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + $text = substr($Line['body'], $requiredIndent); + + $Block['li']['handler']['argument'] []= $text; + + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']); + + $Block['li']['handler']['argument'] []= $text; + + return $Block; + } + } + + protected function blockListComplete(array $Block) + { + if (isset($Block['loose'])) + { + foreach ($Block['element']['elements'] as &$li) + { + if (end($li['handler']['argument']) !== '') + { + $li['handler']['argument'] []= ''; + } + } + } + + return $Block; + } + + # + # Quote + + protected function blockQuote($Line) + { + if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) + { + $Block = array( + 'element' => array( + 'name' => 'blockquote', + 'handler' => array( + 'function' => 'linesElements', + 'argument' => (array) $matches[1], + 'destination' => 'elements', + ) + ), + ); + + return $Block; + } + } + + protected function blockQuoteContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) + { + $Block['element']['handler']['argument'] []= $matches[1]; + + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $Block['element']['handler']['argument'] []= $Line['text']; + + return $Block; + } + } + + # + # Rule + + protected function blockRule($Line) + { + $marker = $Line['text'][0]; + + if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '') + { + $Block = array( + 'element' => array( + 'name' => 'hr', + ), + ); + + return $Block; + } + } + + # + # Setext + + protected function blockSetextHeader($Line, array $Block = null) + { + if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) + { + return; + } + + if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '') + { + $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; + + return $Block; + } + } + + # + # Markup + + protected function blockMarkup($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\/)?>/', $Line['text'], $matches)) + { + $element = strtolower($matches[1]); + + if (in_array($element, $this->textLevelElements)) + { + return; + } + + $Block = array( + 'name' => $matches[1], + 'element' => array( + 'rawHtml' => $Line['text'], + 'autobreak' => true, + ), + ); + + return $Block; + } + } + + protected function blockMarkupContinue($Line, array $Block) + { + if (isset($Block['closed']) or isset($Block['interrupted'])) + { + return; + } + + $Block['element']['rawHtml'] .= "\n" . $Line['body']; + + return $Block; + } + + # + # Reference + + protected function blockReference($Line) + { + if (strpos($Line['text'], ']') !== false + and preg_match('/^\[(.+?)\]:[ ]*+?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches) + ) { + $id = strtolower($matches[1]); + + $Data = array( + 'url' => $matches[2], + 'title' => isset($matches[3]) ? $matches[3] : null, + ); + + $this->DefinitionData['Reference'][$id] = $Data; + + $Block = array( + 'element' => array(), + ); + + return $Block; + } + } + + # + # Table + + protected function blockTable($Line, array $Block = null) + { + if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) + { + return; + } + + if ( + strpos($Block['element']['handler']['argument'], '|') === false + and strpos($Line['text'], '|') === false + and strpos($Line['text'], ':') === false + or strpos($Block['element']['handler']['argument'], "\n") !== false + ) { + return; + } + + if (chop($Line['text'], ' -:|') !== '') + { + return; + } + + $alignments = array(); + + $divider = $Line['text']; + + $divider = trim($divider); + $divider = trim($divider, '|'); + + $dividerCells = explode('|', $divider); + + foreach ($dividerCells as $dividerCell) + { + $dividerCell = trim($dividerCell); + + if ($dividerCell === '') + { + return; + } + + $alignment = null; + + if ($dividerCell[0] === ':') + { + $alignment = 'left'; + } + + if (substr($dividerCell, - 1) === ':') + { + $alignment = $alignment === 'left' ? 'center' : 'right'; + } + + $alignments []= $alignment; + } + + # ~ + + $HeaderElements = array(); + + $header = $Block['element']['handler']['argument']; + + $header = trim($header); + $header = trim($header, '|'); + + $headerCells = explode('|', $header); + + if (count($headerCells) !== count($alignments)) + { + return; + } + + foreach ($headerCells as $index => $headerCell) + { + $headerCell = trim($headerCell); + + $HeaderElement = array( + 'name' => 'th', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $headerCell, + 'destination' => 'elements', + ) + ); + + if (isset($alignments[$index])) + { + $alignment = $alignments[$index]; + + $HeaderElement['attributes'] = array( + 'style' => "text-align: $alignment;", + ); + } + + $HeaderElements []= $HeaderElement; + } + + # ~ + + $Block = array( + 'alignments' => $alignments, + 'identified' => true, + 'element' => array( + 'name' => 'table', + 'elements' => array(), + ), + ); + + $Block['element']['elements'] []= array( + 'name' => 'thead', + ); + + $Block['element']['elements'] []= array( + 'name' => 'tbody', + 'elements' => array(), + ); + + $Block['element']['elements'][0]['elements'] []= array( + 'name' => 'tr', + 'elements' => $HeaderElements, + ); + + return $Block; + } + + protected function blockTableContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|')) + { + $Elements = array(); + + $row = $Line['text']; + + $row = trim($row); + $row = trim($row, '|'); + + preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches); + + $cells = array_slice($matches[0], 0, count($Block['alignments'])); + + foreach ($cells as $index => $cell) + { + $cell = trim($cell); + + $Element = array( + 'name' => 'td', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $cell, + 'destination' => 'elements', + ) + ); + + if (isset($Block['alignments'][$index])) + { + $Element['attributes'] = array( + 'style' => 'text-align: ' . $Block['alignments'][$index] . ';', + ); + } + + $Elements []= $Element; + } + + $Element = array( + 'name' => 'tr', + 'elements' => $Elements, + ); + + $Block['element']['elements'][1]['elements'] []= $Element; + + return $Block; + } + } + + # + # ~ + # + + protected function paragraph($Line) + { + return array( + 'type' => 'Paragraph', + 'element' => array( + 'name' => 'p', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $Line['text'], + 'destination' => 'elements', + ), + ), + ); + } + + protected function paragraphContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + $Block['element']['handler']['argument'] .= "\n".$Line['text']; + + return $Block; + } + + # + # Inline Elements + # + + protected $InlineTypes = array( + '!' => array('Image'), + '&' => array('SpecialCharacter'), + '*' => array('Emphasis'), + ':' => array('Url'), + '<' => array('UrlTag', 'EmailTag', 'Markup'), + '[' => array('Link'), + '_' => array('Emphasis'), + '`' => array('Code'), + '~' => array('Strikethrough'), + '\\' => array('EscapeSequence'), + ); + + # ~ + + protected $inlineMarkerList = '!*_&[:<`~\\'; + + # + # ~ + # + + public function line($text, $nonNestables = array()) + { + return $this->elements($this->lineElements($text, $nonNestables)); + } + + protected function lineElements($text, $nonNestables = array()) + { + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + $Elements = array(); + + $nonNestables = (empty($nonNestables) + ? array() + : array_combine($nonNestables, $nonNestables) + ); + + # $excerpt is based on the first occurrence of a marker + + while ($excerpt = strpbrk($text, $this->inlineMarkerList)) + { + $marker = $excerpt[0]; + + $markerPosition = strlen($text) - strlen($excerpt); + + $Excerpt = array('text' => $excerpt, 'context' => $text); + + foreach ($this->InlineTypes[$marker] as $inlineType) + { + # check to see if the current inline type is nestable in the current context + + if (isset($nonNestables[$inlineType])) + { + continue; + } + + $Inline = $this->{"inline$inlineType"}($Excerpt); + + if ( ! isset($Inline)) + { + continue; + } + + # makes sure that the inline belongs to "our" marker + + if (isset($Inline['position']) and $Inline['position'] > $markerPosition) + { + continue; + } + + # sets a default inline position + + if ( ! isset($Inline['position'])) + { + $Inline['position'] = $markerPosition; + } + + # cause the new element to 'inherit' our non nestables + + + $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables']) + ? array_merge($Inline['element']['nonNestables'], $nonNestables) + : $nonNestables + ; + + # the text that comes before the inline + $unmarkedText = substr($text, 0, $Inline['position']); + + # compile the unmarked text + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; + + # compile the inline + $Elements[] = $this->extractElement($Inline); + + # remove the examined text + $text = substr($text, $Inline['position'] + $Inline['extent']); + + continue 2; + } + + # the marker does not belong to an inline + + $unmarkedText = substr($text, 0, $markerPosition + 1); + + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; + + $text = substr($text, $markerPosition + 1); + } + + $InlineText = $this->inlineText($text); + $Elements[] = $InlineText['element']; + + foreach ($Elements as &$Element) + { + if ( ! isset($Element['autobreak'])) + { + $Element['autobreak'] = false; + } + } + + return $Elements; + } + + # + # ~ + # + + protected function inlineText($text) + { + $Inline = array( + 'extent' => strlen($text), + 'element' => array(), + ); + + $Inline['element']['elements'] = self::pregReplaceElements( + $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/', + array( + array('name' => 'br'), + array('text' => "\n"), + ), + $text + ); + + return $Inline; + } + + protected function inlineCode($Excerpt) + { + $marker = $Excerpt['text'][0]; + + if (preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(? strlen($matches[0]), + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ); + } + } + + protected function inlineEmailTag($Excerpt) + { + $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'; + + $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@' + . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*'; + + if (strpos($Excerpt['text'], '>') !== false + and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches) + ){ + $url = $matches[1]; + + if ( ! isset($matches[2])) + { + $url = "mailto:$url"; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[1], + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + protected function inlineEmphasis($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + $marker = $Excerpt['text'][0]; + + if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'strong'; + } + elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'em'; + } + else + { + return; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => $emphasis, + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ) + ), + ); + } + + protected function inlineEscapeSequence($Excerpt) + { + if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) + { + return array( + 'element' => array('rawHtml' => $Excerpt['text'][1]), + 'extent' => 2, + ); + } + } + + protected function inlineImage($Excerpt) + { + if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') + { + return; + } + + $Excerpt['text']= substr($Excerpt['text'], 1); + + $Link = $this->inlineLink($Excerpt); + + if ($Link === null) + { + return; + } + + $Inline = array( + 'extent' => $Link['extent'] + 1, + 'element' => array( + 'name' => 'img', + 'attributes' => array( + 'src' => $Link['element']['attributes']['href'], + 'alt' => $Link['element']['handler']['argument'], + ), + 'autobreak' => true, + ), + ); + + $Inline['element']['attributes'] += $Link['element']['attributes']; + + unset($Inline['element']['attributes']['href']); + + return $Inline; + } + + protected function inlineLink($Excerpt) + { + $Element = array( + 'name' => 'a', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => null, + 'destination' => 'elements', + ), + 'nonNestables' => array('Url', 'Link'), + 'attributes' => array( + 'href' => null, + 'title' => null, + ), + ); + + $extent = 0; + + $remainder = $Excerpt['text']; + + if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) + { + $Element['handler']['argument'] = $matches[1]; + + $extent += strlen($matches[0]); + + $remainder = substr($remainder, $extent); + } + else + { + return; + } + + if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) + { + $Element['attributes']['href'] = $matches[1]; + + if (isset($matches[2])) + { + $Element['attributes']['title'] = substr($matches[2], 1, - 1); + } + + $extent += strlen($matches[0]); + } + else + { + if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) + { + $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument']; + $definition = strtolower($definition); + + $extent += strlen($matches[0]); + } + else + { + $definition = strtolower($Element['handler']['argument']); + } + + if ( ! isset($this->DefinitionData['Reference'][$definition])) + { + return; + } + + $Definition = $this->DefinitionData['Reference'][$definition]; + + $Element['attributes']['href'] = $Definition['url']; + $Element['attributes']['title'] = $Definition['title']; + } + + return array( + 'extent' => $extent, + 'element' => $Element, + ); + } + + protected function inlineMarkup($Excerpt) + { + if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) + { + return; + } + + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches)) + { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) + { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches)) + { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + } + + protected function inlineSpecialCharacter($Excerpt) + { + if (substr($Excerpt['text'], 1, 1) !== ' ' and strpos($Excerpt['text'], ';') !== false + and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches) + ) { + return array( + 'element' => array('rawHtml' => '&' . $matches[1] . ';'), + 'extent' => strlen($matches[0]), + ); + } + + return; + } + + protected function inlineStrikethrough($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) + { + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'del', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ) + ), + ); + } + } + + protected function inlineUrl($Excerpt) + { + if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') + { + return; + } + + if (strpos($Excerpt['context'], 'http') !== false + and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE) + ) { + $url = $matches[0][0]; + + $Inline = array( + 'extent' => strlen($matches[0][0]), + 'position' => $matches[0][1], + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + + return $Inline; + } + } + + protected function inlineUrlTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) + { + $url = $matches[1]; + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + # ~ + + protected function unmarkedText($text) + { + $Inline = $this->inlineText($text); + return $this->element($Inline['element']); + } + + # + # Handlers + # + + protected function handle(array $Element) + { + if (isset($Element['handler'])) + { + if (!isset($Element['nonNestables'])) + { + $Element['nonNestables'] = array(); + } + + if (is_string($Element['handler'])) + { + $function = $Element['handler']; + $argument = $Element['text']; + unset($Element['text']); + $destination = 'rawHtml'; + } + else + { + $function = $Element['handler']['function']; + $argument = $Element['handler']['argument']; + $destination = $Element['handler']['destination']; + } + + $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']); + + if ($destination === 'handler') + { + $Element = $this->handle($Element); + } + + unset($Element['handler']); + } + + return $Element; + } + + protected function handleElementRecursive(array $Element) + { + return $this->elementApplyRecursive(array($this, 'handle'), $Element); + } + + protected function handleElementsRecursive(array $Elements) + { + return $this->elementsApplyRecursive(array($this, 'handle'), $Elements); + } + + protected function elementApplyRecursive($closure, array $Element) + { + $Element = call_user_func($closure, $Element); + + if (isset($Element['elements'])) + { + $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']); + } + elseif (isset($Element['element'])) + { + $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']); + } + + return $Element; + } + + protected function elementApplyRecursiveDepthFirst($closure, array $Element) + { + if (isset($Element['elements'])) + { + $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']); + } + elseif (isset($Element['element'])) + { + $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']); + } + + $Element = call_user_func($closure, $Element); + + return $Element; + } + + protected function elementsApplyRecursive($closure, array $Elements) + { + foreach ($Elements as &$Element) + { + $Element = $this->elementApplyRecursive($closure, $Element); + } + + return $Elements; + } + + protected function elementsApplyRecursiveDepthFirst($closure, array $Elements) + { + foreach ($Elements as &$Element) + { + $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element); + } + + return $Elements; + } + + protected function element(array $Element) + { + if ($this->safeMode) + { + $Element = $this->sanitiseElement($Element); + } + + # identity map if element has no handler + $Element = $this->handle($Element); + + $hasName = isset($Element['name']); + + $markup = ''; + + if ($hasName) + { + $markup .= '<' . $Element['name']; + + if (isset($Element['attributes'])) + { + foreach ($Element['attributes'] as $name => $value) + { + if ($value === null) + { + continue; + } + + $markup .= " $name=\"".self::escape($value).'"'; + } + } + } + + $permitRawHtml = false; + + if (isset($Element['text'])) + { + $text = $Element['text']; + } + // very strongly consider an alternative if you're writing an + // extension + elseif (isset($Element['rawHtml'])) + { + $text = $Element['rawHtml']; + + $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; + $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; + } + + $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']); + + if ($hasContent) + { + $markup .= $hasName ? '>' : ''; + + if (isset($Element['elements'])) + { + $markup .= $this->elements($Element['elements']); + } + elseif (isset($Element['element'])) + { + $markup .= $this->element($Element['element']); + } + else + { + if (!$permitRawHtml) + { + $markup .= self::escape($text, true); + } + else + { + $markup .= $text; + } + } + + $markup .= $hasName ? '' : ''; + } + elseif ($hasName) + { + $markup .= ' />'; + } + + return $markup; + } + + protected function elements(array $Elements) + { + $markup = ''; + + $autoBreak = true; + + foreach ($Elements as $Element) + { + if (empty($Element)) + { + continue; + } + + $autoBreakNext = (isset($Element['autobreak']) + ? $Element['autobreak'] : isset($Element['name']) + ); + // (autobreak === false) covers both sides of an element + $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext; + + $markup .= ($autoBreak ? "\n" : '') . $this->element($Element); + $autoBreak = $autoBreakNext; + } + + $markup .= $autoBreak ? "\n" : ''; + + return $markup; + } + + # ~ + + protected function li($lines) + { + $Elements = $this->linesElements($lines); + + if ( ! in_array('', $lines) + and isset($Elements[0]) and isset($Elements[0]['name']) + and $Elements[0]['name'] === 'p' + ) { + unset($Elements[0]['name']); + } + + return $Elements; + } + + # + # AST Convenience + # + + /** + * Replace occurrences $regexp with $Elements in $text. Return an array of + * elements representing the replacement. + */ + protected static function pregReplaceElements($regexp, $Elements, $text) + { + $newElements = array(); + + while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE)) + { + $offset = $matches[0][1]; + $before = substr($text, 0, $offset); + $after = substr($text, $offset + strlen($matches[0][0])); + + $newElements[] = array('text' => $before); + + foreach ($Elements as $Element) + { + $newElements[] = $Element; + } + + $text = $after; + } + + $newElements[] = array('text' => $text); + + return $newElements; + } + + # + # Deprecated Methods + # + + function parse($text) + { + $markup = $this->text($text); + + return $markup; + } + + protected function sanitiseElement(array $Element) + { + static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; + static $safeUrlNameToAtt = array( + 'a' => 'href', + 'img' => 'src', + ); + + if ( ! isset($Element['name'])) + { + unset($Element['attributes']); + return $Element; + } + + if (isset($safeUrlNameToAtt[$Element['name']])) + { + $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); + } + + if ( ! empty($Element['attributes'])) + { + foreach ($Element['attributes'] as $att => $val) + { + # filter out badly parsed attribute + if ( ! preg_match($goodAttribute, $att)) + { + unset($Element['attributes'][$att]); + } + # dump onevent attribute + elseif (self::striAtStart($att, 'on')) + { + unset($Element['attributes'][$att]); + } + } + } + + return $Element; + } + + protected function filterUnsafeUrlInAttribute(array $Element, $attribute) + { + foreach ($this->safeLinksWhitelist as $scheme) + { + if (self::striAtStart($Element['attributes'][$attribute], $scheme)) + { + return $Element; + } + } + + $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); + + return $Element; + } + + # + # Static Methods + # + + protected static function escape($text, $allowQuotes = false) + { + return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); + } + + protected static function striAtStart($string, $needle) + { + $len = strlen($needle); + + if ($len > strlen($string)) + { + return false; + } + else + { + return strtolower(substr($string, 0, $len)) === strtolower($needle); + } + } + + static function instance($name = 'default') + { + if (isset(self::$instances[$name])) + { + return self::$instances[$name]; + } + + $instance = new static(); + + self::$instances[$name] = $instance; + + return $instance; + } + + private static $instances = array(); + + # + # Fields + # + + protected $DefinitionData; + + # + # Read-Only + + protected $specialCharacters = array( + '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~' + ); + + protected $StrongRegex = array( + '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s', + '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us', + ); + + protected $EmRegex = array( + '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', + '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', + ); + + protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+'; + + protected $voidElements = array( + 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', + ); + + protected $textLevelElements = array( + 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', + 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', + 'i', 'rp', 'del', 'code', 'strike', 'marquee', + 'q', 'rt', 'ins', 'font', 'strong', + 's', 'tt', 'kbd', 'mark', + 'u', 'xm', 'sub', 'nobr', + 'sup', 'ruby', + 'var', 'span', + 'wbr', 'time', + ); +} diff --git a/lib/parsedownextraplugin/parsedownextra.php b/lib/parsedownextraplugin/parsedownextra.php new file mode 100644 index 0000000000..2eeeda6e0b --- /dev/null +++ b/lib/parsedownextraplugin/parsedownextra.php @@ -0,0 +1,686 @@ +BlockTypes[':'] []= 'DefinitionList'; + $this->BlockTypes['*'] []= 'Abbreviation'; + + # identify footnote definitions before reference definitions + array_unshift($this->BlockTypes['['], 'Footnote'); + + # identify footnote markers before before links + array_unshift($this->InlineTypes['['], 'FootnoteMarker'); + } + + # + # ~ + + function text($text) + { + $Elements = $this->textElements($text); + + # convert to markup + $markup = $this->elements($Elements); + + # trim line breaks + $markup = trim($markup, "\n"); + + # merge consecutive dl elements + + $markup = preg_replace('/<\/dl>\s+
    \s+/', '', $markup); + + # add footnotes + + if (isset($this->DefinitionData['Footnote'])) + { + $Element = $this->buildFootnoteElement(); + + $markup .= "\n" . $this->element($Element); + } + + return $markup; + } + + # + # Blocks + # + + # + # Abbreviation + + protected function blockAbbreviation($Line) + { + if (preg_match('/^\*\[(.+?)\]:[ ]*(.+?)[ ]*$/', $Line['text'], $matches)) + { + $this->DefinitionData['Abbreviation'][$matches[1]] = $matches[2]; + + $Block = array( + 'hidden' => true, + ); + + return $Block; + } + } + + # + # Footnote + + protected function blockFootnote($Line) + { + if (preg_match('/^\[\^(.+?)\]:[ ]?(.*)$/', $Line['text'], $matches)) + { + $Block = array( + 'label' => $matches[1], + 'text' => $matches[2], + 'hidden' => true, + ); + + return $Block; + } + } + + protected function blockFootnoteContinue($Line, $Block) + { + if ($Line['text'][0] === '[' and preg_match('/^\[\^(.+?)\]:/', $Line['text'])) + { + return; + } + + if (isset($Block['interrupted'])) + { + if ($Line['indent'] >= 4) + { + $Block['text'] .= "\n\n" . $Line['text']; + + return $Block; + } + } + else + { + $Block['text'] .= "\n" . $Line['text']; + + return $Block; + } + } + + protected function blockFootnoteComplete($Block) + { + $this->DefinitionData['Footnote'][$Block['label']] = array( + 'text' => $Block['text'], + 'count' => null, + 'number' => null, + ); + + return $Block; + } + + # + # Definition List + + protected function blockDefinitionList($Line, $Block) + { + if ( ! isset($Block) or $Block['type'] !== 'Paragraph') + { + return; + } + + $Element = array( + 'name' => 'dl', + 'elements' => array(), + ); + + $terms = explode("\n", $Block['element']['handler']['argument']); + + foreach ($terms as $term) + { + $Element['elements'] []= array( + 'name' => 'dt', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $term, + 'destination' => 'elements' + ), + ); + } + + $Block['element'] = $Element; + + $Block = $this->addDdElement($Line, $Block); + + return $Block; + } + + protected function blockDefinitionListContinue($Line, array $Block) + { + if ($Line['text'][0] === ':') + { + $Block = $this->addDdElement($Line, $Block); + + return $Block; + } + else + { + if (isset($Block['interrupted']) and $Line['indent'] === 0) + { + return; + } + + if (isset($Block['interrupted'])) + { + $Block['dd']['handler']['function'] = 'textElements'; + $Block['dd']['handler']['argument'] .= "\n\n"; + + $Block['dd']['handler']['destination'] = 'elements'; + + unset($Block['interrupted']); + } + + $text = substr($Line['body'], min($Line['indent'], 4)); + + $Block['dd']['handler']['argument'] .= "\n" . $text; + + return $Block; + } + } + + # + # Header + + protected function blockHeader($Line) + { + $Block = parent::blockHeader($Line); + + if ($Block !== null && preg_match('/[ #]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, PREG_OFFSET_CAPTURE)) + { + $attributeString = $matches[1][0]; + + $Block['element']['attributes'] = $this->parseAttributeData($attributeString); + + $Block['element']['handler']['argument'] = substr($Block['element']['handler']['argument'], 0, $matches[0][1]); + } + + return $Block; + } + + # + # Markup + + protected function blockMarkup($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) + { + $element = strtolower($matches[1]); + + if (in_array($element, $this->textLevelElements)) + { + return; + } + + $Block = array( + 'name' => $matches[1], + 'depth' => 0, + 'element' => array( + 'rawHtml' => $Line['text'], + 'autobreak' => true, + ), + ); + + $length = strlen($matches[0]); + $remainder = substr($Line['text'], $length); + + if (trim($remainder) === '') + { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) + { + $Block['closed'] = true; + $Block['void'] = true; + } + } + else + { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) + { + return; + } + if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder)) + { + $Block['closed'] = true; + } + } + + return $Block; + } + } + + protected function blockMarkupContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open + { + $Block['depth'] ++; + } + + if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close + { + if ($Block['depth'] > 0) + { + $Block['depth'] --; + } + else + { + $Block['closed'] = true; + } + } + + if (isset($Block['interrupted'])) + { + $Block['element']['rawHtml'] .= "\n"; + unset($Block['interrupted']); + } + + $Block['element']['rawHtml'] .= "\n".$Line['body']; + + return $Block; + } + + protected function blockMarkupComplete($Block) + { + if ( ! isset($Block['void'])) + { + $Block['element']['rawHtml'] = $this->processTag($Block['element']['rawHtml']); + } + + return $Block; + } + + # + # Setext + + protected function blockSetextHeader($Line, array $Block = null) + { + $Block = parent::blockSetextHeader($Line, $Block); + + if ($Block !== null && preg_match('/[ ]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, PREG_OFFSET_CAPTURE)) + { + $attributeString = $matches[1][0]; + + $Block['element']['attributes'] = $this->parseAttributeData($attributeString); + + $Block['element']['handler']['argument'] = substr($Block['element']['handler']['argument'], 0, $matches[0][1]); + } + + return $Block; + } + + # + # Inline Elements + # + + # + # Footnote Marker + + protected function inlineFootnoteMarker($Excerpt) + { + if (preg_match('/^\[\^(.+?)\]/', $Excerpt['text'], $matches)) + { + $name = $matches[1]; + + if ( ! isset($this->DefinitionData['Footnote'][$name])) + { + return; + } + + $this->DefinitionData['Footnote'][$name]['count'] ++; + + if ( ! isset($this->DefinitionData['Footnote'][$name]['number'])) + { + $this->DefinitionData['Footnote'][$name]['number'] = ++ $this->footnoteCount; # » & + } + + $Element = array( + 'name' => 'sup', + 'attributes' => array('id' => 'fnref'.$this->DefinitionData['Footnote'][$name]['count'].':'.$name), + 'element' => array( + 'name' => 'a', + 'attributes' => array('href' => '#fn:'.$name, 'class' => 'footnote-ref'), + 'text' => $this->DefinitionData['Footnote'][$name]['number'], + ), + ); + + return array( + 'extent' => strlen($matches[0]), + 'element' => $Element, + ); + } + } + + private $footnoteCount = 0; + + # + # Link + + protected function inlineLink($Excerpt) + { + $Link = parent::inlineLink($Excerpt); + + $remainder = $Link !== null ? substr($Excerpt['text'], $Link['extent']) : ''; + + if (preg_match('/^[ ]*{('.$this->regexAttribute.'+)}/', $remainder, $matches)) + { + $Link['element']['attributes'] += $this->parseAttributeData($matches[1]); + + $Link['extent'] += strlen($matches[0]); + } + + return $Link; + } + + # + # ~ + # + + private $currentAbreviation; + private $currentMeaning; + + protected function insertAbreviation(array $Element) + { + if (isset($Element['text'])) + { + $Element['elements'] = self::pregReplaceElements( + '/\b'.preg_quote($this->currentAbreviation, '/').'\b/', + array( + array( + 'name' => 'abbr', + 'attributes' => array( + 'title' => $this->currentMeaning, + ), + 'text' => $this->currentAbreviation, + ) + ), + $Element['text'] + ); + + unset($Element['text']); + } + + return $Element; + } + + protected function inlineText($text) + { + $Inline = parent::inlineText($text); + + if (isset($this->DefinitionData['Abbreviation'])) + { + foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning) + { + $this->currentAbreviation = $abbreviation; + $this->currentMeaning = $meaning; + + $Inline['element'] = $this->elementApplyRecursiveDepthFirst( + array($this, 'insertAbreviation'), + $Inline['element'] + ); + } + } + + return $Inline; + } + + # + # Util Methods + # + + protected function addDdElement(array $Line, array $Block) + { + $text = substr($Line['text'], 1); + $text = trim($text); + + unset($Block['dd']); + + $Block['dd'] = array( + 'name' => 'dd', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $text, + 'destination' => 'elements' + ), + ); + + if (isset($Block['interrupted'])) + { + $Block['dd']['handler']['function'] = 'textElements'; + + unset($Block['interrupted']); + } + + $Block['element']['elements'] []= & $Block['dd']; + + return $Block; + } + + protected function buildFootnoteElement() + { + $Element = array( + 'name' => 'div', + 'attributes' => array('class' => 'footnotes'), + 'elements' => array( + array('name' => 'hr'), + array( + 'name' => 'ol', + 'elements' => array(), + ), + ), + ); + + uasort($this->DefinitionData['Footnote'], 'self::sortFootnotes'); + + foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData) + { + if ( ! isset($DefinitionData['number'])) + { + continue; + } + + $text = $DefinitionData['text']; + + $textElements = parent::textElements($text); + + $numbers = range(1, $DefinitionData['count']); + + $backLinkElements = array(); + + foreach ($numbers as $number) + { + $backLinkElements[] = array('text' => ' '); + $backLinkElements[] = array( + 'name' => 'a', + 'attributes' => array( + 'href' => "#fnref$number:$definitionId", + 'rev' => 'footnote', + 'class' => 'footnote-backref', + ), + 'rawHtml' => '↩', + 'allowRawHtmlInSafeMode' => true, + 'autobreak' => false, + ); + } + + unset($backLinkElements[0]); + + $n = count($textElements) -1; + + if ($textElements[$n]['name'] === 'p') + { + $backLinkElements = array_merge( + array( + array( + 'rawHtml' => ' ', + 'allowRawHtmlInSafeMode' => true, + ), + ), + $backLinkElements + ); + + unset($textElements[$n]['name']); + + $textElements[$n] = array( + 'name' => 'p', + 'elements' => array_merge( + array($textElements[$n]), + $backLinkElements + ), + ); + } + else + { + $textElements[] = array( + 'name' => 'p', + 'elements' => $backLinkElements + ); + } + + $Element['elements'][1]['elements'] []= array( + 'name' => 'li', + 'attributes' => array('id' => 'fn:'.$definitionId), + 'elements' => array_merge( + $textElements + ), + ); + } + + return $Element; + } + + # ~ + + protected function parseAttributeData($attributeString) + { + $Data = array(); + + $attributes = preg_split('/[ ]+/', $attributeString, - 1, PREG_SPLIT_NO_EMPTY); + + foreach ($attributes as $attribute) + { + if ($attribute[0] === '#') + { + $Data['id'] = substr($attribute, 1); + } + else # "." + { + $classes []= substr($attribute, 1); + } + } + + if (isset($classes)) + { + $Data['class'] = implode(' ', $classes); + } + + return $Data; + } + + # ~ + + protected function processTag($elementMarkup) # recursive + { + # http://stackoverflow.com/q/1148928/200145 + libxml_use_internal_errors(true); + + $DOMDocument = new DOMDocument; + + # http://stackoverflow.com/q/11309194/200145 + $elementMarkup = mb_convert_encoding($elementMarkup, 'HTML-ENTITIES', 'UTF-8'); + + # http://stackoverflow.com/q/4879946/200145 + $DOMDocument->loadHTML($elementMarkup); + $DOMDocument->removeChild($DOMDocument->doctype); + $DOMDocument->replaceChild($DOMDocument->firstChild->firstChild->firstChild, $DOMDocument->firstChild); + + $elementText = ''; + + if ($DOMDocument->documentElement->getAttribute('markdown') === '1') + { + foreach ($DOMDocument->documentElement->childNodes as $Node) + { + $elementText .= $DOMDocument->saveHTML($Node); + } + + $DOMDocument->documentElement->removeAttribute('markdown'); + + $elementText = "\n".$this->text($elementText)."\n"; + } + else + { + foreach ($DOMDocument->documentElement->childNodes as $Node) + { + $nodeMarkup = $DOMDocument->saveHTML($Node); + + if ($Node instanceof DOMElement and ! in_array($Node->nodeName, $this->textLevelElements)) + { + $elementText .= $this->processTag($nodeMarkup); + } + else + { + $elementText .= $nodeMarkup; + } + } + } + + # because we don't want for markup to get encoded + $DOMDocument->documentElement->nodeValue = 'placeholder\x1A'; + + $markup = $DOMDocument->saveHTML($DOMDocument->documentElement); + $markup = str_replace('placeholder\x1A', $elementText, $markup); + + return $markup; + } + + # ~ + + protected function sortFootnotes($A, $B) # callback + { + return $A['number'] - $B['number']; + } + + # + # Fields + # + + protected $regexAttribute = '(?:[#.][-\w]+[ ]*)'; +} diff --git a/lib/parsedownextraplugin/parsedownextraplugin.class.php b/lib/parsedownextraplugin/parsedownextraplugin.class.php new file mode 100644 index 0000000000..f4787db640 --- /dev/null +++ b/lib/parsedownextraplugin/parsedownextraplugin.class.php @@ -0,0 +1,590 @@ +'; + + protected $regexAttribute = '(?:[#.][-\w:\\\]+[ ]*|[-\w:\\\]+(?:=(?:["\'][^\n]*?["\']|[^\s]+)?)?[ ]*)'; + + # Method aliases for every configuration property + public function __call($key, array $arguments = array()) { + $property = lcfirst(substr($key, 3)); + if (strpos($key, 'set') === 0 && property_exists($this, $property)) { + $this->{$property} = $arguments[0]; + return $this; + } + throw new Exception('Method ' . $key . ' does not exists.'); + } + + public function __construct() { + if (version_compare(parent::version, '0.8.0-beta-1') < 0) { + throw new Exception('ParsedownExtraPlugin requires a later version of Parsedown'); + } + $this->BlockTypes['!'][] = 'Image'; + parent::__construct(); + } + + protected function blockAbbreviation($Line) { + // Allow empty abbreviations + if (preg_match('/^\*\[(.+?)\]:[ ]*$/', $Line['text'], $matches)) { + $this->DefinitionData['Abbreviation'][$matches[1]] = null; + return array('hidden' => true); + } + return parent::blockAbbreviation($Line); + } + + protected function blockCodeComplete($Block) { + $this->doSetAttributes($Block['element']['element'], $this->blockCodeAttributes); + $this->doSetContent($Block['element']['element'], $this->blockCodeHtml, true); + // Put code attributes on parent element + if ($this->codeAttributesOnParent) { + if ($this->codeAttributesOnParent === true) { + // $this->codeAttributesOnParent = array_keys($Block['element']['element']['attributes']); + $this->codeAttributesOnParent = array('class', 'id'); + } + foreach ((array) $this->codeAttributesOnParent as $Name) { + if (isset($Block['element']['element']['attributes'][$Name])) { + $Block['element']['attributes'][$Name] = $Block['element']['element']['attributes'][$Name]; + unset($Block['element']['element']['attributes'][$Name]); + } + } + } + $Block['element']['element']['rawHtml'] = $Block['element']['element']['text']; + $Block['element']['element']['allowRawHtmlInSafeMode'] = true; + unset($Block['element']['element']['text']); + return $Block; + } + + protected function blockFencedCode($Line) { + // Re-enable the multiple class name feature + $Line['text'] = strtr(trim($Line['text']), array( + ' ' => "\x1A", + '.' => "\x1A." + )); + // Enable custom attribute syntax on code block + $Attributes = array(); + if (strpos($Line['text'], '{') !== false && substr($Line['text'], -1) === '}') { + $Parts = explode('{', $Line['text'], 2); + $Attributes = $this->parseAttributeData(strtr(substr($Parts[1], 0, -1), "\x1A", ' ')); + $Line['text'] = trim($Parts[0]); + } + if (!$Block = parent::blockFencedCode($Line)) { + return; + } + if ($Attributes) { + $Block['element']['element']['attributes'] = $Attributes; + } else if (isset($Block['element']['element']['attributes']['class'])) { + $Classes = explode("\x1A", strtr($Block['element']['element']['attributes']['class'], ' ', "\x1A")); + // `~~~ php` → `
    `
    +            // `~~~ php html` → `
    `
    +            // `~~~ .php` → `
    `
    +            // `~~~ .php.html` → `
    `
    +            // `~~~ .php html` → `
    `
    +            // `~~~ {.php #foo}` → `
    `
    +            $Results = [];
    +            foreach ($Classes as $Class) {
    +                if ($Class === "" || $Class === strtr($this->blockCodeClassFormat, array('%s' => ""))) {
    +                    continue;
    +                }
    +                if ($Class[0] === '.') {
    +                    $Results[] = substr($Class, 1);
    +                } else if (preg_match('/^' . strtr(preg_quote($this->blockCodeClassFormat), array('%s' => '\S+')) . '$/', $Class)) {
    +                    $Results[] = $Class; // Do nothing!
    +                } else {
    +                    $Results[] = sprintf($this->blockCodeClassFormat, $Class);
    +                }
    +            }
    +            if ($Results = array_unique($Results)) {
    +                $Block['element']['element']['attributes']['class'] = implode(' ', $Results);
    +            } else {
    +                unset($Block['element']['element']['attributes']['class']);
    +            }
    +        }
    +        return $Block;
    +    }
    +
    +    protected function blockFencedCodeComplete($Block) {
    +        return $this->blockCodeComplete($Block);
    +    }
    +
    +    protected function blockHeader($Line) {
    +        if (!$Block = parent::blockHeader($Line)) {
    +            return;
    +        }
    +        $Level = strspn($Line['text'], '#');
    +        $this->doSetAttributes($Block['element'], $this->headerAttributes, array($Level));
    +        $this->doSetContent($Block['element'], $this->headerText, false, 'argument', array($Level));
    +        return $Block;
    +    }
    +
    +    protected function blockImage($Line) {
    +        if (!$this->figuresEnabled) {
    +            return;
    +        }
    +        // Match exactly an image syntax in a paragraph (with optional custom attributes, and optional hard break marker)
    +        if (preg_match('/^\!\[[^\n]*?\](\[[^\n]*?\]|\([^\n]*?\))(\s*\{' . $this->regexAttribute . '+?\})?([ ]{2})?$/', $Line['text'])) {
    +            $Block = array(
    +                'description' => "",
    +                'element' => array(
    +                    'name' => 'figure',
    +                    'attributes' => array(),
    +                    'elements' => array(
    +                        $this->inlineImage($Line)
    +                    )
    +                )
    +            );
    +            $this->doSetAttributes($Block['element'], $this->figureAttributes);
    +            return $Block;
    +        }
    +        return;
    +    }
    +
    +    protected function blockImageComplete($Block) {
    +        if (!empty($Block['description'])) {
    +            $Description = $Block['description'];
    +            $Block['element']['elements'][] = array(
    +                'name' => 'figcaption',
    +                'rawHtml' => $this->{strpos($Description, "\n\n") === false ? 'line' : 'text'}(trim($Description, "\n"))
    +            );
    +            // unset($Block['description']);
    +        }
    +        if ($this->imageAttributesOnParent) {
    +            $Inline = $Block['element']['elements'][0];
    +            if ($this->imageAttributesOnParent === true) {
    +                $this->imageAttributesOnParent = array_keys($Inline['element']['attributes']);
    +            }
    +            foreach ((array) $this->imageAttributesOnParent as $Name) {
    +                if (isset($Inline['element']['attributes'][$Name])) {
    +                    // Merge class names
    +                    if (
    +                        $Name === 'class' &&
    +                        isset($Block['element']['attributes'][$Name]) &&
    +                        isset($Inline['element']['attributes'][$Name])
    +                    ) {
    +                        $Classes = array_merge(
    +                            explode(' ', $Block['element']['attributes'][$Name]),
    +                            explode(' ', $Inline['element']['attributes'][$Name])
    +                        );
    +                        sort($Classes);
    +                        $Block['element']['attributes']['class'] = implode(' ', array_unique(array_filter($Classes)));
    +                        unset($Block['element']['elements'][0]['element']['attributes'][$Name]);
    +                        continue;
    +                    }
    +                    $Block['element']['attributes'][$Name] = $Inline['element']['attributes'][$Name];
    +                    unset($Block['element']['elements'][0]['element']['attributes'][$Name]);
    +                }
    +            }
    +        }
    +        return $Block;
    +    }
    +
    +    protected function blockImageContinue($Line, array $Block) {
    +        if (isset($Block['complete'])) {
    +            return;
    +        }
    +        if (isset($Block['interrupted'])) {
    +            $Block['description'] .= "\n";
    +            unset($Block['interrupted']);
    +        }
    +        if ($Line['indent'] === 0) {
    +            $Block['complete'] = true;
    +            return;
    +        }
    +        if ($Line['indent'] > 0 && $Line['indent'] < 4) {
    +            $Block['description'] .= "\n" . $Line['text'];
    +            return $Block;
    +        }
    +        return;
    +    }
    +
    +    protected function blockQuoteComplete($Block) {
    +        $this->doSetAttributes($Block['element'], $this->blockQuoteAttributes);
    +        $this->doSetContent($Block['element'], $this->blockQuoteText, false, 'arguments');
    +        return $Block;
    +    }
    +
    +    protected function blockSetextHeader($Line, array $Block = null) {
    +        if (!$Block = parent::blockSetextHeader($Line, $Block)) {
    +            return;
    +        }
    +        $Level = $Line['text'][0] === '=' ? 1 : 2;
    +        $this->doSetAttributes($Block['element'], $this->headerAttributes, array($Level));
    +        $this->doSetContent($Block['element'], $this->headerText, false, 'argument', array($Level));
    +        return $Block;
    +    }
    +
    +    protected function blockTableContinue($Line, array $Block) {
    +        if (!$Block = parent::blockTableContinue($Line, $Block)) {
    +            return;
    +        }
    +        $Aligns = $Block['alignments'];
    +        // `` or ``
    +        foreach ($Block['element']['elements'] as $Index0 => &$Element0) {
    +            // ``
    +            foreach ($Element0['elements'] as $Index1 => &$Element1) {
    +                // `` or ``
    +                foreach ($Element1['elements'] as $Index2 => &$Element2) {
    +                    $this->doSetAttributes($Element2, $this->tableColumnAttributes, array($Aligns[$Index2], $Index2, $Index1));
    +                }
    +            }
    +        }
    +        return $Block;
    +    }
    +
    +    protected function blockTableComplete($Block) {
    +        $this->doSetAttributes($Block['element'], $this->tableAttributes);
    +        return $Block;
    +    }
    +
    +    protected function buildFootnoteElement() {
    +        $DefinitionData = $this->DefinitionData['Footnote'];
    +        if (!$Footnotes = parent::buildFootnoteElement()) {
    +            return;
    +        }
    +        $DefinitionKey = array_keys($DefinitionData);
    +        $DefinitionData = array_values($DefinitionData);
    +        $this->doSetAttributes($Footnotes, $this->footnoteAttributes);
    +        foreach ($Footnotes['elements'][1]['elements'] as $Index0 => &$Element0) {
    +            $Name = $DefinitionKey[$Index0];
    +            $Count = $DefinitionData[$Index0]['count'];
    +            $Args = array(is_numeric($Name) ? (float) $Name : $Name, $Count);
    +            $this->doSetAttributes($Element0, $this->footnoteBackReferenceAttributes, $Args);
    +            foreach ($Element0['elements'] as $Index1 => &$Element1) {
    +                if (!isset($Element1['elements'])) {
    +                    continue;
    +                }
    +                $Count = 0;
    +                foreach ($Element1['elements'] as $Index2 => &$Element2) {
    +                    if (!isset($Element2['name']) || $Element2['name'] !== 'a') {
    +                        continue;
    +                    }
    +                    $Args[1] = ++$Count;
    +                    $this->doSetAttributes($Element2, $this->footnoteBackLinkAttributes, $Args);
    +                    $this->doSetContent($Element2, $this->footnoteBackLinkHtml, false, 'rawHtml');
    +                }
    +            }
    +        }
    +        return $Footnotes;
    +    }
    +
    +    protected function doGetAttributes($Element) {
    +        if (isset($Element['attributes'])) {
    +            return (array) $Element['attributes'];
    +        }
    +        return array();
    +    }
    +
    +    protected function doGetContent($Element) {
    +        if (isset($Element['text'])) {
    +            return $Element['text'];
    +        }
    +        if (isset($Element['rawHtml'])) {
    +            return $Element['rawHtml'];
    +        }
    +        if (isset($Element['handler']['argument'])) {
    +            return implode("\n", (array) $Element['handler']['argument']);
    +        }
    +        return null;
    +    }
    +
    +    private function doSetLink($Excerpt, $Function) {
    +        if (!$Inline = call_user_func('parent::' . $Function, $Excerpt)) {
    +            return;
    +        }
    +        $this->doSetAttributes($Inline['element'], $this->linkAttributes, array($this->isLocal($Inline['element'], 'href')));
    +        $this->doSetData($this->DefinitionData['Reference'], $this->referenceData);
    +        return $Inline;
    +    }
    +
    +    protected function doSetAttributes(&$Element, $From, $Args = array()) {
    +        $Attributes = $this->doGetAttributes($Element);
    +        $Content = $this->doGetContent($Element);
    +        if (is_callable($From)) {
    +            $Args = array_merge(array($Content, $Attributes, &$Element), $Args);
    +            $Element['attributes'] = array_replace($Attributes, (array) call_user_func_array($From, $Args));
    +        } else {
    +            $Element['attributes'] = array_replace($Attributes, (array) $From);
    +        }
    +    }
    +
    +    protected function doSetContent(&$Element, $From, $Esc = false, $Mode = 'text', $Args = array()) {
    +        $Attributes = $this->doGetAttributes($Element);
    +        $Content = $this->doGetContent($Element);
    +        if ($Esc) {
    +            $Content = parent::escape($Content, true);
    +        }
    +        if (is_callable($From)) {
    +            $Args = array_merge(array($Content, $Attributes, &$Element), $Args);
    +            $Content = call_user_func_array($From, $Args);
    +        } else if (!empty($From)) {
    +            $Content = sprintf($From, $Content);
    +        }
    +        if ($Mode === 'arguments') {
    +            $Element['handler']['argument'] = explode("\n", $Content);
    +        } else if ($Mode === 'argument') {
    +            $Element['handler']['argument'] = $Content;
    +        } else {
    +            $Element[$Mode] = $Content;
    +        }
    +    }
    +
    +    protected function doSetData(&$To, $From) {
    +        $To = array_replace((array) $To, (array) $From);
    +    }
    +
    +    protected function element(array $Element) {
    +        if (!$Any = parent::element($Element)) {
    +            return;
    +        }
    +        if (substr($Any, -3) === ' />') {
    +            if (is_callable($this->voidElementSuffix)) {
    +                $Attributes = $this->doGetAttributes($Element);
    +                $Content = $this->doGetContent($Element);
    +                $Suffix = call_user_func_array($this->voidElementSuffix, [$Content, $Attributes, &$Element]);
    +            } else {
    +                $Suffix = $this->voidElementSuffix;
    +            }
    +            $Any = substr_replace($Any, $Suffix, -3);
    +        }
    +        return $Any;
    +    }
    +
    +    protected function inlineCode($Excerpt) {
    +        if (!$Inline = parent::inlineCode($Excerpt)) {
    +            return;
    +        }
    +        $this->doSetAttributes($Inline['element'], $this->codeAttributes);
    +        $this->doSetContent($Inline['element'], $this->codeHtml, true);
    +        $Inline['element']['rawHtml'] = $Inline['element']['text'];
    +        $Inline['element']['allowRawHtmlInSafeMode'] = true;
    +        unset($Inline['element']['text']);
    +        return $Inline;
    +    }
    +
    +    protected function inlineFootnoteMarker($Excerpt) {
    +        if (!$Inline = parent::inlineFootnoteMarker($Excerpt)) {
    +            return;
    +        }
    +        $Name = null;
    +        if (preg_match('/^\[\^(.+?)\]/', $Excerpt['text'], $matches)) {
    +            $Name = $matches[1];
    +        }
    +        $Args = array(is_numeric($Name) ? (float) $Name : $Name, $this->DefinitionData['Footnote'][$Name]['count']);
    +        $this->doSetAttributes($Inline['element'], $this->footnoteReferenceAttributes, $Args);
    +        $this->doSetAttributes($Inline['element']['element'], $this->footnoteLinkAttributes, $Args);
    +        $this->doSetContent($Inline['element']['element'], $this->footnoteLinkHtml, false, 'text', $Args);
    +        $Inline['element']['element']['rawHtml'] = $Inline['element']['element']['text'];
    +        $Inline['element']['element']['allowRawHtmlInSafeMode'] = true;
    +        unset($Inline['element']['element']['text']);
    +        return $Inline;
    +    }
    +
    +    protected function inlineImage($Excerpt) {
    +        if (!$Inline = parent::inlineImage($Excerpt)) {
    +            return;
    +        }
    +        $this->doSetAttributes($Inline['element'], $this->imageAttributes, array($this->isLocal($Inline['element'], 'src')));
    +        return $Inline;
    +    }
    +
    +    protected function inlineLink($Excerpt) {
    +        return $this->doSetLink($Excerpt, __FUNCTION__);
    +    }
    +
    +    protected function inlineText($Text) {
    +        $this->doSetData($this->DefinitionData['Abbreviation'], $this->abbreviationData);
    +        return parent::inlineText($Text);
    +    }
    +
    +    protected function inlineUrl($Excerpt) {
    +        return $this->doSetLink($Excerpt, __FUNCTION__);
    +    }
    +
    +    protected function inlineUrlTag($Excerpt) {
    +        return $this->doSetLink($Excerpt, __FUNCTION__);
    +    }
    +
    +    protected function isLocal($Element, $Key) {
    +        $Link = isset($Element['attributes'][$Key]) ? (string) $Element['attributes'][$Key] : null;
    +        if (
    +            // ``
    +            $Link === "" ||
    +            // ``
    +            // ``
    +            // ``
    +            // ``
    +            // ``
    +            strpos('./?&#', $Link[0]) !== false && strpos($Link, '//') !== 0 ||
    +            // ``
    +            strpos($Link, 'data:') === 0 ||
    +            // ``
    +            strpos($Link, 'javascript:') === 0 ||
    +            // ``
    +            strpos($Link, 'mailto:') === 0
    +        ) {
    +            return true;
    +        }
    +        if (isset($_SERVER['HTTP_HOST'])) {
    +            $Host = $_SERVER['HTTP_HOST'];
    +        } else if (isset($_SERVER['SERVER_NAME'])) {
    +            $Host = $_SERVER['SERVER_NAME'];
    +        } else {
    +            $Host = "";
    +        }
    +        // ``
    +        if (strpos($Link, '//') === 0 && strpos($Link, '//' . $Host) !== 0) {
    +            return false;
    +        }
    +        if (
    +            // ``
    +            strpos($Link, 'https://' . $Host) === 0 ||
    +            // ``
    +            strpos($Link, 'http://' . $Host) === 0
    +        ) {
    +            return true;
    +        }
    +        // ``
    +        return strpos($Link, '://') === false;
    +    }
    +
    +    protected function parseAttributeData($attributeString) {
    +        // Allow compact attributes
    +        $attributeString = strtr($attributeString, array(
    +            '#' => ' #',
    +            '.' => ' .'
    +        ));
    +        if (strpos($attributeString, '="') !== false || strpos($attributeString, "='") !== false) {
    +            $attributeString = preg_replace_callback('#([-\w]+=)(["\'])([^\n]*?)\2#', function($matches) {
    +                $value = strtr($matches[3], array(
    +                    ' #' => '#',
    +                    ' .' => '.',
    +                    ' ' => "\x1A"
    +                ));
    +                return $matches[1] . $matches[2] . $value . $matches[2];
    +            }, $attributeString);
    +        }
    +        $Attributes = array();
    +        foreach (explode(' ', $attributeString) as $v) {
    +            if (!$v) {
    +                continue;
    +            }
    +            // `{#foo}`
    +            if ($v[0] === '#' && isset($v[1])) {
    +                $Attributes['id'] = substr($v, 1);
    +            // `{.foo}`
    +            } else if ($v[0] === '.' && isset($v[1])) {
    +                $Attributes['class'][] = substr($v, 1);
    +            // ~
    +            } else if (strpos($v, '=') !== false) {
    +                $vv = explode('=', $v, 2);
    +                // `{foo=}`
    +                if ($vv[1] === "") {
    +                    if ($vv[0] === 'class') {
    +                        continue;
    +                    }
    +                    $Attributes[$vv[0]] = "";
    +                // `{foo="bar baz"}`
    +                // `{foo='bar baz'}`
    +                } else if ($vv[1][0] === '"' && substr($vv[1], -1) === '"' || $vv[1][0] === "'" && substr($vv[1], -1) === "'") {
    +                    $values = stripslashes(strtr(substr(substr($vv[1], 1), 0, -1), "\x1A", ' '));
    +                    if ($vv[0] === 'class' && isset($Attributes[$vv[0]])) {
    +                        $values = explode(' ', $values);
    +                        $Attributes[$vv[0]] = array_merge($Attributes[$vv[0]], $values);
    +                    } else {
    +                        $Attributes[$vv[0]] = $values;
    +                    }
    +                // `{foo=bar}`
    +                } else {
    +                    if ($vv[0] === 'class' && isset($Attributes[$vv[0]])) {
    +                        $Attributes[$vv[0]] = array_merge($Attributes[$vv[0]], [$vv[1]]);
    +                    } else {
    +                        $Attributes[$vv[0]] = $vv[1];
    +                    }
    +                }
    +            // `{foo}`
    +            } else {
    +                if ($v === 'class' && isset($Attributes[$v])) {
    +                    continue;
    +                }
    +                $Attributes[$v] = $v;
    +            }
    +        }
    +        if (isset($Attributes['class'])) {
    +            $Attributes['class'] = implode(' ', array_unique((array) $Attributes['class']));
    +        }
    +        return $Attributes;
    +    }
    +
    +}
    diff --git a/module/common/model.php b/module/common/model.php
    index 9bda4e3b14..e1141b7368 100644
    --- a/module/common/model.php
    +++ b/module/common/model.php
    @@ -3276,11 +3276,13 @@ EOD;
             if(empty($markdown)) return false;
     
             global $app;
    -        $app->loadClass('michelf');
    +        $app->loadClass('parsedownextraplugin');
     
    -        $Michelf = new michelf;
    +        $Parsedown = new parsedownextraplugin;
     
    -        return $Michelf->parse($markdown);
    +        $Parsedown->voidElementSuffix = '>'; // HTML5
    +
    +        return $Parsedown->text($markdown);
         }
     }