├── rollup.config.mjs ├── test ├── all.js └── fixtures │ └── default.txt ├── index.html ├── package.json ├── LICENSE ├── browser.js ├── README.md ├── .gitignore ├── index.js └── index.mjs /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | input: 'index.mjs', 3 | output: { 4 | file: 'index.js', 5 | format: 'cjs' 6 | }, 7 | external: ['katex'] 8 | }; 9 | -------------------------------------------------------------------------------- /test/all.js: -------------------------------------------------------------------------------- 1 | var path = require('path'), 2 | tape = require('tape'), 3 | testLoad = require('markdown-it-testgen').load, 4 | mdk = require('../index'); 5 | 6 | var md = require('markdown-it')() 7 | .use(mdk); 8 | 9 | /* this uses the markdown-it-testgen module to automatically generate tests 10 | based on an easy to read text file 11 | */ 12 | testLoad(path.join(__dirname, 'fixtures/default.txt'), function(data){ 13 | data.fixtures.forEach(function (fixture){ 14 | 15 | /* generic test definition code using tape */ 16 | tape(fixture.header, function(t){ 17 | t.plan(1); 18 | 19 | var expected = fixture.second.text, 20 | actual = md.render(fixture.first.text); 21 | 22 | t.equals(actual, expected); 23 | 24 | }); 25 | 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 23 | 24 | 25 | 36 | 37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ruanyf/markdown-it-katex", 3 | "version": "5.0.0", 4 | "description": "Fast math support for markdown-it with KaTeX", 5 | "main": "index.js", 6 | "exports": { 7 | ".": { 8 | "import": "./index.mjs", 9 | "require": "./index.js" 10 | } 11 | }, 12 | "scripts": { 13 | "build": "rollup --config rollup.config.mjs", 14 | "watch": "watchify browser.js -o bundle.js -v", 15 | "test": "npm run build && node test/all.js", 16 | "prepublishOnly": "npm run build" 17 | }, 18 | "homepage": "https://github.com/ruanyf/markdown-it-katex", 19 | "repository": { 20 | "type": "git", 21 | "url": "git+ssh://git@github.com/ruanyf/markdown-it-katex.git" 22 | }, 23 | "keywords": [ 24 | "markdown", 25 | "KaTeX", 26 | "math", 27 | "LaTeX", 28 | "markdown-it-plugin", 29 | "markdown-it" 30 | ], 31 | "author": "Takahiro Ethan Ikeuchi @iktakahiro", 32 | "license": "MIT", 33 | "dependencies": { 34 | "katex": "latest" 35 | }, 36 | "devDependencies": { 37 | "markdown-it": "latest", 38 | "markdown-it-testgen": "0.1.6", 39 | "rollup": "^4.18.0", 40 | "tape": "5.x" 41 | }, 42 | "directories": { 43 | "test": "test" 44 | }, 45 | "bugs": { 46 | "url": "https://github.com/ruanyf/markdown-it-katex/issues" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Waylon Flinn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | --- 24 | 25 | The MIT License (MIT) 26 | 27 | Copyright (c) 2018 Takahiro Ethan Ikeuchi @iktakahiro 28 | 29 | Permission is hereby granted, free of charge, to any person obtaining a copy 30 | of this software and associated documentation files (the "Software"), to deal 31 | in the Software without restriction, including without limitation the rights 32 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 33 | copies of the Software, and to permit persons to whom the Software is 34 | furnished to do so, subject to the following conditions: 35 | 36 | The above copyright notice and this permission notice shall be included in all 37 | copies or substantial portions of the Software. 38 | 39 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 40 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 41 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 42 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 43 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 44 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 45 | SOFTWARE. 46 | -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | var md = require('markdown-it')(), 2 | mk = require('./index'); 3 | 4 | md.use(mk); 5 | 6 | var input = document.getElementById('input'), 7 | output = document.getElementById('output'), 8 | button = document.getElementById('button'); 9 | 10 | button.addEventListener('click', function(ev){ 11 | 12 | var result = md.render(input.value); 13 | 14 | output.innerHTML = result; 15 | 16 | }); 17 | 18 | /* 19 | 20 | # Some Math 21 | 22 | $\sqrt{3x-1}+(1+x)^2$ 23 | 24 | # Maxwells Equations 25 | 26 | $\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} 27 | = \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} = 4 \pi \rho$ 28 | 29 | $\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} = \vec{\mathbf{0}}$ (curl of $\vec{\mathbf{E}}$ is proportional to the time derivative of $\vec{\mathbf{B}}$) 30 | 31 | $\nabla \cdot \vec{\mathbf{B}} = 0$ 32 | 33 | 34 | 35 | \sqrt{3x-1}+(1+x)^2 36 | 37 | c = \pm\sqrt{a^2 + b^2} 38 | 39 | Maxwell's Equations 40 | 41 | \nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} 42 | = \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} = 4 \pi \rho 43 | 44 | \nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} = \vec{\mathbf{0}} 45 | 46 | \nabla \cdot \vec{\mathbf{B}} = 0 47 | 48 | Same thing in a LaTeX array 49 | \begin{array}{c} 50 | 51 | \nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & 52 | = \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\ 53 | 54 | \nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\ 55 | 56 | \nabla \cdot \vec{\mathbf{B}} & = 0 57 | 58 | \end{array} 59 | 60 | 61 | \begin{array}{c} 62 | y_1 \\ 63 | y_2 \mathtt{t}_i \\ 64 | z_{3,4} 65 | \end{array} 66 | 67 | \begin{array}{c} 68 | x' &=& &x \sin\phi &+& z \cos\phi \\ 69 | z' &=& - &x \cos\phi &+& z \sin\phi \\ 70 | \end{array} 71 | 72 | 73 | 74 | # Maxwell's Equations 75 | 76 | 77 | equation | description 78 | ----------|------------ 79 | $\nabla \cdot \vec{\mathbf{B}} = 0$ | divergence of $\vec{\mathbf{B}}$ is zero 80 | $\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} = \vec{\mathbf{0}}$ | curl of $\vec{\mathbf{E}}$ is proportional to the rate of change of $\vec{\mathbf{B}}$ 81 | $\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} = \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} = 4 \pi \rho$ | wha? 82 | 83 | ![electricity](http://i.giphy.com/Gty2oDYQ1fih2.gif) 84 | */ 85 | -------------------------------------------------------------------------------- /test/fixtures/default.txt: -------------------------------------------------------------------------------- 1 | 2 | Simple inline math 3 | . 4 | $1+1 = 2$ 5 | . 6 |

1+1=21+1 = 2

7 | . 8 | 9 | Simple block math 10 | . 11 | $$1+1 = 2$$ 12 | . 13 |

1+1=21+1 = 2 14 |

15 | . 16 | 17 | Ignore single dollar sign 18 | . 19 | It costs $5. 20 | . 21 |

It costs $5.

22 | . 23 | 24 | Ignore multiple dollar signs in separate paragraphs 25 | . 26 | This costs $5. 27 | 28 | That costs $10. 29 | . 30 |

This costs $5.

31 |

That costs $10.

32 | . 33 | 34 | Inline math inside heading 35 | . 36 | # The $N$-eigenvalue problem and two applications 37 | . 38 |

The NN-eigenvalue problem and two applications

39 | . 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This plugin is forked from [@iktakahiro/markdown-it-katex](https://github.com/iktakahiro/markdown-it-katex), adding ESM support and updating dependencies. 2 | 3 | # markdown-it-katex 4 | 5 | Add Math to your Markdown 6 | 7 | [KaTeX](https://github.com/Khan/KaTeX) is a faster alternative to MathJax. This plugin makes it easy to support in your markdown. 8 | 9 | ## Install 10 | 11 | Install markdown-it 12 | 13 | ```bash 14 | npm install markdown-it 15 | ``` 16 | 17 | Install the plugin 18 | 19 | ```bash 20 | npm install @ruanyf/markdown-it-katex 21 | ``` 22 | 23 | ## Usage 24 | 25 | Load it in ES module. 26 | 27 | ```javascript 28 | import markdownit from 'markdown-it'; 29 | import mk from '@ruanyf/markdown-it-katex'; 30 | 31 | const md = markdownit(); 32 | 33 | md.use(mk); 34 | 35 | // double backslash is required for javascript strings, but not html input 36 | var result = md.render('# Math Rulez! \n $\\sqrt{3x-1}+(1+x)^2$'); 37 | ``` 38 | 39 | Or load it in CommonJS module. 40 | 41 | ```javascript 42 | var md = require('markdown-it')(), 43 | mk = require('@ruanyf/markdown-it-katex'); 44 | 45 | md.use(mk); 46 | 47 | // double backslash is required for javascript strings, but not html input 48 | var result = md.render('# Math Rulez! \n $\\sqrt{3x-1}+(1+x)^2$'); 49 | ``` 50 | 51 | Include the KaTeX stylesheet in your html: 52 | 53 | ```html 54 | 55 | ``` 56 | 57 | If you're using the default markdown-it parser, I also recommend the [github stylesheet](https://github.com/sindresorhus/github-markdown-css): 58 | 59 | ```html 60 | 61 | ``` 62 | 63 | `KaTeX` options can be supplied with the second argument to use. 64 | 65 | ```javascript 66 | md.use(mk, {"throwOnError" : false, "errorColor" : " #cc0000"}); 67 | ``` 68 | 69 | ## Examples 70 | 71 | ### Inline 72 | 73 | Surround your LaTeX with a single `$` on each side for inline rendering. 74 | 75 | ```latex 76 | $\sqrt{3x-1}+(1+x)^2$ 77 | ``` 78 | 79 | ### Block 80 | 81 | Use two (`$$`) for block rendering. This mode uses bigger symbols and centers 82 | the result. 83 | 84 | ```latex 85 | $$\begin{array}{c} 86 | 87 | \nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & 88 | = \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\ 89 | 90 | \nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\ 91 | 92 | \nabla \cdot \vec{\mathbf{B}} & = 0 93 | 94 | \end{array}$$ 95 | ``` 96 | 97 | ## Syntax 98 | 99 | Math parsing in markdown is designed to agree with the conventions set by pandoc: 100 | 101 | Anything between two $ characters will be treated as TeX math. The opening $ must 102 | have a non-space character immediately to its right, while the closing $ must 103 | have a non-space character immediately to its left, and must not be followed 104 | immediately by a digit. Thus, $20,000 and $30,000 won’t parse as math. If for some 105 | reason you need to enclose text in literal $ characters, backslash-escape them and 106 | they won’t be treated as math delimiters. 107 | 108 | ## Math Syntax Support 109 | 110 | KaTeX is based on TeX and LaTeX. Support for both is growing. Here's a list of 111 | currently supported functions: 112 | 113 | [Things that KaTeX does not (yet) support](https://github.com/KaTeX/KaTeX/wiki/Things-that-KaTeX-does-not-%28yet%29-support) 114 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | index.js 2 | package-lock.json 3 | 4 | # Created by .ignore support plugin (hsz.mobi) 5 | ### macOS template 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | ### JetBrains template 33 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 34 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 35 | 36 | # User-specific stuff 37 | .idea/**/workspace.xml 38 | .idea/**/tasks.xml 39 | .idea/**/dictionaries 40 | .idea/**/shelf 41 | 42 | # Sensitive or high-churn files 43 | .idea/**/dataSources/ 44 | .idea/**/dataSources.ids 45 | .idea/**/dataSources.local.xml 46 | .idea/**/sqlDataSources.xml 47 | .idea/**/dynamic.xml 48 | .idea/**/uiDesigner.xml 49 | .idea/**/dbnavigator.xml 50 | 51 | # Gradle 52 | .idea/**/gradle.xml 53 | .idea/**/libraries 54 | 55 | # CMake 56 | cmake-build-debug/ 57 | cmake-build-release/ 58 | 59 | # Mongo Explorer plugin 60 | .idea/**/mongoSettings.xml 61 | 62 | # File-based project format 63 | *.iws 64 | 65 | # IntelliJ 66 | out/ 67 | 68 | # mpeltonen/sbt-idea plugin 69 | .idea_modules/ 70 | 71 | # JIRA plugin 72 | atlassian-ide-plugin.xml 73 | 74 | # Cursive Clojure plugin 75 | .idea/replstate.xml 76 | 77 | # Crashlytics plugin (for Android Studio and IntelliJ) 78 | com_crashlytics_export_strings.xml 79 | crashlytics.properties 80 | crashlytics-build.properties 81 | fabric.properties 82 | 83 | # Editor-based Rest Client 84 | .idea/httpRequests 85 | ### Windows template 86 | # Windows thumbnail cache files 87 | Thumbs.db 88 | ehthumbs.db 89 | ehthumbs_vista.db 90 | 91 | # Dump file 92 | *.stackdump 93 | 94 | # Folder config file 95 | [Dd]esktop.ini 96 | 97 | # Recycle Bin used on file shares 98 | $RECYCLE.BIN/ 99 | 100 | # Windows Installer files 101 | *.cab 102 | *.msi 103 | *.msix 104 | *.msm 105 | *.msp 106 | 107 | # Windows shortcuts 108 | *.lnk 109 | ### Node template 110 | # Logs 111 | logs 112 | *.log 113 | npm-debug.log* 114 | yarn-debug.log* 115 | yarn-error.log* 116 | 117 | # Runtime data 118 | pids 119 | *.pid 120 | *.seed 121 | *.pid.lock 122 | 123 | # Directory for instrumented libs generated by jscoverage/JSCover 124 | lib-cov 125 | 126 | # Coverage directory used by tools like istanbul 127 | coverage 128 | 129 | # nyc test coverage 130 | .nyc_output 131 | 132 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 133 | .grunt 134 | 135 | # Bower dependency directory (https://bower.io/) 136 | bower_components 137 | 138 | # node-waf configuration 139 | .lock-wscript 140 | 141 | # Compiled binary addons (https://nodejs.org/api/addons.html) 142 | build/Release 143 | 144 | # Dependency directories 145 | node_modules/ 146 | jspm_packages/ 147 | 148 | # TypeScript v1 declaration files 149 | typings/ 150 | 151 | # Optional npm cache directory 152 | .npm 153 | 154 | # Optional eslint cache 155 | .eslintcache 156 | 157 | # Optional REPL history 158 | .node_repl_history 159 | 160 | # Output of 'npm pack' 161 | *.tgz 162 | 163 | # Yarn Integrity file 164 | .yarn-integrity 165 | 166 | # dotenv environment variables file 167 | .env 168 | 169 | # next.js build output 170 | .next 171 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var katex = require('katex'); 4 | 5 | /* Process inline math */ 6 | /* 7 | Like markdown-it-simplemath, this is a stripped down, simplified version of: 8 | https://github.com/runarberg/markdown-it-math 9 | 10 | It differs in that it takes (a subset of) LaTeX as input and relies on KaTeX 11 | for rendering output. 12 | */ 13 | 14 | 15 | // Test if potential opening or closing delimieter 16 | // Assumes that there is a "$" at state.src[pos] 17 | function isValidDelim(state, pos) { 18 | var prevChar, nextChar, 19 | max = state.posMax, 20 | can_open = true, 21 | can_close = true; 22 | 23 | prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1; 24 | nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1; 25 | 26 | // Check non-whitespace conditions for opening and closing, and 27 | // check that closing delimeter isn't followed by a number 28 | if (prevChar === 0x20/* " " */ || prevChar === 0x09/* \t */ || 29 | (nextChar >= 0x30/* "0" */ && nextChar <= 0x39/* "9" */)) { 30 | can_close = false; 31 | } 32 | if (nextChar === 0x20/* " " */ || nextChar === 0x09/* \t */) { 33 | can_open = false; 34 | } 35 | 36 | return { 37 | can_open: can_open, 38 | can_close: can_close 39 | }; 40 | } 41 | 42 | function math_inline(state, silent) { 43 | var start, match, token, res, pos; 44 | 45 | if (state.src[state.pos] !== "$") { return false; } 46 | 47 | res = isValidDelim(state, state.pos); 48 | if (!res.can_open) { 49 | if (!silent) { state.pending += "$"; } 50 | state.pos += 1; 51 | return true; 52 | } 53 | 54 | // First check for and bypass all properly escaped delimieters 55 | // This loop will assume that the first leading backtick can not 56 | // be the first character in state.src, which is known since 57 | // we have found an opening delimieter already. 58 | start = state.pos + 1; 59 | match = start; 60 | while ( (match = state.src.indexOf("$", match)) !== -1) { 61 | // Found potential $, look for escapes, pos will point to 62 | // first non escape when complete 63 | pos = match - 1; 64 | while (state.src[pos] === "\\") { pos -= 1; } 65 | 66 | // Even number of escapes, potential closing delimiter found 67 | if ( ((match - pos) % 2) == 1 ) { break; } 68 | match += 1; 69 | } 70 | 71 | // No closing delimter found. Consume $ and continue. 72 | if (match === -1) { 73 | if (!silent) { state.pending += "$"; } 74 | state.pos = start; 75 | return true; 76 | } 77 | 78 | // Check if we have empty content, ie: $$. Do not parse. 79 | if (match - start === 0) { 80 | if (!silent) { state.pending += "$$"; } 81 | state.pos = start + 1; 82 | return true; 83 | } 84 | 85 | // Check for valid closing delimiter 86 | res = isValidDelim(state, match); 87 | if (!res.can_close) { 88 | if (!silent) { state.pending += "$"; } 89 | state.pos = start; 90 | return true; 91 | } 92 | 93 | if (!silent) { 94 | token = state.push('math_inline', 'math', 0); 95 | token.markup = "$"; 96 | token.content = state.src.slice(start, match); 97 | } 98 | 99 | state.pos = match + 1; 100 | return true; 101 | } 102 | 103 | function math_block(state, start, end, silent){ 104 | var firstLine, lastLine, next, lastPos, found = false, token, 105 | pos = state.bMarks[start] + state.tShift[start], 106 | max = state.eMarks[start]; 107 | 108 | if(pos + 2 > max){ return false; } 109 | if(state.src.slice(pos,pos+2)!=='$$'){ return false; } 110 | 111 | pos += 2; 112 | firstLine = state.src.slice(pos,max); 113 | 114 | if(silent){ return true; } 115 | if(firstLine.trim().slice(-2)==='$$'){ 116 | // Single line expression 117 | firstLine = firstLine.trim().slice(0, -2); 118 | found = true; 119 | } 120 | 121 | for(next = start; !found; ){ 122 | 123 | next++; 124 | 125 | if(next >= end){ break; } 126 | 127 | pos = state.bMarks[next]+state.tShift[next]; 128 | max = state.eMarks[next]; 129 | 130 | if(pos < max && state.tShift[next] < state.blkIndent){ 131 | // non-empty line with negative indent should stop the list: 132 | break; 133 | } 134 | 135 | if(state.src.slice(pos,max).trim().slice(-2)==='$$'){ 136 | lastPos = state.src.slice(0,max).lastIndexOf('$$'); 137 | lastLine = state.src.slice(pos,lastPos); 138 | found = true; 139 | } 140 | 141 | } 142 | 143 | state.line = next + 1; 144 | 145 | token = state.push('math_block', 'math', 0); 146 | token.block = true; 147 | token.content = (firstLine && firstLine.trim() ? firstLine + '\n' : '') 148 | + state.getLines(start + 1, next, state.tShift[start], true) 149 | + (lastLine && lastLine.trim() ? lastLine : ''); 150 | token.map = [ start, state.line ]; 151 | token.markup = '$$'; 152 | return true; 153 | } 154 | 155 | function escapeHtml(unsafe) { 156 | return unsafe 157 | .replace(/&/g, "&") 158 | .replace(//g, ">") 160 | .replace(/"/g, """) 161 | .replace(/'/g, "'"); 162 | } 163 | 164 | function math_plugin(md, options) { 165 | // Default options 166 | 167 | options = options || {}; 168 | 169 | // set KaTeX as the renderer for markdown-it-simplemath 170 | var katexInline = function(latex){ 171 | options.displayMode = false; 172 | try{ 173 | return katex.renderToString(latex, options); 174 | } 175 | catch(error){ 176 | if(options.throwOnError){ console.log(error); } 177 | return `${escapeHtml(latex)}`; 178 | } 179 | }; 180 | 181 | var inlineRenderer = function(tokens, idx){ 182 | return katexInline(tokens[idx].content); 183 | }; 184 | 185 | var katexBlock = function(latex){ 186 | options.displayMode = true; 187 | try{ 188 | return "

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

"; 189 | } 190 | catch(error){ 191 | if(options.throwOnError){ console.log(error); } 192 | return `

${escapeHtml(latex)}

`; 193 | } 194 | }; 195 | 196 | var blockRenderer = function(tokens, idx){ 197 | return katexBlock(tokens[idx].content) + '\n'; 198 | }; 199 | 200 | md.inline.ruler.after('escape', 'math_inline', math_inline); 201 | md.block.ruler.after('blockquote', 'math_block', math_block, { 202 | alt: [ 'paragraph', 'reference', 'blockquote', 'list' ] 203 | }); 204 | md.renderer.rules.math_inline = inlineRenderer; 205 | md.renderer.rules.math_block = blockRenderer; 206 | } 207 | 208 | module.exports = math_plugin; 209 | -------------------------------------------------------------------------------- /index.mjs: -------------------------------------------------------------------------------- 1 | /* Process inline math */ 2 | /* 3 | Like markdown-it-simplemath, this is a stripped down, simplified version of: 4 | https://github.com/runarberg/markdown-it-math 5 | 6 | It differs in that it takes (a subset of) LaTeX as input and relies on KaTeX 7 | for rendering output. 8 | */ 9 | 10 | /*jslint node: true */ 11 | 'use strict'; 12 | 13 | import katex from 'katex'; 14 | 15 | // Test if potential opening or closing delimieter 16 | // Assumes that there is a "$" at state.src[pos] 17 | function isValidDelim(state, pos) { 18 | var prevChar, nextChar, 19 | max = state.posMax, 20 | can_open = true, 21 | can_close = true; 22 | 23 | prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1; 24 | nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1; 25 | 26 | // Check non-whitespace conditions for opening and closing, and 27 | // check that closing delimeter isn't followed by a number 28 | if (prevChar === 0x20/* " " */ || prevChar === 0x09/* \t */ || 29 | (nextChar >= 0x30/* "0" */ && nextChar <= 0x39/* "9" */)) { 30 | can_close = false; 31 | } 32 | if (nextChar === 0x20/* " " */ || nextChar === 0x09/* \t */) { 33 | can_open = false; 34 | } 35 | 36 | return { 37 | can_open: can_open, 38 | can_close: can_close 39 | }; 40 | } 41 | 42 | function math_inline(state, silent) { 43 | var start, match, token, res, pos, esc_count; 44 | 45 | if (state.src[state.pos] !== "$") { return false; } 46 | 47 | res = isValidDelim(state, state.pos); 48 | if (!res.can_open) { 49 | if (!silent) { state.pending += "$"; } 50 | state.pos += 1; 51 | return true; 52 | } 53 | 54 | // First check for and bypass all properly escaped delimieters 55 | // This loop will assume that the first leading backtick can not 56 | // be the first character in state.src, which is known since 57 | // we have found an opening delimieter already. 58 | start = state.pos + 1; 59 | match = start; 60 | while ( (match = state.src.indexOf("$", match)) !== -1) { 61 | // Found potential $, look for escapes, pos will point to 62 | // first non escape when complete 63 | pos = match - 1; 64 | while (state.src[pos] === "\\") { pos -= 1; } 65 | 66 | // Even number of escapes, potential closing delimiter found 67 | if ( ((match - pos) % 2) == 1 ) { break; } 68 | match += 1; 69 | } 70 | 71 | // No closing delimter found. Consume $ and continue. 72 | if (match === -1) { 73 | if (!silent) { state.pending += "$"; } 74 | state.pos = start; 75 | return true; 76 | } 77 | 78 | // Check if we have empty content, ie: $$. Do not parse. 79 | if (match - start === 0) { 80 | if (!silent) { state.pending += "$$"; } 81 | state.pos = start + 1; 82 | return true; 83 | } 84 | 85 | // Check for valid closing delimiter 86 | res = isValidDelim(state, match); 87 | if (!res.can_close) { 88 | if (!silent) { state.pending += "$"; } 89 | state.pos = start; 90 | return true; 91 | } 92 | 93 | if (!silent) { 94 | token = state.push('math_inline', 'math', 0); 95 | token.markup = "$"; 96 | token.content = state.src.slice(start, match); 97 | } 98 | 99 | state.pos = match + 1; 100 | return true; 101 | } 102 | 103 | function math_block(state, start, end, silent){ 104 | var firstLine, lastLine, next, lastPos, found = false, token, 105 | pos = state.bMarks[start] + state.tShift[start], 106 | max = state.eMarks[start] 107 | 108 | if(pos + 2 > max){ return false; } 109 | if(state.src.slice(pos,pos+2)!=='$$'){ return false; } 110 | 111 | pos += 2; 112 | firstLine = state.src.slice(pos,max); 113 | 114 | if(silent){ return true; } 115 | if(firstLine.trim().slice(-2)==='$$'){ 116 | // Single line expression 117 | firstLine = firstLine.trim().slice(0, -2); 118 | found = true; 119 | } 120 | 121 | for(next = start; !found; ){ 122 | 123 | next++; 124 | 125 | if(next >= end){ break; } 126 | 127 | pos = state.bMarks[next]+state.tShift[next]; 128 | max = state.eMarks[next]; 129 | 130 | if(pos < max && state.tShift[next] < state.blkIndent){ 131 | // non-empty line with negative indent should stop the list: 132 | break; 133 | } 134 | 135 | if(state.src.slice(pos,max).trim().slice(-2)==='$$'){ 136 | lastPos = state.src.slice(0,max).lastIndexOf('$$'); 137 | lastLine = state.src.slice(pos,lastPos); 138 | found = true; 139 | } 140 | 141 | } 142 | 143 | state.line = next + 1; 144 | 145 | token = state.push('math_block', 'math', 0); 146 | token.block = true; 147 | token.content = (firstLine && firstLine.trim() ? firstLine + '\n' : '') 148 | + state.getLines(start + 1, next, state.tShift[start], true) 149 | + (lastLine && lastLine.trim() ? lastLine : ''); 150 | token.map = [ start, state.line ]; 151 | token.markup = '$$'; 152 | return true; 153 | } 154 | 155 | function escapeHtml(unsafe) { 156 | return unsafe 157 | .replace(/&/g, "&") 158 | .replace(//g, ">") 160 | .replace(/"/g, """) 161 | .replace(/'/g, "'"); 162 | } 163 | 164 | function math_plugin(md, options) { 165 | // Default options 166 | 167 | options = options || {}; 168 | 169 | // set KaTeX as the renderer for markdown-it-simplemath 170 | var katexInline = function(latex){ 171 | options.displayMode = false; 172 | try{ 173 | return katex.renderToString(latex, options); 174 | } 175 | catch(error){ 176 | if(options.throwOnError){ console.log(error); } 177 | return `${escapeHtml(latex)}`; 178 | } 179 | }; 180 | 181 | var inlineRenderer = function(tokens, idx){ 182 | return katexInline(tokens[idx].content); 183 | }; 184 | 185 | var katexBlock = function(latex){ 186 | options.displayMode = true; 187 | try{ 188 | return "

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

"; 189 | } 190 | catch(error){ 191 | if(options.throwOnError){ console.log(error); } 192 | return `

${escapeHtml(latex)}

`; 193 | } 194 | } 195 | 196 | var blockRenderer = function(tokens, idx){ 197 | return katexBlock(tokens[idx].content) + '\n'; 198 | } 199 | 200 | md.inline.ruler.after('escape', 'math_inline', math_inline); 201 | md.block.ruler.after('blockquote', 'math_block', math_block, { 202 | alt: [ 'paragraph', 'reference', 'blockquote', 'list' ] 203 | }); 204 | md.renderer.rules.math_inline = inlineRenderer; 205 | md.renderer.rules.math_block = blockRenderer; 206 | }; 207 | 208 | export default math_plugin; 209 | 210 | --------------------------------------------------------------------------------