├── .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 |
68 |
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
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 |
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
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 |
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
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 |
--------------------------------------------------------------------------------