From 5a6cfafee52427cf3a41a6f30e09044f426b0d50 Mon Sep 17 00:00:00 2001 From: Viktor Qvarfordt Date: Sun, 28 Aug 2016 02:47:59 +0200 Subject: [PATCH] Feature: Raw pass-through of math for MathJax / KaTeX etc. --- README.md | 18 ++++++++++- lib/marked.js | 83 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index efa71aaaab..6873a67e19 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,8 @@ marked.setOptions({ pedantic: false, sanitize: true, smartLists: true, - smartypants: false + smartypants: false, + mathDelimiters: false }); console.log(marked('I am using __markdown__.')); @@ -221,6 +222,21 @@ Default: `true` Enable [GitHub flavored markdown][gfm]. +### mathDelimiters + +Type: `Array` +Default: `null` + +Enable raw pass-through of math wrapped in specified delimiters such as `$..$`, `\(..\)`, `\[..\]`, `$$..$$` and `\begin{}..\end{}`. +For use with MathJax / KaTeX etc. +A standard setup would use + +``` +mathDelimiters: [['$', '$'], ['\\(', '\\)'], ['\\[', '\\]'], ['$$', '$$'], 'beginend'] +``` + +Note the special delimiter option `beginend`, this properly handles any `\begin{}..\end{}` with possible nested environments inside. + ### tables Type: `boolean` diff --git a/lib/marked.js b/lib/marked.js index 9f1584bb3b..ef86f95184 100644 --- a/lib/marked.js +++ b/lib/marked.js @@ -12,7 +12,7 @@ var block = { newline: /^\n+/, - code: /^( {4}[^\n]+\n*)+/, + code: /^\n( {4}[^\n]+\n*)+/, fences: noop, hr: /^( *[-*_]){3,} *(?:\n+|$)/, heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/, @@ -103,6 +103,12 @@ function Lexer(options) { this.tokens = []; this.tokens.links = {}; this.options = options || marked.defaults; + // Sort math delimiters, because parsing of $$..$$ must come before $..$ etc. + if (Array.isArray(this.options.mathDelimiters)) { + this.options.mathDelimiters.sort(function(a, b) { + return b[0].length - a[0].length; + }); + } this.rules = block.normal; if (this.options.gfm) { @@ -538,6 +544,20 @@ function InlineLexer(links, options) { } else if (this.options.pedantic) { this.rules = inline.pedantic; } + + // Make rules compatible with specified math delimiters + if (this.options.mathDelimiters && Array.isArray(this.options.mathDelimiters)) { + var regexEscape = function(s) { + return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + }; + var openingMathDelimiters = []; + for (var i = 0; i < this.options.mathDelimiters.length; i++) { + if (Array.isArray(this.options.mathDelimiters[i]) && this.options.mathDelimiters[i].length === 2) { + openingMathDelimiters.push(regexEscape(this.options.mathDelimiters[i][0])); + } + } + this.rules.text = replace(this.rules.text)(']|', ']|(' + openingMathDelimiters.join('|') + ')|')(); + } } /** @@ -567,6 +587,60 @@ InlineLexer.prototype.output = function(src) { , cap; while (src) { + + // Math wrapped in specified math delimiters is pass through unprocessed, + // for use with MathJax / KaTeX etc. + if (this.options.mathDelimiters) { + for (var i = 0; i < this.options.mathDelimiters.length; i++) { + var delim = this.options.mathDelimiters[i]; + if (delim === 'beginend') { + // \begin{}..\end{} + if (src.substr(0, 7) === '\\begin{') { + var idx = 7; + // traverse nested \begin{}..\end{} + var traverse = function() { + while (src.substr(idx, 5) !== '\\end{') { + if (src.substr(idx, 7) === '\\begin{') { + idx += 7; + traverse(); + } else { + idx++; + } + } + idx += 5; + while (src[idx] !== '}') { + idx++; + } + idx++; + }; + traverse(); + out += this.renderer.math(src.substring(0, idx)); + src = src.substring(idx); + continue; + } + } else if (Array.isArray(delim) && delim.length === 2 && src.substr(0, delim[0].length) === delim[0]) { + var idx = delim[0].length; + while (true) { + if (idx > src.length) { + break; + } + // Allow escaping closing delimiter + if (delim[1].length === 1 && src.substr(idx, 2) === '\\' + delim[1]) { + idx += 2; + } else if (src.substr(idx, delim[1].length) !== delim[1]) { + idx++; + } else { + break; + } + } + idx += delim[1].length; + out += this.renderer.math(src.substring(0, idx)); + src = src.substring(idx); + continue; + } + } + } + // escape if (cap = this.rules.escape.exec(src)) { src = src.substring(cap[0].length); @@ -900,6 +974,10 @@ Renderer.prototype.text = function(text) { return text; }; +Renderer.prototype.math = function(text) { + return text; +}; + /** * Parsing & Compiling */ @@ -1094,7 +1172,7 @@ function escape(html, encode) { } function unescape(html) { - // explicitly match decimal, hex, and named HTML entities + // explicitly match decimal, hex, and named HTML entities return html.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/g, function(_, n) { n = n.toLowerCase(); if (n === 'colon') return ':'; @@ -1252,6 +1330,7 @@ marked.defaults = { langPrefix: 'lang-', smartypants: false, headerPrefix: '', + mathDelimiters: null, renderer: new Renderer, xhtml: false };