├── .babelrc ├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── Math.js ├── example.mdx └── render.js ├── jest.config.js ├── package.json ├── src ├── index.ts ├── remarkMdxMathEnhanced.spec.ts └── remarkMdxMathEnhanced.ts ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ], 11 | "@babel/preset-typescript" 12 | ], 13 | "plugins": [ 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['12.x', '14.x'] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - name: Test 26 | run: yarn test --ci --coverage --maxWorkers=2 27 | 28 | - name: Build 29 | run: yarn build 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | dist 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Matt Vague 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remark MDX math enhanced 2 | 3 | > An MDX plugin adding support for math environments with embedded JS expressions 4 | 5 | ## What is this? 6 | 7 | This package allows math environments in MDX documents to contain embedded JavaScript expressions analogous to [MDX expressions](https://mdxjs.com/docs/what-is-mdx/#expressions). These expressions have full access to props, exports, etc. 8 | 9 | ## How it works 10 | 11 | Math nodes produced by [remark-math](https://github.com/remarkjs/remark-math/tree/main/packages/remark-math) are transformed into JSX element nodes at compile time and rendered at run time via a React component which your app is expected to provide (default is `Math` but is configurable) 12 | 13 | --- 14 | 15 | **🚨 Important:** This plugin is quite new and currently still in beta, it's possible the API and/or approach may change so **use at your own risk**. 16 | 17 | --- 18 | 19 | 20 | ## Notes 21 | 22 | - This plugin expects you to define your own `Math` component which will handle rendering. For an example implementation of a `` component using [Katex](http://katex.org) see [examples/Math.js](https://github.com/goodproblems/remark-mdx-math-enhanced/tree/master/examples/Math.js) 23 | 24 | - Rendering math at runtime instead of at compile time means that client-side JS is required, and that more browser processing power is required for rendering. Accordingly, this plugin should only be used in cases where dynamic math (i.e. math with JS expressions inside) is actually required 25 | 26 | ## Install 27 | 28 | Install with npm `npm install remark-mdx-math-enhanced` 29 | 30 | ## Use 31 | 32 | Say we have the following .mdx file where we want to render some math with a generated value of pi times a prop value 33 | 34 | ```mdx 35 | export const pi = Math.PI 36 | 37 | $\js{props.N}\pi = \js{props.N * pi}$ 38 | 39 | $$ 40 | \js{props.N}\pi = \js{props.N * pi} 41 | $$ 42 | ``` 43 | 44 | And an MDX setup something like this 45 | 46 | ```js 47 | import { readFileSync } from 'fs' 48 | 49 | import remarkMath from 'remark-math' 50 | import remarkMdxEnhanced from 'remark-mdx-math-enhanced' 51 | import { compileSync } from '@mdx-js/mdx' 52 | 53 | const { value } = compileSync(readFileSync('example.mdx'), { 54 | remarkPlugins: [remarkMath, [remarkMdxEnhanced, { component: 'Math' }]] 55 | }) 56 | 57 | console.log(value) 58 | ``` 59 | 60 | Will result in something like 61 | 62 | ```jsx 63 | export const pi = Math.PI 64 | 65 | export default function MDXContent(props) { 66 | return <> 67 | {String.raw`${props.N}\pi = ${props.N * pi}`} 68 | {String.raw`${props.N}\pi = ${props.N * pi}`} 69 | 70 | } 71 | ``` 72 | 73 | Note how `\js{...}` have been replaced by `${...}` which are valid [string interpolation placeholders](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#string_interpolation). 74 | 75 | 76 | ## API 77 | 78 | The default export is `remarkMdxMathEnhanced`. 79 | 80 | ### `unified().use(remarkMdx).use(remarkMath).use(remarkMdxMathEnhanced[, options])` 81 | 82 | Plugin to transform math nodes to JSX element nodes which render math at run time 83 | 84 | ##### `options` 85 | 86 | Configuration (optional). 87 | 88 | ###### `options.component` 89 | 90 | Name of react component which will be used to render math, default is 'Math' 91 | 92 | ###### `options.startDelimiter` 93 | 94 | Start delimiter of JS expressions, default is `\js{` 95 | 96 | ###### `options.endDelimiter` 97 | 98 | Start delimiter of JS expressions, default is `}` 99 | -------------------------------------------------------------------------------- /examples/Math.js: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import katex from 'katex'; 3 | 4 | export function Math({ children = '', display = false, options }) { 5 | const Wrapper = display ? 'div' : 'span'; 6 | if (typeof children !== 'string') 7 | throw new Error('Children prop must be a katex string'); 8 | 9 | const renderedKatex = useMemo(() => { 10 | let result; 11 | 12 | try { 13 | result = katex.renderToString(children, { 14 | ...options, 15 | displayMode: display, 16 | throwOnError: true, 17 | globalGroup: true, 18 | trust: true, 19 | strict: false, 20 | }); 21 | } catch (error) { 22 | console.error(error); 23 | result = katex.renderToString(children, { 24 | ...options, 25 | displayMode: display, 26 | throwOnError: false, 27 | strict: 'ignore', 28 | globalGroup: true, 29 | trust: true, 30 | }); 31 | } 32 | 33 | return result; 34 | }, [children]); 35 | 36 | return ; 37 | } 38 | -------------------------------------------------------------------------------- /examples/example.mdx: -------------------------------------------------------------------------------- 1 | export const pi = Math.PI 2 | 3 | $\js{props.N}\pi = \js{props.N * pi}$ 4 | 5 | $$ 6 | \js{props.N}\pi = \js{props.N * pi} 7 | $$ -------------------------------------------------------------------------------- /examples/render.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs' 2 | 3 | import remarkMath from 'remark-math' 4 | import remarkMdxEnhanced from '../dist' 5 | import { compileSync } from '@mdx-js/mdx' 6 | 7 | const mdx = readFileSync('./examples/example.mdx').toString() 8 | 9 | const { value } = compileSync(mdx, { 10 | remarkPlugins: [remarkMath, [remarkMdxEnhanced]] 11 | }) 12 | 13 | console.log(value) -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: 'ts-jest/presets/js-with-babel-esm', 3 | extensionsToTreatAsEsm: ['.ts', '.tsx', '.jsx'], 4 | globals: { 5 | 'ts-jest': { 6 | useESM: true, 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.1-beta.3", 3 | "name": "remark-mdx-math-enhanced", 4 | "license": "MIT", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "type": "module", 8 | "files": [ 9 | "dist", 10 | "src" 11 | ], 12 | "engines": { 13 | "node": ">=12" 14 | }, 15 | "scripts": { 16 | "build": "rm -rf dist && tsc", 17 | "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest", 18 | "size": "size-limit", 19 | "analyze": "size-limit --why" 20 | }, 21 | "peerDependencies": { 22 | "react": ">=16" 23 | }, 24 | "prettier": { 25 | "printWidth": 80, 26 | "semi": true, 27 | "singleQuote": true, 28 | "trailingComma": "es5" 29 | }, 30 | "author": "Matt Vague", 31 | "devDependencies": { 32 | "@babel/core": "^7.17.9", 33 | "@babel/preset-env": "^7.17.10", 34 | "@babel/preset-typescript": "^7.16.7", 35 | "@mdx-js/mdx": "^2.1.1", 36 | "@size-limit/preset-small-lib": "^7.0.8", 37 | "@types/estree-jsx": "^0.0.1", 38 | "@types/jest": "^27.4.1", 39 | "@types/mdast": "^3.0.10", 40 | "@types/react": "^18.0.6", 41 | "@types/react-dom": "^18.0.2", 42 | "babel-jest": "^27.5.1", 43 | "babel-loader": "^8.2.5", 44 | "cross-env": "^7.0.3", 45 | "eslint-plugin-prettier": "^4.0.0", 46 | "hast": "^1.0.0", 47 | "husky": "^7.0.4", 48 | "jest": "^27.5.1", 49 | "mdast": "^3.0.0", 50 | "remark": "^14.0.2", 51 | "remark-math": "^5.1.1", 52 | "remark-mdx": "^2.1.1", 53 | "remark-parse": "^10.0.1", 54 | "remark-stringify": "^10.0.2", 55 | "size-limit": "^7.0.8", 56 | "ts-jest": "^27.1.4", 57 | "tslib": "^2.4.0", 58 | "typescript": "^4.6.3", 59 | "unified": "^10.1.2", 60 | "unist-builder": "^3.0.0", 61 | "unist-util-remove-position": "^4.0.1" 62 | }, 63 | "dependencies": { 64 | "acorn": "^8.7.0", 65 | "unist-util-visit": "^4.1.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import plugin from './remarkMdxMathEnhanced' 2 | 3 | export default plugin 4 | -------------------------------------------------------------------------------- /src/remarkMdxMathEnhanced.spec.ts: -------------------------------------------------------------------------------- 1 | import { unified } from 'unified'; 2 | import { Parser } from 'acorn'; 3 | import { u } from 'unist-builder'; 4 | import remarkMath from 'remark-math'; 5 | import remarkParse from 'remark-parse'; 6 | import remarkMdx from 'remark-mdx'; 7 | import remarkStringify from 'remark-stringify'; 8 | import remarkMdxMathEnhancedPlugin from './remarkMdxMathEnhanced'; 9 | import { removePosition } from 'unist-util-remove-position'; 10 | 11 | describe('remarkMdxMathEnhancedPlugin', () => { 12 | it('should compile inline katex to HTML', () => { 13 | expect( 14 | unified() 15 | .use(remarkParse) 16 | .use(remarkMath) 17 | .use(remarkMdx) 18 | .use(remarkMdxMathEnhancedPlugin) 19 | .use(remarkStringify) 20 | .processSync(String.raw`Hey this is math $\frac{a}{b}$`) 21 | .toString() 22 | ).toEqual( 23 | String.raw`Hey this is math {\frac{a}{b}} 24 | ` 25 | ); 26 | }); 27 | 28 | it('should compile display katex to HTML', () => { 29 | expect( 30 | unified() 31 | .use(remarkParse) 32 | .use(remarkMath) 33 | .use(remarkMdx) 34 | .use(remarkMdxMathEnhancedPlugin) 35 | .use(remarkStringify) 36 | .processSync( 37 | String.raw` 38 | Hey this is math 39 | 40 | $$ 41 | \frac{a}{b} 42 | $$` 43 | ) 44 | .toString() 45 | ).toEqual( 46 | String.raw`Hey this is math 47 | 48 | 49 | {\frac{a}{b}} 50 | 51 | ` 52 | ); 53 | }); 54 | 55 | it('should compile inline katex with JS expressions to HTML', () => { 56 | expect( 57 | unified() 58 | .use(remarkParse) 59 | .use(remarkMath) 60 | .use(remarkMdx) 61 | .use(remarkMdxMathEnhancedPlugin) 62 | .use(remarkStringify) 63 | .processSync( 64 | String.raw`Hey this is math with JS $\pi = \js{Math.PI}$` 65 | ) 66 | .toString() 67 | ).toEqual( 68 | `Hey this is math with JS {\\pi = $\{Math.PI\}} 69 | ` 70 | ); 71 | }); 72 | 73 | it('should compile display katex with JS expressions', () => { 74 | expect( 75 | unified() 76 | .use(remarkParse) 77 | .use(remarkMath) 78 | .use(remarkMdx) 79 | .use(remarkMdxMathEnhancedPlugin) 80 | .use(remarkStringify) 81 | .processSync( 82 | String.raw`Hey this is math with JS 83 | 84 | $$ 85 | \pi = \js{Math.PI} 86 | $$ 87 | ` 88 | ) 89 | .toString() 90 | ).toEqual( 91 | `Hey this is math with JS 92 | 93 | 94 | {\\pi = $\{Math.PI\}} 95 | 96 | ` 97 | ); 98 | }); 99 | 100 | it('should parse simple JS expressions', () => { 101 | expect( 102 | unified() 103 | .use(remarkParse) 104 | .use(remarkMdxMathEnhancedPlugin) 105 | .runSync( 106 | removePosition( 107 | unified() 108 | .use(remarkParse) 109 | .use(remarkMath) 110 | .parse( 111 | String.raw` 112 | $\pi = \js{Math.PI}$ 113 | 114 | $$ 115 | \pi = \js{Math.PI} 116 | $$ 117 | ` 118 | ), 119 | true 120 | ) 121 | ) 122 | ).toEqual( 123 | u('root', [ 124 | u('paragraph', [ 125 | u('mdxJsxTextElement', { 126 | name: 'Math', 127 | attributes: [], 128 | children: [ 129 | { 130 | type: 'mdxTextExpression', 131 | value: '\\pi = ${Math.PI}', 132 | data: { 133 | estree: Parser.parse('String.raw`\\pi = ${Math.PI}`', { 134 | ecmaVersion: 'latest', 135 | sourceType: 'module', 136 | }), 137 | }, 138 | }, 139 | ], 140 | }), 141 | ]), 142 | u('mdxJsxFlowElement', { 143 | name: 'Math', 144 | attributes: [ 145 | { 146 | type: 'mdxJsxAttribute', 147 | name: 'display', 148 | }, 149 | ], 150 | children: [ 151 | { 152 | type: 'mdxFlowExpression', 153 | value: '\\pi = ${Math.PI}', 154 | data: { 155 | estree: Parser.parse('String.raw`\\pi = ${Math.PI}`', { 156 | ecmaVersion: 'latest', 157 | sourceType: 'module', 158 | }), 159 | }, 160 | }, 161 | ], 162 | }), 163 | ]) 164 | ); 165 | }); 166 | 167 | it('should parse JS expressions with nested curlies', () => { 168 | expect( 169 | unified() 170 | .use(remarkParse) 171 | .use(remarkMdxMathEnhancedPlugin) 172 | .runSync( 173 | removePosition( 174 | unified() 175 | .use(remarkParse) 176 | .use(remarkMath) 177 | .parse( 178 | String.raw` 179 | $\pi = \js{myFunc({ a: 10 })}$ 180 | ` 181 | ), 182 | true 183 | ) 184 | ) 185 | ).toEqual( 186 | u('root', [ 187 | u('paragraph', [ 188 | u('mdxJsxTextElement', { 189 | name: 'Math', 190 | attributes: [], 191 | children: [ 192 | { 193 | type: 'mdxTextExpression', 194 | value: '\\pi = ${myFunc({ a: 10 })}', 195 | data: { 196 | estree: Parser.parse('String.raw`\\pi = ${myFunc({ a: 10 })}`', { 197 | ecmaVersion: 'latest', 198 | sourceType: 'module', 199 | }), 200 | }, 201 | }, 202 | ], 203 | }), 204 | ]) 205 | ]) 206 | ); 207 | }); 208 | 209 | it('should parse JS expressions with string matching expression marker', () => { 210 | expect( 211 | unified() 212 | .use(remarkParse) 213 | .use(remarkMdxMathEnhancedPlugin) 214 | .runSync( 215 | removePosition( 216 | unified() 217 | .use(remarkParse) 218 | .use(remarkMath) 219 | .parse( 220 | String.raw` 221 | $\js{"\js{\js{1 + 1}}"}$ 222 | ` 223 | ), 224 | true 225 | ) 226 | ) 227 | ).toEqual( 228 | u('root', [ 229 | u('paragraph', [ 230 | u('mdxJsxTextElement', { 231 | name: 'Math', 232 | attributes: [], 233 | children: [ 234 | { 235 | type: 'mdxTextExpression', 236 | value: '${"\\js{\\js{1 + 1}}"}', 237 | data: { 238 | estree: Parser.parse('String.raw`${"\\js{\\js{1 + 1}}"}`', { 239 | ecmaVersion: 'latest', 240 | sourceType: 'module', 241 | }), 242 | }, 243 | }, 244 | ], 245 | }), 246 | ]) 247 | ]) 248 | ); 249 | }); 250 | 251 | it('should not match expressionMarker without a following curly', () => { 252 | expect( 253 | unified() 254 | .use(remarkParse) 255 | .use(remarkMdxMathEnhancedPlugin) 256 | .runSync( 257 | removePosition( 258 | unified() 259 | .use(remarkParse) 260 | .use(remarkMath) 261 | .parse( 262 | String.raw` 263 | $\pi = \js$ 264 | ` 265 | ), 266 | true 267 | ) 268 | ) 269 | ).toEqual( 270 | u('root', [ 271 | u('paragraph', [ 272 | u('mdxJsxTextElement', { 273 | name: 'Math', 274 | attributes: [], 275 | children: [ 276 | { 277 | type: 'mdxTextExpression', 278 | value: '\\pi = \\js', 279 | data: { 280 | estree: Parser.parse('String.raw`\\pi = \\js`', { 281 | ecmaVersion: 'latest', 282 | sourceType: 'module', 283 | }), 284 | }, 285 | }, 286 | ], 287 | }), 288 | ]) 289 | ]) 290 | ); 291 | }); 292 | 293 | it('should blow up with unclosed js expressions', () => { 294 | expect(() => 295 | unified() 296 | .use(remarkParse) 297 | .use(remarkMath) 298 | .use(remarkMdx) 299 | .use(remarkMdxMathEnhancedPlugin) 300 | .use(remarkStringify) 301 | .processSync( 302 | String.raw`Hey this is math with JS 303 | 304 | $$\pi = \js{Math.PI$$ 305 | ` 306 | ) 307 | .toString() 308 | ).toThrowError() 309 | }); 310 | 311 | it('should allow custom component name', () => { 312 | expect( 313 | unified() 314 | .use(remarkParse) 315 | .use(remarkMath) 316 | .use(remarkMdx, {}) 317 | .use(remarkMdxMathEnhancedPlugin, { 318 | component: 'CustomMath' 319 | } as any) 320 | .use(remarkStringify) 321 | .processSync( 322 | String.raw`Hey this is math with JS $\pi = \js{Math.PI}$` 323 | ) 324 | .toString() 325 | ).toEqual( 326 | `Hey this is math with JS {\\pi = $\{Math.PI\}} 327 | ` 328 | ); 329 | }); 330 | 331 | it('should allow custom expressionMarker', () => { 332 | expect( 333 | unified() 334 | .use(remarkParse) 335 | .use(remarkMath) 336 | .use(remarkMdx) 337 | .use(remarkMdxMathEnhancedPlugin, { 338 | startDelimiter: '[[', 339 | endDelimiter: ']]' 340 | } as any) 341 | .use(remarkStringify) 342 | .processSync( 343 | String.raw`Hey this is math with JS $\pi = [[Math.PI]]$` 344 | ) 345 | .toString() 346 | ).toEqual( 347 | `Hey this is math with JS {\\pi = $\{Math.PI\}} 348 | ` 349 | ); 350 | }); 351 | }); 352 | -------------------------------------------------------------------------------- /src/remarkMdxMathEnhanced.ts: -------------------------------------------------------------------------------- 1 | /** @typedef {import('remark-math')} */ 2 | 3 | import { visit } from 'unist-util-visit'; 4 | import { Parser } from 'acorn'; 5 | import type { Root } from 'mdast'; 6 | import type { Program } from 'estree-jsx'; 7 | 8 | const DEFAULT_OPTIONS = { 9 | component: 'Math', 10 | startDelimiter: '\\js{', 11 | endDelimiter: '}', 12 | }; 13 | 14 | export type Options = { 15 | component?: string 16 | startDelimiter?: string 17 | endDelimiter?: string 18 | }; 19 | 20 | /** 21 | * Plugin to transform math nodes to JSX element nodes which render math at run time 22 | * 23 | * @param options 24 | * @param options.component - Name of react component to transform remark math nodes to (which will render math) 25 | * @param options.startDelimiter - Start delimiter of JS expressions, default is `\js{` 26 | * @param options.endDelimiter - End delimiter of JS expressions, default is `}` 27 | */ 28 | export default function remarkMdxMathEnhancedPlugin(options?: Options) { 29 | const { component, startDelimiter, endDelimiter } = { ...DEFAULT_OPTIONS, ...options }; 30 | 31 | return (tree: Root) => { 32 | visit(tree, (node, index, parent) => { 33 | if (node.type === 'math') { 34 | const transformedMath = transformToTemplateString( 35 | node.value, 36 | startDelimiter, 37 | endDelimiter 38 | ); 39 | const estree = transformMathToEstree(transformedMath); 40 | 41 | parent.children.splice(index, 1, { 42 | type: 'mdxJsxFlowElement', 43 | name: component, 44 | attributes: [ 45 | { 46 | type: 'mdxJsxAttribute', 47 | name: 'display', 48 | }, 49 | ], 50 | children: [ 51 | { 52 | type: 'mdxFlowExpression', 53 | value: transformedMath, 54 | data: { 55 | estree, 56 | }, 57 | }, 58 | ], 59 | }); 60 | } 61 | 62 | if (node.type === 'inlineMath') { 63 | const transformedMath = transformToTemplateString( 64 | node.value, 65 | startDelimiter, 66 | endDelimiter 67 | ); 68 | const estree = transformMathToEstree(transformedMath); 69 | 70 | parent.children.splice(index, 1, { 71 | type: 'mdxJsxTextElement', 72 | name: component, 73 | attributes: [], 74 | children: [ 75 | { 76 | type: 'mdxTextExpression', 77 | value: transformedMath, 78 | data: { 79 | estree, 80 | }, 81 | }, 82 | ], 83 | }); 84 | } 85 | }); 86 | }; 87 | 88 | 89 | /** 90 | * Parse the the contents of a Math node into ESTree 91 | */ 92 | function transformMathToEstree(string: string) { 93 | return Parser.parse(`String.raw\`${string}\``, { 94 | ecmaVersion: 'latest', 95 | sourceType: 'module', 96 | }) as unknown as Program; // acorn types are messed... 97 | } 98 | } 99 | 100 | /** 101 | * Parses string for JS expressions delimited by startDelimiter and endDelimiter 102 | * and wraps them in `${...}` to return a valid template string 103 | */ 104 | function transformToTemplateString( 105 | string: string, 106 | startDelimiter: string, 107 | endDelimiter: string 108 | ) { 109 | return tokenize(string).join(''); 110 | 111 | function readToken(input, i) { 112 | const patterns = [ 113 | ['startDelimiter', new RegExp(`^${escapeDelimiter(startDelimiter)}`)], 114 | ['endDelimiter', new RegExp(`^${escapeDelimiter(endDelimiter)}`)], 115 | ['other', /^[\s\S]/], 116 | ]; 117 | 118 | for (let j = 0; j < patterns.length; j++) { 119 | let regex = patterns[j][1]; 120 | let result = input.slice(i).match(regex); 121 | 122 | if (result !== null) { 123 | let text = result[0]; 124 | let token = [patterns[j][0], text]; 125 | return [token, i + text.length]; 126 | } 127 | } 128 | 129 | throw new Error(`No pattern matched ${input.slice(i)}`); 130 | } 131 | 132 | function tokenize(string) { 133 | let tokens = []; 134 | let state: 'math' | 'js' = 'math'; 135 | 136 | for (let i = 0; i < string.length; ) { 137 | let result = readToken(string, i); 138 | let token = result[0]; 139 | 140 | if (token[0] === 'startDelimiter') { 141 | if (state === 'math') { 142 | state = 'js'; 143 | tokens = [...tokens, '${']; 144 | } else { 145 | tokens = [...tokens, token[1]]; 146 | } 147 | } else if (token[0] === 'endDelimiter') { 148 | state = 'math'; 149 | tokens = [...tokens, '}']; 150 | } else { 151 | tokens = [...tokens, token[1]]; 152 | } 153 | 154 | i = result[1]; 155 | } 156 | 157 | return tokens; 158 | } 159 | 160 | // Escape special characters from delimiter for use in regex 161 | function escapeDelimiter(delimiter: string) { 162 | return delimiter.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src" 4 | ], 5 | "exclude": [ 6 | "**/*.(spec|test).ts", 7 | "node_modules", 8 | "dist" 9 | ], 10 | "compilerOptions": { 11 | "outDir": "dist", 12 | "types": [ 13 | "jest", 14 | "mdast-util-mdx-jsx", 15 | "mdast-util-mdx-expression", 16 | "mdast" 17 | ], 18 | "downlevelIteration": true, 19 | "incremental": true, 20 | "emitDecoratorMetadata": true, 21 | "allowSyntheticDefaultImports": true, 22 | "allowJs": true, 23 | "noImplicitAny": false, 24 | "noImplicitThis": false, 25 | "noImplicitReturns": false, 26 | "strictNullChecks": false, 27 | "experimentalDecorators": true, 28 | "target": "ESNext", 29 | "module": "ESNext", 30 | "moduleResolution": "node", 31 | "esModuleInterop": true, 32 | "skipLibCheck": true, 33 | "forceConsistentCasingInFileNames": true, 34 | "resolveJsonModule": true, 35 | "declaration": true, 36 | // Ensure that Babel can safely transpile files in the TypeScript project 37 | "isolatedModules": true 38 | }, 39 | } 40 | --------------------------------------------------------------------------------