diff --git a/WebKit/scripts/lib/Markdown.js b/WebKit/scripts/lib/Markdown.js new file mode 100644 index 0000000..bbe9077 --- /dev/null +++ b/WebKit/scripts/lib/Markdown.js @@ -0,0 +1,7 @@ +define([ + "lib/vendor/tent-markdown" +], + +function() { + return window.markdown; +}); \ No newline at end of file diff --git a/WebKit/scripts/lib/vendor/tent-markdown.js b/WebKit/scripts/lib/vendor/tent-markdown.js new file mode 100644 index 0000000..34013e2 --- /dev/null +++ b/WebKit/scripts/lib/vendor/tent-markdown.js @@ -0,0 +1,1219 @@ +/* + Adapted from https://github.com/twitter/twitter-text-js + + Copyright 2011 Twitter, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this work except in compliance with the License. + You may obtain a copy of the License below, or at: + + http://www.apache.org/licenses/LICENSE-2.0 +*/ + +(function(expose) { + + twttr = { txt: { regexen: {} } } + + // Builds a RegExp + function regexSupplant(regex, flags) { + flags = flags || ""; + if (typeof regex !== "string") { + if (regex.global && flags.indexOf("g") < 0) { + flags += "g"; + } + if (regex.ignoreCase && flags.indexOf("i") < 0) { + flags += "i"; + } + if (regex.multiline && flags.indexOf("m") < 0) { + flags += "m"; + } + + regex = regex.source; + } + + return new RegExp(regex.replace(/#\{(\w+)\}/g, function(match, name) { + var newRegex = twttr.txt.regexen[name] || ""; + if (typeof newRegex !== "string") { + newRegex = newRegex.source; + } + return newRegex; + }), flags); + } + + twttr.txt.regexSupplant = regexSupplant; + + // simple string interpolation + function stringSupplant(str, values) { + return str.replace(/#\{(\w+)\}/g, function(match, name) { + return values[name] || ""; + }); + } + + var fromCode = String.fromCharCode; + var INVALID_CHARS = [ + fromCode(0xFFFE), + fromCode(0xFEFF), // BOM + fromCode(0xFFFF) // Special + ]; + + twttr.txt.regexen.invalid_chars_group = regexSupplant(INVALID_CHARS.join("")); + + twttr.txt.stringSupplant = stringSupplant; + + twttr.txt.stringSupplant = stringSupplant + + var UNICODE_SPACES = [ + fromCode(0x0020), // White_Space # Zs SPACE + fromCode(0x0085), // White_Space # Cc + fromCode(0x00A0), // White_Space # Zs NO-BREAK SPACE + fromCode(0x1680), // White_Space # Zs OGHAM SPACE MARK + fromCode(0x180E), // White_Space # Zs MONGOLIAN VOWEL SEPARATOR + fromCode(0x2028), // White_Space # Zl LINE SEPARATOR + fromCode(0x2029), // White_Space # Zp PARAGRAPH SEPARATOR + fromCode(0x202F), // White_Space # Zs NARROW NO-BREAK SPACE + fromCode(0x205F), // White_Space # Zs MEDIUM MATHEMATICAL SPACE + fromCode(0x3000) // White_Space # Zs IDEOGRAPHIC SPACE + ]; + + twttr.txt.regexen.spaces_group = regexSupplant(UNICODE_SPACES.join("")); + twttr.txt.regexen.spaces = regexSupplant("[" + UNICODE_SPACES.join("") + "]"); + twttr.txt.regexen.invalid_chars_group = regexSupplant(INVALID_CHARS.join("")); + twttr.txt.regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~\$/; + + // URL related regex collection + twttr.txt.regexen.validUrlPrecedingChars = regexSupplant(/(?:[^A-Za-z0-9@@$###{invalid_chars_group}]|^)/); + twttr.txt.regexen.invalidUrlWithoutProtocolPrecedingChars = /[-_.\/]$/; + twttr.txt.regexen.invalidDomainChars = stringSupplant("#{punct}#{spaces_group}#{invalid_chars_group}", twttr.txt.regexen); + twttr.txt.regexen.validDomainChars = regexSupplant(/[^#{invalidDomainChars}]/); + twttr.txt.regexen.validSubdomain = regexSupplant(/(?:(?:#{validDomainChars}(?:[_-]|#{validDomainChars})*)?#{validDomainChars}\.)/); + twttr.txt.regexen.validDomainName = regexSupplant(/(?:(?:#{validDomainChars}(?:-|#{validDomainChars})*)?#{validDomainChars}\.)/); + twttr.txt.regexen.validGTLD = regexSupplant(/(?:(?:aero|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|xxx|local)(?=[^0-9a-zA-Z]|$))/); + twttr.txt.regexen.validCCTLD = regexSupplant(/(?:(?:ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|ss|st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw)(?=[^0-9a-zA-Z]|$))/); + twttr.txt.regexen.validPunycode = regexSupplant(/(?:xn--[0-9a-z]+)/); + twttr.txt.regexen.validDomain = regexSupplant(/(?:#{validSubdomain}*#{validDomainName}(?:#{validGTLD}|#{validCCTLD}|#{validPunycode}))/); + twttr.txt.regexen.validAsciiDomain = regexSupplant(/(?:(?:[-a-z0-9#{latinAccentChars}]+)\.)+(?:#{validGTLD}|#{validCCTLD}|#{validPunycode})/gi); + twttr.txt.regexen.invalidShortDomain = regexSupplant(/^#{validDomainName}#{validCCTLD}$/); + + twttr.txt.regexen.validPortNumber = regexSupplant(/[0-9]+/); + + twttr.txt.regexen.validGeneralUrlPathChars = regexSupplant(/[a-z0-9!\*';:=\+,\.\$\/%#\[\]\-_~|&#{latinAccentChars}]/i); + // Allow URL paths to contain balanced parens + // 1. Used in Wikipedia URLs like /Primer_(film) + // 2. Used in IIS sessions like /S(dfd346)/ + twttr.txt.regexen.validUrlBalancedParens = regexSupplant(/\(#{validGeneralUrlPathChars}+\)/i); + // Valid end-of-path chracters (so /foo. does not gobble the period). + // 1. Allow =&# for empty URL parameters and other URL-join artifacts + twttr.txt.regexen.validUrlPathEndingChars = regexSupplant(/[\+\-a-z0-9=_#\/#{latinAccentChars}]|(?:#{validUrlBalancedParens})/i); + // Allow @ in a url, but only in the middle. Catch things like http://example.com/@user/ + twttr.txt.regexen.validUrlPath = regexSupplant('(?:' + + '(?:' + + '#{validGeneralUrlPathChars}*' + + '(?:#{validUrlBalancedParens}#{validGeneralUrlPathChars}*)*' + + '#{validUrlPathEndingChars}'+ + ')|(?:@#{validGeneralUrlPathChars}+\/)'+ + ')', 'i'); + + twttr.txt.regexen.validUrlQueryChars = /[a-z0-9!?\*'\(\);:&=\+\$\/%#\[\]\-_\.,~|]/i; + twttr.txt.regexen.validUrlQueryEndingChars = /[a-z0-9_&=#\/]/i; + twttr.txt.regexen.extractUrl = regexSupplant( + '(' + // $1 total match + '(#{validUrlPrecedingChars})' + // $2 Preceeding chracter + '(' + // $3 URL + '(https?:\\/\\/)?' + // $4 Protocol (optional) + '(#{validDomain})' + // $5 Domain(s) + '(?::(#{validPortNumber}))?' + // $6 Port number (optional) + '(\\/#{validUrlPath}*)?' + // $7 URL Path + '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $8 Query String + ')' + + ')' + , 'gi'); + + twttr.txt.regexen.validTcoUrl = /^https?:\/\/t\.co\/[a-z0-9]+/i; + + twttr.extractUrlsWithIndices = function(text, options) { + if (!options) { + options = {extractUrlsWithoutProtocol: true}; + } + + if (!text || (options.extractUrlsWithoutProtocol ? !text.match(/\./) : !text.match(/:/))) { + return []; + } + + var urls = []; + + while (twttr.txt.regexen.extractUrl.exec(text)) { + var before = RegExp.$2, url = RegExp.$3, protocol = RegExp.$4, domain = RegExp.$5, path = RegExp.$7; + var endPosition = twttr.txt.regexen.extractUrl.lastIndex, + startPosition = endPosition - url.length; + + // if protocol is missing and domain contains non-ASCII characters, + // extract ASCII-only domains. + if (!protocol) { + if (!options.extractUrlsWithoutProtocol + || before.match(twttr.txt.regexen.invalidUrlWithoutProtocolPrecedingChars)) { + continue; + } + var lastUrl = null, + lastUrlInvalidMatch = false, + asciiEndPosition = 0; + domain.replace(twttr.txt.regexen.validAsciiDomain, function(asciiDomain) { + var asciiStartPosition = domain.indexOf(asciiDomain, asciiEndPosition); + asciiEndPosition = asciiStartPosition + asciiDomain.length; + lastUrl = { + url: asciiDomain, + indices: [startPosition + asciiStartPosition, startPosition + asciiEndPosition] + }; + if (!before.match(/^[\^]$/)) { + lastUrlInvalidMatch = asciiDomain.match(twttr.txt.regexen.invalidShortDomain); + } + if (!lastUrlInvalidMatch) { + urls.push(lastUrl); + } + }); + + // no ASCII-only domain found. Skip the entire URL. + if (lastUrl == null) { + continue; + } + + // lastUrl only contains domain. Need to add path and query if they exist. + if (path) { + if (lastUrlInvalidMatch) { + urls.push(lastUrl); + } + lastUrl.url = url.replace(domain, lastUrl.url); + lastUrl.indices[1] = endPosition; + } + } else { + // In the case of t.co URLs, don't allow additional path characters. + if (url.match(twttr.txt.regexen.validTcoUrl)) { + url = RegExp.lastMatch; + endPosition = startPosition + url.length; + } + urls.push({ + url: url, + indices: [startPosition, endPosition] + }); + } + } + + return urls; + }; + + expose.extractUrlsWithIndices = twttr.extractUrlsWithIndices; + +})((function() { + if (typeof exports === "undefined") { + window.twttr = {}; + return window.twttr; + } else { + return exports; + } +})()); +// Released under MIT license +// Copyright (c) 2009-2010 Dominic Baggott +// Copyright (c) 2009-2010 Ash Berlin +// Copyright (c) 2011 Christoph Dorn (http://www.christophdorn.com) + +/*jshint browser:true, devel:true */ + +(function( expose ) { + +var Markdown = expose.Markdown = function(dialect) { + switch (typeof dialect) { + case "undefined": + this.dialect = Markdown.dialects.Gruber; + break; + case "object": + this.dialect = dialect; + break; + default: + if ( dialect in Markdown.dialects ) { + this.dialect = Markdown.dialects[dialect]; + } + else { + throw new Error("Unknown Markdown dialect '" + String(dialect) + "'"); + } + break; + } + this.em_state = []; + this.strong_state = []; + this.debug_indent = ""; +}; + +/** + * parse( markdown, [dialect] ) -> JsonML + * - markdown (String): markdown string to parse + * - dialect (String | Dialect): the dialect to use, defaults to gruber + * + * Parse `markdown` and return a markdown document as a Markdown.JsonML tree. + **/ +expose.parse = function( source, dialect ) { + // dialect will default if undefined + var md = new Markdown( dialect ); + return md.toTree( source ); +}; + +/** + * toHTML( markdown, [dialect] ) -> String + * toHTML( md_tree ) -> String + * - markdown (String): markdown string to parse + * - md_tree (Markdown.JsonML): parsed markdown tree + * + * Take markdown (either as a string or as a JsonML tree) and run it through + * [[toHTMLTree]] then turn it into a well-formated HTML fragment. + **/ +expose.toHTML = function toHTML( source , dialect , options ) { + var input = expose.toHTMLTree( source , dialect , options ); + + return expose.renderJsonML( input ); +}; + +/** + * toHTMLTree( markdown, [dialect] ) -> JsonML + * toHTMLTree( md_tree ) -> JsonML + * - markdown (String): markdown string to parse + * - dialect (String | Dialect): the dialect to use, defaults to gruber + * - md_tree (Markdown.JsonML): parsed markdown tree + * + * Turn markdown into HTML, represented as a JsonML tree. If a string is given + * to this function, it is first parsed into a markdown tree by calling + * [[parse]]. + **/ +expose.toHTMLTree = function toHTMLTree( input, dialect , options ) { + // convert string input to an MD tree + if ( typeof input ==="string" ) input = this.parse( input, dialect ); + + // Now convert the MD tree to an HTML tree + + // remove references from the tree + var attrs = extract_attr( input ), + refs = {}; + + if ( attrs && attrs.references ) { + refs = attrs.references; + } + + var html = convert_tree_to_html( input, refs , options ); + merge_text_nodes( html ); + return html; +}; + +// For Spidermonkey based engines +function mk_block_toSource() { + return "Markdown.mk_block( " + + uneval(this.toString()) + + ", " + + uneval(this.trailing) + + ", " + + uneval(this.lineNumber) + + " )"; +} + +// node +function mk_block_inspect() { + var util = require("util"); + return "Markdown.mk_block( " + + util.inspect(this.toString()) + + ", " + + util.inspect(this.trailing) + + ", " + + util.inspect(this.lineNumber) + + " )"; + +} + +var mk_block = Markdown.mk_block = function(block, trail, line) { + // Be helpful for default case in tests. + if ( arguments.length == 1 ) trail = "\n\n"; + + var s = new String(block); + s.trailing = trail; + // To make it clear its not just a string + s.inspect = mk_block_inspect; + s.toSource = mk_block_toSource; + + if ( line != undefined ) + s.lineNumber = line; + + return s; +}; + +function count_lines( str ) { + var n = 0, i = -1; + while ( ( i = str.indexOf("\n", i + 1) ) !== -1 ) n++; + return n; +} + +// Internal - split source into rough blocks +Markdown.prototype.split_blocks = function splitBlocks( input, startLine ) { + input = input.replace(/(\r\n|\n|\r)/g, "\n"); + // [\s\S] matches _anything_ (newline or space) + var re = /([\s\S]+?)($|\n(?:\s*\n|$)+)/g, + blocks = [], + m; + + var line_no = 1; + + if ( ( m = /^(\s*\n)/.exec(input) ) != null ) { + // skip (but count) leading blank lines + line_no += count_lines( m[0] ); + re.lastIndex = m[0].length; + } + + while ( ( m = re.exec(input) ) !== null ) { + blocks.push( mk_block( m[1], m[2], line_no ) ); + line_no += count_lines( m[0] ); + } + + return blocks; +}; + +/** + * Markdown#processBlock( block, next ) -> undefined | [ JsonML, ... ] + * - block (String): the block to process + * - next (Array): the following blocks + * + * Process `block` and return an array of JsonML nodes representing `block`. + * + * It does this by asking each block level function in the dialect to process + * the block until one can. Succesful handling is indicated by returning an + * array (with zero or more JsonML nodes), failure by a false value. + * + * Blocks handlers are responsible for calling [[Markdown#processInline]] + * themselves as appropriate. + * + * If the blocks were split incorrectly or adjacent blocks need collapsing you + * can adjust `next` in place using shift/splice etc. + * + * If any of this default behaviour is not right for the dialect, you can + * define a `__call__` method on the dialect that will get invoked to handle + * the block processing. + */ +Markdown.prototype.processBlock = function processBlock( block, next ) { + var cbs = this.dialect.block, + ord = cbs.__order__; + + if ( "__call__" in cbs ) { + return cbs.__call__.call(this, block, next); + } + + for ( var i = 0; i < ord.length; i++ ) { + //D:this.debug( "Testing", ord[i] ); + var res = cbs[ ord[i] ].call( this, block, next ); + if ( res ) { + //D:this.debug(" matched"); + if ( !isArray(res) || ( res.length > 0 && !( isArray(res[0]) ) ) ) + this.debug(ord[i], "didn't return a proper array"); + //D:this.debug( "" ); + return res; + } + } + + // Uhoh! no match! Should we throw an error? + return []; +}; + +Markdown.prototype.processInline = function processInline( block ) { + return this.dialect.inline.__call__.call( this, String( block ) ); +}; + +/** + * Markdown#toTree( source ) -> JsonML + * - source (String): markdown source to parse + * + * Parse `source` into a JsonML tree representing the markdown document. + **/ +// custom_tree means set this.tree to `custom_tree` and restore old value on return +Markdown.prototype.toTree = function toTree( source, custom_root ) { + var blocks = source instanceof Array ? source : this.split_blocks( source ); + + // Make tree a member variable so its easier to mess with in extensions + var old_tree = this.tree; + try { + this.tree = custom_root || this.tree || [ "markdown" ]; + + blocks: + while ( blocks.length ) { + var b = this.processBlock( blocks.shift(), blocks ); + + // Reference blocks and the like won't return any content + if ( !b.length ) continue blocks; + + this.tree.push.apply( this.tree, b ); + } + return this.tree; + } + finally { + if ( custom_root ) { + this.tree = old_tree; + } + } +}; + +// Noop by default +Markdown.prototype.debug = function () { + var args = Array.prototype.slice.call( arguments); + args.unshift(this.debug_indent); + if ( typeof print !== "undefined" ) + print.apply( print, args ); + if ( typeof console !== "undefined" && typeof console.log !== "undefined" ) + console.log.apply( null, args ); +} + +Markdown.prototype.loop_re_over_block = function( re, block, cb ) { + // Dont use /g regexps with this + var m, + b = block.valueOf(); + + while ( b.length && (m = re.exec(b) ) != null ) { + b = b.substr( m[0].length ); + cb.call(this, m); + } + return b; +}; + +/** + * Markdown.dialects + * + * Namespace of built-in dialects. + **/ +Markdown.dialects = {}; + +// Build default order from insertion order. +Markdown.buildBlockOrder = function(d) { + var ord = []; + for ( var i in d ) { + if ( i == "__order__" || i == "__call__" ) continue; + ord.push( i ); + } + d.__order__ = ord; +}; + +// Build patterns for inline matcher +Markdown.buildInlinePatterns = function(d) { + var patterns = []; + + for ( var i in d ) { + // __foo__ is reserved and not a pattern + if ( i.match( /^__.*__$/) ) continue; + var l = i.replace( /([\\.*+?|()\[\]{}])/g, "\\$1" ) + .replace( /\n/, "\\n" ); + patterns.push( i.length == 1 ? l : "(?:" + l + ")" ); + } + + patterns = patterns.join("|"); + d.__patterns__ = patterns; + //print("patterns:", uneval( patterns ) ); + + var fn = d.__call__; + d.__call__ = function(text, pattern) { + if ( pattern != undefined ) { + return fn.call(this, text, pattern); + } + else + { + return fn.call(this, text, patterns); + } + }; +}; + +Markdown.DialectHelpers = {}; +Markdown.DialectHelpers.inline_until_char = function( text, want ) { + var consumed = 0, + nodes = []; + + while ( true ) { + if ( text.charAt( consumed ) == want ) { + // Found the character we were looking for + consumed++; + return [ consumed, nodes ]; + } + + if ( consumed >= text.length ) { + // No closing char found. Abort. + return null; + } + + var res = this.dialect.inline.__oneElement__.call(this, text.substr( consumed ) ); + consumed += res[ 0 ]; + // Add any returned nodes. + nodes.push.apply( nodes, res.slice( 1 ) ); + } +} + +var isArray = Array.isArray || function(obj) { + return Object.prototype.toString.call(obj) == "[object Array]"; +}; + +function extract_attr( jsonml ) { + return isArray(jsonml) + && jsonml.length > 1 + && typeof jsonml[ 1 ] === "object" + && !( isArray(jsonml[ 1 ]) ) + ? jsonml[ 1 ] + : undefined; +} + + + +/** + * renderJsonML( jsonml[, options] ) -> String + * - jsonml (Array): JsonML array to render to XML + * - options (Object): options + * + * Converts the given JsonML into well-formed XML. + * + * The options currently understood are: + * + * - root (Boolean): wether or not the root node should be included in the + * output, or just its children. The default `false` is to not include the + * root itself. + */ +expose.renderJsonML = function( jsonml, options ) { + options = options || {}; + // include the root element in the rendered output? + options.root = options.root || false; + + var content = []; + + if ( options.root ) { + content.push( render_tree( jsonml ) ); + } + else { + jsonml.shift(); // get rid of the tag + if ( jsonml.length && typeof jsonml[ 0 ] === "object" && !( jsonml[ 0 ] instanceof Array ) ) { + jsonml.shift(); // get rid of the attributes + } + + while ( jsonml.length ) { + content.push( render_tree( jsonml.shift() ) ); + } + } + + return content.join( "\n\n" ); +}; + +function escapeHTML( text ) { + return text.replace( /&/g, "&" ) + .replace( //g, ">" ) + .replace( /"/g, """ ) + .replace( /'/g, "'" ); +} + +function render_tree( jsonml ) { + // basic case + if ( typeof jsonml === "string" ) { + return escapeHTML( jsonml ); + } + + var tag = jsonml.shift(), + attributes = {}, + content = []; + + if ( jsonml.length && typeof jsonml[ 0 ] === "object" && !( jsonml[ 0 ] instanceof Array ) ) { + attributes = jsonml.shift(); + } + + while ( jsonml.length ) { + content.push( render_tree( jsonml.shift() ) ); + } + + // edge case where tag has been removed at some point (e.g. preprocessTreeNode) + if ( !tag ) { + return content + } + + var tag_attrs = ""; + for ( var a in attributes ) { + tag_attrs += " " + a + '="' + escapeHTML( attributes[ a ] ) + '"'; + } + + // be careful about adding whitespace here for inline elements + if ( tag == "img" || tag == "br" || tag == "hr" ) { + return "<"+ tag + tag_attrs + "/>"; + } + else { + return "<"+ tag + tag_attrs + ">" + content.join( "" ) + ""; + } +} + +function convert_tree_to_html( tree, references, options ) { + var i; + options = options || {}; + + // shallow clone + var jsonml = tree.slice( 0 ); + + if ( typeof options.preprocessTreeNode === "function" ) { + jsonml = options.preprocessTreeNode(jsonml, references); + } + + // Clone attributes if they exist + var attrs = extract_attr( jsonml ); + if ( attrs ) { + jsonml[ 1 ] = {}; + for ( i in attrs ) { + jsonml[ 1 ][ i ] = attrs[ i ]; + } + attrs = jsonml[ 1 ]; + } + + // basic case + if ( typeof jsonml === "string" ) { + return jsonml; + } + + // convert this node + switch ( jsonml[ 0 ] ) { + case "header": + jsonml[ 0 ] = "h" + jsonml[ 1 ].level; + delete jsonml[ 1 ].level; + break; + case "bulletlist": + jsonml[ 0 ] = "ul"; + break; + case "numberlist": + jsonml[ 0 ] = "ol"; + break; + case "listitem": + jsonml[ 0 ] = "li"; + break; + case "para": + jsonml[ 0 ] = "p"; + break; + case "markdown": + jsonml[ 0 ] = "html"; + if ( attrs ) delete attrs.references; + break; + case "code_block": + jsonml[ 0 ] = "pre"; + i = attrs ? 2 : 1; + var code = [ "code" ]; + code.push.apply( code, jsonml.splice( i, jsonml.length - i ) ); + jsonml[ i ] = code; + break; + case "inlinecode": + jsonml[ 0 ] = "code"; + break; + case "img": + jsonml[ 1 ].src = jsonml[ 1 ].href; + delete jsonml[ 1 ].href; + break; + case "linebreak": + jsonml[ 0 ] = "br"; + break; + case "link": + jsonml[ 0 ] = "a"; + break; + case "link_ref": + jsonml[ 0 ] = "a"; + + // grab this ref and clean up the attribute node + var ref = references[ attrs.ref ]; + + // if the reference exists, make the link + if ( ref ) { + delete attrs.ref; + + // add in the href and title, if present + attrs.href = ref.href; + if ( ref.title ) { + attrs.title = ref.title; + } + + // get rid of the unneeded original text + delete attrs.original; + } + // the reference doesn't exist, so revert to plain text + else { + return attrs.original; + } + break; + case "img_ref": + jsonml[ 0 ] = "img"; + + // grab this ref and clean up the attribute node + var ref = references[ attrs.ref ]; + + // if the reference exists, make the link + if ( ref ) { + delete attrs.ref; + + // add in the href and title, if present + attrs.src = ref.href; + if ( ref.title ) { + attrs.title = ref.title; + } + + // get rid of the unneeded original text + delete attrs.original; + } + // the reference doesn't exist, so revert to plain text + else { + return attrs.original; + } + break; + } + + // convert all the children + i = 1; + + // deal with the attribute node, if it exists + if ( attrs ) { + // if there are keys, skip over it + for ( var key in jsonml[ 1 ] ) { + i = 2; + } + // if there aren't, remove it + if ( i === 1 ) { + jsonml.splice( i, 1 ); + } + } + + for ( ; i < jsonml.length; ++i ) { + jsonml[ i ] = convert_tree_to_html( jsonml[ i ], references, options ); + } + + return jsonml; +} + + +// merges adjacent text nodes into a single node +function merge_text_nodes( jsonml ) { + // skip the tag name and attribute hash + var i = extract_attr( jsonml ) ? 2 : 1; + + while ( i < jsonml.length ) { + // if it's a string check the next item too + if ( typeof jsonml[ i ] === "string" ) { + if ( i + 1 < jsonml.length && typeof jsonml[ i + 1 ] === "string" ) { + // merge the second string into the first and remove it + jsonml[ i ] += jsonml.splice( i + 1, 1 )[ 0 ]; + } + else { + ++i; + } + } + // if it's not a string recurse + else { + merge_text_nodes( jsonml[ i ] ); + ++i; + } + } +} + +} )( (function() { + if ( typeof exports === "undefined" ) { + window.markdown = {}; + return window.markdown; + } + else { + return exports; + } +} )() ); +// Released under BSD license +// Copyright (c) 2013 Apollic Software, LLC +(function (expose) { + var Preprocesser, forEach; + + (function (Markdown) { + + // Tent markdown flavor (https://github.com/tent/tent.io/issues/180) + Markdown.dialects.Tent = { + block: { + // member name: fn(block, remaining_blocks) -> json markdown tree or undefined + + // Match inline urls + autolink: function autolink( block, next ) { + var urls = expose.extractUrlsWithIndices(block); + + if (!urls.length) { + // no urls matched + return; + } + + var autolink_items = []; + + var item; + for (var i = 0; i < urls.length; i++) { + item = urls[i]; + + if ( block.slice(0, item.indices[1] + 1).match(/\[[^\]]+\]\([^\)]+\)$/) ) { + // markdown link syntax, don't autolink + continue; + } + + if ( block.slice(item.indices[0] - 1, block.length).match(/^\[[^\]]+\]\([^\)]+\)/) ) { + // url inside markdown link display text, don't autolink + continue; + } + + if ( block.match('`') ) { + // check if the url is inside code backticks + + var _indices = [], + _regex = /`/g, + m = null; + while ( m = _regex.exec(block) ) { + _indices.push(m.index); + } + + var skip = false, + _last_index = null; + if ( _indices.length && (_indices.length % 2 === 0) ) { + for (var j = 0; j < _indices.length; j += 2) { + if ( (_indices[j] < item.indices[0]) && (_indices[j+1] > item.indices[1]) ) { + // matched url is inside code backticks, ignore + _last_index = _indices[j+1]; + skip = true; + } + } + } + + if (skip === true) { + // don't autolink + continue; + } + } + + // we're good to process this link + autolink_items.push(item) + } + + if (!autolink_items.length) { + // there's nothing to autolink + return; + } + + // wrap matched urls in links + + var jsonml = ["para"], + _block = block, + item = null, + index_offset = 0, + before = null; + + for (var i = 0; i < autolink_items.length; i++) { + item = autolink_items[i]; + + // process text before url + before = _block.slice(0, item.indices[0] + index_offset); + if (before.length) { + jsonml = jsonml.concat( this.processInline(before) ); + } + + // linkify url + jsonml.push(["link", { href: item.url }, item.url]); + + // discard processed text + // and update index offset + _block = _block.slice(item.indices[1] + index_offset, _block.length) + index_offset -= before.length + (item.indices[1] - item.indices[0]) + } + + // process remaining text + jsonml = jsonml.concat( this.processInline(_block) ); + + return [jsonml]; + }, + + // Taken from Markdown.dialects.Gruber.block.para + para: function para( block, next ) { + // everything's a para! + return [ ["para"].concat( this.processInline( block ) ) ]; + } + }, + + inline: { + // member pattern_or_regex: (text, match, tree) -> [ length, string_or_tree ] + // __x__ members are not patterns + // __call__ is called by Markdown.prototype.processInline() + + /* + * Reserved member functions: + */ + + // Taken from Markdown.dialect.Gruber.inline.__oneElement__ + __oneElement__: function oneElement( text, patterns_or_re, previous_nodes ) { + var m, + res, + lastIndex = 0; + + patterns_or_re = patterns_or_re || this.dialect.inline.__patterns__; + var re = new RegExp( "([\\s\\S]*?)(" + (patterns_or_re.source || patterns_or_re) + ")" ); + + m = re.exec( text ); + if (!m) { + // Just boring text + return [ text.length, text ]; + } + else if ( m[1] ) { + // Some un-interesting text matched. Return that first + return [ m[1].length, m[1] ]; + } + + var res; + if ( m[2] in this.dialect.inline ) { + res = this.dialect.inline[ m[2] ].call( + this, + text.substr( m.index ), m, previous_nodes || [] ); + } + // Default for now to make dev easier. just slurp special and output it. + res = res || [ m[2].length, m[2] ]; + return res; + }, + + // Taken from Markdown.dialect.Gruber.inline.__call__ + __call__: function inline( text, patterns ) { + + var out = [], + res; + + function add(x) { + //D:self.debug(" adding output", uneval(x)); + if ( typeof x == "string" && typeof out[out.length-1] == "string" ) + out[ out.length-1 ] += x; + else + out.push(x); + } + + while ( text.length > 0 ) { + res = this.dialect.inline.__oneElement__.call(this, text, patterns, out ); + text = text.substr( res.shift() ); + forEach(res, add ) + } + + return out; + }, + + /* + * Pattern member functions: + */ + + // Taken from Markdown.dialects.Gruber.inline + // These characters are intersting elsewhere, so have rules for them so that + // chunks of plain text blocks don't include them + "]": function () {}, + "}": function () {}, + + // Taken from Markdown.dialects.Gruber.inline["\\"] + // Modification: change escape chars (removed { } # + - . ! and added ~) + "\\": function escaped( text ) { + // [ length of input processed, node/children to add... ] + // Only esacape: \ ` * _ [ ] ( ) * ~ + if ( text.match( /^\\[\\`\*_\[\]()\~]/ ) ) + return [ 2, text.charAt( 1 ) ]; + else + // Not an esacpe + return [ 1, "\\" ]; + }, + + "*": function bold( text ) { + // Inline content is possible inside `bold text` + var res = Markdown.DialectHelpers.inline_until_char.call( this, text.substr(1), "*" ); + + // Not bold + if ( !res ) return [ 1, "*" ]; + + var consumed = 1 + res[ 0 ], + children = res[ 1 ]; + + + return [consumed, ["strong"].concat(children)] + }, + + "_": function italic( text ) { + // Inline content is possible inside `bold text` + var res = Markdown.DialectHelpers.inline_until_char.call( this, text.substr(1), "_" ); + + // Not bold + if ( !res ) return [ 1, "_" ]; + + var consumed = 1 + res[ 0 ], + children = res[ 1 ]; + + + return [consumed, ["em"].concat(children)] + }, + + "~": function italic( text ) { + // Inline content is possible inside `bold text` + var res = Markdown.DialectHelpers.inline_until_char.call( this, text.substr(1), "~" ); + + // Not bold + if ( !res ) return [ 1, "~" ]; + + var consumed = 1 + res[ 0 ], + children = res[ 1 ]; + + + return [consumed, ["del"].concat(children)] + }, + + // Taken from Markdown.dialects.Gruber.inline["["] + // Modification: Only allow the most basic link syntax. + "[": function link( text ) { + + var orig = String(text); + // Inline content is possible inside `link text` + var res = Markdown.DialectHelpers.inline_until_char.call( this, text.substr(1), "]" ); + + // No closing ']' found. Just consume the [ + if ( !res ) return [ 1, "[" ]; + + var consumed = 1 + res[ 0 ], + children = res[ 1 ], + link, + attrs; + + // At this point the first [...] has been parsed. See what follows to find + // out which kind of link we are (reference or direct url) + text = text.substr( consumed ); + + // [link text](/path/to/img.jpg) + // 1 <--- captures + // This will capture up to the last paren in the block. We then pull + // back based on if there a matching ones in the url + // ([here](/url/(test)) + // The parens have to be balanced + + var m = text.match( /^\(([^"']*)\)/ ); + if ( m ) { + var url = m[1]; + consumed += m[0].length; + + var open_parens = 1; // One open that isn't in the capture + for ( var len = 0; len < url.length; len++ ) { + switch ( url[len] ) { + case "(": + open_parens++; + break; + case ")": + if ( --open_parens == 0) { + consumed -= url.length - len; + url = url.substring(0, len); + } + break; + } + } + + // Process escapes only + url = this.dialect.inline.__call__.call( this, url, /\\/ )[0]; + + attrs = { href: url || "" }; + + link = [ "link", attrs ].concat( children ); + return [ consumed, link ]; + } + + // Just consume the "[" + return [ 1, "[" ]; + }, + + // Taken from Markdown.dialects.Gruber.inline["`"] + // Modification: Only allow a single opening backtick + "`": function inlineCode( text ) { + // Always skip over the opening tick. + var m = text.match( /(`)(([\s\S]*?)\1)/ ); + + if ( m && m[2] ) + return [ m[1].length + m[2].length, [ "inlinecode", m[3] ] ]; + else { + // No closing backtick, it's just text + return [ 1, "`" ]; + } + }, + + // Taken from Markdown.dialects.Gruber.inline[" \n"] + // Modification: Don't require spaces before \n + "\n": function lineBreak( text ) { + return [ 1, [ "linebreak" ] ]; + } + + } + } + + Markdown.buildBlockOrder ( Markdown.dialects.Tent.block ); + Markdown.buildInlinePatterns( Markdown.dialects.Tent.inline ); + + })( expose.Markdown ) + + // Don't mess with Array.prototype. Its not friendly + if ( Array.prototype.forEach ) { + forEach = function( arr, cb, thisp ) { + return arr.forEach( cb, thisp ); + }; + } + else { + forEach = function(arr, cb, thisp) { + for (var i = 0; i < arr.length; i++) { + cb.call(thisp || arr, arr[i], i, arr); + } + } + } + + Preprocesser = function ( options ) { + this.footnotes = options.footnotes || []; + this.preprocessors = [this.expandFootnoteLinkHrefs].concat(options.preprocessors || []); + } + + Preprocesser.prototype.expandFootnoteLinkHrefs = function ( jsonml ) { + // Skip over anything that isn't a link + if (jsonml[0] !== 'link') return jsonml; + + // Skip over links that arn't footnotes + if (!jsonml[1] || !jsonml[1].href || !/^\d+$/.test(jsonml[1].href)) return jsonml; + + // Get href from footnodes array + var index = parseInt(jsonml[1].href); + jsonml[1].href = this.footnotes[index]; + jsonml[1].onclick = "bungloo.entityProfile.showEntity(this, " + index + "); return false;" + jsonml[1].class = "name"; + + // Unlink node if footnote doesn't exist + if (!jsonml[1].href) { + return [null].concat(jsonml.slice(2)); + } + + return jsonml; + } + + Preprocesser.prototype.preprocessTreeNode = function ( jsonml, references ) { + for (var i=0, _len = this.preprocessors.length; i < _len; i++) { + var fn = this.preprocessors[i] + if (!(typeof fn === 'function')) continue; + jsonml = fn.call(this, jsonml, references); + } + return jsonml; + } + + // Pre-process all link nodes to expand the [text](index) footnote syntax to actual links + // and unlink non-existant footnote references. + // Pass options.footnotes = [ href, ... ] to expand footnote links + __toHTML__ = expose.toHTML; + expose.toHTML = function ( source, dialect, options ) { + options = options || {}; + if (dialect === 'Tent') { + if (!(typeof options.preprocessTreeNode === 'function')) { + preprocesser = new Preprocesser( options ); + options.preprocessTreeNode = function () { + return preprocesser.preprocessTreeNode.apply(preprocesser, arguments); + } + } + } + return __toHTML__.call(null, source, dialect, options); + } +})(function () { + if ( typeof exports === "undefined" ) { + window.markdown.extractUrlsWithIndices = window.twttr.extractUrlsWithIndices; + return window.markdown; + } + else { + exports.markdown = require('markdown').markdown; + exports.markdown.extractUrlsWithIndices = require('./link-matcher').extractUrlsWithIndices; + + return exports.markdown; + } +}())