Skip to content

Commit 440ff42

Browse files
siefkenjdimaMachina
andauthoredOct 14, 2023
MathJax Support (#2440)
Co-authored-by: Dimitri POSTOLOV <dmytropostolov@gmail.com>
1 parent 92ec60e commit 440ff42

File tree

16 files changed

+592
-63
lines changed

16 files changed

+592
-63
lines changed
 

‎.changeset/thin-pandas-relate.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'nextra-theme-blog': minor
3+
'nextra-theme-docs': minor
4+
'nextra': minor
5+
---
6+
7+
add MathJax support

‎.eslintrc.cjs

+4-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ module.exports = {
2424
'plugin:import/typescript',
2525
'prettier'
2626
],
27-
plugins: ['import', 'unicorn'],
27+
plugins: ['import', 'unicorn', 'sonarjs'],
2828
rules: {
2929
'prefer-object-has-own': 'error',
3030
'logical-assignment-operators': [
@@ -56,6 +56,9 @@ module.exports = {
5656
}
5757
],
5858
'prefer-object-spread': 'error',
59+
'prefer-arrow-callback': ['error', { allowNamedFunctions: true }],
60+
'unicorn/prefer-at': 'error',
61+
'sonarjs/no-small-switch': 'error',
5962
// todo: enable
6063
'@typescript-eslint/no-explicit-any': 'off',
6164
'@typescript-eslint/no-non-null-assertion': 'off',
+141-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import { buildDynamicMDX } from 'nextra/remote'
2+
import { Callout, RemoteContent as MathJaxExample, MathJax, MathJaxContext } from 'nextra/components'
3+
14
# LaTeX
25

3-
Nextra uses [KaTeX](https://katex.org/) to render LaTeX expressions directly in MDX.
6+
Nextra can use [KaTeX](https://katex.org) to pre-render LaTeX expressions directly in MDX or [MathJax](https://mathjax.org) to
7+
dynamically render math in the browser.
48
To enable LaTeX support, you must enable the `latex` option in your `next.config.mjs` file:
59

6-
```js filename="next.config.mjs"
10+
```js filename="next.config.mjs" {4}
711
import nextra from 'nextra'
812

913
const withNextra = nextra({
@@ -13,26 +17,42 @@ const withNextra = nextra({
1317
export default withNextra()
1418
```
1519

16-
When enabled, KaTeX’s CSS and fonts will be automatically included in your site, and you can start writing math expressions in your MDX files. Using LaTeX within MDX is as simple as wrapping your expression in `$` or `$$`.
20+
A value of `true{:js}` will use KaTeX as the math renderer. To explicitly specify the renderer, you may instead provide an
21+
object `{ renderer: 'katex' }{:js}` or `{ renderer: 'mathjax' }{:js}` as the value to `latex: ...`.
22+
23+
When enabled, the required CSS and fonts will be automatically included in your site,
24+
and you can start writing math expressions by enclosing inline math in `$...$` or display math in a `math`-labeled fenced code block:
25+
26+
~~~mdx
27+
```math
28+
\int x^2
29+
```
30+
~~~
1731

1832
## Example
1933

2034
For example, the following Markdown code:
2135

22-
```md filename="page.md"
23-
The **Pythagorean equation**: $a=\sqrt{b^2 + c^2}$.
36+
~~~md filename="page.md"
37+
The **Pythagorean equation** is $a=\sqrt{b^2 + c^2}$ and the quadratic formula:
38+
39+
```math
40+
x=\frac{-b\pm\sqrt{b^2-4ac}}{2a}
2441
```
42+
~~~
2543

2644
will be rendered as:
2745

2846
<div className="mt-6 rounded-xl border border-gray-200 p-4 dark:border-gray-900">
29-
The **Pythagorean equation**: $a=\sqrt{b^2 + c^2}$.
47+
The **Pythagorean equation** is $a=\sqrt{b^2 + c^2}$ and the quadratic formula:
48+
49+
```math
50+
x=\frac{-b\pm\sqrt{b^2-4ac}}{2a}
51+
```
3052
</div>
3153

3254
You can still use [Markdown and MDX syntax](../markdown) in the same line as your LaTeX expression.
3355

34-
import { Callout } from 'nextra/components'
35-
3656
<Callout>
3757
If you want to display `$` in your content instead of rendering it as an
3858
equation, you can escape it with a backslash (`\`). For example `\$e = mc^2\$`
@@ -41,4 +61,116 @@ import { Callout } from 'nextra/components'
4161

4262
## API
4363

44-
To learn more about KaTeX and its supported functions and conventions, visit [KaTeX’s documentation](https://katex.org/docs/supported.html).
64+
### KaTeX
65+
66+
`rehype-katex` is used to pre-render LaTeX expressions in your content. You can pass
67+
supported [KaTeX options](https://katex.org/docs/options) via the `options` key in
68+
your Nextra config. For example, to add a macro `\RR` that renders as `\mathbb{R}` you could
69+
use the following configuration.
70+
71+
```js filename="next.config.mjs" {4-8}
72+
const withNextra = nextra({
73+
latex: {
74+
renderer: 'katex',
75+
options: {
76+
macros: {
77+
'\\RR': '\\mathbb{R}'
78+
}
79+
}
80+
}
81+
})
82+
```
83+
84+
See [KaTeX’s documentation](https://katex.org/docs/supported) for a list of supported commands.
85+
86+
### MathJax
87+
88+
When MathJax is enabled (by setting `latex: { renderer: 'mathjax' }{:js}`) math is rendered on page load via
89+
[`better-react-mathjax`](https://github.com/fast-reflexes/better-react-mathjax) instead of being pre-rendered.
90+
By default, **MathJax is served via the MathJax CDN** instead of the files being directly included in your site.[^1]
91+
92+
[^1]: This can be changed by setting [`{ options: { src: ... } }{:js}`](https://github.com/fast-reflexes/better-react-mathjax#src-string--undefined) in the Nextra config.
93+
94+
MathJax rendering is enabled by setting `renderer: 'mathjax'{:js}` in your Nextra config.
95+
96+
```js filename="next.config.mjs" {3}
97+
const withNextra = nextra({
98+
latex: {
99+
renderer: 'mathjax'
100+
}
101+
})
102+
```
103+
104+
You can pass additional options to `better-react-mathjax` via the `options` key in your Nextra config. The `config: ...` option sets the
105+
[MathJax configuration](https://docs.mathjax.org/en/latest/options/index.html). However, note that you can only pass serializable
106+
options to `better-react-mathjax` via the `options` key in your Nextra config.[^2]
107+
108+
[^2]: To pass non-serializable objects like Functions, you must use the `<MathJaxContext config={...} />{:jsx}` component directly in your source.
109+
110+
For example, to configure MathJax to render `\RR` as `\mathbb{R}` you could use the following configuration.
111+
112+
```js filename="next.config.mjs" {4-12}
113+
const withNextra = nextra({
114+
latex: {
115+
renderer: 'mathjax',
116+
options: {
117+
config: {
118+
tex: {
119+
macros: {
120+
RR: '\\mathbb{R}'
121+
}
122+
}
123+
}
124+
}
125+
}
126+
})
127+
```
128+
129+
#### MathJax CDN
130+
131+
By default, MathJax is served via the MathJax CDN. To serve files from another location (including locally in your project), you
132+
must pass the `src: ...` option to the latex config. See the [better-react-mathjax documentation](https://github.com/fast-reflexes/better-react-mathjax#src-string--undefined)
133+
for details about the `src` option. Additionally, you may need to copy the MathJax distribution into your `/public` folder for it to be served locally.
134+
135+
## KaTeX vs. MathJax
136+
137+
With KaTeX, math is pre-rendered which means flicker-free and faster page loads. However, KaTeX does not support all of the features
138+
of MathJax, especially features related to accessibility.
139+
140+
The following two examples show the same formula rendered with KaTeX (first) and MathJax (second).
141+
142+
```math
143+
\int_2^3x^3\,\mathrm{d}x
144+
145+
```
146+
147+
<MathJaxExample components={{ MathJax, MathJaxContext }} />
148+
149+
Because of MathJax's accessibility features, the second formula is tab-accessible and has a context menu that helps screen readers reprocess math for the visually impaired.
150+
151+
152+
export async function getStaticProps() {
153+
const rawMdx = `~~~math
154+
\\int_2^3x^3\\,\\mathrm{d}x
155+
~~~
156+
`
157+
const props = await buildDynamicMDX(
158+
rawMdx,
159+
{
160+
latex: {
161+
renderer: 'mathjax',
162+
options: {
163+
config: {
164+
tex: {
165+
macros: {
166+
RR: '\\mathbb{R}'
167+
}
168+
}
169+
}
170+
}
171+
},
172+
}
173+
)
174+
props.__nextra_dynamic_opts.title = 'LaTeX'
175+
return { props }
176+
}

‎docs/pages/docs/guide/built-ins/cards.mdx

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ ${mdx}
3030
3131
## Usage
3232
33-
\`\`\`mdx filename="MDX"
33+
~~~mdx filename="MDX"
3434
import { Cards } from 'nextra/components'
3535
import { CardsIcon, OneIcon, WarningIcon } from '../../icons'
3636
${mdx}
37-
\`\`\``)
37+
~~~`)
3838
return { props }
3939
}
4040

‎package.json

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"eslint-plugin-import": "2.28.1",
3333
"eslint-plugin-react": "7.33.2",
3434
"eslint-plugin-react-hooks": "4.6.0",
35+
"eslint-plugin-sonarjs": "^0.21.0",
3536
"eslint-plugin-tailwindcss": "3.13.0",
3637
"eslint-plugin-typescript-sort-keys": "3.0.0",
3738
"eslint-plugin-unicorn": "48.0.1",

‎packages/nextra-theme-docs/src/components/anchor.tsx

+30-28
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,40 @@ export type AnchorProps = Omit<ComponentProps<'a'>, 'ref'> & {
77
newWindow?: boolean
88
}
99

10-
export const Anchor = forwardRef<HTMLAnchorElement, AnchorProps>(function (
11-
{ href = '', children, newWindow, ...props },
12-
// ref is used in <NavbarMenu />
13-
forwardedRef
14-
): ReactElement {
15-
if (newWindow) {
16-
return (
17-
<a
18-
ref={forwardedRef}
19-
href={href}
20-
target="_blank"
21-
rel="noreferrer"
22-
{...props}
23-
>
24-
{children}
25-
</a>
26-
)
27-
}
10+
export const Anchor = forwardRef<HTMLAnchorElement, AnchorProps>(
11+
(
12+
{ href = '', children, newWindow, ...props },
13+
// ref is used in <NavbarMenu />
14+
forwardedRef
15+
): ReactElement => {
16+
if (newWindow) {
17+
return (
18+
<a
19+
ref={forwardedRef}
20+
href={href}
21+
target="_blank"
22+
rel="noreferrer"
23+
{...props}
24+
>
25+
{children}
26+
</a>
27+
)
28+
}
29+
30+
if (!href) {
31+
return (
32+
<a ref={forwardedRef} {...props}>
33+
{children}
34+
</a>
35+
)
36+
}
2837

29-
if (!href) {
3038
return (
31-
<a ref={forwardedRef} {...props}>
39+
<NextLink ref={forwardedRef} href={href} {...props}>
3240
{children}
33-
</a>
41+
</NextLink>
3442
)
3543
}
36-
37-
return (
38-
<NextLink ref={forwardedRef} href={href} {...props}>
39-
{children}
40-
</NextLink>
41-
)
42-
})
44+
)
4345

4446
Anchor.displayName = 'Anchor'

‎packages/nextra-theme-docs/src/components/search.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export function Search({
9797
)
9898

9999
const handleKeyDown = useCallback(
100-
function <T>(e: KeyboardEvent<T>) {
100+
<T,>(e: KeyboardEvent<T>) => {
101101
switch (e.key) {
102102
case 'ArrowDown': {
103103
if (active + 1 < results.length) {

‎packages/nextra-theme-docs/src/components/skip-nav.tsx

+5-9
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ type SkipNavLinkProps = Omit<
4242
}
4343

4444
export const SkipNavLink = forwardRef<HTMLAnchorElement, SkipNavLinkProps>(
45-
function (
45+
(
4646
{
4747
className: providedClassName,
4848
id,
@@ -51,7 +51,7 @@ export const SkipNavLink = forwardRef<HTMLAnchorElement, SkipNavLinkProps>(
5151
...props
5252
},
5353
forwardedRef
54-
): ReactElement {
54+
): ReactElement => {
5555
const className =
5656
providedClassName === undefined // Give the option to the user to pass a falsy other than undefined to remove the default styles
5757
? styled // Give the user a way to opt-in the default style provided with the theme. Probably remove this option in the next major version (v3.x) and just do a check to use the providedClassName or the default
@@ -71,23 +71,19 @@ export const SkipNavLink = forwardRef<HTMLAnchorElement, SkipNavLinkProps>(
7171
ref={forwardedRef}
7272
href={`#${id || DEFAULT_ID}`}
7373
className={className}
74-
// TODO: Remove in version v3.x. Must keep for compatibility reasons
75-
data-reach-skip-link=""
7674
>
7775
{label}
7876
</a>
7977
)
8078
}
8179
)
82-
8380
SkipNavLink.displayName = 'SkipNavLink'
8481

8582
type SkipNavContentProps = Omit<ComponentProps<'div'>, 'ref' | 'children'>
8683

8784
export const SkipNavContent = forwardRef<HTMLDivElement, SkipNavContentProps>(
88-
function ({ id, ...props }, forwardedRef): ReactElement {
89-
return <div {...props} ref={forwardedRef} id={id || DEFAULT_ID} />
90-
}
85+
({ id, ...props }, forwardedRef): ReactElement => (
86+
<div {...props} ref={forwardedRef} id={id || DEFAULT_ID} />
87+
)
9188
)
92-
9389
SkipNavContent.displayName = 'SkipNavContent'

‎packages/nextra/__test__/latex.test.ts

+116-4
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ const options = {
66
latex: true
77
}
88

9-
describe('latex', () => {
10-
it('should convert ```math code block language', async () => {
11-
const { result } = await compileMdx('```math\nx^2\n```', options)
12-
expect(clean(result)).resolves.toMatchInlineSnapshot(`
9+
describe('LaTeX', () => {
10+
describe('KaTeX', () => {
11+
it('should convert ```math code block language', async () => {
12+
const { result } = await compileMdx('```math\nx^2\n```', options)
13+
expect(clean(result)).resolves.toMatchInlineSnapshot(`
1314
"const { useMDXComponents: _provideComponents } = arguments[0]
1415
const title = ''
1516
const frontMatter = {}
@@ -101,5 +102,116 @@ describe('latex', () => {
101102
}
102103
"
103104
`)
105+
})
106+
})
107+
108+
describe('MathJax', () => {
109+
const options = {
110+
mdxOptions: { jsx: true, outputFormat: 'program' },
111+
latex: { renderer: 'mathjax' }
112+
} as const
113+
114+
const INLINE_MATH = '$a=\\sqrt{b^2 + c^2}$'
115+
const MATH_LANG = '```math\nx^2\n```'
116+
117+
it('should convert math inline', async () => {
118+
const { result } = await compileMdx(INLINE_MATH, options)
119+
expect(clean(result)).resolves.toMatchInlineSnapshot(`
120+
"import { useMDXComponents as _provideComponents } from 'nextra/mdx'
121+
const title = ''
122+
const frontMatter = {}
123+
import { MathJax, MathJaxContext } from 'nextra/components'
124+
export function useTOC(props) {
125+
return []
126+
}
127+
function MDXLayout(props) {
128+
const _components = Object.assign(
129+
{
130+
p: 'p'
131+
},
132+
_provideComponents(),
133+
props.components
134+
)
135+
return (
136+
<MathJaxContext>
137+
{'\\\\n'}
138+
{'\\\\n'}
139+
<_components.p>
140+
<MathJax inline>{'\\\\\\\\(a=\\\\\\\\sqrt{b^2 + c^2}\\\\\\\\)'}</MathJax>
141+
</_components.p>
142+
</MathJaxContext>
143+
)
144+
}
145+
"
146+
`)
147+
})
148+
149+
it('should convert ```math code block language', async () => {
150+
const { result } = await compileMdx(MATH_LANG, options)
151+
expect(clean(result)).resolves.toMatchInlineSnapshot(`
152+
"import { useMDXComponents as _provideComponents } from 'nextra/mdx'
153+
const title = ''
154+
const frontMatter = {}
155+
import { MathJax, MathJaxContext } from 'nextra/components'
156+
export function useTOC(props) {
157+
return []
158+
}
159+
function MDXLayout(props) {
160+
return (
161+
<MathJaxContext>
162+
{'\\\\n'}
163+
{'\\\\n'}
164+
<MathJax>{'\\\\\\\\[x^2\\\\n\\\\\\\\]'}</MathJax>
165+
</MathJaxContext>
166+
)
167+
}
168+
"
169+
`)
170+
})
171+
172+
it('should add imports only once, and move imports/exports at top', async () => {
173+
const rawMdx = `${INLINE_MATH}
174+
175+
import foo from 'foo'
176+
177+
export let bar
178+
179+
${MATH_LANG}`
180+
const { result } = await compileMdx(rawMdx, options)
181+
expect(clean(result)).resolves.toMatchInlineSnapshot(`
182+
"import { useMDXComponents as _provideComponents } from 'nextra/mdx'
183+
const title = ''
184+
const frontMatter = {}
185+
import foo from 'foo'
186+
export let bar
187+
import { MathJax, MathJaxContext } from 'nextra/components'
188+
export function useTOC(props) {
189+
return []
190+
}
191+
function MDXLayout(props) {
192+
const _components = Object.assign(
193+
{
194+
p: 'p'
195+
},
196+
_provideComponents(),
197+
props.components
198+
)
199+
return (
200+
<MathJaxContext>
201+
{'\\\\n'}
202+
{'\\\\n'}
203+
<_components.p>
204+
<MathJax inline>{'\\\\\\\\(a=\\\\\\\\sqrt{b^2 + c^2}\\\\\\\\)'}</MathJax>
205+
</_components.p>
206+
{'\\\\n'}
207+
{'\\\\n'}
208+
{'\\\\n'}
209+
<MathJax>{'\\\\\\\\[x^2\\\\n\\\\\\\\]'}</MathJax>
210+
</MathJaxContext>
211+
)
212+
}
213+
"
214+
`)
215+
})
104216
})
105217
})

‎packages/nextra/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
"@napi-rs/simple-git": "^0.1.9",
120120
"@theguild/remark-mermaid": "^0.0.5",
121121
"@theguild/remark-npm2yarn": "0.3.0",
122+
"better-react-mathjax": "^2.0.3",
122123
"clsx": "^2.0.0",
123124
"estree-util-to-js": "^2.0.0",
124125
"estree-util-value-to-estree": "^3.0.1",

‎packages/nextra/src/client/components/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ export { Tr } from './tr.js'
1212
export { Cards } from './cards.js'
1313
export { FileTree } from './file-tree.js'
1414
export { Mermaid } from '@theguild/remark-mermaid/mermaid'
15+
export { RemoteContent } from '../data.js'
16+
export { MathJax, MathJaxContext } from 'better-react-mathjax'

‎packages/nextra/src/server/compile.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
remarkStaticImage,
4646
remarkStructurize
4747
} from './mdx-plugins/index.js'
48+
import { rehypeBetterReactMathjax } from './mdx-plugins/rehype-better-react-mathjax.js'
4849
import { rehypeExtractTocContent } from './mdx-plugins/rehype-extract-toc-content.js'
4950
import { logger, truthy } from './utils.js'
5051

@@ -152,6 +153,7 @@ export async function compileMdx(
152153

153154
const format =
154155
_format === 'detect' ? (filePath.endsWith('.mdx') ? 'mdx' : 'md') : _format
156+
155157
const fileCompatible = filePath ? { value: source, path: filePath } : source
156158
if (isPageMapImport) {
157159
const compiler = createProcessor({
@@ -232,7 +234,7 @@ export async function compileMdx(
232234

233235
return {
234236
result,
235-
...(title && { title }),
237+
title,
236238
...(hasJsxInH1 && { hasJsxInH1 }),
237239
...(readingTime && { readingTime }),
238240
...(searchIndexKey !== null && { searchIndexKey, structurizedData }),
@@ -300,7 +302,12 @@ export async function compileMdx(
300302
],
301303
[parseMeta, { defaultShowCopyCode }],
302304
// Should be before `rehypePrettyCode`
303-
latex && rehypeKatex,
305+
latex &&
306+
(typeof latex === 'object'
307+
? latex.renderer === 'mathjax'
308+
? [rehypeBetterReactMathjax, latex.options, isRemoteContent]
309+
: [rehypeKatex, latex.options]
310+
: rehypeKatex),
304311
...(codeHighlight === false
305312
? []
306313
: [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import type { ImportDeclaration } from 'estree'
2+
import { valueToEstree } from 'estree-util-value-to-estree'
3+
import type { Element, Root, RootContent } from 'hast'
4+
import type { MdxJsxAttribute } from 'hast-util-to-estree/lib'
5+
import type { Plugin } from 'unified'
6+
import { visit } from 'unist-util-visit'
7+
import type { MathJaxOptions } from '../../types'
8+
9+
const MATHJAX_IMPORTS = {
10+
type: 'mdxjsEsm',
11+
data: {
12+
estree: {
13+
body: [
14+
{
15+
type: 'ImportDeclaration',
16+
source: { type: 'Literal', value: 'nextra/components' },
17+
specifiers: ['MathJax', 'MathJaxContext'].map(name => ({
18+
type: 'ImportSpecifier',
19+
imported: { type: 'Identifier', name },
20+
local: { type: 'Identifier', name }
21+
}))
22+
} satisfies ImportDeclaration
23+
]
24+
}
25+
}
26+
}
27+
28+
function wrapInMathJaxContext(
29+
children: RootContent[],
30+
{ config, src }: NonNullable<MathJaxOptions>
31+
) {
32+
const attributes: MdxJsxAttribute[] = []
33+
if (src) {
34+
attributes.push({ type: 'mdxJsxAttribute', name: 'src', value: src })
35+
}
36+
if (config && Object.keys(config).length) {
37+
attributes.push({
38+
type: 'mdxJsxAttribute',
39+
name: 'config',
40+
value: {
41+
type: 'mdxJsxAttributeValueExpression',
42+
value: '',
43+
data: {
44+
estree: {
45+
type: 'Program',
46+
sourceType: 'module',
47+
body: [
48+
{ type: 'ExpressionStatement', expression: valueToEstree(config) }
49+
]
50+
}
51+
}
52+
}
53+
})
54+
}
55+
56+
return {
57+
type: 'mdxJsxFlowElement',
58+
name: 'MathJaxContext',
59+
attributes,
60+
children
61+
}
62+
}
63+
64+
/**
65+
* Wrap the math in the appropriate braces. Defaults to `\(...\)` for inline and `\[...\]` for display,
66+
* but will use the braces provided by `options` if they are present.
67+
*/
68+
function wrapInBraces(
69+
source: string,
70+
mathInline: boolean,
71+
options: NonNullable<MathJaxOptions>
72+
): string {
73+
const { inlineMath, displayMath } = options.config?.tex || {}
74+
75+
const inlineBraces = inlineMath?.[0] || ['\\(', '\\)']
76+
const displayBraces = displayMath?.[0] || ['\\[', '\\]']
77+
const [before, after] = mathInline ? inlineBraces : displayBraces
78+
return `${before}${source}${after}`
79+
}
80+
81+
/**
82+
* Wraps math in a `<MathJax>` component so that it can be rendered by `better-react-mathjax`.
83+
*/
84+
export const rehypeBetterReactMathjax: Plugin<
85+
[Opts: MathJaxOptions, isRemoteContent: boolean],
86+
Root
87+
> =
88+
(options = {}, isRemoteContent) =>
89+
ast => {
90+
let hasMathJax = false
91+
92+
visit(ast, { tagName: 'code' }, (node, _index, parent) => {
93+
const classes = Array.isArray(node.properties.className)
94+
? node.properties.className
95+
: []
96+
// This class can be generated from markdown with ` ```math `
97+
const hasMathLanguage = classes.includes('language-math')
98+
if (!hasMathLanguage) return
99+
100+
// This class is used by `remark-math` for text math (inline, `$math$`)
101+
const isInlineMath = classes.includes('math-inline')
102+
103+
const [{ value }] = node.children as any
104+
const bracketedValue = wrapInBraces(value, isInlineMath, options)
105+
106+
const mathJaxNode: Element = {
107+
type: 'element',
108+
tagName: 'MathJax',
109+
children: [{ type: 'text', value: bracketedValue }],
110+
properties: isInlineMath ? { inline: true } : {}
111+
}
112+
113+
Object.assign((isInlineMath ? node : parent) as any, mathJaxNode)
114+
hasMathJax = true
115+
})
116+
117+
if (!hasMathJax) return
118+
119+
const mdxjsEsmNodes = []
120+
const rest = []
121+
for (const child of ast.children) {
122+
if (child.type === ('mdxjsEsm' as any)) {
123+
mdxjsEsmNodes.push(child)
124+
} else {
125+
rest.push(child)
126+
}
127+
}
128+
ast.children = [
129+
...mdxjsEsmNodes,
130+
...(isRemoteContent ? [] : [MATHJAX_IMPORTS]),
131+
// Wrap everything in a `<MathJaxContext />` component.
132+
wrapInMathJaxContext(rest, options)
133+
] as any
134+
}

‎packages/nextra/src/server/schemas.ts

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type { ProcessorOptions } from '@mdx-js/mdx'
2+
import type { MathJax3Config } from 'better-react-mathjax'
3+
import type { Options as RehypeKatexOptions } from 'rehype-katex'
24
import type { Options as RehypePrettyCodeOptions } from 'rehype-pretty-code'
35
import { z } from 'zod'
46
import type { PageOpts } from '../types'
@@ -31,14 +33,38 @@ type Transform = (
3133
}
3234
) => string | Promise<string>
3335

36+
export const mathJaxOptionsSchema = z
37+
.strictObject({
38+
/**
39+
* URL for MathJax. Defaults to `https://cdnjs.cloudflare.com`
40+
*/
41+
src: z.string(),
42+
/**
43+
* MathJax config. See https://docs.mathjax.org/en/latest/options/index.html
44+
*/
45+
config: z.custom<MathJax3Config>()
46+
})
47+
.deepPartial()
48+
.optional()
49+
3450
export const nextraConfigSchema = z
3551
.strictObject({
3652
themeConfig: z.string(),
3753
defaultShowCopyCode: z.boolean(),
3854
search: searchSchema,
3955
staticImage: z.boolean(),
4056
readingTime: z.boolean(),
41-
latex: z.boolean(),
57+
latex: z.union([
58+
z.boolean(),
59+
z.strictObject({
60+
renderer: z.literal('mathjax'),
61+
options: mathJaxOptionsSchema
62+
}),
63+
z.strictObject({
64+
renderer: z.literal('katex'),
65+
options: z.custom<RehypeKatexOptions>()
66+
})
67+
]),
4268
codeHighlight: z.boolean(),
4369
/**
4470
* A function to modify the code of compiled MDX pages.

‎packages/nextra/src/types.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import type { NextConfig } from 'next'
33
import type { FC, ReactNode } from 'react'
44
import type { z } from 'zod'
55
import type { NEXTRA_INTERNAL } from './constants.js'
6-
import type { nextraConfigSchema, searchSchema } from './server/schemas'
6+
import type {
7+
mathJaxOptionsSchema,
8+
nextraConfigSchema,
9+
searchSchema
10+
} from './server/schemas'
711

812
export interface LoaderOptions extends NextraConfig {
913
isPageImport?: boolean
@@ -81,6 +85,8 @@ export type Search = z.infer<typeof searchSchema>
8185

8286
export type NextraConfig = z.infer<typeof nextraConfigSchema>
8387

88+
export type MathJaxOptions = z.infer<typeof mathJaxOptionsSchema>
89+
8490
export type Nextra = (
8591
nextraConfig: NextraConfig
8692
) => (nextConfig: NextConfig) => NextConfig

‎pnpm-lock.yaml

+105-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.