diff --git a/.eslintignore b/.eslintignore index 510f2b3d..206e3a31 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,6 @@ **/node_modules/* dist/* website/build/* +website/lib/remarkable-katex.js **/*.min.js contrib/mhchem/* diff --git a/docs/supported.md b/docs/supported.md index 2487f30d..1d5b5a4d 100644 --- a/docs/supported.md +++ b/docs/supported.md @@ -542,8 +542,10 @@ Other KaTeX color functions expect the content to be a function argument: $\textcolor{blue}{F=ma}$ `\textcolor{blue}{F=ma}`
$\textcolor{#228B22}{F=ma}$ `\textcolor{#228B22}{F=ma}`
-$\colorbox{aqua}{A}$ `\colorbox{aqua}{A}`
-$\fcolorbox{red}{aqua}{A}$ `\fcolorbox{red}{aqua}{A}` +$\colorbox{aqua}{$F=ma$}$ `\colorbox{aqua}{$F=ma$}`
+$\fcolorbox{red}{aqua}{$F=ma$}$ `\fcolorbox{red}{aqua}{$F=ma$}` + +Note that, as in LaTeX, `\colorbox` & `\fcolorbox` renders its third argument as text, so you may want to switch back to math mode with `$` as in the examples above. For color definition, KaTeX color functions will accept the standard HTML [predefined color names](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords). They will also accept an RGB argument in CSS hexa­decimal style. The "#" is optional before a six-digit specification. diff --git a/website/lib/remarkable-katex.js b/website/lib/remarkable-katex.js index d6007354..6ea12482 100644 --- a/website/lib/remarkable-katex.js +++ b/website/lib/remarkable-katex.js @@ -1,3 +1,6 @@ +// https://github.com/bradhowes/remarkable-katex/blob/master/index.js +// Modified here to require("../..") instead of require("katex"). + /* MIT License Copyright (c) 2017 Brad Howes @@ -22,179 +25,135 @@ SOFTWARE. */ /** - * Plugin for Remarkable Markdown processor which transforms $..$ and $$..$$ - * sequences into math HTML using the KaTeX package. + * Plugin for Remarkable Markdown processor which transforms $..$ and $$..$$ sequences into math HTML using the + * Katex package. */ -module.exports = function(md, options) { - const katex = require("../../"); +module.exports = (md, options) => { + const dollar = '$'; + const opts = options || {}; + const delimiter = opts.delimiter || dollar; + if (delimiter.length !== 1) throw 'invalid delimiter'; + const katex = require("../../"); - function renderKatex(source, displayMode) { - return katex.renderToString(source, - {displayMode, throwOnError: false, trust: true, strict: false}); + /** + * Render the contents as KaTeX + */ + const renderKatex = (source, displayMode) => katex.renderToString(source, + {displayMode: displayMode, + throwOnError: false}); + + /** + * Parse '$$' as a block. Based off of similar method in remarkable. + */ + const parseBlockKatex = (state, startLine, endLine) => { + let haveEndMarker = false, + pos = state.bMarks[startLine] + state.tShift[startLine], + max = state.eMarks[startLine]; + + if (pos + 1 > max) return false; + + const marker = state.src.charAt(pos); + if (marker !== delimiter) return false; + + // scan marker length + const mem = pos; + pos = state.skipChars(pos, marker); + let len = pos - mem; + + if (len != 2) return false; + + // search end of block + let nextLine = startLine; + + for (;;) { + ++nextLine; + if (nextLine >= endLine) break; + + pos = mem = state.bMarks[nextLine] + state.tShift[nextLine]; + max = state.eMarks[nextLine]; + + if (pos < max && state.tShift[nextLine] < state.blkIndent) break; + if (state.src.charAt(pos) !== delimiter) continue; + if (state.tShift[nextLine] - state.blkIndent >= 4) continue; + + pos = state.skipChars(pos, marker); + if (pos - mem < len) continue; + + pos = state.skipSpaces(pos); + if (pos < max) continue; + + haveEndMarker = true; + break; } - /** - * Parse '$$' as a block. Based off of similar method in remarkable. - */ - function parseBlockKatex(state, startLine, endLine) { - let len; - let params; - let nextLine; - let mem; - let haveEndMarker = false; - let pos = state.bMarks[startLine] + state.tShift[startLine]; - let max = state.eMarks[startLine]; - const dollar = 0x24; + // If a fence has heading spaces, they should be removed from its inner block + len = state.tShift[startLine]; + state.line = nextLine + (haveEndMarker ? 1 : 0); + const content = state.getLines(startLine + 1, nextLine, len, true) + .replace(/[ \n]+/g, ' ') + .trim(); - if (pos + 1 > max) { return false; } + state.tokens.push({type: 'katex', params: null, content: content, lines: [startLine, state.line], + level: state.level, block: true}); + return true; + }; - const marker = state.src.charCodeAt(pos); - if (marker !== dollar) { return false; } + /** + * Look for '$' or '$$' spans in Markdown text. Based off of the 'fenced' parser in remarkable. + */ + const parseInlineKatex = (state, silent) => { + const start = state.pos, max = state.posMax; + let pos = start, marker; - // scan marker length - mem = pos; - pos = state.skipChars(pos, marker); - len = pos - mem; + // Unexpected starting character + if (state.src.charAt(pos) !== delimiter) return false; - if (len !== 2) { return false; } + ++pos; + while (pos < max && state.src.charAt(pos) === delimiter) ++pos; - // search end of block - nextLine = startLine; + // Capture the length of the starting delimiter -- closing one must match in size + marker = state.src.slice(start, pos); + if (marker.length > 2) return false; - 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; - } + let spanStart = pos; + let escapedDepth = 0; + while (pos < max) { + let char = state.src.charAt(pos); + if (char === '{') { + escapedDepth += 1; + } + else if (char === '}') { + escapedDepth -= 1; + if (escapedDepth < 0) return false; + } + else if (char === delimiter && escapedDepth == 0) { + let matchStart = pos; + let matchEnd = pos + 1; + while (matchEnd < max && state.src.charAt(matchEnd) === delimiter) + ++matchEnd; - 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) !== dollar) { 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 (matchEnd - matchStart == marker.length) { + if (!silent) { + const content = state.src.slice(spanStart, matchStart) + .replace(/[ \n]+/g, ' ') + .trim(); + state.push({type: 'katex', content: content, block: marker.length > 1, level: state.level}); + } + state.pos = matchEnd; + return true; } - - // If a fence has heading spaces, they should be removed from - // its inner block - len = state.tShift[startLine]; - - state.line = nextLine + (haveEndMarker ? 1 : 0); - - const content = state.getLines(startLine + 1, nextLine, len, true) - .replace(/[ \n]+/g, ' ') - .trim(); - - state.tokens.push({ - type: 'katex', - params, - content, - lines: [startLine, state.line], - level: state.level, - block: true, - }); - - return true; + } + pos += 1; } - /** - * Look for '$' or '$$' spans in Markdown text. - * Based off of the 'fenced' parser in remarkable. - */ - function parseInlineKatex(state, silent) { - const dollar = 0x24; - const backslash = 0x5c; - let pos = state.pos; - const start = pos; - const max = state.posMax; - let matchStart; - let matchEnd; - let esc; + if (! silent) state.pending += marker; + state.pos += marker.length; - if (state.src.charCodeAt(pos) !== dollar) { return false; } - ++pos; + return true; + }; - while (pos < max && state.src.charCodeAt(pos) === dollar) { - ++pos; - } - - const marker = state.src.slice(start, pos); - if (marker.length > 2) { return false; } - - matchStart = matchEnd = pos; - - while ((matchStart = state.src.indexOf('$', matchEnd)) !== -1) { - matchEnd = matchStart + 1; - - // bypass escaped delimiters - esc = matchStart - 1; - while (state.src.charCodeAt(esc) === backslash) { - --esc; - } - if ((matchStart - esc) % 2 === 0) { continue; } - - while (matchEnd < max && state.src.charCodeAt(matchEnd) === dollar) { - ++matchEnd; - } - - if (matchEnd - matchStart === marker.length) { - if (!silent) { - const content = state.src.slice(pos, matchStart) - .replace(/[ \n]+/g, ' ') - .trim(); - - state.push({ - type: 'katex', - content, - block: marker.length > 1, - level: state.level, - }); - } - - state.pos = matchEnd; - return true; - } - } - - if (!silent) { - state.pending += marker; - } - state.pos += marker.length; - - return true; - } - - md.inline.ruler.push('katex', parseInlineKatex, options); - md.block.ruler.push('katex', parseBlockKatex, options); - md.renderer.rules.katex = function(tokens, idx) { - return renderKatex(tokens[idx].content, tokens[idx].block); - }; + md.inline.ruler.push('katex', parseInlineKatex, options); + md.block.ruler.push('katex', parseBlockKatex, options); + md.renderer.rules.katex = (tokens, idx) => renderKatex(tokens[idx].content, tokens[idx].block); + md.renderer.rules.katex.delimiter = delimiter; };