Skip to content

Commit dcd87de

Browse files
committed
feat: add Typst support as alternative formula renderer
1 parent e6d0568 commit dcd87de

File tree

20 files changed

+667
-118
lines changed

20 files changed

+667
-118
lines changed

docs/features/typst.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
relates:
3+
- Typst: https://typst.app/
4+
tags: [codeblock, syntax]
5+
description: |
6+
Slidev supports Typst as an alternative to KaTeX for formula rendering.
7+
---
8+
9+
# Typst
10+
11+
Slidev supports [Typst](https://typst.app/) as an alternative to KaTeX for formula rendering.
12+
13+
## Setup
14+
15+
To use Typst as your formula renderer, add the following to your frontmatter:
16+
17+
```yaml
18+
---
19+
formulaRenderer: typst
20+
---
21+
```
22+
23+
## Inline
24+
25+
Surround your Typst formula with a single `$` on each side for inline rendering.
26+
27+
```md
28+
$\sqrt{3x-1}+(1+x)^2$
29+
```
30+
31+
## Block
32+
33+
Use two (`$$`) for block rendering. This mode uses bigger symbols and centers
34+
the result.
35+
36+
```typst
37+
$$
38+
\begin{aligned}
39+
\nabla \cdot \vec{E} &= \frac{\rho}{\varepsilon_0} \\
40+
\nabla \cdot \vec{B} &= 0 \\
41+
\nabla \times \vec{E} &= -\frac{\partial\vec{B}}{\partial t} \\
42+
\nabla \times \vec{B} &= \mu_0\vec{J} + \mu_0\varepsilon_0\frac{\partial\vec{E}}{\partial t}
43+
\end{aligned}
44+
$$
45+
```
46+
47+
## Line Highlighting
48+
49+
To highlight specific lines, simply add line numbers within bracket `{}`. Line numbers start counting from 1 by default.
50+
51+
```typst
52+
$$ {1|3|all}
53+
\begin{aligned}
54+
\nabla \cdot \vec{E} &= \frac{\rho}{\varepsilon_0} \\
55+
\nabla \cdot \vec{B} &= 0 \\
56+
\nabla \times \vec{E} &= -\frac{\partial\vec{B}}{\partial t} \\
57+
\nabla \times \vec{B} &= \mu_0\vec{J} + \mu_0\varepsilon_0\frac{\partial\vec{E}}{\partial t}
58+
\end{aligned}
59+
$$
60+
```
61+
62+
The `at` and `finally` options of [code blocks](/features/line-highlighting) are also available for Typst blocks.
63+
64+
## Why Typst?
65+
66+
Typst is a modern typesetting system designed as an alternative to LaTeX. It offers:
67+
68+
- More concise syntax than LaTeX
69+
- Better package manager support
70+
- Powerful math formula typesetting
71+
- Ability to use third-party packages for tasks like plotting and drawing vector graphics
72+
73+
This makes Typst particularly useful for those who are already documenting with Typst and want a consistent experience in their presentations.
74+
75+
## Implementation
76+
77+
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.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<!--
2+
Line highlighting for Typst blocks
3+
(auto transformed, you don't need to use this component directly)
4+
5+
Usage:
6+
$$ {1|3|all}
7+
\begin{array}{c}
8+
9+
\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} &
10+
= \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
11+
12+
\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
13+
14+
\nabla \cdot \vec{\mathbf{B}} & = 0
15+
16+
\end{array}
17+
$$
18+
-->
19+
20+
<script setup lang="ts">
21+
import type { PropType } from 'vue'
22+
import { parseRangeString } from '@slidev/parser/utils'
23+
import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue'
24+
import { CLASS_VCLICK_HIDDEN, CLASS_VCLICK_TARGET, CLICKS_MAX } from '../constants'
25+
import { useSlideContext } from '../context'
26+
import { makeId } from '../logic/utils'
27+
28+
const props = defineProps({
29+
ranges: {
30+
type: Array as PropType<string[]>,
31+
default: () => [],
32+
},
33+
finally: {
34+
type: [String, Number],
35+
default: 'last',
36+
},
37+
startLine: {
38+
type: Number,
39+
default: 1,
40+
},
41+
at: {
42+
type: [String, Number],
43+
default: '+1',
44+
},
45+
})
46+
47+
const { $clicksContext: clicks } = useSlideContext()
48+
const el = ref<HTMLDivElement>()
49+
const id = makeId()
50+
51+
onUnmounted(() => {
52+
clicks!.unregister(id)
53+
})
54+
55+
onMounted(() => {
56+
if (!clicks || !props.ranges?.length)
57+
return
58+
59+
const clicksInfo = clicks.calculateSince(props.at, props.ranges.length - 1)
60+
clicks.register(id, clicksInfo)
61+
62+
const index = computed(() => clicksInfo ? Math.max(0, clicks.current - clicksInfo.start + 1) : CLICKS_MAX)
63+
64+
const finallyRange = computed(() => {
65+
return props.finally === 'last' ? props.ranges.at(-1) : props.finally.toString()
66+
})
67+
68+
watchEffect(() => {
69+
if (!el.value)
70+
return
71+
72+
let rangeStr = props.ranges[index.value] ?? finallyRange.value
73+
const hide = rangeStr === 'hide'
74+
el.value.classList.toggle(CLASS_VCLICK_HIDDEN, hide)
75+
if (hide)
76+
rangeStr = props.ranges[index.value + 1] ?? finallyRange.value
77+
78+
// For Typst, we'll need to implement line highlighting based on the rendered output
79+
// This will depend on how Typst.ts renders formulas
80+
// For now, we'll just add a data attribute with the range
81+
el.value.setAttribute('data-typst-highlight', rangeStr)
82+
})
83+
})
84+
</script>
85+
86+
<template>
87+
<div ref="el" class="slidev-typst-wrapper">
88+
<slot />
89+
</div>
90+
</template>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<!--
2+
Typst formula renderer component
3+
-->
4+
5+
<script setup lang="ts">
6+
import { onMounted, ref } from 'vue'
7+
import { renderTypstFormula } from '../setup/typst'
8+
9+
const props = defineProps({
10+
formula: {
11+
type: String,
12+
required: true,
13+
},
14+
displayMode: {
15+
type: Boolean,
16+
default: false,
17+
},
18+
})
19+
20+
const svgContent = ref('')
21+
const loading = ref(true)
22+
const error = ref(false)
23+
24+
onMounted(async () => {
25+
try {
26+
loading.value = true
27+
svgContent.value = await renderTypstFormula(props.formula, props.displayMode)
28+
loading.value = false
29+
}
30+
catch (e) {
31+
console.error('Failed to render Typst formula:', e)
32+
error.value = true
33+
loading.value = false
34+
}
35+
})
36+
</script>
37+
38+
<template>
39+
<div
40+
:class="[
41+
'typst-renderer',
42+
{ 'typst-display': displayMode, 'typst-inline': !displayMode }
43+
]"
44+
>
45+
<div v-if="loading" class="typst-loading">Loading...</div>
46+
<div v-else-if="error" class="typst-error">{{ formula }}</div>
47+
<div v-else v-html="svgContent" class="typst-content"></div>
48+
</div>
49+
</template>
50+
51+
<style>
52+
.typst-renderer {
53+
display: inline-block;
54+
}
55+
56+
.typst-display {
57+
display: block;
58+
margin: 1em 0;
59+
text-align: center;
60+
}
61+
62+
.typst-error {
63+
color: red;
64+
font-family: var(--slidev-font-mono);
65+
}
66+
67+
.typst-loading {
68+
color: #888;
69+
font-style: italic;
70+
}
71+
72+
.typst-content svg {
73+
vertical-align: middle;
74+
}
75+
</style>

