Skip to content

Commit 67b46a2

Browse files
committed
Provide a handy option for custom-element renderers
1 parent 025bac2 commit 67b46a2

File tree

5 files changed

+214
-44
lines changed

5 files changed

+214
-44
lines changed

README.md

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ use the default math renderer.
101101

102102
## Usage
103103

104-
```javascript
104+
```js
105105
import markdownIt from "markdown-it";
106106
import markdownItMath from "markdown-it-math";
107107

@@ -111,16 +111,17 @@ const options = {
111111
inlineClose: "$",
112112
blockOpen: "$$",
113113
blockClose: "$$",
114-
defaultRendererOptions: {},
115-
inlineRenderer: (src) => mathup(src, defaultRendererOptions).toString(),
116-
blockRenderer: (src) =>
117-
mathup({ ...defaultRendererOptions, display: "block" }).toString(),
114+
defaultRendererOptions,
115+
inlineCustomElement, // see below
116+
inlineRenderer, // see below
117+
blockCustomElement, // see below
118+
blockRenderer, // see below
118119
};
119120

120121
const md = markdownIt().use(markdownItMath, options);
121122
```
122123

123-
```javascript
124+
```js
124125
md.render(`
125126
A text $1+1=2$ with math.
126127
@@ -134,11 +135,49 @@ $$
134135

135136
(See [mathup][mathup] for reference about the default renderer).
136137

138+
### Options
139+
140+
- `inlineOpen`: The delimiter to start an inline math expression. Default `$`
141+
- `inlineClose`: The delimiter to close an inline math expression. Default `$`
142+
- `blockOpen`: The delimiter to start a block math expression. Default `$$`
143+
- `blockClose`: The delimiter to close a block math expression. Default `$$`
144+
- `defaultRendererOptions`:
145+
The options passed into the default renderer. Ignored if you use a custom renderer. Default `{}`
146+
- `inlineCustomElement`:
147+
Specify `"tag-name"` or `["tag-name", { some: "attrs" }]` if you want to
148+
render inline expressions to a custom element. Ignored if you provide a
149+
custom renderer.
150+
- `inlineRenderer`:
151+
Provide a custom inline math renderer. Accepts the source content, the
152+
parsed markdown-it token, and the markdown-it instance. Default:
153+
```js
154+
import mathup from "mathup";
155+
function defaultInlineRenderer(src, token, md) {
156+
return mathup(src, defaultRendererOptions).toString();
157+
}
158+
```
159+
- `blockCustomElement`:
160+
Specify `"tag-name"` or `["tag-name", { some: "attrs" }]` if you want to
161+
render block expressions to a custom element. Ignored if you provide a
162+
custom renderer.
163+
- `blockRenderer`:
164+
Provide a custom block math renderer. Accepts the source content, the
165+
parsed markdown-it token, and the markdown-it instance. Default:
166+
```js
167+
import mathup from "mathup";
168+
function defaultBlockRenderer(src, token, md) {
169+
return mathup(src, {
170+
...defaultRendererOptions,
171+
display: "block",
172+
}).toString();
173+
}
174+
```
175+
137176
## Examples
138177

139178
Using comma as a decimal mark
140179

141-
```javascript
180+
```js
142181
import markdownIt from "markdown-it";
143182
import markdownItMath from "markdown-it-math";
144183

@@ -150,9 +189,30 @@ md.render("$40,2$");
150189
// <p><math><mn>40,2</mn></math></p>
151190
```
152191

192+
Render to a custom `<la-tex>` element
193+
194+
```js
195+
import markdownIt from "markdown-it";
196+
import markdownItMath from "markdown-it-math";
197+
198+
const md = markdownIt().use(markdownItMath, {
199+
inlineCustomElement: "la-tex",
200+
blockCustomElement: ["la-tex", { display: "block" }],
201+
});
202+
203+
md.render(String.raw`
204+
$\sin(2\pi)$.
205+
$$
206+
\int_{0}^{\infty} E[X]
207+
$$
208+
`);
209+
// <p><la-tex>\sin(2\pi)</la-tex>.</p>
210+
// <la-tex display="block">\int_{0}^{\infty} E[X]</la-tex>
211+
```
212+
153213
Using [TeXZilla][texzilla] as renderer
154214

155-
```javascript
215+
```js
156216
import markdownIt from "markdown-it";
157217
import markdownItMath from "markdown-it-math";
158218
import texzilla from "texzilla";
@@ -166,9 +226,9 @@ md.render("$\\sin(2\\pi)$");
166226
// <p><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo lspace="0em" rspace="0em">sin</mo><mo stretchy="false">(</mo><mn>2</mn><mi>π</mi><mo stretchy="false">)</mo></mrow><annotation encoding="TeX">\sin(2\pi)</annotation></semantics></math></p>
167227
```
168228

