From aaf6947951789f1cd377ee4c1ba8b74105052ed6 Mon Sep 17 00:00:00 2001 From: cbreeden Date: Thu, 12 May 2016 18:47:34 -0500 Subject: [PATCH 1/7] Updated KaTeX to 0.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c30c185..cda3b41 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", From 3c4006c14fa2fb8f7693708d1728da47f5b08b6c Mon Sep 17 00:00:00 2001 From: cbreeden Date: Thu, 12 May 2016 18:57:20 -0500 Subject: [PATCH 2/7] Updated Readme to include Syntax information --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 From a5a4427d2b91cd5e71bf23a2ce38e714b4dbfcfb Mon Sep 17 00:00:00 2001 From: cbreeden Date: Thu, 12 May 2016 20:14:33 -0500 Subject: [PATCH 3/7] Add some failing tests --- test/fixtures/default.txt | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/fixtures/default.txt b/test/fixtures/default.txt index 6c6e482..ef24ac3 100644 --- a/test/fixtures/default.txt +++ b/test/fixtures/default.txt @@ -193,3 +193,24 @@ $$ 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 left 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 right of closing inline math. +. +I will give you 20$, if you give me 15$ tomorrow. +. +

I will give you 20$, if you give me 15$ tomorrow.

+. From 4349e4480e7b120d8938e980a0de4df57971695d Mon Sep 17 00:00:00 2001 From: cbreeden Date: Fri, 13 May 2016 10:47:53 -0500 Subject: [PATCH 4/7] refactored inline processing for pandoc syntax --- index.js | 273 ++++++++++++++++++++------------------ npm-debug.log | 23 ++++ test/fixtures/default.txt | 26 +--- 3 files changed, 175 insertions(+), 147 deletions(-) create mode 100644 npm-debug.log diff --git a/index.js b/index.js index 92c0866..64e6723 100644 --- a/index.js +++ b/index.js @@ -7,167 +7,186 @@ 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 = state.pos; - 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, 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; - } + start = state.pos + 1; + match = state.src.indexOf("$", start); - //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 = '$'; + // No closing delimter found. Consume $ and continue. + if (match === -1) { + if (!silent) { state.pending += "$"; } + state.pos = start; + return true; + } - state.pos += 1; - return true; + res = isValidDelim(state, match); + + // We only will look at the very next delimeter while searching + // for closing delimeters. As a consequnce, we will never send + // KaTeX a $ inside of math mode, even if escaped. The other alternative + // would otherwise require escaping commonly used things such as + // \int, \sum, etc... Perhaps there is a way to find a better solution. + // Such as counting the number of \\\\\\\$ and if odd escape $ and remove + // and leading \ or otherwise leave as is. + + if (!res.can_close) { + 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; + } + + 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/npm-debug.log b/npm-debug.log new file mode 100644 index 0000000..c3cf1a7 --- /dev/null +++ b/npm-debug.log @@ -0,0 +1,23 @@ +0 info it worked if it ends with ok +1 verbose cli [ '/usr/bin/nodejs', '/usr/bin/npm', 'run', 'lint' ] +2 info using npm@2.14.20 +3 info using node@v4.4.0 +4 verbose stack Error: missing script: lint +4 verbose stack at run (/usr/lib/node_modules/npm/lib/run-script.js:142:19) +4 verbose stack at /usr/lib/node_modules/npm/lib/run-script.js:58:5 +4 verbose stack at /usr/lib/node_modules/npm/node_modules/read-package-json/read-json.js:345:5 +4 verbose stack at checkBinReferences_ (/usr/lib/node_modules/npm/node_modules/read-package-json/read-json.js:309:45) +4 verbose stack at final (/usr/lib/node_modules/npm/node_modules/read-package-json/read-json.js:343:3) +4 verbose stack at then (/usr/lib/node_modules/npm/node_modules/read-package-json/read-json.js:113:5) +4 verbose stack at /usr/lib/node_modules/npm/node_modules/read-package-json/read-json.js:300:12 +4 verbose stack at /usr/lib/node_modules/npm/node_modules/graceful-fs/graceful-fs.js:78:16 +4 verbose stack at FSReqWrap.readFileAfterClose [as oncomplete] (fs.js:380:3) +5 verbose cwd /home/breeden/github/markdown-it-katex +6 error Linux 3.16.0-4-amd64 +7 error argv "/usr/bin/nodejs" "/usr/bin/npm" "run" "lint" +8 error node v4.4.0 +9 error npm v2.14.20 +10 error missing script: lint +11 error If you need help, you may report this error at: +11 error +12 verbose exit [ 1, true ] diff --git a/test/fixtures/default.txt b/test/fixtures/default.txt index ef24ac3..b61ca05 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 . @@ -201,16 +187,16 @@ 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 left of opening inline 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 right of closing inline math. +Require non whitespace to left of closing inline math. . -I will give you 20$, if you give me 15$ tomorrow. +I will give you $20 today, if you give me more $ tomorrow. . -

