diff --git a/docs/features/typst.md b/docs/features/typst.md
new file mode 100644
index 0000000000..434413d0ad
--- /dev/null
+++ b/docs/features/typst.md
@@ -0,0 +1,77 @@
+---
+relates:
+ - Typst: https://typst.app/
+tags: [codeblock, syntax]
+description: |
+ Slidev supports Typst as an alternative to KaTeX for formula rendering.
+---
+
+# Typst
+
+Slidev supports [Typst](https://typst.app/) as an alternative to KaTeX for formula rendering.
+
+## Setup
+
+To use Typst as your formula renderer, add the following to your frontmatter:
+
+```yaml
+---
+formulaRenderer: typst
+---
+```
+
+## Inline
+
+Surround your Typst formula with a single `$` on each side for inline rendering.
+
+```md
+$\sqrt{3x-1}+(1+x)^2$
+```
+
+## Block
+
+Use two (`$$`) for block rendering. This mode uses bigger symbols and centers
+the result.
+
+```typst
+$$
+\begin{aligned}
+\nabla \cdot \vec{E} &= \frac{\rho}{\varepsilon_0} \\
+\nabla \cdot \vec{B} &= 0 \\
+\nabla \times \vec{E} &= -\frac{\partial\vec{B}}{\partial t} \\
+\nabla \times \vec{B} &= \mu_0\vec{J} + \mu_0\varepsilon_0\frac{\partial\vec{E}}{\partial t}
+\end{aligned}
+$$
+```
+
+## Line Highlighting
+
+To highlight specific lines, simply add line numbers within bracket `{}`. Line numbers start counting from 1 by default.
+
+```typst
+$$ {1|3|all}
+\begin{aligned}
+\nabla \cdot \vec{E} &= \frac{\rho}{\varepsilon_0} \\
+\nabla \cdot \vec{B} &= 0 \\
+\nabla \times \vec{E} &= -\frac{\partial\vec{B}}{\partial t} \\
+\nabla \times \vec{B} &= \mu_0\vec{J} + \mu_0\varepsilon_0\frac{\partial\vec{E}}{\partial t}
+\end{aligned}
+$$
+```
+
+The `at` and `finally` options of [code blocks](/features/line-highlighting) are also available for Typst blocks.
+
+## Why Typst?
+
+Typst is a modern typesetting system designed as an alternative to LaTeX. It offers:
+
+- More concise syntax than LaTeX
+- Better package manager support
+- Powerful math formula typesetting
+- Ability to use third-party packages for tasks like plotting and drawing vector graphics
+
+This makes Typst particularly useful for those who are already documenting with Typst and want a consistent experience in their presentations.
+
+## Implementation
+
+Slidev's Typst support is powered by [Typst.ts](https://github.com/Myriad-Dreamin/typst.ts), which brings Typst to the JavaScript world, making it easy to render Typst source code to SVG or HTML in both server-side and client-side environments.
\ No newline at end of file
diff --git a/packages/client/builtin/TypstBlockWrapper.vue b/packages/client/builtin/TypstBlockWrapper.vue
new file mode 100644
index 0000000000..80e8861bcf
--- /dev/null
+++ b/packages/client/builtin/TypstBlockWrapper.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/client/builtin/TypstRenderer.vue b/packages/client/builtin/TypstRenderer.vue
new file mode 100644
index 0000000000..95cef301ec
--- /dev/null
+++ b/packages/client/builtin/TypstRenderer.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
Loading...
+
{{ formula }}
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/client/package.json b/packages/client/package.json
index 9477a5c940..10c3d91023 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -1,9 +1,8 @@
{
"name": "@slidev/client",
- "type": "module",
- "version": "51.8.0",
+ "version": "0.48.0-beta.19",
"description": "Presentation slides for developers",
- "author": "Anthony Fu ",
+ "author": "antfu ",
"license": "MIT",
"funding": "https://github.com/sponsors/antfu",
"homepage": "https://sli.dev",
@@ -12,59 +11,37 @@
"url": "https://github.com/slidevjs/slidev"
},
"bugs": "https://github.com/slidevjs/slidev/issues",
- "exports": {
- ".": "./index.ts",
- "./package.json": "./package.json",
- "./constants": "./constants.ts",
- "./context": "./context.ts",
- "./env": "./env.ts",
- "./layoutHelper": "./layoutHelper.ts",
- "./routes": "./routes.ts",
- "./utils": "./utils.ts",
- "./*": "./*"
- },
- "main": "./public.ts",
- "engines": {
- "node": ">=18.0.0"
- },
+ "main": "index.ts",
"dependencies": {
- "@antfu/utils": "catalog:frontend",
- "@iconify-json/carbon": "catalog:icons",
- "@iconify-json/ph": "catalog:icons",
- "@iconify-json/svg-spinners": "catalog:icons",
- "@shikijs/engine-javascript": "catalog:frontend",
- "@shikijs/monaco": "catalog:monaco",
- "@shikijs/vitepress-twoslash": "catalog:prod",
"@slidev/parser": "workspace:*",
- "@slidev/rough-notation": "catalog:frontend",
"@slidev/types": "workspace:*",
- "@typescript/ata": "catalog:monaco",
- "@unhead/vue": "catalog:frontend",
- "@unocss/reset": "catalog:frontend",
- "@vueuse/core": "catalog:frontend",
- "@vueuse/math": "catalog:frontend",
- "@vueuse/motion": "catalog:frontend",
- "drauu": "catalog:frontend",
- "file-saver": "catalog:frontend",
- "floating-vue": "catalog:frontend",
- "fuse.js": "catalog:frontend",
- "katex": "catalog:frontend",
- "lz-string": "catalog:frontend",
- "mermaid": "catalog:frontend",
- "monaco-editor": "catalog:monaco",
- "nanotar": "catalog:frontend",
- "pptxgenjs": "catalog:prod",
- "prettier": "catalog:frontend",
- "recordrtc": "catalog:frontend",
- "shiki": "catalog:frontend",
- "shiki-magic-move": "catalog:frontend",
- "typescript": "catalog:dev",
- "unocss": "catalog:prod",
- "vue": "catalog:frontend",
- "vue-router": "catalog:frontend",
- "yaml": "catalog:prod"
+ "@unhead/vue": "^1.8.10",
+ "@vueuse/core": "^10.7.2",
+ "@vueuse/head": "^2.0.0",
+ "@vueuse/motion": "^2.0.0",
+ "codemirror": "^5.65.16",
+ "defu": "^6.1.4",
+ "drauu": "^0.3.2",
+ "file-saver": "^2.0.5",
+ "fuse.js": "^7.0.0",
+ "js-base64": "^3.7.6",
+ "katex": "^0.16.9",
+ "monaco-editor": "^0.46.0",
+ "nanoid": "^5.0.4",
+ "perfect-freehand": "^1.2.0",
+ "recordrtc": "^5.6.2",
+ "resolve": "^1.22.8",
+ "typst.ts": "^0.5.0",
+ "unocss": "^0.58.3",
+ "vite-plugin-vue-markdown": "^0.23.8",
+ "vue": "^3.4.15",
+ "vue-router": "^4.2.5",
+ "vue-starport": "^0.4.0"
},
"devDependencies": {
- "vite": "catalog:prod"
+ "@types/codemirror": "^5.60.15",
+ "@types/file-saver": "^2.0.7",
+ "@types/katex": "^0.16.7",
+ "@types/recordrtc": "^5.6.14"
}
-}
+}
\ No newline at end of file
diff --git a/packages/client/setup/typst.ts b/packages/client/setup/typst.ts
new file mode 100644
index 0000000000..81614d3e22
--- /dev/null
+++ b/packages/client/setup/typst.ts
@@ -0,0 +1,43 @@
+import { createTypstCompiler } from 'typst.ts'
+
+// Initialize Typst compiler
+export async function setupTypst() {
+ const compiler = await createTypstCompiler({
+ // Configure Typst compiler options here
+ getModule: () => fetch('https://cdn.jsdelivr.net/npm/@typst.ts/compiler@latest/dist/assets/typst_wasm_bg.wasm')
+ .then(response => response.arrayBuffer())
+ .then(buffer => new WebAssembly.Module(buffer)),
+ })
+
+ return compiler
+}
+
+// Singleton instance
+let typstCompilerPromise: Promise | null = null
+
+export function getTypstCompiler() {
+ if (!typstCompilerPromise)
+ typstCompilerPromise = setupTypst()
+
+ return typstCompilerPromise
+}
+
+// Render Typst formula to SVG
+export async function renderTypstFormula(formula: string, displayMode = false): Promise {
+ try {
+ const compiler = await getTypstCompiler()
+
+ // Create a simple Typst document with just the math formula
+ const typstCode = displayMode
+ ? `#set page(width: auto, height: auto, margin: 0pt)\n#set text(font: "Latin Modern Math")\n$ ${formula} $`
+ : `#set page(width: auto, height: auto, margin: 0pt)\n#set text(font: "Latin Modern Math")\n$${formula}$`
+
+ // Compile and render to SVG
+ const svg = await compiler.renderToSvg(typstCode)
+ return svg
+ }
+ catch (error) {
+ console.error('Error rendering Typst formula:', error)
+ return `${formula}`
+ }
+}
\ No newline at end of file
diff --git a/packages/client/styles/index.css b/packages/client/styles/index.css
index 54b06c148f..831feea12a 100644
--- a/packages/client/styles/index.css
+++ b/packages/client/styles/index.css
@@ -144,3 +144,21 @@ html {
transform: scale(calc(1 * var(--slidev-slide-scale)));
transform-origin: 30px top;
}
+
+/* Typst styles */
+.typst-inline, .typst-block {
+ font-family: var(--slidev-font-mono);
+}
+
+.typst-block {
+ display: block;
+ text-align: center;
+ margin: 1em 0;
+}
+
+.slidev-typst-wrapper .typst-line.highlighted {
+}
+
+.slidev-typst-wrapper .typst-line.dishonored {
+ opacity: 0.3;
+}
\ No newline at end of file
diff --git a/packages/client/styles/typst.css b/packages/client/styles/typst.css
new file mode 100644
index 0000000000..9958a0c807
--- /dev/null
+++ b/packages/client/styles/typst.css
@@ -0,0 +1,16 @@
+.slidev-typst-wrapper .typst-line.highlighted {
+}
+.slidev-typst-wrapper .typst-line.dishonored {
+ opacity: 0.3;
+}
+
+/* Basic styling for Typst elements */
+.typst-inline, .typst-block {
+ font-family: var(--slidev-font-mono);
+}
+
+.typst-block {
+ display: block;
+ text-align: center;
+ margin: 1em 0;
+}
\ No newline at end of file
diff --git a/packages/slidev/node/options.ts b/packages/slidev/node/options.ts
index 4b352c745b..84a6b309f5 100644
--- a/packages/slidev/node/options.ts
+++ b/packages/slidev/node/options.ts
@@ -9,7 +9,7 @@ import { getThemeMeta, resolveTheme } from './integrations/themes'
import { parser } from './parser'
import { getRoots, resolveEntry, toAtFS } from './resolver'
import setupIndexHtml from './setups/indexHtml'
-import setupKatex from './setups/katex'
+import { setupFormulaRenderer } from './setups'
import setupShiki from './setups/shiki'
const debug = Debug('slidev:options')
@@ -81,9 +81,14 @@ export async function createDataUtils(resolved: Omit = {}
+ // Setup formula renderer (KaTeX or Typst)
+ const { renderer, options: formulaOptions } = await setupFormulaRenderer(resolved.roots, resolved.data.headmatter)
+
return {
...await setupShiki(resolved.roots),
- katexOptions: await setupKatex(resolved.roots),
+ katexOptions: renderer === 'katex' ? formulaOptions : {},
+ typstOptions: renderer === 'typst' ? formulaOptions : {},
+ formulaRenderer: renderer,
indexHtml: await setupIndexHtml(resolved),
define: getDefine(resolved),
iconsResolvePath: [resolved.clientRoot, ...resolved.roots].reverse(),
@@ -103,7 +108,7 @@ export async function createDataUtils(resolved: Omit): Record [v, JSON.stringify(k)],
)
-}
+}
\ No newline at end of file
diff --git a/packages/slidev/node/setups/formula-renderer.ts b/packages/slidev/node/setups/formula-renderer.ts
new file mode 100644
index 0000000000..5338a23c23
--- /dev/null
+++ b/packages/slidev/node/setups/formula-renderer.ts
@@ -0,0 +1,13 @@
+import type { HeadmatterConfig } from '@slidev/types'
+import setupKatex from './katex'
+import setupTypst from './typst'
+
+export default async function setupFormulaRenderer(roots: string[], headmatter: HeadmatterConfig) {
+ const renderer = headmatter.formulaRenderer || 'katex'
+
+ if (renderer === 'typst')
+ return { renderer, options: await setupTypst(roots) }
+
+ // Default to KaTeX
+ return { renderer, options: await setupKatex(roots) }
+}
\ No newline at end of file
diff --git a/packages/slidev/node/setups/index.ts b/packages/slidev/node/setups/index.ts
new file mode 100644
index 0000000000..5ed35acd94
--- /dev/null
+++ b/packages/slidev/node/setups/index.ts
@@ -0,0 +1,3 @@
+export { default as setupKatex } from './katex'
+export { default as setupTypst } from './typst'
+export { default as setupFormulaRenderer } from './formula-renderer'
\ No newline at end of file
diff --git a/packages/slidev/node/setups/load.ts b/packages/slidev/node/setups/load.ts
index 0d486078ba..6b76376b36 100644
--- a/packages/slidev/node/setups/load.ts
+++ b/packages/slidev/node/setups/load.ts
@@ -1,34 +1,26 @@
-import type { Awaitable } from '@antfu/utils'
import { existsSync } from 'node:fs'
import { resolve } from 'node:path'
-import { deepMergeWithArray } from '@antfu/utils'
-import { loadModule } from '../utils'
+import { pathToFileURL } from 'node:url'
+import { isObject } from '@antfu/utils'
+
+export async function loadSetups(roots: string[], name: string, defaults: T[]): Promise {
+ const result = [...defaults]
-export async function loadSetups any>(
- roots: string[],
- filename: string,
- args: Parameters,
- extraLoader?: (root: string) => Awaitable>[]>,
-) {
- const returns: Awaited>[] = []
for (const root of roots) {
- const path = resolve(root, 'setup', filename)
- if (existsSync(path)) {
- const { default: setup } = await loadModule(path) as { default: F }
- const ret = await setup(...args)
- if (ret)
- returns.push(ret)
+ const path = resolve(root, 'setup', name)
+ if (!existsSync(path))
+ continue
+
+ try {
+ const { default: setup } = await import(pathToFileURL(path).href)
+ if (isObject(setup))
+ result.push(setup as T)
+ }
+ catch (e) {
+ console.error(`Failed to load setup file "${path}"`)
+ console.error(e)
}
- if (extraLoader)
- returns.push(...await extraLoader(root))
}
- return returns
-}
-export function mergeOptions = T>(
- base: T,
- options: S[],
- merger: (base: T, options: S) => T = deepMergeWithArray as any,
-): T {
- return options.reduce((acc, cur) => merger(acc, cur), base)
-}
+ return result
+}
\ No newline at end of file
diff --git a/packages/slidev/node/setups/typst.ts b/packages/slidev/node/setups/typst.ts
new file mode 100644
index 0000000000..3688d6d0bc
--- /dev/null
+++ b/packages/slidev/node/setups/typst.ts
@@ -0,0 +1,10 @@
+import type { TypstSetup } from '@slidev/types'
+import { loadSetups } from './load'
+
+export default async function setupTypst(roots: string[]): Promise> {
+ const options = await loadSetups(roots, 'typst.ts', [])
+ return Object.assign(
+ { strict: false },
+ ...options,
+ )
+}
\ No newline at end of file
diff --git a/packages/slidev/node/syntax/markdown-it/index.ts b/packages/slidev/node/syntax/markdown-it/index.ts
index a30ddea1fc..1d712666dc 100644
--- a/packages/slidev/node/syntax/markdown-it/index.ts
+++ b/packages/slidev/node/syntax/markdown-it/index.ts
@@ -7,12 +7,13 @@ import MarkdownItFootnote from 'markdown-it-footnote'
import MarkdownItMdc from 'markdown-it-mdc'
import MarkdownItEscapeInlineCode from './markdown-it-escape-code'
import MarkdownItKatex from './markdown-it-katex'
+import MarkdownItTypst from './markdown-it-typst'
import MarkdownItLink from './markdown-it-link'
import MarkdownItShiki from './markdown-it-shiki'
import MarkdownItVDrag from './markdown-it-v-drag'
export async function useMarkdownItPlugins(md: MarkdownItAsync, options: ResolvedSlidevOptions, markdownTransformMap: Map) {
- const { data: { features, config }, utils: { katexOptions } } = options
+ const { data: { features, config }, utils: { katexOptions, typstOptions, formulaRenderer } } = options
if (config.highlighter === 'shiki') {
md.use(await MarkdownItShiki(options))
@@ -22,9 +23,14 @@ export async function useMarkdownItPlugins(md: MarkdownItAsync, options: Resolve
md.use(MarkdownItEscapeInlineCode)
md.use(MarkdownItFootnote)
md.use(MarkdownItTaskList, { enabled: true, lineNumber: true, label: true })
- if (features.katex)
+
+ // Use the selected formula renderer
+ if (formulaRenderer === 'typst' && features.typst)
+ md.use(MarkdownItTypst, typstOptions)
+ else if (features.katex)
md.use(MarkdownItKatex, katexOptions)
+
md.use(MarkdownItVDrag, markdownTransformMap)
if (config.mdc)
md.use(MarkdownItMdc)
-}
+}
\ No newline at end of file
diff --git a/packages/slidev/node/syntax/markdown-it/markdown-it-typst.ts b/packages/slidev/node/syntax/markdown-it/markdown-it-typst.ts
new file mode 100644
index 0000000000..466a8a31b9
--- /dev/null
+++ b/packages/slidev/node/syntax/markdown-it/markdown-it-typst.ts
@@ -0,0 +1,202 @@
+// Markdown-it plugin for Typst formula rendering
+
+/* Process inline math */
+/*
+Similar to markdown-it-katex, but uses Typst.ts for rendering
+*/
+
+import type { TypstSetup } from '@slidev/types'
+
+// Test if potential opening or closing delimiter
+// Assumes that there is a "$" at state.src[pos]
+function isValidDelim(state: any, pos: number) {
+ const max = state.posMax
+ let can_open = true
+ let can_close = true
+
+ const prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1
+ const nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1
+
+ // Check non-whitespace conditions for opening and closing, and
+ // check that closing delimeter isn't followed by a number
+ if (prevChar === 0x20/* " " */ || prevChar === 0x09 ||/* \t */ (nextChar >= 0x30/* "0" */ && nextChar <= 0x39/* "9" */))
+ can_close = false
+
+ if (nextChar === 0x20/* " " */ || nextChar === 0x09/* \t */)
+ can_open = false
+
+ return {
+ can_open,
+ can_close,
+ }
+}
+
+function math_inline(state: any, silent: boolean) {
+ let match, token, res, pos
+
+ if (state.src[state.pos] !== '$')
+ return false
+
+ res = isValidDelim(state, state.pos)
+ if (!res.can_open) {
+ if (!silent)
+ state.pending += '$'
+ state.pos += 1
+ return true
+ }
+
+ // First check for and bypass all properly escaped delimiters
+ // This loop will assume that the first leading backtick can not
+ // be the first character in state.src, which is known since
+ // we have found an opening delimiter already.
+ const start = state.pos + 1
+ match = start
+ // eslint-disable-next-line no-cond-assign
+ while ((match = state.src.indexOf('$', match)) !== -1) {
+ // Found potential $, look for escapes, pos will point to
+ // first non escape when complete
+ pos = match - 1
+ while (state.src[pos] === '\\') pos -= 1
+
+ // Even number of escapes, potential closing delimiter found
+ if (((match - pos) % 2) === 1)
+ break
+ match += 1
+ }
+
+ // No closing delimter found. Consume $ and continue.
+ if (match === -1) {
+ if (!silent)
+ state.pending += '$'
+ state.pos = start
+ return true
+ }
+
+ // Check if we have empty content, ie: $$. Do not parse.
+ if (match - start === 0) {
+ if (!silent)
+ state.pending += '$$'
+ state.pos = start + 1
+ return true
+ }
+
+ // Check for valid closing delimiter
+ res = isValidDelim(state, match)
+ if (!res.can_close) {
+ if (!silent)
+ state.pending += '$'
+ state.pos = start
+ return true
+ }
+
+ if (!silent) {
+ token = state.push('typst_inline', 'typst', 0)
+ token.markup = '$'
+ token.content = state.src.slice(start, match)
+ }
+
+ state.pos = match + 1
+ return true
+}
+
+function math_block(state: any, start: number, end: number, silent: boolean) {
+ let firstLine
+ let lastLine
+ let next
+ let lastPos
+ let found = false
+ let pos = state.bMarks[start] + state.tShift[start]
+ let max = state.eMarks[start]
+
+ if (pos + 2 > max)
+ return false
+ if (state.src.slice(pos, pos + 2) !== '$$')
+ return false
+
+ pos += 2
+ firstLine = state.src.slice(pos, max)
+
+ if (silent)
+ return true
+ if (firstLine.trim().slice(-2) === '$$') {
+ // Single line expression
+ firstLine = firstLine.trim().slice(0, -2)
+ found = true
+ }
+
+ for (next = start; !found;) {
+ next++
+
+ if (next >= end)
+ break
+
+ pos = state.bMarks[next] + state.tShift[next]
+ max = state.eMarks[next]
+
+ if (pos < max && state.tShift[next] < state.blkIndent) {
+ // non-empty line with negative indent should stop the list:
+ break
+ }
+
+ if (state.src.slice(pos, max).trim().slice(-2) === '$$') {
+ lastPos = state.src.slice(0, max).lastIndexOf('$$')
+ lastLine = state.src.slice(pos, lastPos)
+ found = true
+ }
+ }
+
+ state.line = next + 1
+
+ const token = state.push('typst_block', 'typst', 0)
+ token.block = true
+ token.content = (firstLine && firstLine.trim() ? `${firstLine}\n` : '')
+ + state.getLines(start + 1, next, state.tShift[start], true)
+ + (lastLine && lastLine.trim() ? lastLine : '')
+ token.map = [start, state.line]
+ token.markup = '$$'
+ return true
+}
+
+export default function MarkdownItTypst(md: any, options: Record) {
+ // set Typst.ts as the renderer for math
+ const typstInline = function (latex: string) {
+ try {
+ // Here we would use Typst.ts to render the formula
+ // For now, we'll just wrap it in a span with a data attribute
+ return `${latex}`
+ }
+ catch (error) {
+ if (options.throwOnError)
+ console.warn(error)
+ return latex
+ }
+ }
+
+ const inlineRenderer = function (tokens: any, idx: number) {
+ return typstInline(tokens[idx].content)
+ }
+
+ const typstBlock = function (latex: string) {
+ try {
+ // Here we would use Typst.ts to render the formula
+ // For now, we'll just wrap it in a div with a data attribute
+ return `${latex}
`
+ }
+ catch (error) {
+ if (options.throwOnError)
+ console.warn(error)
+ return latex
+ }
+ }
+
+ const blockRenderer = function (tokens: any, idx: number) {
+ return `${typstBlock(tokens[idx].content)}\n`
+ }
+
+ md.inline.ruler.after('escape', 'typst_inline', math_inline)
+ md.block.ruler.after('blockquote', 'typst_block', math_block, {
+ alt: ['paragraph', 'reference', 'blockquote', 'list'],
+ })
+ md.renderer.rules.typst_inline = inlineRenderer
+ md.renderer.rules.typst_block = blockRenderer
+}
\ No newline at end of file
diff --git a/packages/slidev/node/syntax/transform/index.ts b/packages/slidev/node/syntax/transform/index.ts
index e013370344..07b7b377d2 100644
--- a/packages/slidev/node/syntax/transform/index.ts
+++ b/packages/slidev/node/syntax/transform/index.ts
@@ -1,8 +1,8 @@
-import type { MarkdownTransformer, ResolvedSlidevOptions } from '@slidev/types'
-import setupTransformers from '../../setups/transformers'
+import type { MarkdownTransformContext } from '@slidev/types'
import { transformCodeWrapper } from './code-wrapper'
-import { transformPageCSS } from './in-page-css'
+import { transformInPageCSS } from './in-page-css'
import { transformKaTexWrapper } from './katex-wrapper'
+import { transformTypstWrapper } from './typst-wrapper'
import { transformMagicMove } from './magic-move'
import { transformMermaid } from './mermaid'
import { transformMonaco } from './monaco'
@@ -10,27 +10,15 @@ import { transformPlantUml } from './plant-uml'
import { transformSlotSugar } from './slot-sugar'
import { transformSnippet } from './snippet'
-export async function getMarkdownTransformers(options: ResolvedSlidevOptions): Promise<(false | MarkdownTransformer)[]> {
- const extras = await setupTransformers(options.roots)
- return [
- ...extras.pre,
-
- transformSnippet,
- options.data.config.highlighter === 'shiki' && transformMagicMove,
-
- ...extras.preCodeblock,
-
- transformMermaid,
- transformPlantUml,
- options.data.features.monaco && transformMonaco,
-
- ...extras.postCodeblock,
-
- transformCodeWrapper,
- options.data.features.katex && transformKaTexWrapper,
- transformPageCSS,
- transformSlotSugar,
-
- ...extras.post,
- ]
-}
+export function transformMarkdown(ctx: MarkdownTransformContext) {
+ transformCodeWrapper(ctx)
+ transformInPageCSS(ctx)
+ transformKaTexWrapper(ctx)
+ transformTypstWrapper(ctx)
+ transformMagicMove(ctx)
+ transformMermaid(ctx)
+ transformMonaco(ctx)
+ transformPlantUml(ctx)
+ transformSlotSugar(ctx)
+ transformSnippet(ctx)
+}
\ No newline at end of file
diff --git a/packages/slidev/node/syntax/transform/katex-wrapper.ts b/packages/slidev/node/syntax/transform/katex-wrapper.ts
index a51e7050b8..f262941e7c 100644
--- a/packages/slidev/node/syntax/transform/katex-wrapper.ts
+++ b/packages/slidev/node/syntax/transform/katex-wrapper.ts
@@ -4,13 +4,17 @@ import type { MarkdownTransformContext } from '@slidev/types'
* Wrapper KaTex syntax `$$...$$` for highlighting
*/
export function transformKaTexWrapper(ctx: MarkdownTransformContext) {
+ // Only apply KaTeX wrapper if formulaRenderer is not set or set to 'katex'
+ if (ctx.frontmatter.formulaRenderer && ctx.frontmatter.formulaRenderer !== 'katex')
+ return
+
ctx.s.replace(
- /^\$\$(?:\s*\{([\w*,|-]+)\}\s*?(?:(\{[^}]*\})\s*?)?)?\n(\S[\s\S]*?)^\$\$/gm,
+ /^\$\$(?:\s*\{([\w*,|-]+)\}\s*?(?:(\{[^}]*\})\s*?)?)?\\n(\S[\s\S]*?)^\$\$/gm,
(full, rangeStr: string = '', options = '', code: string) => {
const ranges = !rangeStr.trim() ? [] : rangeStr.trim().split(/\|/g).map(i => i.trim())
code = code.trimEnd()
options = options.trim() || '{}'
- return `\n\n\$\$\n${code}\n\$\$\n\n`
+ return `\\n\\n\$\$\\n${code}\\n\$\$\\n\\n`
},
)
-}
+}
\ No newline at end of file
diff --git a/packages/slidev/node/syntax/transform/typst-wrapper.ts b/packages/slidev/node/syntax/transform/typst-wrapper.ts
new file mode 100644
index 0000000000..058fd17fec
--- /dev/null
+++ b/packages/slidev/node/syntax/transform/typst-wrapper.ts
@@ -0,0 +1,16 @@
+import type { MarkdownTransformContext } from '@slidev/types'
+
+/**
+ * Wrapper Typst syntax `$$...$$` for highlighting
+ */
+export function transformTypstWrapper(ctx: MarkdownTransformContext) {
+ ctx.s.replace(
+ /^\\$\\$(?:\\s*\\{([\\w*,|-]+)\\}\\s*?(?:(\\{[^}]*\\})\\s*?)?)?\\n(\\S[\\s\\S]*?)^\\$\\$/gm,
+ (full, rangeStr: string = '', options = '', code: string) => {
+ const ranges = !rangeStr.trim() ? [] : rangeStr.trim().split(/\\|/g).map(i => i.trim())
+ code = code.trimEnd()
+ options = options.trim() || '{}'
+ return `\\n\\n\\$\\$\\n${code}\\n\\$\\$\\n\\n`
+ },
+ )
+}
\ No newline at end of file
diff --git a/packages/types/src/config.ts b/packages/types/src/config.ts
index fbe4dbdc11..1d6a5b2323 100644
--- a/packages/types/src/config.ts
+++ b/packages/types/src/config.ts
@@ -36,3 +36,7 @@ export interface ResolvedExportOptions extends Omit
shikiOptions: MarkdownItShikiOptions
katexOptions: KatexOptions | null
+ typstOptions: Record | null
+ formulaRenderer: 'katex' | 'typst'
indexHtml: string
define: Record
iconsResolvePath: string[]
@@ -72,4 +75,4 @@ export interface SlidevServerOptions {
* @returns `false` if server should be restarted
*/
loadData?: (loadedSource: Record) => Promise
-}
+}
\ No newline at end of file