Skip to content

feat: add Typst support as alternative formula renderer #2183

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
77 changes: 77 additions & 0 deletions docs/features/typst.md
Original file line number Diff line number Diff line change
@@ -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.
90 changes: 90 additions & 0 deletions packages/client/builtin/TypstBlockWrapper.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<!--
Line highlighting for Typst blocks
(auto transformed, you don't need to use this component directly)

Usage:
$$ {1|3|all}
\begin{array}{c}

\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} &
= \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\

\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\

\nabla \cdot \vec{\mathbf{B}} & = 0

\end{array}
$$
-->

<script setup lang="ts">
import type { PropType } from 'vue'
import { parseRangeString } from '@slidev/parser/utils'
import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue'
import { CLASS_VCLICK_HIDDEN, CLASS_VCLICK_TARGET, CLICKS_MAX } from '../constants'
import { useSlideContext } from '../context'
import { makeId } from '../logic/utils'

const props = defineProps({
ranges: {
type: Array as PropType<string[]>,
default: () => [],
},
finally: {
type: [String, Number],
default: 'last',
},
startLine: {
type: Number,
default: 1,
},
at: {
type: [String, Number],
default: '+1',
},
})

const { $clicksContext: clicks } = useSlideContext()
const el = ref<HTMLDivElement>()
const id = makeId()

onUnmounted(() => {
clicks!.unregister(id)
})

onMounted(() => {
if (!clicks || !props.ranges?.length)
return

const clicksInfo = clicks.calculateSince(props.at, props.ranges.length - 1)
clicks.register(id, clicksInfo)

const index = computed(() => clicksInfo ? Math.max(0, clicks.current - clicksInfo.start + 1) : CLICKS_MAX)

const finallyRange = computed(() => {
return props.finally === 'last' ? props.ranges.at(-1) : props.finally.toString()
})

watchEffect(() => {
if (!el.value)
return

let rangeStr = props.ranges[index.value] ?? finallyRange.value
const hide = rangeStr === 'hide'
el.value.classList.toggle(CLASS_VCLICK_HIDDEN, hide)
if (hide)
rangeStr = props.ranges[index.value + 1] ?? finallyRange.value

// For Typst, we'll need to implement line highlighting based on the rendered output
// This will depend on how Typst.ts renders formulas
// For now, we'll just add a data attribute with the range
el.value.setAttribute('data-typst-highlight', rangeStr)
})
})
</script>

<template>
<div ref="el" class="slidev-typst-wrapper">
<slot />
</div>
</template>
75 changes: 75 additions & 0 deletions packages/client/builtin/TypstRenderer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<!--
Typst formula renderer component
-->

<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { renderTypstFormula } from '../setup/typst'

const props = defineProps({
formula: {
type: String,
required: true,
},
displayMode: {
type: Boolean,
default: false,
},
})

const svgContent = ref('')
const loading = ref(true)
const error = ref(false)

onMounted(async () => {
try {
loading.value = true
svgContent.value = await renderTypstFormula(props.formula, props.displayMode)
loading.value = false
}
catch (e) {
console.error('Failed to render Typst formula:', e)
error.value = true
loading.value = false
}
})
</script>

<template>
<div
:class="[
'typst-renderer',
{ 'typst-display': displayMode, 'typst-inline': !displayMode }
]"
>
<div v-if="loading" class="typst-loading">Loading...</div>
<div v-else-if="error" class="typst-error">{{ formula }}</div>
<div v-else v-html="svgContent" class="typst-content"></div>
</div>
</template>

<style>
.typst-renderer {
display: inline-block;
}

.typst-display {
display: block;
margin: 1em 0;
text-align: center;
}

.typst-error {
color: red;
font-family: var(--slidev-font-mono);
}

.typst-loading {
color: #888;
font-style: italic;
}

.typst-content svg {
vertical-align: middle;
}
</style>
83 changes: 30 additions & 53 deletions packages/client/package.json
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>",
"author": "antfu <[email protected]>",
"license": "MIT",
"funding": "https://github.com/sponsors/antfu",
"homepage": "https://sli.dev",
Expand All @@ -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"
}
}
}
43 changes: 43 additions & 0 deletions packages/client/setup/typst.ts
Original file line number Diff line number Diff line change
@@ -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<any> | 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<string> {
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 `<span class="typst-error">${formula}</span>`
}
}
Loading