diff --git a/admin/editor-js.php b/admin/editor-js.php index 21198f5a..1dd25cf2 100644 --- a/admin/editor-js.php +++ b/admin/editor-js.php @@ -64,7 +64,9 @@ $(document).ready(function () { span = '', cache = {}; - Markdown.Extra.init(converter); + Markdown.Extra.init(converter, { + 'extensions' : ["tables", "fenced_code_gfm", "def_list", "attr_list", "footnotes", "strikethrough", "newlines"] + }); // 自动跟随 converter.hooks.chain('postConversion', function (html) { diff --git a/admin/js/markdown-extra-extended.js b/admin/js/markdown-extra-extended.js deleted file mode 100644 index ba4dc391..00000000 --- a/admin/js/markdown-extra-extended.js +++ /dev/null @@ -1,3012 +0,0 @@ -/*! - * Copyright (c) 2006 js-markdown-extra developers - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. 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. - * 3. The name of the author may not be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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. - */ - -var MARKDOWN_VERSION = "1.0.1o"; -var MARKDOWNEXTRA_VERSION = "1.2.5"; - -// Global default settings: - -/** Change to ">" for HTML output */ -var MARKDOWN_EMPTY_ELEMENT_SUFFIX = " />"; - -/** Define the width of a tab for code blocks. */ -var MARKDOWN_TAB_WIDTH = 4; - -/** Optional title attribute for footnote links and backlinks. */ -var MARKDOWN_FN_LINK_TITLE = ""; -var MARKDOWN_FN_BACKLINK_TITLE = ""; - -/** Optional class attribute for footnote links and backlinks. */ -var MARKDOWN_FN_LINK_CLASS = ""; -var MARKDOWN_FN_BACKLINK_CLASS = ""; - -/** Change to false to remove Markdown from posts and/or comments. */ -var MARKDOWN_WP_POSTS = true; -var MARKDOWN_WP_COMMENTS = true; - -/** Standard Function Interface */ -MARKDOWN_PARSER_CLASS = 'MarkdownExtraExtended_Parser'; - -/** - * Converts Markdown formatted text to HTML. - * @param text Markdown text - * @return HTML - */ -function MarkdownText(text) { - //Initialize the parser and return the result of its transform method. - var parser; - if('undefined' == typeof arguments.callee.parser) { - parser = eval("new " + MARKDOWN_PARSER_CLASS + "()"); - parser.init(); - arguments.callee.parser = parser; - } - else { - parser = arguments.callee.parser; - } - // Transform text using parser. - return parser.transform(text); -} - -/** - * Constructor function. Initialize appropriate member variables. - */ -function Markdown_Parser() { - - this.nested_brackets_depth = 6; - this.nested_url_parenthesis_depth = 4; - this.escape_chars = "\\\\`*_{}[]()>#+-.!"; - - // Document transformations - this.document_gamut = [ - // Strip link definitions, store in hashes. - ['stripLinkDefinitions', 20], - ['runBasicBlockGamut', 30] - ]; - - // These are all the transformations that form block-level - /// tags like paragraphs, headers, and list items. - this.block_gamut = [ - ['doHeaders', 10], - ['doHorizontalRules', 20], - ['doLists', 40], - ['doCodeBlocks', 50], - ['doBlockQuotes', 60] - ]; - - // These are all the transformations that occur *within* block-level - // tags like paragraphs, headers, and list items. - this.span_gamut = [ - // 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] - ]; - - this.em_relist = [ - ['' , '(?:(^|[^\\*])(\\*)(?=[^\\*])|(^|[^_])(_)(?=[^_]))(?=\\S|$)(?![\\.,:;]\\s)'], - ['*', '((?:\\S|^)[^\\*])(\\*)(?!\\*)'], - ['_', '((?:\\S|^)[^_])(_)(?!_)'] - ]; - this.strong_relist = [ - ['' , '(?:(^|[^\\*])(\\*\\*)(?=[^\\*])|(^|[^_])(__)(?=[^_]))(?=\\S|$)(?![\\.,:;]\\s)'], - ['**', '((?:\\S|^)[^\\*])(\\*\\*)(?!\\*)'], - ['__', '((?:\\S|^)[^_])(__)(?!_)'] - ]; - this.em_strong_relist = [ - ['' , '(?:(^|[^\\*])(\\*\\*\\*)(?=[^\\*])|(^|[^_])(___)(?=[^_]))(?=\\S|$)(?![\\.,:;]\\s)'], - ['***', '((?:\\S|^)[^\\*])(\\*\\*\\*)(?!\\*)'], - ['___', '((?:\\S|^)[^_])(___)(?!_)'] - ]; -} - -Markdown_Parser.prototype.init = function() { - // this._initDetab(); // NOTE: JavaScript string length is already based on Unicode - this.prepareItalicsAndBold(); - - // Regex to match balanced [brackets]. - // Needed to insert a maximum bracked depth while converting to PHP. - // NOTE: JavaScript doesn't have so faster option for RegExp - //this.nested_brackets_re = new RegExp( - // str_repeat('(?>[^\\[\\]]+|\\[', this.nested_brackets_depth) + - // str_repeat('\\])*', this.nested_brackets_depth) - //); - // NOTE: JavaScript doesn't have so faster option for RegExp - //this.nested_url_parenthesis_re = new RegExp( - // str_repeat('(?>[^()\\s]+|\\(', this.nested_url_parenthesis_depth) + - // str_repeat('(?>\\)))*', this.nested_url_parenthesis_depth) - //); - - // NOTE: Below codes are hopelessly slow. - //this.nested_brackets_re = - // this._php_str_repeat('(?:[^\\[\\]]+|\\[', this.nested_brackets_depth) + - // this._php_str_repeat('\\])*', this.nested_brackets_depth); - //this.nested_url_parenthesis_re = - // this._php_str_repeat('(?:[^\\(\\)\\s]+|\\(', this.nested_url_parenthesis_depth) + - // this._php_str_repeat('(?:\\)))*', this.nested_url_parenthesis_depth); - - // So, instead: - this.nested_brackets_re = '(?:[^\\]]*?)'; - this.nested_url_parenthesis_re = '(?:[^\\)\\s]*?)'; - - // Table of hash values for escaped characters: - var tmp = []; - for(var i = 0; i < this.escape_chars.length; i++) { - tmp.push(this._php_preg_quote(this.escape_chars.charAt(i))); - } - this.escape_chars_re = '[' + tmp.join('') + ']'; - - // Change to ">" for HTML output. - this.empty_element_suffix = MARKDOWN_EMPTY_ELEMENT_SUFFIX; - this.tab_width = MARKDOWN_TAB_WIDTH; - - // Change to `true` to disallow markup or entities. - this.no_markup = false; - this.no_entities = false; - - // Predefined urls and titles for reference links and images. - this.predef_urls = {}; - this.predef_titles = {}; - - // Sort document, block, and span gamut in ascendent priority order. - function cmp_gamut(a, b) { - a = a[1]; b = b[1]; - return a > b ? 1 : a < b ? -1 : 0; - } - this.document_gamut.sort(cmp_gamut); - this.block_gamut.sort(cmp_gamut); - this.span_gamut.sort(cmp_gamut); - - // Internal hashes used during transformation. - this.urls = {}; - this.titles = {}; - this.html_hashes = {}; - - // Status flag to avoid invalid nesting. - this.in_anchor = false; -}; - -/** - * [porting note] - * JavaScript's RegExp doesn't have escape code \A and \Z. - * So multiline pattern can't match start/end of text. Instead - * wrap whole of text with STX(02) and ETX(03). - */ -Markdown_Parser.prototype.__wrapSTXETX__ = function(text) { - if(text.charAt(0) != '\x02') { text = '\x02' + text; } - if(text.charAt(text.length - 1) != '\x03') { text = text + '\x03'; } - return text; -}; - -/** - * [porting note] - * Strip STX(02) and ETX(03). - */ -Markdown_Parser.prototype.__unwrapSTXETX__ = function(text) { - if(text.charAt(0) == '\x02') { text = text.substr(1); } - if(text.charAt(text.length - 1) == '\x03') { text = text.substr(0, text.length - 1); } - return text; -}; - -/** - * - */ -Markdown_Parser.prototype._php_preg_quote = function(text) { - if(!arguments.callee.sRE) { - arguments.callee.sRE = /(\/|\.|\*|\+|\?|\||\(|\)|\[|\]|\{|\}\\)/g; - } - return text.replace(arguments.callee.sRE, '\\$1'); -}; - -Markdown_Parser.prototype._php_str_repeat = function(str, n) { - var tmp = str; - for(var i = 1; i < n; i++) { - tmp += str; - } - return tmp; -}; - -Markdown_Parser.prototype._php_trim = function(target, charlist) { - var chars = charlist || " \t\n\r"; - return target.replace( - new RegExp("^[" + chars + "]*|[" + chars + "]*$", "g"), "" - ); -}; - -Markdown_Parser.prototype._php_rtrim = function(target, charlist) { - var chars = charlist || " \t\n\r"; - return target.replace( - new RegExp( "[" + chars + "]*$", "g" ), "" - ); -}; - -Markdown_Parser.prototype._php_htmlspecialchars_ENT_NOQUOTES = function(str) { - return str.replace(/&/g, '&').replace(//g, '>'); -}; - - -/** - * Called before the transformation process starts to setup parser - * states. - */ -Markdown_Parser.prototype.setup = function() { - // Clear global hashes. - this.urls = this.predef_urls; - this.titles = this.predef_titles; - this.html_hashes = {}; - - this.in_anchor = false; -}; - -/** - * Called after the transformation process to clear any variable - * which may be taking up memory unnecessarly. - */ -Markdown_Parser.prototype.teardown = function() { - this.urls = {}; - this.titles = {}; - this.html_hashes = {}; -}; - -/** - * Main function. Performs some preprocessing on the input text - * and pass it through the document gamut. - */ -Markdown_Parser.prototype.transform = function(text) { - this.setup(); - - // Remove UTF-8 BOM and marker character in input, if present. - text = text.replace(/^\xEF\xBB\xBF|\x1A/, ""); - - // Standardize line endings: - // DOS to Unix and Mac to Unix - text = text.replace(/\r\n?/g, "\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 = text.replace(/^[ ]+$/m, ""); - - // Run document gamut methods. - for(var i = 0; i < this.document_gamut.length; i++) { - var method = this[this.document_gamut[i][0]]; - if(method) { - text = method.call(this, text); - } - else { - console.log(this.document_gamut[i][0] + ' not implemented'); - } - } - - this.teardown(); - - return text + "\n"; -}; - -Markdown_Parser.prototype.hashHTMLBlocks = function(text) { - if(this.no_markup) { return text; } - - var 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; - - var block_tags_a_re = 'ins|del'; - var block_tags_b_re = '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. - var nested_tags_level = 4; - var 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 ">") - ')*' + - ')?'; - var content = - this._php_str_repeat( - '(?:' + - '[^<]+' + // content without tag - '|' + - '<\\2' + // nested opening tag - attr + // attributes - '(?:' + - '/>' + - '|' + - '>', - nested_tags_level - ) + // end of opening tag - '.*?' + // last level nested tag content - this._php_str_repeat( - '' + // closing nested tag - ')' + - '|' + - '<(?!/\\2\\s*>)' + // other tags with a different name - ')*', - nested_tags_level - ); - - var content2 = content.replace('\\2', '\\3'); - - // 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 `
`. - var all = new RegExp('(?:' + - '(?:' + - '(?:\\n\\n)' + // Starting after a blank line - '|' + // or - '(?:\\x02)\\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 - '' + // the matching end tag - '[ ]*' + // trailing spaces/tabs - '(?=\\n+|\\n*\\x03)' + // followed by a newline or end of document - - '|' + // Special version for tags of group a. - - '[ ]{0,' + less_than_tab + '}' + - '<(' + block_tags_a_re + ')' + // start tag = $3 - attr + '>[ ]*\\n' + // attributes followed by > - content2 + // content, support nesting - '' + // the matching end tag - '[ ]*' + // trailing spaces/tabs - '(?=\\n+|\\n*\\x03)' + // 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,}|\\n*\\x03)' + // followed by a blank line or end of document - - '|' + // Special case for standalone HTML comments: - - '[ ]{0,' + less_than_tab + '}' + - '(?:' + //'(?s:' + - '' + - ')' + - '[ ]*' + - '(?=\\n{2,}|\\n*\\x03)' + // followed by a blank line or end of document - - '|' + // PHP and ASP-style processor instructions (' + - ')' + - '[ ]*' + - '(?=\\n{2,}|\\n*\\x03)' + // followed by a blank line or end of document - - ')' + - ')', 'mig'); - // FIXME: JS doesnt have enough escape sequence \A nor \Z. - - var self = this; - text = this.__wrapSTXETX__(text); - text = text.replace(all, function(match, text) { - //console.log(match); - var key = self.hashBlock(text); - return "\n\n" + key + "\n\n"; - }); - text = this.__unwrapSTXETX__(text); - return text; -}; - -/** - * 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. - */ -Markdown_Parser.prototype.hashPart = function(text, boundary) { - if('undefined' === typeof boundary) { - boundary = 'X'; - } - // Swap back any tag hash found in text so we do not have to `unhash` - // multiple times at the end. - text = this.unhash(text); - - // Then hash the block. - if('undefined' === typeof arguments.callee.i) { - arguments.callee.i = 0; - } - var key = boundary + "\x1A" + (++arguments.callee.i) + boundary; - this.html_hashes[key] = text; - return key; // String that will replace the tag. -}; - -/** - * Shortcut function for hashPart with block-level boundaries. - */ -Markdown_Parser.prototype.hashBlock = function(text) { - return this.hashPart(text, 'B'); -}; - -/** - * Strips link definitions from text, stores the URLs and titles in - * hash references. - */ -Markdown_Parser.prototype.stripLinkDefinitions = function(text) { - var less_than_tab = this.tab_width - 1; - var self = this; - // Link defs are in the form: ^[id]: url "optional title" - text = this.__wrapSTXETX__(text); - text = text.replace(new RegExp( - '^[ ]{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+|\\n*(?=\\x03))', - 'mg'), function(match, id, url2, url3, title) { - //console.log(match); - var link_id = id.toLowerCase(); - var url = url2 ? url2 : url3; - self.urls[link_id] = url; - self.titles[link_id] = title; - return ''; // String that will replace the block - } - ); - text = this.__unwrapSTXETX__(text); - return text; -}; - -/** - * Run block gamut tranformations. - */ -Markdown_Parser.prototype.runBlockGamut = function(text) { - // 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); -}; - -/** - * 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. - */ -Markdown_Parser.prototype.runBasicBlockGamut = function(text) { - for(var i = 0; i < this.block_gamut.length; i++) { - var method = this[this.block_gamut[i][0]]; - if(method) { - text = method.call(this, text); - } - else { - console.log(this.block_gamut[i][0] + ' not implemented'); - } - } - // Finally form paragraph and restore hashed blocks. - text = this.formParagraphs(text); - return text; -}; - -/** - * Do Horizontal Rules: - */ -Markdown_Parser.prototype.doHorizontalRules = function(text) { - var self = this; - return text.replace(new RegExp( - '^[ ]{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. - 'mg'), function(match) { - //console.log(match); - return "\n" + self.hashBlock(" tags. - */ -Markdown_Parser.prototype.doAnchors = function(text) { - if (this.in_anchor) return text; - this.in_anchor = true; - - var self = this; - - var _doAnchors_reference_callback = function(match, whole_match, link_text, link_id) { - //console.log(match); - if(typeof(link_id) !== 'string' || link_id === '') { - // for shortcut links like [this][] or [this]. - link_id = link_text; - } - - // lower-case and turn embedded newlines into spaces - link_id = link_id.toLowerCase(); - link_id = link_id.replace(/[ ]?\n/, ' '); - - var result; - if ('undefined' !== typeof self.urls[link_id]) { - var url = self.urls[link_id]; - url = self.encodeAttribute(url); - - result = ""; - result = self.hashPart(result); - } - else { - result = whole_match; - } - return result; - }; - - // - // First, handle reference-style links: [link text] [id] - // - text = text.replace(new RegExp( - '(' + // wrap whole match in $1 - '\\[' + - '(' + this.nested_brackets_re + ')' + // link text = $2 - '\\]' + - - '[ ]?' + // one optional space - '(?:\\n[ ]*)?' + // one optional newline followed by spaces - - '\\[' + - '(.*?)' + // id = $3 - '\\]' + - ')', - 'mg' - ), _doAnchors_reference_callback); - - // - // Next, inline-style links: [link text](url "optional title") - // - text = text.replace(new RegExp( - '(' + // 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 - '\\)' + - ')', - 'mg' - ), function(match, whole_match, link_text, url3, url4, x0, x1, title) { - //console.log(match); - link_text = self.runSpanGamut(link_text); - var url = url3 ? url3 : url4; - - url = self.encodeAttribute(url); - - var result = ""; - - return self.hashPart(result); - }); - - // - // 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 = text.replace(new RegExp( - '(' + // wrap whole match in $1 - '\\[' + - '([^\\[\\]]+)' + // link text = $2; can\'t contain [ or ] - '\\]' + - ')', - 'mg' - ), _doAnchors_reference_callback); - - this.in_anchor = false; - return text; -}; - -/** - * Turn Markdown image shortcuts into tags. - */ -Markdown_Parser.prototype.doImages = function(text) { - var self = this; - - // - // First, handle reference-style labeled images: ![alt text][id] - // - text = text.replace(new RegExp( - '(' + // wrap whole match in $1 - '!\\[' + - '(' + this.nested_brackets_re + ')' + // alt text = $2 - '\\]' + - - '[ ]?' + // one optional space - '(?:\\n[ ]*)?' + // one optional newline followed by spaces - - '\\[' + - '(.*?)' + // id = $3 - '\\]' + - - ')', - 'mg' - ), function(match, whole_match, alt_text, link_id) { - //console.log(match); - link_id = link_id.toLowerCase(); - - if (typeof(link_id) !== 'string' || link_id === '') { - link_id = alt_text.toLowerCase(); // for shortcut links like ![this][]. - } - - alt_text = self.encodeAttribute(alt_text); - var result; - if ('undefined' !== typeof self.urls[link_id]) { - var url = self.encodeAttribute(self.urls[link_id]); - result = "\""' + // 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 - '\\)' + - ')', - 'mg' - ), function(match, whole_match, alt_text, url3, url4, x5, x6, title) { - //console.log(match); - var url = url3 ? url3 : url4; - - alt_text = self.encodeAttribute(alt_text); - url = self.encodeAttribute(url); - var result = "\""" + self.runSpanGamut(span) + ""; - return "\n" + self.hashBlock(block) + "\n\n"; - }); - - // atx-style headers: - // # Header 1 - // ## Header 2 - // ## Header 2 with closing hashes ## - // ... - // ###### Header 6 - // - text = text.replace(new RegExp( - '^(\\#{1,6})' + // $1 = string of #\'s - '[ ]*' + - '(.+?)' + // $2 = Header text - '[ ]*' + - '\\#*' + // optional closing #\'s (not counted) - '\\n+', - 'mg' - ), function(match, hashes, span) { - //console.log(match); - var level = hashes.length; - var block = "" + self.runSpanGamut(span) + ""; - return "\n" + self.hashBlock(block) + "\n\n"; - }); - - return text; -}; - -/** - * Form HTML ordered (numbered) and unordered (bulleted) lists. - */ -Markdown_Parser.prototype.doLists = function(text) { - var less_than_tab = this.tab_width - 1; - - // Re-usable patterns to match list item bullets and number markers: - var marker_ul_re = '[\\*\\+-]'; - var marker_ol_re = '\\d+[\\.]'; - var marker_any_re = "(?:" + marker_ul_re + "|" + marker_ol_re + ")"; - - var self = this; - var _doLists_callback = function(match, list, x2, x3, type) { - //console.log(match); - // Re-usable patterns to match list item bullets and number markers: - var list_type = type.match(marker_ul_re) ? "ul" : "ol"; - - var marker_any_re = list_type == "ul" ? marker_ul_re : marker_ol_re; - - list += "\n"; - var result = self.processListItems(list, marker_any_re); - - result = self.hashBlock("<" + list_type + ">\n" + result + ""); - return "\n" + result + "\n\n"; - }; - - var markers_relist = [ - [marker_ul_re, marker_ol_re], - [marker_ol_re, marker_ul_re] - ]; - - for (var i = 0; i < markers_relist.length; i++) { - var marker_re = markers_relist[i][0]; - var other_marker_re = markers_relist[i][1]; - // Re-usable pattern to match any entirel ul or ol list: - var whole_list_re = - '(' + // $1 = whole list - '(' + // $2 - '([ ]{0,' + less_than_tab + '})' + // $3 = number of spaces - '(' + marker_re + ')' + // $4 = first list item marker - '[ ]+' + - ')' + - '[\\s\\S]+?' + - '(' + // $5 - '(?=\\x03)' + // \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(). - - text = this.__wrapSTXETX__(text); - if (this.list_level) { - text = text.replace(new RegExp('^' + whole_list_re, "mg"), _doLists_callback); - } - else { - text = text.replace(new RegExp( - '(?:(?=\\n)\\n|\\x02\\n?)' + // Must eat the newline - whole_list_re, "mg" - ), _doLists_callback); - } - text = this.__unwrapSTXETX__(text); - } - - return text; -}; - -// var $list_level = 0; - -/** - * Process the contents of a single ordered or unordered list, splitting it - * into individual list items. - */ -Markdown_Parser.prototype.processListItems = function(list_str, marker_any_re) { - // The $this->list_level global keeps track of when we're inside a list. - // Each time we enter a list, we increment it; when we leave a list, - // we decrement. If it's zero, we're not in a list anymore. - // - // We do this because when we're not inside a list, we want to treat - // something like this: - // - // I recommend upgrading to version - // 8. Oops, now this line is treated - // as a sub-list. - // - // As a single paragraph, despite the fact that the second line starts - // with a digit-period-space sequence. - // - // Whereas when we're inside a list (or sub-list), that line will be - // treated as the start of a sub-list. What a kludge, huh? This is - // an aspect of Markdown's syntax that's hard to parse perfectly - // without resorting to mind-reading. Perhaps the solution is to - // change the syntax rules such that sub-lists must start with a - // starting cardinal number; e.g. "1." or "a.". - - if('undefined' === typeof this.list_level) { - this.list_level = 0; - } - this.list_level++; - - // trim trailing blank lines: - list_str = this.__wrapSTXETX__(list_str); - list_str = list_str.replace(/\n{2,}(?=\x03)/m, "\n"); - list_str = this.__unwrapSTXETX__(list_str); - - var self = this; - list_str = this.__wrapSTXETX__(list_str); - list_str = list_str.replace(new RegExp( - '(\\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\\S]*?)' + // list item text = $4 - '(?:(\\n+(?=\\n))|\\n)' + // tailing blank line = $5 - '(?=\\n*(\\x03|\\2(' + marker_any_re + ')(?:[ ]+|(?=\\n))))', - "gm" - ), function(match, leading_line, leading_space, marker_space, item, tailing_blank_line) { - //console.log(match); - //console.log(item, [leading_line ? leading_line.length : 0, tailing_blank_line ? tailing_blank_line.length : 0]); - if (leading_line || tailing_blank_line || item.match(/\n{2,}/)) { - // Replace marker with the appropriate whitespace indentation - item = leading_space + self._php_str_repeat(' ', marker_space.length) + item; - item = self.runBlockGamut(self.outdent(item) + "\n"); - } - else { - // Recursion for sub-lists: - item = self.doLists(self.outdent(item)); - item = item.replace(/\n+$/m, ''); - item = self.runSpanGamut(item); - } - - return "
  • " + item + "
  • \n"; - }); - list_str = this.__unwrapSTXETX__(list_str); - - this.list_level--; - return list_str; -}; - -/** - * Process Markdown `
    ` blocks.
    - */
    -Markdown_Parser.prototype.doCodeBlocks = function(text) {
    -    var self = this;
    -    text = this.__wrapSTXETX__(text);
    -    text = text.replace(new RegExp(
    -        '(?:\\n\\n|(?=\\x02)\\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)|(?:\\n*(?=\\x03)))',  // Lookahead for non-space at line-start, or end of doc
    -        'mg'
    -    ), function(match, codeblock) {
    -        //console.log(match);
    -        codeblock = self.outdent(codeblock);
    -        codeblock = self._php_htmlspecialchars_ENT_NOQUOTES(codeblock);
    -
    -        // trim leading newlines and trailing newlines
    -        codeblock = self.__wrapSTXETX__(codeblock);
    -        codeblock = codeblock.replace(/(?=\x02)\n+|\n+(?=\x03)/g, '');
    -        codeblock = self.__unwrapSTXETX__(codeblock);
    -
    -        codeblock = "
    " + codeblock + "\n
    "; - return "\n\n" + self.hashBlock(codeblock) + "\n\n"; - }); - text = this.__unwrapSTXETX__(text); - return text; -}; - -/** - * Create a code span markup for $code. Called from handleSpanToken. - */ -Markdown_Parser.prototype.makeCodeSpan = function(code) { - code = this._php_htmlspecialchars_ENT_NOQUOTES(this._php_trim(code)); - return this.hashPart("" + code + ""); -}; - -/** - * Prepare regular expressions for searching emphasis tokens in any - * context. - */ -Markdown_Parser.prototype.prepareItalicsAndBold = function() { - this.em_strong_prepared_relist = {}; - for(var i = 0; i < this.em_relist.length; i++) { - var em = this.em_relist[i][0]; - var em_re = this.em_relist[i][1]; - for(var j = 0; j < this.strong_relist.length; j++) { - var strong = this.strong_relist[j][0]; - var strong_re = this.strong_relist[j][1]; - // Construct list of allowed token expressions. - var token_relist = []; - for(var k = 0; k < this.em_strong_relist.length; k++) { - var em_strong = this.em_strong_relist[k][0]; - var em_strong_re = this.em_strong_relist[k][1]; - if(em + strong == em_strong) { - token_relist.push(em_strong_re); - } - } - token_relist.push(em_re); - token_relist.push(strong_re); - - // Construct master expression from list. - var token_re = new RegExp('(' + token_relist.join('|') + ')'); - this.em_strong_prepared_relist['rx_' + em + strong] = token_re; - } - } -}; - -Markdown_Parser.prototype.doItalicsAndBold = function(text) { - var em = ''; - var strong = ''; - var tree_char_em = false; - var text_stack = ['']; - var token_stack = []; - var token = ''; - - while (1) { - // - // Get prepared regular expression for seraching emphasis tokens - // in current context. - // - var token_re = this.em_strong_prepared_relist['rx_' + em + strong]; - - // - // Each loop iteration search for the next emphasis token. - // Each token is then passed to handleSpanToken. - // - var parts = text.match(token_re); //PREG_SPLIT_DELIM_CAPTURE - if(parts) { - var left = RegExp.leftContext; - var right = RegExp.rightContext; - var pre = ""; - var marker = parts[1]; - for(var mg = 2; mg < parts.length; mg += 2) { - if('undefined' !== typeof parts[mg] && parts[mg] != '') { - pre = parts[mg]; - marker = parts[mg + 1]; - break; - } - } - //console.log([left + pre, marker]); - text_stack[0] += (left + pre); - token = marker; - text = right; - } - else { - text_stack[0] += text; - token = ''; - text = ''; - } - if(token == '') { - // Reached end of text span: empty stack without emitting. - // any more emphasis. - while (token_stack.length > 0 && token_stack[0].length > 0) { - text_stack[1] += token_stack.shift(); - var text_stack_prev0 = text_stack.shift(); // $text_stack[0] .= array_shift($text_stack); - text_stack[0] += text_stack_prev0; - } - break; - } - - var tag, span; - - var token_len = token.length; - 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. - token_stack.shift(); - span = text_stack.shift(); - 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] = this._php_str_repeat(token.charAt(0), 3 - token_len); - tag = token_len == 2 ? "strong" : "em"; - span = text_stack[0]; - span = this.runSpanGamut(span); - span = "<" + tag + ">" + span + ""; - text_stack[0] = this.hashPart(span); - if(tag == 'strong') { strong = ''; } else { em = ''; } - } - tree_char_em = false; - } else if (token_len == 3) { - if (em != '') { - // Reached closing marker for both em and strong. - // Closing strong marker: - for (var i = 0; i < 2; ++i) { - var shifted_token = token_stack.shift(); - tag = shifted_token.length == 2 ? "strong" : "em"; - span = text_stack.shift(); - span = this.runSpanGamut(span); - span = "<" + tag + ">" + span + ""; - text_stack[0] = this.hashPart(span); - if(tag == 'strong') { strong = ''; } else { em = ''; } - } - } else { - // Reached opening three-char emphasis marker. Push on token - // stack; will be handled by the special condition above. - em = token.charAt(0); - strong = em + em; - token_stack.unshift(token); - text_stack.unshift(''); - tree_char_em = true; - } - } else if (token_len == 2) { - if (strong != '') { - // Unwind any dangling emphasis marker: - if (token_stack[0].length == 1) { - text_stack[1] += token_stack.shift(); - var text_stack_prev0 = text_stack.shift(); // $text_stack[0] .= array_shift($text_stack); - text_stack[0] += text_stack_prev0; - } - // Closing strong marker: - token_stack.shift(); - span = text_stack.shift(); - span = this.runSpanGamut(span); - span = "" + span + ""; - text_stack[0] += this.hashPart(span); - strong = ''; - } else { - token_stack.unshift(token); - text_stack.unshift(''); - strong = token; - } - } else { - // Here $token_len == 1 - if (em != '') { - if (token_stack[0].length == 1) { - // Closing emphasis marker: - token_stack.shift(); - span = text_stack.shift(); - span = this.runSpanGamut(span); - span = "" + span + ""; - text_stack[0] += this.hashPart(span); - em = ''; - } else { - text_stack[0] += token; - } - } else { - token_stack.unshift(token); - text_stack.unshift(''); - em = token; - } - } - } - return text_stack[0]; -}; - - -Markdown_Parser.prototype.doBlockQuotes = function(text) { - var self = this; - text = text.replace(new RegExp( - '(' + // Wrap whole match in $1 - '(?:' + - '^[ ]*>[ ]?' + // ">" at the start of a line - '.+\\n' + // rest of the first line - '(.+\\n)*' + // subsequent consecutive lines - '\\n*' + // blanks - ')+' + - ')', - 'mg' - ), function(match, bq) { - //console.log(match); - // trim one level of quoting - trim whitespace-only lines - bq = bq.replace(/^[ ]*>[ ]?|^[ ]+$/mg, ''); - bq = self.runBlockGamut(bq); // recurse - - bq = bq.replace(/^/mg, " "); - // These leading spaces cause problem with
     content, 
    -        // so we need to fix that:
    -        bq = bq.replace(/(\\s*
    [\\s\\S]+?<\/pre>)/mg, function(match, pre) {
    -            //console.log(match);
    -            pre = pre.replace(/^  /m, '');
    -            return pre;
    -        });
    -
    -        return "\n" + self.hashBlock("
    \n" + bq + "\n
    ") + "\n\n"; - }); - return text; -}; - -/** - * Params: - * $text - string to process with html

    tags - */ -Markdown_Parser.prototype.formParagraphs = function(text) { - - // Strip leading and trailing lines: - text = this.__wrapSTXETX__(text); - text = text.replace(/(?:\x02)\n+|\n+(?:\x03)/g, ""); - text = this.__unwrapSTXETX__(text); - // [porting note] - // below may be faster than js regexp. - //for(var s = 0; s < text.length && text.charAt(s) == "\n"; s++) { } - //text = text.substr(s); - //for(var e = text.length; e > 0 && text.charAt(e - 1) == "\n"; e--) { } - //text = text.substr(0, e); - - var grafs = text.split(/\n{2,}/m); - //preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); - - // - // Wrap

    tags and unhashify HTML blocks - // - for(var i = 0; i < grafs.length; i++) { - var value = grafs[i]; - if(value == "") { - // [porting note] - // This case is replacement for PREG_SPLIT_NO_EMPTY. - } - else if (!value.match(/^B\x1A[0-9]+B$/)) { - // Is a paragraph. - value = this.runSpanGamut(value); - value = value.replace(/^([ ]*)/, "

    "); - value += "

    "; - grafs[i] = this.unhash(value); - } - else { - // Is a block. - // Modify elements of @grafs in-place... - var graf = value; - var block = this.html_hashes[graf]; - graf = block; - //if (preg_match('{ - // \A - // ( # $1 =
    tag - //
    ]* - // \b - // markdown\s*=\s* ([\'"]) # $2 = attr quote char - // 1 - // \2 - // [^>]* - // > - // ) - // ( # $3 = contents - // .* - // ) - // (
    ) # $4 = closing tag - // \z - // }xs', $block, $matches)) - //{ - // list(, $div_open, , $div_content, $div_close) = $matches; - // - // # We can't call Markdown(), because that resets the hash; - // # that initialization code should be pulled into its own sub, though. - // $div_content = $this->hashHTMLBlocks($div_content); - // - // # Run document gamut methods on the content. - // foreach ($this->document_gamut as $method => $priority) { - // $div_content = $this->$method($div_content); - // } - // - // $div_open = preg_replace( - // '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open); - // - // $graf = $div_open . "\n" . $div_content . "\n" . $div_close; - //} - grafs[i] = graf; - } - } - - return grafs.join("\n\n"); -}; - -/** - * Encode text for a double-quoted HTML attribute. This function - * is *not* suitable for attributes enclosed in single quotes. - */ -Markdown_Parser.prototype.encodeAttribute = function(text) { - text = this.encodeAmpsAndAngles(text); - text = text.replace(/"/g, '"'); - return text; -}; - -/** - * Smart processing for ampersands and angle brackets that need to - * be encoded. Valid character entities are left alone unless the - * no-entities mode is set. - */ -Markdown_Parser.prototype.encodeAmpsAndAngles = function(text) { - if (this.no_entities) { - text = text.replace(/&/g, '&'); - } else { - // Ampersand-encoding based entirely on Nat Irons's Amputator - // MT plugin: - text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/, '&'); - } - // Encode remaining <'s - text = text.replace(/'; - } - - var level = 0, re = /[()]/; - while (matches = re.exec(link)) { - if ('(' == matches[0]) { - if (level <= 0) { - level = 1; - } else { - level ++; - } - } else { - level --; - } - - re.lastIndex = matches.index + 1; - } - - var tail = ''; - if (level < 0) { - re = new RegExp("\\){1," + (- level) + "}$"); - link = link.replace(re, function (match) { - tail = match; - return ''; - }); - } - - var url = self.encodeAttribute(protocol, link); - link = '
    ' + url + ''; - return self.hashPart(link) + tail; - }); - - text = text.replace(/<((https?|ftp|dict):[^'">\s]+)>/i, function(match, address) { - //console.log(match); - var url = self.encodeAttribute(address); - var link = "" + url + ""; - return self.hashPart(link); - }); - - // Email addresses: - text = text.replace(new RegExp( - '<' + - '(?:mailto:)?' + - '(' + - '(?:' + - '[-!#$%&\'*+/=?^_`.{|}~\\w\\x80-\\xFF]+' + - '|' + - '".*?"' + - ')' + - '\\@' + - '(?:' + - '[-a-z0-9\\x80-\\xFF]+(\\.[-a-z0-9\\x80-\\xFF]+)*\\.[a-z]+' + - '|' + - '\\[[\\d.a-fA-F:]+\\]' + // IPv4 & IPv6 - ')' + - ')' + - '>', - 'i' - ), function(match, address) { - //console.log(match); - var link = self.encodeEmailAddress(address); - return self.hashPart(link); - }); - - return text; -}; - -/** - * Input: an email address, e.g. "foo@example.com" - * - * Output: the email address as a mailto link, with each character - * of the address encoded as either a decimal or hex entity, in - * the hopes of foiling most address harvesting spam bots. E.g.: - * - *

    foo@exampl - * e.com

    - * - * Based by a filter by Matthew Wickline, posted to BBEdit-Talk. - * With some optimizations by Milian Wolff. - */ -Markdown_Parser.prototype.encodeEmailAddress = function(addr) { - if('undefined' === typeof arguments.callee.crctable) { - arguments.callee.crctable = - "00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 " + - "0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 " + - "1DB71064 6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 " + - "136C9856 646BA8C0 FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 " + - "3B6E20C8 4C69105E D56041E4 A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B " + - "35B5A8FA 42B2986C DBBBC9D6 ACBCF940 32D86CE3 45DF5C75 DCD60DCF ABD13D59 " + - "26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 56B3C423 CFBA9599 B8BDA50F " + - "2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 C1611DAB B6662D3D " + - "76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 E8B8D433 " + - "7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 " + - "6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 " + - "65B0D9C6 12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 " + - "4DB26158 3AB551CE A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB " + - "4369E96A 346ED9FC AD678846 DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 " + - "5005713C 270241AA BE0B1010 C90C2086 5768B525 206F85B3 B966D409 CE61E49F " + - "5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 2EB40D81 B7BD5C3B C0BA6CAD " + - "EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF 04DB2615 73DC1683 " + - "E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 7D079EB1 " + - "F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 " + - "FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 " + - "D6D6A3E8 A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B " + - "D80D2BDA AF0A1B4C 36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 " + - "CB61B38C BC66831A 256FD2A0 5268E236 CC0C7795 BB0B4703 220216B9 5505262F " + - "C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D " + - "9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 EB0E363F 72076785 05005713 " + - "95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D 7CDCEFB7 0BDBDF21 " + - "86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 18B74777 " + - "88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 " + - "A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB " + - "AED16A4A D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 " + - "BDBDF21C CABAC28A 53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF " + - "B3667A2E C4614AB8 5D681B02 2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D".split(' '); - } - var crctable = arguments.callee.crctable; - function _crc32(str) { - var crc = 0; - crc = crc ^ (-1); - for (var i = 0; i < str.length; ++i) { - var y = (crc ^ str.charCodeAt(i)) & 0xff; - var x = "0x" + crctable[y]; - crc = (crc >>> 8) ^ x; - } - return (crc ^ (-1)) >>> 0; - } - - addr = "mailto:" + addr; - var chars = []; - var i; - for(i = 0; i < addr.length; i++) { - chars.push(addr.charAt(i)); - } - var seed = Math.floor(Math.abs(_crc32(addr) / addr.length)); // # Deterministic seed. - - for(i = 0; i < chars.length; i++) { - var c = chars[i]; - var ord = c.charCodeAt(0); - // Ignore non-ascii chars. - if(ord < 128) { - var r = (seed * (1 + i)) % 100; // Pseudo-random function. - // roughly 10% raw, 45% hex, 45% dec - // '@' *must* be encoded. I insist. - if(r > 90 && c != '@') { /* do nothing */ } - else if(r < 45) { chars[i] = '&#x' + ord.toString(16) + ';'; } - else { chars[i] = '&#' + ord.toString(10) + ';'; } - } - } - - addr = chars.join(''); - var text = chars.splice(7, chars.length - 1).join(''); // text without `mailto:` - addr = "" + text + ""; - - return addr; -}; - -/** - * Take the string $str and parse it into tokens, hashing embeded HTML, - * escaped characters and handling code spans. -*/ -Markdown_Parser.prototype.parseSpan = function(str) { - var output = ''; - - var span_re = new RegExp( - '(' + - '\\\\' + this.escape_chars_re + - '|' + - // This expression is too difficult for JS: '(?' + // comment - '|' + - '<\\?.*?\\?>|<%.*?%>' + // processing instruction - '|' + - '<[/!$]?[-a-zA-Z0-9:_]+' + // regular tags - '(?:' + - '\\s' + - '(?:[^"\'>]+|"[^"]*"|\'[^\']*\')*' + - ')?' + - '>' - )) + - ')' - ); - - 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. - // - var parts = str.match(span_re); //PREG_SPLIT_DELIM_CAPTURE - if(parts) { - if(RegExp.leftContext) { - output += RegExp.leftContext; - } - // Back quote but after backslash is to be ignored. - if(RegExp.lastMatch.charAt(0) == "`" && - RegExp.leftContext.charAt(RegExp.leftContext.length - 1) == "\\" - ) { - output += RegExp.lastMatch; - str = RegExp.rightContext; - continue; - } - var r = this.handleSpanToken(RegExp.lastMatch, RegExp.rightContext); - output += r[0]; - str = r[1]; - } - else { - output += str; - break; - } - } - return output; -}; - - -/** - * Handle $token provided by parseSpan by determining its nature and - * returning the corresponding value that should replace it. -*/ -Markdown_Parser.prototype.handleSpanToken = function(token, str) { - //console.log([token, str]); - switch (token.charAt(0)) { - case "\\": - return [this.hashPart("&#" + token.charCodeAt(1) + ";"), str]; - case "`": - // Search for end marker in remaining text. - if (str.match(new RegExp('^([\\s\\S]*?[^`])' + this._php_preg_quote(token) + '(?!`)([\\s\\S]*)$', 'm'))) { - var code = RegExp.$1; - str = RegExp.$2; - var codespan = this.makeCodeSpan(code); - return [this.hashPart(codespan), str]; - } - return [token, str]; // return as text since no ending marker found. - default: - return [this.hashPart(token), str]; - } -}; - -/** - * Remove one level of line-leading tabs or spaces - */ -Markdown_Parser.prototype.outdent = function(text) { - return text.replace(new RegExp('^(\\t|[ ]{1,' + this.tab_width + '})', 'mg'), ''); -}; - - -//# 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'; - -/** - * Replace tabs with the appropriate amount of space. - */ -Markdown_Parser.prototype.detab = function(text) { - // 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. - var self = this; - return text.replace(/^.*\t.*$/mg, function(line) { - //$strlen = $this->utf8_strlen; # strlen function for UTF-8. - // Split in blocks. - var blocks = line.split("\t"); - // Add each blocks to the line. - line = blocks.shift(); // Do not add first block twice. - for(var i = 0; i < blocks.length; i++) { - var block = blocks[i]; - // Calculate amount of space, insert spaces, insert block. - var amount = self.tab_width - line.length % self.tab_width; - line += self._php_str_repeat(" ", amount) + block; - } - return line; - }); -}; - -/** - * Swap back in all the tags hashed by _HashHTMLBlocks. - */ -Markdown_Parser.prototype.unhash = function(text) { - var self = this; - return text.replace(/(.)\x1A[0-9]+\1/g, function(match) { - return self.html_hashes[match]; - }); -}; -/*-------------------------------------------------------------------------*/ - -/** - * Constructor function. Initialize the parser object. - */ -function MarkdownExtra_Parser() { - - // Prefix for footnote ids. - this.fn_id_prefix = ""; - - // Optional title attribute for footnote links and backlinks. - this.fn_link_title = MARKDOWN_FN_LINK_TITLE; - this.fn_backlink_title = MARKDOWN_FN_BACKLINK_TITLE; - - // Optional class attribute for footnote links and backlinks. - this.fn_link_class = MARKDOWN_FN_LINK_CLASS; - this.fn_backlink_class = MARKDOWN_FN_BACKLINK_CLASS; - - // Predefined abbreviations. - this.predef_abbr = {}; - - // Extra variables used during extra transformations. - this.footnotes = {}; - this.footnotes_ordered = []; - this.abbr_desciptions = {}; - this.abbr_word_re = ''; - - // Give the current footnote number. - this.footnote_counter = 1; - - // ### HTML Block Parser ### - - // Tags that are always treated as block tags: - this.block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend'; - - // Tags treated as block tags only if the opening tag is alone on it's line: - this.context_block_tags_re = 'script|noscript|math|ins|del'; - - // Tags where markdown="1" default to span mode: - this.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: - this.clean_tags_re = 'script|math'; - - // Tags that do not need to be closed. - this.auto_close_tags_re = 'hr|img'; - - // Redefining emphasis markers so that emphasis by underscore does not - // work in the middle of a word. - this.em_relist = [ - ['' , '(?:(^|[^\\*])(\\*)(?=[^\\*])|(^|[^a-zA-Z0-9_])(_)(?=[^_]))(?=\\S|$)(?![\\.,:;]\\s)'], - ['*', '((?:\\S|^)[^\\*])(\\*)(?!\\*)'], - ['_', '((?:\\S|^)[^_])(_)(?![a-zA-Z0-9_])'] - ]; - this.strong_relist = [ - ['' , '(?:(^|[^\\*])(\\*\\*)(?=[^\\*])|(^|[^a-zA-Z0-9_])(__)(?=[^_]))(?=\\S|$)(?![\\.,:;]\\s)'], - ['**', '((?:\\S|^)[^\\*])(\\*\\*)(?!\\*)'], - ['__', '((?:\\S|^)[^_])(__)(?![a-zA-Z0-9_])'] - ]; - this.em_strong_relist = [ - ['' , '(?:(^|[^\\*])(\\*\\*\\*)(?=[^\\*])|(^|[^a-zA-Z0-9_])(___)(?=[^_]))(?=\\S|$)(?![\\.,:;]\\s)'], - ['***', '((?:\\S|^)[^\\*])(\\*\\*\\*)(?!\\*)'], - ['___', '((?:\\S|^)[^_])(___)(?![a-zA-Z0-9_])'] - ]; - - // 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.push(['doFencedCodeBlocks', 5]); - this.document_gamut.push(['stripFootnotes', 15]); - this.document_gamut.push(['stripAbbreviations', 25]); - this.document_gamut.push(['appendFootnotes', 50]); - - this.block_gamut.push(['doFencedCodeBlocks', 5]); - this.block_gamut.push(['doTables', 15]); - this.block_gamut.push(['doDefLists', 45]); - - this.span_gamut.push(['doFootnotes', 5]); - this.span_gamut.push(['doAbbreviations', 70]); -} -MarkdownExtra_Parser.prototype = new Markdown_Parser(); - -/** - * Setting up Extra-specific variables. - */ -MarkdownExtra_Parser.prototype.setup = function() { - this.constructor.prototype.setup.call(this); - - this.footnotes = {}; - this.footnotes_ordered = []; - this.abbr_desciptions = {}; - this.abbr_word_re = ''; - this.footnote_counter = 1; - - for(var abbr_word in this.predef_abbr) { - var abbr_desc = this.predef_abbr[abbr_word]; - if(this.abbr_word_re != '') { - this.abbr_word_re += '|'; - } - this.abbr_word_re += this._php_preg_quote(abbr_word); // ?? str -> re? - this.abbr_desciptions[abbr_word] = this._php_trim(abbr_desc); - } -}; - -/** - * Clearing Extra-specific variables. - */ -MarkdownExtra_Parser.prototype.teardown = function() { - this.footnotes = {}; - this.footnotes_ordered = []; - this.abbr_desciptions = {}; - this.abbr_word_re = ''; - - this.constructor.prototype.teardown.call(this); -}; - - -/** - * Hashify HTML Blocks and "clean tags". - * - * We only want to do this for block-level HTML tags, such as headers, - * lists, and tables. That's because we still want to wrap

    s around - * "paragraphs" that are wrapped in non-block-level tags, such as anchors, - * phrase emphasis, and spans. The list of tags we're looking for is - * hard-coded. - * - * This works by calling _HashHTMLBlocks_InMarkdown, which then calls - * _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1" - * attribute is found whitin a tag, _HashHTMLBlocks_InHTML calls back - * _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag. - * These two functions are calling each other. It's recursive! - */ -MarkdownExtra_Parser.prototype.hashHTMLBlocks = function(text) { - // - // Call the HTML-in-Markdown hasher. - // - var r = this._hashHTMLBlocks_inMarkdown(text); - text = r[0]; - - return text; -}; - -/** - * Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags. - * - * * $indent is the number of space to be ignored when checking for code - * blocks. This is important because if we don't take the indent into - * account, something like this (which looks right) won't work as expected: - * - *

    - *
    - * Hello World. <-- Is this a Markdown code block or text? - *
    <-- Is this a Markdown code block or a real tag? - *
    - * - * If you don't like this, just don't indent the tag on which - * you apply the markdown="1" attribute. - * - * * If $enclosing_tag_re is not empty, stops at the first unmatched closing - * tag with that name. Nested tags supported. - * - * * If $span is true, text inside must treated as span. So any double - * newline will be replaced by a single newline so that it does not create - * paragraphs. - * - * Returns an array of that form: ( processed text , remaining text ) - */ -MarkdownExtra_Parser.prototype._hashHTMLBlocks_inMarkdown = function(text, indent, enclosing_tag_re, span) { - if('undefined' === typeof indent) { indent = 0; } - if('undefined' === typeof enclosing_tag_re) { enclosing_tag_re = ''; } - if('undefined' === typeof span) { span = false; } - - if(text === '') { return ['', '']; } - - var matches; - - // Regex to check for the presense of newlines around a block tag. - var newline_before_re = /(?:^\n?|\n\n)*$/; - var newline_after_re = new RegExp( - '^' + // Start of text following the tag. - '([ ]*)?' + // Optional comment. - '[ ]*\\n' , // Must be followed by newline. - 'm' - ); - - // Regex to match any tag. - var block_tag_re = new RegExp( - '(' + // $2: Capture hole tag. - '`) - '\'.*?\'|' + // Single quotes (can contain `>`) - '.+?' + // Anything but quotes and `>`. - ')*?' + - ')?' + - '>' + // End of tag. - '|' + - '' + // HTML Comment - '|' + - '<\\?.*?\\?>|<%.*?%>' + // Processing instruction - '|' + - '' + // CData Block - '|' + - // Code span marker - '`+' + - ( !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 + '}~~~+[ ]*\\n' - : '' ) + // # End (if not is span). - ')', - 'm' - ); - - var depth = 0; // Current depth inside the tag tree. - var 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. - // - var parts_available = text.match(block_tag_re); //PREG_SPLIT_DELIM_CAPTURE - var parts; - if(!parts_available) { - parts = [text]; - } - else { - parts = [RegExp.leftContext, RegExp.lastMatch, RegExp.rightContext]; - } - - // If in Markdown span mode, add a empty-string span-level hash - // after each newline to prevent triggering any block element. - if(span) { - var _void = this.hashPart("", ':'); - var newline = _void + "\n"; - parts[0] = _void + parts[0].replace(/\n/g, newline) + _void; - } - - parsed += parts[0]; // Text before current tag. - - // If end of $text has been reached. Stop loop. - if(!parts_available) { - text = ""; - break; - } - - var tag = parts[1]; // Tag to handle. - text = parts[2]; // Remaining text after current tag. - var tag_re = this._php_preg_quote(tag); // For use in a regular expression. - - var t; - var block_text; - // - // Check for: Code span marker - // - if (tag.charAt(0) == "`") { - // Find corresponding end marker. - tag_re = this._php_preg_quote(tag); - if ( - text.substr(1).indexOf('`') != -1 && // [portiong note] To avoid JS's RegExp infinity loop. - (matches = text.match(new RegExp('^(.+?|\\n[?!\\n])*?[^`]' + tag_re + '[?!`]'))) - ) { - // End marker found: pass text unchanged until marker. - parsed += tag + matches[0]; - text = text.substr(matches[0].length); - } - else { - // Unmatched marker: just skip it. - parsed += tag; - } - } - // - // Check for: Fenced code block marker. - // - else if(tag.match(new RegExp('^\\n?[ ]{0,' + (indent + 3) + '}~'))) { - // Fenced code block marker: find matching end marker. - tag_re = this._php_preg_quote(this._php_trim(tag)); - if(matches = text.match(new RegExp('^(?:.*\\n)+?[ ]{0,' + indent + '}' + tag_re + '[ ]*\\n'))) { - // End marker found: pass text unchanged until marker. - parsed += tag + matches[0]; - text = text.substr(matches[0].length); - } - else { - // No end marker: just skip it. - parsed += tag; - } - } - // - // Check for: Indented code block. - // - else if(tag.charAt(0) == "\n" || tag.charAt(0) == " ") { - // Indented code block: pass it unchanged, will be handled - // later. - parsed += tag; - } - // - // Check for: Opening Block level tag or - // Opening Context Block tag (like ins and del) - // used as a block tag (tag is alone on it's line). - // - else if (tag.match(new RegExp('^<(?:' + this.block_tags_re + ')\\b')) || - ( - tag.match(new RegExp('^<(?:' + this.context_block_tags_re + ')\\b')) && - parsed.match(newline_before_re) && - text.match(newline_after_re) - ) - ) { - // Need to parse tag and following text using the HTML parser. - t = this._hashHTMLBlocks_inHTML(tag + text, this.hashBlock, true); - block_text = t[0]; - text = t[1]; - - // 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( - tag.match(new RegExp('^<(?:' + this.clean_tags_re + ')\\b')) || - tag.charAt(1) == '!' || tag.charAt(1) == '?' - ) { - // Need to parse tag and following text using the HTML parser. - // (don't check for markdown attribute) - t = this._hashHTMLBlocks_inHTML(tag + text, this.hashClean, false); - block_text = t[0]; - text = t[1]; - - parsed += block_text; - } - // - // Check for: Tag with same name as enclosing tag. - // - else if (enclosing_tag_re !== '' && - // Same name as enclosing tag. - tag.match(new RegExp('^= 0); - - return [parsed, text]; -}; - -/** - * Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags. - * - * * Calls $hash_method to convert any blocks. - * * Stops when the first opening tag closes. - * * $md_attr indicate if the use of the `markdown="1"` attribute is allowed. - * (it is not inside clean tags) - * - * Returns an array of that form: ( processed text , remaining text ) - */ -MarkdownExtra_Parser.prototype._hashHTMLBlocks_inHTML = function(text, hash_method, md_attr) { - if(text === '') return ['', '']; - - var matches; - - // Regex to match `markdown` attribute inside of a tag. - var markdown_attr_re = new RegExp( - '\\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) - ); - - // Regex to match any tag. - var tag_re = new RegExp( - '(' + // $2: Capture hole tag. - '`) - '\'.*?\'|' + // Single quotes (can contain `>`) - '.+?' + // Anything but quotes and `>`. - ')*?' + - ')?' + - '>' + // End of tag. - '|' + - '' + // HTML Comment - '|' + - '<\\?.*?\\?>|<%.*?%>' + // Processing instruction - '|' + - '' + // CData Block - ')' - ); - - var original_text = text; // Save original text in case of faliure. - - var depth = 0; // Current depth inside the tag tree. - var block_text = ""; // Temporary text holder for current text. - var parsed = ""; // Parsed text that will be returned. - - // - // Get the name of the starting tag. - // (This pattern makes $base_tag_name_re safe without quoting.) - // - var base_tag_name_re = ""; - if(matches = text.match(/^<([\w:$]*)\b/)) { - 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. - // - var parts_available = text.match(tag_re); //PREG_SPLIT_DELIM_CAPTURE); - // If end of $text has been reached. Stop loop. - if(!parts_available) { - // - // 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 [original_text.charAt(0), original_text.substr(1)]; - } - var parts = [RegExp.leftContext, RegExp.lastMatch, RegExp.rightContext]; - - block_text += parts[0]; // Text before current tag. - var 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(tag.match(new RegExp('^ 0) { - block_text = block_text.replace(new RegExp('/^[ ]{1,' + indent + '}', 'm'), ""); - } - - // 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 += hash_method.call(this, block_text); - - return [parsed, 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. - */ -MarkdownExtra_Parser.prototype.hashClean = function(text) { - return this.hashPart(text, 'C'); -}; - - -/** - * Redefined to add id attribute support. - */ -MarkdownExtra_Parser.prototype.doHeaders = function(text) { - var self = this; - - function _doHeaders_attr(attr) { - if('undefined' === typeof attr || attr == "") { return ""; } - return " id=\"" + attr + "\""; - } - - // Setext-style headers: - // Header 1 {#header1} - // ======== - // - // Header 2 {#header2} - // -------- - - text = text.replace(new RegExp( - '(^.+?)' + // $1: Header text - '(?:[ ]+\\{\\#([-_:a-zA-Z0-9]+)\\})?' + // $2: Id attribute - '[ ]*\\n(=+|-+)[ ]*\\n+', // $3: Header footer - 'mg' - ), function(match, span, id, line) { - //console.log(match); - if(line == '-' && span.match(/^- /)) { - return match; - } - var level = line.charAt(0) == '=' ? 1 : 2; - var attr = _doHeaders_attr(id); - var block = "" + self.runSpanGamut(span) + ""; - return "\n" + self.hashBlock(block) + "\n\n"; - }); - - // atx-style headers: - // # Header 1 {#header1} - // ## Header 2 {#header2} - // ## Header 2 with closing hashes ## {#header3} - // ... - // ###### Header 6 {#header2} - - text = text.replace(new RegExp( - '^(\\#{1,6})' + // $1 = string of #\'s - '[ ]*' + - '(.+?)' + // $2 = Header text - '[ ]*' + - '\\#*' + // optional closing #\'s (not counted) - '(?:[ ]+\\{\\#([-_:a-zA-Z0-9]+)\\})?' + // id attribute - '\\n+', - 'mg' - ), function(match, hashes, span, id) { - //console.log(match); - var level = hashes.length; - var attr = _doHeaders_attr(id); - var block = "" + self.runSpanGamut(span) + ""; - return "\n" + self.hashBlock(block) + "\n\n"; - }); - - return text; -}; - -/** - * Form HTML tables. - */ -MarkdownExtra_Parser.prototype.doTables = function(text) { - var self = this; - - var less_than_tab = this.tab_width - 1; - - var _doTable_callback = function(match, head, underline, content) { - //console.log(match); - // Remove any tailing pipes for each line. - head = head.replace(/[|] *$/m, ''); - underline = underline.replace(/[|] *$/m, ''); - content = content.replace(/[|] *$/m, ''); - - var attr = []; - - // Reading alignement from header underline. - var separators = underline.split(/[ ]*[|][ ]*/); - var n; - for(n = 0; n < separators.length; n++) { - var s = separators[n]; - if (s.match(/^ *-+: *$/)) { attr[n] = ' align="right"'; } - else if (s.match(/^ *:-+: *$/)) { attr[n] = ' align="center"'; } - else if (s.match(/^ *:-+ *$/)) { 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 = self.parseSpan(head); - var headers = head.split(/ *[|] */); - var col_count = headers.length; - - // Write column headers. - var text = "\n"; - text += "\n"; - text += "\n"; - for(n = 0; n < headers.length; n++) { - var header = headers[n]; - text += " " + self.runSpanGamut(self._php_trim(header)) + "\n"; - } - text += "\n"; - text += "\n"; - - // Split content by row. - var rows = self._php_trim(content, "\n").split("\n"); - - text += "\n"; - for(var i = 0; i < rows.length; i++) { - var row = rows[i]; - // Parsing span elements, including code spans, character escapes, - // and inline HTML tags, so that pipes inside those gets ignored. - row = self.parseSpan(row); - - // Split row by cell. - var row_cells = row.split(/ *[|] */, col_count); - while(row_cells.length < col_count) { row_cells.push(''); } - - text += "\n"; - for(n = 0; n < row_cells.length; n++) { - var cell = row_cells[n]; - text += " " + self.runSpanGamut(self._php_trim(cell)) + "\n"; - } - text += "\n"; - } - text += "\n"; - text += "
    "; - - return self.hashBlock(text) + "\n"; - }; - - text = this.__wrapSTXETX__(text); - - // - // Find tables with leading pipe. - // - // | Header 1 | Header 2 - // | -------- | -------- - // | Cell 1 | Cell 2 - // | Cell 3 | Cell 4 - // - text = text.replace(new RegExp( - '^' + // 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|\\x03)' , // Stop at final double newline. - 'mg' - ), function(match, head, underline, content) { - // Remove leading pipe for each row. - content = content.replace(/^ *[|]/m, ''); - - return _doTable_callback.call(this, match, head, underline, content); - }); - - // - // Find tables without leading pipe. - // - // Header 1 | Header 2 - // -------- | -------- - // Cell 1 | Cell 2 - // Cell 3 | Cell 4 - // - text = text.replace(new RegExp( - '^' + // 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|\\x03)' , // Stop at final double newline. - 'mg' - ), _doTable_callback); - - text = this.__unwrapSTXETX__(text); - - return text; -}; - -/** - * Form HTML definition lists. - */ -MarkdownExtra_Parser.prototype.doDefLists = function(text) { - var self = this; - - var less_than_tab = this.tab_width - 1; - - // Re-usable pattern to match any entire dl list: - var whole_list_re = '(?:' + - '(' + // $1 = whole list - '(' + // $2 - '[ ]{0,' + less_than_tab + '}' + - '((?:[ \\t]*\\S.*\\n)+)' + // $3 = defined term - // [porting note] Original regex from PHP is - // (?>.*\S.*\n), which matches a line with at - // least one non-space character. Change the - // first .* to [ \t]* stops unneccessary - // backtracking hence improves performance - '\\n?' + - '[ ]{0,' + less_than_tab + '}:[ ]+' + // colon starting definition - ')' + - '([\\s\\S]+?)' + - '(' + // $4 - '(?=\\0x03)' + // \z - '|' + - '(?=' + // [porting note] Our regex will consume leading - // newline characters so we will leave the newlines - // here for the next definition - '\\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 = this.__wrapSTXETX__(text); - text = text.replace(new RegExp( - '(\\x02\\n?|\\n\\n)' + - whole_list_re, 'mg' - ), function(match, pre, list) { - //console.log(match); - // Re-usable patterns to match list item bullets and number markers: - // [portiong note] changed to list = $2 in order to reserve previously \n\n. - - // Turn double returns into triple returns, so that we can make a - // paragraph for the last item in a list, if necessary: - var result = self._php_trim(self.processDefListItems(list)); - result = "
    \n" + result + "\n
    "; - return pre + self.hashBlock(result) + "\n\n"; - }); - text = this.__unwrapSTXETX__(text); - - return text; -}; - -/** - * Process the contents of a single definition list, splitting it - * into individual term and definition list items. - */ -MarkdownExtra_Parser.prototype.processDefListItems = function(list_str) { - var self = this; - - var less_than_tab = this.tab_width - 1; - - list_str = this.__wrapSTXETX__(list_str); - - // trim trailing blank lines: - list_str = list_str.replace(/\n{2,}(?=\\x03)/, "\n"); - - // Process definition terms. - list_str = list_str.replace(new RegExp( - '(\\x02\\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. - 'mg' - ), function(match, pre, terms_str) { - // [portiong note] changed to list = $2 in order to reserve previously \n\n. - var terms = self._php_trim(terms_str).split("\n"); - var text = ''; - for (var i = 0; i < terms.length; i++) { - var term = terms[i]; - term = self.runSpanGamut(self._php_trim(term)); - text += "\n
    " + term + "
    "; - } - return text + "\n"; - }); - - // Process actual definitions. - list_str = list_str.replace(new RegExp( - '\\n(\\n+)?' + // leading line = $1 - '(' + // marker space = $2 - '[ ]{0,' + less_than_tab + '}' + // whitespace before colon - '[:][ ]+' + // definition mark (colon) - ')' + - '([\\s\\S]+?)' + // definition text = $3 - // [porting note] Maybe no trailing - // newlines in our version, changed the - // following line from \n+ to \n*. - '(?=\\n*' + // stop at next definition mark, - '(?:' + // next term or end of text - '\\n[ ]{0,' + less_than_tab + '}[:][ ]|' + // [porting note] do not match - // colon in the middle of a line - '
    |\\x03' + // \z - ')' + - ')', - 'mg' - ), function(match, leading_line, marker_space, def) { - if (leading_line || def.match(/\n{2,}/)) { - // Replace marker with the appropriate whitespace indentation - def = self._php_str_repeat(' ', marker_space.length) + def; - def = self.runBlockGamut(self.outdent(def + "\n\n")); - def = "\n" + def + "\n"; - } - else { - def = self._php_rtrim(def); - def = self.runSpanGamut(self.outdent(def)); - } - - return "\n
    " + def + "
    \n"; - }); - - list_str = this.__unwrapSTXETX__(list_str); - - return list_str; -}; - -/** - * Adding the fenced code block syntax to regular Markdown: - * - * ~~~ - * Code block - * ~~~ - */ -MarkdownExtra_Parser.prototype.doFencedCodeBlocks = function(text) { - var self = this; - - var less_than_tab = this.tab_width; - - text = this.__wrapSTXETX__(text); - text = text.replace(new RegExp( - '(?:\\n|\\x02)' + - // 1: Opening marker - '(' + - '~{3,}' + // Marker: three tilde or more. - ')' + - '[ ]*\\n' + // Whitespace and newline following marker. - // 2: Content - '(' + - '(?:' + - '(?!\\1[ ]*\\n)' + // Not a closing marker. - '.*\\n+' + - ')+' + - ')' + - // Closing marker. - '\\1[ ]*\\n', - "mg" - ), function(match, m1, codeblock) { - codeblock = self._php_htmlspecialchars_ENT_NOQUOTES(codeblock); - codeblock = codeblock.replace(/^\n+/, function(match) { - return self._php_str_repeat("
    "; - return "\n\n" + self.hashBlock(codeblock) + "\n\n"; - }); - text = this.__unwrapSTXETX__(text); - - return text; -}; - -/** - * Params: - * $text - string to process with html

    tags - */ -MarkdownExtra_Parser.prototype.formParagraphs = function(text) { - - // Strip leading and trailing lines: - text = this.__wrapSTXETX__(text); - text = text.replace(/(?:\x02)\n+|\n+(?:\x03)/g, ""); - text = this.__unwrapSTXETX__(text); - - var grafs = text.split(/\n{2,}/m); - //preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); - - // - // Wrap

    tags and unhashify HTML blocks - // - for(var i = 0; i < grafs.length; i++) { - var value = grafs[i]; - if(value == "") { - // [porting note] - // This case is replacement for PREG_SPLIT_NO_EMPTY. - continue; - } - value = this._php_trim(this.runSpanGamut(value)); - - // Check if this should be enclosed in a paragraph. - // Clean tag hashes & block tag hashes are left alone. - var is_p = !value.match(/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/); - - if (is_p) { - value = "

    " + value + "

    "; - } - grafs[i] = value; - } - - // Join grafs in one text, then unhash HTML tags. - text = grafs.join("\n\n"); - - // Finish by removing any tag hashes still present in $text. - text = this.unhash(text); - - return text; -}; - -// ### Footnotes - -/** - * Strips link definitions from text, stores the URLs and titles in - * hash references. - */ -MarkdownExtra_Parser.prototype.stripFootnotes = function(text) { - var self = this; - - var less_than_tab = this.tab_width - 1; - - // Link defs are in the form: [^id]: url "optional title" - text = text.replace(new RegExp( - '^[ ]{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 - ')*' + - ')', - "mg" - ), function(match, m1, m2) { - var note_id = self.fn_id_prefix + m1; - self.footnotes[note_id] = self.outdent(m2); - return ''; //# String that will replace the block - }); - return text; -}; - -/** - * Replace footnote references in $text [^id] with a special text-token - * which will be replaced by the actual footnote marker in appendFootnotes. - */ -MarkdownExtra_Parser.prototype.doFootnotes = function(text) { - if (!this.in_anchor) { - text = text.replace(/\[\^(.+?)\]/g, "F\x1Afn:$1\x1A:"); - } - return text; -}; - -/** - * Append footnote list to text. - */ -MarkdownExtra_Parser.prototype.appendFootnotes = function(text) { - var self = this; - - var _appendFootnotes_callback = function(match, m1) { - var node_id = self.fn_id_prefix + m1; - - // Create footnote marker only if it has a corresponding footnote *and* - // the footnote hasn't been used by another marker. - if (node_id in self.footnotes) { - // Transfert footnote content to the ordered list. - self.footnotes_ordered.push([node_id, self.footnotes[node_id]]); - delete self.footnotes[node_id]; - - var num = self.footnote_counter++; - var attr = " rel=\"footnote\""; - if (self.fn_link_class != "") { - var classname = self.fn_link_class; - classname = self.encodeAttribute(classname); - attr += " class=\"" + classname + "\""; - } - if (self.fn_link_title != "") { - var title = self.fn_link_title; - title = self.encodeAttribute(title); - attr += " title=\"" + title +"\""; - } - - attr = attr.replace(/%%/g, num); - node_id = self.encodeAttribute(node_id); - - return "" + - "" + num + "" + - ""; - } - - return "[^" + m1 + "]"; - }; - - text = text.replace(/F\x1Afn:(.*?)\x1A:/g, _appendFootnotes_callback); - - if (this.footnotes_ordered.length > 0) { - text += "\n\n"; - text += "
    \n"; - text += " 0) { - var head = this.footnotes_ordered.shift(); - var note_id = head[0]; - var footnote = head[1]; - - footnote += "\n"; // Need to append newline before parsing. - footnote = this.runBlockGamut(footnote + "\n"); - footnote = footnote.replace(/F\x1Afn:(.*?)\x1A:/g, _appendFootnotes_callback); - - attr = attr.replace(/%%/g, ++num); - note_id = this.encodeAttribute(note_id); - - // Add backlink to last paragraph; create new paragraph if needed. - var backlink = ""; - if (footnote.match(/<\/p>$/)) { - footnote = footnote.substr(0, footnote.length - 4) + " " + backlink + "

    "; - } else { - footnote += "\n\n

    " + backlink + "

    "; - } - - text += "
  • \n"; - text += footnote + "\n"; - text += "
  • \n\n"; - } - - text += "\n"; - text += "
    "; - } - return text; -}; - -//### Abbreviations ### - -/** - * Strips abbreviations from text, stores titles in hash references. - */ -MarkdownExtra_Parser.prototype.stripAbbreviations = function(text) { - var self = this; - - var less_than_tab = this.tab_width - 1; - - // Link defs are in the form: [id]*: url "optional title" - text = text.replace(new RegExp( - '^[ ]{0,' + less_than_tab + '}\\*\\[(.+?)\\][ ]?:' + // abbr_id = $1 - '(.*)', // text = $2 (no blank lines allowed) - "mg" - ), function(match, abbr_word, abbr_desc) { - if (self.abbr_word_re != '') { - self.abbr_word_re += '|'; - } - self.abbr_word_re += self._php_preg_quote(abbr_word); - self.abbr_desciptions[abbr_word] = self._php_trim(abbr_desc); - return ''; // String that will replace the block - }); - return text; -}; - -/** - * Find defined abbreviations in text and wrap them in elements. - */ -MarkdownExtra_Parser.prototype.doAbbreviations = function(text) { - var self = this; - - if (this.abbr_word_re) { - // cannot use the /x modifier because abbr_word_re may - // contain significant spaces: - text = text.replace(new RegExp( - '(^|[^\\w\\x1A])' + - '(' + this.abbr_word_re + ')' + - '(?![\\w\\x1A])' - , 'g' - ), function(match, prev, abbr) { - if (abbr in self.abbr_desciptions) { - var desc = self.abbr_desciptions[abbr]; - if (!desc || desc == "") { - return self.hashPart(prev + "" + abbr + ""); - } else { - desc = self.encodeAttribute(desc); - return self.hashPart(prev + "" + abbr + ""); - } - } else { - return match; - } - }); - } - return text; -}; - -function MarkdownExtraExtended_Parser () { - this.block_tags_re = 'figure|figcaption|p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend'; - this.document_gamut.push(['doClearBreaks', 100]); -} -MarkdownExtraExtended_Parser.prototype = new MarkdownExtra_Parser(); - -MarkdownExtraExtended_Parser.prototype.doHardBreaks = function(text) { - var self = this; - return text.replace(/ *\n/mg, function(match) { - //console.log(match); - return self.hashPart("\\n)+)\\s*([ ]?' - + '(?:\\((.+?)\\))?' - + '[ ]*(.+\\n(?:.+\\n)*)' - + ')+', 'mg'), function (matches, cite, bq) { - bq = '> ' + bq; - - bq = bq.replace(/^[ ]*>[ ]?|^[ ]+$/mg, ''); - bq = self.runBlockGamut(bq); // recurse - - bq = bq.replace(/^/mg, " "); - // These leading spaces cause problem with
     content, 
    -            // so we need to fix that:
    -            bq = bq.replace(/(\\s*
    [\\s\\S]+?<\/pre>)/mg, function(match, pre) {
    -                //console.log(match);
    -                pre = pre.replace(/^  /m, '');
    -                return pre;
    -            });
    -
    -            return "\n" + self.hashBlock("" : ' cite="' + cite + '">')
    -                + "\n" + bq + "\n") + "\n\n";
    -        });
    -}
    -
    -MarkdownExtraExtended_Parser.prototype.doFencedCodeBlocks = function (text) {
    -    var self = this;
    -
    -    var less_than_tab = this.tab_width;
    -
    -    text = this.__wrapSTXETX__(text);
    -    text = text.replace(new RegExp(
    -        '(?:\\n|\\x02)'          +
    -        // 1: Opening marker
    -        '('                      +
    -            '~{3,}|`{3,}'        + // Marker: three tilde or more.
    -        ')'                      +
    -        '[ ]?(\\w+)?(?:,[ ]?(\\d+))?[ ]*\\n'                + // Whitespace and newline following marker.
    -        // 2: Content
    -        '('                      +
    -            '(?:'                +
    -                '(?!\\1[ ]*\\n)' + // Not a closing marker.
    -                '.*\\n+'         +
    -            ')+'                 +
    -        ')'                      +
    -        // Closing marker.
    -        '\\1[ ]*\\n',
    -        "mg"
    -    ), function(match, m1, m2, m3, m4) {
    -        var codeblock = self.unhash(m4);
    -        codeblock = self._php_htmlspecialchars_ENT_NOQUOTES(codeblock);
    -        codeblock = codeblock.replace(/^\n+/, function(match) {
    -            return self._php_str_repeat("" : " class=\"lang-" + m2 + "\">";
    -        cb += codeblock + '
    '; - - return "\n\n" + self.hashBlock(cb) + "\n\n"; - }); - text = this.__unwrapSTXETX__(text); - - return text; -} - -/** - * Export to Node.js - */ -if (typeof exports != 'undefined') { - exports.Markdown = Markdown; - exports.Markdown_Parser = Markdown_Parser; - exports.MarkdownExtra_Parser = MarkdownExtra_Parser; - exports.MarkdownExtraExtended_Parser = MarkdownExtraExtended_Parser; -} diff --git a/admin/js/typecho.js b/admin/js/typecho.js index ac9652d0..0a89c27e 100644 --- a/admin/js/typecho.js +++ b/admin/js/typecho.js @@ -168,32 +168,6 @@ }; })($); -(function () { - - Typecho.Markdown = function () { - this.hooks = new Markdown.HookCollection(); - this.hooks.addNoop('postConversion'); - this.lastIt = null; - }; - - Typecho.Markdown.prototype.makeHtml = function (text, preview) { - var self = this; - - if (this.lastIt) { - clearTimeout(this.lastIt); - this.lastIt = null; - } - - this.lastIt = setTimeout(function () { - html = MarkdownText(text); - preview(self.hooks.postConversion(html)); - }, 500); - - return false; - }; - -})(); - /** * TableDnD plug-in for JQuery, allows you to drag and drop table rows diff --git a/var/MarkdownExtraExtended.php b/var/MarkdownExtraExtended.php deleted file mode 100644 index 6a5ac418..00000000 --- a/var/MarkdownExtraExtended.php +++ /dev/null @@ -1,3289 +0,0 @@ - -# -# Original Markdown -# Copyright (c) 2004-2006 John Gruber -# -# - -# -# Markdown Parser Class -# - -class _Markdown { - - ### Version ### - - const MARKDOWNLIB_VERSION = "1.3"; - - ### Configuration Variables ### - - # Change to ">" for HTML output. - public $empty_element_suffix = " />"; - public $tab_width = 4; - - # Change to `true` to disallow markup or entities. - public $no_markup = false; - public $no_entities = false; - - # Predefined urls and titles for reference links and images. - public $predef_urls = array(); - public $predef_titles = array(); - - - ### Parser Implementation ### - - # Regex to match balanced [brackets]. - # Needed to insert a maximum bracked depth while converting to PHP. - protected $nested_brackets_depth = 6; - protected $nested_brackets_re; - - protected $nested_url_parenthesis_depth = 4; - protected $nested_url_parenthesis_re; - - # Table of hash values for escaped characters: - protected $escape_chars = '\`*_{}[]()>#+-.!'; - protected $escape_chars_re; - - - public function __construct() { - # - # Constructor function. Initialize appropriate member variables. - # - $this->_initDetab(); - $this->prepareItalicsAndBold(); - - $this->nested_brackets_re = - str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth). - str_repeat('\])*', $this->nested_brackets_depth); - - $this->nested_url_parenthesis_re = - str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth). - str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth); - - $this->escape_chars_re = '['.preg_quote($this->escape_chars).']'; - - # Sort document, block, and span gamut in ascendent priority order. - asort($this->document_gamut); - asort($this->block_gamut); - asort($this->span_gamut); - } - - - # Internal hashes used during transformation. - protected $urls = array(); - protected $titles = array(); - protected $html_hashes = array(); - - # Status flag to avoid invalid nesting. - protected $in_anchor = false; - - - protected function setup() { - # - # Called before the transformation process starts to setup parser - # states. - # - # Clear global hashes. - $this->urls = $this->predef_urls; - $this->titles = $this->predef_titles; - $this->html_hashes = array(); - - $this->in_anchor = false; - } - - protected function teardown() { - # - # Called after the transformation process to clear any variable - # which may be taking up memory unnecessarly. - # - $this->urls = array(); - $this->titles = array(); - $this->html_hashes = array(); - } - - - public function transform($text) { - # - # Main function. Performs some preprocessing on the input text - # and pass it through the document gamut. - # - $this->setup(); - - # Remove UTF-8 BOM and marker character in input, if present. - $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text); - - # Standardize line endings: - # DOS to Unix and Mac to Unix - $text = preg_replace('{\r\n?}', "\n", $text); - - # Make sure $text ends with a couple of newlines: - $text .= "\n\n"; - - # Convert all tabs to spaces. - $text = $this->detab($text); - - # Turn block-level HTML blocks into hash entries - $text = $this->hashHTMLBlocks($text); - - # Strip any lines consisting only of spaces and tabs. - # This makes subsequent regexen easier to write, because we can - # match consecutive blank lines with /\n+/ instead of something - # contorted like /[ ]*\n+/ . - $text = preg_replace('/^[ ]+$/m', '', $text); - - # Run document gamut methods. - foreach ($this->document_gamut as $method => $priority) { - $text = $this->$method($text); - } - - $this->teardown(); - - $text = preg_replace_callback("/<\/?(\!doctype|html|head|body|link|title|input|select|button|textarea|style|noscript)[^>]*>/is", - array($this, '_doEscape_callback'), $text); - - return $text . "\n"; - } - - /** - * _doEscape_callback - * - * @param array $matches - * @access protected - * @return string - */ - protected function _doEscape_callback($matches) { - return htmlspecialchars($matches[0], ENT_NOQUOTES); - } - - protected $document_gamut = array( - # Strip link definitions, store in hashes. - "stripLinkDefinitions" => 20, - - "runBasicBlockGamut" => 30, - ); - - - protected function stripLinkDefinitions($text) { - # - # Strips link definitions from text, stores the URLs and titles in - # hash references. - # - $less_than_tab = $this->tab_width - 1; - - # Link defs are in the form: ^[id]: url "optional title" - $text = preg_replace_callback('{ - ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 - [ ]* - \n? # maybe *one* newline - [ ]* - (?: - <(.+?)> # url = $2 - | - (\S+?) # url = $3 - ) - [ ]* - \n? # maybe one newline - [ ]* - (?: - (?<=\s) # lookbehind for whitespace - ["(] - (.*?) # title = $4 - [")] - [ ]* - )? # title is optional - (?:\n+|\Z) - }xm', - array($this, '_stripLinkDefinitions_callback'), - $text); - return $text; - } - protected function _stripLinkDefinitions_callback($matches) { - $link_id = strtolower($matches[1]); - $url = $matches[2] == '' ? $matches[3] : $matches[2]; - $this->urls[$link_id] = $url; - $this->titles[$link_id] =& $matches[4]; - return ''; # String that will replace the block - } - - - protected function hashHTMLBlocks($text) { - if ($this->no_markup) return $text; - - $less_than_tab = $this->tab_width - 1; - - # Hashify HTML blocks: - # We only want to do this for block-level HTML tags, such as headers, - # lists, and tables. That's because we still want to wrap

    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(' - # 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 - # the matching end tag - [ ]* # trailing spaces/tabs - (?=\n+|\Z) # followed by a newline or end of document - - | # Special version for tags of group a. - - [ ]{0,'.$less_than_tab.'} - <('.$block_tags_a_re.')# start tag = $3 - '.$attr.'>[ ]*\n # attributes followed by > - '.$content2.' # content, support nesting - # the matching end tag - [ ]* # trailing spaces/tabs - (?=\n+|\Z) # followed by a newline or end of document - - | # Special case just for
    . It was easier to make a special - # case than to make the other regex more complicated. - - [ ]{0,'.$less_than_tab.'} - <(hr) # start tag = $2 - '.$attr.' # attributes - /?> # the matching end tag - [ ]* - (?=\n{2,}|\Z) # followed by a blank line or end of document - - | # Special case for standalone HTML comments: - - [ ]{0,'.$less_than_tab.'} - (?s: - - ) - [ ]* - (?=\n{2,}|\Z) # followed by a blank line or end of document - - | # PHP and ASP-style processor instructions ( - ) - [ ]* - (?=\n{2,}|\Z) # followed by a blank line or end of document - - ) - )}Sxmi', - array($this, '_hashHTMLBlocks_callback'), - $text); - - return $text; - } - protected function _hashHTMLBlocks_callback($matches) { - $text = $matches[1]; - return $this->hashBlock($text); - // return "\n\n$key\n\n"; - } - - - protected function hashPart($text, $boundary = 'X') { - # - # Called whenever a tag must be hashed when a function insert an atomic - # element in the text stream. Passing $text to through this function gives - # a unique text-token which will be reverted back when calling unhash. - # - # The $boundary argument specify what character should be used to surround - # the token. By convension, "B" is used for block elements that needs not - # to be wrapped into paragraph tags at the end, ":" is used for elements - # that are word separators and "X" is used in the general case. - # - # Swap back any tag hash found in $text so we do not have to `unhash` - # multiple times at the end. - $text = $this->unhash($text); - - # Then hash the block. - static $i = 0; - $key = "$boundary\x1A" . ++$i . $boundary; - $this->html_hashes[$key] = $text; - return $key; # String that will replace the tag. - } - - - protected function hashBlock($text) { - # - # Shortcut function for hashPart with block-level boundaries. - # - return $this->hashPart($text, 'B'); - } - - - protected $block_gamut = array( - # - # These are all the transformations that form block-level - # tags like paragraphs, headers, and list items. - # - "doHeaders" => 10, - "doHorizontalRules" => 20, - - "doLists" => 40, - "doCodeBlocks" => 50, - "doBlockQuotes" => 60, - ); - - protected function runBlockGamut($text) { - # - # Run block gamut tranformations. - # - # We need to escape raw HTML in Markdown source before doing anything - # else. This need to be done for each block, and not only at the - # begining in the Markdown function since hashed blocks can be part of - # list items and could have been indented. Indented blocks would have - # been seen as a code block in a previous pass of hashHTMLBlocks. - $text = $this->hashHTMLBlocks($text); - - return $this->runBasicBlockGamut($text); - } - - protected function runBasicBlockGamut($text) { - # - # Run block gamut tranformations, without hashing HTML blocks. This is - # useful when HTML blocks are known to be already hashed, like in the first - # whole-document pass. - # - foreach ($this->block_gamut as $method => $priority) { - $text = $this->$method($text); - } - - # Finally form paragraph and restore hashed blocks. - $text = $this->formParagraphs($text); - - return $text; - } - - - protected function doHorizontalRules($text) { - # Do Horizontal Rules: - return preg_replace( - '{ - ^[ ]{0,3} # Leading space - ([-*_]) # $1: First marker - (?> # Repeated marker group - [ ]{0,2} # Zero, one, or two spaces. - \1 # Marker character - ){2,} # Group repeated at least twice - [ ]* # Tailing spaces - $ # End of line. - }mx', - "\n".$this->hashBlock("empty_element_suffix")."\n", - $text); - } - - - protected $span_gamut = array( - # - # These are all the transformations that occur *within* block-level - # tags like paragraphs, headers, and list items. - # - # Process character escapes, code spans, and inline HTML - # in one shot. - "parseSpan" => -30, - - # Process anchor and image tags. Images must come first, - # because ![foo][f] looks like an anchor. - "doImages" => 10, - "doAnchors" => 20, - - # Make links out of things like `` - # Must come after doAnchors, because you can use < and > - # delimiters in inline links like [this](). - "doAutoLinks" => 30, - "encodeAmpsAndAngles" => 40, - - "doItalicsAndBold" => 50, - "doHardBreaks" => 60, - ); - - protected function runSpanGamut($text) { - # - # Run span gamut tranformations. - # - foreach ($this->span_gamut as $method => $priority) { - $text = $this->$method($text); - } - - return $text; - } - - - protected function doHardBreaks($text) { - # Do hard breaks: - return preg_replace_callback('/ {2,}\n/', - array($this, '_doHardBreaks_callback'), $text); - } - protected function _doHardBreaks_callback($matches) { - return $this->hashPart("empty_element_suffix\n"); - } - - - protected 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; - } - protected function _doAnchors_reference_callback($matches) { - $whole_match = $matches[1]; - $link_text = $matches[2]; - $link_id =& $matches[3]; - - if ($link_id == "") { - # for shortcut links like [this][] or [this]. - $link_id = $link_text; - } - - # lower-case and turn embedded newlines into spaces - $link_id = strtolower($link_id); - $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); - - if (isset($this->urls[$link_id])) { - $url = $this->urls[$link_id]; - $url = $this->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; - } - protected function _doAnchors_inline_callback($matches) { - $whole_match = $matches[1]; - $link_text = $this->runSpanGamut($matches[2]); - $url = $matches[3] == '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - - $url = $this->encodeAttribute($url); - - $result = "encodeAttribute($title); - $result .= " title=\"$title\""; - } - - $link_text = $this->runSpanGamut($link_text); - $result .= ">$link_text"; - - return $this->hashPart($result); - } - - - protected 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; - } - protected function _doImages_reference_callback($matches) { - $whole_match = $matches[1]; - $alt_text = $matches[2]; - $link_id = strtolower($matches[3]); - - if ($link_id == "") { - $link_id = strtolower($alt_text); # for shortcut links like ![this][]. - } - - $alt_text = $this->encodeAttribute($alt_text); - if (isset($this->urls[$link_id])) { - $url = $this->encodeAttribute($this->urls[$link_id]); - $result = "\"$alt_text\"";titles[$link_id])) { - $title = $this->titles[$link_id]; - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; - } - $result .= $this->empty_element_suffix; - $result = $this->hashPart($result); - } - else { - # If there's no such link ID, leave intact: - $result = $whole_match; - } - - return $result; - } - protected function _doImages_inline_callback($matches) { - $whole_match = $matches[1]; - $alt_text = $matches[2]; - $url = $matches[3] == '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - - $alt_text = $this->encodeAttribute($alt_text); - $url = $this->encodeAttribute($url); - $result = "\"$alt_text\"";encodeAttribute($title); - $result .= " title=\"$title\""; # $title already quoted - } - $result .= $this->empty_element_suffix; - - return $this->hashPart($result); - } - - - protected function doHeaders($text) { - # Setext-style headers: - # Header 1 - # ======== - # - # Header 2 - # -------- - # - $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', - array($this, '_doHeaders_callback_setext'), $text); - - # atx-style headers: - # # Header 1 - # ## Header 2 - # ## Header 2 with closing hashes ## - # ... - # ###### Header 6 - # - $text = preg_replace_callback('{ - ^(\#{1,6}) # $1 = string of #\'s - [ ]* - (.+?) # $2 = Header text - [ ]* - \#* # optional closing #\'s (not counted) - \n+ - }xm', - array($this, '_doHeaders_callback_atx'), $text); - - return $text; - } - protected function _doHeaders_callback_setext($matches) { - # Terrible hack to check we haven't found an empty list item. - if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1])) - return $matches[0]; - - $level = $matches[2]{0} == '=' ? 1 : 2; - $block = "".$this->runSpanGamut($matches[1]).""; - return "\n" . $this->hashBlock($block) . "\n\n"; - } - protected function _doHeaders_callback_atx($matches) { - $level = strlen($matches[1]); - $block = "".$this->runSpanGamut($matches[2]).""; - return "\n" . $this->hashBlock($block) . "\n\n"; - } - - - protected function doLists($text) { - # - # Form HTML ordered (numbered) and unordered (bulleted) lists. - # - $less_than_tab = $this->tab_width - 1; - - # Re-usable patterns to match list item bullets and number markers: - $marker_ul_re = '[*+-]'; - $marker_ol_re = '\d+[\.]'; - $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; - } - protected function _doLists_callback($matches) { - # Re-usable patterns to match list item bullets and number markers: - $marker_ul_re = '[*+-]'; - $marker_ol_re = '\d+[\.]'; - $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; - - $list = $matches[1]; - $list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol"; - - $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re ); - - $list .= "\n"; - $result = $this->processListItems($list, $marker_any_re); - - $result = $this->hashBlock("<$list_type>\n" . $result . ""); - return "\n". $result ."\n\n"; - } - - protected $list_level = 0; - - protected function processListItems($list_str, $marker_any_re) { - # - # Process the contents of a single ordered or unordered list, splitting it - # into individual list items. - # - # The $this->list_level global keeps track of when we're inside a list. - # Each time we enter a list, we increment it; when we leave a list, - # we decrement. If it's zero, we're not in a list anymore. - # - # We do this because when we're not inside a list, we want to treat - # something like this: - # - # I recommend upgrading to version - # 8. Oops, now this line is treated - # as a sub-list. - # - # As a single paragraph, despite the fact that the second line starts - # with a digit-period-space sequence. - # - # Whereas when we're inside a list (or sub-list), that line will be - # treated as the start of a sub-list. What a kludge, huh? This is - # an aspect of Markdown's syntax that's hard to parse perfectly - # without resorting to mind-reading. Perhaps the solution is to - # change the syntax rules such that sub-lists must start with a - # starting cardinal number; e.g. "1." or "a.". - - $this->list_level++; - - # trim trailing blank lines: - $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); - - $list_str = preg_replace_callback('{ - (\n)? # leading line = $1 - (^[ ]*) # leading whitespace = $2 - ('.$marker_any_re.' # list marker and space = $3 - (?:[ ]+|(?=\n)) # space only required if item is not empty - ) - ((?s:.*?)) # list item text = $4 - (?:(\n+(?=\n))|\n) # tailing blank line = $5 - (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n)))) - }xm', - array($this, '_processListItems_callback'), $list_str); - - $this->list_level--; - return $list_str; - } - protected function _processListItems_callback($matches) { - $item = $matches[4]; - $leading_line =& $matches[1]; - $leading_space =& $matches[2]; - $marker_space = $matches[3]; - $tailing_blank_line =& $matches[5]; - - if ($leading_line || $tailing_blank_line || - preg_match('/\n{2,}/', $item)) - { - # Replace marker with the appropriate whitespace indentation - $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item; - $item = $this->runBlockGamut($this->outdent($item)."\n"); - } - else { - # Recursion for sub-lists: - $item = $this->doLists($this->outdent($item)); - $item = preg_replace('/\n+$/', '', $item); - $item = $this->runSpanGamut($item); - } - - return "
  • " . $item . "
  • \n"; - } - - - protected 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;
    -	}
    -	protected function _doCodeBlocks_callback($matches) {
    -		$codeblock = $this->unhash($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"; - } - - - protected function makeCodeSpan($code) { - # - # Create a code span markup for $code. Called from handleSpanToken. - # - $code = htmlspecialchars(trim($code), ENT_NOQUOTES); - return $this->hashPart("$code"); - } - - - protected $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; - } - } - } - - protected function doItalicsAndBold($text) { - $token_stack = array(''); - $text_stack = array(''); - $em = ''; - $strong = ''; - $tree_char_em = false; - - while (1) { - # - # Get prepared regular expression for seraching emphasis tokens - # in current context. - # - $token_re = $this->em_strong_prepared_relist["$em$strong"]; - - # - # Each loop iteration search for the next emphasis token. - # Each token is then passed to handleSpanToken. - # - $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); - $text_stack[0] .= $parts[0]; - $token =& $parts[1]; - $text =& $parts[2]; - - if (empty($token)) { - # Reached end of text span: empty stack without emitting. - # any more emphasis. - while ($token_stack[0]) { - $text_stack[1] .= array_shift($token_stack); - $text_stack[0] .= array_shift($text_stack); - } - break; - } - - $token_len = strlen($token); - if ($tree_char_em) { - # Reached closing marker while inside a three-char emphasis. - if ($token_len == 3) { - # Three-char closing marker, close em and strong. - array_shift($token_stack); - $span = array_shift($text_stack); - $span = $this->runSpanGamut($span); - $span = "$span"; - $text_stack[0] .= $this->hashPart($span); - $em = ''; - $strong = ''; - } else { - # Other closing marker: close one em or strong and - # change current token state to match the other - $token_stack[0] = str_repeat($token{0}, 3-$token_len); - $tag = $token_len == 2 ? "strong" : "em"; - $span = $text_stack[0]; - $span = $this->runSpanGamut($span); - $span = "<$tag>$span"; - $text_stack[0] = $this->hashPart($span); - $$tag = ''; # $$tag stands for $em or $strong - } - $tree_char_em = false; - } else if ($token_len == 3) { - if ($em) { - # Reached closing marker for both em and strong. - # Closing strong marker: - for ($i = 0; $i < 2; ++$i) { - $shifted_token = array_shift($token_stack); - $tag = strlen($shifted_token) == 2 ? "strong" : "em"; - $span = array_shift($text_stack); - $span = $this->runSpanGamut($span); - $span = "<$tag>$span"; - $text_stack[0] .= $this->hashPart($span); - $$tag = ''; # $$tag stands for $em or $strong - } - } else { - # Reached opening three-char emphasis marker. Push on token - # stack; will be handled by the special condition above. - $em = $token{0}; - $strong = "$em$em"; - array_unshift($token_stack, $token); - array_unshift($text_stack, ''); - $tree_char_em = true; - } - } else if ($token_len == 2) { - if ($strong) { - # Unwind any dangling emphasis marker: - if (strlen($token_stack[0]) == 1) { - $text_stack[1] .= array_shift($token_stack); - $text_stack[0] .= array_shift($text_stack); - } - # 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]; - } - - - protected function doBlockQuotes($text) { - $text = preg_replace_callback('/ - ( # Wrap whole match in $1 - (?> - ^[ ]*>[ ]? # ">" at the start of a line - .+\n # rest of the first line - (.+\n)* # subsequent consecutive lines - \n* # blanks - )+ - ) - /xm', - array($this, '_doBlockQuotes_callback'), $text); - - return $text; - } - protected function _doBlockQuotes_callback($matches) { - $bq = $matches[1]; - # trim one level of quoting - trim whitespace-only lines - $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq); - $bq = $this->runBlockGamut($bq); # recurse - - $bq = preg_replace('/^/m', " ", $bq); - # These leading spaces cause problem with
     content, 
    -		# so we need to fix that:
    -		$bq = preg_replace_callback('{(\s*
    .+?
    )}sx', - array($this, '_doBlockQuotes_callback2'), $bq); - - return "\n". $this->hashBlock("
    \n$bq\n
    ")."\n\n"; - } - protected function _doBlockQuotes_callback2($matches) { - $pre = $matches[1]; - $pre = preg_replace('/^ /m', '', $pre); - return $pre; - } - - - protected function formParagraphs($text) { - # - # Params: - # $text - string to process with html

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

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

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

    "; - $grafs[$key] = $this->unhash($value); - } - else { - # Is a block. - # Modify elements of @grafs in-place... - $graf = $value; - $block = $this->html_hashes[$graf]; - $graf = $block; -// if (preg_match('{ -// \A -// ( # $1 =
    tag -//
    ]* -// \b -// markdown\s*=\s* ([\'"]) # $2 = attr quote char -// 1 -// \2 -// [^>]* -// > -// ) -// ( # $3 = contents -// .* -// ) -// (
    ) # $4 = closing tag -// \z -// }xs', $block, $matches)) -// { -// list(, $div_open, , $div_content, $div_close) = $matches; -// -// # We can't call Markdown(), because that resets the hash; -// # that initialization code should be pulled into its own sub, though. -// $div_content = $this->hashHTMLBlocks($div_content); -// -// # Run document gamut methods on the content. -// foreach ($this->document_gamut as $method => $priority) { -// $div_content = $this->$method($div_content); -// } -// -// $div_open = preg_replace( -// '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open); -// -// $graf = $div_open . "\n" . $div_content . "\n" . $div_close; -// } - $grafs[$key] = $graf; - } - } - - return implode("\n\n", $grafs); - } - - - protected function encodeAttribute($text) { - # - # Encode text for a double-quoted HTML attribute. This function - # is *not* suitable for attributes enclosed in single quotes. - # - $text = $this->encodeAmpsAndAngles($text); - $text = str_replace('"', '"', $text); - return $text; - } - - - protected function encodeAmpsAndAngles($text) { - # - # Smart processing for ampersands and angle brackets that need to - # be encoded. Valid character entities are left alone unless the - # no-entities mode is set. - # - if ($this->no_entities) { - $text = str_replace('&', '&', $text); - } else { - # Ampersand-encoding based entirely on Nat Irons's Amputator - # MT plugin: - $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', - '&', $text);; - } - # Encode remaining <'s - $text = str_replace('<', '<', $text); - - return $text; - } - - - protected function doAutoLinks($text) { - $text = preg_replace_callback('/(="|<)?\b(https?|ftp)(:\/\/[-A-Z0-9+&@#\/%?=~_|\[\]\(\)!:,\.;]*[-A-Z0-9+&@#\/%=~_|\[\])])(?=$|\W)/i', - array($this, '_doAutoLinks_url_callback'), $text); - $text = preg_replace_callback('{<((https?|ftp|dict):[^\'">\s]+)>}i', - array($this, '_doAutoLinks_url_callback_replace'), $text); - - # Email addresses: - $text = preg_replace_callback('{ - < - (?:mailto:)? - ( - (?: - [-!#$%&\'*+/=?^_`.{|}~\w\x80-\xFF]+ - | - ".*?" - ) - \@ - (?: - [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+ - | - \[[\d.a-fA-F:]+\] # IPv4 & IPv6 - ) - ) - > - }xi', - array($this, '_doAutoLinks_email_callback'), $text); - - return $text; - } - protected function _doAutoLinks_url_callback_replace($matches) { - $url = $this->encodeAttribute($matches[1]); - $link = "$url"; - return $this->hashPart($link); - } - - private $tail; - - protected function _doAutoLinks_url_callback($matches) { - list ($wholeMatch, $lookbehind, $protocol, $link) = $matches; - - if ($lookbehind) { - return $wholeMatch; - } - - if ($link[strlen($link) - 1] != ')') { - return '<' . $protocol . $link . '>'; - } - - $level = 0; - if (preg_match_all("/[()]/", $link, $matches)) { - foreach ($matches[0] as $op) { - if ('(' == $op) { - if ($level <= 0) { - $level = 1; - } else { - $level ++; - } - } else { - $level --; - } - } - } - - $this->tail = ''; - if ($level < 0) { - $link = preg_replace_callback("/\){1," . (- $level) . "}$/", - array($this, '_doAutoLinks_url_callback_callback'), $link); - } - - $url = $this->encodeAttribute($protocol . $link); - $link = "$url"; - return $this->hashPart($link) . $this->tail; - } - - protected function _doAutoLinks_email_callback($matches) { - $address = $matches[1]; - $link = $this->encodeEmailAddress($address); - return $this->hashPart($link); - } - - protected function _doAutoLinks_url_callback_callback($matches) { - $this->tail = $matches[0]; - return ''; - } - - protected function encodeEmailAddress($addr) { - # - # Input: an email address, e.g. "foo@example.com" - # - # Output: the email address as a mailto link, with each character - # of the address encoded as either a decimal or hex entity, in - # the hopes of foiling most address harvesting spam bots. E.g.: - # - #

    foo@exampl - # e.com

    - # - # Based by a filter by Matthew Wickline, posted to BBEdit-Talk. - # With some optimizations by Milian Wolff. - # - $addr = "mailto:" . $addr; - $chars = preg_split('/(? $char) { - $ord = ord($char); - # Ignore non-ascii chars. - if ($ord < 128) { - $r = ($seed * (1 + $key)) % 100; # Pseudo-random function. - # roughly 10% raw, 45% hex, 45% dec - # '@' *must* be encoded. I insist. - if ($r > 90 && $char != '@') /* do nothing */; - else if ($r < 45) $chars[$key] = '&#x'.dechex($ord).';'; - else $chars[$key] = '&#'.$ord.';'; - } - } - - $addr = implode('', $chars); - $text = implode('', array_slice($chars, 7)); # text without `mailto:` - $addr = "$text"; - - return $addr; - } - - - protected function parseSpan($str) { - # - # Take the string $str and parse it into tokens, hashing embeded HTML, - # escaped characters and handling code spans. - # - $output = ''; - - $span_re = '{ - ( - \\\\'.$this->escape_chars_re.' - | - (?no_markup ? '' : ' - | - # comment - | - <\?.*?\?> | <%.*?%> # processing instruction - | - <[!$]?[-a-zA-Z0-9:_]+ # regular tags - (?> - \s - (?>[^"\'>]+|"[^"]*"|\'[^\']*\')* - )? - > - | - <[-a-zA-Z0-9:_]+\s*/> # xml-style empty tag - | - # closing tag - ').' - ) - }xs'; - - while (1) { - # - # Each loop iteration seach for either the next tag, the next - # openning code span marker, or the next escaped character. - # Each token is then passed to handleSpanToken. - # - $parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE); - - # Create token from text preceding tag. - if ($parts[0] != "") { - $output .= $parts[0]; - } - - # Check if we reach the end. - if (isset($parts[1])) { - $output .= $this->handleSpanToken($parts[1], $parts[2]); - $str = $parts[2]; - } - else { - break; - } - } - - return $output; - } - - - protected function handleSpanToken($token, &$str) { - # - # Handle $token provided by parseSpan by determining its nature and - # returning the corresponding value that should replace it. - # - switch ($token{0}) { - case "\\": - return $this->hashPart("&#". ord($token{1}). ";"); - case "`": - # Search for end marker in remaining text. - if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm', - $str, $matches)) - { - $str = $matches[2]; - $codespan = $this->makeCodeSpan($matches[1]); - return $this->hashPart($codespan); - } - return $token; // return as text since no ending marker found. - default: - return $this->hashPart($token); - } - } - - - protected function outdent($text) { - # - # Remove one level of line-leading tabs or spaces - # - return preg_replace('/^(\t|[ ]{1,'.$this->tab_width.'})/m', '', $text); - } - - - # String length function for detab. `_initDetab` will create a function to - # hanlde UTF-8 if the default function does not exist. - protected $utf8_strlen = 'mb_strlen'; - - protected function detab($text) { - # - # Replace tabs with the appropriate amount of space. - # - # For each line we separate the line in blocks delemited by - # tab characters. Then we reconstruct every line by adding the - # appropriate number of space between each blocks. - - $text = preg_replace_callback('/^.*\t.*$/m', - array($this, '_detab_callback'), $text); - - return $text; - } - protected function _detab_callback($matches) { - $line = $matches[0]; - $strlen = $this->utf8_strlen; # strlen function for UTF-8. - - # Split in blocks. - $blocks = explode("\t", $line); - # Add each blocks to the line. - $line = $blocks[0]; - unset($blocks[0]); # Do not add first block twice. - foreach ($blocks as $block) { - # Calculate amount of space, insert spaces, insert block. - $amount = $this->tab_width - - call_user_func($strlen, $line, 'UTF-8') % $this->tab_width; - $line .= str_repeat(" ", $amount) . $block; - } - return $line; - } - protected function _initDetab() { - # - # Check for the availability of the function in the `utf8_strlen` property - # (initially `mb_strlen`). If the function is not available, create a - # function that will loosely count the number of UTF-8 characters with a - # regular expression. - # - if (function_exists($this->utf8_strlen)) return; - $this->utf8_strlen = array($this, '_utf8_strlen'); - } - - protected function _utf8_strlen($text) { - return preg_match_all( - "/[\\x00-\\xBF]|[\\xC0-\\xFF][\\x80-\\xBF]*/", - $text, $m); - } - - - protected function unhash($text) { - # - # Swap back in all the tags hashed by _HashHTMLBlocks. - # - return preg_replace_callback('/(.)\x1A[0-9]+\1/', - array($this, '_unhash_callback'), $text); - } - protected function _unhash_callback($matches) { - return $this->html_hashes[trim($matches[0])]; - } -} - -# -# Temporary Markdown Extra Parser Implementation Class -# -# NOTE: DON'T USE THIS CLASS -# Currently the implementation of of Extra resides here in this temporary class. -# This makes it easier to propagate the changes between the three different -# packaging styles of PHP Markdown. When this issue is resolved, this -# MarkdownExtra_TmpImpl class here will disappear and \Michelf\MarkdownExtra -# will contain the code. So please use \Michelf\MarkdownExtra and ignore this -# one. -# - -class MarkdownExtra extends _Markdown { - - ### Configuration Variables ### - - # Prefix for footnote ids. - public $fn_id_prefix = ""; - - # Optional title attribute for footnote links and backlinks. - public $fn_link_title = ""; - public $fn_backlink_title = ""; - - # Optional class attribute for footnote links and backlinks. - public $fn_link_class = "footnote-ref"; - public $fn_backlink_class = "footnote-backref"; - - # Class name for table cell alignment (%% replaced left/center/right) - # For instance: 'go-%%' becomes 'go-left' or 'go-right' or 'go-center' - # If empty, the align attribute is used instead of a class name. - public $table_align_class_tmpl = ''; - - # Optional class prefix for fenced code block. - public $code_class_prefix = ""; - # Class attribute for code blocks goes on the `code` tag; - # setting this to true will put attributes on the `pre` tag instead. - public $code_attr_on_pre = false; - - # Predefined abbreviations. - public $predef_abbr = array(); - - - ### Parser Implementation ### - - public function __construct() { - # - # Constructor function. Initialize the parser object. - # - # Add extra escapable characters before parent constructor - # initialize the table. - $this->escape_chars .= ':|'; - - # Insert extra document, block, and span transformations. - # Parent constructor will do the sorting. - $this->document_gamut += array( - "doFencedCodeBlocks" => 5, - "stripFootnotes" => 15, - "stripAbbreviations" => 25, - "appendFootnotes" => 50, - ); - $this->block_gamut += array( - "doFencedCodeBlocks" => 5, - "doTables" => 15, - "doDefLists" => 45, - ); - $this->span_gamut += array( - "doFootnotes" => 5, - "doAbbreviations" => 70, - ); - - parent::__construct(); - } - - - # Extra variables used during extra transformations. - protected $footnotes = array(); - protected $footnotes_ordered = array(); - protected $footnotes_ref_count = array(); - protected $footnotes_numbers = array(); - protected $abbr_desciptions = array(); - protected $abbr_word_re = ''; - - # Give the current footnote number. - protected $footnote_counter = 1; - - - protected function setup() { - # - # Setting up Extra-specific variables. - # - parent::setup(); - - $this->footnotes = array(); - $this->footnotes_ordered = array(); - $this->footnotes_ref_count = array(); - $this->footnotes_numbers = array(); - $this->abbr_desciptions = array(); - $this->abbr_word_re = ''; - $this->footnote_counter = 1; - - foreach ($this->predef_abbr as $abbr_word => $abbr_desc) { - if ($this->abbr_word_re) - $this->abbr_word_re .= '|'; - $this->abbr_word_re .= preg_quote($abbr_word); - $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); - } - } - - protected function teardown() { - # - # Clearing Extra-specific variables. - # - $this->footnotes = array(); - $this->footnotes_ordered = array(); - $this->footnotes_ref_count = array(); - $this->footnotes_numbers = array(); - $this->abbr_desciptions = array(); - $this->abbr_word_re = ''; - - parent::teardown(); - } - - - ### Extra Attribute Parser ### - - # Expression to use to catch attributes (includes the braces) - protected $id_class_attr_catch_re = '\{((?:[ ]*[#.][-_:a-zA-Z0-9]+){1,})[ ]*\}'; - # Expression to use when parsing in a context when no capture is desired - protected $id_class_attr_nocatch_re = '\{(?:[ ]*[#.][-_:a-zA-Z0-9]+){1,}[ ]*\}'; - - protected function doExtraAttributes($tag_name, $attr) { - # - # Parse attributes caught by the $this->id_class_attr_catch_re expression - # and return the HTML-formatted list of attributes. - # - # Currently supported attributes are .class and #id. - # - if (empty($attr)) return ""; - - # Split on components - preg_match_all('/[#.][-_:a-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; - } - - - protected function stripLinkDefinitions($text) { - # - # Strips link definitions from text, stores the URLs and titles in - # hash references. - # - $less_than_tab = $this->tab_width - 1; - - # Link defs are in the form: ^[id]: url "optional title" - $text = preg_replace_callback('{ - ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 - [ ]* - \n? # maybe *one* newline - [ ]* - (?: - <(.+?)> # url = $2 - | - (\S+?) # url = $3 - ) - [ ]* - \n? # maybe one newline - [ ]* - (?: - (?<=\s) # lookbehind for whitespace - ["(] - (.*?) # title = $4 - [")] - [ ]* - )? # title is optional - (?:[ ]* '.$this->id_class_attr_catch_re.' )? # $5 = extra id & class attr - (?:\n+|\Z) - }xm', - array($this, '_stripLinkDefinitions_callback'), - $text); - return $text; - } - protected function _stripLinkDefinitions_callback($matches) { - $link_id = strtolower($matches[1]); - $url = $matches[2] == '' ? $matches[3] : $matches[2]; - $this->urls[$link_id] = $url; - $this->titles[$link_id] =& $matches[4]; - $this->ref_attr[$link_id] = $this->doExtraAttributes("", $dummy =& $matches[5]); - return ''; # String that will replace the block - } - - - ### HTML Block Parser ### - - # Tags that are always treated as block tags: - protected $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption'; - - # Tags treated as block tags only if the opening tag is alone on its line: - protected $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: - protected $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address'; - - # Tags which must not have their contents modified, no matter where - # they appear: - protected $clean_tags_re = 'script|math|svg'; - - # Tags that do not need to be closed. - protected $auto_close_tags_re = 'hr|img|param|source|track'; - - - protected function hashHTMLBlocks($text) { - # - # Hashify HTML Blocks and "clean tags". - # - # We only want to do this for block-level HTML tags, such as headers, - # lists, and tables. That's because we still want to wrap

    s around - # "paragraphs" that are wrapped in non-block-level tags, such as anchors, - # phrase emphasis, and spans. The list of tags we're looking for is - # hard-coded. - # - # This works by calling _HashHTMLBlocks_InMarkdown, which then calls - # _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1" - # attribute is found within a tag, _HashHTMLBlocks_InHTML calls back - # _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag. - # These two functions are calling each other. It's recursive! - # - if ($this->no_markup) return $text; - - # - # Call the HTML-in-Markdown hasher. - # - list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text); - - return $text; - } - protected function _hashHTMLBlocks_inMarkdown($text, $indent = 0, - $enclosing_tag_re = '', $span = false) - { - # - # Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags. - # - # * $indent is the number of space to be ignored when checking for code - # blocks. This is important because if we don't take the indent into - # account, something like this (which looks right) won't work as expected: - # - #

    - #
    - # 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. - # 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 - | - # Code span marker - `+ - '. ( !$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,} - [ ]* - (?: - \.?[-_:a-zA-Z0-9]+ # standalone class name - | - '.$this->id_class_attr_nocatch_re.' # extra attributes - )? - [ ]* - \n - ' : '' ). ' # End (if not is span). - ) - }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: Code span marker - # - if ($tag{0} == "`") { - # Find corresponding end marker. - $tag_re = preg_quote($tag); - if (preg_match('{^(?>.+?|\n(?!\n))*?(?.*\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: Opening Block level tag or - # Opening Context Block tag (like ins and del) - # used as a block tag (tag is alone on it's line). - # - else if (preg_match('{^<(?:'.$this->block_tags_re.')\b}', $tag) || - ( preg_match('{^<(?:'.$this->context_block_tags_re.')\b}', $tag) && - preg_match($newline_before_re, $parsed) && - preg_match($newline_after_re, $text) ) - ) - { - # Need to parse tag and following text using the HTML parser. - list($block_text, $text) = - $this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true); - - # Make sure it stays outside of any paragraph by adding newlines. - $parsed .= "\n\n$block_text\n\n"; - } - # - # Check for: Clean tag (like script, math) - # HTML Comments, processing instructions. - # - else if (preg_match('{^<(?:'.$this->clean_tags_re.')\b}', $tag) || - $tag{1} == '!' || $tag{1} == '?') - { - # Need to parse tag and following text using the HTML parser. - # (don't check for markdown attribute) - list($block_text, $text) = - $this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false); - - $parsed .= $block_text; - } - # - # Check for: Tag with same name as enclosing tag. - # - else if ($enclosing_tag_re !== '' && - # Same name as enclosing tag. - preg_match('{^= 0); - - return array($parsed, $text); - } - protected function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) { - # - # Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags. - # - # * Calls $hash_method to convert any blocks. - # * Stops when the first opening tag closes. - # * $md_attr indicate if the use of the `markdown="1"` attribute is allowed. - # (it is not inside clean tags) - # - # Returns an array of that form: ( processed text , remaining text ) - # - if ($text === '') return array('', ''); - - # Regex to match `markdown` attribute inside of a tag. - $markdown_attr_re = ' - { - \s* # Eat whitespace before the `markdown` attribute - markdown - \s*=\s* - (?> - (["\']) # $1: quote delimiter - (.*?) # $2: attribute value - \1 # matching delimiter - | - ([^\s>]*) # $3: unquoted attribute value - ) - () # $4: make $3 always defined (avoid warnings) - }xs'; - - # Regex to match any tag. - $tag_re = '{ - ( # $2: Capture whole tag. - - ".*?" | # 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('{^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('{^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 = call_user_func($strlen, $matches[1], 'UTF-8'); - } else { - $indent = 0; - } - - # End preceding block with this tag. - $block_text .= $tag; - $parsed .= $this->$hash_method($block_text); - - # Get enclosing tag name for the ParseMarkdown function. - # (This pattern makes $tag_name_re safe without quoting.) - preg_match('/^<([\w:$]*)\b/', $tag, $matches); - $tag_name_re = $matches[1]; - - # Parse the content using the HTML-in-Markdown parser. - list ($block_text, $text) - = $this->_hashHTMLBlocks_inMarkdown($text, $indent, - $tag_name_re, $span_mode); - - # Outdent markdown text. - if ($indent > 0) { - $block_text = preg_replace("/^[ ]{1,$indent}/m", "", - $block_text); - } - - # Append tag content to parsed text. - if (!$span_mode) $parsed .= "\n\n$block_text\n\n"; - else $parsed .= "$block_text"; - - # Start over with a new block. - $block_text = ""; - } - else $block_text .= $tag; - } - - } while ($depth > 0); - - # - # Hash last block text that wasn't processed inside the loop. - # - $parsed .= $this->$hash_method($block_text); - - return array($parsed, $text); - } - - - protected function hashClean($text) { - # - # Called whenever a tag must be hashed when a function inserts a "clean" tag - # in $text, it passes through this function and is automaticaly escaped, - # blocking invalid nested overlap. - # - return $this->hashPart($text, 'C'); - } - - - protected function doAnchors($text) { - # - # Turn Markdown link shortcuts into XHTML tags. - # - if ($this->in_anchor) return $text; - $this->in_anchor = true; - - # - # First, handle reference-style links: [link text] [id] - # - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - \[ - ('.$this->nested_brackets_re.') # link text = $2 - \] - - [ ]? # one optional space - (?:\n[ ]*)? # one optional newline followed by spaces - - \[ - (.*?) # id = $3 - \] - ) - }xs', - array($this, '_doAnchors_reference_callback'), $text); - - # - # Next, inline-style links: [link text](url "optional title") - # - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - \[ - ('.$this->nested_brackets_re.') # link text = $2 - \] - \( # literal paren - [ \n]* - (?: - <(.+?)> # href = $3 - | - ('.$this->nested_url_parenthesis_re.') # href = $4 - ) - [ \n]* - ( # $5 - ([\'"]) # quote char = $6 - (.*?) # Title = $7 - \6 # matching quote - [ \n]* # ignore any spaces/tabs between closing quote and ) - )? # title is optional - \) - (?:[ ]? '.$this->id_class_attr_catch_re.' )? # $8 = id/class attributes - ) - }xs', - array($this, '_doAnchors_inline_callback'), $text); - - # - # Last, handle reference-style shortcuts: [link text] - # These must come last in case you've also got [link text][1] - # or [link text](/foo) - # - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - \[ - ([^\[\]]+) # link text = $2; can\'t contain [ or ] - \] - ) - }xs', - array($this, '_doAnchors_reference_callback'), $text); - - $this->in_anchor = false; - return $text; - } - protected function _doAnchors_reference_callback($matches) { - $whole_match = $matches[1]; - $link_text = $matches[2]; - $link_id =& $matches[3]; - - if ($link_id == "") { - # for shortcut links like [this][] or [this]. - $link_id = $link_text; - } - - # lower-case and turn embedded newlines into spaces - $link_id = strtolower($link_id); - $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); - - if (isset($this->urls[$link_id])) { - $url = $this->urls[$link_id]; - $url = $this->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; - } - protected function _doAnchors_inline_callback($matches) { - $whole_match = $matches[1]; - $link_text = $this->runSpanGamut($matches[2]); - $url = $matches[3] == '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); - - - $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); - } - - - protected 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; - } - protected function _doImages_reference_callback($matches) { - $whole_match = $matches[1]; - $alt_text = $matches[2]; - $link_id = strtolower($matches[3]); - - if ($link_id == "") { - $link_id = strtolower($alt_text); # for shortcut links like ![this][]. - } - - $alt_text = $this->encodeAttribute($alt_text); - if (isset($this->urls[$link_id])) { - $url = $this->encodeAttribute($this->urls[$link_id]); - $result = "\"$alt_text\"";titles[$link_id])) { - $title = $this->titles[$link_id]; - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; - } - if (isset($this->ref_attr[$link_id])) - $result .= $this->ref_attr[$link_id]; - $result .= $this->empty_element_suffix; - $result = $this->hashPart($result); - } - else { - # If there's no such link ID, leave intact: - $result = $whole_match; - } - - return $result; - } - protected function _doImages_inline_callback($matches) { - $whole_match = $matches[1]; - $alt_text = $matches[2]; - $url = $matches[3] == '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]); - - $alt_text = $this->encodeAttribute($alt_text); - $url = $this->encodeAttribute($url); - $result = "\"$alt_text\"";encodeAttribute($title); - $result .= " title=\"$title\""; # $title already quoted - } - $result .= $attr; - $result .= $this->empty_element_suffix; - - return $this->hashPart($result); - } - - - protected function doHeaders($text) { - # - # Redefined to add id and class attribute support. - # - # Setext-style headers: - # Header 1 {#header1} - # ======== - # - # Header 2 {#header2 .class1 .class2} - # -------- - # - $text = preg_replace_callback( - '{ - (^.+?) # $1: Header text - (?:[ ]+ '.$this->id_class_attr_catch_re.' )? # $3 = id/class attributes - [ ]*\n(=+|-+)[ ]*\n+ # $3: Header footer - }mx', - array($this, '_doHeaders_callback_setext'), $text); - - # atx-style headers: - # # Header 1 {#header1} - # ## Header 2 {#header2} - # ## Header 2 with closing hashes ## {#header3.class1.class2} - # ... - # ###### Header 6 {.class2} - # - $text = preg_replace_callback('{ - ^(\#{1,6}) # $1 = string of #\'s - [ ]* - (.+?) # $2 = Header text - [ ]* - \#* # optional closing #\'s (not counted) - (?:[ ]+ '.$this->id_class_attr_catch_re.' )? # $3 = id/class attributes - [ ]* - \n+ - }xm', - array($this, '_doHeaders_callback_atx'), $text); - - return $text; - } - protected function _doHeaders_callback_setext($matches) { - if ($matches[3] == '-' && preg_match('{^- }', $matches[1])) - return $matches[0]; - $level = $matches[3]{0} == '=' ? 1 : 2; - $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2]); - $block = "".$this->runSpanGamut($matches[1]).""; - return "\n" . $this->hashBlock($block) . "\n\n"; - } - protected function _doHeaders_callback_atx($matches) { - $level = strlen($matches[1]); - $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[3]); - $block = "".$this->runSpanGamut($matches[2]).""; - return "\n" . $this->hashBlock($block) . "\n\n"; - } - - - protected function doTables($text) { - # - # Form HTML tables. - # - $less_than_tab = $this->tab_width - 1; - # - # Find tables with leading pipe. - # - # | Header 1 | Header 2 - # | -------- | -------- - # | Cell 1 | Cell 2 - # | Cell 3 | Cell 4 - # - $text = preg_replace_callback(' - { - ^ # Start of a line - [ ]{0,'.$less_than_tab.'} # Allowed whitespace. - [|] # Optional leading pipe (present) - (.+) \n # $1: Header row (at least one pipe) - - [ ]{0,'.$less_than_tab.'} # Allowed whitespace. - [|] ([ ]*[-:]+[-| :]*) \n # $2: Header underline - - ( # $3: Cells - (?> - [ ]* # Allowed whitespace. - [|] .* \n # Row content. - )* - ) - (?=\n|\Z) # Stop at final double newline. - }xm', - array($this, '_doTable_leadingPipe_callback'), $text); - - # - # Find tables without leading pipe. - # - # Header 1 | Header 2 - # -------- | -------- - # Cell 1 | Cell 2 - # Cell 3 | Cell 4 - # - $text = preg_replace_callback(' - { - ^ # Start of a line - [ ]{0,'.$less_than_tab.'} # Allowed whitespace. - (\S.*[|].*) \n # $1: Header row (at least one pipe) - - [ ]{0,'.$less_than_tab.'} # Allowed whitespace. - ([-:]+[ ]*[|][-| :]*) \n # $2: Header underline - - ( # $3: Cells - (?> - .* [|] .* \n # Row content - )* - ) - (?=\n|\Z) # Stop at final double newline. - }xm', - array($this, '_DoTable_callback'), $text); - - return $text; - } - protected function _doTable_leadingPipe_callback($matches) { - $head = $matches[1]; - $underline = $matches[2]; - $content = $matches[3]; - - # Remove leading pipe for each row. - $content = preg_replace('/^ *[|]/m', '', $content); - - return $this->_doTable_callback(array($matches[0], $head, $underline, $content)); - } - protected function _doTable_makeAlignAttr($alignname) - { - if (empty($this->table_align_class_tmpl)) - return " align=\"$alignname\""; - - $classname = str_replace('%%', $alignname, $this->table_align_class_tmpl); - return " class=\"$classname\""; - } - protected function _doTable_callback($matches) { - $head = $matches[1]; - $underline = $matches[2]; - $content = $matches[3]; - - # Remove any tailing pipes for each line. - $head = preg_replace('/[|] *$/m', '', $head); - $underline = preg_replace('/[|] *$/m', '', $underline); - $content = preg_replace('/[|] *$/m', '', $content); - - # Reading alignement from header underline. - $separators = preg_split('/ *[|] */', $underline); - foreach ($separators as $n => $s) { - if (preg_match('/^ *-+: *$/', $s)) - $attr[$n] = $this->_doTable_makeAlignAttr('right'); - else if (preg_match('/^ *:-+: *$/', $s)) - $attr[$n] = $this->_doTable_makeAlignAttr('center'); - else if (preg_match('/^ *:-+ *$/', $s)) - $attr[$n] = $this->_doTable_makeAlignAttr('left'); - else - $attr[$n] = ''; - } - - # Parsing span elements, including code spans, character escapes, - # and inline HTML tags, so that pipes inside those gets ignored. - $head = $this->parseSpan($head); - $headers = preg_split('/ *[|] */', $head); - $col_count = count($headers); - $attr = array_pad($attr, $col_count, ''); - - # Write column headers. - $text = "\n"; - $text .= "\n"; - $text .= "\n"; - foreach ($headers as $n => $header) - $text .= " ".$this->runSpanGamut(trim($header))."\n"; - $text .= "\n"; - $text .= "\n"; - - # Split content by row. - $rows = explode("\n", trim($content, "\n")); - - $text .= "\n"; - foreach ($rows as $row) { - # Parsing span elements, including code spans, character escapes, - # and inline HTML tags, so that pipes inside those gets ignored. - $row = $this->parseSpan($row); - - # Split row by cell. - $row_cells = preg_split('/ *[|] */', $row, $col_count); - $row_cells = array_pad($row_cells, $col_count, ''); - - $text .= "\n"; - foreach ($row_cells as $n => $cell) - $text .= " ".$this->runSpanGamut(trim($cell))."\n"; - $text .= "\n"; - } - $text .= "\n"; - $text .= "
    "; - - return $this->hashBlock($text) . "\n"; - } - - - protected function doDefLists($text) { - # - # Form HTML definition lists. - # - $less_than_tab = $this->tab_width - 1; - - # Re-usable pattern to match any entire dl list: - $whole_list_re = '(?> - ( # $1 = whole list - ( # $2 - [ ]{0,'.$less_than_tab.'} - ((?>.*\S.*\n)+) # $3 = defined term - \n? - [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition - ) - (?s:.+?) - ( # $4 - \z - | - \n{2,} - (?=\S) - (?! # Negative lookahead for another term - [ ]{0,'.$less_than_tab.'} - (?: \S.*\n )+? # defined term - \n? - [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition - ) - (?! # Negative lookahead for another definition - [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition - ) - ) - ) - )'; // mx - - $text = preg_replace_callback('{ - (?>\A\n?|(?<=\n\n)) - '.$whole_list_re.' - }mx', - array($this, '_doDefLists_callback'), $text); - - return $text; - } - protected function _doDefLists_callback($matches) { - # Re-usable patterns to match list item bullets and number markers: - $list = $matches[1]; - - # Turn double returns into triple returns, so that we can make a - # paragraph for the last item in a list, if necessary: - $result = trim($this->processDefListItems($list)); - $result = "
    \n" . $result . "\n
    "; - return $this->hashBlock($result) . "\n\n"; - } - - - protected function processDefListItems($list_str) { - # - # Process the contents of a single definition list, splitting it - # into individual term and definition list items. - # - $less_than_tab = $this->tab_width - 1; - - # trim trailing blank lines: - $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); - - # Process definition terms. - $list_str = preg_replace_callback('{ - (?>\A\n?|\n\n+) # leading line - ( # definition terms = $1 - [ ]{0,'.$less_than_tab.'} # leading whitespace - (?!\:[ ]|[ ]) # negative lookahead for a definition - # mark (colon) or more whitespace. - (?> \S.* \n)+? # actual term (not whitespace). - ) - (?=\n?[ ]{0,3}:[ ]) # lookahead for following line feed - # with a definition mark. - }xm', - array($this, '_processDefListItems_callback_dt'), $list_str); - - # Process actual definitions. - $list_str = preg_replace_callback('{ - \n(\n+)? # leading line = $1 - ( # marker space = $2 - [ ]{0,'.$less_than_tab.'} # whitespace before colon - \:[ ]+ # definition mark (colon) - ) - ((?s:.+?)) # definition text = $3 - (?= \n+ # stop at next definition mark, - (?: # next term or end of text - [ ]{0,'.$less_than_tab.'} \:[ ] | -
    | \z - ) - ) - }xm', - array($this, '_processDefListItems_callback_dd'), $list_str); - - return $list_str; - } - protected function _processDefListItems_callback_dt($matches) { - $terms = explode("\n", trim($matches[1])); - $text = ''; - foreach ($terms as $term) { - $term = $this->runSpanGamut(trim($term)); - $text .= "\n
    " . $term . "
    "; - } - return $text . "\n"; - } - protected function _processDefListItems_callback_dd($matches) { - $leading_line = $matches[1]; - $marker_space = $matches[2]; - $def = $matches[3]; - - if ($leading_line || preg_match('/\n{2,}/', $def)) { - # Replace marker with the appropriate whitespace indentation - $def = str_repeat(' ', strlen($marker_space)) . $def; - $def = $this->runBlockGamut($this->outdent($def . "\n\n")); - $def = "\n". $def ."\n"; - } - else { - $def = rtrim($def); - $def = $this->runSpanGamut($this->outdent($def)); - } - - return "\n
    " . $def . "
    \n"; - } - - - protected function doFencedCodeBlocks($text) { - # - # Adding the fenced code block syntax to regular Markdown: - # - # ~~~ - # Code block - # ~~~ - # - $less_than_tab = $this->tab_width; - - $text = preg_replace_callback('{ - (?:\n|\A) - # 1: Opening marker - ( - ~{3,} # Marker: three tilde or more. - ) - [ ]* - (?: - \.?([-_:a-zA-Z0-9]+) # 2: standalone class name - | - '.$this->id_class_attr_catch_re.' # 3: Extra attributes - )? - [ ]* \n # Whitespace and newline following marker. - - # 4: Content - ( - (?> - (?!\1 [ ]* \n) # Not a closing marker. - .*\n+ - )+ - ) - - # Closing marker. - \1 [ ]* \n - }xm', - array($this, '_doFencedCodeBlocks_callback'), $text); - - return $text; - } - protected function _doFencedCodeBlocks_callback($matches) { - $classname =& $matches[2]; - $attrs =& $matches[3]; - $codeblock = $matches[4]; - $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); - $codeblock = preg_replace_callback('/^\n+/', - array($this, '_doFencedCodeBlocks_newlines'), $codeblock); - - if ($classname != "") { - if ($classname{0} == '.') - $classname = substr($classname, 1); - $attr_str = ' class="'.$this->code_class_prefix.$classname.'"'; - } else { - $attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs); - } - $pre_attr_str = $this->code_attr_on_pre ? $attr_str : ''; - $code_attr_str = $this->code_attr_on_pre ? '' : $attr_str; - $codeblock = "$codeblock
    "; - - return "\n\n".$this->hashBlock($codeblock)."\n\n"; - } - protected function _doFencedCodeBlocks_newlines($matches) { - return str_repeat("empty_element_suffix", - strlen($matches[0])); - } - - - # - # Redefining emphasis markers so that emphasis by underscore does not - # work in the middle of a word. - # - protected $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 - - protected function stripFootnotes($text) { - # - # Strips link definitions from text, stores the URLs and titles in - # hash references. - # - $less_than_tab = $this->tab_width - 1; - - # Link defs are in the form: [^id]: url "optional title" - $text = preg_replace_callback('{ - ^[ ]{0,'.$less_than_tab.'}\[\^(.+?)\][ ]?: # note_id = $1 - [ ]* - \n? # maybe *one* newline - ( # text = $2 (no blank lines allowed) - (?: - .+ # actual text - | - \n # newlines but - (?!\[\^.+?\]:\s)# negative lookahead for footnote marker. - (?!\n+[ ]{0,3}\S)# ensure line is not blank and followed - # by non-indented content - )* - ) - }xm', - array($this, '_stripFootnotes_callback'), - $text); - return $text; - } - protected function _stripFootnotes_callback($matches) { - $note_id = $this->fn_id_prefix . $matches[1]; - $this->footnotes[$note_id] = $this->outdent($matches[2]); - return ''; # String that will replace the block - } - - - protected function doFootnotes($text) { - # - # Replace footnote references in $text [^id] with a special text-token - # which will be replaced by the actual footnote marker in appendFootnotes. - # - if (!$this->in_anchor) { - $text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text); - } - return $text; - } - - - protected function appendFootnotes($text) { - # - # Append footnote list to text. - # - $text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', - array($this, '_appendFootnotes_callback'), $text); - - if (!empty($this->footnotes_ordered)) { - $text .= "\n\n"; - $text .= "
    \n"; - $text .= "empty_element_suffix ."\n"; - $text .= "
      \n\n"; - - $attr = " rev=\"footnote\""; - if ($this->fn_backlink_class != "") { - $class = $this->fn_backlink_class; - $class = $this->encodeAttribute($class); - $attr .= " class=\"$class\""; - } - if ($this->fn_backlink_title != "") { - $title = $this->fn_backlink_title; - $title = $this->encodeAttribute($title); - $attr .= " title=\"$title\""; - } - $num = 0; - - while (!empty($this->footnotes_ordered)) { - $footnote = reset($this->footnotes_ordered); - $note_id = key($this->footnotes_ordered); - unset($this->footnotes_ordered[$note_id]); - $ref_count = $this->footnotes_ref_count[$note_id]; - unset($this->footnotes_ref_count[$note_id]); - unset($this->footnotes[$note_id]); - - $footnote .= "\n"; # Need to append newline before parsing. - $footnote = $this->runBlockGamut("$footnote\n"); - $footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', - array($this, '_appendFootnotes_callback'), $footnote); - - $attr = str_replace("%%", ++$num, $attr); - $note_id = $this->encodeAttribute($note_id); - - # Prepare backlink, multiple backlinks if multiple references - $backlink = ""; - for ($ref_num = 2; $ref_num <= $ref_count; ++$ref_num) { - $backlink .= " "; - } - # Add backlink to last paragraph; create new paragraph if needed. - if (preg_match('{

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

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

      $backlink

      "; - } - - $text .= "
    1. \n"; - $text .= $footnote . "\n"; - $text .= "
    2. \n\n"; - } - - $text .= "
    \n"; - $text .= "
    "; - } - return $text; - } - protected function _appendFootnotes_callback($matches) { - $node_id = $this->fn_id_prefix . $matches[1]; - - # Create footnote marker only if it has a corresponding footnote *and* - # the footnote hasn't been used by another marker. - if (isset($this->footnotes[$node_id])) { - $num =& $this->footnotes_numbers[$node_id]; - if (!isset($num)) { - # Transfer footnote content to the ordered list and give it its - # number - $this->footnotes_ordered[$node_id] = $this->footnotes[$node_id]; - $this->footnotes_ref_count[$node_id] = 1; - $num = $this->footnote_counter++; - $ref_count_mark = ''; - } else { - $ref_count_mark = $this->footnotes_ref_count[$node_id] += 1; - } - - $attr = ""; - if ($this->fn_link_class != "") { - $class = $this->fn_link_class; - $class = $this->encodeAttribute($class); - $attr .= " class=\"$class\""; - } - if ($this->fn_link_title != "") { - $title = $this->fn_link_title; - $title = $this->encodeAttribute($title); - $attr .= " title=\"$title\""; - } - - $attr = str_replace("%%", $num, $attr); - $node_id = $this->encodeAttribute($node_id); - - return - "". - "$num". - ""; - } - - return "[^".$matches[1]."]"; - } - - - ### Abbreviations ### - - protected function stripAbbreviations($text) { - # - # Strips abbreviations from text, stores titles in hash references. - # - $less_than_tab = $this->tab_width - 1; - - # Link defs are in the form: [id]*: url "optional title" - $text = preg_replace_callback('{ - ^[ ]{0,'.$less_than_tab.'}\*\[(.+?)\][ ]?: # abbr_id = $1 - (.*) # text = $2 (no blank lines allowed) - }xm', - array($this, '_stripAbbreviations_callback'), - $text); - return $text; - } - protected function _stripAbbreviations_callback($matches) { - $abbr_word = $matches[1]; - $abbr_desc = $matches[2]; - if ($this->abbr_word_re) - $this->abbr_word_re .= '|'; - $this->abbr_word_re .= preg_quote($abbr_word); - $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); - return ''; # String that will replace the block - } - - - protected function doAbbreviations($text) { - # - # Find defined abbreviations in text and wrap them in 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; - } - protected function _doAbbreviations_callback($matches) { - $abbr = $matches[0]; - if (isset($this->abbr_desciptions[$abbr])) { - $desc = $this->abbr_desciptions[$abbr]; - if (empty($desc)) { - return $this->hashPart("$abbr"); - } else { - $desc = $this->encodeAttribute($desc); - return $this->hashPart("$abbr"); - } - } else { - return $matches[0]; - } - } - -} - -// https://github.com/egil/php-markdown-extra-extended -class MarkdownExtraExtended extends MarkdownExtra { - # Tags that are always treated as block tags: - protected $block_tags_re = 'figure|figcaption|p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend'; - - public function __construct() { - /* - $this->block_gamut += array( - "doFencedFigures" => 7 - ); - */ - - $this->document_gamut += array( - "doClearBreaks" => 100 - ); - - parent::__construct(); - } - - public function doHardBreaks($text) { - # Do hard breaks: - # EXTENDED: changed to allow breaks without two spaces and just one new line - # original code /* return preg_replace_callback('/ {2,}\n/', */ - return preg_replace_callback('/ *\n/', - array($this, '_doHardBreaks_callback'), $text); - } - - public function doClearBreaks($text) { - return preg_replace("/\s*((?:
    \n)+)\s*(<\/?(?:$this->block_tags_re|li|dd|dt)[^\d])/is", - "\\2", $text); - } - - - public function doBlockQuotes($text) { - $text = preg_replace_callback('/ - (?>^[ ]*>[ ]? - (?:\((.+?)\))? - [ ]*(.+\n(?:.+\n)*) - )+ - /xm', - array($this, '_doBlockQuotes_callback'), $text); - - return $text; - } - - public function _doBlockQuotes_callback($matches) { - $cite = $matches[1]; - $bq = '> ' . $matches[2]; - # trim one level of quoting - trim whitespace-only lines - $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq); - $bq = $this->runBlockGamut($bq); # recurse - - $bq = preg_replace('/^/m', " ", $bq); - # These leading spaces cause problem with
     content, 
    -		# so we need to fix that:
    -		$bq = preg_replace_callback('{(\s*
    .+?
    )}sx', - array($this, '_doBlockQuotes_callback2'), $bq); - - $res = "" : " cite=\"$cite\">"; - $res .= "\n$bq\n"; - return "\n". $this->hashBlock($res)."\n\n"; - } - - public function doFencedCodeBlocks($text) { - $less_than_tab = $this->tab_width; - - $text = preg_replace_callback('{ - (?:\n|\A) - # 1: Opening marker - ( - ~{3,}|`{3,} # Marker: three tilde or more. - ) - - [ ]?(\w+)?(?:,[ ]?(\d+))?[ ]* \n # Whitespace and newline following marker. - - # 3: Content - ( - (?> - (?!\1 [ ]* \n) # Not a closing marker. - .*\n+ - )+ - ) - - # Closing marker. - \1 [ ]* \n - }xm', - array($this, '_doFencedCodeBlocks_callback'), $text); - - return $text; - } - - public function _doFencedCodeBlocks_callback($matches) { - $codeblock = $this->unhash($matches[4]); - $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); - $codeblock = preg_replace_callback('/^\n+/', - array($this, '_doFencedCodeBlocks_newlines'), $codeblock); - //$codeblock = "
    $codeblock
    "; - //$cb = "
    " : " class=\"lang-$matches[2]\">";
    -		$cb .= "$codeblock
    "; - return "\n\n".$this->hashBlock($cb)."\n\n"; - } - - public function doFencedFigures($text){ - $text = preg_replace_callback('{ - (?:\n|\A) - # 1: Opening marker - ( - ={3,} # Marker: equal sign. - ) - - [ ]?(?:\[([^\]]+)\])?[ ]* \n # Whitespace and newline following marker. - - # 3: Content - ( - (?> - (?!\1 [ ]?(?:\[([^\]]+)\])?[ ]* \n) # Not a closing marker. - .*\n+ - )+ - ) - - # Closing marker. - \1 [ ]?(?:\[([^\]]+)\])?[ ]* \n - }xm', array($this, '_doFencedFigures_callback'), $text); - - return $text; - } - - public function _doFencedFigures_callback($matches) { - # get figcaption - $topcaption = empty($matches[2]) ? null : $this->runBlockGamut($matches[2]); - $bottomcaption = empty($matches[5]) ? null : $this->runBlockGamut($matches[5]); - $figure = $matches[3]; - $figure = $this->runBlockGamut($figure); # recurse - - $figure = preg_replace('/^/m', " ", $figure); - # These leading spaces cause problem with
     content, 
    -		# so we need to fix that - reuse blockqoute code to handle this:
    -		$figure = preg_replace_callback('{(\s*
    .+?
    )}sx', - array($this, '_doBlockQuotes_callback2'), $figure); - - $res = "
    "; - if(!empty($topcaption)){ - $res .= "\n
    $topcaption
    "; - } - $res .= "\n$figure\n"; - if(!empty($bottomcaption) && empty($topcaption)){ - $res .= "
    $bottomcaption
    "; - } - $res .= "
    "; - return "\n". $this->hashBlock($res)."\n\n"; - } -} - diff --git a/var/ParsedownExtra.php b/var/ParsedownExtra.php index f5902fe9..35236630 100644 --- a/var/ParsedownExtra.php +++ b/var/ParsedownExtra.php @@ -1755,6 +1755,10 @@ class ParsedownExtra extends Parsedown return $text; } + protected function sortFootnotes($A, $B) { + return $A['number'] - $B['number']; + } + # # ~ # @@ -1777,9 +1781,7 @@ class ParsedownExtra extends Parsedown ), ); - usort($this->Definitions['Footnote'], function($A, $B) { - return $A['number'] - $B['number']; - }); + usort($this->Definitions['Footnote'], array($this, 'sortFootnotes')); foreach ($this->Definitions['Footnote'] as $name => $Data) {