packages/client/package.json

Lines changed: 30 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
{
22
"name": "@slidev/client",
3-
"type": "module",
4-
"version": "51.8.0",
3+
"version": "0.48.0-beta.19",
54
"description": "Presentation slides for developers",
6-
"author": "Anthony Fu <[email protected]>",
5+
"author": "antfu <[email protected]>",
76
"license": "MIT",
87
"funding": "https://github.com/sponsors/antfu",
98
"homepage": "https://sli.dev",
@@ -12,59 +11,37 @@
1211
"url": "https://github.com/slidevjs/slidev"
1312
},
1413
"bugs": "https://github.com/slidevjs/slidev/issues",
15-
"exports": {
16-
".": "./index.ts",
17-
"./package.json": "./package.json",
18-
"./constants": "./constants.ts",
19-
"./context": "./context.ts",
20-
"./env": "./env.ts",
21-
"./layoutHelper": "./layoutHelper.ts",
22-
"./routes": "./routes.ts",
23-
"./utils": "./utils.ts",
24-
"./*": "./*"
25-
},
26-
"main": "./public.ts",
27-
"engines": {
28-
"node": ">=18.0.0"
29-
},
14+
"main": "index.ts",
3015
"dependencies": {
31-
"@antfu/utils": "catalog:frontend",
32-
"@iconify-json/carbon": "catalog:icons",
33-
"@iconify-json/ph": "catalog:icons",
34-
"@iconify-json/svg-spinners": "catalog:icons",
35-
"@shikijs/engine-javascript": "catalog:frontend",
36-
"@shikijs/monaco": "catalog:monaco",
37-
"@shikijs/vitepress-twoslash": "catalog:prod",
3816
"@slidev/parser": "workspace:*",
39-
"@slidev/rough-notation": "catalog:frontend",
4017
"@slidev/types": "workspace:*",
41-
"@typescript/ata": "catalog:monaco",
42-
"@unhead/vue": "catalog:frontend",
43-
"@unocss/reset": "catalog:frontend",
44-
"@vueuse/core": "catalog:frontend",
45-
"@vueuse/math": "catalog:frontend",
46-
"@vueuse/motion": "catalog:frontend",
47-
"drauu": "catalog:frontend",
48-
"file-saver": "catalog:frontend",
49-
"floating-vue": "catalog:frontend",
50-
"fuse.js": "catalog:frontend",
51-
"katex": "catalog:frontend",
52-
"lz-string": "catalog:frontend",
53-
"mermaid": "catalog:frontend",
54-
"monaco-editor": "catalog:monaco",
55-
"nanotar": "catalog:frontend",
56-
"pptxgenjs": "catalog:prod",
57-
"prettier": "catalog:frontend",
58-
"recordrtc": "catalog:frontend",
59-
"shiki": "catalog:frontend",
60-
"shiki-magic-move": "catalog:frontend",
61-
"typescript": "catalog:dev",
62-
"unocss": "catalog:prod",
63-
"vue": "catalog:frontend",
64-
"vue-router": "catalog:frontend",
65-
"yaml": "catalog:prod"
18+
"@unhead/vue": "^1.8.10",
19+
"@vueuse/core": "^10.7.2",
20+
"@vueuse/head": "^2.0.0",
21+
"@vueuse/motion": "^2.0.0",
22+
"codemirror": "^5.65.16",
23+
"defu": "^6.1.4",
24+
"drauu": "^0.3.2",
25+
"file-saver": "^2.0.5",
26+
"fuse.js": "^7.0.0",
27+
"js-base64": "^3.7.6",
28+
"katex": "^0.16.9",
29+
"monaco-editor": "^0.46.0",
30+
"nanoid": "^5.0.4",
31+
"perfect-freehand": "^1.2.0",
32+
"recordrtc": "^5.6.2",
33+
"resolve": "^1.22.8",
34+
"typst.ts": "^0.5.0",
35+
"unocss": "^0.58.3",
36+
"vite-plugin-vue-markdown": "^0.23.8",
37+
"vue": "^3.4.15",
38+
"vue-router": "^4.2.5",
39+
"vue-starport": "^0.4.0"
6640
},
6741
"devDependencies": {
68-
"vite": "catalog:prod"
42+
"@types/codemirror": "^5.60.15",
43+
"@types/file-saver": "^2.0.7",
44+
"@types/katex": "^0.16.7",
45+
"@types/recordrtc": "^5.6.14"
6946
}
70-
}
47+
}

