Skip to content
Draft
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
165 changes: 165 additions & 0 deletions packages/markdown/assets/directive-typst/client.js
Original file line number Diff line number Diff line change
@@ -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 = `<div class="typst-error">${error.message || "Error rendering Typst"}</div>`;
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 {};
})();
165 changes: 165 additions & 0 deletions packages/markdown/assets/directive-typst/style.css
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
1 change: 1 addition & 0 deletions packages/markdown/assets/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
});
Expand Down
9 changes: 8 additions & 1 deletion packages/markdown/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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..."
}
9 changes: 8 additions & 1 deletion packages/markdown/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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..."
}
2 changes: 2 additions & 0 deletions packages/markdown/src/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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 */
Expand Down
Loading