* * Original Markdown * Copyright (c) 2004-2006 John Gruber * * * @package moodlecore * @copyright (c) 2004-2006 John Gruber */ /** MARKDOWN_VERSION = 1.0.1j */ define( 'MARKDOWN_VERSION', "1.0.1j" ); # Tue 4 Sep 2007 /** MARKDOWNEXTRA_VERSION = 1.1.6 */ define( 'MARKDOWNEXTRA_VERSION', "1.1.6" ); # Tue 4 Sep 2007 # # Global default settings: # /** Change to ">" for HTML output */ @define( 'MARKDOWN_EMPTY_ELEMENT_SUFFIX', " />"); /** Define the width of a tab for code blocks. */ @define( 'MARKDOWN_TAB_WIDTH', 4 ); /** Optional title attribute for footnote links and backlinks. */ @define( 'MARKDOWN_FN_LINK_TITLE', "" ); @define( 'MARKDOWN_FN_BACKLINK_TITLE', "" ); # Optional class attribute for footnote links and backlinks. @define( 'MARKDOWN_FN_LINK_CLASS', "" ); @define( 'MARKDOWN_FN_BACKLINK_CLASS', "" ); # # WordPress settings: # # Change to false to remove Markdown from posts and/or comments. @define( 'MARKDOWN_WP_POSTS', true ); @define( 'MARKDOWN_WP_COMMENTS', true ); ### Standard Function Interface ### @define( 'MARKDOWN_PARSER_CLASS', 'MarkdownExtra_Parser' ); function Markdown($text) { # # Initialize the parser and return the result of its transform method. # # Setup static parser variable. static $parser; if (!isset($parser)) { $parser_class = MARKDOWN_PARSER_CLASS; $parser = new $parser_class; } # Transform text using parser. return $parser->transform($text); } ### WordPress Plugin Interface ### /* Plugin Name: Markdown Extra Plugin URI: http://www.michelf.com/projects/php-markdown/ Description: Markdown syntax allows you to write using an easy-to-read, easy-to-write plain text format. Based on the original Perl version by John Gruber. More... Version: 1.1.6 Author: Michel Fortin Author URI: http://www.michelf.com/ */ if (isset($wp_version)) { # More details about how it works here: # # Post content and excerpts # - Remove WordPress paragraph generator. # - Run Markdown on excerpt, then remove all tags. # - Add paragraph tag around the excerpt, but remove it for the excerpt rss. if (MARKDOWN_WP_POSTS) { remove_filter('the_content', 'wpautop'); remove_filter('the_content_rss', 'wpautop'); remove_filter('the_excerpt', 'wpautop'); add_filter('the_content', 'Markdown', 6); add_filter('the_content_rss', 'Markdown', 6); add_filter('get_the_excerpt', 'Markdown', 6); add_filter('get_the_excerpt', 'trim', 7); add_filter('the_excerpt', 'mdwp_add_p'); add_filter('the_excerpt_rss', 'mdwp_strip_p'); remove_filter('content_save_pre', 'balanceTags', 50); remove_filter('excerpt_save_pre', 'balanceTags', 50); add_filter('the_content', 'balanceTags', 50); add_filter('get_the_excerpt', 'balanceTags', 9); } # Comments # - Remove WordPress paragraph generator. # - Remove WordPress auto-link generator. # - Scramble important tags before passing them to the kses filter. # - Run Markdown on excerpt then remove paragraph tags. if (MARKDOWN_WP_COMMENTS) { remove_filter('comment_text', 'wpautop', 30); remove_filter('comment_text', 'make_clickable'); add_filter('pre_comment_content', 'Markdown', 6); add_filter('pre_comment_content', 'mdwp_hide_tags', 8); add_filter('pre_comment_content', 'mdwp_show_tags', 12); add_filter('get_comment_text', 'Markdown', 6); add_filter('get_comment_excerpt', 'Markdown', 6); add_filter('get_comment_excerpt', 'mdwp_strip_p', 7); global $mdwp_hidden_tags, $mdwp_placeholders; $mdwp_hidden_tags = explode(' ', '

 
  • '); $mdwp_placeholders = explode(' ', str_rot13( 'pEj07ZbbBZ U1kqgh4w4p pre2zmeN6K QTi31t9pre ol0MP1jzJR '. 'ML5IjmbRol ulANi1NsGY J7zRLJqPul liA8ctl16T K9nhooUHli')); } function mdwp_add_p($text) { if (!preg_match('{^$|^<(p|ul|ol|dl|pre|blockquote)>}i', $text)) { $text = '

    '.$text.'

    '; $text = preg_replace('{\n{2,}}', "

    \n\n

    ", $text); } return $text; } function mdwp_strip_p($t) { return preg_replace('{}i', '', $t); } function mdwp_hide_tags($text) { global $mdwp_hidden_tags, $mdwp_placeholders; return str_replace($mdwp_hidden_tags, $mdwp_placeholders, $text); } function mdwp_show_tags($text) { global $mdwp_hidden_tags, $mdwp_placeholders; return str_replace($mdwp_placeholders, $mdwp_hidden_tags, $text); } } ### bBlog Plugin Info ### function identify_modifier_markdown() { return array( 'name' => 'markdown', 'type' => 'modifier', 'nicename' => 'PHP Markdown Extra', 'description' => 'A text-to-HTML conversion tool for web writers', 'authors' => 'Michel Fortin and John Gruber', 'licence' => 'GPL', 'version' => MARKDOWNEXTRA_VERSION, 'help' => 'Markdown syntax allows you to write using an easy-to-read, easy-to-write plain text format. Based on the original Perl version by John Gruber. More...', ); } ### Smarty Modifier Interface ### function smarty_modifier_markdown($text) { return Markdown($text); } ### Textile Compatibility Mode ### # Rename this file to "classTextile.php" and it can replace Textile everywhere. if (strcasecmp(substr(__FILE__, -16), "classTextile.php") == 0) { # Try to include PHP SmartyPants. Should be in the same directory. @include_once 'smartypants.php'; # Fake Textile class. It calls Markdown instead. /** * @package moodlecore * @copyright (c) 2004-2006 John Gruber */ class Textile { function TextileThis($text, $lite='', $encode='') { if ($lite == '' && $encode == '') $text = Markdown($text); if (function_exists('SmartyPants')) $text = SmartyPants($text); return $text; } # Fake restricted version: restrictions are not supported for now. function TextileRestricted($text, $lite='', $noimage='') { return $this->TextileThis($text, $lite); } # Workaround to ensure compatibility with TextPattern 4.0.3. function blockLite($text) { return $text; } } } /** * Markdown Parser Class * * @package moodlecore * @copyright (c) 2004-2006 John Gruber */ class Markdown_Parser { # Regex to match balanced [brackets]. # Needed to insert a maximum bracked depth while converting to PHP. var $nested_brackets_depth = 6; var $nested_brackets; var $nested_url_parenthesis_depth = 4; var $nested_url_parenthesis; # Table of hash values for escaped characters: var $escape_chars = '\`*_{}[]()>#+-.!'; # Change to ">" for HTML output. var $empty_element_suffix = MARKDOWN_EMPTY_ELEMENT_SUFFIX; var $tab_width = MARKDOWN_TAB_WIDTH; # Change to `true` to disallow markup or entities. var $no_markup = false; var $no_entities = false; function Markdown_Parser() { # # Constructor function. Initialize appropriate member variables. # $this->_initDetab(); $this->nested_brackets = str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth). str_repeat('\])*', $this->nested_brackets_depth); $this->nested_url_parenthesis = str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth). str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth); # 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. var $urls = array(); var $titles = array(); var $html_hashes = array(); # Status flag to avoid invalid nesting. var $in_anchor = false; function transform($text) { # # Main function. The order in which other subs are called here is # essential. Link and image substitutions need to happen before # _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the # and tags get encoded. # # Clear the global hashes. If we don't clear these, you get conflicts # from other articles when generating a page which contains more than # one article (e.g. an index page that shows the N most recent # articles): $this->urls = array(); $this->titles = array(); $this->html_hashes = array(); # 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); } return $text . "\n"; } var $document_gamut = array( # Strip link definitions, store in hashes. "stripLinkDefinitions" => 20, "runBasicBlockGamut" => 30, ); function stripLinkDefinitions($text) { # # Strips link definitions from text, stores the URLs and titles in # hash references. # $less_than_tab = $this->tab_width - 1; # Link defs are in the form: ^[id]: url "optional title" $text = preg_replace_callback('{ ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 [ ]* \n? # maybe *one* newline [ ]* ? # url = $2 [ ]* \n? # maybe one newline [ ]* (?: (?<=\s) # lookbehind for whitespace ["(] (.*?) # title = $3 [")] [ ]* )? # title is optional (?:\n+|\Z) }xm', array(&$this, '_stripLinkDefinitions_callback'), $text); return $text; } function _stripLinkDefinitions_callback($matches) { $link_id = strtolower($matches[1]); $this->urls[$link_id] = $this->encodeAmpsAndAngles($matches[2]); if (isset($matches[3])) $this->titles[$link_id] = str_replace('"', '"', $matches[3]); return ''; # String that will replace the block } 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 = 'ins|del'; $block_tags_b = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'. 'script|noscript|form|fieldset|iframe|math'; # 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\n) # Starting after a blank line | # or \A\n? # the beginning of the doc ) ( # save in $1 # Match from `\n` to `\n`, handling nested tags # in between. [ ]{0,'.$less_than_tab.'} <('.$block_tags_b.')# 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.')# 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 \b # word break ([^<>])*? # /?> # 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; } function _hashHTMLBlocks_callback($matches) { $text = $matches[1]; $key = $this->hashBlock($text); return "\n\n$key\n\n"; } function hashPart($text, $boundary = 'X') { # # Called whenever a tag must be hashed when a function insert an atomic # element in the text stream. Passing $text to through this function gives # a unique text-token which will be reverted back when calling unhash. # # The $boundary argument specify what character should be used to surround # the token. By convension, "B" is used for block elements that needs not # to be wrapped into paragraph tags at the end, ":" is used for elements # that are word separators and "S" is used for general span-level elements. # # 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. } function hashBlock($text) { # # Shortcut function for hashPart with block-level boundaries. # return $this->hashPart($text, 'B'); } var $block_gamut = array( # # These are all the transformations that form block-level # tags like paragraphs, headers, and list items. # "doHeaders" => 10, "doHorizontalRules" => 20, "doLists" => 40, "doCodeBlocks" => 50, "doBlockQuotes" => 60, ); function runBlockGamut($text) { # # Run block gamut tranformations. # # We need to escape raw HTML in Markdown source before doing anything # else. This need to be done for each block, and not only at the # begining in the Markdown function since hashed blocks can be part of # list items and could have been indented. Indented blocks would have # been seen as a code block in a previous pass of hashHTMLBlocks. $text = $this->hashHTMLBlocks($text); return $this->runBasicBlockGamut($text); } function runBasicBlockGamut($text) { # # Run block gamut tranformations, without hashing HTML blocks. This is # useful when HTML blocks are known to be already hashed, like in the first # whole-document pass. # foreach ($this->block_gamut as $method => $priority) { $text = $this->$method($text); } # Finally form paragraph and restore hashed blocks. $text = $this->formParagraphs($text); return $text; } function doHorizontalRules($text) { # Do Horizontal Rules: return preg_replace( '{ ^[ ]{0,3} # Leading space ([*-_]) # $1: First marker (?> # Repeated marker group [ ]{0,2} # Zero, one, or two spaces. \1 # Marker character ){2,} # Group repeated at least twice [ ]* # Tailing spaces $ # End of line. }mx', "\n".$this->hashBlock("empty_element_suffix")."\n", $text); } var $span_gamut = array( # # These are all the transformations that occur *within* block-level # tags like paragraphs, headers, and list items. # # Process character escapes, code spans, and inline HTML # in one shot. "parseSpan" => -30, # Process anchor and image tags. Images must come first, # because ![foo][f] looks like an anchor. "doImages" => 10, "doAnchors" => 20, # Make links out of things like `` # Must come after doAnchors, because you can use < and > # delimiters in inline links like [this](). "doAutoLinks" => 30, "encodeAmpsAndAngles" => 40, "doItalicsAndBold" => 50, "doHardBreaks" => 60, ); function runSpanGamut($text) { # # Run span gamut tranformations. # foreach ($this->span_gamut as $method => $priority) { $text = $this->$method($text); } return $text; } function doHardBreaks($text) { # Do hard breaks: return preg_replace_callback('/ {2,}\n/', array(&$this, '_doHardBreaks_callback'), $text); } function _doHardBreaks_callback($matches) { return $this->hashPart("empty_element_suffix\n"); } function doAnchors($text) { # # Turn Markdown link shortcuts into XHTML
    tags. # if ($this->in_anchor) return $text; $this->in_anchor = true; # # First, handle reference-style links: [link text] [id] # $text = preg_replace_callback('{ ( # wrap whole match in $1 \[ ('.$this->nested_brackets.') # 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.') # link text = $2 \] \( # literal paren [ ]* (?: <(\S*)> # href = $3 | ('.$this->nested_url_parenthesis.') # href = $4 ) [ ]* ( # $5 ([\'"]) # quote char = $6 (.*?) # Title = $7 \6 # matching quote [ ]* # 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 test][1] # or [link test](/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; } 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->encodeAmpsAndAngles($url); $result = "titles[$link_id] ) ) { $title = $this->titles[$link_id]; $title = $this->encodeAmpsAndAngles($title); $result .= " title=\"$title\""; } $link_text = $this->runSpanGamut($link_text); $result .= ">$link_text"; $result = $this->hashPart($result); } else { $result = $whole_match; } return $result; } function _doAnchors_inline_callback($matches) { $whole_match = $matches[1]; $link_text = $this->runSpanGamut($matches[2]); $url = $matches[3] == '' ? $matches[4] : $matches[3]; $title =& $matches[7]; $url = $this->encodeAmpsAndAngles($url); $result = "encodeAmpsAndAngles($title); $result .= " title=\"$title\""; } $link_text = $this->runSpanGamut($link_text); $result .= ">$link_text"; return $this->hashPart($result); } function doImages($text) { # # Turn Markdown image shortcuts into tags. # # # First, handle reference-style labeled images: ![alt text][id] # $text = preg_replace_callback('{ ( # wrap whole match in $1 !\[ ('.$this->nested_brackets.') # 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.') # alt text = $2 \] \s? # One optional whitespace character \( # literal paren [ ]* (?: <(\S*)> # src url = $3 | ('.$this->nested_url_parenthesis.') # src url = $4 ) [ ]* ( # $5 ([\'"]) # quote char = $6 (.*?) # title = $7 \6 # matching quote [ ]* )? # title is optional \) ) }xs', array(&$this, '_doImages_inline_callback'), $text); return $text; } 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 = str_replace('"', '"', $alt_text); if (isset($this->urls[$link_id])) { $url = $this->urls[$link_id]; $result = "\"$alt_text\"";titles[$link_id])) { $title = $this->titles[$link_id]; $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; } 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 = str_replace('"', '"', $alt_text); $result = "\"$alt_text\"";empty_element_suffix; return $this->hashPart($result); } 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; } function _doHeaders_callback_setext($matches) { $level = $matches[2]{0} == '=' ? 1 : 2; $block = "".$this->runSpanGamut($matches[1]).""; return "\n" . $this->hashBlock($block) . "\n\n"; } function _doHeaders_callback_atx($matches) { $level = strlen($matches[1]); $block = "".$this->runSpanGamut($matches[2]).""; return "\n" . $this->hashBlock($block) . "\n\n"; } function doLists($text) { # # Form HTML ordered (numbered) and unordered (bulleted) lists. # $less_than_tab = $this->tab_width - 1; # Re-usable patterns to match list item bullets and number markers: $marker_ul = '[*+-]'; $marker_ol = '\d+[.]'; $marker_any = "(?:$marker_ul|$marker_ol)"; $markers = array($marker_ul, $marker_ol); foreach ($markers as $marker) { # Re-usable pattern to match any entirel ul or ol list: $whole_list = ' ( # $1 = whole list ( # $2 [ ]{0,'.$less_than_tab.'} ('.$marker.') # $3 = first list item marker [ ]+ ) (?s:.+?) ( # $4 \z | \n{2,} (?=\S) (?! # Negative lookahead for another list item marker [ ]* '.$marker.'[ ]+ ) ) ) '; // 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.' }mx', array(&$this, '_doLists_callback'), $text); } else { $text = preg_replace_callback('{ (?:(?<=\n)\n|\A\n?) # Must eat the newline '.$whole_list.' }mx', array(&$this, '_doLists_callback'), $text); } } return $text; } function _doLists_callback($matches) { # Re-usable patterns to match list item bullets and number markers: $marker_ul = '[*+-]'; $marker_ol = '\d+[.]'; $marker_any = "(?:$marker_ul|$marker_ol)"; $list = $matches[1]; $list_type = preg_match("/$marker_ul/", $matches[3]) ? "ul" : "ol"; $marker_any = ( $list_type == "ul" ? $marker_ul : $marker_ol ); $list .= "\n"; $result = $this->processListItems($list, $marker_any); $result = $this->hashBlock("<$list_type>\n" . $result . ""); return "\n". $result ."\n\n"; } var $list_level = 0; function processListItems($list_str, $marker_any) { # # Process the contents of a single ordered or unordered list, splitting it # into individual list items. # # The $this->list_level global keeps track of when we're inside a list. # Each time we enter a list, we increment it; when we leave a list, # we decrement. If it's zero, we're not in a list anymore. # # We do this because when we're not inside a list, we want to treat # something like this: # # I recommend upgrading to version # 8. Oops, now this line is treated # as a sub-list. # # As a single paragraph, despite the fact that the second line starts # with a digit-period-space sequence. # # Whereas when we're inside a list (or sub-list), that line will be # treated as the start of a sub-list. What a kludge, huh? This is # an aspect of Markdown's syntax that's hard to parse perfectly # without resorting to mind-reading. Perhaps the solution is to # change the syntax rules such that sub-lists must start with a # starting cardinal number; e.g. "1." or "a.". $this->list_level++; # trim trailing blank lines: $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); $list_str = preg_replace_callback('{ (\n)? # leading line = $1 (^[ ]*) # leading whitespace = $2 ('.$marker_any.') [ ]+ # list marker = $3 ((?s:.+?)) # list item text = $4 (?:(\n+(?=\n))|\n) # tailing blank line = $5 (?= \n* (\z | \2 ('.$marker_any.') [ ]+)) }xm', array(&$this, '_processListItems_callback'), $list_str); $this->list_level--; return $list_str; } function _processListItems_callback($matches) { $item = $matches[4]; $leading_line =& $matches[1]; $leading_space =& $matches[2]; $tailing_blank_line =& $matches[5]; if ($leading_line || $tailing_blank_line || preg_match('/\n{2,}/', $item)) { $item = $this->runBlockGamut($this->outdent($item)."\n"); } else { # Recursion for sub-lists: $item = $this->doLists($this->outdent($item)); $item = preg_replace('/\n+$/', '', $item); $item = $this->runSpanGamut($item); } return "
  • " . $item . "
  • \n"; } function doCodeBlocks($text) { # # Process Markdown `
    ` blocks.
    	#
    		$text = preg_replace_callback('{
    				(?:\n\n|\A)
    				(	            # $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;
    	}
    	function _doCodeBlocks_callback($matches) {
    		$codeblock = $matches[1];
    
    		$codeblock = $this->outdent($codeblock);
    		$codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
    
    		# trim leading newlines and trailing newlines
    		$codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock);
    
    		$codeblock = "
    $codeblock\n
    "; return "\n\n".$this->hashBlock($codeblock)."\n\n"; } function makeCodeSpan($code) { # # Create a code span markup for $code. Called from handleSpanToken. # $code = htmlspecialchars(trim($code), ENT_NOQUOTES); return $this->hashPart("$code"); } function doItalicsAndBold($text) { # must go first: $text = preg_replace_callback('{ ( # $1: Marker (? [^*_]+? # Anthing not em markers. | # Balence any regular emphasis inside. \1 (?=\S) .+? (?<=\S) \1 | . # Allow unbalenced * and _. )+? ) (?<=\S) \1\1 # End mark not preceded by whitespace. }sx', array(&$this, '_doItalicAndBold_strong_callback'), $text); # Then : $text = preg_replace_callback( '{ ( (?runSpanGamut($text); return $this->hashPart("$text"); } function _doItalicAndBold_strong_callback($matches) { $text = $matches[2]; $text = $this->runSpanGamut($text); return $this->hashPart("$text"); } 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; } 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"; } function _doBlockQuotes_callback2($matches) { $pre = $matches[1]; $pre = preg_replace('/^ /m', '', $pre); return $pre; } function formParagraphs($text) { # # Params: # $text - string to process with html

    tags # # Strip leading and trailing lines: $text = preg_replace('/\A\n+|\n+\z/', '', $text); $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); # # Wrap

    tags and unhashify HTML blocks # foreach ($grafs as $key => $value) { if (!preg_match('/^B\x1A[0-9]+B$/', $value)) { # Is a paragraph. $value = $this->runSpanGamut($value); $value = preg_replace('/^([ ]*)/', "

    ", $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); } function encodeAmpsAndAngles($text) { # Smart processing for ampersands and angle brackets that need to be encoded. if ($this->no_entities) { $text = str_replace('&', '&', $text); $text = str_replace('<', '<', $text); return $text; } # Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: # http://bumppo.net/projects/amputator/ $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', '&', $text);; # Encode naked <'s $text = preg_replace('{<(?![a-z/?\$!%])}i', '<', $text); return $text; } function doAutoLinks($text) { $text = preg_replace_callback('{<((https?|ftp|dict):[^\'">\s]+)>}', 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]+ ) > }xi', array(&$this, '_doAutoLinks_email_callback'), $text); return $text; } function _doAutoLinks_url_callback($matches) { $url = $this->encodeAmpsAndAngles($matches[1]); $link = "$url"; return $this->hashPart($link); } function _doAutoLinks_email_callback($matches) { $address = $matches[1]; $link = $this->encodeEmailAddress($address); return $this->hashPart($link); } function encodeEmailAddress($addr) { # # Input: an email address, e.g. "foo@example.com" # # Output: the email address as a mailto link, with each character # of the address encoded as either a decimal or hex entity, in # the hopes of foiling most address harvesting spam bots. E.g.: # #

    foo@exampl # e.com

    # # Based by a filter by Matthew Wickline, posted to BBEdit-Talk. # With some optimizations by Milian Wolff. # $addr = "mailto:" . $addr; $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. if ($r > 90 && $char != '@') /* do nothing */; else if ($r < 45) $chars[$key] = '&#x'.dechex($ord).';'; else $chars[$key] = '&#'.$ord.';'; } } $addr = implode('', $chars); $text = implode('', array_slice($chars, 7)); # text without `mailto:` $addr = "$text"; return $addr; } function parseSpan($str) { # # Take the string $str and parse it into tokens, hashing embeded HTML, # escaped characters and handling code spans. # $output = ''; $regex = '{ ( \\\\['.preg_quote($this->escape_chars).'] | (?no_markup ? '' : ' | # comment | <\?.*?\?> | <%.*?%> # processing instruction | <[/!$]?[-a-zA-Z0-9:]+ # regular tags (?> \s (?>[^"\'>]+|"[^"]*"|\'[^\']*\')* )? > ').' ) }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($regex, $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; } function handleSpanToken($token, &$str) { # # Handle $token provided by parseSpan by determining its nature and # returning the corresponding value that should replace it. # switch ($token{0}) { case "\\": return $this->hashPart("&#". ord($token{1}). ";"); case "`": # Search for end marker in remaining text. if (preg_match('/^(.*?[^`])'.$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); } } function outdent($text) { # # Remove one level of line-leading tabs or spaces # return preg_replace('/^(\t|[ ]{1,'.$this->tab_width.'})/m', '', $text); } # String length function for detab. `_initDetab` will create a function to # hanlde UTF-8 if the default function does not exist. var $utf8_strlen = 'mb_strlen'; function detab($text) { # # Replace tabs with the appropriate amount of space. # # For each line we separate the line in blocks delemited by # tab characters. Then we reconstruct every line by adding the # appropriate number of space between each blocks. $text = preg_replace_callback('/^.*\t.*$/m', array(&$this, '_detab_callback'), $text); return $text; } 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; } function _initDetab() { # # Check for the availability of the function in the `utf8_strlen` property # (initially `mb_strlen`). If the function is not available, create a # function that will loosely count the number of UTF-8 characters with a # regular expression. # if (function_exists($this->utf8_strlen)) return; $this->utf8_strlen = create_function('$text', 'return preg_match_all( "/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/", $text, $m);'); } function unhash($text) { # # Swap back in all the tags hashed by _HashHTMLBlocks. # return preg_replace_callback('/(.)\x1A[0-9]+\1/', array(&$this, '_unhash_callback'), $text); } function _unhash_callback($matches) { return $this->html_hashes[$matches[0]]; } } /** * Markdown Extra Parser Class * * @package moodlecore * @copyright (c) 2004-2006 John Gruber */ class MarkdownExtra_Parser extends Markdown_Parser { # Prefix for footnote ids. var $fn_id_prefix = ""; # Optional title attribute for footnote links and backlinks. var $fn_link_title = MARKDOWN_FN_LINK_TITLE; var $fn_backlink_title = MARKDOWN_FN_BACKLINK_TITLE; # Optional class attribute for footnote links and backlinks. var $fn_link_class = MARKDOWN_FN_LINK_CLASS; var $fn_backlink_class = MARKDOWN_FN_BACKLINK_CLASS; function MarkdownExtra_Parser() { # # Constructor function. Initialize the parser object. # # Add extra escapable characters before parent constructor # initialize the table. $this->escape_chars .= ':|'; # Insert extra document, block, and span transformations. # Parent constructor will do the sorting. $this->document_gamut += array( "stripFootnotes" => 15, "stripAbbreviations" => 25, "appendFootnotes" => 50, ); $this->block_gamut += array( "doTables" => 15, "doDefLists" => 45, ); $this->span_gamut += array( "doFootnotes" => 5, "doAbbreviations" => 70, ); parent::Markdown_Parser(); } # Extra hashes used during extra transformations. var $footnotes = array(); var $footnotes_ordered = array(); var $abbr_desciptions = array(); var $abbr_matches = array(); # Status flag to avoid invalid nesting. var $in_footnote = false; function transform($text) { # # Added clear to the new $html_hashes, reordered `hashHTMLBlocks` before # blank line stripping and added extra parameter to `runBlockGamut`. # # Clear the global hashes. If we don't clear these, you get conflicts # from other articles when generating a page which contains more than # one article (e.g. an index page that shows the N most recent # articles): $this->footnotes = array(); $this->footnotes_ordered = array(); $this->abbr_desciptions = array(); $this->abbr_matches = array(); return parent::transform($text); } ### HTML Block Parser ### # Tags that are always treated as block tags: var $block_tags = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend'; # Tags treated as block tags only if the opening tag is alone on it's line: var $context_block_tags = 'script|noscript|math|ins|del'; # Tags where markdown="1" default to span mode: var $contain_span_tags = 'p|h[1-6]|li|dd|dt|td|th|legend|address'; # Tags which must not have their contents modified, no matter where # they appear: var $clean_tags = 'script|math'; # Tags that do not need to be closed. var $auto_close_tags = 'hr|img'; function hashHTMLBlocks($text) { # # Hashify HTML Blocks and "clean tags". # # We only want to do this for block-level HTML tags, such as headers, # lists, and tables. That's because we still want to wrap

    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 whitin 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! # # # Call the HTML-in-Markdown hasher. # list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text); return $text; } function _hashHTMLBlocks_inMarkdown($text, $indent = 0, $enclosing_tag = '', $span = false) { # # Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags. # # * $indent is the number of space to be ignored when checking for code # blocks. This is important because if we don't take the indent into # account, something like this (which looks right) won't work as expected: # #

    #
    # 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 is not empty, stops at the first unmatched closing # tag with that name. Nested tags supported. # # * If $span is true, text inside must treated as span. So any double # newline will be replaced by a single newline so that it does not create # paragraphs. # # Returns an array of that form: ( processed text , remaining text ) # if ($text === '') return array('', ''); # Regex to check for the presense of newlines around a block tag. $newline_match_before = '/(?:^\n?|\n\n)*$/'; $newline_match_after = '{ ^ # Start of text following the tag. (?:[ ]*)? # Optional comment. [ ]*\n # Must be followed by newline. }xs'; # Regex to match any tag. $block_tag_match = '{ ( # $2: Capture hole tag. block_tags.' | '.$this->context_block_tags.' | '.$this->clean_tags.' | (?!\s)'.$enclosing_tag.' ) \s* # Whitespace. (?> ".*?" | # Double quotes (can contain `>`) \'.*?\' | # Single quotes (can contain `>`) .+? # Anything but quotes and `>`. )*? > # End of tag. | # HTML Comment | <\?.*?\?> | <%.*?%> # Processing instruction | # CData Block ) }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_match, $text, 2, PREG_SPLIT_DELIM_CAPTURE); # If in Markdown span mode, add a empty-string span-level hash # after each newline to prevent triggering any block element. if ($span) { $void = $this->hashPart("", ':'); $newline = "$void\n"; $parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void; } $parsed .= $parts[0]; # Text before current tag. # If end of $text has been reached. Stop loop. if (count($parts) < 3) { $text = ""; break; } $tag = $parts[1]; # Tag to handle. $text = $parts[2]; # Remaining text after current tag. # # Check for: Tag inside code block or span # if (# Find current paragraph preg_match('/(?>^\n?|\n\n)((?>.+\n?)*?)$/', $parsed, $matches) && ( # Then match in it either a code block... preg_match('/^ {'.($indent+4).'}.*(?>\n {'.($indent+4).'}.*)*'. '(?!\n)$/', $matches[1], $x) || # ...or unbalenced code span markers. (the regex matches balenced) !preg_match('/^(?>[^`]+|(`+)(?>[^`]+|(?!\1[^`])`)*?\1(?!`))*$/s', $matches[1]) )) { # Tag is in code block or span and may not be a tag at all. So we # simply skip the first char (should be a `<`). $parsed .= $tag{0}; $text = substr($tag, 1) . $text; # Put back $tag minus first char. } # # Check for: Opening Block level tag or # Opening Content Block tag (like ins and del) # used as a block tag (tag is alone on it's line). # else if (preg_match("{^<(?:$this->block_tags)\b}", $tag) || ( preg_match("{^<(?:$this->context_block_tags)\b}", $tag) && preg_match($newline_match_before, $parsed) && preg_match($newline_match_after, $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)\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 !== '' && # Same name as enclosing tag. preg_match("{^= 0); return array($parsed, $text); } function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) { # # Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags. # # * Calls $hash_method to convert any blocks. # * Stops when the first opening tag closes. # * $md_attr indicate if the use of the `markdown="1"` attribute is allowed. # (it is not inside clean tags) # # Returns an array of that form: ( processed text , remaining text ) # if ($text === '') return array('', ''); # Regex to match `markdown` attribute inside of a tag. $markdown_attr_match = ' { \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_match = '{ ( # $2: Capture hole 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. # # Get the name of the starting tag. # if (preg_match("/^<([\w:$]*)\b/", $text, $matches)) $base_tag_name = $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_match, $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)\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("{^mode = $attr_m[2] . $attr_m[3]; $span_mode = $this->mode == 'span' || $this->mode != 'block' && preg_match("{^<(?:$this->contain_span_tags)\b}", $tag); # Calculate indent before tag. preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches); $indent = strlen($matches[1]); # End preceding block with this tag. $block_text .= $tag; $parsed .= $this->$hash_method($block_text); # Get enclosing tag name for the ParseMarkdown function. preg_match('/^<([\w:$]*)\b/', $tag, $matches); $tag_name = $matches[1]; # Parse the content using the HTML-in-Markdown parser. list ($block_text, $text) = $this->_hashHTMLBlocks_inMarkdown($text, $indent, $tag_name, $span_mode); # Outdent markdown text. if ($indent > 0) { $block_text = preg_replace("/^[ ]{1,$indent}/m", "", $block_text); } # Append tag content to parsed text. if (!$span_mode) $parsed .= "\n\n$block_text\n\n"; else $parsed .= "$block_text"; # Start over 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); } function hashClean($text) { # # Called whenever a tag must be hashed when a function insert a "clean" tag # in $text, it pass through this function and is automaticaly escaped, # blocking invalid nested overlap. # return $this->hashPart($text, 'C'); } function doHeaders($text) { # # Redefined to add id attribute support. # # Setext-style headers: # Header 1 {#header1} # ======== # # Header 2 {#header2} # -------- # $text = preg_replace_callback( '{ (^.+?) # $1: Header text (?:[ ]+\{\#([-_:a-zA-Z0-9]+)\})? # $2: Id attribute [ ]*\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} # ... # ###### Header 6 {#header2} # $text = preg_replace_callback('{ ^(\#{1,6}) # $1 = string of #\'s [ ]* (.+?) # $2 = Header text [ ]* \#* # optional closing #\'s (not counted) (?:[ ]+\{\#([-_:a-zA-Z0-9]+)\})? # id attribute [ ]* \n+ }xm', array(&$this, '_doHeaders_callback_atx'), $text); return $text; } function _doHeaders_attr($attr) { if (empty($attr)) return ""; return " id=\"$attr\""; } function _doHeaders_callback_setext($matches) { $level = $matches[3]{0} == '=' ? 1 : 2; $attr = $this->_doHeaders_attr($id =& $matches[2]); $block = "".$this->runSpanGamut($matches[1]).""; return "\n" . $this->hashBlock($block) . "\n\n"; } function _doHeaders_callback_atx($matches) { $level = strlen($matches[1]); $attr = $this->_doHeaders_attr($id =& $matches[3]); $block = "".$this->runSpanGamut($matches[2]).""; return "\n" . $this->hashBlock($block) . "\n\n"; } function doTables($text) { # # Form HTML tables. # $less_than_tab = $this->tab_width - 1; # # Find tables with leading pipe. # # | Header 1 | Header 2 # | -------- | -------- # | Cell 1 | Cell 2 # | Cell 3 | Cell 4 # $text = preg_replace_callback(' { ^ # Start of a line [ ]{0,'.$less_than_tab.'} # Allowed whitespace. [|] # Optional leading pipe (present) (.+) \n # $1: Header row (at least one pipe) [ ]{0,'.$less_than_tab.'} # Allowed whitespace. [|] ([ ]*[-:]+[-| :]*) \n # $2: Header underline ( # $3: Cells (?> [ ]* # Allowed whitespace. [|] .* \n # Row content. )* ) (?=\n|\Z) # Stop at final double newline. }xm', array(&$this, '_doTable_leadingPipe_callback'), $text); # # Find tables without leading pipe. # # Header 1 | Header 2 # -------- | -------- # Cell 1 | Cell 2 # Cell 3 | Cell 4 # $text = preg_replace_callback(' { ^ # Start of a line [ ]{0,'.$less_than_tab.'} # Allowed whitespace. (\S.*[|].*) \n # $1: Header row (at least one pipe) [ ]{0,'.$less_than_tab.'} # Allowed whitespace. ([-:]+[ ]*[|][-| :]*) \n # $2: Header underline ( # $3: Cells (?> .* [|] .* \n # Row content )* ) (?=\n|\Z) # Stop at final double newline. }xm', array(&$this, '_DoTable_callback'), $text); return $text; } function _doTable_leadingPipe_callback($matches) { $head = $matches[1]; $underline = $matches[2]; $content = $matches[3]; # Remove leading pipe for each row. $content = preg_replace('/^ *[|]/m', '', $content); return $this->_doTable_callback(array($matches[0], $head, $underline, $content)); } function _doTable_callback($matches) { $head = $matches[1]; $underline = $matches[2]; $content = $matches[3]; # Remove any tailing pipes for each line. $head = preg_replace('/[|] *$/m', '', $head); $underline = preg_replace('/[|] *$/m', '', $underline); $content = preg_replace('/[|] *$/m', '', $content); # Reading alignement from header underline. $separators = preg_split('/ *[|] */', $underline); foreach ($separators as $n => $s) { if (preg_match('/^ *-+: *$/', $s)) $attr[$n] = ' align="right"'; else if (preg_match('/^ *:-+: *$/', $s))$attr[$n] = ' align="center"'; else if (preg_match('/^ *:-+ *$/', $s)) $attr[$n] = ' align="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); # 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"; } function doDefLists($text) { # # Form HTML definition lists. # $less_than_tab = $this->tab_width - 1; # Re-usable pattern to match any entire dl list: $whole_list = '(?> ( # $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('{ (?:(?<=\n\n)|\A\n?) '.$whole_list.' }mx', array(&$this, '_doDefLists_callback'), $text); return $text; } 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"; } function processDefListItems($list_str) { # # Process the contents of a single definition list, splitting it # into individual term and definition list items. # $less_than_tab = $this->tab_width - 1; # trim trailing blank lines: $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); # Process definition terms. $list_str = preg_replace_callback('{ (?:\n\n+|\A\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 [ ]{0,'.$less_than_tab.'} # whitespace before colon [:][ ]+ # definition mark (colon) ((?s:.+?)) # definition text = $2 (?= \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; } 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"; } function _processDefListItems_callback_dd($matches) { $leading_line = $matches[1]; $def = $matches[2]; if ($leading_line || preg_match('/\n{2,}/', $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"; } function doItalicsAndBold($text) { # # Redefined to change emphasis by underscore behaviour so that it does not # work in the middle of a word. # # must go first: $text = preg_replace_callback(array( '{ ( # $1: Marker (? [^_]+? # Anthing not em markers. | # Balence any regular _ emphasis inside. (? [^*]+? # Anthing not em markers. | # Balence any regular * emphasis inside. \* (?=\S) (.+?) (?<=\S) \* | \* # Allow unbalenced as last resort. )+? ) (?<=\S) \*\* # End mark not preceded by whitespace. }sx', ), array(&$this, '_doItalicAndBold_strong_callback'), $text); # Then : $text = preg_replace_callback(array( '{ ( (? tags # # Strip leading and trailing lines: $text = preg_replace('/\A\n+|\n+\z/', '', $text); $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); # # Wrap

    tags and unhashify HTML blocks # foreach ($grafs as $key => $value) { $value = trim($this->runSpanGamut($value)); # Check if this should be enclosed in a paragraph. # Clean tag hashes & block tag hashes are left alone. $is_p = !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value); if ($is_p) { $value = "

    $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 function stripFootnotes($text) { # # Strips link definitions from text, stores the URLs and titles in # hash references. # $less_than_tab = $this->tab_width - 1; # Link defs are in the form: [^id]: url "optional title" $text = preg_replace_callback('{ ^[ ]{0,'.$less_than_tab.'}\[\^(.+?)\][ ]?: # note_id = $1 [ ]* \n? # maybe *one* newline ( # text = $2 (no blank lines allowed) (?: .+ # actual text | \n # newlines but (?!\[\^.+?\]:\s)# negative lookahead for footnote marker. (?!\n+[ ]{0,3}\S)# ensure line is not blank and followed # by non-indented content )* ) }xm', array(&$this, '_stripFootnotes_callback'), $text); return $text; } 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 } function doFootnotes($text) { # # Replace footnote references in $text [^id] with a special text-token # which will be can be # if (!$this->in_footnote && !$this->in_anchor) { $text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text); } return $text; } function appendFootnotes($text) { # # Append footnote list to text. # $text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', array(&$this, '_appendFootnotes_callback'), $text); if (!empty($this->footnotes_ordered)) { $text .= "\n\n"; $text .= "
    \n"; $text .= "fn_backlink_class != "") { $class = $this->fn_backlink_class; $class = $this->encodeAmpsAndAngles($class); $class = str_replace('"', '"', $class); $attr .= " class=\"$class\""; } if ($this->fn_backlink_title != "") { $title = $this->fn_backlink_title; $title = $this->encodeAmpsAndAngles($title); $title = str_replace('"', '"', $title); $attr .= " title=\"$title\""; } $num = 0; $this->in_footnote = true; foreach ($this->footnotes_ordered as $note_id => $footnote) { $footnote .= "\n"; # Need to append newline before parsing. $footnote = $this->runBlockGamut("$footnote\n"); $attr2 = str_replace("%%", ++$num, $attr); # Add backlink to last paragraph; create new paragraph if needed. $backlink = ""; if (preg_match('{

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

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

    $backlink

    "; } $text .= "
  • \n"; $text .= $footnote . "\n"; $text .= "
  • \n\n"; } $this->in_footnote = false; $text .= "\n"; $text .= "
    "; } return $text; } 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])) { # Transfert footnote content to the ordered list. $this->footnotes_ordered[$node_id] = $this->footnotes[$node_id]; unset($this->footnotes[$node_id]); $num = count($this->footnotes_ordered); $attr = " rel=\"footnote\""; if ($this->fn_link_class != "") { $class = $this->fn_link_class; $class = $this->encodeAmpsAndAngles($class); $class = str_replace('"', '"', $class); $attr .= " class=\"$class\""; } if ($this->fn_link_title != "") { $title = $this->fn_link_title; $title = $this->encodeAmpsAndAngles($title); $title = str_replace('"', '"', $title); $attr .= " title=\"$title\""; } $attr = str_replace("%%", $num, $attr); return "". "$num". ""; } return "[^".$matches[1]."]"; } ### Abbreviations ### function stripAbbreviations($text) { # # Strips abbreviations from text, stores titles in hash references. # $less_than_tab = $this->tab_width - 1; # Link defs are in the form: [id]*: url "optional title" $text = preg_replace_callback('{ ^[ ]{0,'.$less_than_tab.'}\*\[(.+?)\][ ]?: # abbr_id = $1 (.*) # text = $2 (no blank lines allowed) }xm', array(&$this, '_stripAbbreviations_callback'), $text); return $text; } function _stripAbbreviations_callback($matches) { $abbr_word = $matches[1]; $abbr_desc = $matches[2]; $this->abbr_matches[] = preg_quote($abbr_word); $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); return ''; # String that will replace the block } function doAbbreviations($text) { # # Find defined abbreviations in text and wrap them in elements. # if ($this->abbr_matches) { // cannot use the /x modifier because abbr_matches may // contain spaces: $text = preg_replace_callback('{'. '(?abbr_matches) .')'. '(?![\w\x1A])'. '}', array(&$this, '_doAbbreviations_callback'), $text); } return $text; } 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"); } else { $desc = htmlspecialchars($desc, ENT_NOQUOTES); return $this->hashPart("$abbr"); } } else { return $matches[0]; } } } /** * PHP Markdown Extra * ================== * * Description * ----------- * * This is a PHP port of the original Markdown formatter written in Perl * by John Gruber. This special "Extra" version of PHP Markdown features * further enhancements to the syntax for making additional constructs * such as tables and definition list. * * Markdown is a text-to-HTML filter; it translates an easy-to-read / * easy-to-write structured text format into HTML. Markdown's text format * is most similar to that of plain text email, and supports features such * as headers, *emphasis*, code blocks, blockquotes, and links. * * Markdown's syntax is designed not as a generic markup language, but * specifically to serve as a front-end to (X)HTML. You can use span-level * HTML tags anywhere in a Markdown document, and you can use block level * HTML tags (like
    and as well). * * For more information about Markdown's syntax, see: * * * * * Bugs * ---- * * To file bug reports please send email to: * * * * Please include with your report: (1) the example input; (2) the output you * expected; (3) the output Markdown actually produced. * * * Version History * --------------- * * See the readme file for detailed release notes for this version. * * * Copyright and License * --------------------- * * PHP Markdown & Extra * Copyright (c) 2004-2007 Michel Fortin * * All rights reserved. * * Based on Markdown * Copyright (c) 2003-2006 John Gruber * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * - Neither the name "Markdown" nor the names of its contributors may * be used to endorse or promote products derived from this software * without specific prior written permission. * * This software is provided by the copyright holders and contributors "as * is" and any express or implied warranties, including, but not limited * to, the implied warranties of merchantability and fitness for a * particular purpose are disclaimed. In no event shall the copyright owner * or contributors be liable for any direct, indirect, incidental, special, * exemplary, or consequential damages (including, but not limited to, * procurement of substitute goods or services; loss of use, data, or * profits; or business interruption) however caused and on any theory of * liability, whether in contract, strict liability, or tort (including * negligence or otherwise) arising in any way out of the use of this * software, even if advised of the possibility of such damage. */ ?>