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 @@ + + + + + + + \ 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