diff --git a/api/v1/topic.js b/api/v1/topic.js index 99851cb3c2..599e4062b2 100644 --- a/api/v1/topic.js +++ b/api/v1/topic.js @@ -7,7 +7,7 @@ var config = require('../../config'); var eventproxy = require('eventproxy'); var _ = require('lodash'); var at = require('../../common/at'); -var renderHelpers = require('../../common/render_helpers'); +var renderHelper = require('../../common/render_helper'); var validator = require('validator'); var index = function (req, res, next) { @@ -32,7 +32,7 @@ var index = function (req, res, next) { topics.forEach(function (topic) { UserModel.findById(topic.author_id, ep.done(function (author) { if (mdrender) { - topic.content = renderHelpers.markdown(at.linkUsers(topic.content)); + topic.content = renderHelper.markdown(at.linkUsers(topic.content)); } topic.author = _.pick(author, ['loginname', 'avatar_url']); ep.emit('author'); @@ -67,13 +67,13 @@ var show = function (req, res, next) { 'good', 'top', 'author']); if (mdrender) { - topic.content = renderHelpers.markdown(at.linkUsers(topic.content)); + topic.content = renderHelper.markdown(at.linkUsers(topic.content)); } topic.author = _.pick(author, ['loginname', 'avatar_url']); topic.replies = replies.map(function (reply) { if (mdrender) { - reply.content = renderHelpers.markdown(at.linkUsers(reply.content)); + reply.content = renderHelper.markdown(at.linkUsers(reply.content)); } reply.author = _.pick(reply.author, ['loginname', 'avatar_url']); reply = _.pick(reply, ['id', 'author', 'content', 'ups', 'create_at']); diff --git a/app.js b/app.js index 5c1771f6cc..f65e3166f5 100644 --- a/app.js +++ b/app.js @@ -106,7 +106,7 @@ _.extend(app.locals, { assets: assets }); -_.extend(app.locals, require('./common/render_helpers')); +_.extend(app.locals, require('./common/render_helper')); app.use(function (req, res, next) { res.locals.csrf = req.csrfToken ? req.csrfToken() : ''; next(); diff --git a/common/render_helper.js b/common/render_helper.js new file mode 100644 index 0000000000..e9df4b059e --- /dev/null +++ b/common/render_helper.js @@ -0,0 +1,84 @@ +/*! + * nodeclub - common/render_helpers.js + * Copyright(c) 2013 fengmk2 + * MIT Licensed + */ + +"use strict"; + +/** + * Module dependencies. + */ + +var Remarkable = require('remarkable'); +var _ = require('lodash'); +var config = require('../config'); +var validator = require('validator'); +var multiline = require('multiline'); + +// Set default options +var md = new Remarkable(); + +md.set({ + html: false, // Enable HTML tags in source + xhtmlOut: false, // Use '/' to close single tags (
) + breaks: true, // Convert '\n' in paragraphs into
+ linkify: false, // Autoconvert URL-like text to links + typographer: false, // Enable smartypants and other sweet transforms +}); + +md.renderer.rules.fence = function (tokens, idx) { + var token = tokens[idx]; + var language = token.params && ('language-' + token.params) || ''; + language = validator.escape(language); + return '
'
+    + '' + validator.escape(token.content) + ''
+    + '
'; +}; + + +// renderer.code = function (code, lang) { +// var language = lang && ('language-' + lang) || ''; +// language = validator.escape(language); +// return '
'
+//     + '' + validator.escape(code) + ''
+//     + '
'; +// }; + +// marked.setOptions({ +// renderer: renderer, +// gfm: true, +// tables: true, +// breaks: true, +// pedantic: false, +// sanitize: true, +// smartLists: true, +// smartypants: false, +// }); + +exports.markdown = function (text) { + return '
' + md.render(text || '') + '
'; +}; + +exports.multiline = multiline; + +exports.escapeSignature = function (signature) { + return signature.split('\n').map(function (p) { + return _.escape(p); + }).join('
'); +}; + +exports.staticFile = function (filePath) { + return config.site_static_host + filePath; +}; + +exports.tabName = function (tab) { + var pair = _.find(config.tabs, function (pair) { + return pair[0] === tab; + }); + if (pair) { + return pair[1]; + } +}; + +exports._ = _; diff --git a/common/render_helpers.js b/common/render_helpers.js deleted file mode 100644 index f41aece48a..0000000000 --- a/common/render_helpers.js +++ /dev/null @@ -1,66 +0,0 @@ -/*! - * nodeclub - common/render_helpers.js - * Copyright(c) 2013 fengmk2 - * MIT Licensed - */ - -"use strict"; - -/** - * Module dependencies. - */ - -var marked = require('marked'); -var _ = require('lodash'); -var config = require('../config'); -var validator = require('validator'); -var multiline = require('multiline'); - -// Set default options -var renderer = new marked.Renderer(); - -renderer.code = function (code, lang) { - var language = lang && ('language-' + lang) || ''; - language = validator.escape(language); - return '
'
-    + '' + validator.escape(code) + ''
-    + '
'; -}; - -marked.setOptions({ - renderer: renderer, - gfm: true, - tables: true, - breaks: true, - pedantic: false, - sanitize: true, - smartLists: true, - smartypants: false, -}); - -exports.markdown = function (text) { - return '
' + marked(text || '') + '
'; -}; - -exports.multiline = multiline; - -exports.escapeSignature = function (signature) { - return signature.split('\n').map(function (p) { - return _.escape(p); - }).join('
'); -}; - -exports.staticFile = function (filePath) { - return config.site_static_host + filePath; -}; - -exports.tabName = function (tab) { - var pair = _.find(config.tabs, function (pair) { - return pair[0] === tab; - }); - if (pair) { - return pair[1]; - } -}; - -exports._ = _; diff --git a/controllers/rss.js b/controllers/rss.js index 6be9b422d3..fac2f02f18 100644 --- a/controllers/rss.js +++ b/controllers/rss.js @@ -2,7 +2,7 @@ var config = require('../config'); var convert = require('data2xml')(); var Topic = require('../proxy').Topic; var cache = require('../common/cache'); -var marked = require('marked'); +var renderHelper = require('../common/render_helper'); var eventproxy = require('eventproxy'); exports.index = function (req, res, next) { @@ -40,7 +40,7 @@ exports.index = function (req, res, next) { title: topic.title, link: config.rss.link + '/topic/' + topic._id, guid: config.rss.link + '/topic/' + topic._id, - description: marked(topic.content), + description: renderHelper.markdown(topic.content), author: topic.author.loginname, pubDate: topic.create_at.toUTCString() }); diff --git a/controllers/site.js b/controllers/site.js index 29d4fc25ba..fa7b4efc3e 100644 --- a/controllers/site.js +++ b/controllers/site.js @@ -15,7 +15,7 @@ var config = require('../config'); var eventproxy = require('eventproxy'); var cache = require('../common/cache'); var xmlbuilder = require('xmlbuilder'); -var renderHelpers = require('../common/render_helpers'); +var renderHelper = require('../common/render_helper'); // 主页的缓存工作。主页是需要主动缓存的 function indexCache() { @@ -117,7 +117,7 @@ exports.index = function (req, res, next) { } })); - var tabName = renderHelpers.tabName(tab); + var tabName = renderHelper.tabName(tab); proxy.all('topics', 'tops', 'no_reply_topics', 'pages', function (topics, tops, no_reply_topics, pages) { res.render('index', { diff --git a/package.json b/package.json index c2938c6c10..5d18d6ff0f 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "express-session": "1.9.1", "loader": "0.1.4", "lodash": "2.4.1", - "marked": "0.3.2", "memory-cache": "0.0.5", "method-override": "1.0.2", "moment": "2.8.3", @@ -38,6 +37,7 @@ "pm2": "0.11.1", "qn": "1.0.1", "ready": "0.1.1", + "remarkable": "1.3.0", "response-time": "2.2.0", "utility": "1.0.0", "validator": "3.22.0", diff --git a/public/libs/editor/editor.js b/public/libs/editor/editor.js index d3f66b3ad2..a28115a560 100644 --- a/public/libs/editor/editor.js +++ b/public/libs/editor/editor.js @@ -7129,9 +7129,9 @@ Editor.toolbar = toolbar; * Default markdown render. */ Editor.markdown = function(text) { - if (window.marked) { + if (window.remarkable) { // use marked as markdown parser - return marked(text); + return remarkable.render(text); } }; diff --git a/public/libs/editor/ext.js b/public/libs/editor/ext.js index ae4cb79f50..23fd51c322 100644 --- a/public/libs/editor/ext.js +++ b/public/libs/editor/ext.js @@ -1,23 +1,18 @@ -(function(Editor, marked, WebUploader){ - // configure marked - var renderer = new marked.Renderer(); - renderer.code = function (code, lang) { - var ret = '
';
-        ret += '' + code.replace(//g, '>') + '';
-        ret += '
'; - return ret; - }; - marked.setOptions({ - renderer: renderer, - gfm: true, - tables: true, - breaks: true, - pedantic: false, - sanitize: true, - smartLists: true, - smartypants: false, +(function(Editor, Remarkable, WebUploader){ + // Set default options + var md = new Remarkable(); + + md.set({ + html: false, // Enable HTML tags in source + xhtmlOut: false, // Use '/' to close single tags (
) + breaks: true, // Convert '\n' in paragraphs into
+ langPrefix: 'language-', // CSS language prefix for fenced blocks + linkify: false, // Autoconvert URL-like text to links + typographer: false, // Enable smartypants and other sweet transforms }); + window.remarkable = md; + var toolbar = Editor.toolbar; var replaceTool = function(name, callback){ @@ -248,4 +243,4 @@ var line = cm.lastLine(); cm.setLine(line, cm.getLine(line) + txt); }; -})(window.Editor, window.marked, window.WebUploader); +})(window.Editor, window.Remarkable, window.WebUploader); diff --git a/public/libs/marked.js b/public/libs/marked.js deleted file mode 100644 index f1d18c73b5..0000000000 --- a/public/libs/marked.js +++ /dev/null @@ -1,1283 +0,0 @@ -/** - * marked - a markdown parser - * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) - * https://github.com/chjj/marked - */ - -; -(function () { - - /** - * Block-Level Grammar - */ - - var block = { - newline: /^\n+/, - code: /^( {4}[^\n]+\n*)+/, - fences: noop, - hr: /^( *[-*_]){3,} *(?:\n+|$)/, - heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/, - nptable: noop, - lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/, - blockquote: /^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/, - list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, - html: /^ *(?:comment|closed|closing) *(?:\n{2,}|\s*$)/, - def: /^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/, - table: noop, - paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/, - text: /^[^\n]+/ - }; - - block.bullet = /(?:[*+-]|\d+\.)/; - block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/; - block.item = replace(block.item, 'gm') - (/bull/g, block.bullet) - (); - - block.list = replace(block.list) - (/bull/g, block.bullet) - ('hr', '\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))') - ('def', '\\n+(?=' + block.def.source + ')') - (); - - block.blockquote = replace(block.blockquote) - ('def', block.def) - (); - - block._tag = '(?!(?:' - + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code' - + '|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo' - + '|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b'; - - block.html = replace(block.html) - ('comment', //) - ('closed', /<(tag)[\s\S]+?<\/\1>/) - ('closing', /])*?>/) - (/tag/g, block._tag) - (); - - block.paragraph = replace(block.paragraph) - ('hr', block.hr) - ('heading', block.heading) - ('lheading', block.lheading) - ('blockquote', block.blockquote) - ('tag', '<' + block._tag) - ('def', block.def) - (); - - /** - * Normal Block Grammar - */ - - block.normal = merge({}, block); - - /** - * GFM Block Grammar - */ - - block.gfm = merge({}, block.normal, { - fences: /^ *(`{3,}|~{3,}) *(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/, - paragraph: /^/ - }); - - block.gfm.paragraph = replace(block.paragraph) - ('(?!', '(?!' - + block.gfm.fences.source.replace('\\1', '\\2') + '|' - + block.list.source.replace('\\1', '\\3') + '|') - (); - - /** - * GFM + Tables Block Grammar - */ - - block.tables = merge({}, block.gfm, { - nptable: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/, - table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/ - }); - - /** - * Block Lexer - */ - - function Lexer(options) { - this.tokens = []; - this.tokens.links = {}; - this.options = options || marked.defaults; - this.rules = block.normal; - - if (this.options.gfm) { - if (this.options.tables) { - this.rules = block.tables; - } else { - this.rules = block.gfm; - } - } - } - - /** - * Expose Block Rules - */ - - Lexer.rules = block; - - /** - * Static Lex Method - */ - - Lexer.lex = function (src, options) { - var lexer = new Lexer(options); - return lexer.lex(src); - }; - - /** - * Preprocessing - */ - - Lexer.prototype.lex = function (src) { - src = src - .replace(/\r\n|\r/g, '\n') - .replace(/\t/g, ' ') - .replace(/\u00a0/g, ' ') - .replace(/\u2424/g, '\n'); - - return this.token(src, true); - }; - - /** - * Lexing - */ - - Lexer.prototype.token = function (src, top, bq) { - var src = src.replace(/^ +$/gm, '') - , next - , loose - , cap - , bull - , b - , item - , space - , i - , l; - - while (src) { - // newline - if (cap = this.rules.newline.exec(src)) { - src = src.substring(cap[0].length); - if (cap[0].length > 1) { - this.tokens.push({ - type: 'space' - }); - } - } - - // code - if (cap = this.rules.code.exec(src)) { - src = src.substring(cap[0].length); - cap = cap[0].replace(/^ {4}/gm, ''); - this.tokens.push({ - type: 'code', - text: !this.options.pedantic - ? cap.replace(/\n+$/, '') - : cap - }); - continue; - } - - // fences (gfm) - if (cap = this.rules.fences.exec(src)) { - src = src.substring(cap[0].length); - this.tokens.push({ - type: 'code', - lang: cap[2], - text: cap[3] - }); - continue; - } - - // heading - if (cap = this.rules.heading.exec(src)) { - src = src.substring(cap[0].length); - this.tokens.push({ - type: 'heading', - depth: cap[1].length, - text: cap[2] - }); - continue; - } - - // table no leading pipe (gfm) - if (top && (cap = this.rules.nptable.exec(src))) { - src = src.substring(cap[0].length); - - item = { - type: 'table', - header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */), - align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), - cells: cap[3].replace(/\n$/, '').split('\n') - }; - - for (i = 0; i < item.align.length; i++) { - if (/^ *-+: *$/.test(item.align[i])) { - item.align[i] = 'right'; - } else if (/^ *:-+: *$/.test(item.align[i])) { - item.align[i] = 'center'; - } else if (/^ *:-+ *$/.test(item.align[i])) { - item.align[i] = 'left'; - } else { - item.align[i] = null; - } - } - - for (i = 0; i < item.cells.length; i++) { - item.cells[i] = item.cells[i].split(/ *\| */); - } - - this.tokens.push(item); - - continue; - } - - // lheading - if (cap = this.rules.lheading.exec(src)) { - src = src.substring(cap[0].length); - this.tokens.push({ - type: 'heading', - depth: cap[2] === '=' ? 1 : 2, - text: cap[1] - }); - continue; - } - - // hr - if (cap = this.rules.hr.exec(src)) { - src = src.substring(cap[0].length); - this.tokens.push({ - type: 'hr' - }); - continue; - } - - // blockquote - if (cap = this.rules.blockquote.exec(src)) { - src = src.substring(cap[0].length); - - this.tokens.push({ - type: 'blockquote_start' - }); - - cap = cap[0].replace(/^ *> ?/gm, ''); - - // Pass `top` to keep the current - // "toplevel" state. This is exactly - // how markdown.pl works. - this.token(cap, top, true); - - this.tokens.push({ - type: 'blockquote_end' - }); - - continue; - } - - // list - if (cap = this.rules.list.exec(src)) { - src = src.substring(cap[0].length); - bull = cap[2]; - - this.tokens.push({ - type: 'list_start', - ordered: bull.length > 1 - }); - - // Get each top-level item. - cap = cap[0].match(this.rules.item); - - next = false; - l = cap.length; - i = 0; - - for (; i < l; i++) { - item = cap[i]; - - // Remove the list item's bullet - // so it is seen as the next token. - space = item.length; - item = item.replace(/^ *([*+-]|\d+\.) +/, ''); - - // Outdent whatever the - // list item contains. Hacky. - if (~item.indexOf('\n ')) { - space -= item.length; - item = !this.options.pedantic - ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '') - : item.replace(/^ {1,4}/gm, ''); - } - - // Determine whether the next list item belongs here. - // Backpedal if it does not belong in this list. - if (this.options.smartLists && i !== l - 1) { - b = block.bullet.exec(cap[i + 1])[0]; - if (bull !== b && !(bull.length > 1 && b.length > 1)) { - src = cap.slice(i + 1).join('\n') + src; - i = l - 1; - } - } - - // Determine whether item is loose or not. - // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ - // for discount behavior. - loose = next || /\n\n(?!\s*$)/.test(item); - if (i !== l - 1) { - next = item.charAt(item.length - 1) === '\n'; - if (!loose) loose = next; - } - - this.tokens.push({ - type: loose - ? 'loose_item_start' - : 'list_item_start' - }); - - // Recurse. - this.token(item, false, bq); - - this.tokens.push({ - type: 'list_item_end' - }); - } - - this.tokens.push({ - type: 'list_end' - }); - - continue; - } - - // html - if (cap = this.rules.html.exec(src)) { - src = src.substring(cap[0].length); - this.tokens.push({ - type: this.options.sanitize - ? 'paragraph' - : 'html', - pre: cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style', - text: cap[0] - }); - continue; - } - - // def - if ((!bq && top) && (cap = this.rules.def.exec(src))) { - src = src.substring(cap[0].length); - this.tokens.links[cap[1].toLowerCase()] = { - href: cap[2], - title: cap[3] - }; - continue; - } - - // table (gfm) - if (top && (cap = this.rules.table.exec(src))) { - src = src.substring(cap[0].length); - - item = { - type: 'table', - header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */), - align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), - cells: cap[3].replace(/(?: *\| *)?\n$/, '').split('\n') - }; - - for (i = 0; i < item.align.length; i++) { - if (/^ *-+: *$/.test(item.align[i])) { - item.align[i] = 'right'; - } else if (/^ *:-+: *$/.test(item.align[i])) { - item.align[i] = 'center'; - } else if (/^ *:-+ *$/.test(item.align[i])) { - item.align[i] = 'left'; - } else { - item.align[i] = null; - } - } - - for (i = 0; i < item.cells.length; i++) { - item.cells[i] = item.cells[i] - .replace(/^ *\| *| *\| *$/g, '') - .split(/ *\| */); - } - - this.tokens.push(item); - - continue; - } - - // top-level paragraph - if (top && (cap = this.rules.paragraph.exec(src))) { - src = src.substring(cap[0].length); - this.tokens.push({ - type: 'paragraph', - text: cap[1].charAt(cap[1].length - 1) === '\n' - ? cap[1].slice(0, -1) - : cap[1] - }); - continue; - } - - // text - if (cap = this.rules.text.exec(src)) { - // Top-level should never reach here. - src = src.substring(cap[0].length); - this.tokens.push({ - type: 'text', - text: cap[0] - }); - continue; - } - - if (src) { - throw new - Error('Infinite loop on byte: ' + src.charCodeAt(0)); - } - } - - return this.tokens; - }; - - /** - * Inline-Level Grammar - */ - - var inline = { - escape: /^\\([\\`*{}\[\]()#+\-.!_>])/, - autolink: /^<([^ >]+(@|:\/)[^ >]+)>/, - url: noop, - tag: /^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/, - link: /^!?\[(inside)\]\(href\)/, - reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/, - nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/, - strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/, - em: /^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/, - code: /^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/, - br: /^ {2,}\n(?!\s*$)/, - del: noop, - text: /^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/; - - inline.link = replace(inline.link) - ('inside', inline._inside) - ('href', inline._href) - (); - - inline.reflink = replace(inline.reflink) - ('inside', inline._inside) - (); - - /** - * Normal Inline Grammar - */ - - inline.normal = merge({}, inline); - - /** - * Pedantic Inline Grammar - */ - - inline.pedantic = merge({}, inline.normal, { - strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/, - em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/ - }); - - /** - * GFM Inline Grammar - */ - - inline.gfm = merge({}, inline.normal, { - escape: replace(inline.escape)('])', '~|])')(), - url: /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/, - del: /^~~(?=\S)([\s\S]*?\S)~~/, - text: replace(inline.text) - (']|', '~]|') - ('|', '|https?://|') - () - }); - - /** - * GFM + Line Breaks Inline Grammar - */ - - inline.breaks = merge({}, inline.gfm, { - br: replace(inline.br)('{2,}', '*')(), - text: replace(inline.gfm.text)('{2,}', '*')() - }); - - /** - * Inline Lexer & Compiler - */ - - function InlineLexer(links, options) { - this.options = options || marked.defaults; - this.links = links; - this.rules = inline.normal; - this.renderer = this.options.renderer || new Renderer; - this.renderer.options = this.options; - - if (!this.links) { - throw new - Error('Tokens array requires a `links` property.'); - } - - if (this.options.gfm) { - if (this.options.breaks) { - this.rules = inline.breaks; - } else { - this.rules = inline.gfm; - } - } else if (this.options.pedantic) { - this.rules = inline.pedantic; - } - } - - /** - * Expose Inline Rules - */ - - InlineLexer.rules = inline; - - /** - * Static Lexing/Compiling Method - */ - - InlineLexer.output = function (src, links, options) { - var inline = new InlineLexer(links, options); - return inline.output(src); - }; - - /** - * Lexing/Compiling - */ - - InlineLexer.prototype.output = function (src) { - var out = '' - , link - , text - , href - , cap; - - while (src) { - // escape - if (cap = this.rules.escape.exec(src)) { - src = src.substring(cap[0].length); - out += cap[1]; - continue; - } - - // autolink - if (cap = this.rules.autolink.exec(src)) { - src = src.substring(cap[0].length); - if (cap[2] === '@') { - text = cap[1].charAt(6) === ':' - ? this.mangle(cap[1].substring(7)) - : this.mangle(cap[1]); - href = this.mangle('mailto:') + text; - } else { - text = escape(cap[1]); - href = text; - } - out += this.renderer.link(href, null, text); - continue; - } - - // url (gfm) - if (!this.inLink && (cap = this.rules.url.exec(src))) { - src = src.substring(cap[0].length); - text = escape(cap[1]); - href = text; - out += this.renderer.link(href, null, text); - continue; - } - - // tag - if (cap = this.rules.tag.exec(src)) { - if (!this.inLink && /^/i.test(cap[0])) { - this.inLink = false; - } - src = src.substring(cap[0].length); - out += this.options.sanitize - ? escape(cap[0]) - : cap[0]; - continue; - } - - // link - if (cap = this.rules.link.exec(src)) { - src = src.substring(cap[0].length); - this.inLink = true; - out += this.outputLink(cap, { - href: cap[2], - title: cap[3] - }); - this.inLink = false; - continue; - } - - // reflink, nolink - if ((cap = this.rules.reflink.exec(src)) - || (cap = this.rules.nolink.exec(src))) { - src = src.substring(cap[0].length); - link = (cap[2] || cap[1]).replace(/\s+/g, ' '); - link = this.links[link.toLowerCase()]; - if (!link || !link.href) { - out += cap[0].charAt(0); - src = cap[0].substring(1) + src; - continue; - } - this.inLink = true; - out += this.outputLink(cap, link); - this.inLink = false; - continue; - } - - // strong - if (cap = this.rules.strong.exec(src)) { - src = src.substring(cap[0].length); - out += this.renderer.strong(this.output(cap[2] || cap[1])); - continue; - } - - // em - if (cap = this.rules.em.exec(src)) { - src = src.substring(cap[0].length); - out += this.renderer.em(this.output(cap[2] || cap[1])); - continue; - } - - // code - if (cap = this.rules.code.exec(src)) { - src = src.substring(cap[0].length); - out += this.renderer.codespan(escape(cap[2], true)); - continue; - } - - // br - if (cap = this.rules.br.exec(src)) { - src = src.substring(cap[0].length); - out += this.renderer.br(); - continue; - } - - // del (gfm) - if (cap = this.rules.del.exec(src)) { - src = src.substring(cap[0].length); - out += this.renderer.del(this.output(cap[1])); - continue; - } - - // text - if (cap = this.rules.text.exec(src)) { - src = src.substring(cap[0].length); - out += escape(this.smartypants(cap[0])); - continue; - } - - if (src) { - throw new - Error('Infinite loop on byte: ' + src.charCodeAt(0)); - } - } - - return out; - }; - - /** - * Compile Link - */ - - InlineLexer.prototype.outputLink = function (cap, link) { - var href = escape(link.href) - , title = link.title ? escape(link.title) : null; - - return cap[0].charAt(0) !== '!' - ? this.renderer.link(href, title, this.output(cap[1])) - : this.renderer.image(href, title, escape(cap[1])); - }; - - /** - * Smartypants Transformations - */ - - InlineLexer.prototype.smartypants = function (text) { - if (!this.options.smartypants) return text; - return text - // em-dashes - .replace(/--/g, '\u2014') - // opening singles - .replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018') - // closing singles & apostrophes - .replace(/'/g, '\u2019') - // opening doubles - .replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c') - // closing doubles - .replace(/"/g, '\u201d') - // ellipses - .replace(/\.{3}/g, '\u2026'); - }; - - /** - * Mangle Links - */ - - InlineLexer.prototype.mangle = function (text) { - var out = '' - , l = text.length - , i = 0 - , ch; - - for (; i < l; i++) { - ch = text.charCodeAt(i); - if (Math.random() > 0.5) { - ch = 'x' + ch.toString(16); - } - out += '&#' + ch + ';'; - } - - return out; - }; - - /** - * Renderer - */ - - function Renderer(options) { - this.options = options || {}; - } - - Renderer.prototype.code = function (code, lang, escaped) { - if (this.options.highlight) { - var out = this.options.highlight(code, lang); - if (out != null && out !== code) { - escaped = true; - code = out; - } - } - - if (!lang) { - return '
'
-        + (escaped ? code : escape(code, true))
-        + '\n
'; - } - - return '
'
-      + (escaped ? code : escape(code, true))
-      + '\n
\n'; - }; - - Renderer.prototype.blockquote = function (quote) { - return '
\n' + quote + '
\n'; - }; - - Renderer.prototype.html = function (html) { - return html; - }; - - Renderer.prototype.heading = function (text, level, raw) { - return '' - + text - + '\n'; - }; - - Renderer.prototype.hr = function () { - return this.options.xhtml ? '
\n' : '
\n'; - }; - - Renderer.prototype.list = function (body, ordered) { - var type = ordered ? 'ol' : 'ul'; - return '<' + type + '>\n' + body + '\n'; - }; - - Renderer.prototype.listitem = function (text) { - return '
  • ' + text + '
  • \n'; - }; - - Renderer.prototype.paragraph = function (text) { - return '

    ' + text + '

    \n'; - }; - - Renderer.prototype.table = function (header, body) { - return '\n' - + '\n' - + header - + '\n' - + '\n' - + body - + '\n' - + '
    \n'; - }; - - Renderer.prototype.tablerow = function (content) { - return '\n' + content + '\n'; - }; - - Renderer.prototype.tablecell = function (content, flags) { - var type = flags.header ? 'th' : 'td'; - var tag = flags.align - ? '<' + type + ' style="text-align:' + flags.align + '">' - : '<' + type + '>'; - return tag + content + '\n'; - }; - -// span level renderer - Renderer.prototype.strong = function (text) { - return '' + text + ''; - }; - - Renderer.prototype.em = function (text) { - return '' + text + ''; - }; - - Renderer.prototype.codespan = function (text) { - return '' + text + ''; - }; - - Renderer.prototype.br = function () { - return this.options.xhtml ? '
    ' : '
    '; - }; - - Renderer.prototype.del = function (text) { - return '' + text + ''; - }; - - Renderer.prototype.link = function (href, title, text) { - if (this.options.sanitize) { - try { - var prot = decodeURIComponent(unescape(href)) - .replace(/[^\w:]/g, '') - .toLowerCase(); - } catch (e) { - return ''; - } - if (prot.indexOf('javascript:') === 0) { - return ''; - } - } - var out = '
    '; - return out; - }; - - Renderer.prototype.image = function (href, title, text) { - var out = '' + text + '' : '>'; - return out; - }; - - /** - * Parsing & Compiling - */ - - function Parser(options) { - this.tokens = []; - this.token = null; - this.options = options || marked.defaults; - this.options.renderer = this.options.renderer || new Renderer; - this.renderer = this.options.renderer; - this.renderer.options = this.options; - } - - /** - * Static Parse Method - */ - - Parser.parse = function (src, options, renderer) { - var parser = new Parser(options, renderer); - return parser.parse(src); - }; - - /** - * Parse Loop - */ - - Parser.prototype.parse = function (src) { - this.inline = new InlineLexer(src.links, this.options, this.renderer); - this.tokens = src.reverse(); - - var out = ''; - while (this.next()) { - out += this.tok(); - } - - return out; - }; - - /** - * Next Token - */ - - Parser.prototype.next = function () { - return this.token = this.tokens.pop(); - }; - - /** - * Preview Next Token - */ - - Parser.prototype.peek = function () { - return this.tokens[this.tokens.length - 1] || 0; - }; - - /** - * Parse Text Tokens - */ - - Parser.prototype.parseText = function () { - var body = this.token.text; - - while (this.peek().type === 'text') { - body += '\n' + this.next().text; - } - - return this.inline.output(body); - }; - - /** - * Parse Current Token - */ - - Parser.prototype.tok = function () { - switch (this.token.type) { - case 'space': - { - return ''; - } - case 'hr': - { - return this.renderer.hr(); - } - case 'heading': - { - return this.renderer.heading( - this.inline.output(this.token.text), - this.token.depth, - this.token.text); - } - case 'code': - { - return this.renderer.code(this.token.text, - this.token.lang, - this.token.escaped); - } - case 'table': - { - var header = '' - , body = '' - , i - , row - , cell - , flags - , j; - - // header - cell = ''; - for (i = 0; i < this.token.header.length; i++) { - flags = { header: true, align: this.token.align[i] }; - cell += this.renderer.tablecell( - this.inline.output(this.token.header[i]), - { header: true, align: this.token.align[i] } - ); - } - header += this.renderer.tablerow(cell); - - for (i = 0; i < this.token.cells.length; i++) { - row = this.token.cells[i]; - - cell = ''; - for (j = 0; j < row.length; j++) { - cell += this.renderer.tablecell( - this.inline.output(row[j]), - { header: false, align: this.token.align[j] } - ); - } - - body += this.renderer.tablerow(cell); - } - return this.renderer.table(header, body); - } - case 'blockquote_start': - { - var body = ''; - - while (this.next().type !== 'blockquote_end') { - body += this.tok(); - } - - return this.renderer.blockquote(body); - } - case 'list_start': - { - var body = '' - , ordered = this.token.ordered; - - while (this.next().type !== 'list_end') { - body += this.tok(); - } - - return this.renderer.list(body, ordered); - } - case 'list_item_start': - { - var body = ''; - - while (this.next().type !== 'list_item_end') { - body += this.token.type === 'text' - ? this.parseText() - : this.tok(); - } - - return this.renderer.listitem(body); - } - case 'loose_item_start': - { - var body = ''; - - while (this.next().type !== 'list_item_end') { - body += this.tok(); - } - - return this.renderer.listitem(body); - } - case 'html': - { - var html = !this.token.pre && !this.options.pedantic - ? this.inline.output(this.token.text) - : this.token.text; - return this.renderer.html(html); - } - case 'paragraph': - { - return this.renderer.paragraph(this.inline.output(this.token.text)); - } - case 'text': - { - return this.renderer.paragraph(this.parseText()); - } - } - }; - - /** - * Helpers - */ - - function escape(html, encode) { - return html - .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - } - - function unescape(html) { - return html.replace(/&([#\w]+);/g, function (_, n) { - n = n.toLowerCase(); - if (n === 'colon') return ':'; - if (n.charAt(0) === '#') { - return n.charAt(1) === 'x' - ? String.fromCharCode(parseInt(n.substring(2), 16)) - : String.fromCharCode(+n.substring(1)); - } - return ''; - }); - } - - function replace(regex, opt) { - regex = regex.source; - opt = opt || ''; - return function self(name, val) { - if (!name) return new RegExp(regex, opt); - val = val.source || val; - val = val.replace(/(^|[^\[])\^/g, '$1'); - regex = regex.replace(name, val); - return self; - }; - } - - function noop() { - } - - noop.exec = noop; - - function merge(obj) { - var i = 1 - , target - , key; - - for (; i < arguments.length; i++) { - target = arguments[i]; - for (key in target) { - if (Object.prototype.hasOwnProperty.call(target, key)) { - obj[key] = target[key]; - } - } - } - - return obj; - } - - - /** - * Marked - */ - - function marked(src, opt, callback) { - if (callback || typeof opt === 'function') { - if (!callback) { - callback = opt; - opt = null; - } - - opt = merge({}, marked.defaults, opt || {}); - - var highlight = opt.highlight - , tokens - , pending - , i = 0; - - try { - tokens = Lexer.lex(src, opt) - } catch (e) { - return callback(e); - } - - pending = tokens.length; - - var done = function () { - var out, err; - - try { - out = Parser.parse(tokens, opt); - } catch (e) { - err = e; - } - - opt.highlight = highlight; - - return err - ? callback(err) - : callback(null, out); - }; - - if (!highlight || highlight.length < 3) { - return done(); - } - - delete opt.highlight; - - if (!pending) return done(); - - for (; i < tokens.length; i++) { - (function (token) { - if (token.type !== 'code') { - return --pending || done(); - } - return highlight(token.text, token.lang, function (err, code) { - if (code == null || code === token.text) { - return --pending || done(); - } - token.text = code; - token.escaped = true; - --pending || done(); - }); - })(tokens[i]); - } - - return; - } - try { - if (opt) opt = merge({}, marked.defaults, opt); - return Parser.parse(Lexer.lex(src, opt), opt); - } catch (e) { - e.message += '\nPlease report this to https://github.com/chjj/marked.'; - if ((opt || marked.defaults).silent) { - return '

    An error occured:

    '
    -          + escape(e.message + '', true)
    -          + '
    '; - } - throw e; - } - } - - /** - * Options - */ - - marked.options = - marked.setOptions = function (opt) { - merge(marked.defaults, opt); - return marked; - }; - - marked.defaults = { - gfm: true, - tables: true, - breaks: false, - pedantic: false, - sanitize: false, - smartLists: false, - silent: false, - highlight: null, - langPrefix: 'lang-', - smartypants: false, - headerPrefix: '', - renderer: new Renderer, - xhtml: false - }; - - /** - * Expose - */ - - marked.Parser = Parser; - marked.parser = Parser.parse; - - marked.Renderer = Renderer; - - marked.Lexer = Lexer; - marked.lexer = Lexer.lex; - - marked.InlineLexer = InlineLexer; - marked.inlineLexer = InlineLexer.output; - - marked.parse = marked; - - if (typeof exports === 'object') { - module.exports = marked; - } else if (typeof define === 'function' && define.amd) { - define(function () { - return marked; - }); - } else { - this.marked = marked; - } - -}).call(function () { - return this || (typeof window !== 'undefined' ? window : global); - }()); diff --git a/public/libs/remarkable.js b/public/libs/remarkable.js new file mode 100644 index 0000000000..0fffb00ae3 --- /dev/null +++ b/public/libs/remarkable.js @@ -0,0 +1,8312 @@ +/*! remarkable 1.3.0 https://github.com//jonschlinkert/remarkable @license MIT */!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.Remarkable=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o`\x00-\x20]+/; +var single_quoted = /'[^']*'/; +var double_quoted = /"[^"]*"/; + +/*eslint no-spaced-func:0*/ +var attr_value = replace(/(?:unquoted|single_quoted|double_quoted)/) + ('unquoted', unquoted) + ('single_quoted', single_quoted) + ('double_quoted', double_quoted) + (); + +var attribute = replace(/(?:\s+attr_name(?:\s*=\s*attr_value)?)/) + ('attr_name', attr_name) + ('attr_value', attr_value) + (); + +var open_tag = replace(/<[A-Za-z][A-Za-z0-9]*attribute*\s*\/?>/) + ('attribute', attribute) + (); + +var close_tag = /<\/[A-Za-z][A-Za-z0-9]*\s*>/; +var comment = //; +var processing = /<[?].*?[?]>/; +var declaration = /]*>/; +var cdata = /])*\]\]>/; + +var HTML_TAG_RE = replace(/^(?:open_tag|close_tag|comment|processing|declaration|cdata)/) + ('open_tag', open_tag) + ('close_tag', close_tag) + ('comment', comment) + ('processing', processing) + ('declaration', declaration) + ('cdata', cdata) + (); + + +module.exports.HTML_TAG_RE = HTML_TAG_RE; + +},{}],4:[function(require,module,exports){ +// List of valid url schemas, accorting to commonmark spec +// http://jgm.github.io/CommonMark/spec.html#autolinks + +'use strict'; + + +module.exports = [ + 'coap', + 'doi', + 'javascript', + 'aaa', + 'aaas', + 'about', + 'acap', + 'cap', + 'cid', + 'crid', + 'data', + 'dav', + 'dict', + 'dns', + 'file', + 'ftp', + 'geo', + 'go', + 'gopher', + 'h323', + 'http', + 'https', + 'iax', + 'icap', + 'im', + 'imap', + 'info', + 'ipp', + 'iris', + 'iris.beep', + 'iris.xpc', + 'iris.xpcs', + 'iris.lwz', + 'ldap', + 'mailto', + 'mid', + 'msrp', + 'msrps', + 'mtqp', + 'mupdate', + 'news', + 'nfs', + 'ni', + 'nih', + 'nntp', + 'opaquelocktoken', + 'pop', + 'pres', + 'rtsp', + 'service', + 'session', + 'shttp', + 'sieve', + 'sip', + 'sips', + 'sms', + 'snmp', + 'soap.beep', + 'soap.beeps', + 'tag', + 'tel', + 'telnet', + 'tftp', + 'thismessage', + 'tn3270', + 'tip', + 'tv', + 'urn', + 'vemmi', + 'ws', + 'wss', + 'xcon', + 'xcon-userid', + 'xmlrpc.beep', + 'xmlrpc.beeps', + 'xmpp', + 'z39.50r', + 'z39.50s', + 'adiumxtra', + 'afp', + 'afs', + 'aim', + 'apt', + 'attachment', + 'aw', + 'beshare', + 'bitcoin', + 'bolo', + 'callto', + 'chrome', + 'chrome-extension', + 'com-eventbrite-attendee', + 'content', + 'cvs', + 'dlna-playsingle', + 'dlna-playcontainer', + 'dtn', + 'dvb', + 'ed2k', + 'facetime', + 'feed', + 'finger', + 'fish', + 'gg', + 'git', + 'gizmoproject', + 'gtalk', + 'hcp', + 'icon', + 'ipn', + 'irc', + 'irc6', + 'ircs', + 'itms', + 'jar', + 'jms', + 'keyparc', + 'lastfm', + 'ldaps', + 'magnet', + 'maps', + 'market', + 'message', + 'mms', + 'ms-help', + 'msnim', + 'mumble', + 'mvn', + 'notes', + 'oid', + 'palm', + 'paparazzi', + 'platform', + 'proxy', + 'psyc', + 'query', + 'res', + 'resource', + 'rmi', + 'rsync', + 'rtmp', + 'secondlife', + 'sftp', + 'sgn', + 'skype', + 'smb', + 'soldat', + 'spotify', + 'ssh', + 'steam', + 'svn', + 'teamspeak', + 'things', + 'udp', + 'unreal', + 'ut2004', + 'ventrilo', + 'view-source', + 'webcal', + 'wtai', + 'wyciwyg', + 'xfire', + 'xri', + 'ymsgr' +]; + +},{}],5:[function(require,module,exports){ +// Utilities +// +'use strict'; + + +function _class(obj) { return Object.prototype.toString.call(obj); } + +function isString(obj) { return _class(obj) === '[object String]'; } + +// Merge objects +// +function assign(obj /*from1, from2, from3, ...*/) { + var sources = Array.prototype.slice.call(arguments, 1); + while (sources.length) { + var source = sources.shift(); + if (!source) { continue; } + + if (typeof(source) !== 'object') { + throw new TypeError(source + 'must be non-object'); + } + + for (var p in source) { + if (source.hasOwnProperty(p)) { + obj[p] = source[p]; + } + } + } + + return obj; +} + + +var UNESCAPE_MD_RE = /\\([\\!"#$%&'()*+,.\/:;<=>?@[\]^_`{|}~-])/g; + +function unescapeMd(str) { + if (str.indexOf('\\') < 0) { return str; } + return str.replace(UNESCAPE_MD_RE, '$1'); +} + +function isValidEntityCode(c) { + /*eslint no-bitwise:0*/ + // broken sequence + if (c >= 0xD800 && c <= 0xDFFF) { return false; } + // never used + if (c >= 0xFDD0 && c <= 0xFDEF) { return false; } + if ((c & 0xFFFF) === 0xFFFF || (c & 0xFFFF) === 0xFFFE) { return false; } + // control codes + if (c >= 0x00 && c <= 0x08) { return false; } + if (c === 0x0B) { return false; } + if (c >= 0x0E && c <= 0x1F) { return false; } + if (c >= 0x7F && c <= 0x9F) { return false; } + // out of range + if (c > 0x10FFFF) { return false; } + return true; +} + +function fromCodePoint(c) { + /*eslint no-bitwise:0*/ + if (c > 0xffff) { + c -= 0x10000; + var surrogate1 = 0xd800 + (c >> 10), + surrogate2 = 0xdc00 + (c & 0x3ff); + + return String.fromCharCode(surrogate1, surrogate2); + } + return String.fromCharCode(c); +} + +var NAMED_ENTITY_RE = /&([a-z][a-z0-9]{1,31});/gi; +var entities = require('./entities'); + +function replaceEntities(str) { + if (str.indexOf('&') < 0) { return str; } + + return str.replace(NAMED_ENTITY_RE, function(match, name) { + if (entities.hasOwnProperty(name)) { + return entities[name]; + } + return match; + }); +} + + +exports.assign = assign; +exports.isString = isString; +exports.unescapeMd = unescapeMd; +exports.isValidEntityCode = isValidEntityCode; +exports.fromCodePoint = fromCodePoint; +exports.replaceEntities = replaceEntities; + +},{"./entities":1}],6:[function(require,module,exports){ +// Commonmark default options + +'use strict'; + + +module.exports = { + options: { + html: true, // Enable HTML tags in source + xhtmlOut: true, // Use '/' to close single tags (
    ) + breaks: false, // Convert '\n' in paragraphs into
    + langPrefix: 'language-', // CSS language prefix for fenced blocks + linkify: false, // autoconvert URL-like texts to links + typographer: false, // Enable smartypants and other sweet transforms + + // Highlighter function. Should return escaped HTML, + // or '' if input not changed + highlight: function (/*str, lang*/) { return ''; }, + + maxNesting: 20 // Internal protection, recursion limit + }, + + components: { + + block: { + rules: [ + 'blockquote', + 'code', + 'fences', + 'heading', + 'hr', + 'htmlblock', + 'lheading', + 'list', + 'paragraph' + ] + }, + + inline: { + rules: [ + 'autolink', + 'backticks', + 'emphasis', + 'entity', + 'escape', + 'htmltag', + 'links', + 'newline', + 'text' + ] + }, + + typographer: { + options: { + singleQuotes: '‘’', // set empty to disable + doubleQuotes: '“”', // set '«»' for Russian, '„“' for German, empty to disable + copyright: true, // (c) (C) → © + trademark: true, // (tm) (TM) → ™ + registered: true, // (r) (R) → ® + plusminus: true, // +- → ± + paragraph: true, // (p) (P) → § + ellipsis: true, // ... → … + dupes: true, // ???????? → ???, !!!!! → !!!, `,,` → `,` + dashes: true // -- → — + } + } + } +}; + +},{}],7:[function(require,module,exports){ +// Remarkable default options + +'use strict'; + + +module.exports = { + options: { + html: false, // Enable HTML tags in source + xhtmlOut: false, // Use '/' to close single tags (
    ) + breaks: false, // Convert '\n' in paragraphs into
    + langPrefix: 'language-', // CSS language prefix for fenced blocks + linkify: false, // autoconvert URL-like texts to links + typographer: false, // Enable smartypants and other sweet transforms + + // Highlighter function. Should return escaped HTML, + // or '' if input not changed + highlight: function (/*str, lang*/) { return ''; }, + + maxNesting: 20 // Internal protection, recursion limit + }, + + components: { + + block: { + rules: [ + 'blockquote', + 'code', + 'fences', + 'heading', + 'hr', + 'htmlblock', + 'lheading', + 'list', + 'paragraph', + 'table' + ] + }, + + inline: { + rules: [ + 'autolink', + 'backticks', + 'del', + 'emphasis', + 'entity', + 'escape', + 'htmltag', + 'links', + 'newline', + 'text' + ] + }, + + typographer: { + options: { + singleQuotes: '‘’', // set empty to disable + doubleQuotes: '“”', // set '«»' for Russian, '„“' for German, empty to disable + copyright: true, // (c) (C) → © + trademark: true, // (tm) (TM) → ™ + registered: true, // (r) (R) → ® + plusminus: true, // +- → ± + paragraph: true, // (p) (P) → § + ellipsis: true, // ... → … + dupes: true, // ???????? → ???, !!!!! → !!!, `,,` → `,` + dashes: true // -- → — + } + } + } +}; + +},{}],8:[function(require,module,exports){ +// Remarkable default options + +'use strict'; + + +module.exports = { + options: { + html: false, // Enable HTML tags in source + xhtmlOut: false, // Use '/' to close single tags (
    ) + breaks: false, // Convert '\n' in paragraphs into
    + langPrefix: 'language-', // CSS language prefix for fenced blocks + linkify: false, // autoconvert URL-like texts to links + typographer: false, // Enable smartypants and other sweet transforms + + // Highlighter function. Should return escaped HTML, + // or '' if input not changed + highlight: function (/*str, lang*/) { return ''; }, + + maxNesting: 20 // Internal protection, recursion limit + }, + + components: { + + // Don't restrict block/inline rules + block: {}, + inline: {}, + + typographer: { + options: { + singleQuotes: '‘’', // set empty to disable + doubleQuotes: '“”', // set '«»' for Russian, '„“' for German, empty to disable + copyright: true, // (c) (C) → © + trademark: true, // (tm) (TM) → ™ + registered: true, // (r) (R) → ® + plusminus: true, // +- → ± + paragraph: true, // (p) (P) → § + ellipsis: true, // ... → … + dupes: true, // ???????? → ???, !!!!! → !!!, `,,` → `,` + dashes: true // -- → — + } + } + } +}; + +},{}],9:[function(require,module,exports){ +// Main perser class + +'use strict'; + + +var assign = require('./common/utils').assign; +var isString = require('./common/utils').isString; +var Renderer = require('./renderer'); +var ParserBlock = require('./parser_block'); +var ParserInline = require('./parser_inline'); +var Typographer = require('./typographer'); +var Linkifier = require('./linkifier'); + + +var config = { + 'default': require('./configs/default'), + full: require('./configs/full'), + commonmark: require('./configs/commonmark') +}; + + +// Main class +// +function Remarkable(presetName, options) { + if (!options) { + if (!isString(presetName)) { + options = presetName || {}; + presetName = 'default'; + } + } + + this.options = {}; + this.state = null; + + this.inline = new ParserInline(); + this.block = new ParserBlock(); + this.renderer = new Renderer(); + this.typographer = new Typographer(); + this.linkifier = new Linkifier(); + + // Cross-references to simplify code (a bit dirty, but easy). + this.block.inline = this.inline; + this.inline.typographer = this.typographer; + this.inline.linkifier = this.linkifier; + + this.configure(config[presetName]); + + if (options) { this.set(options); } +} + + +// Set options, if you did not passed those to constructor +// +Remarkable.prototype.set = function (options) { + assign(this.options, options); +}; + + +// Batch loader for components rules states & options +// +Remarkable.prototype.configure = function (presets) { + var self = this; + + if (!presets) { throw new Error('Wrong config name'); } + + if (presets.options) { self.set(presets.options); } + + if (presets.components) { + Object.keys(presets.components).forEach(function (name) { + if (presets.components[name].rules) { + self[name].ruler.enable(presets.components[name].rules, true); + } + if (presets.components[name].options) { + self[name].set(presets.components[name].options); + } + }); + } +}; + + +// Sugar for curried plugins init: +// +// var md = new Remarkable(); +// +// md.use(plugin1) +// .use(plugin2, opts) +// .use(plugin3); +// +Remarkable.prototype.use = function (plugin, opts) { + plugin(this, opts); + return this; +}; + + +// Parse input string, returns tokens array. Modify `env` with +// definitions data. +// +Remarkable.prototype.parse = function (src, env) { + var tokens, tok, i, l; + // Parse blocks + tokens = this.block.parse(src, this.options, env); + + // Parse inlines + for (i = 0, l = tokens.length; i < l; i++) { + tok = tokens[i]; + if (tok.type === 'inline') { + tok.children = this.inline.parse(tok.content, this.options, env); + } + } + + return tokens; +}; + +// Main method that does all magic :) +// +Remarkable.prototype.render = function (src) { + var env = { references: {} }; + + return this.renderer.render(this.parse(src, env), this.options, env); +}; + + +module.exports = Remarkable; + +},{"./common/utils":5,"./configs/commonmark":6,"./configs/default":7,"./configs/full":8,"./linkifier":10,"./parser_block":12,"./parser_inline":13,"./renderer":15,"./typographer":46}],10:[function(require,module,exports){ +// Class of link replacement rules +// +'use strict'; + + +var assign = require('./common/utils').assign; +var Ruler = require('./ruler'); + + +var _rules = [ + [ 'linkify', require('./rules_text/linkify') ] +]; + + +function Linkifier() { + this._rules = []; + + this.options = {}; + + this.ruler = new Ruler(this.rulesUpdate.bind(this)); + + for (var i = 0; i < _rules.length; i++) { + this.ruler.push(_rules[i][0], _rules[i][1]); + } +} + + +Linkifier.prototype.rulesUpdate = function () { + this._rules = this.ruler.getRules(); +}; + + +Linkifier.prototype.set = function (options) { + assign(this.options, options); +}; + + +Linkifier.prototype.process = function (state) { + var i, l, rules; + + rules = this._rules; + + for (i = 0, l = rules.length; i < l; i++) { + rules[i](this, state); + } +}; + + +module.exports = Linkifier; + +},{"./common/utils":5,"./ruler":16,"./rules_text/linkify":43}],11:[function(require,module,exports){ + +'use strict'; + + +var unescapeMd = require('./common/utils').unescapeMd; + + +// +// Parse link label +// +// this function assumes that first character ("[") already matches; +// returns the end of the label +function parseLinkLabel(state, start) { + var level, found, marker, + labelEnd = -1, + max = state.posMax, + oldPos = state.pos, + oldFlag = state.isInLabel; + + if (state.isInLabel) { return -1; } + + if (state.labelUnmatchedScopes) { + state.labelUnmatchedScopes--; + return -1; + } + + state.pos = start + 1; + state.isInLabel = true; + level = 1; + + while (state.pos < max) { + marker = state.src.charCodeAt(state.pos); + if (marker === 0x5B /* [ */) { + level++; + } else if (marker === 0x5D /* ] */) { + level--; + if (level === 0) { + found = true; + break; + } + } + + state.parser.skipToken(state); + } + + if (found) { + labelEnd = state.pos; + state.labelUnmatchedScopes = 0; + } else { + state.labelUnmatchedScopes = level - 1; + } + + // restore old state + state.pos = oldPos; + state.isInLabel = oldFlag; + + return labelEnd; +} + +// +// Parse link destination +// +// on success it returns a string and updates state.pos; +// on failure it returns null +function parseLinkDestination(state, pos) { + var code, level, + start = pos, + max = state.posMax; + + if (state.src.charCodeAt(pos) === 0x3C /* < */) { + pos++; + while (pos < max) { + code = state.src.charCodeAt(pos); + if (code === 0x0A /* \n */) { return false; } + if (code === 0x3E /* > */) { + state.pos = pos + 1; + state.linkContent = unescapeMd(state.src.slice(start + 1, pos)); + return true; + } + if (code === 0x5C /* \ */ && pos + 1 < max) { + pos += 2; + continue; + } + + pos++; + } + + // no closing '>' + return false; + } + + // this should be ... } else { ... branch + + level = 0; + while (pos < max) { + code = state.src.charCodeAt(pos); + + if (code === 0x20) { break; } + + // ascii control characters + if (code < 0x20 || code === 0x7F) { break; } + + if (code === 0x5C /* \ */ && pos + 1 < max) { + pos += 2; + continue; + } + + if (code === 0x28 /* ( */) { + level++; + if (level > 1) { break; } + } + + if (code === 0x29 /* ) */) { + level--; + if (level < 0) { break; } + } + + pos++; + } + + if (start === pos) { return false; } + + state.linkContent = unescapeMd(state.src.slice(start, pos)); + if (!state.parser.validateLink(state.linkContent)) { return false; } + + state.pos = pos; + return true; +} + +// +// Parse link title +// +// on success it returns a string and updates state.pos; +// on failure it returns null +function parseLinkTitle(state, pos) { + var code, + start = pos, + max = state.posMax, + marker = state.src.charCodeAt(pos); + + if (marker !== 0x22 /* " */ && marker !== 0x27 /* ' */ && marker !== 0x28 /* ( */) { return false; } + + pos++; + + // if opening marker is "(", switch it to closing marker ")" + if (marker === 0x28) { marker = 0x29; } + + while (pos < max) { + code = state.src.charCodeAt(pos); + if (code === marker) { + state.pos = pos + 1; + state.linkContent = unescapeMd(state.src.slice(start + 1, pos)); + return true; + } + if (code === 0x5C /* \ */ && pos + 1 < max) { + pos += 2; + continue; + } + + pos++; + } + + return false; +} + +function normalizeReference(str) { + return str.trim().replace(/\s+/g, ' ').toLowerCase(); +} + +module.exports.parseLinkLabel = parseLinkLabel; +module.exports.parseLinkDestination = parseLinkDestination; +module.exports.parseLinkTitle = parseLinkTitle; +module.exports.normalizeReference = normalizeReference; + +},{"./common/utils":5}],12:[function(require,module,exports){ +// Block parser + + +'use strict'; + + +var Ruler = require('./ruler'); +var State = require('./rules_block/state_block'); + + +var _rules = [ + [ 'code', require('./rules_block/code') ], + [ 'fences', require('./rules_block/fences'), [ 'paragraph', 'blockquote', 'list' ] ], + [ 'blockquote', require('./rules_block/blockquote'), [ 'paragraph', 'blockquote', 'list' ] ], + [ 'hr', require('./rules_block/hr'), [ 'paragraph', 'blockquote', 'list' ] ], + [ 'list', require('./rules_block/list'), [ 'paragraph', 'blockquote' ] ], + [ 'heading', require('./rules_block/heading'), [ 'paragraph', 'blockquote' ] ], + [ 'lheading', require('./rules_block/lheading') ], + [ 'htmlblock', require('./rules_block/htmlblock'), [ 'paragraph', 'blockquote' ] ], + [ 'table', require('./rules_block/table'), [ 'paragraph' ] ], + [ 'paragraph', require('./rules_block/paragraph') ] +]; + + +// Block Parser class +// +function ParserBlock() { + this._rules = []; + this._rulesParagraphTerm = []; + this._rulesBlockquoteTerm = []; + this._rulesListTerm = []; + + this.ruler = new Ruler(this.rulesUpdate.bind(this)); + + for (var i = 0; i < _rules.length; i++) { + this.ruler.push(_rules[i][0], _rules[i][1], { alt: (_rules[i][2] || []).slice() }); + } +} + + +ParserBlock.prototype.rulesUpdate = function () { + this._rules = this.ruler.getRules(); + this._rulesParagraphTerm = this.ruler.getRules('paragraph'); + this._rulesBlockquoteTerm = this.ruler.getRules('blockquote'); + this._rulesListTerm = this.ruler.getRules('list'); +}; + + +// Generate tokens for input range +// +ParserBlock.prototype.tokenize = function (state, startLine, endLine) { + var ok, i, + rules = this._rules, + len = this._rules.length, + line = startLine, + hasEmptyLines = false; + + while (line < endLine) { + state.line = line = state.skipEmptyLines(line); + if (line >= endLine) { break; } + + // Termination condition for nested calls. + // Nested calls currently used for blockquotes & lists + if (state.tShift[line] < state.blkIndent) { break; } + + // Try all possible rules. + // On success, rule should: + // + // - update `state.line` + // - update `state.tokens` + // - return true + + for (i = 0; i < len; i++) { + ok = rules[i](state, line, endLine, false); + if (ok) { break; } + } + + if (!ok) { throw new Error('No matching rules found'); } + + if (line === state.line) { + throw new Error('None of rules updated state.line'); + } + + // set state.tight iff we had an empty line before current tag + // i.e. latest empty line should not count + state.tight = !hasEmptyLines; + + // paragraph might "eat" one newline after it in nested lists + if (state.isEmpty(state.line - 1)) { + hasEmptyLines = true; + } + + line = state.line; + + if (line < endLine && state.isEmpty(line)) { + hasEmptyLines = true; + line++; + + // two empty lines should stop the parser in list mode + if (line < endLine && state.parentType === 'list' && state.isEmpty(line)) { break; } + state.line = line; + } + } +}; + +var TABS_SCAN_RE = /[\n\t]/g; +var NEWLINES_RE = /\r[\n\u0085]|[\u2424\u2028\u0085]/g; +var SPACES_RE = /\u00a0/g; + +ParserBlock.prototype.parse = function (src, options, env) { + var state, lineStart = 0, lastTabPos = 0; + + if (!src) { return []; } + + // Normalize spaces + src = src.replace(SPACES_RE, ' '); + + // Normalize newlines + src = src.replace(NEWLINES_RE, '\n'); + + // Replace tabs with proper number of spaces (1..4) + if (src.indexOf('\t') >= 0) { + src = src.replace(TABS_SCAN_RE, function (match, offset) { + var result; + if (src.charCodeAt(offset) === 0x0A) { + lineStart = offset + 1; + lastTabPos = 0; + return match; + } + result = ' '.slice((offset - lineStart - lastTabPos) % 4); + lastTabPos = offset - lineStart + 1; + return result; + }); + } + + state = new State( + src, + this, + [], + options, + env + ); + + this.tokenize(state, state.line, state.lineMax); + + return state.tokens; +}; + + +module.exports = ParserBlock; + +},{"./ruler":16,"./rules_block/blockquote":17,"./rules_block/code":18,"./rules_block/fences":19,"./rules_block/heading":20,"./rules_block/hr":21,"./rules_block/htmlblock":22,"./rules_block/lheading":23,"./rules_block/list":24,"./rules_block/paragraph":25,"./rules_block/state_block":26,"./rules_block/table":27}],13:[function(require,module,exports){ +// Inline parser + +'use strict'; + + +var Ruler = require('./ruler'); +var StateInline = require('./rules_inline/state_inline'); + +//////////////////////////////////////////////////////////////////////////////// +// Parser rules + +var _rules = [ + [ 'text', require('./rules_inline/text') ], + [ 'newline', require('./rules_inline/newline') ], + [ 'escape', require('./rules_inline/escape') ], + [ 'backticks', require('./rules_inline/backticks') ], + [ 'del', require('./rules_inline/del') ], + [ 'ins', require('./rules_inline/ins') ], + [ 'mark', require('./rules_inline/mark') ], + [ 'emphasis', require('./rules_inline/emphasis') ], + [ 'sub', require('./rules_inline/sub') ], + [ 'sup', require('./rules_inline/sup') ], + [ 'links', require('./rules_inline/links') ], + [ 'autolink', require('./rules_inline/autolink') ], + [ 'htmltag', require('./rules_inline/htmltag') ], + [ 'entity', require('./rules_inline/entity') ] +]; + + +var BAD_PROTOCOLS = [ 'vbscript', 'javascript', 'file' ]; + +function validateLink(url) { + var str = ''; + + try { + str = decodeURI(url).trim().toLowerCase(); + } catch (_) {} + + if (!str) { return false; } + + if (str.indexOf(':') >= 0 && BAD_PROTOCOLS.indexOf(str.split(':')[0]) >= 0) { + return false; + } + return true; +} + +// Inline Parser class +// +function ParserInline() { + this._rules = []; + + // Rule to skip pure text + // - '{}$%@+=:' reserved for extentions + this.textMatch = /[\n\\`*_^\[\]!&<{}$%@~+=:]/; + + // By default CommonMark allows too much in links + // If you need to restrict it - override this with your validator. + this.validateLink = validateLink; + + this.ruler = new Ruler(this.rulesUpdate.bind(this)); + + for (var i = 0; i < _rules.length; i++) { + this.ruler.push(_rules[i][0], _rules[i][1]); + } +} + + +ParserInline.prototype.rulesUpdate = function () { + this._rules = this.ruler.getRules(); +}; + + +// Skip single token by running all rules in validation mode; +// returns `true` if any rule reported success +// +ParserInline.prototype.skipToken = function (state) { + var i, cached_pos, pos = state.pos, + len = this._rules.length; + + if ((cached_pos = state.cacheGet(pos)) > 0) { + state.pos = cached_pos; + return; + } + + for (i = 0; i < len; i++) { + if (this._rules[i](state, true)) { + state.cacheSet(pos, state.pos); + return; + } + } + + state.pos++; + state.cacheSet(pos, state.pos); +}; + + +// Generate tokens for input range +// +ParserInline.prototype.tokenize = function (state) { + var ok, i, + len = this._rules.length, + end = state.posMax; + + while (state.pos < end) { + + // Try all possible rules. + // On success, rule should: + // + // - update `state.pos` + // - update `state.tokens` + // - return true + + for (i = 0; i < len; i++) { + ok = this._rules[i](state, false); + if (ok) { break; } + } + + if (ok) { + if (state.pos >= end) { break; } + continue; + } + + state.pending += state.src[state.pos++]; + } + + if (state.pending) { + state.pushPending(); + } + + return state.tokens; +}; + + +// Parse input string. +// +ParserInline.prototype.parse = function (str, options, env) { + var state = new StateInline(str, this, options, env); + + this.tokenize(state); + + if (options.linkify) { + this.linkifier.process(state); + } + if (options.typographer) { + this.typographer.process(state); + } + + return state.tokens; +}; + + +module.exports = ParserInline; + +},{"./ruler":16,"./rules_inline/autolink":28,"./rules_inline/backticks":29,"./rules_inline/del":30,"./rules_inline/emphasis":31,"./rules_inline/entity":32,"./rules_inline/escape":33,"./rules_inline/htmltag":34,"./rules_inline/ins":35,"./rules_inline/links":36,"./rules_inline/mark":37,"./rules_inline/newline":38,"./rules_inline/state_inline":39,"./rules_inline/sub":40,"./rules_inline/sup":41,"./rules_inline/text":42}],14:[function(require,module,exports){ + +'use strict'; + + +var StateInline = require('./rules_inline/state_inline'); +var parseLinkLabel = require('./links').parseLinkLabel; +var parseLinkDestination = require('./links').parseLinkDestination; +var parseLinkTitle = require('./links').parseLinkTitle; +var normalizeReference = require('./links').normalizeReference; + + +// Parse link reference definition. +// +module.exports = function parse_reference(str, parser, options, env) { + var state, labelEnd, pos, max, code, start, href, title, label; + + if (str.charCodeAt(0) !== 0x5B/* [ */) { return -1; } + + if (str.indexOf(']:') === -1) { return -1; } + + state = new StateInline(str, parser, options, env); + labelEnd = parseLinkLabel(state, 0); + + if (labelEnd < 0 || str.charCodeAt(labelEnd + 1) !== 0x3A/* : */) { return -1; } + + max = state.posMax; + + // [label]: destination 'title' + // ^^^ skip optional whitespace here + for (pos = labelEnd + 2; pos < max; pos++) { + code = state.src.charCodeAt(pos); + if (code !== 0x20 && code !== 0x0A) { break; } + } + + // [label]: destination 'title' + // ^^^^^^^^^^^ parse this + if (!parseLinkDestination(state, pos)) { return -1; } + href = state.linkContent; + pos = state.pos; + + // [label]: destination 'title' + // ^^^ skipping those spaces + start = pos; + for (pos = pos + 1; pos < max; pos++) { + code = state.src.charCodeAt(pos); + if (code !== 0x20 && code !== 0x0A) { break; } + } + + // [label]: destination 'title' + // ^^^^^^^ parse this + if (pos < max && start !== pos && parseLinkTitle(state, pos)) { + title = state.linkContent; + pos = state.pos; + } else { + title = ''; + pos = start; + } + + // ensure that the end of the line is empty + while (pos < max && state.src.charCodeAt(pos) === 0x20/* space */) { pos++; } + if (pos < max && state.src.charCodeAt(pos) !== 0x0A) { return -1; } + + label = normalizeReference(str.slice(1, labelEnd)); + env.references[label] = env.references[label] || { title: title, href: href }; + + return pos; +}; + +},{"./links":11,"./rules_inline/state_inline":39}],15:[function(require,module,exports){ +'use strict'; + + +var assign = require('./common/utils').assign; +var unescapeMd = require('./common/utils').unescapeMd; +var replaceEntities = require('./common/utils').replaceEntities; + + +//////////////////////////////////////////////////////////////////////////////// +// Helpers + +function escapeUrl(str) { + try { + return encodeURI(str); + } catch (__) {} + return ''; +} +function unescapeUrl(str) { + try { + return decodeURI(str); + } catch (__) {} + return ''; +} + +var HTML_ESCAPE_TEST_RE = /[&<>"]/; +var HTML_ESCAPE_REPLACE_RE = /[&<>"]/g; +var HTML_REPLACEMENTS = { + '&': '&', + '<': '<', + '>': '>', + '"': '"' +}; + +function replaceUnsafeChar(ch) { + return HTML_REPLACEMENTS[ch]; +} + +function escapeHtml(str) { + if (HTML_ESCAPE_TEST_RE.test(str)) { + return str.replace(HTML_ESCAPE_REPLACE_RE, replaceUnsafeChar); + } + return str; +} + + +// check if we need to hide '\n' before next token +function getBreak(tokens, idx) { + if (++idx < tokens.length && + tokens[idx].type === 'list_item_close') { + return ''; + } + + return '\n'; +} + +//////////////////////////////////////////////////////////////////////////////// + +var rules = {}; + + +rules.blockquote_open = function (/*tokens, idx, options*/) { + return '
    \n'; +}; +rules.blockquote_close = function (tokens, idx /*, options*/) { + return '
    ' + getBreak(tokens, idx); +}; + + +rules.code = function (tokens, idx /*, options*/) { + if (tokens[idx].block) { + return '
    ' + escapeHtml(tokens[idx].content) + '
    ' + getBreak(tokens, idx); + } + + return '' + escapeHtml(tokens[idx].content) + ''; +}; + + +rules.fence = function (tokens, idx, options) { + var token = tokens[idx]; + var langClass = ''; + var langPrefix = options.langPrefix || ''; + var params, langName = ''; + var highlighted; + + if (token.params) { + params = token.params.split(/ +/g); + langName = escapeHtml(replaceEntities(unescapeMd(params[0]))); + langClass = ' class="' + langPrefix + langName + '"'; + } + + highlighted = options.highlight(token.content, langName) || escapeHtml(token.content); + + return '
    '
    +        + highlighted
    +        + '
    ' + getBreak(tokens, idx); +}; + + +rules.heading_open = function (tokens, idx /*, options*/) { + return ''; +}; +rules.heading_close = function (tokens, idx /*, options*/) { + return '\n'; +}; + + +rules.hr = function (tokens, idx, options) { + return (options.xhtmlOut ? '
    ' : '
    ') + getBreak(tokens, idx); +}; + + +rules.bullet_list_open = function (/*tokens, idx, options*/) { + return '
      \n'; +}; +rules.bullet_list_close = function (tokens, idx /*, options*/) { + return '
    ' + getBreak(tokens, idx); +}; +rules.list_item_open = function (/*tokens, idx, options*/) { + return '
  • '; +}; +rules.list_item_close = function (/*tokens, idx, options*/) { + return '
  • \n'; +}; +rules.ordered_list_open = function (tokens, idx /*, options*/) { + var token = tokens[idx]; + return ' 1 ? ' start="' + token.order + '"' : '') + + '>\n'; +}; +rules.ordered_list_close = function (tokens, idx /*, options*/) { + return '' + getBreak(tokens, idx); +}; + + +rules.paragraph_open = function (tokens, idx/*, options*/) { + return tokens[idx].tight ? '' : '

    '; +}; +rules.paragraph_close = function (tokens, idx /*, options*/) { + return (tokens[idx].tight ? '' : '

    ') + getBreak(tokens, idx); +}; + + +rules.link_open = function (tokens, idx /*, options*/) { + var title = tokens[idx].title ? (' title="' + escapeHtml(replaceEntities(tokens[idx].title)) + '"') : ''; + return '
    '; +}; +rules.link_close = function (/*tokens, idx, options*/) { + return ''; +}; + + +rules.image = function (tokens, idx, options) { + var src = ' src="' + escapeHtml(escapeUrl(tokens[idx].src)) + '"'; + var title = tokens[idx].title ? (' title="' + escapeHtml(replaceEntities(tokens[idx].title)) + '"') : ''; + var alt = ' alt="' + (tokens[idx].alt ? escapeHtml(replaceEntities(tokens[idx].alt)) : '') + '"'; + var suffix = options.xhtmlOut ? ' /' : ''; + return ''; +}; + + +rules.table_open = function (/*tokens, idx, options*/) { + return '\n'; +}; +rules.table_close = function (/*tokens, idx, options*/) { + return '
    \n'; +}; +rules.thead_open = function (/*tokens, idx, options*/) { + return '\n'; +}; +rules.thead_close = function (/*tokens, idx, options*/) { + return '\n'; +}; +rules.tbody_open = function (/*tokens, idx, options*/) { + return '\n'; +}; +rules.tbody_close = function (/*tokens, idx, options*/) { + return '\n'; +}; +rules.tr_open = function (/*tokens, idx, options*/) { + return ''; +}; +rules.tr_close = function (/*tokens, idx, options*/) { + return '\n'; +}; +rules.th_open = function (tokens, idx /*, options*/) { + var token = tokens[idx]; + return ''; +}; +rules.th_close = function (/*tokens, idx, options*/) { + return ''; +}; +rules.td_open = function (tokens, idx /*, options*/) { + var token = tokens[idx]; + return ''; +}; +rules.td_close = function (/*tokens, idx, options*/) { + return ''; +}; + + +rules.strong_open = function(/*tokens, idx, options*/) { + return ''; +}; +rules.strong_close = function(/*tokens, idx, options*/) { + return ''; +}; +rules.em_open = function(/*tokens, idx, options*/) { + return ''; +}; +rules.em_close = function(/*tokens, idx, options*/) { + return ''; +}; + + +rules.del_open = function(/*tokens, idx, options*/) { + return ''; +}; +rules.del_close = function(/*tokens, idx, options*/) { + return ''; +}; + + +rules.ins_open = function(/*tokens, idx, options*/) { + return ''; +}; +rules.ins_close = function(/*tokens, idx, options*/) { + return ''; +}; + + +rules.mark_open = function(/*tokens, idx, options*/) { + return ''; +}; +rules.mark_close = function(/*tokens, idx, options*/) { + return ''; +}; + + +rules.sub = function(tokens, idx/*, options*/) { + return '' + escapeHtml(tokens[idx].content) + ''; +}; +rules.sup = function(tokens, idx/*, options*/) { + return '' + escapeHtml(tokens[idx].content) + ''; +}; + + +rules.hardbreak = function (tokens, idx, options) { + return options.xhtmlOut ? '
    \n' : '
    \n'; +}; +rules.softbreak = function (tokens, idx, options) { + return options.breaks ? (options.xhtmlOut ? '
    \n' : '
    \n') : '\n'; +}; + + +rules.text = function (tokens, idx /*, options*/) { + return escapeHtml(tokens[idx].content); +}; + + +rules.htmlblock = function (tokens, idx /*, options*/) { + return tokens[idx].content; +}; +rules.htmltag = function (tokens, idx /*, options*/) { + return tokens[idx].content; +}; + + +// Renderer class +function Renderer() { + // Clone rules object to allow local modifications + this.rules = assign({}, rules); +} + + +Renderer.prototype.renderInline = function (tokens, options) { + var result = ''; + + for (var i = 0, len = tokens.length; i < len; i++) { + result += rules[tokens[i].type](tokens, i, options); + } + + return result; +}; + + +Renderer.prototype.render = function (tokens, options) { + var i, len, + result = '', + _rules = this.rules; + + for (i = 0, len = tokens.length; i < len; i++) { + if (tokens[i].type === 'inline') { + result += this.renderInline(tokens[i].children, options); + } else { + result += _rules[tokens[i].type](tokens, i, options); + } + } + + return result; +}; + +module.exports = Renderer; + +},{"./common/utils":5}],16:[function(require,module,exports){ +// Ruler is helper class to build responsibility chains from parse rules. +// It allows: +// +// - easy stack rules chains +// - getting main chain and named chains content (as arrays of functions) +// +'use strict'; + + +//////////////////////////////////////////////////////////////////////////////// + +function Ruler(compileFn) { + this.compile = compileFn; // callback to call after each change + + // List of added rules. Each element is: + // + // { + // name: XXX, + // enabled: Boolean, + // fn: Function(), + // alt: [ name2, name3 ] + // } + // + this.rules = []; +} + + +// Find rule index by name +// +Ruler.prototype.find = function (name) { + for (var i = 0; i < this.rules.length; i++) { + if (this.rules[i].name === name) { + return i; + } + } + return -1; +}; + + +// Replace rule function +// +Ruler.prototype.at = function (name, fn, options) { + var index = this.find(name); + var opt = options || {}; + + if (index === -1) { throw new Error('Parser rule not found: ' + name); } + + this.rules[index].fn = fn; + this.rules[index].alt = opt.alt || []; + this.compile(); +}; + + +// Add rule to chain before one with given name. +// +Ruler.prototype.before = function (beforeName, ruleName, fn, options) { + var index = this.find(beforeName); + var opt = options || {}; + + if (index === -1) { throw new Error('Parser rule not found: ' + beforeName); } + + this.rules.splice(index, 0, { + name: ruleName, + enabled: true, + fn: fn, + alt: opt.alt || [] + }); + + this.compile(); +}; + + +// Add rule to chain after one with given name. +// +Ruler.prototype.after = function (afterName, ruleName, fn, options) { + var index = this.find(afterName); + var opt = options || {}; + + if (index === -1) { throw new Error('Parser rule not found: ' + afterName); } + + this.rules.splice(index + 1, 0, { + name: ruleName, + enabled: true, + fn: fn, + alt: opt.alt || [] + }); + + this.compile(); +}; + +// Add rule to the end of chain. +// +Ruler.prototype.push = function (ruleName, fn, options) { + var opt = options || {}; + + this.rules.push({ + name: ruleName, + enabled: true, + fn: fn, + alt: opt.alt || [] + }); + + this.compile(); +}; + + +// Get rules list as array of functions. By default returns main chain +// +Ruler.prototype.getRules = function (chainName) { + var result = []; + + if (!chainName) { + this.rules.forEach(function (rule) { + if (rule.enabled) { + result.push(rule.fn); + } + }); + return result; + } + + this.rules.forEach(function (rule) { + if (rule.alt.indexOf(chainName) >= 0 && rule.enabled) { + result.push(rule.fn); + } + }); + return result; +}; + + +// Enable list of rules by names. If `strict` is true, then all non listed +// rules will be disabled. +// +Ruler.prototype.enable = function (list, strict) { + if (!Array.isArray(list)) { + list = [ list ]; + } + + // In strict mode disable all existing rules first + if (strict) { + this.rules.forEach(function (rule) { + rule.enabled = false; + }); + } + + // Search by name and enable + list.forEach(function (name) { + var idx = this.find(name); + + if (idx < 0) { throw new Error('Rules manager: invalid rule name ' + name); } + this.rules[idx].enabled = true; + + }, this); + + this.compile(); +}; + + +// Disable list of rules by names. +// +Ruler.prototype.disable = function (list) { + if (!Array.isArray(list)) { + list = [ list ]; + } + + // Search by name and disable + list.forEach(function (name) { + var idx = this.find(name); + + if (idx < 0) { throw new Error('Rules manager: invalid rule name ' + name); } + this.rules[idx].enabled = false; + + }, this); + + this.compile(); +}; + + +module.exports = Ruler; + +},{}],17:[function(require,module,exports){ +// Block quotes + +'use strict'; + + +module.exports = function blockquote(state, startLine, endLine, silent) { + var nextLine, lastLineEmpty, oldTShift, oldBMarks, oldIndent, oldParentType, lines, + terminatorRules = state.parser._rulesBlockquoteTerm, i, l, terminate, + pos = state.bMarks[startLine] + state.tShift[startLine], + max = state.eMarks[startLine]; + + if (pos > max) { return false; } + + // check the block quote marker + if (state.src.charCodeAt(pos++) !== 0x3E/* > */) { return false; } + + if (state.level >= state.options.maxNesting) { return false; } + + // we know that it's going to be a valid blockquote, + // so no point trying to find the end of it in silent mode + if (silent) { return true; } + + // skip one optional space after '>' + if (state.src.charCodeAt(pos) === 0x20) { pos++; } + + oldIndent = state.blkIndent; + state.blkIndent = 0; + + oldBMarks = [ state.bMarks[startLine] ]; + state.bMarks[startLine] = pos; + + // check if we have an empty blockquote + pos = pos < max ? state.skipSpaces(pos) : pos; + lastLineEmpty = pos >= max; + + oldTShift = [ state.tShift[startLine] ]; + state.tShift[startLine] = pos - state.bMarks[startLine]; + + // Search the end of the block + // + // Block ends with either: + // 1. an empty line outside: + // ``` + // > test + // + // ``` + // 2. an empty line inside: + // ``` + // > + // test + // ``` + // 3. another tag + // ``` + // > test + // - - - + // ``` + for (nextLine = startLine + 1; nextLine < endLine; nextLine++) { + pos = state.bMarks[nextLine] + state.tShift[nextLine]; + max = state.eMarks[nextLine]; + + if (pos >= max) { + // Case 1: line is not inside the blockquote, and this line is empty. + break; + } + + if (state.src.charCodeAt(pos++) === 0x3E/* > */) { + // This line is inside the blockquote. + + // skip one optional space after '>' + if (state.src.charCodeAt(pos) === 0x20) { pos++; } + + oldBMarks.push(state.bMarks[nextLine]); + state.bMarks[nextLine] = pos; + + pos = pos < max ? state.skipSpaces(pos) : pos; + lastLineEmpty = pos >= max; + + oldTShift.push(state.tShift[nextLine]); + state.tShift[nextLine] = pos - state.bMarks[nextLine]; + continue; + } + + // Case 2: line is not inside the blockquote, and the last line was empty. + if (lastLineEmpty) { break; } + + // Case 3: another tag found. + terminate = false; + for (i = 0, l = terminatorRules.length; i < l; i++) { + if (terminatorRules[i](state, nextLine, endLine, true)) { + terminate = true; + break; + } + } + if (terminate) { break; } + + oldBMarks.push(state.bMarks[nextLine]); + oldTShift.push(state.tShift[nextLine]); + + // A negative number means that this is a paragraph continuation; + // + // Any negative number will do the job here, but it's better for it + // to be large enough to make any bugs obvious. + state.tShift[nextLine] = -1337; + } + + oldParentType = state.parentType; + state.parentType = 'blockquote'; + state.tokens.push({ + type: 'blockquote_open', + lines: lines = [ startLine, 0 ], + level: state.level++ + }); + state.parser.tokenize(state, startLine, nextLine); + state.tokens.push({ + type: 'blockquote_close', + level: --state.level + }); + state.parentType = oldParentType; + lines[1] = state.line; + + // Restore original tShift; this might not be necessary since the parser + // has already been here, but just to make sure we can do that. + for (i = 0; i < oldTShift.length; i++) { + state.bMarks[i + startLine] = oldBMarks[i]; + state.tShift[i + startLine] = oldTShift[i]; + } + state.blkIndent = oldIndent; + + return true; +}; + +},{}],18:[function(require,module,exports){ +// Code block (4 spaces padded) + +'use strict'; + + +module.exports = function code(state, startLine, endLine, silent) { + var nextLine, last; + + if (state.tShift[startLine] - state.blkIndent < 4) { return false; } + + last = nextLine = startLine + 1; + + while (nextLine < endLine) { + if (state.isEmpty(nextLine)) { + nextLine++; + continue; + } + if (state.tShift[nextLine] - state.blkIndent >= 4) { + nextLine++; + last = nextLine; + continue; + } + break; + } + + if (silent) { return true; } + + state.line = nextLine; + state.tokens.push({ + type: 'code', + content: state.getLines(startLine, last, 4 + state.blkIndent, true), + block: true, + lines: [ startLine, state.line ], + level: state.level + }); + + return true; +}; + +},{}],19:[function(require,module,exports){ +// fences (``` lang, ~~~ lang) + +'use strict'; + + +module.exports = function fences(state, startLine, endLine, silent) { + var marker, len, params, nextLine, mem, + haveEndMarker = false, + pos = state.bMarks[startLine] + state.tShift[startLine], + max = state.eMarks[startLine]; + + if (pos + 3 > max) { return false; } + + marker = state.src.charCodeAt(pos); + + if (marker !== 0x7E/* ~ */ && marker !== 0x60 /* ` */) { + return false; + } + + // scan marker length + mem = pos; + pos = state.skipChars(pos, marker); + + len = pos - mem; + + if (len < 3) { return false; } + + params = state.src.slice(pos, max).trim(); + + if (params.indexOf('`') >= 0) { return false; } + + // Since start is found, we can report success here in validation mode + if (silent) { return true; } + + // search end of block + nextLine = startLine; + + for (;;) { + nextLine++; + if (nextLine >= endLine) { + // unclosed block should be autoclosed by end of document. + // also block seems to be autoclosed by end of parent + break; + } + + pos = mem = state.bMarks[nextLine] + state.tShift[nextLine]; + max = state.eMarks[nextLine]; + + if (pos < max && state.tShift[nextLine] < state.blkIndent) { + // non-empty line with negative indent should stop the list: + // - ``` + // test + break; + } + + if (state.src.charCodeAt(pos) !== marker) { continue; } + + if (state.tShift[nextLine] - state.blkIndent >= 4) { + // closing fence should be indented less than 4 spaces + continue; + } + + pos = state.skipChars(pos, marker); + + // closing code fence must be at least as long as the opening one + if (pos - mem < len) { continue; } + + // make sure tail has spaces only + pos = state.skipSpaces(pos); + + if (pos < max) { continue; } + + haveEndMarker = true; + // found! + break; + } + + // If a fence has heading spaces, they should be removed from its inner block + len = state.tShift[startLine]; + + state.line = nextLine + (haveEndMarker ? 1 : 0); + state.tokens.push({ + type: 'fence', + params: params, + content: state.getLines(startLine + 1, nextLine, len, true), + lines: [ startLine, state.line ], + level: state.level + }); + + return true; +}; + +},{}],20:[function(require,module,exports){ +// heading (#, ##, ...) + +'use strict'; + + +module.exports = function heading(state, startLine, endLine, silent) { + var ch, level, tmp, + pos = state.bMarks[startLine] + state.tShift[startLine], + max = state.eMarks[startLine]; + + if (pos >= max) { return false; } + + ch = state.src.charCodeAt(pos); + + if (ch !== 0x23/* # */ || pos >= max) { return false; } + + // count heading level + level = 1; + ch = state.src.charCodeAt(++pos); + while (ch === 0x23/* # */ && pos < max && level <= 6) { + level++; + ch = state.src.charCodeAt(++pos); + } + + if (level > 6 || (pos < max && ch !== 0x20/* space */)) { return false; } + + if (silent) { return true; } + + // Let's cut tails like ' ### ' from the end of string + + max = state.skipCharsBack(max, 0x20/* space */, pos); + tmp = state.skipCharsBack(max, 0x23/* # */, pos); + if (tmp > pos && state.src.charCodeAt(tmp - 1) === 0x20/* space */) { + max = tmp; + } + + state.line = startLine + 1; + + state.tokens.push({ type: 'heading_open', + hLevel: level, + lines: [ startLine, state.line ], + level: state.level + }); + + // only if header is not empty + if (pos < max) { + state.tokens.push({ + type: 'inline', + content: state.src.slice(pos, max).trim(), + level: state.level + 1, + lines: [ startLine, state.line ], + children: [] + }); + } + state.tokens.push({ type: 'heading_close', hLevel: level, level: state.level }); + + return true; +}; + +},{}],21:[function(require,module,exports){ +// Horizontal rule + +'use strict'; + + +module.exports = function hr(state, startLine, endLine, silent) { + var marker, cnt, ch, + pos = state.bMarks[startLine], + max = state.eMarks[startLine]; + + pos += state.tShift[startLine]; + + if (pos > max) { return false; } + + marker = state.src.charCodeAt(pos++); + + // Check hr marker + if (marker !== 0x2A/* * */ && + marker !== 0x2D/* - */ && + marker !== 0x5F/* _ */) { + return false; + } + + // markers can be mixed with spaces, but there should be at least 3 one + + cnt = 1; + while (pos < max) { + ch = state.src.charCodeAt(pos++); + if (ch !== marker && ch !== 0x20/* space */) { return false; } + if (ch === marker) { cnt++; } + } + + if (cnt < 3) { return false; } + + if (silent) { return true; } + + state.line = startLine + 1; + state.tokens.push({ + type: 'hr', + lines: [ startLine, state.line ], + level: state.level + }); + + return true; +}; + +},{}],22:[function(require,module,exports){ +// HTML block + +'use strict'; + + +var block_names = require('../common/html_blocks'); + + +var HTML_TAG_OPEN_RE = /^<([a-zA-Z]{1,15})[\s\/>]/; +var HTML_TAG_CLOSE_RE = /^<\/([a-zA-Z]{1,15})[\s>]/; + +function isLetter(ch) { + /*eslint no-bitwise:0*/ + var lc = ch | 0x20; // to lower case + return (lc >= 0x61/* a */) && (lc <= 0x7a/* z */); +} + +module.exports = function htmlblock(state, startLine, endLine, silent) { + var ch, match, nextLine, + pos = state.bMarks[startLine], + max = state.eMarks[startLine], + shift = state.tShift[startLine]; + + pos += shift; + + if (!state.options.html) { return false; } + + if (shift > 3 || pos + 2 >= max) { return false; } + + if (state.src.charCodeAt(pos) !== 0x3C/* < */) { return false; } + + ch = state.src.charCodeAt(pos + 1); + + if (ch === 0x21/* ! */ || ch === 0x3F/* ? */) { + // Directive start / comment start / processing instruction start + if (silent) { return true; } + + } else if (ch === 0x2F/* / */ || isLetter(ch)) { + + // Probably start or end of tag + if (ch === 0x2F/* \ */) { + // closing tag + match = state.src.slice(pos, max).match(HTML_TAG_CLOSE_RE); + if (!match) { return false; } + } else { + // opening tag + match = state.src.slice(pos, max).match(HTML_TAG_OPEN_RE); + if (!match) { return false; } + } + // Make sure tag name is valid + if (block_names[match[1].toLowerCase()] !== true) { return false; } + if (silent) { return true; } + + } else { + return false; + } + + // If we are here - we detected HTML block. + // Let's roll down till empty line (block end). + nextLine = startLine + 1; + while (nextLine < state.lineMax && !state.isEmpty(nextLine)) { + nextLine++; + } + + state.line = nextLine; + state.tokens.push({ + type: 'htmlblock', + level: state.level, + lines: [ startLine, state.line ], + content: state.getLines(startLine, nextLine, 0, true) + }); + + return true; +}; + +},{"../common/html_blocks":2}],23:[function(require,module,exports){ +// lheading (---, ===) + +'use strict'; + + +module.exports = function lheading(state, startLine, endLine, silent) { + var marker, pos, max, + next = startLine + 1; + + if (next >= endLine) { return false; } + if (state.tShift[next] < state.blkIndent) { return false; } + + // Scan next line + + if (state.tShift[next] - state.blkIndent > 3) { return false; } + + pos = state.bMarks[next] + state.tShift[next]; + max = state.eMarks[next]; + + if (pos >= max) { return false; } + + marker = state.src.charCodeAt(pos); + + if (marker !== 0x2D/* - */ && marker !== 0x3D/* = */) { return false; } + + pos = state.skipChars(pos, marker); + + pos = state.skipSpaces(pos); + + if (pos < max) { return false; } + + if (silent) { return true; } + + pos = state.bMarks[startLine] + state.tShift[startLine]; + + state.line = next + 1; + state.tokens.push({ + type: 'heading_open', + hLevel: marker === 0x3D/* = */ ? 1 : 2, + lines: [ startLine, state.line ], + level: state.level + }); + state.tokens.push({ + type: 'inline', + content: state.src.slice(pos, state.eMarks[startLine]).trim(), + level: state.level + 1, + lines: [ startLine, state.line - 1 ], + children: [] + }); + state.tokens.push({ + type: 'heading_close', + hLevel: marker === 0x3D/* = */ ? 1 : 2, + level: state.level + }); + + return true; +}; + +},{}],24:[function(require,module,exports){ +// Lists + +'use strict'; + + +// Search `[-+*][\n ]`, returns next pos arter marker on success +// or -1 on fail. +function skipBulletListMarker(state, startLine) { + var marker, pos, max; + + pos = state.bMarks[startLine] + state.tShift[startLine]; + max = state.eMarks[startLine]; + + if (pos >= max) { return -1; } + + marker = state.src.charCodeAt(pos++); + // Check bullet + if (marker !== 0x2A/* * */ && + marker !== 0x2D/* - */ && + marker !== 0x2B/* + */) { + return -1; + } + + if (pos < max && state.src.charCodeAt(pos) !== 0x20) { + // " 1.test " - is not a list item + return -1; + } + + return pos; +} + +// Search `\d+[.)][\n ]`, returns next pos arter marker on success +// or -1 on fail. +function skipOrderedListMarker(state, startLine) { + var ch, + pos = state.bMarks[startLine] + state.tShift[startLine], + max = state.eMarks[startLine]; + + if (pos + 1 >= max) { return -1; } + + ch = state.src.charCodeAt(pos++); + + if (ch < 0x30/* 0 */ || ch > 0x39/* 9 */) { return -1; } + + for (;;) { + // EOL -> fail + if (pos >= max) { return -1; } + + ch = state.src.charCodeAt(pos++); + + if (ch >= 0x30/* 0 */ && ch <= 0x39/* 9 */) { + continue; + } + + // found valid marker + if (ch === 0x29/* ) */ || ch === 0x2e/* . */) { + break; + } + + return -1; + } + + + if (pos < max && state.src.charCodeAt(pos) !== 0x20/* space */) { + // " 1.test " - is not a list item + return -1; + } + return pos; +} + +function markTightParagraphs(state, idx) { + var i, l, + level = state.level + 2; + + for (i = idx + 2, l = state.tokens.length - 2; i < l; i++) { + if (state.tokens[i].level === level && state.tokens[i].type === 'paragraph_open') { + state.tokens[i + 2].tight = true; + state.tokens[i].tight = true; + i += 2; + } + } +} + + +module.exports = function list(state, startLine, endLine, silent) { + var nextLine, + indent, + oldTShift, + oldIndent, + oldTight, + oldParentType, + start, + posAfterMarker, + max, + indentAfterMarker, + markerValue, + markerCharCode, + isOrdered, + contentStart, + listTokIdx, + prevEmptyEnd, + listLines, + itemLines, + tight = true, + terminatorRules = state.parser._rulesListTerm, + i, l, terminate; + + // Detect list type and position after marker + if ((posAfterMarker = skipOrderedListMarker(state, startLine)) >= 0) { + isOrdered = true; + } else if ((posAfterMarker = skipBulletListMarker(state, startLine)) >= 0) { + isOrdered = false; + } else { + return false; + } + + if (state.level >= state.options.maxNesting) { return false; } + + // We should terminate list on style change. Remember first one to compare. + markerCharCode = state.src.charCodeAt(posAfterMarker - 1); + + // For validation mode we can terminate immediately + if (silent) { return true; } + + // Start list + listTokIdx = state.tokens.length; + + if (isOrdered) { + start = state.bMarks[startLine] + state.tShift[startLine]; + markerValue = Number(state.src.substr(start, posAfterMarker - start - 1)); + + state.tokens.push({ + type: 'ordered_list_open', + order: markerValue, + lines: listLines = [ startLine, 0 ], + level: state.level++ + }); + + } else { + state.tokens.push({ + type: 'bullet_list_open', + lines: listLines = [ startLine, 0 ], + level: state.level++ + }); + } + + // + // Iterate list items + // + + nextLine = startLine; + prevEmptyEnd = false; + + while (nextLine < endLine) { + contentStart = state.skipSpaces(posAfterMarker); + max = state.eMarks[nextLine]; + + if (contentStart >= max) { + // trimming space in "- \n 3" case, indent is 1 here + indentAfterMarker = 1; + } else { + indentAfterMarker = contentStart - posAfterMarker; + } + + // If we have more than 4 spaces, the indent is 1 + // (the rest is just indented code block) + if (indentAfterMarker > 4) { indentAfterMarker = 1; } + + // If indent is less than 1, assume that it's one, example: + // "-\n test" + if (indentAfterMarker < 1) { indentAfterMarker = 1; } + + // " - test" + // ^^^^^ - calculating total length of this thing + indent = (posAfterMarker - state.bMarks[nextLine]) + indentAfterMarker; + + // Run subparser & write tokens + state.tokens.push({ + type: 'list_item_open', + lines: itemLines = [ startLine, 0 ], + level: state.level++ + }); + + //nextLine++; + + oldIndent = state.blkIndent; + oldTight = state.tight; + oldTShift = state.tShift[startLine]; + oldParentType = state.parentType; + state.tShift[startLine] = contentStart - state.bMarks[startLine]; + state.blkIndent = indent; + state.tight = true; + state.parentType = 'list'; + + state.parser.tokenize(state, startLine, endLine, true); + + // If any of list item is tight, mark list as tight + if (!state.tight || prevEmptyEnd) { + tight = false; + } + // Item become loose if finish with empty line, + // but we should filter last element, because it means list finish + prevEmptyEnd = (state.line - startLine) > 1 && state.isEmpty(state.line - 1); + + state.blkIndent = oldIndent; + state.tShift[startLine] = oldTShift; + state.tight = oldTight; + state.parentType = oldParentType; + + state.tokens.push({ + type: 'list_item_close', + level: --state.level + }); + + nextLine = startLine = state.line; + itemLines[1] = nextLine; + contentStart = state.bMarks[startLine]; + + if (nextLine >= endLine) { break; } + + if (state.isEmpty(nextLine)) { + break; + } + + // + // Try to check if list is terminated or continued. + // + if (state.tShift[nextLine] < state.blkIndent) { break; } + + // fail if terminating block found + terminate = false; + for (i = 0, l = terminatorRules.length; i < l; i++) { + if (terminatorRules[i](state, nextLine, endLine, true)) { + terminate = true; + break; + } + } + if (terminate) { break; } + + // fail if list has another type + if (isOrdered) { + posAfterMarker = skipOrderedListMarker(state, nextLine); + if (posAfterMarker < 0) { break; } + } else { + posAfterMarker = skipBulletListMarker(state, nextLine); + if (posAfterMarker < 0) { break; } + } + + if (markerCharCode !== state.src.charCodeAt(posAfterMarker - 1)) { break; } + } + + // Finilize list + state.tokens.push({ + type: isOrdered ? 'ordered_list_close' : 'bullet_list_close', + level: --state.level + }); + listLines[1] = nextLine; + + state.line = nextLine; + + // mark paragraphs tight if needed + if (tight) { + markTightParagraphs(state, listTokIdx); + } + + return true; +}; + +},{}],25:[function(require,module,exports){ +// Paragraph + +'use strict'; + + +var parseRef = require('../parser_ref'); + + +module.exports = function paragraph(state, startLine/*, endLine*/) { + var endLine, content, pos, terminate, i, l, + nextLine = startLine + 1, + terminatorRules = state.parser._rulesParagraphTerm; + + endLine = state.lineMax; + + // jump line-by-line until empty one or EOF + for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) { + // this would be a code block normally, but after paragraph + // it's considered a lazy continuation regardless of what's there + if (state.tShift[nextLine] - state.blkIndent > 3) { continue; } + + // Some tags can terminate paragraph without empty line. + terminate = false; + for (i = 0, l = terminatorRules.length; i < l; i++) { + if (terminatorRules[i](state, nextLine, endLine, true)) { + terminate = true; + break; + } + } + if (terminate) { break; } + } + + content = state.getLines(startLine, nextLine, state.blkIndent, false).trim(); + + while (content.length) { + pos = parseRef(content, state.parser.inline, state.options, state.env); + if (pos < 0) { break; } + content = content.slice(pos).trim(); + } + + state.line = nextLine; + if (content.length) { + state.tokens.push({ + type: 'paragraph_open', + tight: false, + lines: [ startLine, state.line ], + level: state.level + }); + state.tokens.push({ + type: 'inline', + content: content, + level: state.level + 1, + lines: [ startLine, state.line ], + children: [] + }); + state.tokens.push({ + type: 'paragraph_close', + tight: false, + level: state.level + }); + } + + return true; +}; + +},{"../parser_ref":14}],26:[function(require,module,exports){ +// Parser state class + +'use strict'; + + +function StateBlock(src, parser, tokens, options, env) { + var ch, s, start, pos, len, indent, indent_found; + + this.src = src; + + // Shortcuts to simplify nested calls + this.parser = parser; + + this.options = options; + + this.env = env; + + // + // Internal state vartiables + // + + this.tokens = tokens; + + this.bMarks = []; // line begin offsets for fast jumps + this.eMarks = []; // line end offsets for fast jumps + this.tShift = []; // indent for each line + + // block parser variables + this.blkIndent = 0; // required block content indent + // (for example, if we are in list) + this.line = 0; // line index in src + this.lineMax = 0; // lines count + this.tight = false; // loose/tight mode for lists + this.parentType = 'root'; // if `list`, block parser stops on two newlines + + this.level = 0; + + // renderer + this.result = ''; + + // Create caches + // Generate markers. + s = this.src; + indent = 0; + indent_found = false; + + for (start = pos = indent = 0, len = s.length; pos < len; pos++) { + ch = s.charCodeAt(pos); + + if (!indent_found) { + if (ch === 0x20/* space */) { + indent++; + continue; + } else { + indent_found = true; + } + } + + if (ch === 0x0A || pos === len - 1) { + if (ch !== 0x0A) { pos++; } + this.bMarks.push(start); + this.eMarks.push(pos); + this.tShift.push(indent); + + indent_found = false; + indent = 0; + start = pos + 1; + } + } + + // Push fake entry to simplify cache bounds checks + this.bMarks.push(s.length); + this.eMarks.push(s.length); + this.tShift.push(0); + + this.lineMax = this.bMarks.length - 1; // don't count last fake line +} + +StateBlock.prototype.isEmpty = function isEmpty(line) { + return this.bMarks[line] + this.tShift[line] >= this.eMarks[line]; +}; + +StateBlock.prototype.skipEmptyLines = function skipEmptyLines(from) { + for (var max = this.lineMax; from < max; from++) { + if (this.bMarks[from] + this.tShift[from] < this.eMarks[from]) { + break; + } + } + return from; +}; + +// Skip spaces from given position. +StateBlock.prototype.skipSpaces = function skipSpaces(pos) { + for (var max = this.src.length; pos < max; pos++) { + if (this.src.charCodeAt(pos) !== 0x20/* space */) { break; } + } + return pos; +}; + +// Skip char codes from given position +StateBlock.prototype.skipChars = function skipChars(pos, code) { + for (var max = this.src.length; pos < max; pos++) { + if (this.src.charCodeAt(pos) !== code) { break; } + } + return pos; +}; + +// Skip char codes reverse from given position - 1 +StateBlock.prototype.skipCharsBack = function skipCharsBack(pos, code, min) { + if (pos <= min) { return pos; } + + while (pos > min) { + if (code !== this.src.charCodeAt(--pos)) { return pos + 1; } + } + return pos; +}; + +// cut lines range from source. +StateBlock.prototype.getLines = function getLines(begin, end, indent, keepLastLF) { + var i, first, last, queue, shift, + line = begin; + + if (begin >= end) { + return ''; + } + + // Opt: don't use push queue for single line; + if (line + 1 === end) { + first = this.bMarks[line] + Math.min(this.tShift[line], indent); + last = keepLastLF ? this.bMarks[end] : this.eMarks[end - 1]; + return this.src.slice(first, last); + } + + queue = new Array(end - begin); + + for (i = 0; line < end; line++, i++) { + shift = this.tShift[line]; + if (shift > indent) { shift = indent; } + if (shift < 0) { shift = 0; } + + first = this.bMarks[line] + shift; + + if (line + 1 < end || keepLastLF) { + // No need for bounds check because we have fake entry on tail. + last = this.eMarks[line] + 1; + } else { + last = this.eMarks[line]; + } + + queue[i] = this.src.slice(first, last); + } + + return queue.join(''); +}; + + +module.exports = StateBlock; + +},{}],27:[function(require,module,exports){ +// GFM table, non-standard + +'use strict'; + + +function lineMatch(state, line, reg) { + var pos = state.bMarks[line] + state.blkIndent, + max = state.eMarks[line]; + + return state.src.substr(pos, max - pos).match(reg); +} + + +module.exports = function table(state, startLine, endLine, silent) { + var ch, firstLineMatch, secondLineMatch, pos, i, nextLine, m, rows, + aligns, t, tableLines, tbodyLines; + + // should have at least three lines + if (startLine + 2 > endLine) { return false; } + + nextLine = startLine + 1; + + if (state.tShift[nextLine] < state.blkIndent) { return false; } + + // first character of the second line should be '|' or '-' + + pos = state.bMarks[nextLine] + state.tShift[nextLine]; + if (pos >= state.eMarks[nextLine]) { return false; } + + ch = state.src.charCodeAt(pos); + if (ch !== 0x7C/* | */ && ch !== 0x2D/* - */ && ch !== 0x3A/* : */) { return false; } + + secondLineMatch = lineMatch(state, startLine + 1, + /^ *\|?(( *[:-]-+[:-] *\|)+( *[:-]-+[:-] *))\|? *$/); + if (!secondLineMatch) { return false; } + + rows = secondLineMatch[1].split('|'); + aligns = []; + for (i = 0; i < rows.length; i++) { + t = rows[i].trim(); + if (t.charCodeAt(t.length - 1) === 0x3A/* : */) { + aligns[i] = t.charCodeAt(0) === 0x3A/* : */ ? 'center' : 'right'; + } else if (t.charCodeAt(0) === 0x3A/* : */) { + aligns[i] = 'left'; + } else { + aligns[i] = ''; + } + } + + firstLineMatch = lineMatch(state, startLine, /^ *\|?(.*?\|.*?)\|? *$/); + if (!firstLineMatch) { return false; } + + rows = firstLineMatch[1].split('|'); + if (aligns.length !== rows.length) { return false; } + if (silent) { return true; } + + state.tokens.push({ + type: 'table_open', + lines: tableLines = [ startLine, 0 ], + level: state.level++ + }); + state.tokens.push({ + type: 'thead_open', + lines: [ startLine, startLine + 1 ], + level: state.level++ + }); + + state.tokens.push({ + type: 'tr_open', + lines: [ startLine, startLine + 1 ], + level: state.level++ + }); + for (i = 0; i < rows.length; i++) { + state.tokens.push({ + type: 'th_open', + align: aligns[i], + lines: [ startLine, startLine + 1 ], + level: state.level++ + }); + state.tokens.push({ + type: 'inline', + content: rows[i].trim(), + lines: [ startLine, startLine + 1 ], + level: state.level, + children: [] + }); + state.tokens.push({ type: 'th_close', level: --state.level }); + } + state.tokens.push({ type: 'tr_close', level: --state.level }); + state.tokens.push({ type: 'thead_close', level: --state.level }); + + state.tokens.push({ + type: 'tbody_open', + lines: tbodyLines = [ startLine + 2, 0 ], + level: state.level++ + }); + + for (nextLine = startLine + 2; nextLine < endLine; nextLine++) { + if (state.tShift[nextLine] < state.blkIndent) { break; } + + m = lineMatch(state, nextLine, /^ *\|?(.*?\|.*?)\|? *$/); + if (!m) { break; } + rows = m[1].split('|'); + + state.tokens.push({ type: 'tr_open', level: state.level++ }); + for (i = 0; i < rows.length; i++) { + state.tokens.push({ type: 'td_open', align: aligns[i], level: state.level++ }); + state.tokens.push({ + type: 'inline', + content: rows[i].replace(/^\|? *| *\|?$/g, ''), + level: state.level, + children: [] + }); + state.tokens.push({ type: 'td_close', level: --state.level }); + } + state.tokens.push({ type: 'tr_close', level: --state.level }); + } + state.tokens.push({ type: 'tbody_close', level: --state.level }); + state.tokens.push({ type: 'table_close', level: --state.level }); + + tableLines[1] = tbodyLines[1] = nextLine; + state.line = nextLine; + return true; +}; + +},{}],28:[function(require,module,exports){ +// Process autolinks '' + +'use strict'; + +var url_schemas = require('../common/url_schemas'); + + +/*eslint max-len:0*/ +var EMAIL_RE = /^<([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>/; +var AUTOLINK_RE = /^<([a-zA-Z.\-]{1,25}):([^<>\x00-\x20]*)>/; + + +module.exports = function autolink(state, silent) { + var tail, linkMatch, emailMatch, url, pos = state.pos; + + if (state.src.charCodeAt(pos) !== 0x3C/* < */) { return false; } + + tail = state.src.slice(pos); + + if (tail.indexOf('>') < 0) { return false; } + + linkMatch = tail.match(AUTOLINK_RE); + + if (linkMatch) { + if (url_schemas.indexOf(linkMatch[1].toLowerCase()) < 0) { return false; } + + url = linkMatch[0].slice(1, -1); + + if (!state.parser.validateLink(url)) { return false; } + + if (!silent) { + state.push({ + type: 'link_open', + href: url, + level: state.level + }); + state.push({ + type: 'text', + content: url, + level: state.level + 1 + }); + state.push({ type: 'link_close', level: state.level }); + } + + state.pos += linkMatch[0].length; + return true; + } + + emailMatch = tail.match(EMAIL_RE); + + if (emailMatch) { + + url = emailMatch[0].slice(1, -1); + + if (!state.parser.validateLink('mailto:' + url)) { return false; } + + if (!silent) { + state.push({ + type: 'link_open', + href: 'mailto:' + url, + level: state.level + }); + state.push({ + type: 'text', + content: url, + level: state.level + 1 + }); + state.push({ type: 'link_close', level: state.level }); + } + + state.pos += emailMatch[0].length; + return true; + } + + return false; +}; + +},{"../common/url_schemas":4}],29:[function(require,module,exports){ +// Parse backticks + +'use strict'; + +module.exports = function backticks(state, silent) { + var start, max, marker, matchStart, matchEnd, + pos = state.pos, + ch = state.src.charCodeAt(pos); + + if (ch !== 0x60/* ` */) { return false; } + + start = pos; + pos++; + max = state.posMax; + + while (pos < max && state.src.charCodeAt(pos) === 0x60/* ` */) { pos++; } + + marker = state.src.slice(start, pos); + + matchStart = matchEnd = pos; + + while ((matchStart = state.src.indexOf('`', matchEnd)) !== -1) { + matchEnd = matchStart + 1; + + while (matchEnd < max && state.src.charCodeAt(matchEnd) === 0x60/* ` */) { matchEnd++; } + + if (matchEnd - matchStart === marker.length) { + if (!silent) { + state.push({ + type: 'code', + content: state.src.slice(pos, matchStart) + .replace(/[ \n]+/g,' ') + .trim(), + block: false, + level: state.level + }); + } + state.pos = matchEnd; + return true; + } + } + + if (!silent) { state.pending += marker; } + state.pos += marker.length; + return true; +}; + +},{}],30:[function(require,module,exports){ +// Process ~~deleted text~~ + +'use strict'; + +module.exports = function del(state, silent) { + var found, + pos, + stack, + max = state.posMax, + start = state.pos, + lastChar, + nextChar; + + if (state.src.charCodeAt(start) !== 0x7E/* ~ */) { return false; } + if (silent) { return false; } // don't run any pairs in validation mode + if (start + 4 >= max) { return false; } + if (state.src.charCodeAt(start + 1) !== 0x7E/* ~ */) { return false; } + if (state.level >= state.options.maxNesting) { return false; } + + lastChar = start > 0 ? state.src.charCodeAt(start - 1) : -1; + nextChar = state.src.charCodeAt(start + 2); + + if (lastChar === 0x7E/* ~ */) { return false; } + if (nextChar === 0x7E/* ~ */) { return false; } + if (nextChar === 0x20 || nextChar === 0x0A) { return false; } + + pos = start + 2; + while (pos < max && state.src.charCodeAt(pos) === 0x7E/* ~ */) { pos++; } + if (pos > start + 3) { + // sequence of 4+ markers taking as literal, same as in a emphasis + state.pos += pos - start; + if (!silent) { state.pending += state.src.slice(start, pos); } + return true; + } + + state.pos = start + 2; + stack = 1; + + while (state.pos + 1 < max) { + if (state.src.charCodeAt(state.pos) === 0x7E/* ~ */) { + if (state.src.charCodeAt(state.pos + 1) === 0x7E/* ~ */) { + lastChar = state.src.charCodeAt(state.pos - 1); + nextChar = state.pos + 2 < max ? state.src.charCodeAt(state.pos + 2) : -1; + if (nextChar !== 0x7E/* ~ */ && lastChar !== 0x7E/* ~ */) { + if (lastChar !== 0x20 && lastChar !== 0x0A) { + // closing '~~' + stack--; + } else if (nextChar !== 0x20 && nextChar !== 0x0A) { + // opening '~~' + stack++; + } // else { + // // standalone ' ~~ ' indented with spaces + //} + if (stack <= 0) { + found = true; + break; + } + } + } + } + + state.parser.skipToken(state); + } + + if (!found) { + // parser failed to find ending tag, so it's not valid emphasis + state.pos = start; + return false; + } + + // found! + state.posMax = state.pos; + state.pos = start + 2; + + if (!silent) { + state.push({ type: 'del_open', level: state.level++ }); + state.parser.tokenize(state); + state.push({ type: 'del_close', level: --state.level }); + } + + state.pos = state.posMax + 2; + state.posMax = max; + return true; +}; + +},{}],31:[function(require,module,exports){ +// Process *this* and _that_ + +'use strict'; + + +function isAlphaNum(code) { + return (code >= 0x30 /* 0 */ && code <= 0x39 /* 9 */) || + (code >= 0x41 /* A */ && code <= 0x5A /* Z */) || + (code >= 0x61 /* a */ && code <= 0x7A /* z */); +} + +// parse sequence of emphasis markers, +// "start" should point at a valid marker +function scanDelims(state, start) { + var pos = start, lastChar, nextChar, count, + can_open = true, + can_close = true, + max = state.posMax, + marker = state.src.charCodeAt(start); + + lastChar = start > 0 ? state.src.charCodeAt(start - 1) : -1; + + while (pos < max && state.src.charCodeAt(pos) === marker) { pos++; } + if (pos >= max) { can_open = false; } + count = pos - start; + + if (count >= 4) { + // sequence of four or more unescaped markers can't start/end an emphasis + can_open = can_close = false; + } else { + nextChar = pos < max ? state.src.charCodeAt(pos) : -1; + + // check whitespace conditions + if (nextChar === 0x20 || nextChar === 0x0A) { can_open = false; } + if (lastChar === 0x20 || lastChar === 0x0A) { can_close = false; } + + if (marker === 0x5F /* _ */) { + // check if we aren't inside the word + if (isAlphaNum(lastChar)) { can_open = false; } + if (isAlphaNum(nextChar)) { can_close = false; } + } + } + + return { + can_open: can_open, + can_close: can_close, + delims: count + }; +} + +module.exports = function emphasis(state, silent) { + var startCount, + count, + found, + oldCount, + newCount, + stack, + res, + max = state.posMax, + start = state.pos, + marker = state.src.charCodeAt(start); + + if (marker !== 0x5F/* _ */ && marker !== 0x2A /* * */) { return false; } + if (silent) { return false; } // don't run any pairs in validation mode + + res = scanDelims(state, start); + startCount = res.delims; + if (!res.can_open) { + state.pos += startCount; + if (!silent) { state.pending += state.src.slice(start, state.pos); } + return true; + } + + if (state.level >= state.options.maxNesting) { return false; } + + state.pos = start + startCount; + stack = [ startCount ]; + + while (state.pos < max) { + if (state.src.charCodeAt(state.pos) === marker) { + res = scanDelims(state, state.pos); + count = res.delims; + if (res.can_close) { + oldCount = stack.pop(); + newCount = count; + + while (oldCount !== newCount) { + if (newCount < oldCount) { + stack.push(oldCount - newCount); + break; + } + + // assert(newCount > oldCount) + newCount -= oldCount; + + if (stack.length === 0) { break; } + state.pos += oldCount; + oldCount = stack.pop(); + } + + if (stack.length === 0) { + startCount = oldCount; + found = true; + break; + } + state.pos += count; + continue; + } + + if (res.can_open) { stack.push(count); } + state.pos += count; + continue; + } + + state.parser.skipToken(state); + } + + if (!found) { + // parser failed to find ending tag, so it's not valid emphasis + state.pos = start; + return false; + } + + // found! + state.posMax = state.pos; + state.pos = start + startCount; + + if (!silent) { + if (startCount === 2 || startCount === 3) { + state.push({ type: 'strong_open', level: state.level++ }); + } + if (startCount === 1 || startCount === 3) { + state.push({ type: 'em_open', level: state.level++ }); + } + + state.parser.tokenize(state); + + if (startCount === 1 || startCount === 3) { + state.push({ type: 'em_close', level: --state.level }); + } + if (startCount === 2 || startCount === 3) { + state.push({ type: 'strong_close', level: --state.level }); + } + } + + state.pos = state.posMax + startCount; + state.posMax = max; + return true; +}; + +},{}],32:[function(require,module,exports){ +// Process html entity - {, ¯, ", ... + +'use strict'; + +var entities = require('../common/entities'); +var isValidEntityCode = require('../common/utils').isValidEntityCode; +var fromCodePoint = require('../common/utils').fromCodePoint; + + +var DIGITAL_RE = /^&#((?:x[a-f0-9]{1,8}|[0-9]{1,8}));/i; +var NAMED_RE = /^&([a-z][a-z0-9]{1,31});/i; + + +module.exports = function entity(state, silent) { + var ch, code, match, pos = state.pos, max = state.posMax; + + if (state.src.charCodeAt(pos) !== 0x26/* & */) { return false; } + + if (pos + 1 < max) { + ch = state.src.charCodeAt(pos + 1); + + if (ch === 0x23 /* # */) { + match = state.src.slice(pos).match(DIGITAL_RE); + if (match) { + if (!silent) { + code = match[1][0].toLowerCase() === 'x' ? parseInt(match[1].slice(1), 16) : parseInt(match[1], 10); + state.pending += isValidEntityCode(code) ? fromCodePoint(code) : fromCodePoint(0xFFFD); + } + state.pos += match[0].length; + return true; + } + } else { + match = state.src.slice(pos).match(NAMED_RE); + if (match) { + if (entities.hasOwnProperty(match[1])) { + if (!silent) { state.pending += entities[match[1]]; } + state.pos += match[0].length; + return true; + } + } + } + } + + if (!silent) { state.pending += '&'; } + state.pos++; + return true; +}; + +},{"../common/entities":1,"../common/utils":5}],33:[function(require,module,exports){ +// Proceess escaped chars and hardbreaks + +'use strict'; + +var ESCAPED = []; + +for (var i = 0; i < 256; i++) { ESCAPED.push(0); } + +'\\!"#$%&\'()*+,./:;<=>?@[]^_`{|}~-' + .split('').forEach(function(ch) { ESCAPED[ch.charCodeAt(0)] = 1; }); + + +module.exports = function escape(state, silent) { + var ch, pos = state.pos, max = state.posMax; + + if (state.src.charCodeAt(pos) !== 0x5C/* \ */) { return false; } + + pos++; + + if (pos < max) { + ch = state.src.charCodeAt(pos); + + if (ch < 256 && ESCAPED[ch] !== 0) { + if (!silent) { state.pending += state.src[pos]; } + state.pos += 2; + return true; + } + + if (ch === 0x0A) { + if (!silent) { + state.push({ + type: 'hardbreak', + level: state.level + }); + } + + pos++; + // skip leading whitespaces from next line + while (pos < max && state.src.charCodeAt(pos) === 0x20) { pos++; } + + state.pos = pos; + return true; + } + } + + if (!silent) { state.pending += '\\'; } + state.pos++; + return true; +}; + +},{}],34:[function(require,module,exports){ +// Process html tags + +'use strict'; + + +var HTML_TAG_RE = require('../common/html_re').HTML_TAG_RE; + + +function isLetter(ch) { + /*eslint no-bitwise:0*/ + var lc = ch | 0x20; // to lower case + return (lc >= 0x61/* a */) && (lc <= 0x7a/* z */); +} + + +module.exports = function htmltag(state, silent) { + var ch, match, max, pos = state.pos; + + if (!state.options.html) { return false; } + + // Check start + max = state.posMax; + if (state.src.charCodeAt(pos) !== 0x3C/* < */ || + pos + 2 >= max) { + return false; + } + + // Quick fail on second char + ch = state.src.charCodeAt(pos + 1); + if (ch !== 0x21/* ! */ && + ch !== 0x3F/* ? */ && + ch !== 0x2F/* / */ && + !isLetter(ch)) { + return false; + } + + match = state.src.slice(pos).match(HTML_TAG_RE); + if (!match) { return false; } + + if (!silent) { + state.push({ + type: 'htmltag', + content: state.src.slice(pos, pos + match[0].length), + level: state.level + }); + } + state.pos += match[0].length; + return true; +}; + +},{"../common/html_re":3}],35:[function(require,module,exports){ +// Process ++inserted text++ + +'use strict'; + +module.exports = function ins(state, silent) { + var found, + pos, + stack, + max = state.posMax, + start = state.pos, + lastChar, + nextChar; + + if (state.src.charCodeAt(start) !== 0x2B/* + */) { return false; } + if (silent) { return false; } // don't run any pairs in validation mode + if (start + 4 >= max) { return false; } + if (state.src.charCodeAt(start + 1) !== 0x2B/* + */) { return false; } + if (state.level >= state.options.maxNesting) { return false; } + + lastChar = start > 0 ? state.src.charCodeAt(start - 1) : -1; + nextChar = state.src.charCodeAt(start + 2); + + if (lastChar === 0x2B/* + */) { return false; } + if (nextChar === 0x2B/* + */) { return false; } + if (nextChar === 0x20 || nextChar === 0x0A) { return false; } + + pos = start + 2; + while (pos < max && state.src.charCodeAt(pos) === 0x2B/* + */) { pos++; } + if (pos !== start + 2) { + // sequence of 3+ markers taking as literal, same as in a emphasis + state.pos += pos - start; + if (!silent) { state.pending += state.src.slice(start, pos); } + return true; + } + + state.pos = start + 2; + stack = 1; + + while (state.pos + 1 < max) { + if (state.src.charCodeAt(state.pos) === 0x2B/* + */) { + if (state.src.charCodeAt(state.pos + 1) === 0x2B/* + */) { + lastChar = state.src.charCodeAt(state.pos - 1); + nextChar = state.pos + 2 < max ? state.src.charCodeAt(state.pos + 2) : -1; + if (nextChar !== 0x2B/* + */ && lastChar !== 0x2B/* + */) { + if (lastChar !== 0x20 && lastChar !== 0x0A) { + // closing '++' + stack--; + } else if (nextChar !== 0x20 && nextChar !== 0x0A) { + // opening '++' + stack++; + } // else { + // // standalone ' ++ ' indented with spaces + //} + if (stack <= 0) { + found = true; + break; + } + } + } + } + + state.parser.skipToken(state); + } + + if (!found) { + // parser failed to find ending tag, so it's not valid emphasis + state.pos = start; + return false; + } + + // found! + state.posMax = state.pos; + state.pos = start + 2; + + if (!silent) { + state.push({ type: 'ins_open', level: state.level++ }); + state.parser.tokenize(state); + state.push({ type: 'ins_close', level: --state.level }); + } + + state.pos = state.posMax + 2; + state.posMax = max; + return true; +}; + +},{}],36:[function(require,module,exports){ +// Process [links]( "stuff") + +'use strict'; + +var parseLinkLabel = require('../links').parseLinkLabel; +var parseLinkDestination = require('../links').parseLinkDestination; +var parseLinkTitle = require('../links').parseLinkTitle; +var normalizeReference = require('../links').normalizeReference; + + +module.exports = function links(state, silent) { + var labelStart, + labelEnd, + label, + href, + title, + pos, + ref, + code, + isImage = false, + oldPos = state.pos, + max = state.posMax, + start = state.pos, + marker = state.src.charCodeAt(start); + + if (marker === 0x21/* ! */) { + isImage = true; + marker = state.src.charCodeAt(++start); + } + + if (marker !== 0x5B/* [ */) { return false; } + if (state.level >= state.options.maxNesting) { return false; } + + labelStart = start + 1; + labelEnd = parseLinkLabel(state, start); + + // parser failed to find ']', so it's not a valid link + if (labelEnd < 0) { return false; } + + pos = labelEnd + 1; + if (pos < max && state.src.charCodeAt(pos) === 0x28/* ( */) { + // + // Inline link + // + + // [link]( "title" ) + // ^^ skipping these spaces + pos++; + for (; pos < max; pos++) { + code = state.src.charCodeAt(pos); + if (code !== 0x20 && code !== 0x0A) { break; } + } + if (pos >= max) { return false; } + + // [link]( "title" ) + // ^^^^^^ parsing link destination + start = pos; + if (parseLinkDestination(state, pos)) { + href = state.linkContent; + pos = state.pos; + } else { + href = ''; + } + + // [link]( "title" ) + // ^^ skipping these spaces + start = pos; + for (; pos < max; pos++) { + code = state.src.charCodeAt(pos); + if (code !== 0x20 && code !== 0x0A) { break; } + } + + // [link]( "title" ) + // ^^^^^^^ parsing link title + if (pos < max && start !== pos && parseLinkTitle(state, pos)) { + title = state.linkContent; + pos = state.pos; + + // [link]( "title" ) + // ^^ skipping these spaces + for (; pos < max; pos++) { + code = state.src.charCodeAt(pos); + if (code !== 0x20 && code !== 0x0A) { break; } + } + } else { + title = ''; + } + + if (pos >= max || state.src.charCodeAt(pos) !== 0x29/* ) */) { + state.pos = oldPos; + return false; + } + pos++; + } else { + // + // Link reference + // + + // do not allow nested reference links + if (state.linkLevel > 0) { return false; } + + // [foo] [bar] + // ^^ optional whitespace (can include newlines) + for (; pos < max; pos++) { + code = state.src.charCodeAt(pos); + if (code !== 0x20 && code !== 0x0A) { break; } + } + + if (pos < max && state.src.charCodeAt(pos) === 0x5B/* [ */) { + start = pos + 1; + pos = parseLinkLabel(state, pos); + if (pos >= 0) { + label = state.src.slice(start, pos++); + } else { + pos = start - 1; + } + } + + // covers label === '' and label === undefined + // (collapsed reference link and shortcut reference link respectively) + if (!label) { label = state.src.slice(labelStart, labelEnd); } + + ref = state.env.references[normalizeReference(label)]; + if (!ref) { + state.pos = oldPos; + return false; + } + href = ref.href; + title = ref.title; + } + + // + // We found the end of the link, and know for a fact it's a valid link; + // so all that's left to do is to call tokenizer. + // + if (!silent) { + state.pos = labelStart; + state.posMax = labelEnd; + + if (isImage) { + state.push({ + type: 'image', + src: href, + title: title, + alt: state.src.substr(labelStart, labelEnd - labelStart), + level: state.level + }); + } else { + state.push({ + type: 'link_open', + href: href, + title: title, + level: state.level++ + }); + state.linkLevel++; + state.parser.tokenize(state); + state.linkLevel--; + state.push({ type: 'link_close', level: --state.level }); + } + } + + state.pos = pos; + state.posMax = max; + return true; +}; + +},{"../links":11}],37:[function(require,module,exports){ +// Process ==highlighted text== + +'use strict'; + +module.exports = function del(state, silent) { + var found, + pos, + stack, + max = state.posMax, + start = state.pos, + lastChar, + nextChar; + + if (state.src.charCodeAt(start) !== 0x3D/* = */) { return false; } + if (silent) { return false; } // don't run any pairs in validation mode + if (start + 4 >= max) { return false; } + if (state.src.charCodeAt(start + 1) !== 0x3D/* = */) { return false; } + if (state.level >= state.options.maxNesting) { return false; } + + lastChar = start > 0 ? state.src.charCodeAt(start - 1) : -1; + nextChar = state.src.charCodeAt(start + 2); + + if (lastChar === 0x3D/* = */) { return false; } + if (nextChar === 0x3D/* = */) { return false; } + if (nextChar === 0x20 || nextChar === 0x0A) { return false; } + + pos = start + 2; + while (pos < max && state.src.charCodeAt(pos) === 0x3D/* = */) { pos++; } + if (pos !== start + 2) { + // sequence of 3+ markers taking as literal, same as in a emphasis + state.pos += pos - start; + if (!silent) { state.pending += state.src.slice(start, pos); } + return true; + } + + state.pos = start + 2; + stack = 1; + + while (state.pos + 1 < max) { + if (state.src.charCodeAt(state.pos) === 0x3D/* = */) { + if (state.src.charCodeAt(state.pos + 1) === 0x3D/* = */) { + lastChar = state.src.charCodeAt(state.pos - 1); + nextChar = state.pos + 2 < max ? state.src.charCodeAt(state.pos + 2) : -1; + if (nextChar !== 0x3D/* = */ && lastChar !== 0x3D/* = */) { + if (lastChar !== 0x20 && lastChar !== 0x0A) { + // closing '==' + stack--; + } else if (nextChar !== 0x20 && nextChar !== 0x0A) { + // opening '==' + stack++; + } // else { + // // standalone ' == ' indented with spaces + //} + if (stack <= 0) { + found = true; + break; + } + } + } + } + + state.parser.skipToken(state); + } + + if (!found) { + // parser failed to find ending tag, so it's not valid emphasis + state.pos = start; + return false; + } + + // found! + state.posMax = state.pos; + state.pos = start + 2; + + if (!silent) { + state.push({ type: 'mark_open', level: state.level++ }); + state.parser.tokenize(state); + state.push({ type: 'mark_close', level: --state.level }); + } + + state.pos = state.posMax + 2; + state.posMax = max; + return true; +}; + +},{}],38:[function(require,module,exports){ +// Proceess '\n' + +'use strict'; + +module.exports = function newline(state, silent) { + var pmax, max, pos = state.pos; + + if (state.src.charCodeAt(pos) !== 0x0A/* \n */) { return false; } + + pmax = state.pending.length - 1; + max = state.posMax; + + // ' \n' -> hardbreak + // Lookup in pending chars is bad practice! Don't copy to other rules! + // Pending string is stored in concat mode, indexed lookups will cause + // convertion to flat mode. + if (!silent) { + if (pmax >= 0 && state.pending.charCodeAt(pmax) === 0x20) { + if (pmax >= 1 && state.pending.charCodeAt(pmax - 1) === 0x20) { + state.pending = state.pending.replace(/ +$/, ''); + state.push({ + type: 'hardbreak', + level: state.level + }); + } else { + state.pending = state.pending.slice(0, -1); + state.push({ + type: 'softbreak', + level: state.level + }); + } + + } else { + state.push({ + type: 'softbreak', + level: state.level + }); + } + } + + pos++; + + // skip heading spaces for next line + while (pos < max && state.src.charCodeAt(pos) === 0x20) { pos++; } + + state.pos = pos; + return true; +}; + +},{}],39:[function(require,module,exports){ +// Inline parser state + +'use strict'; + + +function StateInline(src, parser, options, env) { + this.src = src; + this.env = env; + this.options = options; + this.parser = parser; + this.tokens = []; + this.pos = 0; + this.posMax = this.src.length; + this.level = 0; + this.pending = ''; + this.pendingLevel = 0; + + this.cache = []; // Stores { start: end } pairs. Useful for backtrack + // optimization of pairs parse (emphasis, strikes). + + // Link parser state vars + + this.isInLabel = false; // Set true when seek link label - we should disable + // "paired" rules (emphasis, strikes) to not skip + // tailing `]` + + this.linkLevel = 0; // Increment for each nesting link. Used to prevent + // nesting in definitions + + this.linkContent = ''; // Temporary storage for link url + + this.labelUnmatchedScopes = 0; // Track unpaired `[` for link labels + // (backtrack optimization) +} + + +// Flush pending text +// +StateInline.prototype.pushPending = function () { + this.tokens.push({ + type: 'text', + content: this.pending, + level: this.pendingLevel + }); + this.pending = ''; +}; + + +// Push new token to "stream". +// If pending text exists - flush it as text token +// +StateInline.prototype.push = function (token) { + if (this.pending) { + this.pushPending(); + } + + this.tokens.push(token); + this.pendingLevel = this.level; +}; + + +// Store value to cache. +// !!! Implementation has parser-specific optimizations +// !!! keys MUST be integer, >= 0; values MUST be integer, > 0 +// +StateInline.prototype.cacheSet = function (key, val) { + for (var i = this.cache.length; i <= key; i++) { + this.cache.push(0); + } + + this.cache[key] = val; +}; + + +// Get cache value +// +StateInline.prototype.cacheGet = function (key) { + return key < this.cache.length ? this.cache[key] : 0; +}; + + +module.exports = StateInline; + +},{}],40:[function(require,module,exports){ +// Process ~subscript~ + +'use strict'; + +// same as UNESCAPE_MD_RE plus a space +var UNESCAPE_RE = /\\([ \\!"#$%&'()*+,.\/:;<=>?@[\]^_`{|}~-])/g; + +module.exports = function sub(state, silent) { + var found, + content, + max = state.posMax, + start = state.pos; + + if (state.src.charCodeAt(start) !== 0x7E/* ~ */) { return false; } + if (silent) { return false; } // don't run any pairs in validation mode + if (start + 2 >= max) { return false; } + if (state.level >= state.options.maxNesting) { return false; } + + state.pos = start + 1; + + while (state.pos < max) { + if (state.src.charCodeAt(state.pos) === 0x7E/* ~ */) { + found = true; + break; + } + + state.parser.skipToken(state); + } + + if (!found || start + 1 === state.pos) { + state.pos = start; + return false; + } + + content = state.src.slice(start + 1, state.pos); + + // don't allow unescaped spaces/newlines inside + if (content.match(/(^|[^\\])(\\\\)*\s/)) { + state.pos = start; + return false; + } + + // found! + state.posMax = state.pos; + state.pos = start + 1; + + if (!silent) { + state.push({ + type: 'sub', + level: state.level, + content: content.replace(UNESCAPE_RE, '$1') + }); + } + + state.pos = state.posMax + 1; + state.posMax = max; + return true; +}; + +},{}],41:[function(require,module,exports){ +// Process ^superscript^ + +'use strict'; + +// same as UNESCAPE_MD_RE plus a space +var UNESCAPE_RE = /\\([ \\!"#$%&'()*+,.\/:;<=>?@[\]^_`{|}~-])/g; + +module.exports = function sup(state, silent) { + var found, + content, + max = state.posMax, + start = state.pos; + + if (state.src.charCodeAt(start) !== 0x5E/* ^ */) { return false; } + if (silent) { return false; } // don't run any pairs in validation mode + if (start + 2 >= max) { return false; } + if (state.level >= state.options.maxNesting) { return false; } + + state.pos = start + 1; + + while (state.pos < max) { + if (state.src.charCodeAt(state.pos) === 0x5E/* ^ */) { + found = true; + break; + } + + state.parser.skipToken(state); + } + + if (!found || start + 1 === state.pos) { + state.pos = start; + return false; + } + + content = state.src.slice(start + 1, state.pos); + + // don't allow unescaped spaces/newlines inside + if (content.match(/(^|[^\\])(\\\\)*\s/)) { + state.pos = start; + return false; + } + + // found! + state.posMax = state.pos; + state.pos = start + 1; + + if (!silent) { + state.push({ + type: 'sup', + level: state.level, + content: content.replace(UNESCAPE_RE, '$1') + }); + } + + state.pos = state.posMax + 1; + state.posMax = max; + return true; +}; + +},{}],42:[function(require,module,exports){ +// Skip text characters for text token, place those to pending buffer +// and increment current pos + +'use strict'; + +module.exports = function text(state, silent) { + var str = state.src.slice(state.pos), + next = str.search(state.parser.textMatch); + + if (next === 0) { return false; } + + if (next < 0) { next = str.length; } + + if (!silent) { state.pending += str.slice(0, next); } + state.pos += next; + + return true; +}; + +},{}],43:[function(require,module,exports){ +// Replace link-like texts with link nodes. +// +// Currently restricted to http/https/ftp +// +'use strict'; + + +var Autolinker = require('autolinker'); + + +var LINK_SCAN_RE = /www|\:\/\//; + +var links = []; +var autolinker = new Autolinker({ + stripPrefix: false, + replaceFn: function (autolinker, match) { + // Only collect matched strings but don't change anything. + if (match.getType() === 'url') { + links.push({ text: match.matchedText, url: match.getUrl() }); + } + return false; + } +}); + +function isLinkOpen(str) { + return /^\s]/i.test(str); +} +function isLinkClose(str) { + return /^<\/a\s*>/i.test(str); +} + + +module.exports = function linkify(t, state) { + var i, token, text, nodes, ln, pos, level, + htmlLinkLevel = 0, + tokens = state.tokens; + + // We scan from the end, to keep position when new tags added. + // Use reversed logic in links start/end match + for (i = tokens.length - 1; i >= 0; i--) { + token = tokens[i]; + + // Skip content of markdown links + if (token.type === 'link_close') { + i--; + while (tokens[i].level !== token.level && tokens[i].type !== 'link_open') { + i--; + } + continue; + } + + // Skip content of html tag links + if (token.type === 'htmltag') { + if (isLinkOpen(token.content) && htmlLinkLevel > 0) { + htmlLinkLevel--; + } + if (isLinkClose(token.content)) { + htmlLinkLevel++; + } + } + if (htmlLinkLevel > 0) { continue; } + + if (token.type === 'text' && LINK_SCAN_RE.test(token.content)) { + + text = token.content; + links.length = 0; + autolinker.link(text); + + if (!links.length) { continue; } + + // Now split string to nodes + nodes = []; + level = token.level; + + for (ln = 0; ln < links.length; ln++) { + + if (!state.parser.validateLink(links[ln].url)) { continue; } + + pos = text.indexOf(links[ln].text); + + if (pos === -1) { continue; } + + if (pos) { + level = level; + nodes.push({ + type: 'text', + content: text.slice(0, pos), + level: level + }); + } + nodes.push({ + type: 'link_open', + href: links[ln].url, + title: '', + level: level++ + }); + nodes.push({ + type: 'text', + content: links[ln].text, + level: level + }); + nodes.push({ + type: 'link_close', + level: --level + }); + text = text.slice(pos + links[ln].text.length); + } + if (text.length) { + nodes.push({ + type: 'text', + content: text, + level: level + }); + } + + // replace cuttent node + state.tokens = tokens = [].concat(tokens.slice(0, i), nodes, tokens.slice(i + 1)); + } + } +}; + +},{"autolinker":47}],44:[function(require,module,exports){ +// Simple typographyc replacements +// +'use strict'; + + +var COPY_RE = /\((c|tm|r|p)\)/i; +var RARE_RE = /\+-|\.\.|\?\?\?\?|!!!!|,,|--/; + +module.exports = function replace(t, state) { + var i, token, text, + tokens = state.tokens, + options = t.options; + + for (i = tokens.length - 1; i >= 0; i--) { + token = tokens[i]; + if (token.type === 'text') { + text = token.content; + + if (COPY_RE.test(text)) { + if (options.copyright) { + text = text.replace(/\(c\)/gi, '©'); + } + if (options.trademark) { + text = text.replace(/\(tm\)/gi, '™'); + } + if (options.registered) { + text = text.replace(/\(r\)/gi, '®'); + } + if (options.paragraph) { + text = text.replace(/\(p\)/gi, '§'); + } + } + + if (RARE_RE.test(text)) { + if (options.plusminus) { + text = text.replace(/\+-/g, '±'); + } + if (options.ellipsis) { + // .., ..., ....... -> … + // but ?..... & !..... -> ?.. & !.. + text = text.replace(/\.{2,}/g, '…').replace(/([?!])…/g, '$1..'); + } + if (options.dupes) { + text = text.replace(/([?!]){4,}/g, '$1$1$1').replace(/,{2,}/g, ','); + } + if (options.dashes) { + text = text + // em-dash + .replace(/(^|[^-])---([^-]|$)/mg, '$1\u2014$2') + // en-dash + .replace(/(^|\s)--(\s|$)/mg, '$1\u2013$2') + .replace(/(^|[^-\s])--([^-\s]|$)/mg, '$1\u2013$2'); + } + } + + token.content = text; + } + } +}; + +},{}],45:[function(require,module,exports){ +// Convert straight quotation marks to typographic ones +// +'use strict'; + + +var QUOTE_TEST_RE = /['"]/; +var QUOTE_RE = /['"]/g; +var PUNCT_RE = /[-\s()\[\]]/; +var APOSTROPHE = '’'; + +// This function returns true if the character at `pos` +// could be inside a word. +function isLetter(str, pos) { + if (pos < 0 || pos >= str.length) { return false; } + return !PUNCT_RE.test(str[pos]); +} + + +function replaceAt(str, index, ch) { + return str.substr(0, index) + ch + str.substr(index + 1); +} + +var stack = []; + +module.exports = function smartquotes(typographer, state) { + /*eslint max-depth:0*/ + var i, token, text, t, pos, max, thisLevel, lastSpace, nextSpace, item, canOpen, canClose, j, isSingle, chars, + options = typographer.options, + tokens = state.tokens; + + stack.length = 0; + + for (i = 0; i < tokens.length; i++) { + token = tokens[i]; + + if (token.type !== 'text' || QUOTE_TEST_RE.test(token.text)) { continue; } + + thisLevel = tokens[i].level; + + for (j = stack.length - 1; j >= 0; j--) { + if (stack[j].level <= thisLevel) { break; } + } + stack.length = j + 1; + + text = token.content; + pos = 0; + max = text.length; + + /*eslint no-labels:0,block-scoped-var:0*/ + OUTER: + while (pos < max) { + QUOTE_RE.lastIndex = pos; + t = QUOTE_RE.exec(text); + if (!t) { break; } + + lastSpace = !isLetter(text, t.index - 1); + pos = t.index + 1; + isSingle = (t[0] === "'"); + nextSpace = !isLetter(text, pos); + + if (!nextSpace && !lastSpace) { + // middle of word + if (isSingle) { + token.content = replaceAt(token.content, t.index, APOSTROPHE); + } + continue; + } + + canOpen = !nextSpace; + canClose = !lastSpace; + + if (canClose) { + // this could be a closing quote, rewind the stack to get a match + for (j = stack.length - 1; j >= 0; j--) { + item = stack[j]; + if (stack[j].level < thisLevel) { break; } + if (item.single === isSingle && stack[j].level === thisLevel) { + item = stack[j]; + chars = isSingle ? options.singleQuotes : options.doubleQuotes; + if (chars) { + tokens[item.token].content = replaceAt(tokens[item.token].content, item.pos, chars[0]); + token.content = replaceAt(token.content, t.index, chars[1]); + } + stack.length = j; + continue OUTER; + } + } + } + + if (canOpen) { + stack.push({ + token: i, + pos: t.index, + single: isSingle, + level: thisLevel + }); + } else if (canClose && isSingle) { + token.content = replaceAt(token.content, t.index, APOSTROPHE); + } + } + } +}; + +},{}],46:[function(require,module,exports){ +// Class of typographic replacement rules +// +'use strict'; + +// TODO: +// - fractionals 1/2, 1/4, 3/4 -> ½, ¼, ¾ +// - miltiplication 2 x 4 -> 2 × 4 + + +var assign = require('./common/utils').assign; +var Ruler = require('./ruler'); + + +var _rules = [ + [ 'replace', require('./rules_text/replace') ], + [ 'smartquotes', require('./rules_text/smartquotes') ] +]; + + +function Typographer() { + this._rules = []; + + this.options = {}; + + this.ruler = new Ruler(this.rulesUpdate.bind(this)); + + for (var i = 0; i < _rules.length; i++) { + this.ruler.push(_rules[i][0], _rules[i][1]); + } +} + + +Typographer.prototype.rulesUpdate = function () { + this._rules = this.ruler.getRules(); +}; + + +Typographer.prototype.set = function (options) { + assign(this.options, options); +}; + + +Typographer.prototype.process = function (state) { + var i, l, rules; + + rules = this._rules; + + for (i = 0, l = rules.length; i < l; i++) { + rules[i](this, state); + } +}; + + +module.exports = Typographer; + +},{"./common/utils":5,"./ruler":16,"./rules_text/replace":44,"./rules_text/smartquotes":45}],47:[function(require,module,exports){ +/*! + * Autolinker.js + * 0.12.2 + * + * Copyright(c) 2014 Gregory Jacobs + * MIT Licensed. http://www.opensource.org/licenses/mit-license.php + * + * https://github.com/gregjacobs/Autolinker.js + */ +/*global define, module */ +( function( root, factory ) { + + if( typeof define === 'function' && define.amd ) { + define( factory ); // Define as AMD module if an AMD loader is present (ex: RequireJS). + } else if( typeof exports !== 'undefined' ) { + module.exports = factory(); // Define as CommonJS module for Node.js, if available. + } else { + root.Autolinker = factory(); // Finally, define as a browser global if no module loader. + } +}( this, function() { + + /** + * @class Autolinker + * @extends Object + * + * Utility class used to process a given string of text, and wrap the URLs, email addresses, and Twitter handles in + * the appropriate anchor (<a>) tags to turn them into links. + * + * Any of the configuration options may be provided in an Object (map) provided to the Autolinker constructor, which + * will configure how the {@link #link link()} method will process the links. + * + * For example: + * + * var autolinker = new Autolinker( { + * newWindow : false, + * truncate : 30 + * } ); + * + * var html = autolinker.link( "Joe went to www.yahoo.com" ); + * // produces: 'Joe went to yahoo.com' + * + * + * The {@link #static-link static link()} method may also be used to inline options into a single call, which may + * be more convenient for one-off uses. For example: + * + * var html = Autolinker.link( "Joe went to www.yahoo.com", { + * newWindow : false, + * truncate : 30 + * } ); + * // produces: 'Joe went to yahoo.com' + * + * + * ## Custom Replacements of Links + * + * If the configuration options do not provide enough flexibility, a {@link #replaceFn} may be provided to fully customize + * the output of Autolinker. This function is called once for each URL/Email/Twitter handle match that is encountered. + * + * For example: + * + * var input = "..."; // string with URLs, Email Addresses, and Twitter Handles + * + * var linkedText = Autolinker.link( input, { + * replaceFn : function( autolinker, match ) { + * console.log( "href = ", match.getAnchorHref() ); + * console.log( "text = ", match.getAnchorText() ); + * + * switch( match.getType() ) { + * case 'url' : + * console.log( "url: ", match.getUrl() ); + * + * if( match.getUrl().indexOf( 'mysite.com' ) === -1 ) { + * var tag = autolinker.getTagBuilder().build( match ); // returns an `Autolinker.HtmlTag` instance, which provides mutator methods for easy changes + * tag.setAttr( 'rel', 'nofollow' ); + * tag.addClass( 'external-link' ); + * + * return tag; + * + * } else { + * return true; // let Autolinker perform its normal anchor tag replacement + * } + * + * case 'email' : + * var email = match.getEmail(); + * console.log( "email: ", email ); + * + * if( email === "my@own.address" ) { + * return false; // don't auto-link this particular email address; leave as-is + * } else { + * return; // no return value will have Autolinker perform its normal anchor tag replacement (same as returning `true`) + * } + * + * case 'twitter' : + * var twitterHandle = match.getTwitterHandle(); + * console.log( twitterHandle ); + * + * return '' + twitterHandle + ''; + * } + * } + * } ); + * + * + * The function may return the following values: + * + * - `true` (Boolean): Allow Autolinker to replace the match as it normally would. + * - `false` (Boolean): Do not replace the current match at all - leave as-is. + * - Any String: If a string is returned from the function, the string will be used directly as the replacement HTML for + * the match. + * - An {@link Autolinker.HtmlTag} instance, which can be used to build/modify an HTML tag before writing out its HTML text. + * + * @constructor + * @param {Object} [config] The configuration options for the Autolinker instance, specified in an Object (map). + */ + var Autolinker = function( cfg ) { + Autolinker.Util.assign( this, cfg ); // assign the properties of `cfg` onto the Autolinker instance. Prototype properties will be used for missing configs. + }; + + + Autolinker.prototype = { + constructor : Autolinker, // fix constructor property + + /** + * @cfg {Boolean} urls + * + * `true` if miscellaneous URLs should be automatically linked, `false` if they should not be. + */ + urls : true, + + /** + * @cfg {Boolean} email + * + * `true` if email addresses should be automatically linked, `false` if they should not be. + */ + email : true, + + /** + * @cfg {Boolean} twitter + * + * `true` if Twitter handles ("@example") should be automatically linked, `false` if they should not be. + */ + twitter : true, + + /** + * @cfg {Boolean} newWindow + * + * `true` if the links should open in a new window, `false` otherwise. + */ + newWindow : true, + + /** + * @cfg {Boolean} stripPrefix + * + * `true` if 'http://' or 'https://' and/or the 'www.' should be stripped from the beginning of URL links' text, + * `false` otherwise. + */ + stripPrefix : true, + + /** + * @cfg {Number} truncate + * + * A number for how many characters long URLs/emails/twitter handles should be truncated to inside the text of + * a link. If the URL/email/twitter is over this number of characters, it will be truncated to this length by + * adding a two period ellipsis ('..') to the end of the string. + * + * For example: A url like 'http://www.yahoo.com/some/long/path/to/a/file' truncated to 25 characters might look + * something like this: 'yahoo.com/some/long/pat..' + */ + + /** + * @cfg {String} className + * + * A CSS class name to add to the generated links. This class will be added to all links, as well as this class + * plus url/email/twitter suffixes for styling url/email/twitter links differently. + * + * For example, if this config is provided as "myLink", then: + * + * - URL links will have the CSS classes: "myLink myLink-url" + * - Email links will have the CSS classes: "myLink myLink-email", and + * - Twitter links will have the CSS classes: "myLink myLink-twitter" + */ + className : "", + + /** + * @cfg {Function} replaceFn + * + * A function to individually process each URL/Email/Twitter match found in the input string. + * + * See the class's description for usage. + * + * This function is called with the following parameters: + * + * @cfg {Autolinker} replaceFn.autolinker The Autolinker instance, which may be used to retrieve child objects from (such + * as the instance's {@link #getTagBuilder tag builder}). + * @cfg {Autolinker.match.Match} replaceFn.match The Match instance which can be used to retrieve information about the + * {@link Autolinker.match.Url URL}/{@link Autolinker.match.Email email}/{@link Autolinker.match.Twitter Twitter} + * match that the `replaceFn` is currently processing. + */ + + + /** + * @private + * @property {RegExp} htmlCharacterEntitiesRegex + * + * The regular expression that matches common HTML character entities. + * + * Ignoring & as it could be part of a query string -- handling it separately. + */ + htmlCharacterEntitiesRegex: /( | |<|<|>|>)/gi, + + /** + * @private + * @property {RegExp} matcherRegex + * + * The regular expression that matches URLs, email addresses, and Twitter handles. + * + * This regular expression has the following capturing groups: + * + * 1. Group that is used to determine if there is a Twitter handle match (i.e. \@someTwitterUser). Simply check for its + * existence to determine if there is a Twitter handle match. The next couple of capturing groups give information + * about the Twitter handle match. + * 2. The whitespace character before the \@sign in a Twitter handle. This is needed because there are no lookbehinds in + * JS regular expressions, and can be used to reconstruct the original string in a replace(). + * 3. The Twitter handle itself in a Twitter match. If the match is '@someTwitterUser', the handle is 'someTwitterUser'. + * 4. Group that matches an email address. Used to determine if the match is an email address, as well as holding the full + * address. Ex: 'me@my.com' + * 5. Group that matches a URL in the input text. Ex: 'http://google.com', 'www.google.com', or just 'google.com'. + * This also includes a path, url parameters, or hash anchors. Ex: google.com/path/to/file?q1=1&q2=2#myAnchor + * 6. A protocol-relative ('//') match for the case of a 'www.' prefixed URL. Will be an empty string if it is not a + * protocol-relative match. We need to know the character before the '//' in order to determine if it is a valid match + * or the // was in a string we don't want to auto-link. + * 7. A protocol-relative ('//') match for the case of a known TLD prefixed URL. Will be an empty string if it is not a + * protocol-relative match. See #6 for more info. + */ + matcherRegex : (function() { + var twitterRegex = /(^|[^\w])@(\w{1,15})/, // For matching a twitter handle. Ex: @gregory_jacobs + + emailRegex = /(?:[\-;:&=\+\$,\w\.]+@)/, // something@ for email addresses (a.k.a. local-part) + + protocolRegex = /(?:[A-Za-z]{3,9}:(?:\/\/)?)/, // match protocol, allow in format http:// or mailto: + wwwRegex = /(?:www\.)/, // starting with 'www.' + domainNameRegex = /[A-Za-z0-9\.\-]*[A-Za-z0-9\-]/, // anything looking at all like a domain, non-unicode domains, not ending in a period + tldRegex = /\.(?:international|construction|contractors|enterprises|photography|productions|foundation|immobilien|industries|management|properties|technology|christmas|community|directory|education|equipment|institute|marketing|solutions|vacations|bargains|boutique|builders|catering|cleaning|clothing|computer|democrat|diamonds|graphics|holdings|lighting|partners|plumbing|supplies|training|ventures|academy|careers|company|cruises|domains|exposed|flights|florist|gallery|guitars|holiday|kitchen|neustar|okinawa|recipes|rentals|reviews|shiksha|singles|support|systems|agency|berlin|camera|center|coffee|condos|dating|estate|events|expert|futbol|kaufen|luxury|maison|monash|museum|nagoya|photos|repair|report|social|supply|tattoo|tienda|travel|viajes|villas|vision|voting|voyage|actor|build|cards|cheap|codes|dance|email|glass|house|mango|ninja|parts|photo|shoes|solar|today|tokyo|tools|watch|works|aero|arpa|asia|best|bike|blue|buzz|camp|club|cool|coop|farm|fish|gift|guru|info|jobs|kiwi|kred|land|limo|link|menu|mobi|moda|name|pics|pink|post|qpon|rich|ruhr|sexy|tips|vote|voto|wang|wien|wiki|zone|bar|bid|biz|cab|cat|ceo|com|edu|gov|int|kim|mil|net|onl|org|pro|pub|red|tel|uno|wed|xxx|xyz|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|cu|cv|cw|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|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|st|su|sv|sx|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)\b/, // match our known top level domains (TLDs) + + // Allow optional path, query string, and hash anchor, not ending in the following characters: "!:,.;" + // http://blog.codinghorror.com/the-problem-with-urls/ + urlSuffixRegex = /(?:[\-A-Za-z0-9+&@#\/%?=~_()|!:,.;]*[\-A-Za-z0-9+&@#\/%=~_()|])?/; // note: optional part of the full regex + + return new RegExp( [ + '(', // *** Capturing group $1, which can be used to check for a twitter handle match. Use group $3 for the actual twitter handle though. $2 may be used to reconstruct the original string in a replace() + // *** Capturing group $2, which matches the whitespace character before the '@' sign (needed because of no lookbehinds), and + // *** Capturing group $3, which matches the actual twitter handle + twitterRegex.source, + ')', + + '|', + + '(', // *** Capturing group $4, which is used to determine an email match + emailRegex.source, + domainNameRegex.source, + tldRegex.source, + ')', + + '|', + + '(', // *** Capturing group $5, which is used to match a URL + '(?:', // parens to cover match for protocol (optional), and domain + '(?:', // non-capturing paren for a protocol-prefixed url (ex: http://google.com) + protocolRegex.source, + domainNameRegex.source, + ')', + + '|', + + '(?:', // non-capturing paren for a 'www.' prefixed url (ex: www.google.com) + '(.?//)?', // *** Capturing group $6 for an optional protocol-relative URL. Must be at the beginning of the string or start with a non-word character + wwwRegex.source, + domainNameRegex.source, + ')', + + '|', + + '(?:', // non-capturing paren for known a TLD url (ex: google.com) + '(.?//)?', // *** Capturing group $7 for an optional protocol-relative URL. Must be at the beginning of the string or start with a non-word character + domainNameRegex.source, + tldRegex.source, + ')', + ')', + + urlSuffixRegex.source, // match for path, query string, and/or hash anchor + ')' + ].join( "" ), 'gi' ); + } )(), + + /** + * @private + * @property {RegExp} invalidProtocolRelMatchRegex + * + * The regular expression used to check a potential protocol-relative URL match, coming from the {@link #matcherRegex}. + * A protocol-relative URL is, for example, "//yahoo.com" + * + * This regular expression is used in conjunction with the {@link #matcherRegex}, and checks to see if there is a word character + * before the '//' in order to determine if we should actually autolink a protocol-relative URL. This is needed because there + * is no negative look-behind in JavaScript regular expressions. + * + * For instance, we want to autolink something like "//google.com", but we don't want to autolink something + * like "abc//google.com" + */ + invalidProtocolRelMatchRegex : /^[\w]\/\//, + + /** + * @private + * @property {RegExp} charBeforeProtocolRelMatchRegex + * + * The regular expression used to retrieve the character before a protocol-relative URL match. + * + * This is used in conjunction with the {@link #matcherRegex}, which needs to grab the character before a protocol-relative + * '//' due to the lack of a negative look-behind in JavaScript regular expressions. The character before the match is stripped + * from the URL. + */ + charBeforeProtocolRelMatchRegex : /^(.)?\/\//, + + /** + * @private + * @property {Autolinker.HtmlParser} htmlParser + * + * The HtmlParser instance used to skip over HTML tags, while finding text nodes to process. This is lazily instantiated + * in the {@link #getHtmlParser} method. + */ + + /** + * @private + * @property {Autolinker.AnchorTagBuilder} tagBuilder + * + * The AnchorTagBuilder instance used to build the URL/email/Twitter replacement anchor tags. This is lazily instantiated + * in the {@link #getTagBuilder} method. + */ + + + /** + * Automatically links URLs, email addresses, and Twitter handles found in the given chunk of HTML. + * Does not link URLs found within HTML tags. + * + * For instance, if given the text: `You should go to http://www.yahoo.com`, then the result + * will be `You should go to <a href="http://www.yahoo.com">http://www.yahoo.com</a>` + * + * This method finds the text around any HTML elements in the input `textOrHtml`, which will be the text that is processed. + * Any original HTML elements will be left as-is, as well as the text that is already wrapped in anchor (<a>) tags. + * + * @param {String} textOrHtml The HTML or text to link URLs, email addresses, and Twitter handles within. + * @return {String} The HTML, with URLs/emails/Twitter handles automatically linked. + */ + link : function( textOrHtml ) { + var me = this, // for closure + htmlParser = this.getHtmlParser(), + htmlCharacterEntitiesRegex = this.htmlCharacterEntitiesRegex, + anchorTagStackCount = 0, // used to only process text around anchor tags, and any inner text/html they may have + resultHtml = []; + + htmlParser.parse( textOrHtml, { + // Process HTML nodes in the input `textOrHtml` + processHtmlNode : function( tagText, tagName, isClosingTag ) { + if( tagName === 'a' ) { + if( !isClosingTag ) { // it's the start tag + anchorTagStackCount++; + } else { // it's the end tag + anchorTagStackCount = Math.max( anchorTagStackCount - 1, 0 ); // attempt to handle extraneous tags by making sure the stack count never goes below 0 + } + } + resultHtml.push( tagText ); // now add the text of the tag itself verbatim + }, + + // Process text nodes in the input `textOrHtml` + processTextNode : function( text ) { + if( anchorTagStackCount === 0 ) { + // If we're not within an tag, process the text node + var unescapedText = Autolinker.Util.splitAndCapture( text, htmlCharacterEntitiesRegex ); // split at HTML entities, but include the HTML entities in the results array + + for ( var i = 0, len = unescapedText.length; i < len; i++ ) { + var textToProcess = unescapedText[ i ], + processedTextNode = me.processTextNode( textToProcess ); + + resultHtml.push( processedTextNode ); + } + + } else { + // `text` is within an tag, simply append the text - we do not want to autolink anything + // already within an ... tag + resultHtml.push( text ); + } + } + } ); + + return resultHtml.join( "" ); + }, + + + /** + * Lazily instantiates and returns the {@link #htmlParser} instance for this Autolinker instance. + * + * @protected + * @return {Autolinker.HtmlParser} + */ + getHtmlParser : function() { + var htmlParser = this.htmlParser; + + if( !htmlParser ) { + htmlParser = this.htmlParser = new Autolinker.HtmlParser(); + } + + return htmlParser; + }, + + + /** + * Returns the {@link #tagBuilder} instance for this Autolinker instance, lazily instantiating it + * if it does not yet exist. + * + * This method may be used in a {@link #replaceFn} to generate the {@link Autolinker.HtmlTag HtmlTag} instance that + * Autolinker would normally generate, and then allow for modifications before returning it. For example: + * + * var html = Autolinker.link( "Test google.com", { + * replaceFn : function( autolinker, match ) { + * var tag = autolinker.getTagBuilder().build( match ); // returns an {@link Autolinker.HtmlTag} instance + * tag.setAttr( 'rel', 'nofollow' ); + * + * return tag; + * } + * } ); + * + * // generated html: + * // Test google.com + * + * @return {Autolinker.AnchorTagBuilder} + */ + getTagBuilder : function() { + var tagBuilder = this.tagBuilder; + + if( !tagBuilder ) { + tagBuilder = this.tagBuilder = new Autolinker.AnchorTagBuilder( { + newWindow : this.newWindow, + truncate : this.truncate, + className : this.className + } ); + } + + return tagBuilder; + }, + + + /** + * Process the text that lies inbetween HTML tags. This method does the actual wrapping of URLs with + * anchor tags. + * + * @private + * @param {String} text The text to auto-link. + * @return {String} The text with anchor tags auto-filled. + */ + processTextNode : function( text ) { + var me = this, // for closure + charBeforeProtocolRelMatchRegex = this.charBeforeProtocolRelMatchRegex; + + return text.replace( this.matcherRegex, function( matchStr, $1, $2, $3, $4, $5, $6, $7 ) { + var twitterMatch = $1, + twitterHandlePrefixWhitespaceChar = $2, // The whitespace char before the @ sign in a Twitter handle match. This is needed because of no lookbehinds in JS regexes. + twitterHandle = $3, // The actual twitterUser (i.e the word after the @ sign in a Twitter handle match) + emailAddressMatch = $4, // For both determining if it is an email address, and stores the actual email address + urlMatch = $5, // The matched URL string + protocolRelativeMatch = $6 || $7, // The '//' for a protocol-relative match, with the character that comes before the '//' + + prefixStr = "", // A string to use to prefix the anchor tag that is created. This is needed for the Twitter handle match + suffixStr = "", // A string to suffix the anchor tag that is created. This is used if there is a trailing parenthesis that should not be auto-linked. + + match; // Will be an Autolinker.match.Match object + + + // Return out with no changes for match types that are disabled (url, email, twitter), or for matches that are + // invalid (false positives from the matcherRegex, which can't use look-behinds since they are unavailable in JS). + if( !me.isValidMatch( twitterMatch, emailAddressMatch, urlMatch, protocolRelativeMatch ) ) { + return matchStr; + } + + // Handle a closing parenthesis at the end of the match, and exclude it if there is not a matching open parenthesis + // in the match itself. + if( me.matchHasUnbalancedClosingParen( matchStr ) ) { + matchStr = matchStr.substr( 0, matchStr.length - 1 ); // remove the trailing ")" + suffixStr = ")"; // this will be added after the generated tag + } + + + if( emailAddressMatch ) { + match = new Autolinker.match.Email( { matchedText: matchStr, email: emailAddressMatch } ); + + } else if( twitterMatch ) { + // fix up the `matchStr` if there was a preceding whitespace char, which was needed to determine the match + // itself (since there are no look-behinds in JS regexes) + if( twitterHandlePrefixWhitespaceChar ) { + prefixStr = twitterHandlePrefixWhitespaceChar; + matchStr = matchStr.slice( 1 ); // remove the prefixed whitespace char from the match + } + match = new Autolinker.match.Twitter( { matchedText: matchStr, twitterHandle: twitterHandle } ); + + } else { // url match + // If it's a protocol-relative '//' match, remove the character before the '//' (which the matcherRegex needed + // to match due to the lack of a negative look-behind in JavaScript regular expressions) + if( protocolRelativeMatch ) { + var charBeforeMatch = protocolRelativeMatch.match( charBeforeProtocolRelMatchRegex )[ 1 ] || ""; + + if( charBeforeMatch ) { // fix up the `matchStr` if there was a preceding char before a protocol-relative match, which was needed to determine the match itself (since there are no look-behinds in JS regexes) + prefixStr = charBeforeMatch; + matchStr = matchStr.slice( 1 ); // remove the prefixed char from the match + } + } + + match = new Autolinker.match.Url( { + matchedText : matchStr, + url : matchStr, + protocolRelativeMatch : protocolRelativeMatch, + stripPrefix : me.stripPrefix + } ); + } + + // Generate the replacement text for the match + var matchReturnVal = me.createMatchReturnVal( match, matchStr ); + return prefixStr + matchReturnVal + suffixStr; + } ); + }, + + + /** + * Determines if a given match found by {@link #processTextNode} is valid. Will return `false` for: + * + * 1) Disabled link types (i.e. having a Twitter match, but {@link #twitter} matching is disabled) + * 2) URL matches which do not have at least have one period ('.') in the domain name (effectively skipping over + * matches like "abc:def") + * 3) A protocol-relative url match (a URL beginning with '//') whose previous character is a word character + * (effectively skipping over strings like "abc//google.com") + * + * Otherwise, returns `true`. + * + * @private + * @param {String} twitterMatch The matched Twitter handle, if there was one. Will be empty string if the match is not a + * Twitter match. + * @param {String} emailAddressMatch The matched Email address, if there was one. Will be empty string if the match is not + * an Email address match. + * @param {String} urlMatch The matched URL, if there was one. Will be an empty string if the match is not a URL match. + * @param {String} protocolRelativeMatch The protocol-relative string for a URL match (i.e. '//'), possibly with a preceding + * character (ex, a space, such as: ' //', or a letter, such as: 'a//'). The match is invalid if there is a word character + * preceding the '//'. + * @return {Boolean} `true` if the match given is valid and should be processed, or `false` if the match is invalid and/or + * should just not be processed (such as, if it's a Twitter match, but {@link #twitter} matching is disabled}. + */ + isValidMatch : function( twitterMatch, emailAddressMatch, urlMatch, protocolRelativeMatch ) { + if( + ( twitterMatch && !this.twitter ) || ( emailAddressMatch && !this.email ) || ( urlMatch && !this.urls ) || + ( urlMatch && urlMatch.indexOf( '.' ) === -1 ) || // At least one period ('.') must exist in the URL match for us to consider it an actual URL + ( urlMatch && /^[A-Za-z]{3,9}:/.test( urlMatch ) && !/:.*?[A-Za-z]/.test( urlMatch ) ) || // At least one letter character must exist in the domain name after a protocol match. Ex: skip over something like "git:1.0" + ( protocolRelativeMatch && this.invalidProtocolRelMatchRegex.test( protocolRelativeMatch ) ) // a protocol-relative match which has a word character in front of it (so we can skip something like "abc//google.com") + ) { + return false; + } + + return true; + }, + + + /** + * Determines if a match found has an unmatched closing parenthesis. If so, this parenthesis will be removed + * from the match itself, and appended after the generated anchor tag in {@link #processTextNode}. + * + * A match may have an extra closing parenthesis at the end of the match because the regular expression must include parenthesis + * for URLs such as "wikipedia.com/something_(disambiguation)", which should be auto-linked. + * + * However, an extra parenthesis *will* be included when the URL itself is wrapped in parenthesis, such as in the case of + * "(wikipedia.com/something_(disambiguation))". In this case, the last closing parenthesis should *not* be part of the URL + * itself, and this method will return `true`. + * + * @private + * @param {String} matchStr The full match string from the {@link #matcherRegex}. + * @return {Boolean} `true` if there is an unbalanced closing parenthesis at the end of the `matchStr`, `false` otherwise. + */ + matchHasUnbalancedClosingParen : function( matchStr ) { + var lastChar = matchStr.charAt( matchStr.length - 1 ); + + if( lastChar === ')' ) { + var openParensMatch = matchStr.match( /\(/g ), + closeParensMatch = matchStr.match( /\)/g ), + numOpenParens = ( openParensMatch && openParensMatch.length ) || 0, + numCloseParens = ( closeParensMatch && closeParensMatch.length ) || 0; + + if( numOpenParens < numCloseParens ) { + return true; + } + } + + return false; + }, + + + /** + * Creates the return string value for a given match in the input string, for the {@link #processTextNode} method. + * + * This method handles the {@link #replaceFn}, if one was provided. + * + * @private + * @param {Autolinker.match.Match} match The Match object that represents the match. + * @param {String} matchStr The original match string, after having been preprocessed to fix match edge cases (see + * the `prefixStr` and `suffixStr` vars in {@link #processTextNode}. + * @return {String} The string that the `match` should be replaced with. This is usually the anchor tag string, but + * may be the `matchStr` itself if the match is not to be replaced. + */ + createMatchReturnVal : function( match, matchStr ) { + // Handle a custom `replaceFn` being provided + var replaceFnResult; + if( this.replaceFn ) { + replaceFnResult = this.replaceFn.call( this, this, match ); // Autolinker instance is the context, and the first arg + } + + if( typeof replaceFnResult === 'string' ) { + return replaceFnResult; // `replaceFn` returned a string, use that + + } else if( replaceFnResult === false ) { + return matchStr; // no replacement for the match + + } else if( replaceFnResult instanceof Autolinker.HtmlTag ) { + return replaceFnResult.toString(); + + } else { // replaceFnResult === true, or no/unknown return value from function + // Perform Autolinker's default anchor tag generation + var tagBuilder = this.getTagBuilder(), + anchorTag = tagBuilder.build( match ); // returns an Autolinker.HtmlTag instance + + return anchorTag.toString(); + } + } + + }; + + + /** + * Automatically links URLs, email addresses, and Twitter handles found in the given chunk of HTML. + * Does not link URLs found within HTML tags. + * + * For instance, if given the text: `You should go to http://www.yahoo.com`, then the result + * will be `You should go to <a href="http://www.yahoo.com">http://www.yahoo.com</a>` + * + * Example: + * + * var linkedText = Autolinker.link( "Go to google.com", { newWindow: false } ); + * // Produces: "Go to google.com" + * + * @static + * @method link + * @param {String} html The HTML text to link URLs within. + * @param {Object} [options] Any of the configuration options for the Autolinker class, specified in an Object (map). + * See the class description for an example call. + * @return {String} The HTML text, with URLs automatically linked + */ + Autolinker.link = function( text, options ) { + var autolinker = new Autolinker( options ); + return autolinker.link( text ); + }; + + + // Namespace for `match` classes + Autolinker.match = {}; + /*global Autolinker */ + /*jshint eqnull:true, boss:true */ + /** + * @class Autolinker.Util + * @singleton + * + * A few utility methods for Autolinker. + */ + Autolinker.Util = { + + /** + * @property {Function} abstractMethod + * + * A function object which represents an abstract method. + */ + abstractMethod : function() { throw "abstract"; }, + + + /** + * Assigns (shallow copies) the properties of `src` onto `dest`. + * + * @param {Object} dest The destination object. + * @param {Object} src The source object. + * @return {Object} The destination object. + */ + assign : function( dest, src ) { + for( var prop in src ) { + if( src.hasOwnProperty( prop ) ) { + dest[ prop ] = src[ prop ]; + } + } + + return dest; + }, + + + /** + * Extends `superclass` to create a new subclass, adding the `protoProps` to the new subclass's prototype. + * + * @param {Function} superclass The constructor function for the superclass. + * @param {Object} protoProps The methods/properties to add to the subclass's prototype. This may contain the + * special property `constructor`, which will be used as the new subclass's constructor function. + * @return {Function} The new subclass function. + */ + extend : function( superclass, protoProps ) { + var superclassProto = superclass.prototype; + + var F = function() {}; + F.prototype = superclassProto; + + var subclass; + if( protoProps.hasOwnProperty( 'constructor' ) ) { + subclass = protoProps.constructor; + } else { + subclass = function() { superclassProto.constructor.apply( this, arguments ); }; + } + + var subclassProto = subclass.prototype = new F(); // set up prototype chain + subclassProto.constructor = subclass; // fix constructor property + subclassProto.superclass = superclassProto; + + delete protoProps.constructor; // don't re-assign constructor property to the prototype, since a new function may have been created (`subclass`), which is now already there + Autolinker.Util.assign( subclassProto, protoProps ); + + return subclass; + }, + + + /** + * Truncates the `str` at `len - ellipsisChars.length`, and adds the `ellipsisChars` to the + * end of the string (by default, two periods: '..'). If the `str` length does not exceed + * `len`, the string will be returned unchanged. + * + * @param {String} str The string to truncate and add an ellipsis to. + * @param {Number} truncateLen The length to truncate the string at. + * @param {String} [ellipsisChars=..] The ellipsis character(s) to add to the end of `str` + * when truncated. Defaults to '..' + */ + ellipsis : function( str, truncateLen, ellipsisChars ) { + if( str.length > truncateLen ) { + ellipsisChars = ( ellipsisChars == null ) ? '..' : ellipsisChars; + str = str.substring( 0, truncateLen - ellipsisChars.length ) + ellipsisChars; + } + return str; + }, + + + /** + * Supports `Array.prototype.indexOf()` functionality for old IE (IE8 and below). + * + * @param {Array} arr The array to find an element of. + * @param {*} element The element to find in the array, and return the index of. + * @return {Number} The index of the `element`, or -1 if it was not found. + */ + indexOf : function( arr, element ) { + if( Array.prototype.indexOf ) { + return arr.indexOf( element ); + + } else { + for( var i = 0, len = arr.length; i < len; i++ ) { + if( arr[ i ] === element ) return i; + } + return -1; + } + }, + + + + /** + * Performs the functionality of what modern browsers do when `String.prototype.split()` is called + * with a regular expression that contains capturing parenthesis. + * + * For example: + * + * // Modern browsers: + * "a,b,c".split( /(,)/ ); // --> [ 'a', ',', 'b', ',', 'c' ] + * + * // Old IE (including IE8): + * "a,b,c".split( /(,)/ ); // --> [ 'a', 'b', 'c' ] + * + * This method emulates the functionality of modern browsers for the old IE case. + * + * @param {String} str The string to split. + * @param {RegExp} splitRegex The regular expression to split the input `str` on. The splitting + * character(s) will be spliced into the array, as in the "modern browsers" example in the + * description of this method. + * Note #1: the supplied regular expression **must** have the 'g' flag specified. + * Note #2: for simplicity's sake, the regular expression does not need + * to contain capturing parenthesis - it will be assumed that any match has them. + * @return {String[]} The split array of strings, with the splitting character(s) included. + */ + splitAndCapture : function( str, splitRegex ) { + if( !splitRegex.global ) throw new Error( "`splitRegex` must have the 'g' flag set" ); + + var result = [], + lastIdx = 0, + match; + + while( match = splitRegex.exec( str ) ) { + result.push( str.substring( lastIdx, match.index ) ); + result.push( match[ 0 ] ); // push the splitting char(s) + + lastIdx = match.index + match[ 0 ].length; + } + result.push( str.substring( lastIdx ) ); + + return result; + } + + }; + /*global Autolinker */ + /** + * @private + * @class Autolinker.HtmlParser + * @extends Object + * + * An HTML parser implementation which simply walks an HTML string and calls the provided visitor functions to process + * HTML and text nodes. + * + * Autolinker uses this to only link URLs/emails/Twitter handles within text nodes, basically ignoring HTML tags. + */ + Autolinker.HtmlParser = Autolinker.Util.extend( Object, { + + /** + * @private + * @property {RegExp} htmlRegex + * + * The regular expression used to pull out HTML tags from a string. Handles namespaced HTML tags and + * attribute names, as specified by http://www.w3.org/TR/html-markup/syntax.html. + * + * Capturing groups: + * + * 1. If it is an end tag, this group will have the '/'. + * 2. The tag name. + */ + htmlRegex : (function() { + var tagNameRegex = /[0-9a-zA-Z:]+/, + attrNameRegex = /[^\s\0"'>\/=\x01-\x1F\x7F]+/, // the unicode range accounts for excluding control chars, and the delete char + attrValueRegex = /(?:".*?"|'.*?'|[^'"=<>`\s]+)/, // double quoted, single quoted, or unquoted attribute values + nameEqualsValueRegex = attrNameRegex.source + '(?:\\s*=\\s*' + attrValueRegex.source + ')?'; // optional '=[value]' + + return new RegExp( [ + '<(?:!|(/))?', // Beginning of a tag. Either '<' for a start tag, ' tag. The slash or an empty string is Capturing Group 1. + + // The tag name (Capturing Group 2) + '(' + tagNameRegex.source + ')', + + // Zero or more attributes following the tag name + '(?:', + '\\s+', // one or more whitespace chars before an attribute + + // Either: + // A. tag="value", or + // B. "value" alone (for tag. Ex: ) + '(?:', nameEqualsValueRegex, '|', attrValueRegex.source + ')', + ')*', + + '\\s*/?', // any trailing spaces and optional '/' before the closing '>' + '>' + ].join( "" ), 'g' ); + } )(), + + + /** + * Walks an HTML string, calling the `options.processHtmlNode` function for each HTML tag that is encountered, and calling + * the `options.processTextNode` function when each text around HTML tags is encountered. + * + * @param {String} html The HTML to parse. + * @param {Object} [options] An Object (map) which may contain the following properties: + * + * @param {Function} [options.processHtmlNode] A visitor function which allows processing of an encountered HTML node. + * This function is called with the following arguments: + * @param {String} [options.processHtmlNode.tagText] The HTML tag text that was found. + * @param {String} [options.processHtmlNode.tagName] The tag name for the HTML tag that was found. Ex: 'a' for an anchor tag. + * @param {String} [options.processHtmlNode.isClosingTag] `true` if the tag is a closing tag (ex: </a>), `false` otherwise. + * + * @param {Function} [options.processTextNode] A visitor function which allows processing of an encountered text node. + * This function is called with the following arguments: + * @param {String} [options.processTextNode.text] The text node that was matched. + */ + parse : function( html, options ) { + options = options || {}; + + var processHtmlNodeVisitor = options.processHtmlNode || function() {}, + processTextNodeVisitor = options.processTextNode || function() {}, + htmlRegex = this.htmlRegex, + currentResult, + lastIndex = 0; + + // Loop over the HTML string, ignoring HTML tags, and processing the text that lies between them, + // wrapping the URLs in anchor tags + while( ( currentResult = htmlRegex.exec( html ) ) !== null ) { + var tagText = currentResult[ 0 ], + tagName = currentResult[ 2 ], + isClosingTag = !!currentResult[ 1 ], + inBetweenTagsText = html.substring( lastIndex, currentResult.index ); + + if( inBetweenTagsText ) { + processTextNodeVisitor( inBetweenTagsText ); + } + + processHtmlNodeVisitor( tagText, tagName, isClosingTag ); + + lastIndex = currentResult.index + tagText.length; + } + + // Process any remaining text after the last HTML element. Will process all of the text if there were no HTML elements. + if( lastIndex < html.length ) { + var text = html.substring( lastIndex ); + + if( text ) { + processTextNodeVisitor( text ); + } + } + } + + } ); + /*global Autolinker */ + /*jshint boss:true */ + /** + * @class Autolinker.HtmlTag + * @extends Object + * + * Represents an HTML tag, which can be used to easily build/modify HTML tags programmatically. + * + * Autolinker uses this abstraction to create HTML tags, and then write them out as strings. You may also use + * this class in your code, especially within a {@link Autolinker#replaceFn replaceFn}. + * + * ## Examples + * + * Example instantiation: + * + * var tag = new Autolinker.HtmlTag( { + * tagName : 'a', + * attrs : { 'href': 'http://google.com', 'class': 'external-link' }, + * innerHtml : 'Google' + * } ); + * + * tag.toString(); // Google + * + * // Individual accessor methods + * tag.getTagName(); // 'a' + * tag.getAttr( 'href' ); // 'http://google.com' + * tag.hasClass( 'external-link' ); // true + * + * + * Using mutator methods (which may be used in combination with instantiation config properties): + * + * var tag = new Autolinker.HtmlTag(); + * tag.setTagName( 'a' ); + * tag.setAttr( 'href', 'http://google.com' ); + * tag.addClass( 'external-link' ); + * tag.setInnerHtml( 'Google' ); + * + * tag.getTagName(); // 'a' + * tag.getAttr( 'href' ); // 'http://google.com' + * tag.hasClass( 'external-link' ); // true + * + * tag.toString(); // Google + * + * + * ## Example use within a {@link Autolinker#replaceFn replaceFn} + * + * var html = Autolinker.link( "Test google.com", { + * replaceFn : function( autolinker, match ) { + * var tag = autolinker.getTagBuilder().build( match ); // returns an {@link Autolinker.HtmlTag} instance, configured with the Match's href and anchor text + * tag.setAttr( 'rel', 'nofollow' ); + * + * return tag; + * } + * } ); + * + * // generated html: + * // Test google.com + * + * + * ## Example use with a new tag for the replacement + * + * var html = Autolinker.link( "Test google.com", { + * replaceFn : function( autolinker, match ) { + * var tag = new Autolinker.HtmlTag( { + * tagName : 'button', + * attrs : { 'title': 'Load URL: ' + match.getAnchorHref() }, + * innerHtml : 'Load URL: ' + match.getAnchorText() + * } ); + * + * return tag; + * } + * } ); + * + * // generated html: + * // Test + */ + Autolinker.HtmlTag = Autolinker.Util.extend( Object, { + + /** + * @cfg {String} tagName + * + * The tag name. Ex: 'a', 'button', etc. + * + * Not required at instantiation time, but should be set using {@link #setTagName} before {@link #toString} + * is executed. + */ + + /** + * @cfg {Object.} attrs + * + * An key/value Object (map) of attributes to create the tag with. The keys are the attribute names, and the + * values are the attribute values. + */ + + /** + * @cfg {String} innerHtml + * + * The inner HTML for the tag. + * + * Note the camel case name on `innerHtml`. Acronyms are camelCased in this utility (such as not to run into the acronym + * naming inconsistency that the DOM developers created with `XMLHttpRequest`). You may alternatively use {@link #innerHTML} + * if you prefer, but this one is recommended. + */ + + /** + * @cfg {String} innerHTML + * + * Alias of {@link #innerHtml}, accepted for consistency with the browser DOM api, but prefer the camelCased version + * for acronym names. + */ + + + /** + * @protected + * @property {RegExp} whitespaceRegex + * + * Regular expression used to match whitespace in a string of CSS classes. + */ + whitespaceRegex : /\s+/, + + + /** + * @constructor + * @param {Object} [cfg] The configuration properties for this class, in an Object (map) + */ + constructor : function( cfg ) { + Autolinker.Util.assign( this, cfg ); + + this.innerHtml = this.innerHtml || this.innerHTML; // accept either the camelCased form or the fully capitalized acronym + }, + + + /** + * Sets the tag name that will be used to generate the tag with. + * + * @param {String} tagName + * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. + */ + setTagName : function( tagName ) { + this.tagName = tagName; + return this; + }, + + + /** + * Retrieves the tag name. + * + * @return {String} + */ + getTagName : function() { + return this.tagName || ""; + }, + + + /** + * Sets an attribute on the HtmlTag. + * + * @param {String} attrName The attribute name to set. + * @param {String} attrValue The attribute value to set. + * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. + */ + setAttr : function( attrName, attrValue ) { + var tagAttrs = this.getAttrs(); + tagAttrs[ attrName ] = attrValue; + + return this; + }, + + + /** + * Retrieves an attribute from the HtmlTag. If the attribute does not exist, returns `undefined`. + * + * @param {String} name The attribute name to retrieve. + * @return {String} The attribute's value, or `undefined` if it does not exist on the HtmlTag. + */ + getAttr : function( attrName ) { + return this.getAttrs()[ attrName ]; + }, + + + /** + * Sets one or more attributes on the HtmlTag. + * + * @param {Object.} attrs A key/value Object (map) of the attributes to set. + * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. + */ + setAttrs : function( attrs ) { + var tagAttrs = this.getAttrs(); + Autolinker.Util.assign( tagAttrs, attrs ); + + return this; + }, + + + /** + * Retrieves the attributes Object (map) for the HtmlTag. + * + * @return {Object.} A key/value object of the attributes for the HtmlTag. + */ + getAttrs : function() { + return this.attrs || ( this.attrs = {} ); + }, + + + /** + * Sets the provided `cssClass`, overwriting any current CSS classes on the HtmlTag. + * + * @param {String} cssClass One or more space-separated CSS classes to set (overwrite). + * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. + */ + setClass : function( cssClass ) { + return this.setAttr( 'class', cssClass ); + }, + + + /** + * Convenience method to add one or more CSS classes to the HtmlTag. Will not add duplicate CSS classes. + * + * @param {String} cssClass One or more space-separated CSS classes to add. + * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. + */ + addClass : function( cssClass ) { + var classAttr = this.getClass(), + whitespaceRegex = this.whitespaceRegex, + indexOf = Autolinker.Util.indexOf, // to support IE8 and below + classes = ( !classAttr ) ? [] : classAttr.split( whitespaceRegex ), + newClasses = cssClass.split( whitespaceRegex ), + newClass; + + while( newClass = newClasses.shift() ) { + if( indexOf( classes, newClass ) === -1 ) { + classes.push( newClass ); + } + } + + this.getAttrs()[ 'class' ] = classes.join( " " ); + return this; + }, + + + /** + * Convenience method to remove one or more CSS classes from the HtmlTag. + * + * @param {String} cssClass One or more space-separated CSS classes to remove. + * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. + */ + removeClass : function( cssClass ) { + var classAttr = this.getClass(), + whitespaceRegex = this.whitespaceRegex, + indexOf = Autolinker.Util.indexOf, // to support IE8 and below + classes = ( !classAttr ) ? [] : classAttr.split( whitespaceRegex ), + removeClasses = cssClass.split( whitespaceRegex ), + removeClass; + + while( classes.length && ( removeClass = removeClasses.shift() ) ) { + var idx = indexOf( classes, removeClass ); + if( idx !== -1 ) { + classes.splice( idx, 1 ); + } + } + + this.getAttrs()[ 'class' ] = classes.join( " " ); + return this; + }, + + + /** + * Convenience method to retrieve the CSS class(es) for the HtmlTag, which will each be separated by spaces when + * there are multiple. + * + * @return {String} + */ + getClass : function() { + return this.getAttrs()[ 'class' ] || ""; + }, + + + /** + * Convenience method to check if the tag has a CSS class or not. + * + * @param {String} cssClass The CSS class to check for. + * @return {Boolean} `true` if the HtmlTag has the CSS class, `false` otherwise. + */ + hasClass : function( cssClass ) { + return ( ' ' + this.getClass() + ' ' ).indexOf( ' ' + cssClass + ' ' ) !== -1; + }, + + + /** + * Sets the inner HTML for the tag. + * + * @param {String} html The inner HTML to set. + * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. + */ + setInnerHtml : function( html ) { + this.innerHtml = html; + + return this; + }, + + + /** + * Retrieves the inner HTML for the tag. + * + * @return {String} + */ + getInnerHtml : function() { + return this.innerHtml || ""; + }, + + + /** + * Override of superclass method used to generate the HTML string for the tag. + * + * @return {String} + */ + toString : function() { + var tagName = this.getTagName(), + attrsStr = this.buildAttrsStr(); + + attrsStr = ( attrsStr ) ? ' ' + attrsStr : ''; // prepend a space if there are actually attributes + + return [ '<', tagName, attrsStr, '>', this.getInnerHtml(), '' ].join( "" ); + }, + + + /** + * Support method for {@link #toString}, returns the string space-separated key="value" pairs, used to populate + * the stringified HtmlTag. + * + * @protected + * @return {String} Example return: `attr1="value1" attr2="value2"` + */ + buildAttrsStr : function() { + if( !this.attrs ) return ""; // no `attrs` Object (map) has been set, return empty string + + var attrs = this.getAttrs(), + attrsArr = []; + + for( var prop in attrs ) { + if( attrs.hasOwnProperty( prop ) ) { + attrsArr.push( prop + '="' + attrs[ prop ] + '"' ); + } + } + return attrsArr.join( " " ); + } + + } ); + /*global Autolinker */ + /*jshint sub:true */ + /** + * @protected + * @class Autolinker.AnchorTagBuilder + * @extends Object + * + * Builds anchor (<a>) tags for the Autolinker utility when a match is found. + * + * Normally this class is instantiated, configured, and used internally by an {@link Autolinker} instance, but may + * actually be retrieved in a {@link Autolinker#replaceFn replaceFn} to create {@link Autolinker.HtmlTag HtmlTag} instances + * which may be modified before returning from the {@link Autolinker#replaceFn replaceFn}. For example: + * + * var html = Autolinker.link( "Test google.com", { + * replaceFn : function( autolinker, match ) { + * var tag = autolinker.getTagBuilder().build( match ); // returns an {@link Autolinker.HtmlTag} instance + * tag.setAttr( 'rel', 'nofollow' ); + * + * return tag; + * } + * } ); + * + * // generated html: + * // Test google.com + */ + Autolinker.AnchorTagBuilder = Autolinker.Util.extend( Object, { + + /** + * @cfg {Boolean} newWindow + * @inheritdoc Autolinker#newWindow + */ + + /** + * @cfg {Number} truncate + * @inheritdoc Autolinker#truncate + */ + + /** + * @cfg {String} className + * @inheritdoc Autolinker#className + */ + + + /** + * @constructor + * @param {Object} [cfg] The configuration options for the AnchorTagBuilder instance, specified in an Object (map). + */ + constructor : function( cfg ) { + Autolinker.Util.assign( this, cfg ); + }, + + + /** + * Generates the actual anchor (<a>) tag to use in place of the matched URL/email/Twitter text, + * via its `match` object. + * + * @param {Autolinker.match.Match} match The Match instance to generate an anchor tag from. + * @return {Autolinker.HtmlTag} The HtmlTag instance for the anchor tag. + */ + build : function( match ) { + var tag = new Autolinker.HtmlTag( { + tagName : 'a', + attrs : this.createAttrs( match.getType(), match.getAnchorHref() ), + innerHtml : this.processAnchorText( match.getAnchorText() ) + } ); + + return tag; + }, + + + /** + * Creates the Object (map) of the HTML attributes for the anchor (<a>) tag being generated. + * + * @protected + * @param {"url"/"email"/"twitter"} matchType The type of match that an anchor tag is being generated for. + * @param {String} href The href for the anchor tag. + * @return {Object} A key/value Object (map) of the anchor tag's attributes. + */ + createAttrs : function( matchType, anchorHref ) { + var attrs = { + 'href' : anchorHref // we'll always have the `href` attribute + }; + + var cssClass = this.createCssClass( matchType ); + if( cssClass ) { + attrs[ 'class' ] = cssClass; + } + if( this.newWindow ) { + attrs[ 'target' ] = "_blank"; + } + + return attrs; + }, + + + /** + * Creates the CSS class that will be used for a given anchor tag, based on the `matchType` and the {@link #className} + * config. + * + * @private + * @param {"url"/"email"/"twitter"} matchType The type of match that an anchor tag is being generated for. + * @return {String} The CSS class string for the link. Example return: "myLink myLink-url". If no {@link #className} + * was configured, returns an empty string. + */ + createCssClass : function( matchType ) { + var className = this.className; + + if( !className ) + return ""; + else + return className + " " + className + "-" + matchType; // ex: "myLink myLink-url", "myLink myLink-email", or "myLink myLink-twitter" + }, + + + /** + * Processes the `anchorText` by truncating the text according to the {@link #truncate} config. + * + * @private + * @param {String} anchorText The anchor tag's text (i.e. what will be displayed). + * @return {String} The processed `anchorText`. + */ + processAnchorText : function( anchorText ) { + anchorText = this.doTruncate( anchorText ); + + return anchorText; + }, + + + /** + * Performs the truncation of the `anchorText`, if the `anchorText` is longer than the {@link #truncate} option. + * Truncates the text to 2 characters fewer than the {@link #truncate} option, and adds ".." to the end. + * + * @private + * @param {String} text The anchor tag's text (i.e. what will be displayed). + * @return {String} The truncated anchor text. + */ + doTruncate : function( anchorText ) { + return Autolinker.Util.ellipsis( anchorText, this.truncate || Number.POSITIVE_INFINITY ); + } + + } ); + /*global Autolinker */ + /** + * @abstract + * @class Autolinker.match.Match + * + * Represents a match found in an input string which should be Autolinked. A Match object is what is provided in a + * {@link Autolinker#replaceFn replaceFn}, and may be used to query for details about the match. + * + * For example: + * + * var input = "..."; // string with URLs, Email Addresses, and Twitter Handles + * + * var linkedText = Autolinker.link( input, { + * replaceFn : function( autolinker, match ) { + * console.log( "href = ", match.getAnchorHref() ); + * console.log( "text = ", match.getAnchorText() ); + * + * switch( match.getType() ) { + * case 'url' : + * console.log( "url: ", match.getUrl() ); + * + * case 'email' : + * console.log( "email: ", match.getEmail() ); + * + * case 'twitter' : + * console.log( "twitter: ", match.getTwitterHandle() ); + * } + * } + * } ); + * + * See the {@link Autolinker} class for more details on using the {@link Autolinker#replaceFn replaceFn}. + */ + Autolinker.match.Match = Autolinker.Util.extend( Object, { + + /** + * @cfg {String} matchedText (required) + * + * The original text that was matched. + */ + + + /** + * @constructor + * @param {Object} cfg The configuration properties for the Match instance, specified in an Object (map). + */ + constructor : function( cfg ) { + Autolinker.Util.assign( this, cfg ); + }, + + + /** + * Returns a string name for the type of match that this class represents. + * + * @abstract + * @return {String} + */ + getType : Autolinker.Util.abstractMethod, + + + /** + * Returns the original text that was matched. + * + * @return {String} + */ + getMatchedText : function() { + return this.matchedText; + }, + + + /** + * Returns the anchor href that should be generated for the match. + * + * @abstract + * @return {String} + */ + getAnchorHref : Autolinker.Util.abstractMethod, + + + /** + * Returns the anchor text that should be generated for the match. + * + * @abstract + * @return {String} + */ + getAnchorText : Autolinker.Util.abstractMethod + + } ); + /*global Autolinker */ + /** + * @class Autolinker.match.Email + * @extends Autolinker.match.Match + * + * Represents a Email match found in an input string which should be Autolinked. + * + * See this class's superclass ({@link Autolinker.match.Match}) for more details. + */ + Autolinker.match.Email = Autolinker.Util.extend( Autolinker.match.Match, { + + /** + * @cfg {String} email (required) + * + * The email address that was matched. + */ + + + /** + * Returns a string name for the type of match that this class represents. + * + * @return {String} + */ + getType : function() { + return 'email'; + }, + + + /** + * Returns the email address that was matched. + * + * @return {String} + */ + getEmail : function() { + return this.email; + }, + + + /** + * Returns the anchor href that should be generated for the match. + * + * @return {String} + */ + getAnchorHref : function() { + return 'mailto:' + this.email; + }, + + + /** + * Returns the anchor text that should be generated for the match. + * + * @return {String} + */ + getAnchorText : function() { + return this.email; + } + + } ); + /*global Autolinker */ + /** + * @class Autolinker.match.Twitter + * @extends Autolinker.match.Match + * + * Represents a Twitter match found in an input string which should be Autolinked. + * + * See this class's superclass ({@link Autolinker.match.Match}) for more details. + */ + Autolinker.match.Twitter = Autolinker.Util.extend( Autolinker.match.Match, { + + /** + * @cfg {String} twitterHandle (required) + * + * The Twitter handle that was matched. + */ + + + /** + * Returns the type of match that this class represents. + * + * @return {String} + */ + getType : function() { + return 'twitter'; + }, + + + /** + * Returns a string name for the type of match that this class represents. + * + * @return {String} + */ + getTwitterHandle : function() { + return this.twitterHandle; + }, + + + /** + * Returns the anchor href that should be generated for the match. + * + * @return {String} + */ + getAnchorHref : function() { + return 'https://twitter.com/' + this.twitterHandle; + }, + + + /** + * Returns the anchor text that should be generated for the match. + * + * @return {String} + */ + getAnchorText : function() { + return '@' + this.twitterHandle; + } + + } ); + /*global Autolinker */ + /** + * @class Autolinker.match.Url + * @extends Autolinker.match.Match + * + * Represents a Url match found in an input string which should be Autolinked. + * + * See this class's superclass ({@link Autolinker.match.Match}) for more details. + */ + Autolinker.match.Url = Autolinker.Util.extend( Autolinker.match.Match, { + + /** + * @cfg {String} url (required) + * + * The url that was matched. + */ + + /** + * @cfg {Boolean} protocolRelativeMatch (required) + * + * `true` if the URL is a protocol-relative match. A protocol-relative match is a URL that starts with '//', + * and will be either http:// or https:// based on the protocol that the site is loaded under. + */ + + /** + * @cfg {Boolean} stripPrefix (required) + * @inheritdoc {@link Autolinker#stripPrefix} + */ + + + /** + * @private + * @property {RegExp} urlPrefixRegex + * + * A regular expression used to remove the 'http://' or 'https://' and/or the 'www.' from URLs. + */ + urlPrefixRegex: /^(https?:\/\/)?(www\.)?/i, + + /** + * @private + * @property {RegExp} protocolRelativeRegex + * + * The regular expression used to remove the protocol-relative '//' from the {@link #url} string, for purposes + * of {@link #getAnchorText}. A protocol-relative URL is, for example, "//yahoo.com" + */ + protocolRelativeRegex : /^\/\//, + + /** + * @protected + * @property {RegExp} checkForProtocolRegex + * + * A regular expression used to check if the {@link #url} is missing a protocol (in which case, 'http://' + * will be added). + */ + checkForProtocolRegex: /^[A-Za-z]{3,9}:/, + + + /** + * Returns a string name for the type of match that this class represents. + * + * @return {String} + */ + getType : function() { + return 'url'; + }, + + + /** + * Returns the url that was matched, assuming the protocol to be 'http://' if the match + * was missing a protocol. + * + * @return {String} + */ + getUrl : function() { + var url = this.url; + + // if the url string doesn't begin with a protocol, assume http:// + if( !this.protocolRelativeMatch && !this.checkForProtocolRegex.test( url ) ) { + url = this.url = 'http://' + url; + } + + return url; + }, + + + /** + * Returns the anchor href that should be generated for the match. + * + * @return {String} + */ + getAnchorHref : function() { + var url = this.getUrl(); + + return url.replace( /&/g, '&' ); // any &'s in the URL should be converted back to '&' if they were displayed as & in the source html + }, + + + /** + * Returns the anchor text that should be generated for the match. + * + * @return {String} + */ + getAnchorText : function() { + var anchorText = this.getUrl(); + + if( this.protocolRelativeMatch ) { + // Strip off any protocol-relative '//' from the anchor text + anchorText = this.stripProtocolRelativePrefix( anchorText ); + } + if( this.stripPrefix ) { + anchorText = this.stripUrlPrefix( anchorText ); + } + anchorText = this.removeTrailingSlash( anchorText ); // remove trailing slash, if there is one + + return anchorText; + }, + + + // --------------------------------------- + + // Utility Functionality + + /** + * Strips the URL prefix (such as "http://" or "https://") from the given text. + * + * @private + * @param {String} text The text of the anchor that is being generated, for which to strip off the + * url prefix (such as stripping off "http://") + * @return {String} The `anchorText`, with the prefix stripped. + */ + stripUrlPrefix : function( text ) { + return text.replace( this.urlPrefixRegex, '' ); + }, + + + /** + * Strips any protocol-relative '//' from the anchor text. + * + * @private + * @param {String} text The text of the anchor that is being generated, for which to strip off the + * protocol-relative prefix (such as stripping off "//") + * @return {String} The `anchorText`, with the protocol-relative prefix stripped. + */ + stripProtocolRelativePrefix : function( text ) { + return text.replace( this.protocolRelativeRegex, '' ); + }, + + + /** + * Removes any trailing slash from the given `anchorText`, in preparation for the text to be displayed. + * + * @private + * @param {String} anchorText The text of the anchor that is being generated, for which to remove any trailing + * slash ('/') that may exist. + * @return {String} The `anchorText`, with the trailing slash removed. + */ + removeTrailingSlash : function( anchorText ) { + if( anchorText.charAt( anchorText.length - 1 ) === '/' ) { + anchorText = anchorText.slice( 0, -1 ); + } + return anchorText; + } + + } ); + + return Autolinker; + +} ) ); +},{}],"/":[function(require,module,exports){ +'use strict'; + + +module.exports = require('./lib/'); + +},{"./lib/":9}]},{},[])("/") +}); \ No newline at end of file diff --git a/test/common/render_helpers.test.js b/test/common/render_helper.test.js similarity index 73% rename from test/common/render_helpers.test.js rename to test/common/render_helper.test.js index 32f6dba95b..e995249125 100644 --- a/test/common/render_helpers.test.js +++ b/test/common/render_helper.test.js @@ -6,9 +6,9 @@ var support = require('../support/support'); var _ = require('lodash'); var pedding = require('pedding'); var multiline = require('multiline'); -var renderHelpers = require('../../common/render_helpers'); +var renderHelper = require('../../common/render_helper'); -describe('test/common/render_helpers.test.js', function () { +describe('test/common/render_helper.test.js', function () { describe('#markdown', function () { it('should render code', function () { var text = multiline(function () {; @@ -19,8 +19,8 @@ var a = 1; */ }); - var rendered = renderHelpers.markdown(text); - rendered.should.equal('
    var a = 1;
    '); + var rendered = renderHelper.markdown(text); + rendered.should.equal('
    var a = 1;\n
    '); }); }); @@ -32,14 +32,14 @@ var a = 1; */ }); - var escaped = renderHelpers.escapeSignature(signature); + var escaped = renderHelper.escapeSignature(signature); escaped.should.equal('我爱北京天安门<script>alert(1)
    </script>'); }) }) describe('#tabName', function () { it('should translate', function () { - renderHelpers.tabName('share') + renderHelper.tabName('share') .should.equal('分享') }) }) diff --git a/views/layout.html b/views/layout.html index fc3a58d317..54c5e33620 100644 --- a/views/layout.html +++ b/views/layout.html @@ -25,21 +25,22 @@ .css('/public/libs/bootstrap/css/bootstrap.css') .css('/public/stylesheets/common.css') .css('/public/stylesheets/style.less') - .css('/public/libs/code-prettify/prettify.css') .css('/public/stylesheets/responsive.css') .css('/public/stylesheets/jquery.atwho.css') .css('/public/libs/editor/editor.css') .css('/public/libs/webuploader/webuploader.css') + .css('/public/libs/code-prettify/prettify.css') .done(assets, config.site_static_host, config.mini_assets) %> <%- Loader('/public/index.min.js') - .js('/public/libs/code-prettify/prettify.js') .js('/public/libs/jquery-2.1.0.js') .js('/public/libs/lodash.compat.js') .js('/public/libs/jquery-ujs.js') .js('/public/libs/bootstrap/js/bootstrap.js') .js('/public/libs/jquery.caret.js') .js('/public/libs/jquery.atwho.js') + .js('/public/libs/remarkable.js') + .js('/public/libs/code-prettify/prettify.js') .js('/public/javascripts/main.js') .js('/public/javascripts/responsive.js') .done(assets, config.site_static_host, config.mini_assets) diff --git a/views/reply/edit.html b/views/reply/edit.html index df5073b8af..fd965ba64e 100644 --- a/views/reply/edit.html +++ b/views/reply/edit.html @@ -47,7 +47,6 @@ <%- Loader('/public/editor.min.js') -.js('/public/libs/marked.js') .js('/public/libs/editor/editor.js') .js('/public/libs/webuploader/webuploader.withoutimage.js') .js('/public/libs/editor/ext.js') diff --git a/views/topic/edit.html b/views/topic/edit.html index 30193f395d..e2ac9bd2b2 100644 --- a/views/topic/edit.html +++ b/views/topic/edit.html @@ -76,7 +76,6 @@ <%- Loader('/public/editor.min.js') -.js('/public/libs/marked.js') .js('/public/libs/editor/editor.js') .js('/public/libs/webuploader/webuploader.withoutimage.js') .js('/public/libs/editor/ext.js') diff --git a/views/topic/index.html b/views/topic/index.html index 9bd030d573..f63118ec63 100644 --- a/views/topic/index.html +++ b/views/topic/index.html @@ -189,7 +189,6 @@ <% if (typeof(current_user) !== 'undefined' && typeof(topic) !== 'undefined') { %> <%- Loader('/public/editor.min.js') -.js('/public/libs/marked.js') .js('/public/libs/editor/editor.js') .js('/public/libs/webuploader/webuploader.withoutimage.js') .js('/public/libs/editor/ext.js')