1219 lines
37 KiB
JavaScript
1219 lines
37 KiB
JavaScript
/*
|
||
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 <control-0085>
|
||
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 <christoph@christophdorn.com> (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, """ )
|
||
.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( "" ) + "</" + tag + ">";
|
||
}
|
||
}
|
||
|
||
function convert_tree_to_html( tree, references, options ) {
|
||
var i;
|
||
options = options || {};
|
||
|
||
// shallow clone
|
||
var jsonml = tree.slice( 0 );
|
||
|
||
if ( typeof options.preprocessTreeNode === "function" ) {
|
||
jsonml = options.preprocessTreeNode(jsonml, references);
|
||
}
|
||
|
||
// Clone attributes if they exist
|
||
var attrs = extract_attr( jsonml );
|
||
if ( attrs ) {
|
||
jsonml[ 1 ] = {};
|
||
for ( i in attrs ) {
|
||
jsonml[ 1 ][ i ] = attrs[ i ];
|
||
}
|
||
attrs = jsonml[ 1 ];
|
||
}
|
||
|
||
// basic case
|
||
if ( typeof jsonml === "string" ) {
|
||
return jsonml;
|
||
}
|
||
|
||
// convert this node
|
||
switch ( jsonml[ 0 ] ) {
|
||
case "header":
|
||
jsonml[ 0 ] = "h" + jsonml[ 1 ].level;
|
||
delete jsonml[ 1 ].level;
|
||
break;
|
||
case "bulletlist":
|
||
jsonml[ 0 ] = "ul";
|
||
break;
|
||
case "numberlist":
|
||
jsonml[ 0 ] = "ol";
|
||
break;
|
||
case "listitem":
|
||
jsonml[ 0 ] = "li";
|
||
break;
|
||
case "para":
|
||
jsonml[ 0 ] = "p";
|
||
break;
|
||
case "markdown":
|
||
jsonml[ 0 ] = "html";
|
||
if ( attrs ) delete attrs.references;
|
||
break;
|
||
case "code_block":
|
||
jsonml[ 0 ] = "pre";
|
||
i = attrs ? 2 : 1;
|
||
var code = [ "code" ];
|
||
code.push.apply( code, jsonml.splice( i, jsonml.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;
|
||
}
|
||
}())
|