", $text);
+ }
+ return $text;
+ }
+
+ function mdwp_strip_p($t) { return preg_replace('{?p>}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.
+ 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
+#
+
+class Markdown_Parser {
+
+ ### Configuration Variables ###
+
+ # 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;
+
+ # Predefined urls and titles for reference links and images.
+ var $predef_urls = array();
+ var $predef_titles = array();
+
+
+ ### Parser Implementation ###
+
+ # Regex to match balanced [brackets].
+ # Needed to insert a maximum bracked depth while converting to PHP.
+ var $nested_brackets_depth = 6;
+ var $nested_brackets_re;
+
+ var $nested_url_parenthesis_depth = 4;
+ var $nested_url_parenthesis_re;
+
+ # Table of hash values for escaped characters:
+ var $escape_chars = '\`*_{}[]()>#+-.!';
+ var $escape_chars_re;
+
+
+ function Markdown_Parser() {
+ #
+ # Constructor function. Initialize appropriate member variables.
+ #
+ $this->_initDetab();
+ $this->prepareItalicsAndBold();
+
+ $this->nested_brackets_re =
+ str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth).
+ str_repeat('\])*', $this->nested_brackets_depth);
+
+ $this->nested_url_parenthesis_re =
+ str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth).
+ str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth);
+
+ $this->escape_chars_re = '['.preg_quote($this->escape_chars).']';
+
+ # Sort document, block, and span gamut in ascendent priority order.
+ asort($this->document_gamut);
+ asort($this->block_gamut);
+ asort($this->span_gamut);
+ }
+
+
+ # Internal hashes used during transformation.
+ var $urls = array();
+ var $titles = array();
+ var $html_hashes = array();
+
+ # Status flag to avoid invalid nesting.
+ var $in_anchor = false;
+
+
+ function setup() {
+ #
+ # Called before the transformation process starts to setup parser
+ # states.
+ #
+ # Clear global hashes.
+ $this->urls = $this->predef_urls;
+ $this->titles = $this->predef_titles;
+ $this->html_hashes = array();
+
+ $this->in_anchor = false;
+ }
+
+ function teardown() {
+ #
+ # Called after the transformation process to clear any variable
+ # which may be taking up memory unnecessarly.
+ #
+ $this->urls = array();
+ $this->titles = array();
+ $this->html_hashes = array();
+ }
+
+
+ function transform($text) {
+ #
+ # Main function. Performs some preprocessing on the input text
+ # and pass it through the document gamut.
+ #
+ $this->setup();
+
+ # Remove UTF-8 BOM and marker character in input, if present.
+ $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text);
+
+ # Standardize line endings:
+ # DOS to Unix and Mac to Unix
+ $text = preg_replace('{\r\n?}', "\n", $text);
+
+ # Make sure $text ends with a couple of newlines:
+ $text .= "\n\n";
+
+ # Convert all tabs to spaces.
+ $text = $this->detab($text);
+
+ # Turn block-level HTML blocks into hash entries
+ $text = $this->hashHTMLBlocks($text);
+
+ # Strip any lines consisting only of spaces and tabs.
+ # This makes subsequent regexen easier to write, because we can
+ # match consecutive blank lines with /\n+/ instead of something
+ # contorted like /[ ]*\n+/ .
+ $text = preg_replace('/^[ ]+$/m', '', $text);
+
+ # Run document gamut methods.
+ foreach ($this->document_gamut as $method => $priority) {
+ $text = $this->$method($text);
+ }
+
+ $this->teardown();
+
+ return $text . "\n";
+ }
+
+ 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
+ |
+ (\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;
+ }
+ 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
+ }
+
+
+ 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|form|fieldset|iframe|math|svg|'.
+ 'article|section|nav|aside|hgroup|header|footer|'.
+ 'figure';
+
+ # Regular expression for the content of a block tag.
+ $nested_tags_level = 4;
+ $attr = '
+ (?> # optional tag attributes
+ \s # starts with whitespace
+ (?>
+ [^>"/]+ # text outside quotes
+ |
+ /+(?!>) # slash not followed by ">"
+ |
+ "[^"]*" # text inside double quotes (tolerate ">")
+ |
+ \'[^\']*\' # text inside single quotes (tolerate ">")
+ )*
+ )?
+ ';
+ $content =
+ str_repeat('
+ (?>
+ [^<]+ # content without tag
+ |
+ <\2 # nested opening tag
+ '.$attr.' # attributes
+ (?>
+ />
+ |
+ >', $nested_tags_level). # end of opening tag
+ '.*?'. # last level nested tag content
+ str_repeat('
+ \2\s*> # closing nested tag
+ )
+ |
+ <(?!/\2\s*> # other tags with a different name
+ )
+ )*',
+ $nested_tags_level);
+ $content2 = str_replace('\2', '\3', $content);
+
+ # First, look for nested blocks, e.g.:
+ #
+ #
+ # 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_re.')# start tag = $2
+ '.$attr.'> # attributes followed by > and \n
+ '.$content.' # content, support nesting
+ \2> # the matching end tag
+ [ ]* # trailing spaces/tabs
+ (?=\n+|\Z) # followed by a newline or end of document
+
+ | # Special version for tags of group a.
+
+ [ ]{0,'.$less_than_tab.'}
+ <('.$block_tags_a_re.')# start tag = $3
+ '.$attr.'>[ ]*\n # attributes followed by >
+ '.$content2.' # content, support nesting
+ \3> # the matching end tag
+ [ ]* # trailing spaces/tabs
+ (?=\n+|\Z) # followed by a newline or end of document
+
+ | # Special case just for . It was easier to make a special
+ # case than to make the other regex more complicated.
+
+ [ ]{0,'.$less_than_tab.'}
+ <(hr) # start tag = $2
+ '.$attr.' # attributes
+ /?> # the matching end tag
+ [ ]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+
+ | # Special case for standalone HTML comments:
+
+ [ ]{0,'.$less_than_tab.'}
+ (?s:
+
+ )
+ [ ]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+
+ | # PHP and ASP-style processor instructions ( and <%)
+
+ [ ]{0,'.$less_than_tab.'}
+ (?s:
+ <([?%]) # $2
+ .*?
+ \2>
+ )
+ [ ]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+
+ )
+ )}Sxmi',
+ array(&$this, '_hashHTMLBlocks_callback'),
+ $text);
+
+ return $text;
+ }
+ 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 "X" is used in the general case.
+ #
+ # Swap back any tag hash found in $text so we do not have to `unhash`
+ # multiple times at the end.
+ $text = $this->unhash($text);
+
+ # Then hash the block.
+ static $i = 0;
+ $key = "$boundary\x1A" . ++$i . $boundary;
+ $this->html_hashes[$key] = $text;
+ return $key; # String that will replace the tag.
+ }
+
+
+ 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_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;
+ }
+ 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->encodeAttribute($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;
+ }
+ 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->encodeAttribute($url);
+
+ $result = "encodeAttribute($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_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;
+ }
+ 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->encodeAttribute($this->urls[$link_id]);
+ $result = "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;
+ }
+ 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->encodeAttribute($url);
+ $result = "encodeAttribute($title);
+ $result .= " title=\"$title\""; # $title already quoted
+ }
+ $result .= $this->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) {
+ # Terrible hack to check we haven't found an empty list item.
+ if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1]))
+ return $matches[0];
+
+ $level = $matches[2]{0} == '=' ? 1 : 2;
+ $block = "".$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_re = '[*+-]';
+ $marker_ol_re = '\d+[\.]';
+ $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)";
+
+ $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;
+ }
+ function _doLists_callback($matches) {
+ # Re-usable patterns to match list item bullets and number markers:
+ $marker_ul_re = '[*+-]';
+ $marker_ol_re = '\d+[\.]';
+ $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)";
+
+ $list = $matches[1];
+ $list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol";
+
+ $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re );
+
+ $list .= "\n";
+ $result = $this->processListItems($list, $marker_any_re);
+
+ $result = $this->hashBlock("<$list_type>\n" . $result . "$list_type>");
+ return "\n". $result ."\n\n";
+ }
+
+ var $list_level = 0;
+
+ function processListItems($list_str, $marker_any_re) {
+ #
+ # Process the contents of a single ordered or unordered list, splitting it
+ # into individual list items.
+ #
+ # The $this->list_level global keeps track of when we're inside a list.
+ # Each time we enter a list, we increment it; when we leave a list,
+ # we decrement. If it's zero, we're not in a list anymore.
+ #
+ # We do this because when we're not inside a list, we want to treat
+ # something like this:
+ #
+ # I recommend upgrading to version
+ # 8. Oops, now this line is treated
+ # as a sub-list.
+ #
+ # As a single paragraph, despite the fact that the second line starts
+ # with a digit-period-space sequence.
+ #
+ # Whereas when we're inside a list (or sub-list), that line will be
+ # treated as the start of a sub-list. What a kludge, huh? This is
+ # an aspect of Markdown's syntax that's hard to parse perfectly
+ # without resorting to mind-reading. Perhaps the solution is to
+ # change the syntax rules such that sub-lists must start with a
+ # starting cardinal number; e.g. "1." or "a.".
+
+ $this->list_level++;
+
+ # trim trailing blank lines:
+ $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
+
+ $list_str = preg_replace_callback('{
+ (\n)? # leading line = $1
+ (^[ ]*) # leading whitespace = $2
+ ('.$marker_any_re.' # list marker and space = $3
+ (?:[ ]+|(?=\n)) # space only required if item is not empty
+ )
+ ((?s:.*?)) # list item text = $4
+ (?:(\n+(?=\n))|\n) # tailing blank line = $5
+ (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n))))
+ }xm',
+ array(&$this, '_processListItems_callback'), $list_str);
+
+ $this->list_level--;
+ return $list_str;
+ }
+ function _processListItems_callback($matches) {
+ $item = $matches[4];
+ $leading_line =& $matches[1];
+ $leading_space =& $matches[2];
+ $marker_space = $matches[3];
+ $tailing_blank_line =& $matches[5];
+
+ if ($leading_line || $tailing_blank_line ||
+ preg_match('/\n{2,}/', $item))
+ {
+ # Replace marker with the appropriate whitespace indentation
+ $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item;
+ $item = $this->runBlockGamut($this->outdent($item)."\n");
+ }
+ else {
+ # Recursion for sub-lists:
+ $item = $this->doLists($this->outdent($item));
+ $item = preg_replace('/\n+$/', '', $item);
+ $item = $this->runSpanGamut($item);
+ }
+
+ return "
" . $item . "
\n";
+ }
+
+
+ function doCodeBlocks($text) {
+ #
+ # Process Markdown `
` blocks.
+ #
+ $text = preg_replace_callback('{
+ (?:\n\n|\A\n?)
+ ( # $1 = the code block -- one or more lines, starting with a space/tab
+ (?>
+ [ ]{'.$this->tab_width.'} # Lines must start with a tab or a tab-width of spaces
+ .*\n+
+ )+
+ )
+ ((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z) # Lookahead for non-space at line-start, or end of doc
+ }xm',
+ array(&$this, '_doCodeBlocks_callback'), $text);
+
+ return $text;
+ }
+ 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");
+ }
+
+
+ var $em_relist = array(
+ '' => '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(?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;
+ }
+ }
+ }
+
+ function doItalicsAndBold($text) {
+ $token_stack = array('');
+ $text_stack = array('');
+ $em = '';
+ $strong = '';
+ $tree_char_em = false;
+
+ while (1) {
+ #
+ # Get prepared regular expression for seraching emphasis tokens
+ # in current context.
+ #
+ $token_re = $this->em_strong_prepared_relist["$em$strong"];
+
+ #
+ # Each loop iteration search for the next emphasis token.
+ # Each token is then passed to handleSpanToken.
+ #
+ $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
+ $text_stack[0] .= $parts[0];
+ $token =& $parts[1];
+ $text =& $parts[2];
+
+ if (empty($token)) {
+ # Reached end of text span: empty stack without emitting.
+ # any more emphasis.
+ while ($token_stack[0]) {
+ $text_stack[1] .= array_shift($token_stack);
+ $text_stack[0] .= array_shift($text_stack);
+ }
+ break;
+ }
+
+ $token_len = strlen($token);
+ if ($tree_char_em) {
+ # Reached closing marker while inside a three-char emphasis.
+ if ($token_len == 3) {
+ # Three-char closing marker, close em and strong.
+ array_shift($token_stack);
+ $span = array_shift($text_stack);
+ $span = $this->runSpanGamut($span);
+ $span = "$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$tag>";
+ $text_stack[0] = $this->hashPart($span);
+ $$tag = ''; # $$tag stands for $em or $strong
+ }
+ $tree_char_em = false;
+ } else if ($token_len == 3) {
+ if ($em) {
+ # Reached closing marker for both em and strong.
+ # Closing strong marker:
+ for ($i = 0; $i < 2; ++$i) {
+ $shifted_token = array_shift($token_stack);
+ $tag = strlen($shifted_token) == 2 ? "strong" : "em";
+ $span = array_shift($text_stack);
+ $span = $this->runSpanGamut($span);
+ $span = "<$tag>$span$tag>";
+ $text_stack[0] .= $this->hashPart($span);
+ $$tag = ''; # $$tag stands for $em or $strong
+ }
+ } else {
+ # Reached opening three-char emphasis marker. Push on token
+ # stack; will be handled by the special condition above.
+ $em = $token{0};
+ $strong = "$em$em";
+ array_unshift($token_stack, $token);
+ array_unshift($text_stack, '');
+ $tree_char_em = true;
+ }
+ } else if ($token_len == 2) {
+ if ($strong) {
+ # Unwind any dangling emphasis marker:
+ if (strlen($token_stack[0]) == 1) {
+ $text_stack[1] .= array_shift($token_stack);
+ $text_stack[0] .= array_shift($text_stack);
+ }
+ # Closing strong marker:
+ array_shift($token_stack);
+ $span = array_shift($text_stack);
+ $span = $this->runSpanGamut($span);
+ $span = "$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;
+ }
+ }
+ }
+ return $text_stack[0];
+ }
+
+
+ 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*
+ #
+ # 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] = ''.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 = '';
+
+ $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
+ |
+ [-a-zA-Z0-9:_]+\s*> # closing tag
+ ').'
+ )
+ }xs';
+
+ while (1) {
+ #
+ # Each loop iteration seach for either the next tag, the next
+ # openning code span marker, or the next escaped character.
+ # Each token is then passed to handleSpanToken.
+ #
+ $parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE);
+
+ # Create token from text preceding tag.
+ if ($parts[0] != "") {
+ $output .= $parts[0];
+ }
+
+ # Check if we reach the end.
+ if (isset($parts[1])) {
+ $output .= $this->handleSpanToken($parts[1], $parts[2]);
+ $str = $parts[2];
+ }
+ else {
+ break;
+ }
+ }
+
+ return $output;
+ }
+
+
+ function handleSpanToken($token, &$str) {
+ #
+ # Handle $token provided by parseSpan by determining its nature and
+ # returning the corresponding value that should replace it.
+ #
+ switch ($token{0}) {
+ case "\\":
+ return $this->hashPart("". ord($token{1}). ";");
+ case "`":
+ # Search for end marker in remaining text.
+ if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm',
+ $str, $matches))
+ {
+ $str = $matches[2];
+ $codespan = $this->makeCodeSpan($matches[1]);
+ return $this->hashPart($codespan);
+ }
+ return $token; // return as text since no ending marker found.
+ default:
+ return $this->hashPart($token);
+ }
+ }
+
+
+ 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
+#
+
+class MarkdownExtra_Parser extends Markdown_Parser {
+
+ ### Configuration Variables ###
+
+ # 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;
+
+ # Optional class prefix for fenced code block.
+ var $code_class_prefix = MARKDOWN_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.
+ var $code_attr_on_pre = MARKDOWN_CODE_ATTR_ON_PRE;
+
+ # Predefined abbreviations.
+ var $predef_abbr = array();
+
+
+ ### Parser Implementation ###
+
+ 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(
+ "doFencedCodeBlocks" => 5,
+ "stripFootnotes" => 15,
+ "stripAbbreviations" => 25,
+ "appendFootnotes" => 50,
+ );
+ $this->block_gamut += array(
+ "doFencedCodeBlocks" => 5,
+ "doTables" => 15,
+ "doDefLists" => 45,
+ );
+ $this->span_gamut += array(
+ "doFootnotes" => 5,
+ "doAbbreviations" => 70,
+ );
+
+ parent::Markdown_Parser();
+ }
+
+
+ # Extra variables used during extra transformations.
+ var $footnotes = array();
+ var $footnotes_ordered = array();
+ var $footnotes_ref_count = array();
+ var $footnotes_numbers = array();
+ var $abbr_desciptions = array();
+ var $abbr_word_re = '';
+
+ # Give the current footnote number.
+ var $footnote_counter = 1;
+
+
+ function setup() {
+ #
+ # Setting up Extra-specific variables.
+ #
+ parent::setup();
+
+ $this->footnotes = array();
+ $this->footnotes_ordered = array();
+ $this->footnotes_ref_count = array();
+ $this->footnotes_numbers = array();
+ $this->abbr_desciptions = array();
+ $this->abbr_word_re = '';
+ $this->footnote_counter = 1;
+
+ foreach ($this->predef_abbr as $abbr_word => $abbr_desc) {
+ if ($this->abbr_word_re)
+ $this->abbr_word_re .= '|';
+ $this->abbr_word_re .= preg_quote($abbr_word);
+ $this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
+ }
+ }
+
+ function teardown() {
+ #
+ # Clearing Extra-specific variables.
+ #
+ $this->footnotes = array();
+ $this->footnotes_ordered = array();
+ $this->footnotes_ref_count = array();
+ $this->footnotes_numbers = array();
+ $this->abbr_desciptions = array();
+ $this->abbr_word_re = '';
+
+ parent::teardown();
+ }
+
+
+ ### Extra Attribute Parser ###
+
+ # Expression to use to catch attributes (includes the braces)
+ var $id_class_attr_catch_re = '\{((?:[ ]*[#.][-_:a-zA-Z0-9]+){1,})[ ]*\}';
+ # Expression to use when parsing in a context when no capture is desired
+ var $id_class_attr_nocatch_re = '\{(?:[ ]*[#.][-_:a-zA-Z0-9]+){1,}[ ]*\}';
+
+ function doExtraAttributes($tag_name, $attr) {
+ #
+ # Parse attributes caught by the $this->id_class_attr_catch_re expression
+ # and return the HTML-formatted list of attributes.
+ #
+ # Currently supported attributes are .class and #id.
+ #
+ if (empty($attr)) return "";
+
+ # Split on components
+ preg_match_all('/[#.][-_:a-zA-Z0-9]+/', $attr, $matches);
+ $elements = $matches[0];
+
+ # handle classes and ids (only first id taken into account)
+ $classes = 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);
+ }
+ }
+
+ # compose attributes as string
+ $attr_str = "";
+ if (!empty($id)) {
+ $attr_str .= ' id="'.$id.'"';
+ }
+ if (!empty($classes)) {
+ $attr_str .= ' class="'.implode(" ", $classes).'"';
+ }
+ return $attr_str;
+ }
+
+
+ function stripLinkDefinitions($text) {
+ #
+ # Strips link definitions from text, stores the URLs and titles in
+ # hash references.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # Link defs are in the form: ^[id]: url "optional title"
+ $text = preg_replace_callback('{
+ ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1
+ [ ]*
+ \n? # maybe *one* newline
+ [ ]*
+ (?:
+ <(.+?)> # url = $2
+ |
+ (\S+?) # url = $3
+ )
+ [ ]*
+ \n? # maybe one newline
+ [ ]*
+ (?:
+ (?<=\s) # lookbehind for whitespace
+ ["(]
+ (.*?) # title = $4
+ [")]
+ [ ]*
+ )? # title is optional
+ (?:[ ]* '.$this->id_class_attr_catch_re.' )? # $5 = extra id & class attr
+ (?:\n+|\Z)
+ }xm',
+ array(&$this, '_stripLinkDefinitions_callback'),
+ $text);
+ return $text;
+ }
+ 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:
+ var $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';
+
+ # Tags treated as block tags only if the opening tag is alone on its line:
+ var $context_block_tags_re = 'script|noscript|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video';
+
+ # Tags where markdown="1" default to span mode:
+ var $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:
+ var $clean_tags_re = 'script|math|svg';
+
+ # Tags that do not need to be closed.
+ var $auto_close_tags_re = 'hr|img|param|source|track';
+
+
+ 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 within a tag, _HashHTMLBlocks_InHTML calls back
+ # _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag.
+ # These two functions are calling each other. It's recursive!
+ #
+ if ($this->no_markup) return $text;
+
+ #
+ # Call the HTML-in-Markdown hasher.
+ #
+ list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text);
+
+ return $text;
+ }
+ function _hashHTMLBlocks_inMarkdown($text, $indent = 0,
+ $enclosing_tag_re = '', $span = false)
+ {
+ #
+ # Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags.
+ #
+ # * $indent is the number of space to be ignored when checking for code
+ # blocks. This is important because if we don't take the indent into
+ # account, something like this (which looks right) won't work as expected:
+ #
+ #
+ #
+ # 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 )
+ #
+ if ($text === '') return array('', '');
+
+ # Regex to check for the presense of newlines around a block tag.
+ $newline_before_re = '/(?:^\n?|\n\n)*$/';
+ $newline_after_re =
+ '{
+ ^ # Start of text following the tag.
+ (?>[ ]*)? # Optional comment.
+ [ ]*\n # Must be followed by newline.
+ }xs';
+
+ # Regex to match any tag.
+ $block_tag_re =
+ '{
+ ( # $2: Capture whole tag.
+ ? # Any opening or closing tag.
+ (?> # Tag name.
+ '.$this->block_tags_re.' |
+ '.$this->context_block_tags_re.' |
+ '.$this->clean_tags_re.' |
+ (?!\s)'.$enclosing_tag_re.'
+ )
+ (?:
+ (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name.
+ (?>
+ ".*?" | # Double quotes (can contain `>`)
+ \'.*?\' | # Single quotes (can contain `>`)
+ .+? # Anything but quotes and `>`.
+ )*?
+ )?
+ > # End of tag.
+ |
+ # HTML Comment
+ |
+ <\?.*?\?> | <%.*?%> # Processing instruction
+ |
+ # CData Block
+ '. ( !$span ? ' # If not in span.
+ |
+ # Indented code block
+ (?: ^[ ]*\n | ^ | \n[ ]*\n )
+ [ ]{'.($indent+4).'}[^\n]* \n
+ (?>
+ (?: [ ]{'.($indent+4).'}[^\n]* | [ ]* ) \n
+ )*
+ |
+ # Fenced code block marker
+ (?<= ^ | \n )
+ [ ]{0,'.($indent+3).'}(?:~{3,}|`{3,})
+ [ ]*
+ (?:
+ \.?[-_:a-zA-Z0-9]+ # standalone class name
+ |
+ '.$this->id_class_attr_nocatch_re.' # extra attributes
+ )?
+ [ ]*
+ (?= \n )
+ ' : '' ). ' # End (if not is span).
+ |
+ # Code span marker
+ # Note, this regex needs to go after backtick fenced
+ # code blocks but it should also be kept outside of the
+ # "if not in span" condition adding backticks to the parser
+ `+
+ )
+ }xs';
+
+
+ $depth = 0; # Current depth inside the tag tree.
+ $parsed = ""; # Parsed text that will be returned.
+
+ #
+ # Loop through every tag until we find the closing tag of the parent
+ # or loop until reaching the end of text if no parent tag specified.
+ #
+ do {
+ #
+ # Split the text using the first $tag_match pattern found.
+ # Text before pattern will be first in the array, text after
+ # pattern will be at the end, and between will be any catches made
+ # by the pattern.
+ #
+ $parts = preg_split($block_tag_re, $text, 2,
+ PREG_SPLIT_DELIM_CAPTURE);
+
+ # If in Markdown span mode, add a empty-string span-level hash
+ # after each newline to prevent triggering any block element.
+ if ($span) {
+ $void = $this->hashPart("", ':');
+ $newline = "$void\n";
+ $parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void;
+ }
+
+ $parsed .= $parts[0]; # Text before current tag.
+
+ # If end of $text has been reached. Stop loop.
+ if (count($parts) < 3) {
+ $text = "";
+ break;
+ }
+
+ $tag = $parts[1]; # Tag to handle.
+ $text = $parts[2]; # Remaining text after current tag.
+ $tag_re = preg_quote($tag); # For use in a regular expression.
+
+ #
+ # Check for: Fenced code block marker.
+ # Note: need to recheck the whole tag to disambiguate backtick
+ # fences from code spans
+ #
+ if (preg_match('{^\n?([ ]{0,'.($indent+3).'})(~{3,}|`{3,})[ ]*(?:\.?[-_:a-zA-Z0-9]+|'.$this->id_class_attr_nocatch_re.')?[ ]*\n?$}', $tag, $capture)) {
+ # Fenced code block marker: find matching end marker.
+ $fence_indent = strlen($capture[1]); # use captured indent in re
+ $fence_re = $capture[2]; # use captured fence in re
+ if (preg_match('{^(?>.*\n)*?[ ]{'.($fence_indent).'}'.$fence_re.'[ ]*(?:\n|$)}', $text,
+ $matches))
+ {
+ # End marker found: pass text unchanged until marker.
+ $parsed .= $tag . $matches[0];
+ $text = substr($text, strlen($matches[0]));
+ }
+ else {
+ # No end marker: just skip it.
+ $parsed .= $tag;
+ }
+ }
+ #
+ # Check for: Indented code block.
+ #
+ else if ($tag{0} == "\n" || $tag{0} == " ") {
+ # Indented code block: pass it unchanged, will be handled
+ # later.
+ $parsed .= $tag;
+ }
+ #
+ # Check for: Code span marker
+ # Note: need to check this after backtick fenced code blocks
+ #
+ else if ($tag{0} == "`") {
+ # Find corresponding end marker.
+ $tag_re = preg_quote($tag);
+ if (preg_match('{^(?>.+?|\n(?!\n))*?(?block_tags_re.')\b}', $tag) ||
+ ( preg_match('{^<(?:'.$this->context_block_tags_re.')\b}', $tag) &&
+ preg_match($newline_before_re, $parsed) &&
+ preg_match($newline_after_re, $text) )
+ )
+ {
+ # Need to parse tag and following text using the HTML parser.
+ list($block_text, $text) =
+ $this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true);
+
+ # Make sure it stays outside of any paragraph by adding newlines.
+ $parsed .= "\n\n$block_text\n\n";
+ }
+ #
+ # Check for: Clean tag (like script, math)
+ # HTML Comments, processing instructions.
+ #
+ else if (preg_match('{^<(?:'.$this->clean_tags_re.')\b}', $tag) ||
+ $tag{1} == '!' || $tag{1} == '?')
+ {
+ # Need to parse tag and following text using the HTML parser.
+ # (don't check for markdown attribute)
+ list($block_text, $text) =
+ $this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false);
+
+ $parsed .= $block_text;
+ }
+ #
+ # Check for: Tag with same name as enclosing tag.
+ #
+ else if ($enclosing_tag_re !== '' &&
+ # Same name as enclosing tag.
+ preg_match('{^?(?:'.$enclosing_tag_re.')\b}', $tag))
+ {
+ #
+ # Increase/decrease nested tag count.
+ #
+ if ($tag{1} == '/') $depth--;
+ else if ($tag{strlen($tag)-2} != '/') $depth++;
+
+ if ($depth < 0) {
+ #
+ # Going out of parent element. Clean up and break so we
+ # return to the calling function.
+ #
+ $text = $tag . $text;
+ break;
+ }
+
+ $parsed .= $tag;
+ }
+ else {
+ $parsed .= $tag;
+ }
+ } while ($depth >= 0);
+
+ return array($parsed, $text);
+ }
+ function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) {
+ #
+ # Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags.
+ #
+ # * Calls $hash_method to convert any blocks.
+ # * Stops when the first opening tag closes.
+ # * $md_attr indicate if the use of the `markdown="1"` attribute is allowed.
+ # (it is not inside clean tags)
+ #
+ # Returns an array of that form: ( processed text , remaining text )
+ #
+ if ($text === '') return array('', '');
+
+ # Regex to match `markdown` attribute inside of a tag.
+ $markdown_attr_re = '
+ {
+ \s* # Eat whitespace before the `markdown` attribute
+ markdown
+ \s*=\s*
+ (?>
+ (["\']) # $1: quote delimiter
+ (.*?) # $2: attribute value
+ \1 # matching delimiter
+ |
+ ([^\s>]*) # $3: unquoted attribute value
+ )
+ () # $4: make $3 always defined (avoid warnings)
+ }xs';
+
+ # Regex to match any tag.
+ $tag_re = '{
+ ( # $2: Capture whole tag.
+ ? # Any opening or closing tag.
+ [\w:$]+ # Tag name.
+ (?:
+ (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name.
+ (?>
+ ".*?" | # Double quotes (can contain `>`)
+ \'.*?\' | # Single quotes (can contain `>`)
+ .+? # Anything but quotes and `>`.
+ )*?
+ )?
+ > # End of tag.
+ |
+ # HTML Comment
+ |
+ <\?.*?\?> | <%.*?%> # Processing instruction
+ |
+ # CData Block
+ )
+ }xs';
+
+ $original_text = $text; # Save original text in case of faliure.
+
+ $depth = 0; # Current depth inside the tag tree.
+ $block_text = ""; # Temporary text holder for current text.
+ $parsed = ""; # Parsed text that will be returned.
+
+ #
+ # Get the name of the starting tag.
+ # (This pattern makes $base_tag_name_re safe without quoting.)
+ #
+ if (preg_match('/^<([\w:$]*)\b/', $text, $matches))
+ $base_tag_name_re = $matches[1];
+
+ #
+ # Loop through every tag until we find the corresponding closing tag.
+ #
+ do {
+ #
+ # Split the text using the first $tag_match pattern found.
+ # Text before pattern will be first in the array, text after
+ # pattern will be at the end, and between will be any catches made
+ # by the pattern.
+ #
+ $parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
+
+ if (count($parts) < 3) {
+ #
+ # End of $text reached with unbalenced tag(s).
+ # In that case, we return original text unchanged and pass the
+ # first character as filtered to prevent an infinite loop in the
+ # parent function.
+ #
+ return array($original_text{0}, substr($original_text, 1));
+ }
+
+ $block_text .= $parts[0]; # Text before current tag.
+ $tag = $parts[1]; # Tag to handle.
+ $text = $parts[2]; # Remaining text after current tag.
+
+ #
+ # Check for: Auto-close tag (like )
+ # Comments and Processing Instructions.
+ #
+ if (preg_match('{^?(?:'.$this->auto_close_tags_re.')\b}', $tag) ||
+ $tag{1} == '!' || $tag{1} == '?')
+ {
+ # Just add the tag to the block as if it was text.
+ $block_text .= $tag;
+ }
+ else {
+ #
+ # Increase/decrease nested tag count. Only do so if
+ # the tag's name match base tag's.
+ #
+ if (preg_match('{^?'.$base_tag_name_re.'\b}', $tag)) {
+ if ($tag{1} == '/') $depth--;
+ else if ($tag{strlen($tag)-2} != '/') $depth++;
+ }
+
+ #
+ # Check for `markdown="1"` attribute and handle it.
+ #
+ if ($md_attr &&
+ preg_match($markdown_attr_re, $tag, $attr_m) &&
+ preg_match('/^1|block|span$/', $attr_m[2] . $attr_m[3]))
+ {
+ # Remove `markdown` attribute from opening tag.
+ $tag = preg_replace($markdown_attr_re, '', $tag);
+
+ # Check if text inside this tag must be parsed in span mode.
+ $this->mode = $attr_m[2] . $attr_m[3];
+ $span_mode = $this->mode == 'span' || $this->mode != 'block' &&
+ preg_match('{^<(?:'.$this->contain_span_tags_re.')\b}', $tag);
+
+ # Calculate indent before tag.
+ if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) {
+ $strlen = $this->utf8_strlen;
+ $indent = $strlen($matches[1], 'UTF-8');
+ } else {
+ $indent = 0;
+ }
+
+ # End preceding block with this tag.
+ $block_text .= $tag;
+ $parsed .= $this->$hash_method($block_text);
+
+ # Get enclosing tag name for the ParseMarkdown function.
+ # (This pattern makes $tag_name_re safe without quoting.)
+ preg_match('/^<([\w:$]*)\b/', $tag, $matches);
+ $tag_name_re = $matches[1];
+
+ # Parse the content using the HTML-in-Markdown parser.
+ list ($block_text, $text)
+ = $this->_hashHTMLBlocks_inMarkdown($text, $indent,
+ $tag_name_re, $span_mode);
+
+ # Outdent markdown text.
+ if ($indent > 0) {
+ $block_text = preg_replace("/^[ ]{1,$indent}/m", "",
+ $block_text);
+ }
+
+ # Append tag content to parsed text.
+ if (!$span_mode) $parsed .= "\n\n$block_text\n\n";
+ else $parsed .= "$block_text";
+
+ # Start over with a new block.
+ $block_text = "";
+ }
+ else $block_text .= $tag;
+ }
+
+ } while ($depth > 0);
+
+ #
+ # Hash last block text that wasn't processed inside the loop.
+ #
+ $parsed .= $this->$hash_method($block_text);
+
+ return array($parsed, $text);
+ }
+
+
+ function hashClean($text) {
+ #
+ # Called whenever a tag must be hashed when a function inserts a "clean" tag
+ # in $text, it passes through this function and is automaticaly escaped,
+ # blocking invalid nested overlap.
+ #
+ return $this->hashPart($text, 'C');
+ }
+
+
+ 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_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;
+ }
+ 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->encodeAttribute($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;
+ }
+ function _doAnchors_inline_callback($matches) {
+ $whole_match = $matches[1];
+ $link_text = $this->runSpanGamut($matches[2]);
+ $url = $matches[3] == '' ? $matches[4] : $matches[3];
+ $title =& $matches[7];
+ $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]);
+
+
+ $url = $this->encodeAttribute($url);
+
+ $result = "encodeAttribute($title);
+ $result .= " title=\"$title\"";
+ }
+ $result .= $attr;
+
+ $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_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;
+ }
+ 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->encodeAttribute($this->urls[$link_id]);
+ $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];
+ $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];
+ $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]);
+
+ $alt_text = $this->encodeAttribute($alt_text);
+ $url = $this->encodeAttribute($url);
+ $result = "encodeAttribute($title);
+ $result .= " title=\"$title\""; # $title already quoted
+ }
+ $result .= $attr;
+ $result .= $this->empty_element_suffix;
+
+ return $this->hashPart($result);
+ }
+
+
+ function doHeaders($text) {
+ #
+ # Redefined to add id and class attribute support.
+ #
+ # Setext-style headers:
+ # Header 1 {#header1}
+ # ========
+ #
+ # Header 2 {#header2 .class1 .class2}
+ # --------
+ #
+ $text = preg_replace_callback(
+ '{
+ (^.+?) # $1: Header text
+ (?:[ ]+ '.$this->id_class_attr_catch_re.' )? # $3 = id/class attributes
+ [ ]*\n(=+|-+)[ ]*\n+ # $3: Header footer
+ }mx',
+ array(&$this, '_doHeaders_callback_setext'), $text);
+
+ # atx-style headers:
+ # # Header 1 {#header1}
+ # ## Header 2 {#header2}
+ # ## Header 2 with closing hashes ## {#header3.class1.class2}
+ # ...
+ # ###### Header 6 {.class2}
+ #
+ $text = preg_replace_callback('{
+ ^(\#{1,6}) # $1 = string of #\'s
+ [ ]*
+ (.+?) # $2 = Header text
+ [ ]*
+ \#* # optional closing #\'s (not counted)
+ (?:[ ]+ '.$this->id_class_attr_catch_re.' )? # $3 = id/class attributes
+ [ ]*
+ \n+
+ }xm',
+ array(&$this, '_doHeaders_callback_atx'), $text);
+
+ return $text;
+ }
+ function _doHeaders_callback_setext($matches) {
+ if ($matches[3] == '-' && preg_match('{^- }', $matches[1]))
+ return $matches[0];
+ $level = $matches[3]{0} == '=' ? 1 : 2;
+ $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2]);
+ $block = "".$this->runSpanGamut($matches[1])."";
+ return "\n" . $this->hashBlock($block) . "\n\n";
+ }
+ function _doHeaders_callback_atx($matches) {
+ $level = strlen($matches[1]);
+ $attr = $this->doExtraAttributes("h$level", $dummy =& $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);
+ $attr = array_pad($attr, $col_count, '');
+
+ # Write column headers.
+ $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_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;
+ }
+ 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('{
+ (?>\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.'} \:[ ] |
+
";
+
+ return "\n\n".$this->hashBlock($codeblock)."\n\n";
+ }
+ 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 $em_relist = array(
+ '' => '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? 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 replaced by the actual footnote marker in appendFootnotes.
+ #
+ if (!$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 .= "
";
+ }
+ 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])) {
+ $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 = " rel=\"footnote\"";
+ if ($this->fn_link_class != "") {
+ $class = $this->fn_link_class;
+ $class = $this->encodeAttribute($class);
+ $attr .= " class=\"$class\"";
+ }
+ if ($this->fn_link_title != "") {
+ $title = $this->fn_link_title;
+ $title = $this->encodeAttribute($title);
+ $attr .= " title=\"$title\"";
+ }
+
+ $attr = str_replace("%%", $num, $attr);
+ $node_id = $this->encodeAttribute($node_id);
+
+ return
+ "".
+ "$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];
+ 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
+ }
+
+
+ function doAbbreviations($text) {
+ #
+ # Find defined abbreviations in text and wrap them in elements.
+ #
+ 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;
+ }
+ 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 = $this->encodeAttribute($desc);
+ 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 mostly 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-2013 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.
+
+*/
+?>
diff --git a/milfs/js/bootstrap-markdown.js b/milfs/js/bootstrap-markdown.js
new file mode 100644
index 0000000..d9a7b76
--- /dev/null
+++ b/milfs/js/bootstrap-markdown.js
@@ -0,0 +1,1369 @@
+/* ===================================================
+ * bootstrap-markdown.js v2.7.0
+ * http://github.com/toopay/bootstrap-markdown
+ * ===================================================
+ * Copyright 2013-2014 Taufan Aditya
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================================================== */
+
+!function ($) {
+
+ "use strict"; // jshint ;_;
+
+
+ /* MARKDOWN CLASS DEFINITION
+ * ========================== */
+
+ var Markdown = function (element, options) {
+ // Class Properties
+ this.$ns = 'bootstrap-markdown'
+ this.$element = $(element)
+ this.$editable = {el:null, type:null,attrKeys:[], attrValues:[], content:null}
+ this.$options = $.extend(true, {}, $.fn.markdown.defaults, options, this.$element.data(), this.$element.data('options'))
+ this.$oldContent = null
+ this.$isPreview = false
+ this.$isFullscreen = false
+ this.$editor = null
+ this.$textarea = null
+ this.$handler = []
+ this.$callback = []
+ this.$nextTab = []
+
+ this.showEditor()
+ }
+
+ Markdown.prototype = {
+
+ constructor: Markdown
+
+ , __alterButtons: function(name,alter) {
+ var handler = this.$handler, isAll = (name == 'all'),that = this
+
+ $.each(handler,function(k,v) {
+ var halt = true
+ if (isAll) {
+ halt = false
+ } else {
+ halt = v.indexOf(name) < 0
+ }
+
+ if (halt == false) {
+ alter(that.$editor.find('button[data-handler="'+v+'"]'))
+ }
+ })
+ }
+
+ , __buildButtons: function(buttonsArray, container) {
+ var i,
+ ns = this.$ns,
+ handler = this.$handler,
+ callback = this.$callback
+
+ for (i=0;i', {
+ 'class': 'btn-group'
+ })
+
+ for (z=0;z');
+ buttonContainer.text(' ' + this.__localize(btnText)).addClass('btn-default btn-sm').addClass(btnClass);
+ if(btnClass.match(/btn\-(primary|success|info|warning|danger|link)/)){
+ buttonContainer.removeClass('btn-default');
+ }
+ buttonContainer.attr({
+ 'type': 'button',
+ 'title': this.__localize(button.title) + hotkeyCaption,
+ 'tabindex': tabIndex,
+ 'data-provider': ns,
+ 'data-handler': buttonHandler,
+ 'data-hotkey': hotkey
+ });
+ if (button.toggle == true){
+ buttonContainer.attr('data-toggle', 'button');
+ }
+ buttonIconContainer = $('');
+ buttonIconContainer.addClass(buttonIcon);
+ buttonIconContainer.prependTo(buttonContainer);
+
+ // Attach the button object
+ btnGroupContainer.append(buttonContainer);
+
+ // Register handler and callback
+ handler.push(buttonHandler);
+ callback.push(button.callback);
+ }
+
+ // Attach the button group into container dom
+ container.append(btnGroupContainer);
+ }
+ }
+
+ return container;
+ }
+ , __setListener: function() {
+ // Set size and resizable Properties
+ var hasRows = typeof this.$textarea.attr('rows') != 'undefined',
+ maxRows = this.$textarea.val().split("\n").length > 5 ? this.$textarea.val().split("\n").length : '5',
+ rowsVal = hasRows ? this.$textarea.attr('rows') : maxRows
+
+ this.$textarea.attr('rows',rowsVal)
+ if (this.$options.resize) {
+ this.$textarea.css('resize',this.$options.resize)
+ }
+
+ this.$textarea
+ .on('focus', $.proxy(this.focus, this))
+ .on('keypress', $.proxy(this.keypress, this))
+ .on('keyup', $.proxy(this.keyup, this))
+ .on('change', $.proxy(this.change, this))
+
+ if (this.eventSupported('keydown')) {
+ this.$textarea.on('keydown', $.proxy(this.keydown, this))
+ }
+
+ // Re-attach markdown data
+ this.$textarea.data('markdown',this)
+ }
+
+ , __handle: function(e) {
+ var target = $(e.currentTarget),
+ handler = this.$handler,
+ callback = this.$callback,
+ handlerName = target.attr('data-handler'),
+ callbackIndex = handler.indexOf(handlerName),
+ callbackHandler = callback[callbackIndex]
+
+ // Trigger the focusin
+ $(e.currentTarget).focus()
+
+ callbackHandler(this)
+
+ // Trigger onChange for each button handle
+ this.change(this);
+
+ // Unless it was the save handler,
+ // focusin the textarea
+ if (handlerName.indexOf('cmdSave') < 0) {
+ this.$textarea.focus()
+ }
+
+ e.preventDefault()
+ }
+
+ , __localize: function(string) {
+ var messages = $.fn.markdown.messages,
+ language = this.$options.language
+ if (
+ typeof messages !== 'undefined' &&
+ typeof messages[language] !== 'undefined' &&
+ typeof messages[language][string] !== 'undefined'
+ ) {
+ return messages[language][string];
+ }
+ return string;
+ }
+
+ , __getIcon: function(src) {
+ return typeof src == 'object' ? src[this.$options.iconlibrary] : src;
+ }
+
+ , setFullscreen: function(mode) {
+ var $editor = this.$editor,
+ $textarea = this.$textarea
+
+ if (mode === true) {
+ $editor.addClass('md-fullscreen-mode')
+ $('body').addClass('md-nooverflow')
+ this.$options.onFullscreen(this)
+ } else {
+ $editor.removeClass('md-fullscreen-mode')
+ $('body').removeClass('md-nooverflow')
+ }
+
+ this.$isFullscreen = mode;
+ $textarea.focus()
+ }
+
+ , showEditor: function() {
+ var instance = this,
+ textarea,
+ ns = this.$ns,
+ container = this.$element,
+ originalHeigth = container.css('height'),
+ originalWidth = container.css('width'),
+ editable = this.$editable,
+ handler = this.$handler,
+ callback = this.$callback,
+ options = this.$options,
+ editor = $( '', {
+ 'class': 'md-editor',
+ click: function() {
+ instance.focus()
+ }
+ })
+
+ // Prepare the editor
+ if (this.$editor == null) {
+ // Create the panel
+ var editorHeader = $('', {
+ 'class': 'md-header btn-toolbar'
+ })
+
+ // Merge the main & additional button groups together
+ var allBtnGroups = []
+ if (options.buttons.length > 0) allBtnGroups = allBtnGroups.concat(options.buttons[0])
+ if (options.additionalButtons.length > 0) allBtnGroups = allBtnGroups.concat(options.additionalButtons[0])
+
+ // Reduce and/or reorder the button groups
+ if (options.reorderButtonGroups.length > 0) {
+ allBtnGroups = allBtnGroups
+ .filter(function(btnGroup) {
+ return options.reorderButtonGroups.indexOf(btnGroup.name) > -1
+ })
+ .sort(function(a, b) {
+ if (options.reorderButtonGroups.indexOf(a.name) < options.reorderButtonGroups.indexOf(b.name)) return -1
+ if (options.reorderButtonGroups.indexOf(a.name) > options.reorderButtonGroups.indexOf(b.name)) return 1
+ return 0
+ })
+ }
+
+ // Build the buttons
+ if (allBtnGroups.length > 0) {
+ editorHeader = this.__buildButtons([allBtnGroups], editorHeader)
+ }
+
+ if (options.fullscreen.enable) {
+ editorHeader.append('
',minLength:1},a.fn.typeahead.Constructor=b,a.fn.typeahead.noConflict=function(){return a.fn.typeahead=c,this},a(document).on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(b){var c=a(this);if(c.data("typeahead"))return;c.typeahead(c.data())})}(window.jQuery)
\ No newline at end of file
diff --git a/milfs/js/markdown.js b/milfs/js/markdown.js
new file mode 100644
index 0000000..07f4e46
--- /dev/null
+++ b/milfs/js/markdown.js
@@ -0,0 +1,1616 @@
+// Released under MIT license
+// Copyright (c) 2009-2010 Dominic Baggott
+// Copyright (c) 2009-2010 Ash Berlin
+// Copyright (c) 2011 Christoph Dorn (http://www.christophdorn.com)
+
+(function( expose ) {
+
+/**
+ * class Markdown
+ *
+ * Markdown processing in Javascript done right. We have very particular views
+ * on what constitutes 'right' which include:
+ *
+ * - produces well-formed HTML (this means that em and strong nesting is
+ * important)
+ *
+ * - has an intermediate representation to allow processing of parsed data (We
+ * in fact have two, both as [JsonML]: a markdown tree and an HTML tree).
+ *
+ * - is easily extensible to add new dialects without having to rewrite the
+ * entire parsing mechanics
+ *
+ * - has a good test suite
+ *
+ * This implementation fulfills all of these (except that the test suite could
+ * do with expanding to automatically run all the fixtures from other Markdown
+ * implementations.)
+ *
+ * ##### Intermediate Representation
+ *
+ * *TODO* Talk about this :) Its JsonML, but document the node names we use.
+ *
+ * [JsonML]: http://jsonml.org/ "JSON Markup Language"
+ **/
+var Markdown = expose.Markdown = function Markdown(dialect) {
+ switch (typeof dialect) {
+ case "undefined":
+ this.dialect = Markdown.dialects.Gruber;
+ break;
+ case "object":
+ this.dialect = dialect;
+ break;
+ default:
+ if (dialect in Markdown.dialects) {
+ this.dialect = Markdown.dialects[dialect];
+ }
+ else {
+ throw new Error("Unknown Markdown dialect '" + String(dialect) + "'");
+ }
+ break;
+ }
+ this.em_state = [];
+ this.strong_state = [];
+ this.debug_indent = "";
+};
+
+/**
+ * parse( markdown, [dialect] ) -> JsonML
+ * - markdown (String): markdown string to parse
+ * - dialect (String | Dialect): the dialect to use, defaults to gruber
+ *
+ * Parse `markdown` and return a markdown document as a Markdown.JsonML tree.
+ **/
+expose.parse = function( source, dialect ) {
+ // dialect will default if undefined
+ var md = new Markdown( dialect );
+ return md.toTree( source );
+};
+
+/**
+ * toHTML( markdown, [dialect] ) -> String
+ * toHTML( md_tree ) -> String
+ * - markdown (String): markdown string to parse
+ * - md_tree (Markdown.JsonML): parsed markdown tree
+ *
+ * Take markdown (either as a string or as a JsonML tree) and run it through
+ * [[toHTMLTree]] then turn it into a well-formated HTML fragment.
+ **/
+expose.toHTML = function toHTML( source , dialect , options ) {
+ var input = expose.toHTMLTree( source , dialect , options );
+
+ return expose.renderJsonML( input );
+};
+
+/**
+ * toHTMLTree( markdown, [dialect] ) -> JsonML
+ * toHTMLTree( md_tree ) -> JsonML
+ * - markdown (String): markdown string to parse
+ * - dialect (String | Dialect): the dialect to use, defaults to gruber
+ * - md_tree (Markdown.JsonML): parsed markdown tree
+ *
+ * Turn markdown into HTML, represented as a JsonML tree. If a string is given
+ * to this function, it is first parsed into a markdown tree by calling
+ * [[parse]].
+ **/
+expose.toHTMLTree = function toHTMLTree( input, dialect , options ) {
+ // convert string input to an MD tree
+ if ( typeof input ==="string" ) input = this.parse( input, dialect );
+
+ // Now convert the MD tree to an HTML tree
+
+ // remove references from the tree
+ var attrs = extract_attr( input ),
+ refs = {};
+
+ if ( attrs && attrs.references ) {
+ refs = attrs.references;
+ }
+
+ var html = convert_tree_to_html( input, refs , options );
+ merge_text_nodes( html );
+ return html;
+};
+
+// For Spidermonkey based engines
+function mk_block_toSource() {
+ return "Markdown.mk_block( " +
+ uneval(this.toString()) +
+ ", " +
+ uneval(this.trailing) +
+ ", " +
+ uneval(this.lineNumber) +
+ " )";
+}
+
+// node
+function mk_block_inspect() {
+ var util = require('util');
+ return "Markdown.mk_block( " +
+ util.inspect(this.toString()) +
+ ", " +
+ util.inspect(this.trailing) +
+ ", " +
+ util.inspect(this.lineNumber) +
+ " )";
+
+}
+
+var mk_block = Markdown.mk_block = function(block, trail, line) {
+ // Be helpful for default case in tests.
+ if ( arguments.length == 1 ) trail = "\n\n";
+
+ var s = new String(block);
+ s.trailing = trail;
+ // To make it clear its not just a string
+ s.inspect = mk_block_inspect;
+ s.toSource = mk_block_toSource;
+
+ if (line != undefined)
+ s.lineNumber = line;
+
+ return s;
+};
+
+function count_lines( str ) {
+ var n = 0, i = -1;
+ while ( ( i = str.indexOf('\n', i+1) ) !== -1) n++;
+ return n;
+}
+
+// Internal - split source into rough blocks
+Markdown.prototype.split_blocks = function splitBlocks( input, startLine ) {
+ // [\s\S] matches _anything_ (newline or space)
+ var re = /([\s\S]+?)($|\n(?:\s*\n|$)+)/g,
+ blocks = [],
+ m;
+
+ var line_no = 1;
+
+ if ( ( m = /^(\s*\n)/.exec(input) ) != null ) {
+ // skip (but count) leading blank lines
+ line_no += count_lines( m[0] );
+ re.lastIndex = m[0].length;
+ }
+
+ while ( ( m = re.exec(input) ) !== null ) {
+ blocks.push( mk_block( m[1], m[2], line_no ) );
+ line_no += count_lines( m[0] );
+ }
+
+ return blocks;
+};
+
+/**
+ * Markdown#processBlock( block, next ) -> undefined | [ JsonML, ... ]
+ * - block (String): the block to process
+ * - next (Array): the following blocks
+ *
+ * Process `block` and return an array of JsonML nodes representing `block`.
+ *
+ * It does this by asking each block level function in the dialect to process
+ * the block until one can. Succesful handling is indicated by returning an
+ * array (with zero or more JsonML nodes), failure by a false value.
+ *
+ * Blocks handlers are responsible for calling [[Markdown#processInline]]
+ * themselves as appropriate.
+ *
+ * If the blocks were split incorrectly or adjacent blocks need collapsing you
+ * can adjust `next` in place using shift/splice etc.
+ *
+ * If any of this default behaviour is not right for the dialect, you can
+ * define a `__call__` method on the dialect that will get invoked to handle
+ * the block processing.
+ */
+Markdown.prototype.processBlock = function processBlock( block, next ) {
+ var cbs = this.dialect.block,
+ ord = cbs.__order__;
+
+ if ( "__call__" in cbs ) {
+ return cbs.__call__.call(this, block, next);
+ }
+
+ for ( var i = 0; i < ord.length; i++ ) {
+ //D:this.debug( "Testing", ord[i] );
+ var res = cbs[ ord[i] ].call( this, block, next );
+ if ( res ) {
+ //D:this.debug(" matched");
+ if ( !isArray(res) || ( res.length > 0 && !( isArray(res[0]) ) ) )
+ this.debug(ord[i], "didn't return a proper array");
+ //D:this.debug( "" );
+ return res;
+ }
+ }
+
+ // Uhoh! no match! Should we throw an error?
+ return [];
+};
+
+Markdown.prototype.processInline = function processInline( block ) {
+ return this.dialect.inline.__call__.call( this, String( block ) );
+};
+
+/**
+ * Markdown#toTree( source ) -> JsonML
+ * - source (String): markdown source to parse
+ *
+ * Parse `source` into a JsonML tree representing the markdown document.
+ **/
+// custom_tree means set this.tree to `custom_tree` and restore old value on return
+Markdown.prototype.toTree = function toTree( source, custom_root ) {
+ var blocks = source instanceof Array ? source : this.split_blocks( source );
+
+ // Make tree a member variable so its easier to mess with in extensions
+ var old_tree = this.tree;
+ try {
+ this.tree = custom_root || this.tree || [ "markdown" ];
+
+ blocks:
+ while ( blocks.length ) {
+ var b = this.processBlock( blocks.shift(), blocks );
+
+ // Reference blocks and the like won't return any content
+ if ( !b.length ) continue blocks;
+
+ this.tree.push.apply( this.tree, b );
+ }
+ return this.tree;
+ }
+ finally {
+ if ( custom_root ) {
+ this.tree = old_tree;
+ }
+ }
+};
+
+// Noop by default
+Markdown.prototype.debug = function () {
+ var args = Array.prototype.slice.call( arguments);
+ args.unshift(this.debug_indent);
+ if (typeof print !== "undefined")
+ print.apply( print, args );
+ if (typeof console !== "undefined" && typeof console.log !== "undefined")
+ console.log.apply( null, args );
+}
+
+Markdown.prototype.loop_re_over_block = function( re, block, cb ) {
+ // Dont use /g regexps with this
+ var m,
+ b = block.valueOf();
+
+ while ( b.length && (m = re.exec(b) ) != null) {
+ b = b.substr( m[0].length );
+ cb.call(this, m);
+ }
+ return b;
+};
+
+/**
+ * Markdown.dialects
+ *
+ * Namespace of built-in dialects.
+ **/
+Markdown.dialects = {};
+
+/**
+ * Markdown.dialects.Gruber
+ *
+ * The default dialect that follows the rules set out by John Gruber's
+ * markdown.pl as closely as possible. Well actually we follow the behaviour of
+ * that script which in some places is not exactly what the syntax web page
+ * says.
+ **/
+Markdown.dialects.Gruber = {
+ block: {
+ atxHeader: function atxHeader( block, next ) {
+ var m = block.match( /^(#{1,6})\s*(.*?)\s*#*\s*(?:\n|$)/ );
+
+ if ( !m ) return undefined;
+
+ var header = [ "header", { level: m[ 1 ].length } ];
+ Array.prototype.push.apply(header, this.processInline(m[ 2 ]));
+
+ if ( m[0].length < block.length )
+ next.unshift( mk_block( block.substr( m[0].length ), block.trailing, block.lineNumber + 2 ) );
+
+ return [ header ];
+ },
+
+ setextHeader: function setextHeader( block, next ) {
+ var m = block.match( /^(.*)\n([-=])\2\2+(?:\n|$)/ );
+
+ if ( !m ) return undefined;
+
+ var level = ( m[ 2 ] === "=" ) ? 1 : 2;
+ var header = [ "header", { level : level }, m[ 1 ] ];
+
+ if ( m[0].length < block.length )
+ next.unshift( mk_block( block.substr( m[0].length ), block.trailing, block.lineNumber + 2 ) );
+
+ return [ header ];
+ },
+
+ code: function code( block, next ) {
+ // | Foo
+ // |bar
+ // should be a code block followed by a paragraph. Fun
+ //
+ // There might also be adjacent code block to merge.
+
+ var ret = [],
+ re = /^(?: {0,3}\t| {4})(.*)\n?/,
+ lines;
+
+ // 4 spaces + content
+ if ( !block.match( re ) ) return undefined;
+
+ block_search:
+ do {
+ // Now pull out the rest of the lines
+ var b = this.loop_re_over_block(
+ re, block.valueOf(), function( m ) { ret.push( m[1] ); } );
+
+ if (b.length) {
+ // Case alluded to in first comment. push it back on as a new block
+ next.unshift( mk_block(b, block.trailing) );
+ break block_search;
+ }
+ else if (next.length) {
+ // Check the next block - it might be code too
+ if ( !next[0].match( re ) ) break block_search;
+
+ // Pull how how many blanks lines follow - minus two to account for .join
+ ret.push ( block.trailing.replace(/[^\n]/g, '').substring(2) );
+
+ block = next.shift();
+ }
+ else {
+ break block_search;
+ }
+ } while (true);
+
+ return [ [ "code_block", ret.join("\n") ] ];
+ },
+
+ horizRule: function horizRule( block, next ) {
+ // this needs to find any hr in the block to handle abutting blocks
+ var m = block.match( /^(?:([\s\S]*?)\n)?[ \t]*([-_*])(?:[ \t]*\2){2,}[ \t]*(?:\n([\s\S]*))?$/ );
+
+ if ( !m ) {
+ return undefined;
+ }
+
+ var jsonml = [ [ "hr" ] ];
+
+ // if there's a leading abutting block, process it
+ if ( m[ 1 ] ) {
+ jsonml.unshift.apply( jsonml, this.processBlock( m[ 1 ], [] ) );
+ }
+
+ // if there's a trailing abutting block, stick it into next
+ if ( m[ 3 ] ) {
+ next.unshift( mk_block( m[ 3 ] ) );
+ }
+
+ return jsonml;
+ },
+
+ // There are two types of lists. Tight and loose. Tight lists have no whitespace
+ // between the items (and result in text just in the
) and loose lists,
+ // which have an empty line between list items, resulting in (one or more)
+ // paragraphs inside the
.
+ //
+ // There are all sorts weird edge cases about the original markdown.pl's
+ // handling of lists:
+ //
+ // * Nested lists are supposed to be indented by four chars per level. But
+ // if they aren't, you can get a nested list by indenting by less than
+ // four so long as the indent doesn't match an indent of an existing list
+ // item in the 'nest stack'.
+ //
+ // * The type of the list (bullet or number) is controlled just by the
+ // first item at the indent. Subsequent changes are ignored unless they
+ // are for nested lists
+ //
+ lists: (function( ) {
+ // Use a closure to hide a few variables.
+ var any_list = "[*+-]|\\d+\\.",
+ bullet_list = /[*+-]/,
+ number_list = /\d+\./,
+ // Capture leading indent as it matters for determining nested lists.
+ is_list_re = new RegExp( "^( {0,3})(" + any_list + ")[ \t]+" ),
+ indent_re = "(?: {0,3}\\t| {4})";
+
+ // TODO: Cache this regexp for certain depths.
+ // Create a regexp suitable for matching an li for a given stack depth
+ function regex_for_depth( depth ) {
+
+ return new RegExp(
+ // m[1] = indent, m[2] = list_type
+ "(?:^(" + indent_re + "{0," + depth + "} {0,3})(" + any_list + ")\\s+)|" +
+ // m[3] = cont
+ "(^" + indent_re + "{0," + (depth-1) + "}[ ]{0,4})"
+ );
+ }
+ function expand_tab( input ) {
+ return input.replace( / {0,3}\t/g, " " );
+ }
+
+ // Add inline content `inline` to `li`. inline comes from processInline
+ // so is an array of content
+ function add(li, loose, inline, nl) {
+ if (loose) {
+ li.push( [ "para" ].concat(inline) );
+ return;
+ }
+ // Hmmm, should this be any block level element or just paras?
+ var add_to = li[li.length -1] instanceof Array && li[li.length - 1][0] == "para"
+ ? li[li.length -1]
+ : li;
+
+ // If there is already some content in this list, add the new line in
+ if (nl && li.length > 1) inline.unshift(nl);
+
+ for (var i=0; i < inline.length; i++) {
+ var what = inline[i],
+ is_str = typeof what == "string";
+ if (is_str && add_to.length > 1 && typeof add_to[add_to.length-1] == "string" ) {
+ add_to[ add_to.length-1 ] += what;
+ }
+ else {
+ add_to.push( what );
+ }
+ }
+ }
+
+ // contained means have an indent greater than the current one. On
+ // *every* line in the block
+ function get_contained_blocks( depth, blocks ) {
+
+ var re = new RegExp( "^(" + indent_re + "{" + depth + "}.*?\\n?)*$" ),
+ replace = new RegExp("^" + indent_re + "{" + depth + "}", "gm"),
+ ret = [];
+
+ while ( blocks.length > 0 ) {
+ if ( re.exec( blocks[0] ) ) {
+ var b = blocks.shift(),
+ // Now remove that indent
+ x = b.replace( replace, "");
+
+ ret.push( mk_block( x, b.trailing, b.lineNumber ) );
+ }
+ break;
+ }
+ return ret;
+ }
+
+ // passed to stack.forEach to turn list items up the stack into paras
+ function paragraphify(s, i, stack) {
+ var list = s.list;
+ var last_li = list[list.length-1];
+
+ if (last_li[1] instanceof Array && last_li[1][0] == "para") {
+ return;
+ }
+ if (i+1 == stack.length) {
+ // Last stack frame
+ // Keep the same array, but replace the contents
+ last_li.push( ["para"].concat( last_li.splice(1) ) );
+ }
+ else {
+ var sublist = last_li.pop();
+ last_li.push( ["para"].concat( last_li.splice(1) ), sublist );
+ }
+ }
+
+ // The matcher function
+ return function( block, next ) {
+ var m = block.match( is_list_re );
+ if ( !m ) return undefined;
+
+ function make_list( m ) {
+ var list = bullet_list.exec( m[2] )
+ ? ["bulletlist"]
+ : ["numberlist"];
+
+ stack.push( { list: list, indent: m[1] } );
+ return list;
+ }
+
+
+ var stack = [], // Stack of lists for nesting.
+ list = make_list( m ),
+ last_li,
+ loose = false,
+ ret = [ stack[0].list ],
+ i;
+
+ // Loop to search over block looking for inner block elements and loose lists
+ loose_search:
+ while( true ) {
+ // Split into lines preserving new lines at end of line
+ var lines = block.split( /(?=\n)/ );
+
+ // We have to grab all lines for a li and call processInline on them
+ // once as there are some inline things that can span lines.
+ var li_accumulate = "";
+
+ // Loop over the lines in this block looking for tight lists.
+ tight_search:
+ for (var line_no=0; line_no < lines.length; line_no++) {
+ var nl = "",
+ l = lines[line_no].replace(/^\n/, function(n) { nl = n; return ""; });
+
+ // TODO: really should cache this
+ var line_re = regex_for_depth( stack.length );
+
+ m = l.match( line_re );
+ //print( "line:", uneval(l), "\nline match:", uneval(m) );
+
+ // We have a list item
+ if ( m[1] !== undefined ) {
+ // Process the previous list item, if any
+ if ( li_accumulate.length ) {
+ add( last_li, loose, this.processInline( li_accumulate ), nl );
+ // Loose mode will have been dealt with. Reset it
+ loose = false;
+ li_accumulate = "";
+ }
+
+ m[1] = expand_tab( m[1] );
+ var wanted_depth = Math.floor(m[1].length/4)+1;
+ //print( "want:", wanted_depth, "stack:", stack.length);
+ if ( wanted_depth > stack.length ) {
+ // Deep enough for a nested list outright
+ //print ( "new nested list" );
+ list = make_list( m );
+ last_li.push( list );
+ last_li = list[1] = [ "listitem" ];
+ }
+ else {
+ // We aren't deep enough to be strictly a new level. This is
+ // where Md.pl goes nuts. If the indent matches a level in the
+ // stack, put it there, else put it one deeper then the
+ // wanted_depth deserves.
+ var found = false;
+ for (i = 0; i < stack.length; i++) {
+ if ( stack[ i ].indent != m[1] ) continue;
+ list = stack[ i ].list;
+ stack.splice( i+1 );
+ found = true;
+ break;
+ }
+
+ if (!found) {
+ //print("not found. l:", uneval(l));
+ wanted_depth++;
+ if (wanted_depth <= stack.length) {
+ stack.splice(wanted_depth);
+ //print("Desired depth now", wanted_depth, "stack:", stack.length);
+ list = stack[wanted_depth-1].list;
+ //print("list:", uneval(list) );
+ }
+ else {
+ //print ("made new stack for messy indent");
+ list = make_list(m);
+ last_li.push(list);
+ }
+ }
+
+ //print( uneval(list), "last", list === stack[stack.length-1].list );
+ last_li = [ "listitem" ];
+ list.push(last_li);
+ } // end depth of shenegains
+ nl = "";
+ }
+
+ // Add content
+ if (l.length > m[0].length) {
+ li_accumulate += nl + l.substr( m[0].length );
+ }
+ } // tight_search
+
+ if ( li_accumulate.length ) {
+ add( last_li, loose, this.processInline( li_accumulate ), nl );
+ // Loose mode will have been dealt with. Reset it
+ loose = false;
+ li_accumulate = "";
+ }
+
+ // Look at the next block - we might have a loose list. Or an extra
+ // paragraph for the current li
+ var contained = get_contained_blocks( stack.length, next );
+
+ // Deal with code blocks or properly nested lists
+ if (contained.length > 0) {
+ // Make sure all listitems up the stack are paragraphs
+ forEach( stack, paragraphify, this);
+
+ last_li.push.apply( last_li, this.toTree( contained, [] ) );
+ }
+
+ var next_block = next[0] && next[0].valueOf() || "";
+
+ if ( next_block.match(is_list_re) || next_block.match( /^ / ) ) {
+ block = next.shift();
+
+ // Check for an HR following a list: features/lists/hr_abutting
+ var hr = this.dialect.block.horizRule( block, next );
+
+ if (hr) {
+ ret.push.apply(ret, hr);
+ break;
+ }
+
+ // Make sure all listitems up the stack are paragraphs
+ forEach( stack, paragraphify, this);
+
+ loose = true;
+ continue loose_search;
+ }
+ break;
+ } // loose_search
+
+ return ret;
+ };
+ })(),
+
+ blockquote: function blockquote( block, next ) {
+ if ( !block.match( /^>/m ) )
+ return undefined;
+
+ var jsonml = [];
+
+ // separate out the leading abutting block, if any
+ if ( block[ 0 ] != ">" ) {
+ var lines = block.split( /\n/ ),
+ prev = [];
+
+ // keep shifting lines until you find a crotchet
+ while ( lines.length && lines[ 0 ][ 0 ] != ">" ) {
+ prev.push( lines.shift() );
+ }
+
+ // reassemble!
+ block = lines.join( "\n" );
+ jsonml.push.apply( jsonml, this.processBlock( prev.join( "\n" ), [] ) );
+ }
+
+ // if the next block is also a blockquote merge it in
+ while ( next.length && next[ 0 ][ 0 ] == ">" ) {
+ var b = next.shift();
+ block = new String(block + block.trailing + b);
+ block.trailing = b.trailing;
+ }
+
+ // Strip off the leading "> " and re-process as a block.
+ var input = block.replace( /^> ?/gm, '' ),
+ old_tree = this.tree;
+ jsonml.push( this.toTree( input, [ "blockquote" ] ) );
+
+ return jsonml;
+ },
+
+ referenceDefn: function referenceDefn( block, next) {
+ var re = /^\s*\[(.*?)\]:\s*(\S+)(?:\s+(?:(['"])(.*?)\3|\((.*?)\)))?\n?/;
+ // interesting matches are [ , ref_id, url, , title, title ]
+
+ if ( !block.match(re) )
+ return undefined;
+
+ // make an attribute node if it doesn't exist
+ if ( !extract_attr( this.tree ) ) {
+ this.tree.splice( 1, 0, {} );
+ }
+
+ var attrs = extract_attr( this.tree );
+
+ // make a references hash if it doesn't exist
+ if ( attrs.references === undefined ) {
+ attrs.references = {};
+ }
+
+ var b = this.loop_re_over_block(re, block, function( m ) {
+
+ if ( m[2] && m[2][0] == '<' && m[2][m[2].length-1] == '>' )
+ m[2] = m[2].substring( 1, m[2].length - 1 );
+
+ var ref = attrs.references[ m[1].toLowerCase() ] = {
+ href: m[2]
+ };
+
+ if (m[4] !== undefined)
+ ref.title = m[4];
+ else if (m[5] !== undefined)
+ ref.title = m[5];
+
+ } );
+
+ if (b.length)
+ next.unshift( mk_block( b, block.trailing ) );
+
+ return [];
+ },
+
+ para: function para( block, next ) {
+ // everything's a para!
+ return [ ["para"].concat( this.processInline( block ) ) ];
+ }
+ }
+};
+
+Markdown.dialects.Gruber.inline = {
+
+ __oneElement__: function oneElement( text, patterns_or_re, previous_nodes ) {
+ var m,
+ res,
+ lastIndex = 0;
+
+ patterns_or_re = patterns_or_re || this.dialect.inline.__patterns__;
+ var re = new RegExp( "([\\s\\S]*?)(" + (patterns_or_re.source || patterns_or_re) + ")" );
+
+ m = re.exec( text );
+ if (!m) {
+ // Just boring text
+ return [ text.length, text ];
+ }
+ else if ( m[1] ) {
+ // Some un-interesting text matched. Return that first
+ return [ m[1].length, m[1] ];
+ }
+
+ var res;
+ if ( m[2] in this.dialect.inline ) {
+ res = this.dialect.inline[ m[2] ].call(
+ this,
+ text.substr( m.index ), m, previous_nodes || [] );
+ }
+ // Default for now to make dev easier. just slurp special and output it.
+ res = res || [ m[2].length, m[2] ];
+ return res;
+ },
+
+ __call__: function inline( text, patterns ) {
+
+ var out = [],
+ res;
+
+ function add(x) {
+ //D:self.debug(" adding output", uneval(x));
+ if (typeof x == "string" && typeof out[out.length-1] == "string")
+ out[ out.length-1 ] += x;
+ else
+ out.push(x);
+ }
+
+ while ( text.length > 0 ) {
+ res = this.dialect.inline.__oneElement__.call(this, text, patterns, out );
+ text = text.substr( res.shift() );
+ forEach(res, add )
+ }
+
+ return out;
+ },
+
+ // These characters are intersting elsewhere, so have rules for them so that
+ // chunks of plain text blocks don't include them
+ "]": function () {},
+ "}": function () {},
+
+ "\\": function escaped( text ) {
+ // [ length of input processed, node/children to add... ]
+ // Only esacape: \ ` * _ { } [ ] ( ) # * + - . !
+ if ( text.match( /^\\[\\`\*_{}\[\]()#\+.!\-]/ ) )
+ return [ 2, text[1] ];
+ else
+ // Not an esacpe
+ return [ 1, "\\" ];
+ },
+
+ "![": function image( text ) {
+
+ // Unlike images, alt text is plain text only. no other elements are
+ // allowed in there
+
+ // ![Alt text](/path/to/img.jpg "Optional title")
+ // 1 2 3 4 <--- captures
+ var m = text.match( /^!\[(.*?)\][ \t]*\([ \t]*(\S*)(?:[ \t]+(["'])(.*?)\3)?[ \t]*\)/ );
+
+ if ( m ) {
+ if ( m[2] && m[2][0] == '<' && m[2][m[2].length-1] == '>' )
+ m[2] = m[2].substring( 1, m[2].length - 1 );
+
+ m[2] = this.dialect.inline.__call__.call( this, m[2], /\\/ )[0];
+
+ var attrs = { alt: m[1], href: m[2] || "" };
+ if ( m[4] !== undefined)
+ attrs.title = m[4];
+
+ return [ m[0].length, [ "img", attrs ] ];
+ }
+
+ // ![Alt text][id]
+ m = text.match( /^!\[(.*?)\][ \t]*\[(.*?)\]/ );
+
+ if ( m ) {
+ // We can't check if the reference is known here as it likely wont be
+ // found till after. Check it in md tree->hmtl tree conversion
+ return [ m[0].length, [ "img_ref", { alt: m[1], ref: m[2].toLowerCase(), original: m[0] } ] ];
+ }
+
+ // Just consume the '!['
+ return [ 2, "![" ];
+ },
+
+ "[": function link( text ) {
+
+ var orig = String(text);
+ // Inline content is possible inside `link text`
+ var res = Markdown.DialectHelpers.inline_until_char.call( this, text.substr(1), ']' );
+
+ // No closing ']' found. Just consume the [
+ if ( !res ) return [ 1, '[' ];
+
+ var consumed = 1 + res[ 0 ],
+ children = res[ 1 ],
+ link,
+ attrs;
+
+ // At this point the first [...] has been parsed. See what follows to find
+ // out which kind of link we are (reference or direct url)
+ text = text.substr( consumed );
+
+ // [link text](/path/to/img.jpg "Optional title")
+ // 1 2 3 <--- captures
+ // This will capture up to the last paren in the block. We then pull
+ // back based on if there a matching ones in the url
+ // ([here](/url/(test))
+ // The parens have to be balanced
+ var m = text.match( /^\s*\([ \t]*(\S+)(?:[ \t]+(["'])(.*?)\2)?[ \t]*\)/ );
+ if ( m ) {
+ var url = m[1];
+ consumed += m[0].length;
+
+ if ( url && url[0] == '<' && url[url.length-1] == '>' )
+ url = url.substring( 1, url.length - 1 );
+
+ // If there is a title we don't have to worry about parens in the url
+ if ( !m[3] ) {
+ var open_parens = 1; // One open that isn't in the capture
+ for (var len = 0; len < url.length; len++) {
+ switch ( url[len] ) {
+ case '(':
+ open_parens++;
+ break;
+ case ')':
+ if ( --open_parens == 0) {
+ consumed -= url.length - len;
+ url = url.substring(0, len);
+ }
+ break;
+ }
+ }
+ }
+
+ // Process escapes only
+ url = this.dialect.inline.__call__.call( this, url, /\\/ )[0];
+
+ attrs = { href: url || "" };
+ if ( m[3] !== undefined)
+ attrs.title = m[3];
+
+ link = [ "link", attrs ].concat( children );
+ return [ consumed, link ];
+ }
+
+ // [Alt text][id]
+ // [Alt text] [id]
+ m = text.match( /^\s*\[(.*?)\]/ );
+
+ if ( m ) {
+
+ consumed += m[ 0 ].length;
+
+ // [links][] uses links as its reference
+ attrs = { ref: ( m[ 1 ] || String(children) ).toLowerCase(), original: orig.substr( 0, consumed ) };
+
+ link = [ "link_ref", attrs ].concat( children );
+
+ // We can't check if the reference is known here as it likely wont be
+ // found till after. Check it in md tree->hmtl tree conversion.
+ // Store the original so that conversion can revert if the ref isn't found.
+ return [ consumed, link ];
+ }
+
+ // [id]
+ // Only if id is plain (no formatting.)
+ if ( children.length == 1 && typeof children[0] == "string" ) {
+
+ attrs = { ref: children[0].toLowerCase(), original: orig.substr( 0, consumed ) };
+ link = [ "link_ref", attrs, children[0] ];
+ return [ consumed, link ];
+ }
+
+ // Just consume the '['
+ return [ 1, "[" ];
+ },
+
+
+ "<": function autoLink( text ) {
+ var m;
+
+ if ( ( m = text.match( /^<(?:((https?|ftp|mailto):[^>]+)|(.*?@.*?\.[a-zA-Z]+))>/ ) ) != null ) {
+ if ( m[3] ) {
+ return [ m[0].length, [ "link", { href: "mailto:" + m[3] }, m[3] ] ];
+
+ }
+ else if ( m[2] == "mailto" ) {
+ return [ m[0].length, [ "link", { href: m[1] }, m[1].substr("mailto:".length ) ] ];
+ }
+ else
+ return [ m[0].length, [ "link", { href: m[1] }, m[1] ] ];
+ }
+
+ return [ 1, "<" ];
+ },
+
+ "`": function inlineCode( text ) {
+ // Inline code block. as many backticks as you like to start it
+ // Always skip over the opening ticks.
+ var m = text.match( /(`+)(([\s\S]*?)\1)/ );
+
+ if ( m && m[2] )
+ return [ m[1].length + m[2].length, [ "inlinecode", m[3] ] ];
+ else {
+ // TODO: No matching end code found - warn!
+ return [ 1, "`" ];
+ }
+ },
+
+ " \n": function lineBreak( text ) {
+ return [ 3, [ "linebreak" ] ];
+ }
+
+};
+
+// Meta Helper/generator method for em and strong handling
+function strong_em( tag, md ) {
+
+ var state_slot = tag + "_state",
+ other_slot = tag == "strong" ? "em_state" : "strong_state";
+
+ function CloseTag(len) {
+ this.len_after = len;
+ this.name = "close_" + md;
+ }
+
+ return function ( text, orig_match ) {
+
+ if (this[state_slot][0] == md) {
+ // Most recent em is of this type
+ //D:this.debug("closing", md);
+ this[state_slot].shift();
+
+ // "Consume" everything to go back to the recrusion in the else-block below
+ return[ text.length, new CloseTag(text.length-md.length) ];
+ }
+ else {
+ // Store a clone of the em/strong states
+ var other = this[other_slot].slice(),
+ state = this[state_slot].slice();
+
+ this[state_slot].unshift(md);
+
+ //D:this.debug_indent += " ";
+
+ // Recurse
+ var res = this.processInline( text.substr( md.length ) );
+ //D:this.debug_indent = this.debug_indent.substr(2);
+
+ var last = res[res.length - 1];
+
+ //D:this.debug("processInline from", tag + ": ", uneval( res ) );
+
+ var check = this[state_slot].shift();
+ if (last instanceof CloseTag) {
+ res.pop();
+ // We matched! Huzzah.
+ var consumed = text.length - last.len_after;
+ return [ consumed, [ tag ].concat(res) ];
+ }
+ else {
+ // Restore the state of the other kind. We might have mistakenly closed it.
+ this[other_slot] = other;
+ this[state_slot] = state;
+
+ // We can't reuse the processed result as it could have wrong parsing contexts in it.
+ return [ md.length, md ];
+ }
+ }
+ }; // End returned function
+}
+
+Markdown.dialects.Gruber.inline["**"] = strong_em("strong", "**");
+Markdown.dialects.Gruber.inline["__"] = strong_em("strong", "__");
+Markdown.dialects.Gruber.inline["*"] = strong_em("em", "*");
+Markdown.dialects.Gruber.inline["_"] = strong_em("em", "_");
+
+
+// Build default order from insertion order.
+Markdown.buildBlockOrder = function(d) {
+ var ord = [];
+ for ( var i in d ) {
+ if ( i == "__order__" || i == "__call__" ) continue;
+ ord.push( i );
+ }
+ d.__order__ = ord;
+};
+
+// Build patterns for inline matcher
+Markdown.buildInlinePatterns = function(d) {
+ var patterns = [];
+
+ for ( var i in d ) {
+ // __foo__ is reserved and not a pattern
+ if ( i.match( /^__.*__$/) ) continue;
+ var l = i.replace( /([\\.*+?|()\[\]{}])/g, "\\$1" )
+ .replace( /\n/, "\\n" );
+ patterns.push( i.length == 1 ? l : "(?:" + l + ")" );
+ }
+
+ patterns = patterns.join("|");
+ d.__patterns__ = patterns;
+ //print("patterns:", uneval( patterns ) );
+
+ var fn = d.__call__;
+ d.__call__ = function(text, pattern) {
+ if (pattern != undefined) {
+ return fn.call(this, text, pattern);
+ }
+ else
+ {
+ return fn.call(this, text, patterns);
+ }
+ };
+};
+
+Markdown.DialectHelpers = {};
+Markdown.DialectHelpers.inline_until_char = function( text, want ) {
+ var consumed = 0,
+ nodes = [];
+
+ while ( true ) {
+ if ( text[ consumed ] == want ) {
+ // Found the character we were looking for
+ consumed++;
+ return [ consumed, nodes ];
+ }
+
+ if ( consumed >= text.length ) {
+ // No closing char found. Abort.
+ return null;
+ }
+
+ var res = this.dialect.inline.__oneElement__.call(this, text.substr( consumed ) );
+ consumed += res[ 0 ];
+ // Add any returned nodes.
+ nodes.push.apply( nodes, res.slice( 1 ) );
+ }
+}
+
+// Helper function to make sub-classing a dialect easier
+Markdown.subclassDialect = function( d ) {
+ function Block() {}
+ Block.prototype = d.block;
+ function Inline() {}
+ Inline.prototype = d.inline;
+
+ return { block: new Block(), inline: new Inline() };
+};
+
+Markdown.buildBlockOrder ( Markdown.dialects.Gruber.block );
+Markdown.buildInlinePatterns( Markdown.dialects.Gruber.inline );
+
+Markdown.dialects.Maruku = Markdown.subclassDialect( Markdown.dialects.Gruber );
+
+Markdown.dialects.Maruku.processMetaHash = function processMetaHash( meta_string ) {
+ var meta = split_meta_hash( meta_string ),
+ attr = {};
+
+ for ( var i = 0; i < meta.length; ++i ) {
+ // id: #foo
+ if ( /^#/.test( meta[ i ] ) ) {
+ attr.id = meta[ i ].substring( 1 );
+ }
+ // class: .foo
+ else if ( /^\./.test( meta[ i ] ) ) {
+ // if class already exists, append the new one
+ if ( attr['class'] ) {
+ attr['class'] = attr['class'] + meta[ i ].replace( /./, " " );
+ }
+ else {
+ attr['class'] = meta[ i ].substring( 1 );
+ }
+ }
+ // attribute: foo=bar
+ else if ( /\=/.test( meta[ i ] ) ) {
+ var s = meta[ i ].split( /\=/ );
+ attr[ s[ 0 ] ] = s[ 1 ];
+ }
+ }
+
+ return attr;
+}
+
+function split_meta_hash( meta_string ) {
+ var meta = meta_string.split( "" ),
+ parts = [ "" ],
+ in_quotes = false;
+
+ while ( meta.length ) {
+ var letter = meta.shift();
+ switch ( letter ) {
+ case " " :
+ // if we're in a quoted section, keep it
+ if ( in_quotes ) {
+ parts[ parts.length - 1 ] += letter;
+ }
+ // otherwise make a new part
+ else {
+ parts.push( "" );
+ }
+ break;
+ case "'" :
+ case '"' :
+ // reverse the quotes and move straight on
+ in_quotes = !in_quotes;
+ break;
+ case "\\" :
+ // shift off the next letter to be used straight away.
+ // it was escaped so we'll keep it whatever it is
+ letter = meta.shift();
+ default :
+ parts[ parts.length - 1 ] += letter;
+ break;
+ }
+ }
+
+ return parts;
+}
+
+Markdown.dialects.Maruku.block.document_meta = function document_meta( block, next ) {
+ // we're only interested in the first block
+ if ( block.lineNumber > 1 ) return undefined;
+
+ // document_meta blocks consist of one or more lines of `Key: Value\n`
+ if ( ! block.match( /^(?:\w+:.*\n)*\w+:.*$/ ) ) return undefined;
+
+ // make an attribute node if it doesn't exist
+ if ( !extract_attr( this.tree ) ) {
+ this.tree.splice( 1, 0, {} );
+ }
+
+ var pairs = block.split( /\n/ );
+ for ( p in pairs ) {
+ var m = pairs[ p ].match( /(\w+):\s*(.*)$/ ),
+ key = m[ 1 ].toLowerCase(),
+ value = m[ 2 ];
+
+ this.tree[ 1 ][ key ] = value;
+ }
+
+ // document_meta produces no content!
+ return [];
+};
+
+Markdown.dialects.Maruku.block.block_meta = function block_meta( block, next ) {
+ // check if the last line of the block is an meta hash
+ var m = block.match( /(^|\n) {0,3}\{:\s*((?:\\\}|[^\}])*)\s*\}$/ );
+ if ( !m ) return undefined;
+
+ // process the meta hash
+ var attr = this.dialect.processMetaHash( m[ 2 ] );
+
+ var hash;
+
+ // if we matched ^ then we need to apply meta to the previous block
+ if ( m[ 1 ] === "" ) {
+ var node = this.tree[ this.tree.length - 1 ];
+ hash = extract_attr( node );
+
+ // if the node is a string (rather than JsonML), bail
+ if ( typeof node === "string" ) return undefined;
+
+ // create the attribute hash if it doesn't exist
+ if ( !hash ) {
+ hash = {};
+ node.splice( 1, 0, hash );
+ }
+
+ // add the attributes in
+ for ( a in attr ) {
+ hash[ a ] = attr[ a ];
+ }
+
+ // return nothing so the meta hash is removed
+ return [];
+ }
+
+ // pull the meta hash off the block and process what's left
+ var b = block.replace( /\n.*$/, "" ),
+ result = this.processBlock( b, [] );
+
+ // get or make the attributes hash
+ hash = extract_attr( result[ 0 ] );
+ if ( !hash ) {
+ hash = {};
+ result[ 0 ].splice( 1, 0, hash );
+ }
+
+ // attach the attributes to the block
+ for ( a in attr ) {
+ hash[ a ] = attr[ a ];
+ }
+
+ return result;
+};
+
+Markdown.dialects.Maruku.block.definition_list = function definition_list( block, next ) {
+ // one or more terms followed by one or more definitions, in a single block
+ var tight = /^((?:[^\s:].*\n)+):\s+([\s\S]+)$/,
+ list = [ "dl" ],
+ i;
+
+ // see if we're dealing with a tight or loose block
+ if ( ( m = block.match( tight ) ) ) {
+ // pull subsequent tight DL blocks out of `next`
+ var blocks = [ block ];
+ while ( next.length && tight.exec( next[ 0 ] ) ) {
+ blocks.push( next.shift() );
+ }
+
+ for ( var b = 0; b < blocks.length; ++b ) {
+ var m = blocks[ b ].match( tight ),
+ terms = m[ 1 ].replace( /\n$/, "" ).split( /\n/ ),
+ defns = m[ 2 ].split( /\n:\s+/ );
+
+ // print( uneval( m ) );
+
+ for ( i = 0; i < terms.length; ++i ) {
+ list.push( [ "dt", terms[ i ] ] );
+ }
+
+ for ( i = 0; i < defns.length; ++i ) {
+ // run inline processing over the definition
+ list.push( [ "dd" ].concat( this.processInline( defns[ i ].replace( /(\n)\s+/, "$1" ) ) ) );
+ }
+ }
+ }
+ else {
+ return undefined;
+ }
+
+ return [ list ];
+};
+
+Markdown.dialects.Maruku.inline[ "{:" ] = function inline_meta( text, matches, out ) {
+ if ( !out.length ) {
+ return [ 2, "{:" ];
+ }
+
+ // get the preceeding element
+ var before = out[ out.length - 1 ];
+
+ if ( typeof before === "string" ) {
+ return [ 2, "{:" ];
+ }
+
+ // match a meta hash
+ var m = text.match( /^\{:\s*((?:\\\}|[^\}])*)\s*\}/ );
+
+ // no match, false alarm
+ if ( !m ) {
+ return [ 2, "{:" ];
+ }
+
+ // attach the attributes to the preceeding element
+ var meta = this.dialect.processMetaHash( m[ 1 ] ),
+ attr = extract_attr( before );
+
+ if ( !attr ) {
+ attr = {};
+ before.splice( 1, 0, attr );
+ }
+
+ for ( var k in meta ) {
+ attr[ k ] = meta[ k ];
+ }
+
+ // cut out the string and replace it with nothing
+ return [ m[ 0 ].length, "" ];
+};
+
+Markdown.buildBlockOrder ( Markdown.dialects.Maruku.block );
+Markdown.buildInlinePatterns( Markdown.dialects.Maruku.inline );
+
+var isArray = Array.isArray || function(obj) {
+ return Object.prototype.toString.call(obj) == '[object Array]';
+};
+
+var forEach;
+// Don't mess with Array.prototype. Its not friendly
+if ( Array.prototype.forEach ) {
+ forEach = function( arr, cb, thisp ) {
+ return arr.forEach( cb, thisp );
+ };
+}
+else {
+ forEach = function(arr, cb, thisp) {
+ for (var i = 0; i < arr.length; i++) {
+ cb.call(thisp || arr, arr[i], i, arr);
+ }
+ }
+}
+
+function extract_attr( jsonml ) {
+ return isArray(jsonml)
+ && jsonml.length > 1
+ && typeof jsonml[ 1 ] === "object"
+ && !( isArray(jsonml[ 1 ]) )
+ ? jsonml[ 1 ]
+ : undefined;
+}
+
+
+
+/**
+ * renderJsonML( jsonml[, options] ) -> String
+ * - jsonml (Array): JsonML array to render to XML
+ * - options (Object): options
+ *
+ * Converts the given JsonML into well-formed XML.
+ *
+ * The options currently understood are:
+ *
+ * - root (Boolean): wether or not the root node should be included in the
+ * output, or just its children. The default `false` is to not include the
+ * root itself.
+ */
+expose.renderJsonML = function( jsonml, options ) {
+ options = options || {};
+ // include the root element in the rendered output?
+ options.root = options.root || false;
+
+ var content = [];
+
+ if ( options.root ) {
+ content.push( render_tree( jsonml ) );
+ }
+ else {
+ jsonml.shift(); // get rid of the tag
+ if ( jsonml.length && typeof jsonml[ 0 ] === "object" && !( jsonml[ 0 ] instanceof Array ) ) {
+ jsonml.shift(); // get rid of the attributes
+ }
+
+ while ( jsonml.length ) {
+ content.push( render_tree( jsonml.shift() ) );
+ }
+ }
+
+ return content.join( "\n\n" );
+};
+
+function escapeHTML( text ) {
+ return text.replace( /&/g, "&" )
+ .replace( //g, ">" )
+ .replace( /"/g, """ )
+ .replace( /'/g, "'" );
+}
+
+function render_tree( jsonml ) {
+ // basic case
+ if ( typeof jsonml === "string" ) {
+ return escapeHTML( jsonml );
+ }
+
+ var tag = jsonml.shift(),
+ attributes = {},
+ content = [];
+
+ if ( jsonml.length && typeof jsonml[ 0 ] === "object" && !( jsonml[ 0 ] instanceof Array ) ) {
+ attributes = jsonml.shift();
+ }
+
+ while ( jsonml.length ) {
+ content.push( arguments.callee( jsonml.shift() ) );
+ }
+
+ var tag_attrs = "";
+ for ( var a in attributes ) {
+ tag_attrs += " " + a + '="' + escapeHTML( attributes[ a ] ) + '"';
+ }
+
+ // be careful about adding whitespace here for inline elements
+ if ( tag == "img" || tag == "br" || tag == "hr" ) {
+ return "<"+ tag + tag_attrs + "/>";
+ }
+ else {
+ return "<"+ tag + tag_attrs + ">" + content.join( "" ) + "" + tag + ">";
+ }
+}
+
+function convert_tree_to_html( tree, references, options ) {
+ var i;
+ options = options || {};
+
+ // shallow clone
+ var jsonml = tree.slice( 0 );
+
+ if (typeof options.preprocessTreeNode === "function") {
+ jsonml = options.preprocessTreeNode(jsonml, references);
+ }
+
+ // Clone attributes if they exist
+ var attrs = extract_attr( jsonml );
+ if ( attrs ) {
+ jsonml[ 1 ] = {};
+ for ( i in attrs ) {
+ jsonml[ 1 ][ i ] = attrs[ i ];
+ }
+ attrs = jsonml[ 1 ];
+ }
+
+ // basic case
+ if ( typeof jsonml === "string" ) {
+ return jsonml;
+ }
+
+ // convert this node
+ switch ( jsonml[ 0 ] ) {
+ case "header":
+ jsonml[ 0 ] = "h" + jsonml[ 1 ].level;
+ delete jsonml[ 1 ].level;
+ break;
+ case "bulletlist":
+ jsonml[ 0 ] = "ul";
+ break;
+ case "numberlist":
+ jsonml[ 0 ] = "ol";
+ break;
+ case "listitem":
+ jsonml[ 0 ] = "li";
+ break;
+ case "para":
+ jsonml[ 0 ] = "p";
+ break;
+ case "markdown":
+ jsonml[ 0 ] = "html";
+ if ( attrs ) delete attrs.references;
+ break;
+ case "code_block":
+ jsonml[ 0 ] = "pre";
+ i = attrs ? 2 : 1;
+ var code = [ "code" ];
+ code.push.apply( code, jsonml.splice( i ) );
+ jsonml[ i ] = code;
+ break;
+ case "inlinecode":
+ jsonml[ 0 ] = "code";
+ break;
+ case "img":
+ jsonml[ 1 ].src = jsonml[ 1 ].href;
+ delete jsonml[ 1 ].href;
+ break;
+ case "linebreak":
+ jsonml[ 0 ] = "br";
+ break;
+ case "link":
+ jsonml[ 0 ] = "a";
+ break;
+ case "link_ref":
+ jsonml[ 0 ] = "a";
+
+ // grab this ref and clean up the attribute node
+ var ref = references[ attrs.ref ];
+
+ // if the reference exists, make the link
+ if ( ref ) {
+ delete attrs.ref;
+
+ // add in the href and title, if present
+ attrs.href = ref.href;
+ if ( ref.title ) {
+ attrs.title = ref.title;
+ }
+
+ // get rid of the unneeded original text
+ delete attrs.original;
+ }
+ // the reference doesn't exist, so revert to plain text
+ else {
+ return attrs.original;
+ }
+ break;
+ case "img_ref":
+ jsonml[ 0 ] = "img";
+
+ // grab this ref and clean up the attribute node
+ var ref = references[ attrs.ref ];
+
+ // if the reference exists, make the link
+ if ( ref ) {
+ delete attrs.ref;
+
+ // add in the href and title, if present
+ attrs.src = ref.href;
+ if ( ref.title ) {
+ attrs.title = ref.title;
+ }
+
+ // get rid of the unneeded original text
+ delete attrs.original;
+ }
+ // the reference doesn't exist, so revert to plain text
+ else {
+ return attrs.original;
+ }
+ break;
+ }
+
+ // convert all the children
+ i = 1;
+
+ // deal with the attribute node, if it exists
+ if ( attrs ) {
+ // if there are keys, skip over it
+ for ( var key in jsonml[ 1 ] ) {
+ i = 2;
+ }
+ // if there aren't, remove it
+ if ( i === 1 ) {
+ jsonml.splice( i, 1 );
+ }
+ }
+
+ for ( ; i < jsonml.length; ++i ) {
+ jsonml[ i ] = arguments.callee( jsonml[ i ], references, options );
+ }
+
+ return jsonml;
+}
+
+
+// merges adjacent text nodes into a single node
+function merge_text_nodes( jsonml ) {
+ // skip the tag name and attribute hash
+ var i = extract_attr( jsonml ) ? 2 : 1;
+
+ while ( i < jsonml.length ) {
+ // if it's a string check the next item too
+ if ( typeof jsonml[ i ] === "string" ) {
+ if ( i + 1 < jsonml.length && typeof jsonml[ i + 1 ] === "string" ) {
+ // merge the second string into the first and remove it
+ jsonml[ i ] += jsonml.splice( i + 1, 1 )[ 0 ];
+ }
+ else {
+ ++i;
+ }
+ }
+ // if it's not a string recurse
+ else {
+ arguments.callee( jsonml[ i ] );
+ ++i;
+ }
+ }
+}
+
+} )( (function() {
+ if ( typeof exports === "undefined" ) {
+ window.markdown = {};
+ return window.markdown;
+ }
+ else {
+ return exports;
+ }
+} )() );
\ No newline at end of file
diff --git a/milfs/js/to-markdown.js b/milfs/js/to-markdown.js
new file mode 100644
index 0000000..405baf3
--- /dev/null
+++ b/milfs/js/to-markdown.js
@@ -0,0 +1,184 @@
+/*
+ * to-markdown - an HTML to Markdown converter
+ *
+ * Copyright 2011, Dom Christie
+ * Licenced under the MIT licence
+ *
+ */
+
+var toMarkdown = function(string) {
+
+ var ELEMENTS = [
+ {
+ patterns: 'p',
+ replacement: function(str, attrs, innerHTML) {
+ return innerHTML ? '\n\n' + innerHTML + '\n' : '';
+ }
+ },
+ {
+ patterns: 'br',
+ type: 'void',
+ replacement: '\n'
+ },
+ {
+ patterns: 'h([1-6])',
+ replacement: function(str, hLevel, attrs, innerHTML) {
+ var hPrefix = '';
+ for(var i = 0; i < hLevel; i++) {
+ hPrefix += '#';
+ }
+ return '\n\n' + hPrefix + ' ' + innerHTML + '\n';
+ }
+ },
+ {
+ patterns: 'hr',
+ type: 'void',
+ replacement: '\n\n* * *\n'
+ },
+ {
+ patterns: 'a',
+ replacement: function(str, attrs, innerHTML) {
+ var href = attrs.match(attrRegExp('href')),
+ title = attrs.match(attrRegExp('title'));
+ return href ? '[' + innerHTML + ']' + '(' + href[1] + (title && title[1] ? ' "' + title[1] + '"' : '') + ')' : str;
+ }
+ },
+ {
+ patterns: ['b', 'strong'],
+ replacement: function(str, attrs, innerHTML) {
+ return innerHTML ? '**' + innerHTML + '**' : '';
+ }
+ },
+ {
+ patterns: ['i', 'em'],
+ replacement: function(str, attrs, innerHTML) {
+ return innerHTML ? '_' + innerHTML + '_' : '';
+ }
+ },
+ {
+ patterns: 'code',
+ replacement: function(str, attrs, innerHTML) {
+ return innerHTML ? '`' + innerHTML + '`' : '';
+ }
+ },
+ {
+ patterns: 'img',
+ type: 'void',
+ replacement: function(str, attrs, innerHTML) {
+ var src = attrs.match(attrRegExp('src')),
+ alt = attrs.match(attrRegExp('alt')),
+ title = attrs.match(attrRegExp('title'));
+ return '![' + (alt && alt[1] ? alt[1] : '') + ']' + '(' + src[1] + (title && title[1] ? ' "' + title[1] + '"' : '') + ')';
+ }
+ }
+ ];
+
+ for(var i = 0, len = ELEMENTS.length; i < len; i++) {
+ if(typeof ELEMENTS[i].patterns === 'string') {
+ string = replaceEls(string, { tag: ELEMENTS[i].patterns, replacement: ELEMENTS[i].replacement, type: ELEMENTS[i].type });
+ }
+ else {
+ for(var j = 0, pLen = ELEMENTS[i].patterns.length; j < pLen; j++) {
+ string = replaceEls(string, { tag: ELEMENTS[i].patterns[j], replacement: ELEMENTS[i].replacement, type: ELEMENTS[i].type });
+ }
+ }
+ }
+
+ function replaceEls(html, elProperties) {
+ var pattern = elProperties.type === 'void' ? '<' + elProperties.tag + '\\b([^>]*)\\/?>' : '<' + elProperties.tag + '\\b([^>]*)>([\\s\\S]*?)<\\/' + elProperties.tag + '>',
+ regex = new RegExp(pattern, 'gi'),
+ markdown = '';
+ if(typeof elProperties.replacement === 'string') {
+ markdown = html.replace(regex, elProperties.replacement);
+ }
+ else {
+ markdown = html.replace(regex, function(str, p1, p2, p3) {
+ return elProperties.replacement.call(this, str, p1, p2, p3);
+ });
+ }
+ return markdown;
+ }
+
+ function attrRegExp(attr) {
+ return new RegExp(attr + '\\s*=\\s*["\']?([^"\']*)["\']?', 'i');
+ }
+
+ // Pre code blocks
+
+ string = string.replace(/
]*>`([\s\S]*)`<\/pre>/gi, function(str, innerHTML) {
+ innerHTML = innerHTML.replace(/^\t+/g, ' '); // convert tabs to spaces (you know it makes sense)
+ innerHTML = innerHTML.replace(/\n/g, '\n ');
+ return '\n\n ' + innerHTML + '\n';
+ });
+
+ // Lists
+
+ // Escape numbers that could trigger an ol
+ // If there are more than three spaces before the code, it would be in a pre tag
+ // Make sure we are escaping the period not matching any character
+ string = string.replace(/^(\s{0,3}\d+)\. /g, '$1\\. ');
+
+ // Converts lists that have no child lists (of same type) first, then works it's way up
+ var noChildrenRegex = /<(ul|ol)\b[^>]*>(?:(?!