Skip to content

Commit a7658e2

Browse files
authored
Merge pull request #212 from marp-team/math-context-handling
Math plugin: Better context handling for defined macro
2 parents 6fcc426 + 4422ed3 commit a7658e2

File tree

6 files changed

+150
-39
lines changed

6 files changed

+150
-39
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## [Unreleased]
44

5+
### Fixed
6+
7+
- KaTeX: Persist defined global macro between math renderings ([#212](https://github.com/marp-team/marp-core/pull/212))
8+
- MathJax: Prevent leaking defined macro between Markdown renderings ([#212](https://github.com/marp-team/marp-core/pull/212))
9+
510
### Changed
611

712
- Upgrade Marpit to [v1.6.4](https://github.com/marp-team/marpit/releases/v1.6.4) ([#210](https://github.com/marp-team/marp-core/pull/210))

src/math/context.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { MathOptionsInterface } from './math'
2+
3+
type MathContext = {
4+
enabled: boolean
5+
options: MathOptionsInterface
6+
7+
// Library specific contexts
8+
katexMacroContext: Record<string, string>
9+
mathjaxContext: any
10+
}
11+
12+
const contextSymbol = Symbol('marp-math-context')
13+
14+
export const setMathContext = (
15+
target: any,
16+
setter: (current: MathContext) => MathContext
17+
) => {
18+
if (!Object.prototype.hasOwnProperty.call(target, contextSymbol)) {
19+
Object.defineProperty(target, contextSymbol, { writable: true })
20+
}
21+
target[contextSymbol] = setter(target[contextSymbol])
22+
}
23+
24+
export const getMathContext = (target: any): MathContext => ({
25+
...target[contextSymbol],
26+
})

src/math/katex.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
import katex from 'katex'
22
import { version } from 'katex/package.json'
3+
import { getMathContext } from './context'
34
import katexScss from './katex.scss'
45

56
const convertedCSS = Object.create(null)
67
const katexMatcher = /url\(['"]?fonts\/(.*?)['"]?\)/g
78

8-
export const inline = (opts: Record<string, unknown> = {}) => (tokens, idx) => {
9+
export const inline = (marpit: any) => (tokens, idx) => {
910
const { content } = tokens[idx]
11+
const {
12+
options: { katexOption },
13+
katexMacroContext,
14+
} = getMathContext(marpit)
1015

1116
try {
1217
return katex.renderToString(content, {
1318
throwOnError: false,
14-
...opts,
19+
...(katexOption || {}),
20+
macros: katexMacroContext,
1521
displayMode: false,
1622
})
1723
} catch (e) {
@@ -20,13 +26,18 @@ export const inline = (opts: Record<string, unknown> = {}) => (tokens, idx) => {
2026
}
2127
}
2228

23-
export const block = (opts: Record<string, unknown> = {}) => (tokens, idx) => {
29+
export const block = (marpit: any) => (tokens, idx) => {
2430
const { content } = tokens[idx]
31+
const {
32+
options: { katexOption },
33+
katexMacroContext,
34+
} = getMathContext(marpit)
2535

2636
try {
2737
return `<p>${katex.renderToString(content, {
2838
throwOnError: false,
29-
...opts,
39+
...(katexOption || {}),
40+
macros: katexMacroContext,
3041
displayMode: true,
3142
})}</p>`
3243
} catch (e) {

src/math/math.ts

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import marpitPlugin from '@marp-team/marpit/plugin'
2+
import { getMathContext, setMathContext } from './context'
23
import * as katex from './katex'
34
import * as mathjax from './mathjax'
45

5-
interface MathOptionsInterface {
6+
export interface MathOptionsInterface {
67
lib?: 'katex' | 'mathjax'
78
katexOption?: Record<string, unknown>
89
katexFontPath?: string | false
910
}
1011

11-
const contextSymbol = Symbol('marp-math-context')
12-
1312
export type MathOptions =
1413
| boolean
1514
| MathOptionsInterface['lib']
@@ -24,52 +23,70 @@ export const markdown = marpitPlugin((md) => {
2423
? { lib: typeof opts === 'string' ? opts : undefined }
2524
: opts
2625

27-
Object.defineProperty(md.marpit, contextSymbol, { writable: true })
26+
// Initialize
27+
const { parse, parseInline } = md
28+
29+
const initializeMathContext = () =>
30+
setMathContext(md.marpit, () => ({
31+
enabled: false,
32+
options: parsedOpts,
33+
katexMacroContext: {
34+
...((parsedOpts.katexOption?.macros as any) || {}),
35+
},
36+
mathjaxContext: null,
37+
}))
38+
39+
md.parse = function (...args) {
40+
initializeMathContext()
41+
return parse.apply(this, args)
42+
}
2843

29-
md.core.ruler.before('block', 'marp_math_initialize', ({ inlineMode }) => {
30-
if (!inlineMode) md.marpit[contextSymbol] = null
31-
})
44+
md.parseInline = function (...args) {
45+
initializeMathContext()
46+
return parseInline.apply(this, args)
47+
}
48+
49+
const enableMath = () =>
50+
setMathContext(md.marpit, (ctx) => ({ ...ctx, enabled: true }))
3251

3352
// Inline
3453
md.inline.ruler.after('escape', 'marp_math_inline', (state, silent) => {
35-
if (parseInlineMath(state, silent)) {
36-
md.marpit[contextSymbol] = parsedOpts
37-
return true
38-
}
39-
return false
54+
const ret = parseInlineMath(state, silent)
55+
if (ret) enableMath()
56+
57+
return ret
4058
})
4159

4260
// Block
4361
md.block.ruler.after(
4462
'blockquote',
4563
'marp_math_block',
4664
(state, start, end, silent) => {
47-
if (parseMathBlock(state, start, end, silent)) {
48-
md.marpit[contextSymbol] = parsedOpts
49-
return true
50-
}
51-
return false
65+
const ret = parseMathBlock(state, start, end, silent)
66+
if (ret) enableMath()
67+
68+
return ret
5269
},
5370
{ alt: ['paragraph', 'reference', 'blockquote', 'list'] }
5471
)
5572

5673
// Renderer
5774
if (parsedOpts.lib === 'mathjax') {
58-
md.renderer.rules.marp_math_inline = mathjax.inline()
59-
md.renderer.rules.marp_math_block = mathjax.block()
75+
md.renderer.rules.marp_math_inline = mathjax.inline(md.marpit)
76+
md.renderer.rules.marp_math_block = mathjax.block(md.marpit)
6077
} else {
61-
md.renderer.rules.marp_math_inline = katex.inline(parsedOpts.katexOption)
62-
md.renderer.rules.marp_math_block = katex.block(parsedOpts.katexOption)
78+
md.renderer.rules.marp_math_inline = katex.inline(md.marpit)
79+
md.renderer.rules.marp_math_block = katex.block(md.marpit)
6380
}
6481
})
6582

6683
export const css = (marpit: any): string | null => {
67-
const opts: MathOptionsInterface | null = marpit[contextSymbol]
68-
if (!opts) return null
84+
const { enabled, options } = getMathContext(marpit)
85+
if (!enabled) return null
6986

70-
if (opts.lib === 'mathjax') return mathjax.css()
87+
if (options.lib === 'mathjax') return mathjax.css(marpit)
7188

72-
return katex.css(opts.katexFontPath)
89+
return katex.css(options.katexFontPath)
7390
}
7491

7592
function isValidDelim(state, pos = state.pos) {

src/math/mathjax.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@ import { TeX } from 'mathjax-full/js/input/tex'
44
import { AllPackages } from 'mathjax-full/js/input/tex/AllPackages'
55
import { mathjax } from 'mathjax-full/js/mathjax'
66
import { SVG } from 'mathjax-full/js/output/svg'
7+
import { getMathContext, setMathContext } from './context'
78

89
interface MathJaxContext {
910
adaptor: LiteAdaptor
1011
css: string
1112
document: ReturnType<typeof mathjax['document']>
1213
}
1314

14-
let lazyContext: MathJaxContext | undefined
15+
const context = (marpit: any): MathJaxContext => {
16+
let { mathjaxContext } = getMathContext(marpit)
1517

16-
const context = (): MathJaxContext => {
17-
if (!lazyContext) {
18+
if (!mathjaxContext) {
1819
const adaptor = liteAdaptor()
1920
RegisterHTMLHandler(adaptor)
2021

@@ -23,13 +24,15 @@ const context = (): MathJaxContext => {
2324
const document = mathjax.document('', { InputJax: tex, OutputJax: svg })
2425
const css = adaptor.textContent(svg.styleSheet(document) as any)
2526

26-
lazyContext = { adaptor, css, document }
27+
mathjaxContext = { adaptor, css, document }
28+
setMathContext(marpit, (ctx) => ({ ...ctx, mathjaxContext }))
2729
}
28-
return lazyContext
30+
31+
return mathjaxContext
2932
}
3033

31-
export const inline = () => (tokens, idx) => {
32-
const { adaptor, document } = context()
34+
export const inline = (marpit: any) => (tokens, idx) => {
35+
const { adaptor, document } = context(marpit)
3336
const { content } = tokens[idx]
3437

3538
try {
@@ -40,10 +43,10 @@ export const inline = () => (tokens, idx) => {
4043
}
4144
}
4245

43-
export const block = () =>
46+
export const block = (marpit: any) =>
4447
Object.assign(
4548
(tokens, idx) => {
46-
const { adaptor, document } = context()
49+
const { adaptor, document } = context(marpit)
4750
const { content } = tokens[idx]
4851

4952
try {
@@ -66,4 +69,4 @@ export const block = () =>
6669
{ scaled: true }
6770
)
6871

69-
export const css = () => context().css
72+
export const css = (marpit: any) => context(marpit).css

test/marp.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,31 @@ describe('Marp', () => {
347347
expect(katexFonts).toMatchSnapshot('katex-css-cdn')
348348
})
349349

350+
it('has a unique context for macro by Markdown rendering', () => {
351+
const instance = marp()
352+
353+
const plain = cheerio
354+
.load(instance.render('$x^2$').html)('.katex-html')
355+
.html()
356+
357+
// KaTeX can modify macros through \gdef
358+
const globallyDefined = cheerio
359+
.load(instance.render('$\\gdef\\foo{x^2}$ $\\foo$').html)(
360+
'.katex-html'
361+
)
362+
.eq(1)
363+
.html()
364+
365+
expect(globallyDefined).toBe(plain)
366+
367+
// Defined command through \gdef in another rendering cannot use
368+
const notDefined = cheerio
369+
.load(instance.render('$\\foo$').html)('.katex-html')
370+
.html()
371+
372+
expect(notDefined).not.toBe(plain)
373+
})
374+
350375
describe('when math typesetting syntax is not using', () => {
351376
it('does not inject KaTeX css', () =>
352377
expect(marp().render('plain text').css).not.toContain('.katex'))
@@ -440,6 +465,30 @@ describe('Marp', () => {
440465
expect(css).toContain('mjx-container')
441466
})
442467

468+
it('has a unique context for macro by Markdown rendering', () => {
469+
const instance = marp({ math: 'mathjax' })
470+
471+
const plain = cheerio
472+
.load(instance.render('$x^2$').html)('mjx-container')
473+
.html()
474+
475+
const defined = cheerio
476+
.load(instance.render('$\\def\\foo{x^2}$ $\\foo$').html)(
477+
'mjx-container'
478+
)
479+
.eq(1)
480+
.html()
481+
482+
expect(defined).toBe(plain)
483+
484+
// Defined command through \def in another rendering cannot use
485+
const notDefined = cheerio
486+
.load(instance.render('$\\foo$').html)('mjx-container')
487+
.html()
488+
489+
expect(notDefined).not.toBe(plain)
490+
})
491+
443492
describe('when math typesetting syntax is not using', () => {
444493
it('does not inject MathJax css', () =>
445494
expect(

0 commit comments

Comments
 (0)