Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add math directive for switching typesetting library #246

Merged
merged 7 commits into from
Jul 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added

- `math` global directive for switching math typesetting library in current Markdown ([#243](https://github.com/marp-team/marp-core/issues/243), [#246](https://github.com/marp-team/marp-core/pull/246))

### Changed

- Upgrade dependent packages to the latest version ([#241](https://github.com/marp-team/marp-core/pull/241))
Expand Down
40 changes: 34 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,33 @@ $$
</tbody>
</table>

You can choose a library for math from [KaTeX](https://khan.github.io/KaTeX/) and [MathJax](https://www.mathjax.org/) in [the constructor option](#constructor-options). By default, we prefer KaTeX for compatibility and performance, but MathJax has better syntax support than KaTeX.
You can choose using library for math from [KaTeX](https://khan.github.io/KaTeX/) and [MathJax](https://www.mathjax.org/) in [`math` global directive](#math-global-directive) (or [JS constructor option](#math-constructor-option)). By default, we prefer KaTeX for compatibility and performance, but MathJax has better rendering and syntax support than KaTeX.

#### `math` global directive

Through `math` global directive, Marp Core is supporting to declare math library that will be used within current Markdown.

Set **`katex`** or **`mathjax`** in the `math` global directive like this:

```markdown
---
# Declare to use MathJax in this Markdown
math: mathjax
---

$$
\begin{align}
x &= 1+1 \tag{1} \\
&= 2
\end{align}
$$
```

If not declared, Marp Core will use the default library to render math (KaTeX in v2).

We may change the default in the future and would break existing slides, so recommend to declare the library whenever to use math typesetting.

> :warning: The declaration of math library is given priority over [`math` JS constructor option](#math-constructor-option), but you cannot turn on again via `math` global directive if disabled math typesetting by the constructor.

### Auto-scaling features

Expand Down Expand Up @@ -253,18 +279,20 @@ Setting about emoji conversions.

> **For developers:** When you setting `unicode` option as `true`, Markdown parser will convert Unicode emoji into tokens internally. The rendering result is same as in `false`.

### `math`: _`boolean` | `"katex"` | `"mathjax"` | `object`_
### `math`: _`boolean` | `"katex"` | `"mathjax"` | `object`_ <a name="math-constructor-option" id="math-constructor-option"></a>

Enable or disable [math typesetting](#math-typesetting) syntax and [`math` global directive](#math-global-directive).

Enable or disable [math typesetting](#math-typesetting) syntax. You can choose a library for math by passing **`katex`** (default) or **`mathjax`**, and modify more settings by passing an object of sub-options.
You can choose the default library for math by passing **`"katex"`** (default) or **`"mathjax"`**, and modify more settings by passing an object of sub-options.

- **`lib`**: _`"katex"` | `"mathjax"`_
- Choose a library for math typesetting. _(`katex` by default)_
- Choose the default library for math typesetting. _(`katex` by default)_

* **`katexOption`**: _`object`_
- The options passing to KaTeX. Please refer to [KaTeX document](https://khan.github.io/KaTeX/docs/options.html).
- Options that will be passed to KaTeX. Please refer to [KaTeX document](https://khan.github.io/KaTeX/docs/options.html).

- **`katexFontPath`**: _`string` | `false`_
- By default, marp-core will use [online web-font resources through jsDelivr CDN](https://cdn.jsdelivr.net/npm/katex@latest/dist/fonts/). You have to set path to fonts directory if you want to use local resources. If you set `false`, we will not manipulate the path (Use KaTeX's original path: `fonts/KaTeX_***-***.woff2`).
- By default, Marp Core will use [online web-font resources through jsDelivr CDN](https://cdn.jsdelivr.net/npm/katex@latest/dist/fonts/). You have to set path to fonts directory if you want to use local resources. If you set `false`, we will not manipulate the path (Use KaTeX's original path: `fonts/KaTeX_***-***.woff2`).

### `minifyCSS`: _`boolean`_

Expand Down
15 changes: 10 additions & 5 deletions src/fitting/fitting.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import marpitPlugin from '@marp-team/marpit/plugin'
import { Marp } from '../marp'
import { getMathContext } from '../math/context'
import { attr, code, math, svgContentAttr, svgContentWrapAttr } from './data'
import fittingCSS from './fitting.scss'

Expand Down Expand Up @@ -98,15 +99,19 @@ function fittingHeader(md): void {
: ''
}

function fittingMathBlock(md): void {
function fittingKaTeXMathBlock(md): void {
const { marp_math_block } = md.renderer.rules
if (!marp_math_block || marp_math_block.scaled) return
if (!marp_math_block) return

md.renderer.rules.marp_math_block = (...args) => {
// Rendered math block is wrapped by `<p>` tag in math plugin
// Rendered math block is wrapped by `<p>` tag in KaTeX
const rendered: string = marp_math_block(...args)

if (isEnabledAutoScaling(md.marpit, 'math')) {
const {
options: { lib },
} = getMathContext(md.marpit)

if (lib === 'katex' && isEnabledAutoScaling(md.marpit, 'math')) {
const katex = rendered.slice(3, -4)

if (md.marpit.options.inlineSVG) {
Expand All @@ -125,5 +130,5 @@ function fittingMathBlock(md): void {
}

export const markdown = marpitPlugin((md) => {
md.use(fittingHeader).use(fittingCode).use(fittingMathBlock)
md.use(fittingHeader).use(fittingCode).use(fittingKaTeXMathBlock)
})
69 changes: 53 additions & 16 deletions src/math/math.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,45 @@
import marpitPlugin from '@marp-team/marpit/plugin'
import { Marp } from '../marp'
import { getMathContext, setMathContext } from './context'
import * as katex from './katex'
import * as mathjax from './mathjax'

export type MathPreferredLibrary = 'katex' | 'mathjax'

export interface MathOptionsInterface {
lib?: 'katex' | 'mathjax'
lib?: MathPreferredLibrary
katexOption?: Record<string, unknown>
katexFontPath?: string | false
}

export type MathOptions =
| boolean
| MathOptionsInterface['lib']
| MathOptionsInterface
export type MathOptions = boolean | MathPreferredLibrary | MathOptionsInterface

export const markdown = marpitPlugin((md) => {
const opts: MathOptions | undefined = md.marpit.options.math
const marp: Marp = md.marpit
const opts: MathOptions | undefined = marp.options.math

if (!opts) return

const parsedOpts =
typeof opts !== 'object'
? { lib: typeof opts === 'string' ? opts : undefined }
: opts

// Define `math` global directive to choose preferred library
Object.defineProperty(marp.customDirectives.global, 'math', {
value: (math: unknown): { math?: MathPreferredLibrary } => {
if (math === 'katex' || math === 'mathjax') return { math }
return {}
},
})

// Initialize
const { parse, parseInline } = md

const initializeMathContext = () => {
if (getMathContext(md.marpit).processing) return false
if (getMathContext(marp).processing) return false

setMathContext(md.marpit, () => ({
setMathContext(marp, () => ({
enabled: false,
options: parsedOpts,
processing: true,
Expand All @@ -52,7 +62,7 @@ export const markdown = marpitPlugin((md) => {
return func.apply(this, args)
} finally {
if (initialized) {
setMathContext(md.marpit, (ctx) => ({ ...ctx, processing: false }))
setMathContext(marp, (ctx) => ({ ...ctx, processing: false }))
}
}
}
Expand All @@ -62,7 +72,7 @@ export const markdown = marpitPlugin((md) => {
md.parseInline = parseWithMath(parseInline)

const enableMath = () =>
setMathContext(md.marpit, (ctx) => ({ ...ctx, enabled: true }))
setMathContext(marp, (ctx) => ({ ...ctx, enabled: true }))

// Inline
md.inline.ruler.after('escape', 'marp_math_inline', (state, silent) => {
Expand All @@ -86,13 +96,38 @@ export const markdown = marpitPlugin((md) => {
)

// Renderer
if (parsedOpts.lib === 'mathjax') {
md.renderer.rules.marp_math_inline = mathjax.inline(md.marpit)
md.renderer.rules.marp_math_block = mathjax.block(md.marpit)
} else {
md.renderer.rules.marp_math_inline = katex.inline(md.marpit)
md.renderer.rules.marp_math_block = katex.block(md.marpit)
md.core.ruler.after(
'marpit_directives_global_parse',
'marp_math_directive',
() => {
const { enabled } = getMathContext(marp)
if (!enabled) return

const preffered: MathPreferredLibrary | undefined = (marp as any)
.lastGlobalDirectives.math

setMathContext(marp, (ctx) => ({
...ctx,
options: {
...ctx.options,

// TODO: Change the default math library from `katex` to `mathjax` in the next major version
lib: preffered ?? parsedOpts.lib ?? 'katex',
},
}))
}
)

const getPreferredLibrary = () => {
const { options } = getMathContext(marp)
return options.lib === 'mathjax' ? mathjax : katex
}

const getRenderer = (type: 'inline' | 'block') => (tokens: any, idx: any) =>
getPreferredLibrary()[type](marp)(tokens, idx)

md.renderer.rules.marp_math_inline = getRenderer('inline')
md.renderer.rules.marp_math_block = getRenderer('block')
})

export const css = (marpit: any): string | null => {
Expand All @@ -104,6 +139,8 @@ export const css = (marpit: any): string | null => {
return katex.css(options.katexFontPath)
}

// ---

function isValidDelim(state, pos = state.pos) {
const ret = { openable: true, closable: true }
const { posMax, src } = state
Expand Down
47 changes: 47 additions & 0 deletions test/marp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,53 @@ describe('Marp', () => {
expect(notDefined).not.toBe(plain)
})

describe('math global directive', () => {
it('allows to switch rendering library from katex to mathjax', () => {
const instance = marp({ math: 'katex' })
const { html, css } = instance.render(
`<!-- math: mathjax -->\n\n${inline}\n\n${block}`
)
const $ = cheerio.load(html)

expect($('.MathJax')).toHaveLength(2)
expect($('.katex')).not.toHaveLength(2)

expect(css).toContain('mjx-container')
expect(css).not.toContain('.katex')
})

it('allows to switch rendering library from mathjax to katex', () => {
const instance = marp({ math: 'mathjax' })
const { html, css } = instance.render(
`<!-- math: katex -->\n\n${inline}\n\n${block}`
)
const $ = cheerio.load(html)

expect($('.MathJax')).not.toHaveLength(2)
expect($('.katex')).toHaveLength(2)

expect(css).not.toContain('mjx-container')
expect(css).toContain('.katex')
})

it('ignores if defined unknown keyword', () => {
const katex = marp({ math: 'katex' })

for (const keyword of ['unknown', 'false', 'true']) {
const katexRendered = katex.render(
`<!-- math: ${keyword} -->\n\n${inline}`
)
const $katex = cheerio.load(katexRendered.html)

expect($katex('.MathJax')).not.toHaveLength(1)
expect($katex('.katex')).toHaveLength(1)

expect(katexRendered.css).not.toContain('mjx-container')
expect(katexRendered.css).toContain('.katex')
}
})
})

describe('when math typesetting syntax is not using', () => {
it('does not inject MathJax css', () =>
expect(
Expand Down
11 changes: 11 additions & 0 deletions test/math/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ describe('markdown-it math plugin', () => {
const md = new MarkdownIt()

md.marpit = { options: { math: true } }

// Mock Marpit instance
md.use(() => {
Object.assign(md.marpit, {
customDirectives: { global: {} },
lastGlobalDirectives: {},
})
md.core.ruler.push('marpit_directives_global_parse', () => {
// no ops
})
})
md.use(mathPlugin)

it('renders simple inline math', () => {
Expand Down