From 36011ea1d2450f761fe6bddca02ff900e652c4b5 Mon Sep 17 00:00:00 2001
From: thepurpleblob ') == 0) return $text;
- return ' '.$text.' ' => md5(' '), '
' => md5(''), ''), ''=> md5('
'.$text.'
'; + $text = preg_replace('{\n{2,}}', "\n\n", $text);
}
+ return $text;
+ }
+
+ function mdwp_strip_p($t) { return preg_replace('{?p>}i', '', $t); }
+
+ function mdwp_hide_tags($text) {
+ global $markdown_hidden_tags;
+ return str_replace(array_keys($markdown_hidden_tags),
+ array_values($markdown_hidden_tags), $text);
+ }
+ function mdwp_show_tags($text) {
+ global $markdown_hidden_tags;
+ return str_replace(array_values($markdown_hidden_tags),
+ array_keys($markdown_hidden_tags), $text);
}
}
-# -- bBlog Plugin Info --------------------------------------------------------
+### bBlog Plugin Info ###
+
function identify_modifier_markdown() {
- global $MarkdownPHPVersion;
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' => $MarkdownPHPVersion,
- '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...'
- );
+ '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 ------------------------------------------------
+
+### 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 anywhere.
+
+### 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='', $noimage='', $strict='') {
- if ($lite == '' && $encode == '') $text = Markdown($text);
- if (function_exists('SmartyPants')) $text = SmartyPants($text);
+ 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; }
}
}
#
-# Globals:
+# Markdown Parser Class
#
-# Regex to match balanced [brackets].
-# Needed to insert a maximum bracked depth while converting to PHP.
-$md_nested_brackets_depth = 6;
-$md_nested_brackets =
- str_repeat('(?>[^\[\]]+|\[', $md_nested_brackets_depth).
- str_repeat('\])*', $md_nested_brackets_depth);
-
-# Table of hash values for escaped characters:
-$md_escape_table = array(
- "\\" => md5("\\"),
- "`" => md5("`"),
- "*" => md5("*"),
- "_" => md5("_"),
- "{" => md5("{"),
- "}" => md5("}"),
- "[" => md5("["),
- "]" => md5("]"),
- "(" => md5("("),
- ")" => md5(")"),
- ">" => md5(">"),
- "#" => md5("#"),
- "+" => md5("+"),
- "-" => md5("-"),
- "." => md5("."),
- "!" => md5("!"),
- ":" => md5(":"),
- "|" => md5("|"),
-);
-# Create an identical table but for escaped characters.
-$md_backslash_escape_table;
-foreach ($md_escape_table as $key => $char)
- $md_backslash_escape_table["\\$key"] = $char;
+class Markdown_Parser {
+ # Regex to match balanced [brackets].
+ # Needed to insert a maximum bracked depth while converting to PHP.
+ var $nested_brackets_depth = 6;
+ var $nested_brackets;
+ # Table of hash values for escaped characters:
+ var $escape_chars = '\`*_{}[]()>#+-.!';
+ var $escape_table = array();
+ var $backslash_escape_table = array();
-function Markdown($text) {
-#
-# Main function. The order in which other subs are called here is
-# essential. Link and image substitutions need to happen before
-# _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the
-# and 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:
+ $block_tags_a = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'.
+ 'script|noscript|form|fieldset|iframe|math|ins|del';
+ $block_tags_b = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'.
+ 'script|noscript|form|fieldset|iframe|math';
+
+ # Regular expression for the content of a block tag.
+ $nested_tags_level = 4;
+ $attr = '
+ (?> # optional tag attributes
+ \s # starts with whitespace
+ (?>
+ [^>"/]+ # text outside quotes
+ |
+ /+(?!>) # slash not followed by ">"
+ |
+ "[^"]*" # text inside double quotes (tolerate ">")
+ |
+ \'[^\']*\' # text inside single quotes (tolerate ">")
+ )*
+ )?
+ ';
+ $content =
+ str_repeat('
+ (?>
+ [^<]+ # content without tag
+ |
+ <\2 # nested opening tag
+ '.$attr.' # attributes
+ (?:
+ />
+ |
+ >', $nested_tags_level). # end of opening tag
+ '.*?'. # last level nested tag content
+ str_repeat('
+ \2\s*> # closing nested tag
+ )
+ |
+ <(?!/\2\s*> # other tags with a different name
+ )
+ )*',
+ $nested_tags_level);
+
+ # First, look for nested blocks, e.g.:
+ # s around
-# "paragraphs" that are wrapped in non-block-level tags, such as anchors,
-# phrase emphasis, and spans. The list of tags we're looking for is
-# hard-coded.
-#
-# This works by calling _HashHTMLBlocks_InMarkdown, which then calls
-# _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1"
-# attribute is found whitin a tag, _HashHTMLBlocks_InHTML calls back
-# _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag.
-# These two functions are calling each other. It's recursive!
-#
- global $block_tags, $context_block_tags, $contain_span_tags,
- $clean_tags, $auto_close_tags;
-
- # Tags that are always treated as block tags:
- $block_tags = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|'.
- 'form|fieldset|iframe|hr|legend';
-
- # Tags treated as block tags only if the opening tag is alone on it's line:
- $context_block_tags = 'script|noscript|math|ins|del';
-
- # Tags where markdown="1" default to span mode:
- $contain_span_tags = 'p|h[1-6]|li|dd|dt|td|th|legend';
-
- # Tags which must not have their contents modified, no matter where
- # they appear:
- $clean_tags = 'script|math';
-
- # Tags that do not need to be closed.
- $auto_close_tags = 'hr|img';
-
- # Regex to match any tag.
- global $tag_match;
- $tag_match =
- '{
- ( # $2: Capture hole tag.
- ? # Any opening or closing tag.
- [\w:$]+ # Tag name.
- \s* # Whitespace.
+ # Special case just for Just type tags
+ #
+ # Strip leading and trailing lines:
+ $text = preg_replace(array('/\A\n+/', '/\n+\z/'), '', $text);
+
+ $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
+
+ #
+ # Wrap tags.
+ #
+ foreach ($grafs as $key => $value) {
+ if (!isset( $this->html_blocks[$value] )) {
+ $value = $this->runSpanGamut($value);
+ $value = preg_replace('/^([ \t]*)/', " ", $value);
+ $value .= " s around
+ # "paragraphs" that are wrapped in non-block-level tags, such as anchors,
+ # phrase emphasis, and spans. The list of tags we're looking for is
+ # hard-coded.
+ #
+ # This works by calling _HashHTMLBlocks_InMarkdown, which then calls
+ # _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1"
+ # attribute is found whitin a tag, _HashHTMLBlocks_InHTML calls back
+ # _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag.
+ # These two functions are calling each other. It's recursive!
+ #
+ #
+ # Call the HTML-in-Markdown hasher.
+ #
+ list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text);
+
+ return $text;
+ }
+ function _hashHTMLBlocks_inMarkdown($text, $indent = 0,
+ $enclosing_tag = '', $span = false)
+ {
+ #
+ # Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags.
+ #
+ # * $indent is the number of space to be ignored when checking for code
+ # blocks. This is important because if we don't take the indent into
+ # account, something like this (which looks right) won't work as expected:
+ #
+ # Just type 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.
+ $clean_key = $value;
+ $block_key = substr($value, 0, 32);
+
+ $is_p = (!isset($this->html_blocks[$block_key]) &&
+ !isset($this->html_cleans[$clean_key]));
+
+ if ($is_p) {
+ $value = " $value tags
-#
- global $md_html_blocks, $md_html_hashes;
+ function doFootnotes($text) {
+ #
+ # Replace footnote references in $text [^id] with a special text-token
+ # which will be can be
+ #
+ $text = preg_replace('{\[\^(.+?)\]}', "a\0fn:\\1\0z", $text);
+ return $text;
+ }
- # Strip leading and trailing lines:
- $text = preg_replace(array('/\A\n+/', '/\n+\z/'), '', $text);
- $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
-
+ function appendFootnotes($text) {
#
- # Wrap tags and unhashify HTML blocks
+ # Append footnote list to text.
#
- foreach ($grafs as $key => $value) {
- $value = trim(_RunSpanGamut($value));
-
- # Check if this should be enclosed in a paragraph.
- # Text equaling to a clean tag hash are not enclosed.
- # Text starting with a block tag hash are not either.
- $clean_key = $value;
- $block_key = substr($value, 0, 32);
-
- $is_p = (!isset($md_html_blocks[$block_key]) &&
- !isset($md_html_hashes[$clean_key]));
+ $text = preg_replace_callback('{a\0fn:(.*?)\0z}',
+ array(&$this, '_appendFootnotes_callback'), $text);
+
+ if (!empty($this->footnotes_ordered)) {
+ $text .= "\n\n";
+ $text .= " $backlink $value tags get encoded.
-#
- # Clear the global hashes. If we don't clear these, you get conflicts
- # from other articles when generating a page which contains more than
- # one article (e.g. an index page that shows the N most recent
- # articles):
- global $md_urls, $md_titles, $md_html_blocks, $md_html_hashes;
- $md_urls = array();
- $md_titles = array();
- $md_html_blocks = array();
- $md_html_hashes = array();
+ # Change to ">" for HTML output.
+ var $empty_element_suffix = MARKDOWN_EMPTY_ELEMENT_SUFFIX;
+ var $tab_width = MARKDOWN_TAB_WIDTH;
- # Standardize line endings:
- # DOS to Unix and Mac to Unix
- $text = str_replace(array("\r\n", "\r"), "\n", $text);
- # Make sure $text ends with a couple of newlines:
- $text .= "\n\n";
+ function Markdown_Parser() {
+ #
+ # Constructor function. Initialize appropriate member variables.
+ #
+ $this->_initDetab();
+
+ $this->nested_brackets =
+ str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth).
+ str_repeat('\])*', $this->nested_brackets_depth);
+
+ # Create an identical table but for escaped characters.
+ foreach (preg_split('/(?!^|$)/', $this->escape_chars) as $char) {
+ $hash = md5($char);
+ $this->escape_table[$char] = $hash;
+ $this->backslash_escape_table["\\$char"] = $hash;
+ }
+
+ # Sort document, block, and span gamut in ascendent priority order.
+ asort($this->document_gamut);
+ asort($this->block_gamut);
+ asort($this->span_gamut);
+ }
- # Convert all tabs to spaces.
- $text = _Detab($text);
- # Turn block-level HTML blocks into hash entries
- $text = _HashHTMLBlocks($text);
+ # Internal hashes used during transformation.
+ var $urls = array();
+ var $titles = array();
+ var $html_blocks = array();
+ var $html_hashes = array(); # Contains both blocks and span hashes.
- # 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 /[ \t]*\n+/ .
- $text = preg_replace('/^[ \t]+$/m', '', $text);
- # Strip link definitions, store in hashes.
- $text = _StripLinkDefinitions($text);
+ function transform($text) {
+ #
+ # Main function. The order in which other subs are called here is
+ # essential. Link and image substitutions need to happen before
+ # _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the
+ # and
tags get encoded.
+ #
+ # Clear the global hashes. If we don't clear these, you get conflicts
+ # from other articles when generating a page which contains more than
+ # one article (e.g. an index page that shows the N most recent
+ # articles):
+ $this->urls = array();
+ $this->titles = array();
+ $this->html_blocks = array();
+ $this->html_hashes = array();
+
+ # Standardize line endings:
+ # DOS to Unix and Mac to Unix
+ $text = str_replace(array("\r\n", "\r"), "\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 /[ \t]*\n+/ .
+ $text = preg_replace('/^[ \t]+$/m', '', $text);
+
+ # Run document gamut methods.
+ foreach ($this->document_gamut as $method => $priority) {
+ $text = $this->$method($text);
+ }
- $text = _RunBlockGamut($text, FALSE);
+ return $text . "\n";
+ }
+
+ var $document_gamut = array(
+ # Strip link definitions, store in hashes.
+ "stripLinkDefinitions" => 20,
+
+ "runBasicBlockGamut" => 30,
+ "unescapeSpecialChars" => 90,
+ );
- $text = _UnescapeSpecialChars($text);
- return $text . "\n";
-}
+ 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
+ [ \t]*
+ \n? # maybe *one* newline
+ [ \t]*
+ (\S+?)>? # url = $2
+ [ \t]*
+ \n? # maybe one newline
+ [ \t]*
+ (?:
+ (?<=\s) # lookbehind for whitespace
+ ["(]
+ (.*?) # title = $3
+ [")]
+ [ \t]*
+ )? # title is optional
+ (?:\n+|\Z)
+ }xm',
+ array(&$this, '_stripLinkDefinitions_callback'),
+ $text);
+ return $text;
+ }
+ function _stripLinkDefinitions_callback($matches) {
+ $link_id = strtolower($matches[1]);
+ $this->urls[$link_id] = $this->encodeAmpsAndAngles($matches[2]);
+ if (isset($matches[3]))
+ $this->titles[$link_id] = str_replace('"', '"', $matches[3]);
+ return ''; # String that will replace the block
+ }
-function _StripLinkDefinitions($text) {
-#
-# Strips link definitions from text, stores the URLs and titles in
-# hash references.
-#
- global $md_tab_width;
- $less_than_tab = $md_tab_width - 1;
-
- # Link defs are in the form: ^[id]: url "optional title"
- $text = preg_replace_callback('{
- ^[ ]{0,'.$less_than_tab.'}\[(.+)\]: # id = $1
- [ \t]*
- \n? # maybe *one* newline
- [ \t]*
- (\S+?)>? # url = $2
- [ \t]*
- \n? # maybe one newline
- [ \t]*
- (?:
- (?<=\s) # lookbehind for whitespace
- ["(]
- (.+?) # title = $3
- [")]
- [ \t]*
- )? # title is optional
- (?:\n+|\Z)
- }xm',
- '_StripLinkDefinitions_callback',
- $text);
- return $text;
-}
-function _StripLinkDefinitions_callback($matches) {
- global $md_urls, $md_titles;
- $link_id = strtolower($matches[1]);
- $md_urls[$link_id] = _EncodeAmpsAndAngles($matches[2]);
- if (isset($matches[3]))
- $md_titles[$link_id] = str_replace('"', '"', $matches[3]);
- return ''; # String that will replace the block
-}
+ function hashHTMLBlocks($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
. It was easier to make a special case than
+ # to make the other regex more complicated.
+ $text = preg_replace_callback('{
(?:
- ".*?" | # Double quotes (can contain `>`)
- \'.*?\' | # Single quotes (can contain `>`)
- .+? # Anything but quotes and `>`.
- )*?
- > # End of tag.
- |
- # HTML Comment
- |
- <\? .*? \?> # Processing instruction
- |
- # CData Block
- )
- }xs';
-
+ (?<=\n\n) # Starting after a blank line
+ | # or
+ \A\n? # the beginning of the doc
+ )
+ ( # save in $1
+ [ ]{0,'.$less_than_tab.'}
+ <(hr) # start tag = $2
+ \b # word break
+ ([^<>])*? #
+ /?> # the matching end tag
+ [ \t]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+ )
+ }x',
+ array(&$this, '_hashHTMLBlocks_callback'),
+ $text);
+
+ # Special case for standalone HTML comments:
+ $text = preg_replace_callback('{
+ (?:
+ (?<=\n\n) # Starting after a blank line
+ | # or
+ \A\n? # the beginning of the doc
+ )
+ ( # save in $1
+ [ ]{0,'.$less_than_tab.'}
+ (?s:
+
+ )
+ [ \t]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+ )
+ }x',
+ array(&$this, '_hashHTMLBlocks_callback'),
+ $text);
+
+ # PHP and ASP-style processor instructions ( and <%)
+ $text = preg_replace_callback('{
+ (?:
+ (?<=\n\n) # Starting after a blank line
+ | # or
+ \A\n? # the beginning of the doc
+ )
+ ( # save in $1
+ [ ]{0,'.$less_than_tab.'}
+ (?s:
+ <([?%]) # $2
+ .*?
+ \2>
+ )
+ [ \t]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+ )
+ }x',
+ 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 hashBlock($text) {
#
- # Call the HTML-in-Markdown hasher.
+ # Called whenever a tag must be hashed when a function insert a block-level
+ # tag in $text, it pass through this function and is automaticaly escaped,
+ # which remove the need to call _HashHTMLBlocks at every step.
#
- list($text, ) = _HashHTMLBlocks_InMarkdown($text);
-
- return $text;
-}
-function _HashHTMLBlocks_InMarkdown($text, $indent = 0,
- $enclosing_tag = '', $md_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:
-#
-#
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.
+ #
+ "escapeSpecialCharsWithinTagAttributes" => -20,
+ "doCodeSpans" => -10,
+ "encodeBackslashEscapes" => -5,
+
+ # Process anchor and image tags. Images must come first,
+ # because ![foo][f] looks like an anchor.
+ "doImages" => 10,
+ "doAnchors" => 20,
- #
- # Check for: Tag inside code block or span
- #
- if (# Find current paragraph
- preg_match('/(?>^\n?|\n\n)((?>.\n?)+?)$/', $parsed, $matches) &&
- (
- # Then match in it either a code block...
- preg_match('/^ {'.($indent+4).'}.*(?>\n {'.($indent+4).'}.*)*'.
- '(?!\n)$/', $matches[1], $x) ||
- # ...or unbalenced code span markers. (the regex matches balenced)
- !preg_match('/^(?>[^`]+|(`+)(?>[^`]+|(?!\1[^`])`)*?\1(?!`))*$/s',
- $matches[1])
- ))
- {
- # Tag is in code block or span and may not be a tag at all. So we
- # simply skip the first char (should be a `<`).
- $parsed .= $tag{0};
- $text = substr($tag, 1) . $text; # Put back $tag minus first char.
+ # Make links out of things like `
empty_element_suffix\n");
+ return preg_replace('/ {2,}\n/', $br_tag, $text);
+ }
+
+
+ function escapeSpecialCharsWithinTagAttributes($text) {
+ #
+ # Within tags -- meaning between < and > -- encode [\ ` * _] so they
+ # don't conflict with their use in Markdown for code, italics and strong.
+ # We're replacing each such character with its corresponding MD5 checksum
+ # value; this is likely overkill, but it should prevent us from colliding
+ # with the escape values by accident.
+ #
+ $tokens = $this->tokenizeHTML($text);
+ $text = ''; # rebuild $text from the tokens
+
+ foreach ($tokens as $cur_token) {
+ if ($cur_token[0] == 'tag') {
+ $cur_token[1] = str_replace('\\', $this->escape_table['\\'], $cur_token[1]);
+ $cur_token[1] = str_replace(array('`'), $this->escape_table['`'], $cur_token[1]);
+ $cur_token[1] = str_replace('*', $this->escape_table['*'], $cur_token[1]);
+ $cur_token[1] = str_replace('_', $this->escape_table['_'], $cur_token[1]);
+ }
+ $text .= $cur_token[1];
}
+ return $text;
+ }
+
+
+ function doAnchors($text) {
+ #
+ # Turn Markdown link shortcuts into XHTML tags.
+ #
#
- # Check for: Opening Block level tag or
- # Opening Content Block tag (like ins and del)
- # used as a block tag (tag is alone on it's line).
+ # First, handle reference-style links: [link text] [id]
#
- else if (preg_match("{^<(?:$block_tags)\b}", $tag) ||
- ( preg_match("{^<(?:$context_block_tags)\b}", $tag) &&
- preg_match($newline_match_before, $parsed) &&
- preg_match($newline_match_after, $text) )
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ \[
+ ('.$this->nested_brackets.') # link text = $2
+ \]
+
+ [ ]? # one optional space
+ (?:\n[ ]*)? # one optional newline followed by spaces
+
+ \[
+ (.*?) # id = $3
+ \]
)
- {
- # Need to parse tag and following text using the HTML parser.
- list($block_text, $text) =
- _HashHTMLBlocks_InHTML($tag . $text,
- "_HashHTMLBlocks_HashBlock", TRUE);
-
- # Make sure it stays outside of any paragraph by adding newlines.
- $parsed .= "\n\n$block_text\n\n";
- }
+ }xs',
+ array(&$this, '_doAnchors_reference_callback'), $text);
+
#
- # Check for: Clean tag (like script, math)
- # HTML Comments, processing instructions.
+ # Next, inline-style links: [link text](url "optional title")
#
- else if (preg_match("{^<(?:$clean_tags)\b}", $tag) ||
- $tag{1} == '!' || $tag{1} == '?')
- {
- # Need to parse tag and following text using the HTML parser.
- # (don't check for markdown attribute)
- list($block_text, $text) =
- _HashHTMLBlocks_InHTML($tag . $text,
- "_HashHTMLBlocks_HashClean", FALSE);
-
- $parsed .= $block_text;
- }
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ \[
+ ('.$this->nested_brackets.') # link text = $2
+ \]
+ \( # literal paren
+ [ \t]*
+ (.*?)>? # href = $3
+ [ \t]*
+ ( # $4
+ ([\'"]) # quote char = $5
+ (.*?) # Title = $6
+ \5 # matching quote
+ [ \t]* # ignore any spaces/tabs between closing quote and )
+ )? # title is optional
+ \)
+ )
+ }xs',
+ array(&$this, '_DoAnchors_inline_callback'), $text);
+
#
- # Check for: Tag with same name as enclosing tag.
+ # Last, handle reference-style shortcuts: [link text]
+ # These must come last in case you've also got [link test][1]
+ # or [link test](/foo)
#
- else if ($enclosing_tag !== '' &&
- # Same name as enclosing tag.
- preg_match("{^?(?:$enclosing_tag)\b}", $tag))
- {
- #
- # Increase/decrease nested tag count.
- #
- if ($tag{1} == '/') $depth--;
- else if ($tag{strlen($tag)-2} != '/') $depth++;
+// $text = preg_replace_callback('{
+// ( # wrap whole match in $1
+// \[
+// ([^\[\]]+) # link text = $2; can\'t contain [ or ]
+// \]
+// )
+// }xs',
+// array(&$this, '_doAnchors_reference_callback'), $text);
+
+ 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 ($depth < 0) {
- #
- # Going out of parent element. Clean up and break so we
- # return to the calling function.
- #
- $text = $tag . $text;
- break;
- }
+ if (isset($this->urls[$link_id])) {
+ $url = $this->urls[$link_id];
+ $url = $this->encodeAmpsAndAngles($url);
- $parsed .= $tag;
+ $result = "titles[$link_id] ) ) {
+ $title = $this->titles[$link_id];
+ $title = $this->encodeAmpsAndAngles($title);
+ $result .= " title=\"$title\"";
+ }
+
+ $link_text = $this->runSpanGamut($link_text);
+ $result .= ">$link_text";
+ $result = $this->hashSpan($result);
}
else {
- $parsed .= $tag;
+ $result = $whole_match;
}
- } while ($depth >= 0);
-
- return array($parsed, $text);
-}
-function _HashHTMLBlocks_InHTML($text, $hash_function, $md_attr) {
-#
-# Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags.
-#
-# * Calls $hash_function 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 )
-#
- global $auto_close_tags, $contain_span_tags, $tag_match;
-
- if ($text === '') return array('', '');
-
- # Regex to match `markdown` attribute inside of a tag.
- $markdown_attr_match = '
- {
- \s* # Eat whitespace before the `markdown` attribute
- markdown
- \s*=\s*
- (["\']) # $1: quote delimiter
- (.*?) # $2: attribute value
- \1 # matching delimiter
- }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.
+ return $result;
+ }
+ function _doAnchors_inline_callback($matches) {
+ $whole_match = $matches[1];
+ $link_text = $this->runSpanGamut($matches[2]);
+ $url = $matches[3];
+ $title =& $matches[6];
+
+ $url = $this->encodeAmpsAndAngles($url);
- #
- # Get the name of the starting tag.
- #
- if (preg_match("/^<([\w:$]*)\b/", $text, $matches))
- $base_tag_name = $matches[1];
+ $result = "encodeAmpsAndAngles($title);
+ $result .= " title=\"$title\"";
+ }
+
+ $link_text = $this->runSpanGamut($link_text);
+ $result .= ">$link_text";
+ return $this->hashSpan($result);
+ }
+
+
+ function doImages($text) {
#
- # Loop through every tag until we find the corresponding closing tag.
+ # Turn Markdown image shortcuts into tags.
#
- 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.
+ # First, handle reference-style labeled images: ![alt text][id]
#
- $parts = preg_split($tag_match, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
-
- if (count($parts) < 3) {
- #
- # End of $text reached with unbalenced tag(s).
- # In that case, we return original text unchanged and pass the
- # first character as filtered to prevent an infinite loop in the
- # parent function.
- #
- return array($original_text{0}, substr($original_text, 1));
- }
-
- $block_text .= $parts[0]; # Text before current tag.
- $tag = $parts[1]; # Tag to handle.
- $text = $parts[2]; # Remaining text after current tag.
-
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ !\[
+ ('.$this->nested_brackets.') # alt text = $2
+ \]
+
+ [ ]? # one optional space
+ (?:\n[ ]*)? # one optional newline followed by spaces
+
+ \[
+ (.*?) # id = $3
+ \]
+
+ )
+ }xs',
+ array(&$this, '_doImages_reference_callback'), $text);
+
#
- # Check for: Auto-close tag (like
)
- # Comments and Processing Instructions.
+ # Next, handle inline images: 
+ # Don't forget: encode * and _
#
- if (preg_match("{^?(?:$auto_close_tags)\b}", $tag) ||
- $tag{1} == '!' || $tag{1} == '?')
- {
- # Just add the tag to the block as if it was text.
- $block_text .= $tag;
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ !\[
+ ('.$this->nested_brackets.') # alt text = $2
+ \]
+ \s? # One optional whitespace character
+ \( # literal paren
+ [ \t]*
+ (\S+?)>? # src url = $3
+ [ \t]*
+ ( # $4
+ ([\'"]) # quote char = $5
+ (.*?) # title = $6
+ \5 # matching quote
+ [ \t]*
+ )? # 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][].
}
- else {
- #
- # Increase/decrease nested tag count. Only do so if
- # the tag's name match base tag's.
- #
- if (preg_match("{^?$base_tag_name\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_match, $tag, $attr_matches) &&
- preg_match('/^(?:1|block|span)$/', $attr_matches[2]))
- {
- # Remove `markdown` attribute from opening tag.
- $tag = preg_replace($markdown_attr_match, '', $tag);
-
- # Check if text inside this tag must be parsed in span mode.
- $md_mode = $attr_matches[2];
- $span_mode = $md_mode == 'span' || $md_mode != 'block' &&
- preg_match("{^<(?:$contain_span_tags)\b}", $tag);
-
- # Calculate indent before tag.
- preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches);
- $indent = strlen($matches[1]);
-
- # End preceding block with this tag.
- $block_text .= $tag;
- $parsed .= $hash_function($block_text, $span_mode);
-
- # Get enclosing tag name for the ParseMarkdown function.
- preg_match('/^<([\w:$]*)\b/', $tag, $matches);
- $tag_name = $matches[1];
-
- # Parse the content using the HTML-in-Markdown parser.
- list ($block_text, $text)
- = _HashHTMLBlocks_InMarkdown($text, $indent,
- $tag_name, $span_mode);
-
- # Outdent markdown text.
- if ($indent > 0) {
- $block_text = preg_replace("/^[ ]{1,$indent}/m", "",
- $block_text);
- }
-
- # Append tag content to parsed text.
- if (!$span_mode) $parsed .= "\n\n$block_text\n\n";
- else $parsed .= "$block_text";
-
- # Start over a new block.
- $block_text = "";
+
+ $alt_text = str_replace('"', '"', $alt_text);
+ if (isset($this->urls[$link_id])) {
+ $url = $this->urls[$link_id];
+ $result = "titles[$link_id])) {
+ $title = $this->titles[$link_id];
+ $result .= " title=\"$title\"";
}
- else $block_text .= $tag;
+ $result .= $this->empty_element_suffix;
+ $result = $this->hashSpan($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];
+ $title =& $matches[6];
+
+ $alt_text = str_replace('"', '"', $alt_text);
+ $result = "
empty_element_suffix;
+
+ return $this->hashSpan($result);
+ }
+
+
+ function doHeaders($text) {
+ # Setext-style headers:
+ # Header 1
+ # ========
+ #
+ # Header 2
+ # --------
+ #
+ $text = preg_replace_callback('{ ^(.+)[ \t]*\n=+[ \t]*\n+ }mx',
+ array(&$this, '_doHeaders_callback_setext_h1'), $text);
+ $text = preg_replace_callback('{ ^(.+)[ \t]*\n-+[ \t]*\n+ }mx',
+ array(&$this, '_doHeaders_callback_setext_h2'), $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
+ [ \t]*
+ (.+?) # $2 = Header text
+ [ \t]*
+ \#* # optional closing #\'s (not counted)
+ \n+
+ }xm',
+ array(&$this, '_doHeaders_callback_atx'), $text);
+
+ return $text;
+ }
+ function _doHeaders_callback_setext_h1($matches) {
+ $block = "
".$this->runSpanGamut($matches[1])."
";
+ return "\n" . $this->hashBlock($block) . "\n\n";
+ }
+ function _doHeaders_callback_setext_h2($matches) {
+ $block = "".$this->runSpanGamut($matches[1])."
";
+ return "\n" . $this->hashBlock($block) . "\n\n";
+ }
+ function _doHeaders_callback_atx($matches) {
+ $level = strlen($matches[1]);
+ $block = "` blocks.
+ #
+ $text = preg_replace_callback('{
+ (?:\n\n|\A)
+ ( # $1 = the code block -- one or more lines, starting with a space/tab
+ (?:
+ (?:[ ]{'.$this->tab_width.'} | \t) # 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];
-function _RunBlockGamut($text, $hash_html_blocks = TRUE) {
-#
-# These are all the transformations that form block-level
-# tags like paragraphs, headers, and list items.
-#
- if ($hash_html_blocks) {
- # 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
- # a list item and could have been indented. Indented blocks would have
- # been seen as a code block in previous pass of _HashHTMLBlocks.
- $text = _HashHTMLBlocks($text);
- }
-
- $text = _DoHeaders($text);
- $text = _DoTables($text);
-
- # Do Horizontal Rules:
- global $md_empty_element_suffix;
- $text = preg_replace(
- array('{^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$}emx',
- '{^[ ]{0,2}([ ]? -[ ]?){3,}[ \t]*$}emx',
- '{^[ ]{0,2}([ ]? _[ ]?){3,}[ \t]*$}emx'),
- "_HashBlock('\n
encodeCode($this->outdent($codeblock));
+ // $codeblock = $this->detab($codeblock);
+ # trim leading newlines and trailing whitespace
+ $codeblock = preg_replace(array('/\A\n+/', '/\n+\z/'), '', $codeblock);
+ $result = "\n\n".$this->hashBlock("
")."\n\n";
+
+ return $result;
+ }
-function _RunSpanGamut($text) {
-#
-# These are all the transformations that occur *within* block-level
-# tags like paragraphs, headers, and list items.
-#
- global $md_empty_element_suffix;
- $text = _DoCodeSpans($text);
+ function doCodeSpans($text) {
+ #
+ # * Backtick quotes are used for " . $codeblock . "\n
spans.
+ #
+ # * You can use multiple backticks as the delimiters if you want to
+ # include literal backticks in the code span. So, this input:
+ #
+ # Just type ``foo `bar` baz`` at the prompt.
+ #
+ # Will translate to:
+ #
+ #
foo `bar` baz
at the prompt.`bar`
...
+ #
+ $text = preg_replace_callback('@
+ (?encodeCode($c);
+ return $this->hashSpan("$c
");
+ }
- $text = _EscapeSpecialChars($text);
- # Process anchor and image tags. Images must come first,
- # because ![foo][f] looks like an anchor.
- $text = _DoImages($text);
- $text = _DoAnchors($text);
+ function encodeCode($_) {
+ #
+ # Encode/escape certain characters inside Markdown code runs.
+ # The point is that in code, these characters are literals,
+ # and lose their special Markdown meanings.
+ #
+ # Encode all ampersands; HTML entities are not
+ # entities within a Markdown code span.
+ $_ = str_replace('&', '&', $_);
- # Make links out of things like `
escape_table),
+// array_values($this->escape_table), $_);
- return $text;
-}
+ return $_;
+ }
-function _EscapeSpecialChars($text) {
- global $md_escape_table;
- $tokens = _TokenizeHTML($text);
-
- $text = ''; # rebuild $text from the tokens
-# $in_pre = 0; # Keep track of when we're inside or
tags.
-# $tags_to_skip = "!<(/?)(?:pre|code|kbd|script|math)[\s>]!";
-
- foreach ($tokens as $cur_token) {
- if ($cur_token[0] == 'tag') {
- # Within tags, encode * and _ so they don't conflict
- # with their use in Markdown for italics and strong.
- # We're replacing each such character with its
- # corresponding MD5 checksum value; this is likely
- # overkill, but it should prevent us from colliding
- # with the escape values by accident.
- $cur_token[1] = str_replace(array('*', '_'),
- array($md_escape_table['*'], $md_escape_table['_']),
- $cur_token[1]);
- $text .= $cur_token[1];
- } else {
- $t = $cur_token[1];
- $t = _EncodeBackslashEscapes($t);
- $text .= $t;
- }
+ function doItalicsAndBold($text) {
+ # must go first:
+ $text = preg_replace_callback('{
+ ( # $1: Marker
+ (?:
+ $text = preg_replace_callback(
+ '{ ( (?runSpanGamut($text);
+ return $this->hashSpan("$text");
+ }
+ function _doItalicAndBold_strong_callback($matches) {
+ $text = $matches[2];
+ $text = $this->runSpanGamut($text);
+ return $this->hashSpan("$text");
}
- return $text;
-}
-function _DoAnchors($text) {
-#
-# Turn Markdown link shortcuts into XHTML tags.
-#
- global $md_nested_brackets;
- #
- # First, handle reference-style links: [link text] [id]
- #
- $text = preg_replace_callback("{
- ( # wrap whole match in $1
- \\[
- ($md_nested_brackets) # link text = $2
- \\]
-
- [ ]? # one optional space
- (?:\\n[ ]*)? # one optional newline followed by spaces
-
- \\[
- (.*?) # id = $3
- \\]
- )
- }xs",
- '_DoAnchors_reference_callback', $text);
-
- #
- # Next, inline-style links: [link text](url "optional title")
- #
- $text = preg_replace_callback("{
- ( # wrap whole match in $1
- \\[
- ($md_nested_brackets) # link text = $2
- \\]
- \\( # literal paren
- [ \\t]*
- (.*?)>? # href = $3
- [ \\t]*
- ( # $4
- (['\"]) # quote char = $5
- (.*?) # Title = $6
- \\5 # matching quote
- )? # title is optional
- \\)
- )
- }xs",
- '_DoAnchors_inline_callback', $text);
-
- return $text;
-}
-function _DoAnchors_reference_callback($matches) {
- global $md_urls, $md_titles, $md_escape_table;
- $whole_match = $matches[1];
- $link_text = $matches[2];
- $link_id = strtolower($matches[3]);
-
- if ($link_id == "") {
- $link_id = strtolower($link_text); # for shortcut links like [this][].
- }
-
- if (isset($md_urls[$link_id])) {
- $url = $md_urls[$link_id];
- # We've got to encode these to avoid conflicting with italics/bold.
- $url = str_replace(array('*', '_'),
- array($md_escape_table['*'], $md_escape_table['_']),
- $url);
- $result = "[ \t]? # ">" 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(array('/^[ \t]*>[ \t]?/m', '/^[ \t]+$/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*
";
+
+ #
+ # Unhashify HTML blocks
+ #
+ foreach ($grafs as $key => $graf) {
+ # Modify elements of @grafs in-place...
+ if (isset($this->html_blocks[$graf])) {
+ $block = $this->html_blocks[$graf];
+ $graf = $block;
+// if (preg_match('{
+// \A
+// ( # $1 = .+?
)}sx',
+ array(&$this, '_DoBlockQuotes_callback2'), $bq);
+
+ return "\n". $this->hashBlock("\n$bq\n
")."\n\n";
+ }
+ function _doBlockQuotes_callback2($matches) {
+ $pre = $matches[1];
+ $pre = preg_replace('/^ /m', '', $pre);
+ return $pre;
+ }
+
+
+ function formParagraphs($text) {
+ #
+ # Params:
+ # $text - string to process with html tags.
-#
- global $md_nested_brackets;
+ # Email addresses:
+ $text = preg_replace_callback('{
+ <
+ (?:mailto:)?
+ (
+ [-.\w\x80-\xFF]+
+ \@
+ [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+
+ )
+ >
+ }xi',
+ array(&$this, '_doAutoLinks_email_callback'), $text);
+
+ return $text;
+ }
+ function _doAutoLinks_url_callback($matches) {
+ $url = $this->encodeAmpsAndAngles($matches[1]);
+ $link = "$url";
+ return $this->hashSpan($link);
+ }
+ function _doAutoLinks_email_callback($matches) {
+ $address = $matches[1];
+ $address = $this->unescapeSpecialChars($address);
+ $link = $this->encodeEmailAddress($address);
+ return $this->hashSpan($link);
+ }
+
+
+ function encodeEmailAddress($addr) {
+ #
+ # Input: an email address, e.g. "foo@example.com"
+ #
+ # Output: the email address as a mailto link, with each character
+ # of the address encoded as either a decimal or hex entity, in
+ # the hopes of foiling most address harvesting spam bots. E.g.:
+ #
+ #
+ #
+ # 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 unescapeSpecialChars($text) {
#
- # First, handle reference-style labeled images: ![alt text][id]
+ # Swap back in all the special characters we've hidden.
#
- $text = preg_replace_callback('{
- ( # wrap whole match in $1
- !\[
- ('.$md_nested_brackets.') # alt text = $2
- \]
+ return str_replace(array_values($this->escape_table),
+ array_keys($this->escape_table), $text);
+ }
+
- [ ]? # one optional space
- (?:\n[ ]*)? # one optional newline followed by spaces
+ function tokenizeHTML($str) {
+ #
+ # Parameter: String containing HTML + Markdown markup.
+ # Returns: An array of the tokens comprising the input
+ # string. Each token is either a tag or a run of text
+ # between tags. Each element of the array is a
+ # two-element array; the first is either 'tag' or 'text';
+ # the second is the actual value.
+ # Note: Markdown code spans are taken into account: no tag token is
+ # generated within a code span.
+ #
+ $tokens = array();
- \[
- (.*?) # id = $3
- \]
+ while ($str != "") {
+ #
+ # Each loop iteration seach for either the next tag or the next
+ # openning code span marker. If a code span marker is found, the
+ # code span is extracted in entierty and will result in an extra
+ # text token.
+ #
+ $parts = preg_split('{
+ (
+ (? # comment
+ |
+ <\?.*?\?> | <%.*?%> # processing instruction
+ |
+ <[/!$]?[-a-zA-Z0-9:]+ # regular tags
+ (?:
+ \s
+ (?>[^"\'>]+|"[^"]*"|\'[^\']*\')*
+ )?
+ >
+ )
+ }xs', $str, 2, PREG_SPLIT_DELIM_CAPTURE);
+
+ # Create token from text preceding tag.
+ if ($parts[0] != "") {
+ $tokens[] = array('text', $parts[0]);
+ }
+
+ # Check if we reach the end.
+ if (count($parts) < 3) {
+ break;
+ }
+
+ # Create token from tag or code span.
+ if ($parts[1]{0} == "`") {
+ $tokens[] = array('text', $parts[1]);
+ $str = $parts[2];
+
+ # Skip the whole code span, pass as text token.
+ if (preg_match('/^(.*(?tab_width})/m", "", $text);
+ }
- $text = preg_replace_callback('{
- ( # wrap whole match in $1
- !\[
- ('.$md_nested_brackets.') # alt text = $2
- \]
- \( # literal paren
- [ \t]*
- (\S+?)>? # src url = $3
- [ \t]*
- ( # $4
- ([\'"]) # quote char = $5
- (.*?) # title = $6
- \5 # matching quote
- [ \t]*
- )? # title is optional
- \)
- )
- }xs',
- '_DoImages_inline_callback', $text);
- return $text;
-}
-function _DoImages_reference_callback($matches) {
- global $md_urls, $md_titles, $md_empty_element_suffix, $md_escape_table;
- $whole_match = $matches[1];
- $alt_text = $matches[2];
- $link_id = strtolower($matches[3]);
-
- if ($link_id == "") {
- $link_id = strtolower($alt_text); # for shortcut links like ![this][].
- }
-
- $alt_text = str_replace('"', '"', $alt_text);
- if (isset($md_urls[$link_id])) {
- $url = $md_urls[$link_id];
- # We've got to encode these to avoid conflicting with italics/bold.
- $url = str_replace(array('*', '_'),
- array($md_escape_table['*'], $md_escape_table['_']),
- $url);
- $result = "
utf8_strlen; # best strlen function for UTF-8.
+ $lines = explode("\n", $text);
+ $text = "";
+
+ foreach ($lines as $line) {
+ # 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;
+ }
+ $text .= "$line\n";
}
- $result .= $md_empty_element_suffix;
+ return $text;
}
- else {
- # If there's no such link ID, leave intact:
- $result = $whole_match;
+ function _initDetab() {
+ #
+ # Check for the availability of the function in the `utf8_strlen` property
+ # (probably `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 = 'Markdown_UTF8_strlen';
+
+ if (function_exists($this->utf8_strlen)) return;
+ function Markdown_UTF8_strlen($text) {
+ return preg_match_all('/[\x00-\xBF]|[\xC0-\xFF][\x80-\xBF]*/',
+ $text, $m);
+ }
}
- return $result;
-}
-function _DoImages_inline_callback($matches) {
- global $md_empty_element_suffix, $md_escape_table;
- $whole_match = $matches[1];
- $alt_text = $matches[2];
- $url = $matches[3];
- $title = '';
- if (isset($matches[6])) {
- $title = $matches[6];
- }
-
- $alt_text = str_replace('"', '"', $alt_text);
- $title = str_replace('"', '"', $title);
- # We've got to encode these to avoid conflicting with italics/bold.
- $url = str_replace(array('*', '_'),
- array($md_escape_table['*'], $md_escape_table['_']),
- $url);
- $result = "
html_hashes),
+ array_values($this->html_hashes), $text);
+ }
-function _DoHeaders($text) {
- # Setext-style headers:
- # Header 1
- # ========
- #
- # Header 2
- # --------
- #
- $text = preg_replace(
- array('{ (^.+?) (?:[ ]+\{\#([-_:a-zA-Z0-9]+)\})? [ \t]*\n=+[ \t]*\n+ }emx',
- '{ (^.+?) (?:[ ]+\{\#([-_:a-zA-Z0-9]+)\})? [ \t]*\n-+[ \t]*\n+ }emx'),
- array("_HashBlock('
'._RunSpanGamut(_UnslashQuotes('\\1')).'
'
- ) . '\n\n'",
- "_HashBlock(''._RunSpanGamut(_UnslashQuotes('\\1')).'
'
- ) . '\n\n'"),
- $text);
-
- # atx-style headers:
- # # Header 1
- # ## Header 2
- # ## Header 2 with closing hashes ##
- # ...
- # ###### Header 6
- #
- $text = preg_replace('{
- ^(\#{1,6}) # $1 = string of #\'s
- [ \t]*
- (.+?) # $2 = Header text
- [ \t]*
- \#* # optional closing #\'s (not counted)
- (?:[ ]+\{\#([-_:a-zA-Z0-9]+)\}[ ]*)? # id attribute
- \n+
- }xme',
- "_HashBlock(
- '
)
+ # Comments and Processing Instructions.
+ #
+ if (preg_match("{^?(?:$this->auto_close_tags)\b}", $tag) ||
+ $tag{1} == '!' || $tag{1} == '?')
+ {
+ # Just add the tag to the block as if it was text.
+ $block_text .= $tag;
+ }
+ else {
+ #
+ # Increase/decrease nested tag count. Only do so if
+ # the tag's name match base tag's.
+ #
+ if (preg_match("{^?$base_tag_name\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_match, $tag, $attr_matches) &&
+ preg_match('/^1|block|span$/', $attr_matches[2]))
+ {
+ # Remove `markdown` attribute from opening tag.
+ $tag = preg_replace($markdown_attr_match, '', $tag);
+
+ # Check if text inside this tag must be parsed in span mode.
+ $this->mode = $attr_matches[2];
+ $span_mode = $this->mode == 'span' || $this->mode != 'block' &&
+ preg_match("{^<(?:$this->contain_span_tags)\b}", $tag);
+
+ # Calculate indent before tag.
+ preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches);
+ $indent = strlen($matches[1]);
+
+ # End preceding block with this tag.
+ $block_text .= $tag;
+ $parsed .= $this->$hash_method($block_text);
+
+ # Get enclosing tag name for the ParseMarkdown function.
+ preg_match('/^<([\w:$]*)\b/', $tag, $matches);
+ $tag_name = $matches[1];
+
+ # Parse the content using the HTML-in-Markdown parser.
+ list ($block_text, $text)
+ = $this->_hashHTMLBlocks_inMarkdown($text, $indent,
+ $tag_name, $span_mode);
+
+ # Outdent markdown text.
+ if ($indent > 0) {
+ $block_text = preg_replace("/^[ ]{1,$indent}/m", "",
+ $block_text);
+ }
+
+ # Append tag content to parsed text.
+ if (!$span_mode) $parsed .= "\n\n$block_text\n\n";
+ else $parsed .= "$block_text";
+
+ # Start over a new block.
+ $block_text = "";
+ }
+ else $block_text .= $tag;
+ }
+
+ } while ($depth > 0);
+
+ #
+ # Hash last block text that wasn't processed inside the loop.
+ #
+ $parsed .= $this->$hash_method($block_text);
+
+ return array($parsed, $text);
+ }
- 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 _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] = '';
+
+ function hashClean($text) {
+ #
+ # Called whenever a tag must be hashed when a function insert a "clean" tag
+ # in $text, it pass through this function and is automaticaly escaped,
+ # blocking invalid nested overlap.
+ #
+ # 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 tag.
+ $key = md5($text);
+ $this->html_cleans[$key] = $text;
+ $this->html_hashes[$key] = $text;
+ return $key; # String that will replace the clean tag.
+ }
+
+
+ function doHeaders($text) {
+ #
+ # Redefined to add id attribute support.
+ #
+ # Setext-style headers:
+ # Header 1 {#header1}
+ # ========
+ #
+ # Header 2 {#header2}
+ # --------
+ #
+ $text = preg_replace_callback(
+ '{ (^.+?) (?:[ ]+\{\#([-_:a-zA-Z0-9]+)\})? [ \t]*\n=+[ \t]*\n+ }mx',
+ array(&$this, '_doHeaders_callback_setext_h1'), $text);
+ $text = preg_replace_callback(
+ '{ (^.+?) (?:[ ]+\{\#([-_:a-zA-Z0-9]+)\})? [ \t]*\n-+[ \t]*\n+ }mx',
+ array(&$this, '_doHeaders_callback_setext_h2'), $text);
+
+ # atx-style headers:
+ # # Header 1 {#header1}
+ # ## Header 2 {#header2}
+ # ## Header 2 with closing hashes ## {#header3}
+ # ...
+ # ###### Header 6 {#header2}
+ #
+ $text = preg_replace_callback('{
+ ^(\#{1,6}) # $1 = string of #\'s
+ [ \t]*
+ (.+?) # $2 = Header text
+ [ \t]*
+ \#* # optional closing #\'s (not counted)
+ (?:[ ]+\{\#([-_:a-zA-Z0-9]+)\})? # id attribute
+ [ \t]*
+ \n+
+ }xm',
+ array(&$this, '_doHeaders_callback_atx'), $text);
+
+ return $text;
+ }
+ function _doHeaders_attr($attr) {
+ if (empty($attr)) return "";
+ return " id=\"$attr\"";
+ }
+ function _doHeaders_callback_setext_h1($matches) {
+ $attr = $this->_doHeaders_attr($id =& $matches[2]);
+ $block = "".$this->runSpanGamut($matches[1])."
";
+ return "\n" . $this->hashBlock($block) . "\n\n";
+ }
+ function _doHeaders_callback_setext_h2($matches) {
+ $attr = $this->_doHeaders_attr($id =& $matches[2]);
+ $block = "".$this->runSpanGamut($matches[1])."
";
+ return "\n" . $this->hashBlock($block) . "\n\n";
+ }
+ function _doHeaders_callback_atx($matches) {
+ $level = strlen($matches[1]);
+ $attr = $this->_doHeaders_attr($id =& $matches[3]);
+ $block = "\n";
- $text .= "\n";
- $text .= "
";
-
- return _HashBlock($text) . "\n";
-}
+
+ function doDefLists($text) {
+ #
+ # Form HTML definition lists.
+ #
+ $less_than_tab = $this->tab_width - 1;
-function _DoLists($text) {
-#
-# Form HTML ordered (numbered) and unordered (bulleted) lists.
-#
- global $md_tab_width, $md_list_level;
- $less_than_tab = $md_tab_width - 1;
-
- # Re-usable patterns to match list item bullets and number markers:
- $marker_ul = '[*+-]';
- $marker_ol = '\d+[.]';
- $marker_any = "(?:$marker_ul|$marker_ol)";
-
- $markers = array($marker_ul, $marker_ol);
-
- foreach ($markers as $marker) {
- # Re-usable pattern to match any entirel ul or ol list:
+ # Re-usable pattern to match any entire dl list:
$whole_list = '
( # $1 = whole list
( # $2
[ ]{0,'.$less_than_tab.'}
- ('.$marker.') # $3 = first list item marker
- [ \t]+
+ ((?>.*\S.*\n)+) # $3 = defined term
+ \n?
+ [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition
)
(?s:.+?)
( # $4
@@ -1216,669 +2252,393 @@ function _DoLists($text) {
|
\n{2,}
(?=\S)
- (?! # Negative lookahead for another list item marker
- [ \t]*
- '.$marker.'[ \t]+
+ (?! # 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
-
- # We use a different prefix before nested lists than top-level lists.
- # See extended comment in _ProcessListItems().
-
- if ($md_list_level) {
- $text = preg_replace_callback('{
- ^
- '.$whole_list.'
- }mx',
- '_DoLists_callback', $text);
- }
- else {
- $text = preg_replace_callback('{
- (?:(?<=\n\n)|\A\n?)
- '.$whole_list.'
- }mx',
- '_DoLists_callback', $text);
- }
- }
-
- return $text;
-}
-function _DoLists_callback($matches) {
- # Re-usable patterns to match list item bullets and number markers:
- $marker_ul = '[*+-]';
- $marker_ol = '\d+[.]';
- $marker_any = "(?:$marker_ul|$marker_ol)";
-
- $list = $matches[1];
- $list_type = preg_match("/$marker_ul/", $matches[3]) ? "ul" : "ol";
-
- $marker_any = ( $list_type == "ul" ? $marker_ul : $marker_ol );
-
- # Turn double returns into triple returns, so that we can make a
- # paragraph for the last item in a list, if necessary:
- $list = preg_replace("/\n{2,}/", "\n\n\n", $list);
- $result = _ProcessListItems($list, $marker_any);
- $result = "<$list_type>\n" . $result . "$list_type>";
- return "\n" . _HashBlock($result) . "\n\n";
-}
+ $text = preg_replace_callback('{
+ (?:(?<=\n\n)|\A\n?)
+ '.$whole_list.'
+ }mx',
+ array(&$this, '_doDefLists_callback'), $text);
-function _ProcessListItems($list_str, $marker_any) {
-#
-# Process the contents of a single ordered or unordered list, splitting it
-# into individual list items.
-#
- global $md_list_level;
-
- # The $md_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.".
-
- $md_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
- (^[ \t]*) # leading whitespace = $2
- ('.$marker_any.') [ \t]+ # list marker = $3
- ((?s:.+?) # list item text = $4
- (\n{1,2}))
- (?= \n* (\z | \2 ('.$marker_any.') [ \t]+))
- }xm',
- '_ProcessListItems_callback', $list_str);
-
- $md_list_level--;
- return $list_str;
-}
-function _ProcessListItems_callback($matches) {
- $item = $matches[4];
- $leading_line =& $matches[1];
- $leading_space =& $matches[2];
-
- if ($leading_line || preg_match('/\n{2,}/', $item)) {
- $item = _RunBlockGamut(_Outdent($item));
+ return $text;
}
- else {
- # Recursion for sub-lists:
- $item = _DoLists(_Outdent($item));
- $item = preg_replace('/\n+$/', '', $item);
- $item = _RunSpanGamut($item);
+ 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";
- foreach ($headers as $n => $header)
- $text .= " \n";
- $text .= "\n";
-
- # Split content by row.
- $rows = explode("\n", trim($content, "\n"));
-
- $text .= "\n";
- foreach ($rows as $row) {
+ 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] = '';
+ }
+
# Creating code spans before splitting the row is an easy way to
# handle a code span containg pipes.
- $row = _DoCodeSpans($row);
-
- # Split row by cell.
- $row_cells = preg_split('/ *[|] */', $row, $col_count);
- $row_cells = array_pad($row_cells, $col_count, '');
+ $head = $this->doCodeSpans($head);
+ $headers = preg_split('/ *[|] */', $head);
+ $col_count = count($headers);
+ # Write column headers.
+ $text = ""._RunSpanGamut(trim($header))." \n";
- $text .= "\n";
+ $text .= "\n";
$text .= "
";
+
+ return $this->hashBlock($text) . "\n";
}
- $text .= "\n";
- $text .= "\n";
- foreach ($row_cells as $n => $cell)
- $text .= " \n";
+ $text .= "\n";
+
+ # Split content by row.
+ $rows = explode("\n", trim($content, "\n"));
+
+ $text .= "\n";
+ foreach ($rows as $row) {
+ # Creating code spans before splitting the row is an easy way to
+ # handle a code span containg pipes.
+ $row = $this->doCodeSpans($row);
+
+ # Split row by cell.
+ $row_cells = preg_split('/ *[|] */', $row, $col_count);
+ $row_cells = array_pad($row_cells, $col_count, '');
+
+ $text .= ""._RunSpanGamut(trim($cell))." \n";
+ foreach ($headers as $n => $header)
+ $text .= " ".$this->runSpanGamut(trim($header))." \n";
$text .= "\n";
+ foreach ($row_cells as $n => $cell)
+ $text .= " \n";
+ }
+ $text .= "\n";
+ $text .= "".$this->runSpanGamut(trim($cell))." \n";
+ $text .= "\n" . $result . "\n
";
+ return $this->hashBlock($result) . "\n\n";
}
- return "\n" . $result . "\n
";
- return _HashBlock($result) . "\n\n";
-}
-
-function _ProcessDefListItems($list_str) {
-#
-# Process the contents of a single ordered or unordered list, splitting it
-# into individual list items.
-#
- global $md_tab_width;
- $less_than_tab = $md_tab_width - 1;
-
- # trim trailing blank lines:
- $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
-
- # Process definition terms.
- $list_str = preg_replace_callback('{
- (?:\n\n+|\A\n?) # leading line
- ( # definition terms = $1
- [ ]{0,'.$less_than_tab.'} # leading whitespace
- (?![:][ ]|[ ]) # negative lookahead for a definition
- # mark (colon) or more whitespace.
- (?: \S.* \n)+? # actual term (not whitespace).
- )
- (?=\n?[ ]{0,3}:[ ]) # lookahead for following line feed
- # with a definition mark.
- }xm',
- '_ProcessDefListItems_callback_dt', $list_str);
-
- # Process actual definitions.
- $list_str = preg_replace_callback('{
- \n(\n+)? # leading line = $1
- [ ]{0,'.$less_than_tab.'} # whitespace before colon
- [:][ ]+ # definition mark (colon)
- ((?s:.+?)) # definition text = $2
- (?= \n+ # stop at next definition mark,
- (?: # next term or end of text
- [ ]{0,'.$less_than_tab.'} [:][ ] |
- ` blocks.
-#
- global $md_tab_width;
- $text = preg_replace_callback('{
- (?:\n\n|\A)
- ( # $1 = the code block -- one or more lines, starting with a space/tab
- (?:
- (?:[ ]{'.$md_tab_width.'} | \t) # Lines must start with a tab or a tab-width of spaces
- .*\n+
- )+
- )
- ((?=^[ ]{0,'.$md_tab_width.'}\S)|\Z) # Lookahead for non-space at line-start, or end of doc
- }xm',
- '_DoCodeBlocks_callback', $text);
-
- return $text;
-}
-function _DoCodeBlocks_callback($matches) {
- $codeblock = $matches[1];
-
- $codeblock = _EncodeCode(_Outdent($codeblock));
-// $codeblock = _Detab($codeblock);
- # trim leading newlines and trailing whitespace
- $codeblock = preg_replace(array('/\A\n+/', '/\s+\z/'), '', $codeblock);
-
- $result = "
";
-}
-
+ if ($leading_line || preg_match('/\n{2,}/', $def)) {
+ $def = $this->runBlockGamut($this->outdent($def . "\n\n"));
+ $def = "\n". $def ."\n";
+ }
+ else {
+ $def = rtrim($def);
+ $def = $this->runSpanGamut($this->outdent($def));
+ }
-function _EncodeCode($_) {
-#
-# Encode/escape certain characters inside Markdown code runs.
-# The point is that in code, these characters are literals,
-# and lose their special Markdown meanings.
-#
- global $md_escape_table;
+ return "\n
";
-
- return "\n\n" . _HashBlock($result) . "\n\n";
-}
-
-
-function _DoCodeSpans($text) {
-#
-# * Backtick quotes are used for " . $codeblock . "\n
spans.
-#
-# * You can use multiple backticks as the delimiters if you want to
-# include literal backticks in the code span. So, this input:
-#
-# Just type ``foo `bar` baz`` at the prompt.
-#
-# Will translate to:
-#
-#
foo `bar` baz
at the prompt.`bar`
...
-#
- $text = preg_replace_callback('@
- (?$c content, so we need to fix that:
- $bq = preg_replace_callback('{(\s*
.+?
)}sx',
- '_DoBlockQuotes_callback2', $bq);
-
- return _HashBlock("\n$bq\n
") . "\n\n";
-}
-function _DoBlockQuotes_callback2($matches) {
- $pre = $matches[1];
- $pre = preg_replace('/^ /m', '', $pre);
- return $pre;
-}
+ \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 = $matches[1];
+ $this->footnotes[$note_id] = $this->outdent($matches[2]);
+ return ''; # String that will replace the block
+ }
-function _FormParagraphs($text) {
-#
-# Params:
-# $text - string to process with html
fn_backlink_class != "") {
+ $class = $this->fn_backlink_class;
+ $class = $this->encodeAmpsAndAngles($class);
+ $class = str_replace('"', '"', $class);
+ $attr .= " class=\"$class\"";
+ }
+ if ($this->fn_backlink_title != "") {
+ $title = $this->fn_backlink_title;
+ $title = $this->encodeAmpsAndAngles($title);
+ $title = str_replace('"', '"', $title);
+ $attr .= " title=\"$title\"";
+ }
+ $num = 0;
+
+ foreach ($this->footnotes_ordered as $note_id => $footnote) {
+ $footnote .= "\n"; # Need to append newline before parsing.
+ $footnote = $this->runBlockGamut("$footnote\n");
+
+ $attr2 = str_replace("%%", ++$num, $attr);
+
+ # Add backlink to last paragraph; create new paragraph if needed.
+ $backlink = "↩";
+ if (preg_match('{$}', $footnote)) {
+ $footnote = substr($footnote, 0, -4) . " $backlink";
+ } else {
+ $footnote .= "\n\n