/** Discourse uses the Markdown.js as its main parser. `Discourse.Dialect` is the framework for extending it with additional formatting. **/ var parser = window.BetterMarkdown, MD = parser.Markdown, DialectHelpers = parser.DialectHelpers, dialect = MD.dialects.Discourse = DialectHelpers.subclassDialect( MD.dialects.Gruber ), initialized = false, emitters = []; /** Initialize our dialects for processing. @method initializeDialects **/ function initializeDialects() { MD.buildBlockOrder(dialect.block); MD.buildInlinePatterns(dialect.inline); initialized = true; } /** Process the text nodes in the JsonML tree, calling any emitters that have been added. @method processTextNodes @param {Array} node the JsonML tree @param {Object} event the parse node event data @param {Function} emitter the function to call on the text node **/ function processTextNodes(node, event, emitter) { if (node.length < 2) { return; } if (node[0] === '__RAW') { return; } var skipSanitize = []; for (var j=1; j$/m.exec(n[1])) { // Remove paragraphs around comment-only nodes. tree[i] = n[1]; } else { parseTree(n, path, insideCounts); } insideCounts[tagName] = insideCounts[tagName] - 1; } path.pop(); } return tree; } /** Returns true if there's an invalid word boundary for a match. @method invalidBoundary @param {Object} args our arguments, including whether we care about boundaries @param {Array} prev the previous content, if exists @returns {Boolean} whether there is an invalid word boundary **/ function invalidBoundary(args, prev) { if (!args.wordBoundary && !args.spaceBoundary) { return false; } var last = prev[prev.length - 1]; if (typeof last !== "string") { return false; } if (args.wordBoundary && (last.match(/(\w|\/)$/))) { return true; } if (args.spaceBoundary && (!last.match(/\s$/))) { return true; } } /** An object used for rendering our dialects. @class Dialect @namespace Discourse @module Discourse **/ Discourse.Dialect = { /** Cook text using the dialects. @method cook @param {String} text the raw text to cook @param {Object} opts hash of options @returns {String} the cooked text **/ cook: function(text, opts) { if (!initialized) { initializeDialects(); } dialect.options = opts; var tree = parser.toHTMLTree(text, 'Discourse'); return parser.renderJsonML(parseTree(tree)); }, /** Registers an inline replacer function @method registerInline @param {String} start The token the replacement begins with @param {Function} fn The replacing function **/ registerInline: function(start, fn) { dialect.inline[start] = fn; }, /** The simplest kind of replacement possible. Replace a stirng token with JsonML. For example to replace all occurrances of :) with a smile image: ```javascript Discourse.Dialect.inlineReplace(':)', function (text) { return ['img', {src: '/images/smile.png'}]; }); ``` @method inlineReplace @param {String} token The token we want to replace @param {Function} emitter A function that emits the JsonML for the replacement. **/ inlineReplace: function(token, emitter) { this.registerInline(token, function() { return [token.length, emitter.call(this, token)]; }); }, /** Matches inline using a regular expression. The emitter function is passed the matches from the regular expression. For example, this auto links URLs: ```javascript Discourse.Dialect.inlineRegexp({ matcher: /((?:https?:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.])(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^`!()\[\]{};:'".,<>?«»“”‘’\s]))/gm, spaceBoundary: true, start: 'http', emitter: function(matches) { var url = matches[1]; return ['a', {href: url}, url]; } }); ``` @method inlineRegexp @param {Object} args Our replacement options @param {Function} [opts.emitter] The function that will be called with the contents and regular expresison match and returns JsonML. @param {String} [opts.start] The starting token we want to find @param {String} [opts.matcher] The regular expression to match @param {Boolean} [opts.wordBoundary] If true, the match must be on a word boundary @param {Boolean} [opts.spaceBoundary] If true, the match must be on a space boundary **/ inlineRegexp: function(args) { this.registerInline(args.start, function(text, match, prev) { if (invalidBoundary(args, prev)) { return; } args.matcher.lastIndex = 0; var m = args.matcher.exec(text); if (m) { var result = args.emitter.call(this, m); if (result) { return [m[0].length, result]; } } }); }, /** Handles inline replacements surrounded by tokens. For example, to handle markdown style bold. Note we use `concat` on the array because the contents are JsonML too since we didn't pass `rawContents` as true. This supports recursive markup. ```javascript Discourse.Dialect.inlineBetween({ between: '**', wordBoundary: true. emitter: function(contents) { return ['strong'].concat(contents); } }); ``` @method inlineBetween @param {Object} args Our replacement options @param {Function} [opts.emitter] The function that will be called with the contents and returns JsonML. @param {String} [opts.start] The starting token we want to find @param {String} [opts.stop] The ending token we want to find @param {String} [opts.between] A shortcut for when the `start` and `stop` are the same. @param {Boolean} [opts.rawContents] If true, the contents between the tokens will not be parsed. @param {Boolean} [opts.wordBoundary] If true, the match must be on a word boundary @param {Boolean} [opts.spaceBoundary] If true, the match must be on a sppace boundary **/ inlineBetween: function(args) { var start = args.start || args.between, stop = args.stop || args.between, startLength = start.length; this.registerInline(start, function(text, match, prev) { if (invalidBoundary(args, prev)) { return; } var endPos = text.indexOf(stop, startLength); if (endPos === -1) { return; } var between = text.slice(startLength, endPos); // If rawcontents is set, don't process inline if (!args.rawContents) { between = this.processInline(between); } var contents = args.emitter.call(this, between); if (contents) { return [endPos+stop.length, contents]; } }); }, /** Registers a block for processing. This is more complicated than using one of the other helpers such as `replaceBlock` so consider using them first! @method registerBlock @param {String} name the name of the block handler @param {Function} handler the handler **/ registerBlock: function(name, handler) { dialect.block[name] = handler; }, /** Replaces a block of text between a start and stop. As opposed to inline, these might span multiple lines. Here's an example that takes the content between `[code]` ... `[/code]` and puts them inside a `pre` tag: ```javascript Discourse.Dialect.replaceBlock({ start: /(\[code\])([\s\S]*)/igm, stop: '[/code]', emitter: function(blockContents) { return ['p', ['pre'].concat(blockContents)]; } }); ``` @method replaceBlock @param {Object} args Our replacement options @param {String} [opts.start] The starting regexp we want to find @param {String} [opts.stop] The ending token we want to find @param {Function} [opts.emitter] The emitting function to transform the contents of the block into jsonML **/ replaceBlock: function(args) { this.registerBlock(args.start.toString(), function(block, next) { args.start.lastIndex = 0; var m = (args.start).exec(block); if (!m) { return; } var startPos = block.indexOf(m[0]), leading, blockContents = [], result = [], lineNumber = block.lineNumber; if (startPos > 0) { leading = block.slice(0, startPos); lineNumber += (leading.split("\n").length - 1); var para = ['p']; this.processInline(leading).forEach(function (l) { para.push(l); }); result.push(para); } if (m[2]) { next.unshift(MD.mk_block(m[2], null, lineNumber + 1)); } lineNumber++; var blockClosed = false; if (next.length > 0) { for (var i=0; i= 0) { blockClosed = true; break; } } } if (!blockClosed) { if (m[2]) { next.shift(); } return; } while (next.length > 0) { var b = next.shift(), blockLine = b.lineNumber, diff = ((typeof blockLine === "undefined") ? lineNumber : blockLine) - lineNumber, endFound = b.indexOf(args.stop), leadingContents = b.slice(0, endFound), trailingContents = b.slice(endFound+args.stop.length); if (endFound >= 0) { blockClosed = true; } for (var j=1; j