diff --git a/README.md b/README.md index 1b36e7b..49dcc91 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,17 @@ $$\begin{array}{c} \end{array}$$ ``` +## Syntax + +Math parsing in markdown is designed to agree with the conventions set by pandoc: + + Anything between two $ characters will be treated as TeX math. The opening $ must + have a non-space character immediately to its right, while the closing $ must + have a non-space character immediately to its left, and must not be followed + immediately by a digit. Thus, $20,000 and $30,000 won’t parse as math. If for some + reason you need to enclose text in literal $ characters, backslash-escape them and + they won’t be treated as math delimiters. + ## Math Syntax Support KaTeX is based on TeX and LaTeX. Support for both is growing. Here's a list of diff --git a/index.js b/index.js index 92c0866..57e8eea 100644 --- a/index.js +++ b/index.js @@ -7,167 +7,191 @@ It differs in that it takes (a subset of) LaTeX as input and relies on KaTeX for rendering output. */ +/*jslint node: true */ 'use strict'; var katex = require('katex'); -//return if we have valid delimiter, '$', is in the position. -function isValidDelim(state) { - var lastChar, secondLastChar, nextChar, pos = state.pos; +// Test if potential opening or closing delimieter +// Assumes that there is a "$" at state.src[pos] +function isValidDelim(state, pos) { + var prevChar, nextChar, + max = state.posMax, + can_open = true, + can_close = true; - if(state.src[pos]!=='$'){ - return false; //the character $ must be in its position. - } + prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1; + nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1; - secondLastChar= pos > 1 ? state.src[pos-2] : ' ' - lastChar = pos > 0 ? state.src[pos-1] : ' ' - nextChar = pos + 1 < state.src.length ? state.src[pos+1] : ' ' + // Check non-whitespace conditions for opening and closing, and + // check that closing delimeter isn't followed by a number + if (prevChar === 0x20/* " " */ || prevChar === 0x09/* \t */ || + (nextChar >= 0x30/* "0" */ && nextChar <= 0x39/* "9" */)) { + can_close = false; + } + if (nextChar === 0x20/* " " */ || nextChar === 0x09/* \t */) { + can_open = false; + } - if( - lastChar === '\\' //$ we found was escaped. - || (lastChar === '$' && secondLastChar !== '\\') // $ we found was after $ but not escaped. - || nextChar === '$' //$ we found was actually block delimiter $$. - ) - { - return false; - } - - return true; + return { + can_open: can_open, + can_close: can_close + }; } -function math_inline(state, silent){ - var start, found = false, token; - if(silent) { return false; } +function math_inline(state, silent) { + var start, match, token, res, pos, esc_count; - start = state.pos; - if(state.src[start] !== '$'){ return false; } - if(!isValidDelim(state)){ - state.pos += 1; - state.pending += '$'; - return true; - } - state.pos+=1; + if (state.src[state.pos] !== "$") { return false; } - while(state.pos < state.posMax){ - if(isValidDelim(state)){ - found = true; - break; + res = isValidDelim(state, state.pos); + if (!res.can_open) { + if (!silent) { state.pending += "$"; } + state.pos += 1; + return true; } - state.md.inline.skipToken(state); - } - if(!found){ - // Parser failed to find closing delimiter, so it is not a valid math - state.pos = start; - return false; - } - if (start + 1 === state.pos) { - // There is nothing between the delimiters -- don't match. - state.pos = start; - return false; - } + // First check for and bypass all properly escaped delimieters + // This loop will assume that the first leading backtick can not + // be the first character in state.src, which is known since + // we have found an opening delimieter already. + start = state.pos + 1; + match = start; + while ( (match = state.src.indexOf("$", match)) !== -1) { + // Found potential $, look for escapes, pos will point to + // first non escape when complete + pos = match - 1; + while (state.src[pos] === "\\") { pos -= 1; } - //found the closing delimiter and state.pos is pointing it - token = state.push('math_inline','math',0); - token.content = state.src.slice(start+1,state.pos).trim(); - token.markup = '$'; + // Even number of escapes, potential closing delimiter found + if ( ((match - pos) % 2) == 1 ) { break; } + match += 1; + } - state.pos += 1; - return true; + // No closing delimter found. Consume $ and continue. + if (match === -1) { + if (!silent) { state.pending += "$"; } + state.pos = start; + return true; + } + + // Check if we have empty content, ie: $$. Do not parse. + if (match - start === 0) { + if (!silent) { state.pending += "$$"; } + state.pos = start + 1; + return true; + } + + // Check for valid closing delimiter + res = isValidDelim(state, match); + if (!res.can_close) { + if (!silent) { state.pending += "$"; } + state.pos = start; + return true; + } + + if (!silent) { + token = state.push('math_inline', 'math', 0); + token.markup = "$"; + token.content = state.src.slice(start, match); + } + + state.pos = match + 1; + return true; } function math_block(state, start, end, silent){ - var firstLine, lastLine, next, lastPos, found = false, token, - pos = state.bMarks[start] + state.tShift[start], - max = state.eMarks[start] + var firstLine, lastLine, next, lastPos, found = false, token, + pos = state.bMarks[start] + state.tShift[start], + max = state.eMarks[start] - if(pos + 2 > max){ return false; } - if(state.src.slice(pos,pos+2)!=='$$'){ return false; } + if(pos + 2 > max){ return false; } + if(state.src.slice(pos,pos+2)!=='$$'){ return false; } - pos += 2; - firstLine = state.src.slice(pos,max); + pos += 2; + firstLine = state.src.slice(pos,max); - if(silent){ return true; } - if(firstLine.trim().slice(-2)==='$$'){ - // Single line expression - firstLine = firstLine.trim().slice(0, -2); - found = true; - } - - for(next = start; !found; ){ - - next++; - - if(next >= end){ break; } - - pos = state.bMarks[next]+state.tShift[next]; - max = state.eMarks[next]; - - if(pos < max && state.tShift[next] < state.blkIndent){ - // non-empty line with negative indent should stop the list: - break; + if(silent){ return true; } + if(firstLine.trim().slice(-2)==='$$'){ + // Single line expression + firstLine = firstLine.trim().slice(0, -2); + found = true; } - if(state.src.slice(pos,max).trim().slice(-2)==='$$'){ - lastPos = state.src.slice(0,max).lastIndexOf('$$'); - lastLine = state.src.slice(pos,lastPos); - found = true; + for(next = start; !found; ){ + + next++; + + if(next >= end){ break; } + + pos = state.bMarks[next]+state.tShift[next]; + max = state.eMarks[next]; + + if(pos < max && state.tShift[next] < state.blkIndent){ + // non-empty line with negative indent should stop the list: + break; + } + + if(state.src.slice(pos,max).trim().slice(-2)==='$$'){ + lastPos = state.src.slice(0,max).lastIndexOf('$$'); + lastLine = state.src.slice(pos,lastPos); + found = true; + } + } - } + state.line = next + 1; - state.line = next + 1; - - token = state.push('math_block', 'math', 0); - token.block = true; - token.content = (firstLine && firstLine.trim() ? firstLine + '\n' : '') + token = state.push('math_block', 'math', 0); + token.block = true; + token.content = (firstLine && firstLine.trim() ? firstLine + '\n' : '') + state.getLines(start + 1, next, state.tShift[start], true) + (lastLine && lastLine.trim() ? lastLine : ''); - token.map = [ start, state.line ]; - token.markup = '$$'; - return true; + token.map = [ start, state.line ]; + token.markup = '$$'; + return true; } module.exports = function math_plugin(md, options) { - // Default options + // Default options - options = options || {}; + options = options || {}; - // set KaTeX as the renderer for markdown-it-simplemath - var katexInline = function(latex){ - options.displayMode = false; - try{ - return katex.renderToString(latex, options); + // set KaTeX as the renderer for markdown-it-simplemath + var katexInline = function(latex){ + options.displayMode = false; + try{ + return katex.renderToString(latex, options); + } + catch(error){ + if(options.throwOnError){ console.log(error); } + return latex; + } + }; + + var inlineRenderer = function(tokens, idx){ + return katexInline(tokens[idx].content); + }; + + var katexBlock = function(latex){ + options.displayMode = true; + try{ + return "

" + katex.renderToString(latex, options) + "

"; + } + catch(error){ + if(options.throwOnError){ console.log(error); } + return latex; + } } - catch(error){ - if(options.throwOnError){ console.log(error); } - return latex; + + var blockRenderer = function(tokens, idx){ + return katexBlock(tokens[idx].content) + '\n'; } - }; - var inlineRenderer = function(tokens, idx){ - return katexInline(tokens[idx].content); - }; - - var katexBlock = function(latex){ - options.displayMode = true; - try{ - return "

" + katex.renderToString(latex, options) + "

"; - } - catch(error){ - if(options.throwOnError){ console.log(error); } - return latex; - } - } - - var blockRenderer = function(tokens, idx){ - return katexBlock(tokens[idx].content) + '\n'; - } - - md.inline.ruler.before('escape', 'math_inline', math_inline); - md.block.ruler.after('blockquote', 'math_block', math_block, { - alt: [ 'paragraph', 'reference', 'blockquote', 'list' ] - }); - md.renderer.rules.math_inline = inlineRenderer; - md.renderer.rules.math_block = blockRenderer; + md.inline.ruler.after('escape', 'math_inline', math_inline); + md.block.ruler.after('blockquote', 'math_block', math_block, { + alt: [ 'paragraph', 'reference', 'blockquote', 'list' ] + }); + md.renderer.rules.math_inline = inlineRenderer; + md.renderer.rules.math_block = blockRenderer; }; diff --git a/package.json b/package.json index 60dd09e..c093e15 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "author": "waylonflinn@gmail.com", "license": "MIT", "dependencies": { - "katex": "^0.5.1" + "katex": "^0.6.0" }, "devDependencies": { "markdown-it": "^6.0.0", diff --git a/test/fixtures/default.txt b/test/fixtures/default.txt index 6c6e482..bf9dc4e 100644 --- a/test/fixtures/default.txt +++ b/test/fixtures/default.txt @@ -35,20 +35,13 @@ aaa $$ bbb

aaa $$ bbb

. -Shouldn't render USD +Should require a closing delimiter . aaa $5.99 bbb .

aaa $5.99 bbb

. -Shouldn't render trailing delimiter -. -aaa 5.99$ bbb -. -

aaa 5.99$ bbb

-. - Paragraph break in inline math is not allowed . foo $1+1 @@ -59,14 +52,7 @@ foo $1+1

= 2$ bar

. -Neither is an end of document -. -foo $1+1 = 2 -. -

foo $1+1 = 2

-. - -Inline math with apparent markup +Inline math with apparent markup should not be processed . foo $1 *i* 1$ bar . @@ -193,3 +179,45 @@ $$ 1 $$

. + +Numbers can not follow closing inline math +. +Thus, $20,000 and USD$30,000 won't parse as math. +. +

Thus, $20,000 and USD$30,000 won't parse as math.

+. + +Require non whitespace to right of opening inline math +. +For some Europeans, it is 2$ for a can of soda, not 1$. +. +

For some Europeans, it is 2$ for a can of soda, not 1$.

+. + +Require non whitespace to left of closing inline math. +. +I will give you $20 today, if you give me more $ tomorrow. +. +

I will give you $20 today, if you give me more $ tomorrow.

+. + +Inline blockmath is not (currently) registered. +. +It's well know that $$1 + 1 = 3$$ for sufficiently large 1. +. +

It's well know that $$1 + 1 = 3$$ for sufficiently large 1.

+. + +Escaped delimiters in math mode +. +Money adds: $\$X + \$Y = \$Z$. +. +

Money adds: $X+$Y=$Z\$X + \$Y = \$Z.

+. + +Multiple escaped delimiters in math module +. +Weird-o: $\displaystyle{\begin{pmatrix} \$ & 1\\\$ \end{pmatrix}}$. +. +

Weird-o: ($1$)\displaystyle{\begin{pmatrix} \$ & 1\\\$ \end{pmatrix}}.

+.