diff --git a/packages/markdown/assets/directive-typst/client.js b/packages/markdown/assets/directive-typst/client.js
new file mode 100644
index 00000000..17e1a2d3
--- /dev/null
+++ b/packages/markdown/assets/directive-typst/client.js
@@ -0,0 +1,165 @@
+hyperbook.typst = (function () {
+ // Register code-input template for typst syntax highlighting
+ window.codeInput?.registerTemplate(
+ "typst-highlighted",
+ codeInput.templates.prism(window.Prism, [
+ new codeInput.plugins.AutoCloseBrackets(),
+ new codeInput.plugins.Indent(true, 2),
+ ]),
+ );
+
+ const elems = document.getElementsByClassName("directive-typst");
+
+ // Typst WASM module URLs
+ const TYPST_COMPILER_URL = "https://cdn.jsdelivr.net/npm/@myriaddreamin/typst-ts-web-compiler/pkg/typst_ts_web_compiler_bg.wasm";
+ const TYPST_RENDERER_URL = "https://cdn.jsdelivr.net/npm/@myriaddreamin/typst-ts-renderer/pkg/typst_ts_renderer_bg.wasm";
+
+ // Load typst all-in-one bundle
+ let typstLoaded = false;
+ let typstLoadPromise = null;
+
+ const loadTypst = () => {
+ if (typstLoaded) {
+ return Promise.resolve();
+ }
+ if (typstLoadPromise) {
+ return typstLoadPromise;
+ }
+
+ typstLoadPromise = new Promise((resolve, reject) => {
+ const script = document.createElement("script");
+ script.src = "https://cdn.jsdelivr.net/npm/@myriaddreamin/typst.ts/dist/esm/contrib/all-in-one-lite.bundle.js";
+ script.type = "module";
+ script.id = "typst-loader";
+ script.onload = () => {
+ // Wait a bit for the module to initialize
+ const checkTypst = () => {
+ if (typeof $typst !== "undefined") {
+ // Initialize the Typst compiler and renderer
+ $typst.setCompilerInitOptions({
+ getModule: () => TYPST_COMPILER_URL,
+ });
+ $typst.setRendererInitOptions({
+ getModule: () => TYPST_RENDERER_URL,
+ });
+ typstLoaded = true;
+ resolve();
+ } else {
+ setTimeout(checkTypst, 50);
+ }
+ };
+ checkTypst();
+ };
+ script.onerror = reject;
+ document.head.appendChild(script);
+ });
+
+ return typstLoadPromise;
+ };
+
+ // Render typst code to SVG
+ const renderTypst = async (code, container, loadingIndicator) => {
+ // Show loading indicator
+ if (loadingIndicator) {
+ loadingIndicator.style.display = "flex";
+ }
+
+ await loadTypst();
+
+ try {
+ const svg = await $typst.svg({ mainContent: code });
+ container.innerHTML = svg;
+ } catch (error) {
+ container.innerHTML = `
${error.message || "Error rendering Typst"}
`;
+ console.error("Typst rendering error:", error);
+ } finally {
+ // Hide loading indicator
+ if (loadingIndicator) {
+ loadingIndicator.style.display = "none";
+ }
+ }
+ };
+
+ // Export to PDF
+ const exportPdf = async (code, id) => {
+ await loadTypst();
+
+ try {
+ const pdfData = await $typst.pdf({ mainContent: code });
+ const pdfFile = new Blob([pdfData], { type: "application/pdf" });
+ const link = document.createElement("a");
+ link.href = URL.createObjectURL(pdfFile);
+ link.download = `typst-${id}.pdf`;
+ link.click();
+ URL.revokeObjectURL(link.href);
+ } catch (error) {
+ console.error("PDF export error:", error);
+ alert(i18n.get("typst-pdf-error") || "Error exporting PDF");
+ }
+ };
+
+ for (let elem of elems) {
+ const id = elem.getAttribute("data-id");
+ const preview = elem.querySelector(".typst-preview");
+ const loadingIndicator = elem.querySelector(".typst-loading");
+ const editor = elem.querySelector(".editor.typst");
+ const downloadBtn = elem.querySelector(".download-pdf");
+ const copyBtn = elem.querySelector(".copy");
+ const resetBtn = elem.querySelector(".reset");
+ const sourceTextarea = elem.querySelector(".typst-source");
+
+ // Get initial code
+ let initialCode = "";
+ if (editor) {
+ // Edit mode - code is in the editor
+ // Wait for code-input to load
+ editor.addEventListener("code-input_load", async () => {
+ // Check for stored code
+ const result = await store.typst?.get(id);
+ if (result) {
+ editor.value = result.code;
+ }
+ initialCode = editor.value;
+ renderTypst(initialCode, preview, loadingIndicator);
+
+ // Listen for input changes
+ editor.addEventListener("input", () => {
+ store.typst?.put({ id, code: editor.value });
+ renderTypst(editor.value, preview, loadingIndicator);
+ });
+ });
+ } else if (sourceTextarea) {
+ // Preview mode - code is in hidden textarea
+ initialCode = sourceTextarea.value;
+ loadTypst().then(() => {
+ renderTypst(initialCode, preview, loadingIndicator);
+ });
+ }
+
+ // Download PDF button
+ downloadBtn?.addEventListener("click", async () => {
+ const code = editor ? editor.value : initialCode;
+ await exportPdf(code, id);
+ });
+
+ // Copy button
+ copyBtn?.addEventListener("click", async () => {
+ const code = editor ? editor.value : initialCode;
+ try {
+ await navigator.clipboard.writeText(code);
+ } catch (error) {
+ console.error("Copy error:", error);
+ }
+ });
+
+ // Reset button (edit mode only)
+ resetBtn?.addEventListener("click", async () => {
+ if (window.confirm(i18n.get("typst-reset-prompt") || "Are you sure you want to reset the code?")) {
+ store.typst?.delete(id);
+ window.location.reload();
+ }
+ });
+ }
+
+ return {};
+})();
diff --git a/packages/markdown/assets/directive-typst/style.css b/packages/markdown/assets/directive-typst/style.css
new file mode 100644
index 00000000..e10a4455
--- /dev/null
+++ b/packages/markdown/assets/directive-typst/style.css
@@ -0,0 +1,165 @@
+.directive-typst {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ margin-bottom: 16px;
+ overflow: hidden;
+ gap: 8px;
+}
+
+.directive-typst.preview-only {
+ gap: 0;
+}
+
+code-input {
+ margin: 0;
+}
+
+.directive-typst .preview-container {
+ width: 100%;
+ border: 1px solid var(--color-spacer);
+ border-radius: 8px;
+ border-bottom: none;
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ overflow: auto;
+ background-color: white;
+}
+
+.directive-typst .typst-preview {
+ padding: 16px;
+ min-height: 100px;
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+}
+
+.directive-typst .typst-preview svg {
+ max-width: 100%;
+ height: auto;
+ border-radius: 8px;
+}
+
+.directive-typst .typst-loading {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 32px;
+ gap: 12px;
+ color: var(--color-text);
+}
+
+.directive-typst .typst-spinner {
+ width: 32px;
+ height: 32px;
+ border: 3px solid var(--color-spacer);
+ border-top-color: var(--color-primary, #3b82f6);
+ border-radius: 50%;
+ animation: typst-spin 1s linear infinite;
+}
+
+@keyframes typst-spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.directive-typst .typst-error {
+ color: #dc2626;
+ padding: 16px;
+ background-color: #fef2f2;
+ border-radius: 4px;
+ font-family: monospace;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.directive-typst .editor-container {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.directive-typst .editor {
+ width: 100%;
+ border: 1px solid var(--color-spacer);
+ flex: 1;
+}
+
+.directive-typst .editor:not(.active) {
+ display: none;
+}
+
+.directive-typst .buttons {
+ display: flex;
+ border: 1px solid var(--color-spacer);
+ border-radius: 8px;
+ border-bottom: none;
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.directive-typst .buttons.bottom {
+ border: 1px solid var(--color-spacer);
+ border-radius: 8px;
+ border-top: none;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
+
+.directive-typst .buttons-container {
+ display: flex;
+ width: 100%;
+ border: 1px solid var(--color-spacer);
+ border-radius: 8px;
+ border-top: none;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
+
+.directive-typst button {
+ flex: 1;
+ padding: 8px 16px;
+ border: none;
+ border-right: 1px solid var(--color-spacer);
+ background-color: var(--color--background);
+ color: var(--color-text);
+ cursor: pointer;
+}
+
+.directive-typst button:last-child {
+ border-right: none;
+}
+
+.directive-typst button:not(.active) {
+ opacity: 0.6;
+}
+
+.directive-typst .buttons:last-child {
+ border-right: none;
+}
+
+.directive-typst button:hover {
+ background-color: var(--color-spacer);
+}
+
+.directive-typst .hidden {
+ display: none !important;
+}
+
+@media screen and (min-width: 1024px) {
+ .directive-typst:not(.preview-only) {
+ flex-direction: row;
+ .preview-container {
+ flex: 1;
+ }
+
+ .editor-container {
+ flex: 1;
+ height: 100%;
+ overflow: hidden;
+ }
+ }
+}
diff --git a/packages/markdown/assets/store.js b/packages/markdown/assets/store.js
index 68250a90..467befdc 100644
--- a/packages/markdown/assets/store.js
+++ b/packages/markdown/assets/store.js
@@ -28,6 +28,7 @@ store.version(1).stores({
geogebra: `id,state`,
learningmap: `id,nodes,x,y,zoom`,
textinput: `id,text`,
+ typst: `id,code`,
custom: `id,payload`,
multievent: `id,state`,
});
diff --git a/packages/markdown/locales/de.json b/packages/markdown/locales/de.json
index e32e85d1..b550e263 100644
--- a/packages/markdown/locales/de.json
+++ b/packages/markdown/locales/de.json
@@ -53,5 +53,12 @@
"webide-reset": "Zurücksetzen",
"webide-reset-prompt": "Sind Sie sicher, dass Sie den Code zurücksetzen möchten?",
"webide-copy": "Kopieren",
- "webide-download": "Herunterladen"
+ "webide-download": "Herunterladen",
+ "typst-code": "Typst",
+ "typst-reset": "Zurücksetzen",
+ "typst-reset-prompt": "Sind Sie sicher, dass Sie den Code zurücksetzen möchten?",
+ "typst-download-pdf": "PDF herunterladen",
+ "typst-pdf-error": "Fehler beim PDF-Export",
+ "typst-copy": "Kopieren",
+ "typst-loading": "Typst wird geladen..."
}
diff --git a/packages/markdown/locales/en.json b/packages/markdown/locales/en.json
index c9361eb3..d7b57b7a 100644
--- a/packages/markdown/locales/en.json
+++ b/packages/markdown/locales/en.json
@@ -53,5 +53,12 @@
"webide-reset": "Reset",
"webide-reset-prompt": "Are you sure you want to reset the code?",
"webide-copy": "Copy",
- "webide-download": "Download"
+ "webide-download": "Download",
+ "typst-code": "Typst",
+ "typst-reset": "Reset",
+ "typst-reset-prompt": "Are you sure you want to reset the code?",
+ "typst-download-pdf": "Download PDF",
+ "typst-pdf-error": "Error exporting PDF",
+ "typst-copy": "Copy",
+ "typst-loading": "Loading Typst..."
}
diff --git a/packages/markdown/src/process.ts b/packages/markdown/src/process.ts
index 2c0a012a..f4a38722 100644
--- a/packages/markdown/src/process.ts
+++ b/packages/markdown/src/process.ts
@@ -62,6 +62,7 @@ import remarkSubSup from "./remarkSubSup";
import remarkImageAttrs from "./remarkImageAttrs";
import remarkDirectiveLearningmap from "./remarkDirectiveLearningmap";
import remarkDirectiveTextinput from "./remarkDirectiveTextinput";
+import remarkDirectiveTypst from "./remarkDirectiveTypst";
export const remark = (ctx: HyperbookContext) => {
i18n.init(ctx.config.language || "en");
@@ -106,6 +107,7 @@ export const remark = (ctx: HyperbookContext) => {
remarkDirectiveMultievent(ctx),
remarkDirectiveLearningmap(ctx),
remarkDirectiveTextinput(ctx),
+ remarkDirectiveTypst(ctx),
remarkCode(ctx),
remarkMath,
/* needs to be last directive */
diff --git a/packages/markdown/src/remarkDirectiveTypst.ts b/packages/markdown/src/remarkDirectiveTypst.ts
new file mode 100644
index 00000000..6dfd8b84
--- /dev/null
+++ b/packages/markdown/src/remarkDirectiveTypst.ts
@@ -0,0 +1,260 @@
+// Register directive nodes in mdast:
+///
+//
+import { HyperbookContext } from "@hyperbook/types";
+import { Code, Root } from "mdast";
+import { visit } from "unist-util-visit";
+import { VFile } from "vfile";
+import {
+ expectContainerDirective,
+ isDirective,
+ registerDirective,
+ requestCSS,
+ requestJS,
+} from "./remarkHelper";
+import hash from "./objectHash";
+import { i18n } from "./i18n";
+import { Element, ElementContent } from "hast";
+import { readFile } from "./helper";
+
+function htmlEntities(str: string) {
+ return String(str)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+}
+
+export default (ctx: HyperbookContext) => () => {
+ const name = "typst";
+
+ return (tree: Root, file: VFile) => {
+ visit(tree, function (node) {
+ if (isDirective(node) && node.name === name) {
+ const { height, id = hash(node), mode = "preview", src = "" } = node.attributes || {};
+ const data = node.data || (node.data = {});
+
+ expectContainerDirective(node, file, name);
+ registerDirective(file, name, ["client.js"], ["style.css"], []);
+ requestJS(file, ["code-input", "code-input.min.js"]);
+ requestCSS(file, ["code-input", "code-input.min.css"]);
+ requestJS(file, ["code-input", "auto-close-brackets.min.js"]);
+ requestJS(file, ["code-input", "indent.min.js"]);
+
+ let typstCode = "";
+
+ // Load from external file if src is provided
+ if (src) {
+ typstCode = readFile(src, ctx) || "";
+ } else {
+ // Find typst code block inside the directive
+ const typstNode = node.children.find(
+ (n) => n.type === "code" && (n.lang === "typ" || n.lang === "typst"),
+ ) as Code;
+
+ if (typstNode) {
+ typstCode = typstNode.value;
+ }
+ }
+
+ const isEditMode = mode === "edit";
+ const isPreviewMode = mode === "preview" || !isEditMode;
+
+ // Determine container height based on mode and custom height
+ let containerHeight: string;
+ if (height) {
+ // Custom height provided - use as-is if string, add px if number
+ containerHeight = typeof height === "number" || /^\d+$/.test(height) ? `${height}px` : height;
+ } else {
+ // Default heights: auto for preview, calc(100dvh - 128px) for edit
+ containerHeight = isPreviewMode ? "auto" : "calc(100dvh - 128px)";
+ }
+
+ data.hName = "div";
+ data.hProperties = {
+ class: ["directive-typst", isPreviewMode ? "preview-only" : ""].join(" ").trim(),
+ "data-id": id,
+ };
+
+ const previewContainer: Element = {
+ type: "element",
+ tagName: "div",
+ properties: {
+ class: "preview-container",
+ style: `height: ${containerHeight};`,
+ },
+ children: [
+ {
+ type: "element",
+ tagName: "div",
+ properties: {
+ class: "typst-loading",
+ },
+ children: [
+ {
+ type: "element",
+ tagName: "div",
+ properties: {
+ class: "typst-spinner",
+ },
+ children: [],
+ },
+ {
+ type: "element",
+ tagName: "span",
+ properties: {},
+ children: [
+ {
+ type: "text",
+ value: i18n.get("typst-loading"),
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: "element",
+ tagName: "div",
+ properties: {
+ class: "typst-preview",
+ },
+ children: [],
+ },
+ ],
+ };
+
+ const downloadButton: Element = {
+ type: "element",
+ tagName: "button",
+ properties: {
+ class: "download-pdf",
+ },
+ children: [
+ {
+ type: "text",
+ value: i18n.get("typst-download-pdf"),
+ },
+ ],
+ };
+
+ const copyButton: Element = {
+ type: "element",
+ tagName: "button",
+ properties: {
+ class: "copy",
+ },
+ children: [
+ {
+ type: "text",
+ value: i18n.get("typst-copy"),
+ },
+ ],
+ };
+
+ if (isEditMode) {
+ // Edit mode: show editor and preview side by side
+ data.hChildren = [
+ previewContainer,
+ {
+ type: "element",
+ tagName: "div",
+ properties: {
+ class: "editor-container",
+ },
+ children: [
+ {
+ type: "element",
+ tagName: "div",
+ properties: {
+ class: "buttons",
+ },
+ children: [
+ {
+ type: "element",
+ tagName: "button",
+ properties: {
+ class: "typst",
+ },
+ children: [
+ {
+ type: "text",
+ value: i18n.get("typst-code"),
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: "element",
+ tagName: "code-input",
+ properties: {
+ class: "editor typst active line-numbers",
+ language: "typst",
+ template: "typst-highlighted",
+ },
+ children: [
+ {
+ type: "raw",
+ value: htmlEntities(typstCode),
+ },
+ ],
+ },
+ {
+ type: "element",
+ tagName: "div",
+ properties: {
+ class: "buttons bottom",
+ },
+ children: [
+ {
+ type: "element",
+ tagName: "button",
+ properties: {
+ class: "reset",
+ },
+ children: [
+ {
+ type: "text",
+ value: i18n.get("typst-reset"),
+ },
+ ],
+ },
+ copyButton,
+ downloadButton,
+ ],
+ },
+ ],
+ },
+ ];
+ } else {
+ // Preview mode: show only preview with download button
+ data.hChildren = [
+ previewContainer,
+ {
+ type: "element",
+ tagName: "div",
+ properties: {
+ class: "buttons-container",
+ },
+ children: [copyButton, downloadButton],
+ },
+ {
+ type: "element",
+ tagName: "textarea",
+ properties: {
+ class: "typst-source hidden",
+ style: "display: none;",
+ },
+ children: [
+ {
+ type: "text",
+ value: typstCode,
+ },
+ ],
+ },
+ ];
+ }
+ }
+ });
+ };
+};
diff --git a/packages/markdown/tests/__snapshots__/remarkDirectiveTypst.test.ts.snap b/packages/markdown/tests/__snapshots__/remarkDirectiveTypst.test.ts.snap
new file mode 100644
index 00000000..eba39821
--- /dev/null
+++ b/packages/markdown/tests/__snapshots__/remarkDirectiveTypst.test.ts.snap
@@ -0,0 +1,63 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`remarkDirectiveTypst > should default to preview mode 1`] = `
+"
+
+
+
+
+"
+`;
+
+exports[`remarkDirectiveTypst > should handle typst language code block 1`] = `
+"
+
+
+
+
+"
+`;
+
+exports[`remarkDirectiveTypst > should transform edit mode 1`] = `
+"
+
+
+
+
+
= Hello World!
+
+This is editable content.
+
+
+
+"
+`;
+
+exports[`remarkDirectiveTypst > should transform preview mode 1`] = `
+"
+
+
+
+
+"
+`;
diff --git a/packages/markdown/tests/remarkDirectiveTypst.test.ts b/packages/markdown/tests/remarkDirectiveTypst.test.ts
new file mode 100644
index 00000000..32c6212b
--- /dev/null
+++ b/packages/markdown/tests/remarkDirectiveTypst.test.ts
@@ -0,0 +1,135 @@
+import { HyperbookContext } from "@hyperbook/types/dist";
+import { describe, expect, it } from "vitest";
+import rehypeStringify from "rehype-stringify";
+import remarkToRehype from "remark-rehype";
+import rehypeFormat from "rehype-format";
+import { unified, PluggableList } from "unified";
+import remarkDirective from "remark-directive";
+import remarkDirectiveRehype from "remark-directive-rehype";
+import { ctx } from "./mock";
+import remarkDirectiveTypst from "../src/remarkDirectiveTypst";
+import remarkParse from "../src/remarkParse";
+
+export const toHtml = (md: string, ctx: HyperbookContext) => {
+ const remarkPlugins: PluggableList = [
+ remarkDirective,
+ remarkDirectiveRehype,
+ remarkDirectiveTypst(ctx),
+ ];
+
+ return unified()
+ .use(remarkParse)
+ .use(remarkPlugins)
+ .use(remarkToRehype)
+ .use(rehypeFormat)
+ .use(rehypeStringify, {
+ allowDangerousCharacters: true,
+ allowDangerousHtml: true,
+ })
+ .processSync(md);
+};
+
+describe("remarkDirectiveTypst", () => {
+ it("should transform preview mode", async () => {
+ expect(
+ toHtml(
+ `
+:::typst{mode="preview"}
+
+\`\`\`typ
+= Hello World!
+\`\`\`
+
+:::
+`,
+ ctx,
+ ).value,
+ ).toMatchSnapshot();
+ });
+
+ it("should transform edit mode", async () => {
+ expect(
+ toHtml(
+ `
+:::typst{mode="edit"}
+
+\`\`\`typ
+= Hello World!
+
+This is editable content.
+\`\`\`
+
+:::
+`,
+ ctx,
+ ).value,
+ ).toMatchSnapshot();
+ });
+
+ it("should default to preview mode", async () => {
+ expect(
+ toHtml(
+ `
+:::typst
+
+\`\`\`typ
+= Default Mode
+\`\`\`
+
+:::
+`,
+ ctx,
+ ).value,
+ ).toMatchSnapshot();
+ });
+
+ it("should register directives", async () => {
+ expect(
+ toHtml(
+ `
+:::typst{mode="preview"}
+
+\`\`\`typ
+= Test
+\`\`\`
+
+:::
+`,
+ ctx,
+ ).data.directives?.["typst"],
+ ).toBeDefined();
+ });
+
+ it("should accept custom height", async () => {
+ const result = toHtml(
+ `
+:::typst{mode="preview" height=600}
+
+\`\`\`typ
+= Custom Height
+\`\`\`
+
+:::
+`,
+ ctx,
+ ).value;
+ expect(result).toContain("height: 600px");
+ });
+
+ it("should handle typst language code block", async () => {
+ expect(
+ toHtml(
+ `
+:::typst{mode="preview"}
+
+\`\`\`typst
+= Using typst language
+\`\`\`
+
+:::
+`,
+ ctx,
+ ).value,
+ ).toMatchSnapshot();
+ });
+});
diff --git a/platforms/vscode/snipptes/hyperbook.code-snippets b/platforms/vscode/snipptes/hyperbook.code-snippets
index ef3bc63d..25d1270c 100644
--- a/platforms/vscode/snipptes/hyperbook.code-snippets
+++ b/platforms/vscode/snipptes/hyperbook.code-snippets
@@ -269,5 +269,17 @@
"body": [
":Snippet{#${1:snippet-name}}"
]
+ },
+ "Element Typst": {
+ "prefix": [":typst"],
+ "body": [
+ ":::typst{mode=\"${1|preview,edit|}\"}",
+ "",
+ "```typ",
+ "$TM_SELECTED_TEXT$0",
+ "```",
+ "",
+ ":::"
+ ]
}
}
diff --git a/platforms/vscode/syntaxes/hyperbook.json b/platforms/vscode/syntaxes/hyperbook.json
index a6c78704..6991f728 100644
--- a/platforms/vscode/syntaxes/hyperbook.json
+++ b/platforms/vscode/syntaxes/hyperbook.json
@@ -110,6 +110,9 @@
},
{
"include": "#directive-webide"
+ },
+ {
+ "include": "#directive-typst"
}
],
"repository": {
@@ -1443,6 +1446,55 @@
"include": "text.html.markdown"
}
]
+ },
+ "directive-typst": {
+ "name": "meta.directive.typst.hyperbook",
+ "begin": "(^|\\G)(:{3,})(typst)({.*})?\\s*$",
+ "end": "(^|\\G)(\\2|:{3,})\\s*$",
+ "beginCaptures": {
+ "2": {
+ "name": "punctuation.definition.directive.level.hyperbook"
+ },
+ "3": {
+ "name": "entity.name.function.directive.typst.hyperbook"
+ },
+ "4": {
+ "patterns": [
+ {
+ "match": "(mode)=\"([^\"]*)\"",
+ "captures": {
+ "1": {
+ "name": "keyword.parameter.directive.typst.hyperbook"
+ },
+ "2": {
+ "name": "string.quoted.double.untitled"
+ }
+ }
+ },
+ {
+ "match": "(height)=([0-9]+)",
+ "captures": {
+ "1": {
+ "name": "keyword.parameter.directive.typst.hyperbook"
+ },
+ "2": {
+ "name": "constant.numeric"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "endCaptures": {
+ "2": {
+ "name": "punctuation.definition.directive.level.hyperbook"
+ }
+ },
+ "patterns": [
+ {
+ "include": "text.html.markdown"
+ }
+ ]
}
}
}
diff --git a/website/de/book/elements/typst.md b/website/de/book/elements/typst.md
new file mode 100644
index 00000000..973256af
--- /dev/null
+++ b/website/de/book/elements/typst.md
@@ -0,0 +1,152 @@
+---
+name: Typst
+permaid: typst
+---
+
+# Typst
+
+Die Typst-Direktive ermöglicht es, [Typst](https://typst.app/)-Dokumente direkt in Ihrem Hyperbook zu rendern. Typst ist ein modernes, Markup-basiertes Satzsystem, das leicht zu erlernen ist und schöne Dokumente erstellt.
+
+## Verwendung
+
+Um die Typst-Direktive zu verwenden, umschließen Sie Ihren Typst-Code in einem `:::typst`-Block mit einem Code-Block, der die Sprache `typ` oder `typst` verwendet.
+
+### Vorschau-Modus
+
+Im Vorschau-Modus wird nur die gerenderte Ausgabe angezeigt, zusammen mit einer Download-Schaltfläche zum Exportieren als PDF.
+
+````md
+:::typst{mode="preview"}
+
+```typ
+= Hallo Welt!
+
+Dies ist ein einfaches Typst-Dokument.
+
+- Erster Punkt
+- Zweiter Punkt
+- Dritter Punkt
+```
+
+:::
+````
+
+:::typst{mode="preview"}
+
+```typ
+= Hallo Welt!
+
+Dies ist ein einfaches Typst-Dokument.
+
+- Erster Punkt
+- Zweiter Punkt
+- Dritter Punkt
+```
+
+:::
+
+### Bearbeiten-Modus
+
+Im Bearbeiten-Modus wird ein Editor neben der Vorschau angezeigt, mit dem Benutzer den Typst-Code ändern und Live-Updates sehen können.
+
+````md
+:::typst{mode="edit"}
+
+```typ
+= Interaktives Dokument
+
+Sie können diesen Text bearbeiten und die Änderungen live sehen!
+
+$ sum_(i=1)^n i = (n(n+1))/2 $
+```
+
+:::
+````
+
+:::typst{mode="edit"}
+
+```typ
+= Interaktives Dokument
+
+Sie können diesen Text bearbeiten und die Änderungen live sehen!
+
+$ sum_(i=1)^n i = (n(n+1))/2 $
+```
+
+:::
+
+## Optionen
+
+| Option | Beschreibung | Standard |
+|--------|--------------|----------|
+| `mode` | Anzeigemodus: `preview` (nur Ansicht) oder `edit` (mit Editor) | `preview` |
+| `height` | Höhe des Vorschau-Containers. Akzeptiert CSS-Werte wie `100px`, `50vh`, `calc(100dvh - 200px)` | `auto` für preview, `calc(100dvh - 128px)` für edit |
+| `src` | Pfad zu einer externen `.typ`-Datei | - |
+
+### Laden aus externer Datei
+
+Sie können Typst-Code aus einer externen Datei laden, indem Sie das `src`-Attribut verwenden:
+
+````md
+:::typst{mode="preview" src="dokument.typ"}
+:::
+````
+
+Die Datei wird in folgenden Verzeichnissen gesucht (in dieser Reihenfolge):
+1. `public/`-Verzeichnis
+2. `book/`-Verzeichnis
+3. Verzeichnis der aktuellen Seite
+
+## Beispiele
+
+### Mathematische Formeln
+
+:::typst{mode="preview" height=300}
+
+```typ
+= Mathematische Formeln
+
+Typst unterstützt schöne mathematische Formeln:
+
+$ integral_0^infinity e^(-x^2) dif x = sqrt(pi)/2 $
+
+Die quadratische Formel:
+
+$ x = (-b plus.minus sqrt(b^2 - 4a c)) / (2a) $
+```
+
+:::
+
+### Tabellen
+
+:::typst{mode="preview" height=250}
+
+```typ
+= Datentabelle
+
+#table(
+ columns: (auto, auto, auto),
+ [*Name*], [*Alter*], [*Stadt*],
+ [Alice], [25], [Berlin],
+ [Bob], [30], [München],
+ [Carol], [28], [Hamburg],
+)
+```
+
+:::
+
+### Code-Blöcke
+
+:::typst{mode="preview" height=200}
+
+```typ
+= Code-Beispiel
+
+Hier ist etwas Inline-`Code` und ein Code-Block:
+
+#raw(block: true, lang: "python",
+"def hallo():
+ print('Hallo, Welt!')")
+```
+
+:::
diff --git a/website/en/book/elements/typst.md b/website/en/book/elements/typst.md
new file mode 100644
index 00000000..41e884b1
--- /dev/null
+++ b/website/en/book/elements/typst.md
@@ -0,0 +1,152 @@
+---
+name: Typst
+permaid: typst
+---
+
+# Typst
+
+The Typst directive allows you to render [Typst](https://typst.app/) documents directly in your hyperbook. Typst is a modern markup-based typesetting system that is easy to learn and produces beautiful documents.
+
+## Usage
+
+To use the Typst directive, wrap your Typst code in a `:::typst` block with a code block using the `typ` or `typst` language.
+
+### Preview Mode
+
+In preview mode, only the rendered output is shown with a download button for exporting to PDF.
+
+````md
+:::typst{mode="preview"}
+
+```typ
+= Hello World!
+
+This is a simple Typst document.
+
+- First item
+- Second item
+- Third item
+```
+
+:::
+````
+
+:::typst{mode="preview"}
+
+```typ
+= Hello World!
+
+This is a simple Typst document.
+
+- First item
+- Second item
+- Third item
+```
+
+:::
+
+### Edit Mode
+
+In edit mode, an editor is shown alongside the preview, allowing users to modify the Typst code and see live updates.
+
+````md
+:::typst{mode="edit"}
+
+```typ
+= Interactive Document
+
+You can edit this text and see the changes live!
+
+$ sum_(i=1)^n i = (n(n+1))/2 $
+```
+
+:::
+````
+
+:::typst{mode="edit"}
+
+```typ
+= Interactive Document
+
+You can edit this text and see the changes live!
+
+$ sum_(i=1)^n i = (n(n+1))/2 $
+```
+
+:::
+
+## Options
+
+| Option | Description | Default |
+|--------|-------------|---------|
+| `mode` | Display mode: `preview` (view only) or `edit` (with editor) | `preview` |
+| `height` | Height of the preview container. Accepts CSS values like `100px`, `50vh`, `calc(100dvh - 200px)` | `auto` for preview, `calc(100dvh - 128px)` for edit |
+| `src` | Path to an external `.typ` file to load | - |
+
+### Loading from External File
+
+You can load Typst code from an external file using the `src` attribute:
+
+````md
+:::typst{mode="preview" src="document.typ"}
+:::
+````
+
+The file is searched in the following locations (in order):
+1. `public/` directory
+2. `book/` directory
+3. Current page's directory
+
+## Examples
+
+### Mathematical Formulas
+
+:::typst{mode="preview" height=300}
+
+```typ
+= Mathematical Formulas
+
+Typst supports beautiful mathematical typesetting:
+
+$ integral_0^infinity e^(-x^2) dif x = sqrt(pi)/2 $
+
+The quadratic formula:
+
+$ x = (-b plus.minus sqrt(b^2 - 4a c)) / (2a) $
+```
+
+:::
+
+### Tables
+
+:::typst{mode="preview" height=250}
+
+```typ
+= Data Table
+
+#table(
+ columns: (auto, auto, auto),
+ [*Name*], [*Age*], [*City*],
+ [Alice], [25], [Berlin],
+ [Bob], [30], [Munich],
+ [Carol], [28], [Hamburg],
+)
+```
+
+:::
+
+### Code Blocks
+
+:::typst{mode="preview" height=200}
+
+```typ
+= Code Example
+
+Here is some inline `code` and a code block:
+
+#raw(block: true, lang: "python",
+"def hello():
+ print('Hello, World!')")
+```
+
+:::