├── index.js ├── util.js ├── readme.md ├── package.json ├── inline.js └── block.js /index.js: -------------------------------------------------------------------------------- 1 | const inlinePlugin = require('./inline') 2 | const blockPlugin = require('./block') 3 | 4 | module.exports = math 5 | 6 | function math(options) { 7 | var settings = options || {} 8 | blockPlugin.call(this, settings) 9 | inlinePlugin.call(this, settings) 10 | } 11 | -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | exports.isRemarkParser = isRemarkParser 2 | exports.isRemarkCompiler = isRemarkCompiler 3 | 4 | function isRemarkParser(parser) { 5 | return Boolean(parser && parser.prototype && parser.prototype.blockTokenizers) 6 | } 7 | 8 | function isRemarkCompiler(compiler) { 9 | return Boolean(compiler && compiler.prototype && compiler.prototype.visitors) 10 | } 11 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # remark-math 2 | 3 | [**remark**][remark] plugin to parse and stringify math. 4 | 5 | ## Install 6 | 7 | [npm][]: 8 | 9 | ```sh 10 | npm install remark-math 11 | ``` 12 | 13 | ## Use 14 | 15 | Say we have the following file, `example.md`: 16 | 17 | ```markdown 18 | Lift($L$) can be determined by Lift Coefficient ($C_L$) like the following equation. 19 | 20 | $$ 21 | L = \frac{1}{2} \rho v^2 S C_L 22 | $$ 23 | ``` 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remark-math-keep-fence", 3 | "version": "2.0.1", 4 | "description": "remark plugin to parse and stringify math", 5 | "license": "MIT", 6 | "keywords": [ 7 | "unified", 8 | "remark", 9 | "remark-plugin", 10 | "plugin", 11 | "mdast", 12 | "markdown", 13 | "math", 14 | "katex", 15 | "latex", 16 | "tex" 17 | ], 18 | "repository": "https://github.com/skyme5/remark-math-keep-fence", 19 | "bugs": "https://github.com/skyme5/remark-math-keep-fence/issues", 20 | "author": "Aakash Gajjar ", 21 | "files": [ 22 | "index.js", 23 | "block.js", 24 | "inline.js", 25 | "util.js" 26 | ], 27 | "main": "index.js", 28 | "dependencies": {}, 29 | "devDependencies": {}, 30 | "xo": false 31 | } 32 | -------------------------------------------------------------------------------- /inline.js: -------------------------------------------------------------------------------- 1 | var util = require('./util') 2 | 3 | module.exports = mathInline 4 | 5 | const tab = 9 // '\t' 6 | const space = 32 // ' ' 7 | const dollarSign = 36 // '$' 8 | const digit0 = 48 // '0' 9 | const digit9 = 57 // '9' 10 | const backslash = 92 // '\\' 11 | 12 | const classList = ['math', 'math-inline'] 13 | const mathDisplay = 'math-display' 14 | 15 | function mathInline(options) { 16 | const parser = this.Parser 17 | const compiler = this.Compiler 18 | 19 | if (util.isRemarkParser(parser)) { 20 | attachParser(parser, options) 21 | } 22 | 23 | if (util.isRemarkCompiler(compiler)) { 24 | attachCompiler(compiler, options) 25 | } 26 | } 27 | 28 | function attachParser(parser, options) { 29 | const proto = parser.prototype 30 | const inlineMethods = proto.inlineMethods 31 | 32 | mathInlineTokenizer.locator = locator 33 | 34 | proto.inlineTokenizers.math = mathInlineTokenizer 35 | 36 | inlineMethods.splice(inlineMethods.indexOf('text'), 0, 'math') 37 | 38 | function locator(value, fromIndex) { 39 | return value.indexOf('$', fromIndex) 40 | } 41 | 42 | function mathInlineTokenizer(eat, value, silent) { 43 | const length = value.length 44 | let double = false 45 | let escaped = false 46 | let index = 0 47 | let previous 48 | let code 49 | let next 50 | let contentStart 51 | let contentEnd 52 | let valueEnd 53 | let content 54 | 55 | if (value.charCodeAt(index) === backslash) { 56 | escaped = true 57 | index++ 58 | } 59 | 60 | if (value.charCodeAt(index) !== dollarSign) { 61 | return 62 | } 63 | 64 | index++ 65 | 66 | // Support escaped dollars. 67 | if (escaped) { 68 | /* istanbul ignore if - never used (yet) */ 69 | if (silent) { 70 | return true 71 | } 72 | 73 | return eat(value.slice(0, index))({type: 'text', value: '$'}) 74 | } 75 | 76 | if (value.charCodeAt(index) === dollarSign) { 77 | double = true 78 | index++ 79 | } 80 | 81 | next = value.charCodeAt(index) 82 | 83 | // Opening fence cannot be followed by a space or a tab. 84 | if (next === space || next === tab) { 85 | return 86 | } 87 | 88 | contentStart = index 89 | 90 | while (index < length) { 91 | code = next 92 | next = value.charCodeAt(index + 1) 93 | 94 | if (code === dollarSign) { 95 | previous = value.charCodeAt(index - 1) 96 | 97 | // Closing fence cannot be preceded by a space or a tab, or followed by 98 | // a digit. 99 | // If a double marker was used to open, the closing fence must consist 100 | // of two dollars as well. 101 | if ( 102 | previous !== space && 103 | previous !== tab && 104 | // eslint-disable-next-line no-self-compare 105 | (next !== next || next < digit0 || next > digit9) && 106 | (!double || next === dollarSign) 107 | ) { 108 | contentEnd = index - 1 109 | 110 | index++ 111 | 112 | if (double) { 113 | index++ 114 | } 115 | 116 | valueEnd = index 117 | break 118 | } 119 | } else if (code === backslash) { 120 | index++ 121 | next = value.charCodeAt(index + 1) 122 | } 123 | 124 | index++ 125 | } 126 | 127 | if (valueEnd === undefined) { 128 | return 129 | } 130 | 131 | /* istanbul ignore if - never used (yet) */ 132 | if (silent) { 133 | return true 134 | } 135 | 136 | content = value.slice(contentStart, contentEnd + 1) 137 | 138 | return eat(value.slice(0, valueEnd))({ 139 | type: 'inlineMath', 140 | value: content, 141 | data: { 142 | hName: 'span', 143 | hProperties: { 144 | className: classList.concat( 145 | double && options.inlineMathDouble ? [mathDisplay] : [] 146 | ) 147 | }, 148 | hChildren: [{type: 'text', value: `\$${content}\$`}] 149 | } 150 | }) 151 | } 152 | } 153 | 154 | function attachCompiler(compiler) { 155 | const proto = compiler.prototype 156 | 157 | proto.visitors.inlineMath = compileInlineMath 158 | 159 | function compileInlineMath(node) { 160 | let fence = '$' 161 | const classes = 162 | (node.data && node.data.hProperties && node.data.hProperties.className) || 163 | [] 164 | 165 | if (classes.includes(mathDisplay)) { 166 | fence = '$$' 167 | } 168 | 169 | return fence + node.value + fence 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /block.js: -------------------------------------------------------------------------------- 1 | const util = require('./util') 2 | 3 | module.exports = mathBlock 4 | 5 | const lineFeed = 10 // '\n' 6 | const space = 32 // ' ' 7 | const dollarSign = 36 // '$' 8 | 9 | const lineFeedChar = '\n' 10 | const dollarSignChar = '$' 11 | 12 | const minFenceCount = 2 13 | 14 | const classList = ['math', 'math-display'] 15 | 16 | function mathBlock() { 17 | const parser = this.Parser 18 | const compiler = this.Compiler 19 | 20 | if (util.isRemarkParser(parser)) { 21 | attachParser(parser) 22 | } 23 | 24 | if (util.isRemarkCompiler(compiler)) { 25 | attachCompiler(compiler) 26 | } 27 | } 28 | 29 | function attachParser(parser) { 30 | const proto = parser.prototype 31 | const blockMethods = proto.blockMethods 32 | const interruptParagraph = proto.interruptParagraph 33 | const interruptList = proto.interruptList 34 | const interruptBlockquote = proto.interruptBlockquote 35 | 36 | proto.blockTokenizers.math = mathBlockTokenizer 37 | 38 | blockMethods.splice(blockMethods.indexOf('fencedCode') + 1, 0, 'math') 39 | 40 | // Inject math to interrupt rules 41 | interruptParagraph.splice(interruptParagraph.indexOf('fencedCode') + 1, 0, [ 42 | 'math' 43 | ]) 44 | interruptList.splice(interruptList.indexOf('fencedCode') + 1, 0, ['math']) 45 | interruptBlockquote.splice(interruptBlockquote.indexOf('fencedCode') + 1, 0, [ 46 | 'math' 47 | ]) 48 | 49 | function mathBlockTokenizer(eat, value, silent) { 50 | var length = value.length 51 | var index = 0 52 | let code 53 | let content 54 | let lineEnd 55 | let lineIndex 56 | let openingFenceIndentSize 57 | let openingFenceSize 58 | let openingFenceContentStart 59 | let closingFence 60 | let closingFenceSize 61 | let lineContentStart 62 | let lineContentEnd 63 | 64 | // Skip initial spacing. 65 | while (index < length && value.charCodeAt(index) === space) { 66 | index++ 67 | } 68 | 69 | openingFenceIndentSize = index 70 | 71 | // Skip the fence. 72 | while (index < length && value.charCodeAt(index) === dollarSign) { 73 | index++ 74 | } 75 | 76 | openingFenceSize = index - openingFenceIndentSize 77 | 78 | // Exit if there is not enough of a fence. 79 | if (openingFenceSize < minFenceCount) { 80 | return 81 | } 82 | 83 | // Skip spacing after the fence. 84 | while (index < length && value.charCodeAt(index) === space) { 85 | index++ 86 | } 87 | 88 | openingFenceContentStart = index 89 | 90 | // Eat everything after the fence. 91 | while (index < length) { 92 | code = value.charCodeAt(index) 93 | 94 | // We don’t allow dollar signs here, as that could interfere with inline 95 | // math. 96 | if (code === dollarSign) { 97 | return 98 | } 99 | 100 | if (code === lineFeed) { 101 | break 102 | } 103 | 104 | index++ 105 | } 106 | 107 | if (value.charCodeAt(index) !== lineFeed) { 108 | return 109 | } 110 | 111 | if (silent) { 112 | return true 113 | } 114 | 115 | content = [] 116 | 117 | if (openingFenceContentStart !== index) { 118 | content.push(value.slice(openingFenceContentStart, index)) 119 | } 120 | 121 | index++ 122 | lineEnd = value.indexOf(lineFeedChar, index + 1) 123 | lineEnd = lineEnd === -1 ? length : lineEnd 124 | 125 | while (index < length) { 126 | closingFence = false 127 | lineContentStart = index 128 | lineContentEnd = lineEnd 129 | lineIndex = lineEnd 130 | closingFenceSize = 0 131 | 132 | // First, let’s see if this is a valid closing fence. 133 | // Skip trailing white space 134 | while ( 135 | lineIndex > lineContentStart && 136 | value.charCodeAt(lineIndex - 1) === space 137 | ) { 138 | lineIndex-- 139 | } 140 | 141 | // Skip the fence. 142 | while ( 143 | lineIndex > lineContentStart && 144 | value.charCodeAt(lineIndex - 1) === dollarSign 145 | ) { 146 | closingFenceSize++ 147 | lineIndex-- 148 | } 149 | 150 | // Check if this is a valid closing fence line. 151 | if ( 152 | openingFenceSize <= closingFenceSize && 153 | value.indexOf(dollarSignChar, lineContentStart) === lineIndex 154 | ) { 155 | closingFence = true 156 | lineContentEnd = lineIndex 157 | } 158 | 159 | // Sweet, next, we need to trim the line. 160 | // Skip initial spacing. 161 | while ( 162 | lineContentStart <= lineContentEnd && 163 | lineContentStart - index < openingFenceIndentSize && 164 | value.charCodeAt(lineContentStart) === space 165 | ) { 166 | lineContentStart++ 167 | } 168 | 169 | // If this is a closing fence, skip final spacing. 170 | if (closingFence) { 171 | while ( 172 | lineContentEnd > lineContentStart && 173 | value.charCodeAt(lineContentEnd - 1) === space 174 | ) { 175 | lineContentEnd-- 176 | } 177 | } 178 | 179 | // If this is a content line, or if there is content before the fence: 180 | if (!closingFence || lineContentStart !== lineContentEnd) { 181 | content.push(value.slice(lineContentStart, lineContentEnd)) 182 | } 183 | 184 | if (closingFence) { 185 | break 186 | } 187 | 188 | index = lineEnd + 1 189 | lineEnd = value.indexOf(lineFeedChar, index + 1) 190 | lineEnd = lineEnd === -1 ? length : lineEnd 191 | } 192 | 193 | content = content.join('\n') 194 | 195 | return eat(value.slice(0, lineEnd))({ 196 | type: 'math', 197 | value: content, 198 | data: { 199 | hName: 'div', 200 | hProperties: {className: classList.concat()}, 201 | hChildren: [{type: 'text', value: `\$\$${content}\$\$`}] 202 | } 203 | }) 204 | } 205 | } 206 | 207 | function attachCompiler(compiler) { 208 | const proto = compiler.prototype 209 | 210 | proto.visitors.math = compileBlockMath 211 | 212 | function compileBlockMath(node) { 213 | return '$$\n' + node.value + '\n$$' 214 | } 215 | } 216 | --------------------------------------------------------------------------------