2014-07-04 14:14:50 -04:00
|
|
|
/*global md5:true */
|
2013-08-08 18:14:12 -04:00
|
|
|
/**
|
|
|
|
|
|
|
|
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,
|
2013-10-21 15:05:52 -04:00
|
|
|
DialectHelpers = parser.DialectHelpers,
|
|
|
|
dialect = MD.dialects.Discourse = DialectHelpers.subclassDialect( MD.dialects.Gruber ),
|
2013-10-18 15:20:27 -04:00
|
|
|
initialized = false,
|
2014-07-04 14:14:50 -04:00
|
|
|
emitters = [],
|
2014-08-25 13:11:20 -04:00
|
|
|
hoisted,
|
2015-03-09 07:32:37 -04:00
|
|
|
preProcessors = [],
|
|
|
|
escape = Handlebars.Utils.escapeExpression;
|
2013-08-08 18:14:12 -04:00
|
|
|
|
2013-08-27 12:52:00 -04:00
|
|
|
/**
|
|
|
|
Initialize our dialects for processing.
|
|
|
|
|
|
|
|
@method initializeDialects
|
|
|
|
**/
|
|
|
|
function initializeDialects() {
|
|
|
|
MD.buildBlockOrder(dialect.block);
|
2014-09-10 06:59:21 -04:00
|
|
|
var index = dialect.block.__order__.indexOf("code");
|
|
|
|
if (index > -1) {
|
|
|
|
dialect.block.__order__.splice(index, 1);
|
|
|
|
dialect.block.__order__.unshift("code");
|
|
|
|
}
|
2013-08-27 12:52:00 -04:00
|
|
|
MD.buildInlinePatterns(dialect.inline);
|
|
|
|
initialized = true;
|
|
|
|
}
|
|
|
|
|
2013-10-18 15:20:27 -04:00
|
|
|
/**
|
|
|
|
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
|
2013-12-16 15:21:46 -05:00
|
|
|
@param {Function} emitter the function to call on the text node
|
2013-10-18 15:20:27 -04:00
|
|
|
**/
|
2013-12-16 15:21:46 -05:00
|
|
|
function processTextNodes(node, event, emitter) {
|
2013-10-18 15:20:27 -04:00
|
|
|
if (node.length < 2) { return; }
|
|
|
|
|
|
|
|
if (node[0] === '__RAW') {
|
2014-07-04 14:14:50 -04:00
|
|
|
var hash = md5(node[1]);
|
|
|
|
hoisted[hash] = node[1];
|
|
|
|
node[1] = hash;
|
2013-10-18 15:20:27 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (var j=1; j<node.length; j++) {
|
|
|
|
var textContent = node[j];
|
|
|
|
if (typeof textContent === "string") {
|
2013-12-16 15:21:46 -05:00
|
|
|
var result = emitter(textContent, event);
|
2013-10-18 15:20:27 -04:00
|
|
|
if (result) {
|
|
|
|
if (result instanceof Array) {
|
|
|
|
node.splice.apply(node, [j, 1].concat(result));
|
|
|
|
} else {
|
|
|
|
node[j] = result;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
node[j] = textContent;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-04-28 14:43:49 -04:00
|
|
|
|
2013-08-27 12:52:00 -04:00
|
|
|
/**
|
|
|
|
Parse a JSON ML tree, using registered handlers to adjust it if necessary.
|
|
|
|
|
|
|
|
@method parseTree
|
|
|
|
@param {Array} tree the JsonML tree to parse
|
|
|
|
@param {Array} path the path of ancestors to the current node in the tree. Can be used for matching.
|
|
|
|
@param {Object} insideCounts counts what tags we're inside
|
|
|
|
@returns {Array} the parsed tree
|
|
|
|
**/
|
|
|
|
function parseTree(tree, path, insideCounts) {
|
2013-09-11 15:52:37 -04:00
|
|
|
|
2013-08-27 12:52:00 -04:00
|
|
|
if (tree instanceof Array) {
|
2013-10-18 15:20:27 -04:00
|
|
|
var event = {node: tree, path: path, dialect: dialect, insideCounts: insideCounts || {}};
|
|
|
|
Discourse.Dialect.trigger('parseNode', event);
|
2013-12-16 15:21:46 -05:00
|
|
|
|
|
|
|
for (var j=0; j<emitters.length; j++) {
|
|
|
|
processTextNodes(tree, event, emitters[j]);
|
|
|
|
}
|
2013-09-11 15:52:37 -04:00
|
|
|
|
2013-08-27 12:52:00 -04:00
|
|
|
path = path || [];
|
|
|
|
insideCounts = insideCounts || {};
|
|
|
|
|
|
|
|
path.push(tree);
|
2013-09-11 15:52:37 -04:00
|
|
|
|
|
|
|
for (var i=1; i<tree.length; i++) {
|
|
|
|
var n = tree[i],
|
|
|
|
tagName = n[0];
|
|
|
|
|
2013-08-27 12:52:00 -04:00
|
|
|
insideCounts[tagName] = (insideCounts[tagName] || 0) + 1;
|
2013-09-11 15:52:37 -04:00
|
|
|
|
2014-04-28 14:43:49 -04:00
|
|
|
if (n && n.length === 2 && n[0] === "p" && /^<!--([\s\S]*)-->$/.exec(n[1])) {
|
2013-09-11 15:52:37 -04:00
|
|
|
// Remove paragraphs around comment-only nodes.
|
|
|
|
tree[i] = n[1];
|
|
|
|
} else {
|
|
|
|
parseTree(n, path, insideCounts);
|
|
|
|
}
|
|
|
|
|
2013-08-27 12:52:00 -04:00
|
|
|
insideCounts[tagName] = insideCounts[tagName] - 1;
|
2013-09-11 15:52:37 -04:00
|
|
|
}
|
2014-07-04 15:12:30 -04:00
|
|
|
|
|
|
|
// If raw nodes are in paragraphs, pull them up
|
|
|
|
if (tree.length === 2 && tree[0] === 'p' && tree[1] instanceof Array && tree[1][0] === "__RAW") {
|
|
|
|
var text = tree[1][1];
|
|
|
|
tree[0] = "__RAW";
|
|
|
|
tree[1] = text;
|
|
|
|
}
|
|
|
|
|
2013-08-27 12:52:00 -04:00
|
|
|
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) {
|
2014-07-03 16:54:56 -04:00
|
|
|
if (!(args.wordBoundary || args.spaceBoundary || args.spaceOrTagBoundary)) { return false; }
|
2013-08-27 12:52:00 -04:00
|
|
|
|
|
|
|
var last = prev[prev.length - 1];
|
2014-03-18 21:23:15 -04:00
|
|
|
if (typeof last !== "string") { return false; }
|
2013-08-27 12:52:00 -04:00
|
|
|
|
2013-08-30 10:56:41 -04:00
|
|
|
if (args.wordBoundary && (last.match(/(\w|\/)$/))) { return true; }
|
2013-08-27 12:52:00 -04:00
|
|
|
if (args.spaceBoundary && (!last.match(/\s$/))) { return true; }
|
2014-07-03 16:54:56 -04:00
|
|
|
if (args.spaceOrTagBoundary && (!last.match(/(\s|\>)$/))) { return true; }
|
2013-08-27 12:52:00 -04:00
|
|
|
}
|
2013-08-08 18:14:12 -04:00
|
|
|
|
2014-07-27 10:07:47 -04:00
|
|
|
/**
|
|
|
|
Returns the number of (terminated) lines in a string.
|
|
|
|
|
|
|
|
@method countLines
|
|
|
|
@param {string} str the string.
|
|
|
|
@returns {Integer} number of terminated lines in str
|
|
|
|
**/
|
|
|
|
function countLines(str) {
|
|
|
|
var index = -1, count = 0;
|
|
|
|
while ((index = str.indexOf("\n", index + 1)) !== -1) { count++; }
|
|
|
|
return count;
|
|
|
|
}
|
|
|
|
|
2014-09-22 16:51:48 -04:00
|
|
|
function hoister(t, target, replacement) {
|
|
|
|
var regexp = new RegExp(target.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), "g");
|
|
|
|
if (t.match(regexp)) {
|
|
|
|
var hash = md5(target);
|
|
|
|
t = t.replace(regexp, hash);
|
|
|
|
hoisted[hash] = replacement;
|
|
|
|
}
|
|
|
|
return t;
|
|
|
|
}
|
|
|
|
|
2015-03-09 07:32:37 -04:00
|
|
|
function outdent(t) {
|
|
|
|
return t.replace(/^([ ]{4}|\t)/gm, "");
|
|
|
|
}
|
|
|
|
|
2015-03-17 12:27:16 -04:00
|
|
|
function removeEmptyLines(t) {
|
|
|
|
return t.replace(/^\n+/, "")
|
|
|
|
.replace(/\s+$/, "");
|
|
|
|
}
|
|
|
|
|
2015-03-09 07:32:37 -04:00
|
|
|
function hideBackslashEscapedCharacters(t) {
|
|
|
|
return t.replace(/\\\\/g, "\u1E800")
|
|
|
|
.replace(/\\`/g, "\u1E8001");
|
|
|
|
}
|
|
|
|
|
|
|
|
function showBackslashEscapedCharacters(t) {
|
|
|
|
return t.replace(/\u1E8001/g, "\\`")
|
|
|
|
.replace(/\u1E800/g, "\\\\");
|
|
|
|
}
|
|
|
|
|
|
|
|
function hoistCodeBlocksAndSpans(text) {
|
|
|
|
// replace all "\`" with a single character
|
|
|
|
text = hideBackslashEscapedCharacters(text);
|
|
|
|
|
2015-03-12 06:17:00 -04:00
|
|
|
// /!\ the order is important /!\
|
|
|
|
|
|
|
|
// fenced code blocks (AKA GitHub code blocks)
|
2015-07-31 03:53:20 -04:00
|
|
|
text = text.replace(/(^\n*|\n)```([a-z0-9\-]*)\n([\s\S]*?)\n```/g, function(_, before, language, content) {
|
2015-03-12 06:17:00 -04:00
|
|
|
var hash = md5(content);
|
2015-03-17 12:27:16 -04:00
|
|
|
hoisted[hash] = escape(showBackslashEscapedCharacters(removeEmptyLines(content)));
|
2015-03-12 06:17:00 -04:00
|
|
|
return before + "```" + language + "\n" + hash + "\n```";
|
|
|
|
});
|
|
|
|
|
2015-03-09 07:32:37 -04:00
|
|
|
// markdown code blocks
|
|
|
|
text = text.replace(/(^\n*|\n\n)((?:(?:[ ]{4}|\t).*\n*)+)/g, function(match, before, content, index) {
|
|
|
|
// make sure we aren't in a list
|
|
|
|
var previousLine = text.slice(0, index).trim().match(/.*$/);
|
|
|
|
if (previousLine && previousLine[0].length) {
|
|
|
|
previousLine = previousLine[0].trim();
|
|
|
|
if (/^(?:\*|\+|-|\d+\.)\s+/.test(previousLine)) {
|
|
|
|
return match;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// we can safely hoist the code block
|
|
|
|
var hash = md5(content);
|
2015-03-17 12:27:16 -04:00
|
|
|
hoisted[hash] = escape(outdent(showBackslashEscapedCharacters(removeEmptyLines(content))));
|
2015-03-09 07:32:37 -04:00
|
|
|
return before + " " + hash + "\n";
|
|
|
|
});
|
|
|
|
|
2015-07-31 04:27:23 -04:00
|
|
|
// <pre>...</pre> code blocks
|
|
|
|
text = text.replace(/(\s|^)<pre>([\s\S]*?)<\/pre>/ig, function(_, before, content) {
|
|
|
|
var hash = md5(content);
|
|
|
|
hoisted[hash] = escape(showBackslashEscapedCharacters(removeEmptyLines(content)));
|
|
|
|
return before + "<pre>" + hash + "</pre>";
|
|
|
|
});
|
|
|
|
|
2015-03-09 07:32:37 -04:00
|
|
|
// code spans (double & single `)
|
|
|
|
["``", "`"].forEach(function(delimiter) {
|
|
|
|
var regexp = new RegExp("(^|[^`])" + delimiter + "([^`\\n]+?)" + delimiter + "([^`]|$)", "g");
|
|
|
|
text = text.replace(regexp, function(_, before, content, after) {
|
|
|
|
var hash = md5(content);
|
|
|
|
hoisted[hash] = escape(showBackslashEscapedCharacters(content.trim()));
|
|
|
|
return before + delimiter + hash + delimiter + after;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// replace back all weird character with "\`"
|
|
|
|
return showBackslashEscapedCharacters(text);
|
|
|
|
}
|
2014-09-22 16:51:48 -04:00
|
|
|
|
2013-08-08 18:14:12 -04:00
|
|
|
/**
|
|
|
|
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
|
2014-03-18 21:19:20 -04:00
|
|
|
@param {Object} opts hash of options
|
2013-08-08 18:14:12 -04:00
|
|
|
@returns {String} the cooked text
|
|
|
|
**/
|
|
|
|
cook: function(text, opts) {
|
|
|
|
if (!initialized) { initializeDialects(); }
|
2014-08-25 13:11:20 -04:00
|
|
|
|
2015-03-09 07:32:37 -04:00
|
|
|
dialect.options = opts;
|
|
|
|
|
2014-09-22 16:51:48 -04:00
|
|
|
// Helps us hoist out HTML
|
|
|
|
hoisted = {};
|
|
|
|
|
2015-03-09 07:32:37 -04:00
|
|
|
// pre-hoist all code-blocks/spans
|
|
|
|
text = hoistCodeBlocksAndSpans(text);
|
|
|
|
|
|
|
|
// pre-processors
|
2014-08-25 13:11:20 -04:00
|
|
|
preProcessors.forEach(function(p) {
|
2014-09-22 16:51:48 -04:00
|
|
|
text = p(text, hoister);
|
2014-08-25 13:11:20 -04:00
|
|
|
});
|
|
|
|
|
2014-07-03 16:54:56 -04:00
|
|
|
var tree = parser.toHTMLTree(text, 'Discourse'),
|
|
|
|
result = parser.renderJsonML(parseTree(tree));
|
|
|
|
|
|
|
|
if (opts.sanitize) {
|
|
|
|
result = Discourse.Markdown.sanitize(result);
|
|
|
|
} else if (opts.sanitizerFunction) {
|
|
|
|
result = opts.sanitizerFunction(result);
|
|
|
|
}
|
2013-10-18 15:20:27 -04:00
|
|
|
|
2014-07-04 14:14:50 -04:00
|
|
|
// If we hoisted out anything, put it back
|
|
|
|
var keys = Object.keys(hoisted);
|
|
|
|
if (keys.length) {
|
2015-07-31 03:53:20 -04:00
|
|
|
var found = true;
|
|
|
|
|
|
|
|
var unhoist = function(key) {
|
2015-03-23 11:33:41 -04:00
|
|
|
result = result.replace(new RegExp(key, "g"), function() {
|
2015-07-31 03:53:20 -04:00
|
|
|
found = true;
|
2015-03-23 11:33:41 -04:00
|
|
|
return hoisted[key];
|
|
|
|
});
|
2015-07-31 03:53:20 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
while(found) {
|
|
|
|
found = false;
|
|
|
|
keys.forEach(unhoist);
|
|
|
|
}
|
2014-07-04 14:14:50 -04:00
|
|
|
}
|
|
|
|
|
2014-07-03 16:54:56 -04:00
|
|
|
return result.trim();
|
2013-08-27 12:52:00 -04:00
|
|
|
},
|
|
|
|
|
2014-08-25 13:11:20 -04:00
|
|
|
/**
|
|
|
|
Adds a text pre-processor. Use only if necessary, as a dialect
|
|
|
|
that emits JsonML is much better if possible.
|
|
|
|
**/
|
|
|
|
addPreProcessor: function(preProc) {
|
|
|
|
preProcessors.push(preProc);
|
|
|
|
},
|
|
|
|
|
2013-11-08 11:42:26 -05:00
|
|
|
/**
|
|
|
|
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;
|
|
|
|
},
|
|
|
|
|
|
|
|
|
2013-08-28 15:27:03 -04:00
|
|
|
/**
|
|
|
|
The simplest kind of replacement possible. Replace a stirng token with JsonML.
|
|
|
|
|
|
|
|
For example to replace all occurrances of :) with a smile image:
|
|
|
|
|
2013-08-29 13:11:12 -04:00
|
|
|
```javascript
|
2013-08-29 13:59:41 -04:00
|
|
|
Discourse.Dialect.inlineReplace(':)', function (text) {
|
|
|
|
return ['img', {src: '/images/smile.png'}];
|
|
|
|
});
|
|
|
|
|
2013-08-28 15:27:03 -04:00
|
|
|
```
|
|
|
|
|
|
|
|
@method inlineReplace
|
|
|
|
@param {String} token The token we want to replace
|
2013-08-29 13:11:12 -04:00
|
|
|
@param {Function} emitter A function that emits the JsonML for the replacement.
|
2013-08-28 15:27:03 -04:00
|
|
|
**/
|
2013-08-29 13:11:12 -04:00
|
|
|
inlineReplace: function(token, emitter) {
|
2014-06-04 15:48:08 -04:00
|
|
|
this.registerInline(token, function(text, match, prev) {
|
|
|
|
return [token.length, emitter.call(this, token, match, prev)];
|
2013-11-08 11:42:26 -05:00
|
|
|
});
|
2013-08-28 15:27:03 -04:00
|
|
|
},
|
|
|
|
|
2013-08-28 13:55:08 -04:00
|
|
|
/**
|
|
|
|
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,
|
2013-11-25 11:35:10 -05:00
|
|
|
start: 'http',
|
2013-08-28 13:55:08 -04:00
|
|
|
|
|
|
|
emitter: function(matches) {
|
|
|
|
var url = matches[1];
|
|
|
|
return ['a', {href: url}, url];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
```
|
|
|
|
|
2013-08-28 15:27:03 -04:00
|
|
|
@method inlineRegexp
|
2013-08-28 13:55:08 -04:00
|
|
|
@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
|
2014-01-12 13:38:46 -05:00
|
|
|
@param {Boolean} [opts.spaceBoundary] If true, the match must be on a space boundary
|
2013-08-28 13:55:08 -04:00
|
|
|
**/
|
2013-08-27 12:52:00 -04:00
|
|
|
inlineRegexp: function(args) {
|
2013-11-08 11:42:26 -05:00
|
|
|
this.registerInline(args.start, function(text, match, prev) {
|
2013-08-27 12:52:00 -04:00
|
|
|
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];
|
|
|
|
}
|
|
|
|
}
|
2013-11-08 11:42:26 -05:00
|
|
|
});
|
2013-08-27 12:52:00 -04:00
|
|
|
},
|
|
|
|
|
2013-08-28 13:55:08 -04:00
|
|
|
/**
|
|
|
|
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
|
|
|
|
|
2013-08-28 15:27:03 -04:00
|
|
|
Discourse.Dialect.inlineBetween({
|
2013-08-28 13:55:08 -04:00
|
|
|
between: '**',
|
|
|
|
wordBoundary: true.
|
|
|
|
emitter: function(contents) {
|
|
|
|
return ['strong'].concat(contents);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
```
|
|
|
|
|
2013-08-28 15:27:03 -04:00
|
|
|
@method inlineBetween
|
2013-08-28 13:55:08 -04:00
|
|
|
@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
|
2014-06-26 03:44:41 -04:00
|
|
|
@param {Boolean} [opts.spaceBoundary] If true, the match must be on a space boundary
|
2013-08-28 13:55:08 -04:00
|
|
|
**/
|
2013-08-28 15:27:03 -04:00
|
|
|
inlineBetween: function(args) {
|
2013-08-27 12:52:00 -04:00
|
|
|
var start = args.start || args.between,
|
|
|
|
stop = args.stop || args.between,
|
2014-06-09 17:46:17 -04:00
|
|
|
startLength = start.length,
|
|
|
|
self = this;
|
2013-08-27 12:52:00 -04:00
|
|
|
|
2013-11-08 11:42:26 -05:00
|
|
|
this.registerInline(start, function(text, match, prev) {
|
2013-08-27 12:52:00 -04:00
|
|
|
if (invalidBoundary(args, prev)) { return; }
|
|
|
|
|
2014-07-24 11:34:13 -04:00
|
|
|
var endPos = self.findEndPos(text, start, stop, args, startLength);
|
2013-08-27 12:52:00 -04:00
|
|
|
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) {
|
2013-08-28 11:14:06 -04:00
|
|
|
return [endPos+stop.length, contents];
|
2013-08-27 12:52:00 -04:00
|
|
|
}
|
2013-11-08 11:42:26 -05:00
|
|
|
});
|
2013-08-28 16:15:11 -04:00
|
|
|
},
|
|
|
|
|
2014-07-24 11:34:13 -04:00
|
|
|
findEndPos: function(text, start, stop, args, offset) {
|
|
|
|
var endPos, nextStart;
|
|
|
|
do {
|
|
|
|
endPos = text.indexOf(stop, offset);
|
|
|
|
if (endPos === -1) { return -1; }
|
|
|
|
nextStart = text.indexOf(start, offset);
|
|
|
|
offset = endPos + stop.length;
|
|
|
|
} while (nextStart !== -1 && nextStart < endPos);
|
2014-06-09 17:46:17 -04:00
|
|
|
return endPos;
|
|
|
|
},
|
|
|
|
|
2013-09-11 15:52:37 -04:00
|
|
|
/**
|
|
|
|
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
|
2014-03-18 21:19:20 -04:00
|
|
|
@param {String} name the name of the block handler
|
|
|
|
@param {Function} handler the handler
|
2013-09-11 15:52:37 -04:00
|
|
|
**/
|
|
|
|
registerBlock: function(name, handler) {
|
|
|
|
dialect.block[name] = handler;
|
|
|
|
},
|
|
|
|
|
2013-08-29 13:59:41 -04:00
|
|
|
/**
|
|
|
|
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]',
|
2014-05-27 22:46:31 -04:00
|
|
|
rawContents: true,
|
2013-08-29 13:59:41 -04:00
|
|
|
|
|
|
|
emitter: function(blockContents) {
|
|
|
|
return ['p', ['pre'].concat(blockContents)];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
```
|
|
|
|
|
|
|
|
@method replaceBlock
|
|
|
|
@param {Object} args Our replacement options
|
2014-05-27 22:46:31 -04:00
|
|
|
@param {RegExp} [args.start] The starting regexp we want to find
|
|
|
|
@param {String} [args.stop] The ending token we want to find
|
|
|
|
@param {Boolean} [args.rawContents] True to skip recursive processing
|
|
|
|
@param {Function} [args.emitter] The emitting function to transform the contents of the block into jsonML
|
2013-08-29 13:59:41 -04:00
|
|
|
|
|
|
|
**/
|
2013-08-29 11:38:51 -04:00
|
|
|
replaceBlock: function(args) {
|
2015-07-20 02:56:32 -04:00
|
|
|
var fn = function(block, next) {
|
2013-09-11 15:52:37 -04:00
|
|
|
|
2014-06-23 15:21:07 -04:00
|
|
|
var linebreaks = dialect.options.traditional_markdown_linebreaks ||
|
|
|
|
Discourse.SiteSettings.traditional_markdown_linebreaks;
|
|
|
|
if (linebreaks && args.skipIfTradtionalLinebreaks) { return; }
|
|
|
|
|
2013-08-29 11:38:51 -04:00
|
|
|
args.start.lastIndex = 0;
|
2014-07-27 10:07:47 -04:00
|
|
|
var result = [], match = (args.start).exec(block);
|
|
|
|
if (!match) { return; }
|
|
|
|
|
|
|
|
var lastChance = function() {
|
2014-08-13 19:58:01 -04:00
|
|
|
return !next.some(function(blk) { return blk.match(args.stop); });
|
2014-07-27 10:07:47 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
// shave off start tag and leading text, if any.
|
|
|
|
var pos = args.start.lastIndex - match[0].length,
|
|
|
|
leading = block.slice(0, pos),
|
|
|
|
trailing = match[2] ? match[2].replace(/^\n*/, "") : "";
|
2014-07-28 12:53:10 -04:00
|
|
|
// just give up if there's no stop tag in this or any next block
|
2014-08-13 19:58:01 -04:00
|
|
|
args.stop.lastIndex = block.length - trailing.length;
|
|
|
|
if (!args.stop.exec(block) && lastChance()) { return; }
|
2014-12-29 15:59:23 -05:00
|
|
|
if (leading.length > 0) {
|
|
|
|
var parsedLeading = this.processBlock(MD.mk_block(leading), []);
|
|
|
|
if (parsedLeading && parsedLeading[0]) {
|
|
|
|
result.push(parsedLeading[0]);
|
|
|
|
}
|
|
|
|
}
|
2014-07-27 10:07:47 -04:00
|
|
|
if (trailing.length > 0) {
|
|
|
|
next.unshift(MD.mk_block(trailing, block.trailing,
|
|
|
|
block.lineNumber + countLines(leading) + (match[2] ? match[2].length : 0) - trailing.length));
|
2013-08-29 11:38:51 -04:00
|
|
|
}
|
2013-08-29 14:42:31 -04:00
|
|
|
|
2014-07-28 12:53:10 -04:00
|
|
|
// go through the available blocks to find the matching stop tag.
|
|
|
|
var contentBlocks = [], nesting = 0, actualEndPos = -1, currentBlock;
|
2014-07-27 10:07:47 -04:00
|
|
|
blockloop:
|
2014-07-28 12:53:10 -04:00
|
|
|
while (currentBlock = next.shift()) {
|
|
|
|
// collect all the start and stop tags in the current block
|
2014-07-27 10:07:47 -04:00
|
|
|
args.start.lastIndex = 0;
|
2014-07-28 12:53:10 -04:00
|
|
|
var startPos = [], m;
|
|
|
|
while (m = (args.start).exec(currentBlock)) {
|
2014-07-27 10:07:47 -04:00
|
|
|
startPos.push(args.start.lastIndex - m[0].length);
|
|
|
|
args.start.lastIndex = args.start.lastIndex - (m[2] ? m[2].length : 0);
|
2013-09-11 15:52:37 -04:00
|
|
|
}
|
2014-08-13 19:58:01 -04:00
|
|
|
args.stop.lastIndex = 0;
|
|
|
|
var endPos = [];
|
|
|
|
while (m = (args.stop).exec(currentBlock)) {
|
|
|
|
endPos.push(args.stop.lastIndex - m[0].length);
|
2014-05-27 22:46:31 -04:00
|
|
|
}
|
2013-08-29 11:38:51 -04:00
|
|
|
|
2014-07-28 12:53:10 -04:00
|
|
|
// go through the available end tags:
|
|
|
|
var ep = 0, sp = 0; // array indices
|
2014-07-27 10:07:47 -04:00
|
|
|
while (ep < endPos.length) {
|
|
|
|
if (sp < startPos.length && startPos[sp] < endPos[ep]) {
|
2014-07-28 12:53:10 -04:00
|
|
|
// there's an end tag, but there's also another start tag first. we need to go deeper.
|
2014-07-27 10:07:47 -04:00
|
|
|
sp++; nesting++;
|
|
|
|
} else if (nesting > 0) {
|
2014-07-28 12:53:10 -04:00
|
|
|
// found an end tag, but we must go up a level first.
|
2014-07-27 10:07:47 -04:00
|
|
|
ep++; nesting--;
|
|
|
|
} else {
|
2014-08-13 19:58:01 -04:00
|
|
|
// found an end tag and we're at the top: done! -- or: start tag and end tag are
|
|
|
|
// identical, (i.e. startPos[sp] == endPos[ep]), so we don't do nesting at all.
|
2014-07-28 12:53:10 -04:00
|
|
|
actualEndPos = endPos[ep];
|
2014-07-27 10:07:47 -04:00
|
|
|
break blockloop;
|
2014-05-27 22:46:31 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-07-27 10:07:47 -04:00
|
|
|
if (lastChance()) {
|
2014-07-28 12:53:10 -04:00
|
|
|
// when lastChance() becomes true the first time, currentBlock contains the last
|
|
|
|
// end tag available in the input blocks but it's not on the right nesting level
|
|
|
|
// or we would have terminated the loop already. the only thing we can do is to
|
|
|
|
// treat the last available end tag as tho it were matched with our start tag
|
|
|
|
// and let the emitter figure out how to render the garbage inside.
|
|
|
|
actualEndPos = endPos[endPos.length - 1];
|
2014-07-27 10:07:47 -04:00
|
|
|
break;
|
2013-08-29 11:38:51 -04:00
|
|
|
}
|
|
|
|
|
2014-07-28 12:53:10 -04:00
|
|
|
// any left-over start tags still increase the nesting level
|
2014-07-27 10:07:47 -04:00
|
|
|
nesting += startPos.length - sp;
|
2014-07-28 12:53:10 -04:00
|
|
|
contentBlocks.push(currentBlock);
|
2013-08-29 11:38:51 -04:00
|
|
|
}
|
|
|
|
|
2014-08-13 19:58:01 -04:00
|
|
|
var stopLen = currentBlock.match(args.stop)[0].length,
|
|
|
|
before = currentBlock.slice(0, actualEndPos).replace(/\n*$/, ""),
|
|
|
|
after = currentBlock.slice(actualEndPos + stopLen).replace(/^\n*/, "");
|
2014-07-28 12:53:10 -04:00
|
|
|
if (before.length > 0) contentBlocks.push(MD.mk_block(before, "", currentBlock.lineNumber));
|
2014-08-26 08:11:23 -04:00
|
|
|
if (after.length > 0) next.unshift(MD.mk_block(after, currentBlock.trailing, currentBlock.lineNumber + countLines(before)));
|
2014-07-27 10:07:47 -04:00
|
|
|
|
|
|
|
var emitterResult = args.emitter.call(this, contentBlocks, match, dialect.options);
|
|
|
|
if (emitterResult) { result.push(emitterResult); }
|
2013-08-29 11:38:51 -04:00
|
|
|
return result;
|
2015-07-20 02:56:32 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
if (args.priority) {
|
|
|
|
fn.priority = args.priority;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.registerBlock(args.start.toString(), fn);
|
2013-08-29 11:38:51 -04:00
|
|
|
},
|
|
|
|
|
2013-08-28 16:15:11 -04:00
|
|
|
/**
|
|
|
|
After the parser has been executed, post process any text nodes in the HTML document.
|
|
|
|
This is useful if you want to apply a transformation to the text.
|
2013-08-27 12:52:00 -04:00
|
|
|
|
2013-08-28 16:15:11 -04:00
|
|
|
If you are generating HTML from the text, it is preferable to use the replacer
|
|
|
|
functions and do it in the parsing part of the pipeline. This function is best for
|
|
|
|
simple transformations or transformations that have to happen after all earlier
|
|
|
|
processing is done.
|
|
|
|
|
|
|
|
For example, to convert all text to upper case:
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
|
|
|
Discourse.Dialect.postProcessText(function (text) {
|
|
|
|
return text.toUpperCase();
|
|
|
|
});
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
@method postProcessText
|
|
|
|
@param {Function} emitter The function to call with the text. It returns JsonML to modify the tree.
|
|
|
|
**/
|
|
|
|
postProcessText: function(emitter) {
|
2013-10-18 15:20:27 -04:00
|
|
|
emitters.push(emitter);
|
2013-08-28 16:15:11 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
After the parser has been executed, change the contents of a HTML tag.
|
|
|
|
|
|
|
|
Let's say you want to replace the contents of all code tags to prepend
|
|
|
|
"EVIL TROUT HACKED YOUR CODE!":
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
Discourse.Dialect.postProcessTag('code', function (contents) {
|
|
|
|
return "EVIL TROUT HACKED YOUR CODE!\n\n" + contents;
|
|
|
|
});
|
|
|
|
```
|
|
|
|
|
|
|
|
@method postProcessTag
|
|
|
|
@param {String} tag The HTML tag you want to match on
|
|
|
|
@param {Function} emitter The function to call with the text. It returns JsonML to modify the tree.
|
|
|
|
**/
|
|
|
|
postProcessTag: function(tag, emitter) {
|
|
|
|
Discourse.Dialect.on('parseNode', function (event) {
|
|
|
|
var node = event.node;
|
|
|
|
if (node[0] === tag) {
|
|
|
|
node[node.length-1] = emitter(node[node.length-1]);
|
|
|
|
}
|
|
|
|
});
|
2013-08-08 18:14:12 -04:00
|
|
|
}
|
2013-08-27 12:52:00 -04:00
|
|
|
|
2013-08-08 18:14:12 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
RSVP.EventTarget.mixin(Discourse.Dialect);
|
2013-08-27 12:52:00 -04:00
|
|
|
|
|
|
|
|