I will give you 20$, if you give me 15$ tomorrow.

+

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

. From 97cea23133fabccafbc79b991d9786afb80d65c6 Mon Sep 17 00:00:00 2001 From: Christopher Breeden Date: Sun, 15 May 2016 13:10:37 -0500 Subject: [PATCH 5/7] Added failing test --- test/fixtures/default.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/fixtures/default.txt b/test/fixtures/default.txt index b61ca05..3d6acc3 100644 --- a/test/fixtures/default.txt +++ b/test/fixtures/default.txt @@ -200,3 +200,10 @@ 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.

+. From fd464f82dae0b427d80517ce7aa96dccb72b2f47 Mon Sep 17 00:00:00 2001 From: Christopher Breeden Date: Sun, 15 May 2016 13:57:01 -0500 Subject: [PATCH 6/7] Added proper math-mode escaping --- index.js | 45 ++++++++++++++++++++++----------------- test/fixtures/default.txt | 14 ++++++++++++ 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/index.js b/index.js index 64e6723..57e8eea 100644 --- a/index.js +++ b/index.js @@ -40,20 +40,33 @@ function isValidDelim(state, pos) { } function math_inline(state, silent) { - var start, match, token, res, - pos = state.pos; + var start, match, token, res, pos, esc_count; if (state.src[state.pos] !== "$") { return false; } - res = isValidDelim(state, pos); + res = isValidDelim(state, state.pos); if (!res.can_open) { if (!silent) { state.pending += "$"; } state.pos += 1; return true; } + // 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 = state.src.indexOf("$", start); + 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; } + + // Even number of escapes, potential closing delimiter found + if ( ((match - pos) % 2) == 1 ) { break; } + match += 1; + } // No closing delimter found. Consume $ and continue. if (match === -1) { @@ -62,22 +75,6 @@ function math_inline(state, silent) { return true; } - res = isValidDelim(state, match); - - // We only will look at the very next delimeter while searching - // for closing delimeters. As a consequnce, we will never send - // KaTeX a $ inside of math mode, even if escaped. The other alternative - // would otherwise require escaping commonly used things such as - // \int, \sum, etc... Perhaps there is a way to find a better solution. - // Such as counting the number of \\\\\\\$ and if odd escape $ and remove - // and leading \ or otherwise leave as is. - - if (!res.can_close) { - 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 += "$$"; } @@ -85,6 +82,14 @@ function math_inline(state, silent) { 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 = "$"; diff --git a/test/fixtures/default.txt b/test/fixtures/default.txt index 3d6acc3..bf9dc4e 100644 --- a/test/fixtures/default.txt +++ b/test/fixtures/default.txt @@ -207,3 +207,17 @@ 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}}.

+. From a38d2cbd6369688d1c89c1e4c74592a6b7d14e1e Mon Sep 17 00:00:00 2001 From: Christopher Breeden Date: Wed, 18 May 2016 18:23:28 -0500 Subject: [PATCH 7/7] delete npm-debug.log --- npm-debug.log | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 npm-debug.log diff --git a/npm-debug.log b/npm-debug.log deleted file mode 100644 index c3cf1a7..0000000 --- a/npm-debug.log +++ /dev/null @@ -1,23 +0,0 @@ -0 info it worked if it ends with ok -1 verbose cli [ '/usr/bin/nodejs', '/usr/bin/npm', 'run', 'lint' ] -2 info using npm@2.14.20 -3 info using node@v4.4.0 -4 verbose stack Error: missing script: lint -4 verbose stack at run (/usr/lib/node_modules/npm/lib/run-script.js:142:19) -4 verbose stack at /usr/lib/node_modules/npm/lib/run-script.js:58:5 -4 verbose stack at /usr/lib/node_modules/npm/node_modules/read-package-json/read-json.js:345:5 -4 verbose stack at checkBinReferences_ (/usr/lib/node_modules/npm/node_modules/read-package-json/read-json.js:309:45) -4 verbose stack at final (/usr/lib/node_modules/npm/node_modules/read-package-json/read-json.js:343:3) -4 verbose stack at then (/usr/lib/node_modules/npm/node_modules/read-package-json/read-json.js:113:5) -4 verbose stack at /usr/lib/node_modules/npm/node_modules/read-package-json/read-json.js:300:12 -4 verbose stack at /usr/lib/node_modules/npm/node_modules/graceful-fs/graceful-fs.js:78:16 -4 verbose stack at FSReqWrap.readFileAfterClose [as oncomplete] (fs.js:380:3) -5 verbose cwd /home/breeden/github/markdown-it-katex -6 error Linux 3.16.0-4-amd64 -7 error argv "/usr/bin/nodejs" "/usr/bin/npm" "run" "lint" -8 error node v4.4.0 -9 error npm v2.14.20 -10 error missing script: lint -11 error If you need help, you may report this error at: -11 error -12 verbose exit [ 1, true ]