diff --git a/.changeset/angry-years-drum.md b/.changeset/angry-years-drum.md new file mode 100644 index 0000000000..7b3411f32a --- /dev/null +++ b/.changeset/angry-years-drum.md @@ -0,0 +1,5 @@ +--- +"nextra": patch +--- + +Add LaTeX support diff --git a/examples/docs/next.config.js b/examples/docs/next.config.js index 06cbbcfb45..07f81dde3b 100644 --- a/examples/docs/next.config.js +++ b/examples/docs/next.config.js @@ -2,6 +2,7 @@ const withNextra = require('nextra')({ theme: 'nextra-theme-docs', themeConfig: './src/theme.config.js', staticImage: true, + latex: true, flexsearch: { codeblock: false } diff --git a/examples/docs/src/pages/features/_meta.json b/examples/docs/src/pages/features/_meta.json index 369452f003..32fcc7e745 100644 --- a/examples/docs/src/pages/features/_meta.json +++ b/examples/docs/src/pages/features/_meta.json @@ -3,5 +3,6 @@ "ssg": "Next.js SSG", "i18n": "Next.js i18n", "image": "Next.js Image", - "themes": "Themes" + "themes": "Themes", + "latex": "LaTeX" } diff --git a/examples/docs/src/pages/features/latex.mdx b/examples/docs/src/pages/features/latex.mdx new file mode 100644 index 0000000000..0c85735b1e --- /dev/null +++ b/examples/docs/src/pages/features/latex.mdx @@ -0,0 +1,18 @@ +# LaTeX + +Nextra uses [KaTeX](https://katex.org/) to render LaTeX expressions directly in MDX. +To enable LaTeX support, you must add the following to your `next.config.js`: + +```js filename="next.config.js" +module.exports = require('nextra')({ + latex: true, +}) +``` + +Using LaTeX within MDX is as simple as wrapping your expression in `$$` or `$`. For example, the following code +```latex +$\sqrt{a^2 + b^2}$ +``` +will be rendered as: $\sqrt{a^2 + b^2}$ + +To learn more about KaTeX and its supported functions, visit their [documentation](https://katex.org/docs/supported.html). \ No newline at end of file diff --git a/packages/nextra/package.json b/packages/nextra/package.json index c8c07f17c7..4de683a54d 100644 --- a/packages/nextra/package.json +++ b/packages/nextra/package.json @@ -81,10 +81,13 @@ "github-slugger": "^2.0.0", "graceful-fs": "^4.2.10", "gray-matter": "^4.0.3", + "katex": "^0.16.4", "p-limit": "^3.1.0", + "rehype-katex": "^6.0.2", "rehype-mdx-title": "^2.0.0", "rehype-pretty-code": "0.6.0", "remark-gfm": "^3.0.1", + "remark-math": "^5.1.1", "remark-reading-time": "^2.0.1", "shiki": "0.12.1", "slash": "^3.0.0", diff --git a/packages/nextra/src/compile.ts b/packages/nextra/src/compile.ts index 5a2623bf15..11f981c385 100644 --- a/packages/nextra/src/compile.ts +++ b/packages/nextra/src/compile.ts @@ -14,6 +14,8 @@ import { import { LoaderOptions, PageOpts, ReadingTime } from './types' import theme from './theme.json' import { truthy } from './utils' +import remarkMath from 'remark-math' +import rehypeKatex from 'rehype-katex' const createCompiler = (mdxOptions: ProcessorOptions): Processor => { const compiler = createProcessor(mdxOptions) @@ -44,7 +46,11 @@ export async function compileMdx( source: string, loaderOptions: Pick< LoaderOptions, - 'staticImage' | 'flexsearch' | 'defaultShowCopyCode' | 'readingTime' + | 'staticImage' + | 'flexsearch' + | 'defaultShowCopyCode' + | 'readingTime' + | 'latex' > & { mdxOptions?: LoaderOptions['mdxOptions'] & Pick @@ -66,7 +72,8 @@ export async function compileMdx( loaderOptions.staticImage && ([remarkStaticImage, { filePath }] as any), loaderOptions.flexsearch && structurize(structurizedData, loaderOptions.flexsearch), - loaderOptions.readingTime && readingTime + loaderOptions.readingTime && readingTime, + loaderOptions.latex && remarkMath ].filter(truthy), rehypePlugins: [ ...(mdxOptions.rehypePlugins || []), @@ -76,7 +83,8 @@ export async function compileMdx( { ...rehypePrettyCodeOptions, ...mdxOptions.rehypePrettyCodeOptions } ], [rehypeMdxTitle, { name: '__nextra_title__' }], - [attachMeta, { defaultShowCopyCode: loaderOptions.defaultShowCopyCode }] + [attachMeta, { defaultShowCopyCode: loaderOptions.defaultShowCopyCode }], + ...(loaderOptions.latex ? [rehypeKatex] : []) ] }) try { diff --git a/packages/nextra/src/loader.ts b/packages/nextra/src/loader.ts index 8e6d7f77ce..7f88bc7360 100644 --- a/packages/nextra/src/loader.ts +++ b/packages/nextra/src/loader.ts @@ -67,6 +67,7 @@ async function loader( defaultLocale, defaultShowCopyCode, flexsearch, + latex, staticImage, readingTime: _readingTime, mdxOptions, @@ -127,14 +128,18 @@ async function loader( readingTime: _readingTime, defaultShowCopyCode, staticImage, - flexsearch + flexsearch, + latex }, mdxPath ) - // @ts-expect-error - const cssImport = OFFICIAL_THEMES.includes(theme) - ? `import '${theme}/style.css'` - : '' + + const katexCss = latex ? "import 'katex/dist/katex.min.css'\n" : '' + + const cssImport = + katexCss + + // @ts-expect-error + (OFFICIAL_THEMES.includes(theme) ? `import '${theme}/style.css'` : '') // Imported as a normal component, no need to add the layout. if (!pageImport) { diff --git a/packages/nextra/src/types.ts b/packages/nextra/src/types.ts index 2c477befda..62966a3bd7 100644 --- a/packages/nextra/src/types.ts +++ b/packages/nextra/src/types.ts @@ -94,6 +94,7 @@ export type NextraConfig = { flexsearch?: Flexsearch staticImage?: boolean readingTime?: boolean + latex?: boolean mdxOptions?: Pick & { rehypePrettyCodeOptions?: Partial } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 863c1b10f6..8fd4102dc8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -135,13 +135,16 @@ importers: github-slugger: ^2.0.0 graceful-fs: ^4.2.10 gray-matter: ^4.0.3 + katex: ^0.16.4 next: ^13.1.1 p-limit: ^3.1.0 react: ^18.2.0 react-dom: ^18.2.0 + rehype-katex: ^6.0.2 rehype-mdx-title: ^2.0.0 rehype-pretty-code: 0.6.0 remark-gfm: ^3.0.1 + remark-math: ^5.1.1 remark-reading-time: ^2.0.1 shiki: 0.12.1 slash: ^3.0.0 @@ -155,10 +158,13 @@ importers: github-slugger: 2.0.0 graceful-fs: 4.2.10 gray-matter: 4.0.3 + katex: 0.16.4 p-limit: 3.1.0 + rehype-katex: 6.0.2 rehype-mdx-title: 2.0.0 rehype-pretty-code: 0.6.0_shiki@0.12.1 remark-gfm: 3.0.1 + remark-math: 5.1.1 remark-reading-time: 2.0.1 shiki: 0.12.1 slash: 3.0.0 @@ -1035,6 +1041,10 @@ packages: resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} dev: true + /@types/katex/0.11.1: + resolution: {integrity: sha512-DUlIj2nk0YnJdlWgsFuVKcX27MLW0KbKmGVoUHmFr+74FYYNUDAaj9ZqTADvsbE8rfxuVmSFc7KczYn5Y09ozg==} + dev: false + /@types/mdast/3.0.10: resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==} dependencies: @@ -1068,6 +1078,10 @@ packages: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} dev: true + /@types/parse5/6.0.3: + resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} + dev: false + /@types/prop-types/15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} @@ -1750,6 +1764,11 @@ packages: engines: {node: '>= 6'} dev: true + /commander/8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + dev: false + /compute-scroll-into-view/2.0.2: resolution: {integrity: sha512-W+4Iti92hktsTtNPNeRM1vE0JdqCBk5qIabRafpr5pGrQhQ+xzCv0NGnFzTCKmW4yGLm9Aovbw8YNxloe2z9tQ==} dev: false @@ -3055,6 +3074,32 @@ packages: type-fest: 1.4.0 dev: false + /hast-util-from-parse5/7.1.0: + resolution: {integrity: sha512-m8yhANIAccpU4K6+121KpPP55sSl9/samzQSQGpb0mTExcNh2WlvjtMwSWFhg6uqD4Rr6Nfa8N6TMypQM51rzQ==} + dependencies: + '@types/hast': 2.3.4 + '@types/parse5': 6.0.3 + '@types/unist': 2.0.6 + hastscript: 7.1.0 + property-information: 6.1.1 + vfile: 5.3.4 + vfile-location: 4.0.1 + web-namespaces: 2.0.1 + dev: false + + /hast-util-is-element/2.1.2: + resolution: {integrity: sha512-thjnlGAnwP8ef/GSO1Q8BfVk2gundnc2peGQqEg2kUt/IqesiGg/5mSwN2fE7nLzy61pg88NG6xV+UrGOrx9EA==} + dependencies: + '@types/hast': 2.3.4 + '@types/unist': 2.0.6 + dev: false + + /hast-util-parse-selector/3.1.0: + resolution: {integrity: sha512-AyjlI2pTAZEOeu7GeBPZhROx0RHBnydkQIXlhnFzDi0qfXTmGUWoCYZtomHbrdrheV4VFUlPcfJ6LMF5T6sQzg==} + dependencies: + '@types/hast': 2.3.4 + dev: false + /hast-util-to-estree/2.0.2: resolution: {integrity: sha512-UQrZVeBj6A9od0lpFvqHKNSH9zvDrNoyWKbveu1a2oSCXEDUI+3bnd6BoiQLPnLrcXXn/jzJ6y9hmJTTlvf8lQ==} dependencies: @@ -3082,10 +3127,28 @@ packages: '@types/hast': 2.3.4 dev: false + /hast-util-to-text/3.1.1: + resolution: {integrity: sha512-7S3mOBxACy8syL45hCn3J7rHqYaXkxRfsX6LXEU5Shz4nt4GxdjtMUtG+T6G/ZLUHd7kslFAf14kAN71bz30xA==} + dependencies: + '@types/hast': 2.3.4 + hast-util-is-element: 2.1.2 + unist-util-find-after: 4.0.0 + dev: false + /hast-util-whitespace/2.0.0: resolution: {integrity: sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg==} dev: false + /hastscript/7.1.0: + resolution: {integrity: sha512-uBjaTTLN0MkCZxY/R2fWUOcu7FRtUVzKRO5P/RAfgsu3yFiMB1JWCO4AjeVkgHxAira1f2UecHK5WfS9QurlWA==} + dependencies: + '@types/hast': 2.3.4 + comma-separated-tokens: 2.0.2 + hast-util-parse-selector: 3.1.0 + property-information: 6.1.1 + space-separated-tokens: 2.0.1 + dev: false + /hosted-git-info/2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: true @@ -3432,6 +3495,27 @@ packages: graceful-fs: 4.2.10 dev: true + /katex/0.13.24: + resolution: {integrity: sha512-jZxYuKCma3VS5UuxOx/rFV1QyGSl3Uy/i0kTJF3HgQ5xMinCQVF8Zd4bMY/9aI9b9A2pjIBOsjSSm68ykTAr8w==} + hasBin: true + dependencies: + commander: 8.3.0 + dev: false + + /katex/0.15.6: + resolution: {integrity: sha512-UpzJy4yrnqnhXvRPhjEuLA4lcPn6eRngixW7Q3TJErjg3Aw2PuLFBzTkdUb89UtumxjhHTqL3a5GDGETMSwgJA==} + hasBin: true + dependencies: + commander: 8.3.0 + dev: false + + /katex/0.16.4: + resolution: {integrity: sha512-WudRKUj8yyBeVDI4aYMNxhx5Vhh2PjpzQw1GRu/LVGqL4m1AxwD1GcUp0IMbdJaf5zsjtj8ghP0DOQRYhroNkw==} + hasBin: true + dependencies: + commander: 8.3.0 + dev: false + /kind-of/6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} @@ -3766,6 +3850,14 @@ packages: - supports-color dev: false + /mdast-util-math/2.0.1: + resolution: {integrity: sha512-ZZtjyRwobsiVg4bY0Q5CzAZztpbjRIA7ZlMMb0PNkwTXOnJTUoHvzBhVG95LIuek5Mlj1l2P+jBvWviqW7G+0A==} + dependencies: + '@types/mdast': 3.0.10 + longest-streak: 3.0.1 + mdast-util-to-markdown: 1.3.0 + dev: false + /mdast-util-mdx-expression/1.2.1: resolution: {integrity: sha512-BtQwyalaq6jRjx0pagtuAwGrmzL1yInrfA4EJv7GOoiPOUbR4gr6h65I+G3WTh1/Cag2Eda4ip400Ch6CFmWiA==} dependencies: @@ -3971,6 +4063,18 @@ packages: micromark-util-types: 1.0.2 dev: false + /micromark-extension-math/2.0.2: + resolution: {integrity: sha512-cFv2B/E4pFPBBFuGgLHkkNiFAIQv08iDgPH2HCuR2z3AUgMLecES5Cq7AVtwOtZeRrbA80QgMUk8VVW0Z+D2FA==} + dependencies: + '@types/katex': 0.11.1 + katex: 0.13.24 + micromark-factory-space: 1.0.0 + micromark-util-character: 1.1.0 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + uvu: 0.5.6 + dev: false + /micromark-extension-mdx-expression/1.0.3: resolution: {integrity: sha512-TjYtjEMszWze51NJCZmhv7MEBcgYRgb3tJeMAJ+HQCAaZHHRBaDCccqQzGizR/H4ODefP44wRTgOn2vE5I6nZA==} dependencies: @@ -4563,6 +4667,10 @@ packages: parse-path: 7.0.0 dev: false + /parse5/6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + dev: false + /path-exists/4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -4940,6 +5048,19 @@ packages: engines: {node: '>=8'} dev: true + /rehype-katex/6.0.2: + resolution: {integrity: sha512-C4gDAlS1+l0hJqctyiU64f9CvT00S03qV1T6HiMzbSuLBgWUtcqydWHY9OpKrm0SpkK16FNd62CDKyWLwV2ppg==} + dependencies: + '@types/hast': 2.3.4 + '@types/katex': 0.11.1 + hast-util-to-text: 3.1.1 + katex: 0.15.6 + rehype-parse: 8.0.4 + unified: 10.1.2 + unist-util-remove-position: 4.0.1 + unist-util-visit: 4.1.1 + dev: false + /rehype-mdx-title/2.0.0: resolution: {integrity: sha512-IemxnNjM+mrABwH2V0UQjg5YULJmN55dF+zEajmoDgjnuAESIIm54iSKR0VwKpFrvQ9hWLn88RTr2deqwSOw0A==} engines: {node: '>=14'} @@ -4951,6 +5072,15 @@ packages: unist-util-visit: 4.1.1 dev: false + /rehype-parse/8.0.4: + resolution: {integrity: sha512-MJJKONunHjoTh4kc3dsM1v3C9kGrrxvA3U8PxZlP2SjH8RNUSrb+lF7Y0KVaUDnGH2QZ5vAn7ulkiajM9ifuqg==} + dependencies: + '@types/hast': 2.3.4 + hast-util-from-parse5: 7.1.0 + parse5: 6.0.1 + unified: 10.1.2 + dev: false + /rehype-pretty-code/0.6.0_shiki@0.12.1: resolution: {integrity: sha512-VfntYoWYOBVURXYDdB8p/E1sZTm2W5ry89fJyY94WJAo1jUH/5sVhDC7cX5PPnksMyW9PYMxRLNfjkBpSgJrzQ==} engines: {node: ^12.16.0 || >=13.2.0} @@ -4974,6 +5104,15 @@ packages: - supports-color dev: false + /remark-math/5.1.1: + resolution: {integrity: sha512-cE5T2R/xLVtfFI4cCePtiRn+e6jKMtFDR3P8V3qpv8wpKjwvHoBA4eJzvX+nVrnlNy0911bdGmuspCSwetfYHw==} + dependencies: + '@types/mdast': 3.0.10 + mdast-util-math: 2.0.1 + micromark-extension-math: 2.0.2 + unified: 10.1.2 + dev: false + /remark-mdx/2.1.2: resolution: {integrity: sha512-npQagPdczPAv0xN9F8GSi5hJfAe/z6nBjylyfOfjLOmz086ahWrIjlk4BulRfNhA+asutqWxyuT3DFVsxiTVHA==} dependencies: @@ -5857,6 +5996,13 @@ packages: '@types/unist': 2.0.6 dev: false + /unist-util-find-after/4.0.0: + resolution: {integrity: sha512-gfpsxKQde7atVF30n5Gff2fQhAc4/HTOV4CvkXpTg9wRfQhZWdXitpyXHWB6YcYgnsxLx+4gGHeVjCTAAp9sjw==} + dependencies: + '@types/unist': 2.0.6 + unist-util-is: 5.1.1 + dev: false + /unist-util-generated/2.0.0: resolution: {integrity: sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw==} dev: false @@ -6091,6 +6237,10 @@ packages: defaults: 1.0.3 dev: true + /web-namespaces/2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + dev: false + /webidl-conversions/4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} dev: true @@ -6370,13 +6520,16 @@ packages: github-slugger: 2.0.0 graceful-fs: 4.2.10 gray-matter: 4.0.3 + katex: 0.16.4 next: 13.1.1_biqbaboplfbrettd7655fr4n2y p-limit: 3.1.0 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 + rehype-katex: 6.0.2 rehype-mdx-title: 2.0.0 rehype-pretty-code: 0.6.0_shiki@0.12.1 remark-gfm: 3.0.1 + remark-math: 5.1.1 remark-reading-time: 2.0.1 shiki: 0.12.1 slash: 3.0.0