169-
Using LaTeX style delimiters
229+
Using LaTeX style delimiters:
170230

171-
```javascript
231+
```js
172232
import markdownIt from "markdown-it";
173233
import markdownItMath from "markdown-it-math";
174234

eslint.config.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,14 @@ export default [
8383
"no-throw-literal": "error",
8484
"no-unmodified-loop-condition": "error",
8585
"no-unused-expressions": "error",
86-
"no-unused-vars": ["error", { varsIgnorePattern: "^_" }],
86+
"no-unused-vars": [
87+
"error",
88+
{
89+
varsIgnorePattern: "^_",
90+
argsIgnorePattern: "^_",
91+
reportUsedIgnorePattern: true,
92+
},
93+
],
8794
"no-useless-backreference": "error",
8895
"no-useless-concat": "error",
8996
"no-useless-return": "error",

index.js

Lines changed: 68 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/**
22
* @typedef {import("mathup").Options} MathupOptions
3+
* @typedef {import("markdown-it").default} MarkdownIt
34
* @typedef {import("markdown-it/lib/parser_block.mjs").RuleBlock} RuleBlock
45
* @typedef {import("markdown-it/lib/parser_inline.mjs").RuleInline} RuleInline
56
* @typedef {import("markdown-it/lib/rules_block/state_block.mjs").default} StateBlock
@@ -203,11 +204,12 @@ function createBlockMathRule(open, close) {
203204

204205
const token = state.push("math_block", "math", 0);
205206
token.block = true;
206-
token.content =
207-
(firstLine && firstLine.trim() ? `${firstLine}\n` : "") +
208-
state.getLines(startLine + 1, nextLine, len, true) +
209-
(lastLine && lastLine.trim() ? lastLine : "");
210207

208+
const firstLineContent = firstLine && firstLine.trim() ? firstLine : "";
209+
const contentLines = state.getLines(startLine + 1, nextLine, len, false);
210+
const lastLineContent = lastLine && lastLine.trim() ? lastLine : "";
211+
212+
token.content = `${firstLineContent}${firstLineContent && (contentLines || lastLineContent) ? "\n" : ""}${contentLines}${contentLines && lastLineContent ? "\n" : ""}${lastLineContent}`;
211213
token.map = [startLine, state.line];
212214
token.markup = open;
213215

@@ -216,56 +218,93 @@ function createBlockMathRule(open, close) {
216218
}
217219

218220
/**
219-
* @param {MathupOptions} [options]
221+
* @typedef {string | [tag: string, attrs?: Record<string, string>]} CustomElementOption
222+
* @param {CustomElementOption} customElementOption
223+
* @param {MarkdownIt} md
220224
* @returns {(src: string) => string}
221225
*/
222-
function defaultInlineRenderer(options) {
226+
function createCustomElementRenderer(customElementOption, md) {
227+
const { escapeHtml } = md.utils;
228+
229+
/** @type {string} */
230+
let tag;
231+
let attrs = "";
232+
if (typeof customElementOption === "string") {
233+
tag = customElementOption;
234+
} else {
235+
const [tagName, attrsObj = {}] = customElementOption;
236+
tag = tagName;
237+
238+
for (const [key, value] of Object.entries(attrsObj)) {
239+
attrs += ` ${key}="${escapeHtml(value)}"`;
240+
}
241+
}
242+
243+
return (src) => `<${tag}${attrs}>${escapeHtml(src)}</${tag}>`;
244+
}
245+
246+
/**
247+
* @param {MathupOptions} options
248+
* @param {MarkdownIt} md
249+
* @returns {(src: string) => string}
250+
*/
251+
function defaultInlineRenderer(options, md) {
223252
if (!mathup) {
224-
return (src) => `<span class="math inline">${src}</span>`;
253+
return createCustomElementRenderer(["span", { class: "math inline" }], md);
225254
}
226255

227256
return (src) => mathup(src, options).toString();
228257
}
229258

230259
/**
231-
* @param {MathupOptions} [options]
260+
* @param {MathupOptions} options
261+
* @param {MarkdownIt} md
232262
* @returns {(src: string) => string}
233263
*/
234-
function defaultBlockRenderer(options = {}) {
264+
function defaultBlockRenderer(options, md) {
235265
if (!mathup) {
236-
return (src) => `<div class="math block">${src}</div>`;
266+
return createCustomElementRenderer(["div", { class: "math block" }], md);
237267
}
238268

239-
return (src) =>
240-
mathup(src.trim(), { ...options, display: "block" }).toString();
269+
return (src) => mathup(src, { ...options, display: "block" }).toString();
241270
}
242271

243272
/**
244-
* @typedef {Record<string, string>} AttrsOption
273+
* @callback Renderer
274+
* @param {string} src - The source content
275+
* @param {Token} token - The parsed markdown-it token
276+
* @param {MarkdownIt} md - The markdown-it instance
245277
* @typedef {object} PluginOptions
246-
* @property {string} [inlineOpen]
247-
* @property {string} [inlineClose]
248-
* @property {(src: string, token: Token) => string} [inlineRenderer]
249-
* @property {string} [blockOpen]
250-
* @property {string} [blockClose]
251-
* @property {(src: string, token: Token) => string} [blockRenderer]
252-
* @property {import("mathup").Options} [defaultRendererOptions]
253-
* @typedef {import("markdown-it").PluginWithOptions<PluginOptions>} Plugin
278+
* @property {string} [inlineOpen] - Inline math open delimiter.
279+
* @property {string} [inlineClose] - Inline math close delimeter.
280+
* @property {CustomElementOption} [inlineCustomElement] - If you want to render to a custom element.
281+
* @property {MathupOptions} [defaultRendererOptions] - The options passed into the default renderer.
282+
* @property {Renderer} [inlineRenderer] - Custom renderer for inline math. Default mathup.
283+
* @property {string} [blockOpen] - Block math open delimter.
284+
* @property {string} [blockClose] - Block math close delimter.
285+
* @property {CustomElementOption} [blockCustomElement] - If you want to render to a custom element.
286+
* @property {Renderer} [blockRenderer] - Custom renderer for block math. Default mathup with display = "block".
254287
*/
255288

256-
/**
257-
* @type {Plugin}
258-
*/
289+
/** @type {import("markdown-it").PluginWithOptions<PluginOptions>} */
259290
export default function markdownItMath(
260291
md,
261292
{
262293
inlineOpen = "$",
263294
inlineClose = "$",
264295
blockOpen = "$$",
265296
blockClose = "$$",
266-
defaultRendererOptions,
267-
inlineRenderer = defaultInlineRenderer(defaultRendererOptions),
268-
blockRenderer = defaultBlockRenderer(defaultRendererOptions),
297+
defaultRendererOptions = {},
298+
299+
inlineCustomElement,
300+
inlineRenderer = inlineCustomElement
301+
? createCustomElementRenderer(inlineCustomElement, md)
302+
: defaultInlineRenderer(defaultRendererOptions, md),
303+
304+
blockCustomElement,
305+
blockRenderer = blockCustomElement
306+
? createCustomElementRenderer(blockCustomElement, md)
307+
: defaultBlockRenderer(defaultRendererOptions, md),
269308
} = {},
270309
) {
271310
const inlineMathRule = createInlineMathRule(inlineOpen, inlineClose);
@@ -277,8 +316,8 @@ export default function markdownItMath(
277316
});
278317

279318
md.renderer.rules.math_inline = (tokens, idx) =>
280-
inlineRenderer(tokens[idx].content, tokens[idx]);
319+
inlineRenderer(tokens[idx].content, tokens[idx], md);
281320

282321
md.renderer.rules.math_block = (tokens, idx) =>
283-
`${blockRenderer(tokens[idx].content, tokens[idx])}\n`;
322+
`${blockRenderer(tokens[idx].content, tokens[idx], md)}\n`;
284323
}

test/test.js

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { suite, test } from "node:test";
1+
import assert from "node:assert/strict";
2+
import { mock, suite, test } from "node:test";
23

34
import markdownIt from "markdown-it";
5+
import Token from "markdown-it/lib/token.mjs";
46
import texzilla from "texzilla";
57

68
import markdownItMath from "../index.js";
@@ -286,4 +288,66 @@ $$`;
286288
t.assert.snapshot(md.render("$$\n\\sin(2\\pi)\n$$"));
287289
});
288290
});
291+
292+
suite("renderer", () => {
293+
test("Custom renderer", () => {
294+
const md = markdownIt().use(markdownItMath, {
295+
inlineRenderer: (src) => `<inline-math>${src}</inline-math>`,
296+
blockRenderer: (src) => `<block-math>${src}</block-math>`,
297+
});
298+
299+
const res = md.render("$foo$\n$$\nbar\n$$");
300+
301+
assert.equal(
302+
res,
303+
"<p><inline-math>foo</inline-math></p>\n<block-math>bar</block-math>\n",
304+
);
305+
});
306+
307+
test("Correct arguments passed into renderer", () => {
308+
const inlineRenderer = mock.fn((_src, _token, _md) => "");
309+
const blockRenderer = mock.fn((_src, _token, _md) => "");
310+
const md = markdownIt().use(markdownItMath, {
311+
inlineRenderer,
312+
blockRenderer,
313+
});
314+
315+
md.render("$foo$\n$$\nbar\n$$");
316+
317+
{
318+
const [firstCall] = inlineRenderer.mock.calls;
319+
assert.ok(firstCall);
320+
321+
const args = firstCall.arguments;
322+
323+
assert.equal(args[0], "foo");
324+
assert.equal(args[1] instanceof Token, true);
325+
assert.equal(args[2], md);
326+
}
327+
{
328+
const [firstCall] = blockRenderer.mock.calls;
329+
assert.ok(firstCall);
330+
331+
const args = firstCall.arguments;
332+
333+
assert.equal(args[0], "bar");
334+
assert.equal(args[1] instanceof Token, true);
335+
assert.equal(args[2], md);
336+
}
337+
});
338+
339+
test("customElement", () => {
340+
const md = markdownIt().use(markdownItMath, {
341+
inlineCustomElement: "my-el",
342+
blockCustomElement: ["my-el", { some: "attr" }],
343+
});
344+
345+
const res = md.render("$foo$\n$$\nbar\n$$");
346+
347+
assert.equal(
348+
res,
349+
'<p><my-el>foo</my-el></p>\n<my-el some="attr">bar</my-el>\n',
350+
);
351+
});
352+
});
289353
});

test/test.js.snapshot

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ exports[`Block Math > Can span multiple lines 1`] = `
2727
`;
2828

2929
exports[`Block Math > Multiline block math 1`] = `
30-
"<math display=\\"block\\"><mn>1</mn><mspace depth=\\"1em\\" /><mo>+</mo><mn>1</mn><mspace depth=\\"2em\\" /><mo>=</mo><mn>2</mn></math>\\n"
30+
"<math display=\\"block\\"><mspace depth=\\"1em\\" /><mspace width=\\"0.35ex\\" /><mn>1</mn><mspace depth=\\"1em\\" /><mo>+</mo><mn>1</mn><mspace depth=\\"2em\\" /><mo>=</mo><mn>2</mn><mspace depth=\\"1em\\" /></math>\\n"
3131
`;
3232

3333
exports[`Block Math > Multiline math that might look like an unordered list 1`] = `
34-
"<math display=\\"block\\"><mn>1</mn><mspace depth=\\"2em\\" /><mo>+</mo><mn>1</mn><mspace depth=\\"1em\\" /><mo>+</mo><mn>1</mn><mspace depth=\\"2em\\" /><mo>=</mo><mn>3</mn></math>\\n"
34+
"<math display=\\"block\\"><mspace width=\\"0.35ex\\" /><mn>1</mn><mspace depth=\\"2em\\" /><mo>+</mo><mn>1</mn><mspace depth=\\"1em\\" /><mo>+</mo><mn>1</mn><mspace depth=\\"2em\\" /><mo>=</mo><mn>3</mn></math>\\n"
3535
`;
3636

3737
exports[`Block Math > Paragraph breaks around delims are not required 1`] = `
@@ -111,7 +111,7 @@ exports[`Options > Thick dollar delims 1`] = `
111111
`;
112112

113113
exports[`Options > Use TexZilla as renderer > block 1`] = `
114-
"<math display=\\"block\\" xmlns=\\"http://www.w3.org/1998/Math/MathML\\"><semantics><mrow><mo lspace=\\"0em\\" rspace=\\"0em\\">sin</mo><mo stretchy=\\"false\\">(</mo><mn>2</mn><mi>π</mi><mo stretchy=\\"false\\">)</mo></mrow><annotation encoding=\\"TeX\\">\\\\sin(2\\\\pi)\\n</annotation></semantics></math>\\n"
114+
"<math display=\\"block\\" xmlns=\\"http://www.w3.org/1998/Math/MathML\\"><semantics><mrow><mo lspace=\\"0em\\" rspace=\\"0em\\">sin</mo><mo stretchy=\\"false\\">(</mo><mn>2</mn><mi>π</mi><mo stretchy=\\"false\\">)</mo></mrow><annotation encoding=\\"TeX\\">\\\\sin(2\\\\pi)</annotation></semantics></math>\\n"
115115
`;
116116

117117
exports[`Options > Use TexZilla as renderer > inline 1`] = `

0 commit comments

Comments
 (0)