packages/client/setup/typst.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { createTypstCompiler } from 'typst.ts'
2+
3+
// Initialize Typst compiler
4+
export async function setupTypst() {
5+
const compiler = await createTypstCompiler({
6+
// Configure Typst compiler options here
7+
getModule: () => fetch('https://cdn.jsdelivr.net/npm/@typst.ts/compiler@latest/dist/assets/typst_wasm_bg.wasm')
8+
.then(response => response.arrayBuffer())
9+
.then(buffer => new WebAssembly.Module(buffer)),
10+
})
11+
12+
return compiler
13+
}
14+
15+
// Singleton instance
16+
let typstCompilerPromise: Promise<any> | null = null
17+
18+
export function getTypstCompiler() {
19+
if (!typstCompilerPromise)
20+
typstCompilerPromise = setupTypst()
21+
22+
return typstCompilerPromise
23+
}
24+
25+
// Render Typst formula to SVG
26+
export async function renderTypstFormula(formula: string, displayMode = false): Promise<string> {
27+
try {
28+
const compiler = await getTypstCompiler()
29+
30+
// Create a simple Typst document with just the math formula
31+
const typstCode = displayMode
32+
? `#set page(width: auto, height: auto, margin: 0pt)\n#set text(font: "Latin Modern Math")\n$ ${formula} $`
33+
: `#set page(width: auto, height: auto, margin: 0pt)\n#set text(font: "Latin Modern Math")\n$${formula}$`
34+
35+
// Compile and render to SVG
36+
const svg = await compiler.renderToSvg(typstCode)
37+
return svg
38+
}
39+
catch (error) {
40+
console.error('Error rendering Typst formula:', error)
41+
return `<span class="typst-error">${formula}</span>`
42+
}
43+
}

0 commit comments

Comments
 (0)