diff --git a/.changeset/add-typst-directive.md b/.changeset/add-typst-directive.md
new file mode 100644
index 00000000..bdae5f41
--- /dev/null
+++ b/.changeset/add-typst-directive.md
@@ -0,0 +1,5 @@
+---
+"@hyperbook/markdown": minor
+---
+
+Add Typst directive with interactive editor and improved error handling. The new Typst directive enables users to write and preview Typst documents directly in Hyperbook with support for multiple files, binary assets, and PDF export. Errors display as dismissible overlays preserving the last successful render, with clean error messages parsed from the Rust SourceDiagnostic format.
diff --git a/packages/markdown/assets/directive-typst/client.js b/packages/markdown/assets/directive-typst/client.js
new file mode 100644
index 00000000..9279f384
--- /dev/null
+++ b/packages/markdown/assets/directive-typst/client.js
@@ -0,0 +1,631 @@
+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;
+
+ // Rendering queue to ensure only one render at a time
+ let renderQueue = Promise.resolve();
+
+ const queueRender = (renderFn) => {
+ renderQueue = renderQueue.then(renderFn).catch((error) => {
+ console.error("Queued render error:", error);
+ });
+ return renderQueue;
+ };
+
+ 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;
+ };
+
+ // Parse error message from SourceDiagnostic format
+ const parseTypstError = (errorMessage) => {
+ try {
+ // Try to extract message from SourceDiagnostic format
+ const match = errorMessage.match(/message:\s*"([^"]+)"/);
+ if (match) {
+ return match[1];
+ }
+ } catch (e) {
+ // Fallback to original message
+ }
+ return errorMessage;
+ };
+
+ // Render typst code to SVG
+ const renderTypst = async (code, container, loadingIndicator, sourceFiles, binaryFiles, id, previewContainer) => {
+ // Queue this render to ensure only one compilation runs at a time
+ return queueRender(async () => {
+ // Show loading indicator
+ if (loadingIndicator) {
+ loadingIndicator.style.display = "flex";
+ }
+
+ await loadTypst();
+
+ try {
+ // Reset shadow files for this render
+ $typst.resetShadow();
+
+ // Add source files
+ for (const { filename, content } of sourceFiles) {
+ const path = filename.startsWith('/') ? filename.substring(1) : filename;
+ await $typst.addSource(`/${path}`, content);
+ }
+
+ // Add binary files
+ for (const { dest, url } of binaryFiles) {
+ try {
+ let arrayBuffer;
+
+ // Check if URL is a data URL (user-uploaded file)
+ if (url.startsWith('data:')) {
+ const response = await fetch(url);
+ arrayBuffer = await response.arrayBuffer();
+ } else {
+ // External URL
+ const response = await fetch(url);
+ if (!response.ok) {
+ console.warn(`Failed to load binary file: ${url}`);
+ continue;
+ }
+ arrayBuffer = await response.arrayBuffer();
+ }
+
+ const path = dest.startsWith('/') ? dest.substring(1) : dest;
+ $typst.mapShadow(`/${path}`, new Uint8Array(arrayBuffer));
+ } catch (error) {
+ console.warn(`Error loading binary file ${url}:`, error);
+ }
+ }
+
+ const svg = await $typst.svg({ mainContent: code });
+
+ // Remove any existing error overlay from preview-container
+ if (previewContainer) {
+ const existingError = previewContainer.querySelector('.typst-error-overlay');
+ if (existingError) {
+ existingError.remove();
+ }
+ }
+
+ container.innerHTML = svg;
+
+ // Scale SVG to fit container
+ const svgElem = container.firstElementChild;
+ if (svgElem) {
+ const width = Number.parseFloat(svgElem.getAttribute("width"));
+ const height = Number.parseFloat(svgElem.getAttribute("height"));
+ const containerWidth = container.clientWidth - 20;
+ if (width > 0 && containerWidth > 0) {
+ svgElem.setAttribute("width", containerWidth);
+ svgElem.setAttribute("height", (height * containerWidth) / width);
+ }
+ }
+ } catch (error) {
+ const errorText = parseTypstError(error || "Error rendering Typst");
+
+ // Check if we have existing content (previous successful render)
+ const hasExistingContent = container.querySelector('svg') !== null;
+
+ // Always use error overlay in preview-container if available
+ if (previewContainer) {
+ // Remove any existing error overlay
+ const existingError = previewContainer.querySelector('.typst-error-overlay');
+ if (existingError) {
+ existingError.remove();
+ }
+
+ // Clear preview if no existing content
+ if (!hasExistingContent) {
+ container.innerHTML = '';
+ }
+
+ // Create floating error overlay in preview-container
+ const errorOverlay = document.createElement('div');
+ errorOverlay.className = 'typst-error-overlay';
+ errorOverlay.innerHTML = `
+
+ `;
+
+ // Add close button functionality
+ const closeBtn = errorOverlay.querySelector('.typst-error-close');
+ closeBtn.addEventListener('click', () => {
+ errorOverlay.remove();
+ });
+
+ previewContainer.appendChild(errorOverlay);
+ } else {
+ // Fallback: show error in preview container directly
+ container.innerHTML = `${errorText}
`;
+ }
+ } finally {
+ // Hide loading indicator
+ if (loadingIndicator) {
+ loadingIndicator.style.display = "none";
+ }
+ }
+ });
+ };
+
+ // Export to PDF
+ const exportPdf = async (code, id, sourceFiles, binaryFiles) => {
+ // Queue this export to ensure only one compilation runs at a time
+ return queueRender(async () => {
+ await loadTypst();
+
+ try {
+ // Reset shadow files for this export
+ $typst.resetShadow();
+
+ // Add source files
+ for (const { filename, content } of sourceFiles) {
+ const path = filename.startsWith('/') ? filename.substring(1) : filename;
+ await $typst.addSource(`/${path}`, content);
+ }
+
+ // Add binary files
+ for (const { dest, url } of binaryFiles) {
+ try {
+ let arrayBuffer;
+
+ // Check if URL is a data URL (user-uploaded file)
+ if (url.startsWith('data:')) {
+ const response = await fetch(url);
+ arrayBuffer = await response.arrayBuffer();
+ } else {
+ // External URL
+ const response = await fetch(url);
+ if (!response.ok) {
+ continue;
+ }
+ arrayBuffer = await response.arrayBuffer();
+ }
+
+ const path = dest.startsWith('/') ? dest.substring(1) : dest;
+ $typst.mapShadow(`/${path}`, new Uint8Array(arrayBuffer));
+ } catch (error) {
+ console.warn(`Error loading binary file ${url}:`, error);
+ }
+ }
+
+ 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 previewContainer = elem.querySelector(".preview-container");
+ 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 downloadProjectBtn = elem.querySelector(".download-project");
+ const resetBtn = elem.querySelector(".reset");
+ const sourceTextarea = elem.querySelector(".typst-source");
+ const tabsList = elem.querySelector(".tabs-list");
+ const binaryFilesList = elem.querySelector(".binary-files-list");
+ const addSourceFileBtn = elem.querySelector(".add-source-file");
+ const addBinaryFileBtn = elem.querySelector(".add-binary-file");
+
+ // Parse source files and binary files from data attributes
+ const sourceFilesData = elem.getAttribute("data-source-files");
+ const binaryFilesData = elem.getAttribute("data-binary-files");
+
+ let sourceFiles = sourceFilesData
+ ? JSON.parse(atob(sourceFilesData))
+ : [];
+ let binaryFiles = binaryFilesData
+ ? JSON.parse(atob(binaryFilesData))
+ : [];
+
+ // Track current active file
+ let currentFile = sourceFiles.find(f => f.filename === "main.typ" || f.filename === "main.typst") || sourceFiles[0];
+
+ // Store file contents in memory
+ const fileContents = new Map();
+ sourceFiles.forEach(f => fileContents.set(f.filename, f.content));
+
+ // Function to update tabs UI
+ const updateTabs = () => {
+ if (!tabsList) return;
+
+ tabsList.innerHTML = "";
+
+ // Add source file tabs
+ sourceFiles.forEach(file => {
+ const tab = document.createElement("div");
+ tab.className = "file-tab";
+ if (file.filename === currentFile.filename) {
+ tab.classList.add("active");
+ }
+
+ const tabName = document.createElement("span");
+ tabName.className = "tab-name";
+ tabName.textContent = file.filename;
+ tab.appendChild(tabName);
+
+ // Add delete button (except for main file)
+ if (file.filename !== "main.typ" && file.filename !== "main.typst") {
+ const deleteBtn = document.createElement("button");
+ deleteBtn.className = "tab-delete";
+ deleteBtn.textContent = "×";
+ deleteBtn.title = i18n.get("typst-delete-file") || "Delete file";
+ deleteBtn.addEventListener("click", (e) => {
+ e.stopPropagation();
+ if (confirm(`${i18n.get("typst-delete-confirm") || "Delete"} ${file.filename}?`)) {
+ sourceFiles = sourceFiles.filter(f => f.filename !== file.filename);
+ fileContents.delete(file.filename);
+
+ // Switch to main file if we deleted the current file
+ if (currentFile.filename === file.filename) {
+ currentFile = sourceFiles[0];
+ if (editor) {
+ editor.value = fileContents.get(currentFile.filename) || "";
+ }
+ }
+
+ updateTabs();
+ saveState();
+ rerenderTypst();
+ }
+ });
+ tab.appendChild(deleteBtn);
+ }
+
+ tab.addEventListener("click", () => {
+ if (currentFile.filename !== file.filename) {
+ // Save current file content
+ if (editor) {
+ fileContents.set(currentFile.filename, editor.value);
+ }
+
+ // Switch to new file
+ currentFile = file;
+ if (editor) {
+ editor.value = fileContents.get(currentFile.filename) || "";
+ }
+
+ updateTabs();
+ saveState();
+ }
+ });
+
+ tabsList.appendChild(tab);
+ });
+ };
+
+ // Function to update binary files list
+ const updateBinaryFilesList = () => {
+ if (!binaryFilesList) return;
+
+ binaryFilesList.innerHTML = "";
+
+ if (binaryFiles.length === 0) {
+ const emptyMsg = document.createElement("div");
+ emptyMsg.className = "binary-files-empty";
+ emptyMsg.textContent = i18n.get("typst-no-binary-files") || "No binary files";
+ binaryFilesList.appendChild(emptyMsg);
+ return;
+ }
+
+ binaryFiles.forEach(file => {
+ const item = document.createElement("div");
+ item.className = "binary-file-item";
+
+ const icon = document.createElement("span");
+ icon.className = "binary-file-icon";
+ icon.textContent = "📎";
+ item.appendChild(icon);
+
+ const name = document.createElement("span");
+ name.className = "binary-file-name";
+ name.textContent = file.dest;
+ item.appendChild(name);
+
+ const deleteBtn = document.createElement("button");
+ deleteBtn.className = "binary-file-delete";
+ deleteBtn.textContent = "×";
+ deleteBtn.title = i18n.get("typst-delete-file") || "Delete file";
+ deleteBtn.addEventListener("click", () => {
+ if (confirm(`${i18n.get("typst-delete-confirm") || "Delete"} ${file.dest}?`)) {
+ binaryFiles = binaryFiles.filter(f => f.dest !== file.dest);
+ updateBinaryFilesList();
+ saveState();
+ rerenderTypst();
+ }
+ });
+ item.appendChild(deleteBtn);
+
+ binaryFilesList.appendChild(item);
+ });
+ };
+
+ // Function to save state to store
+ const saveState = async () => {
+ if (!editor) return;
+
+ // Update current file content
+ fileContents.set(currentFile.filename, editor.value);
+
+ // Update sourceFiles array with latest content
+ sourceFiles = sourceFiles.map(f => ({
+ filename: f.filename,
+ content: fileContents.get(f.filename) || f.content
+ }));
+
+ await store.typst?.put({
+ id,
+ code: editor.value,
+ sourceFiles,
+ binaryFiles,
+ currentFile: currentFile.filename
+ });
+ };
+
+ // Function to rerender typst
+ const rerenderTypst = () => {
+ if (editor) {
+ // Update sourceFiles with current editor content
+ fileContents.set(currentFile.filename, editor.value);
+ sourceFiles = sourceFiles.map(f => ({
+ filename: f.filename,
+ content: fileContents.get(f.filename) || f.content
+ }));
+
+ const mainFile = sourceFiles.find(f => f.filename === "main.typ" || f.filename === "main.typst");
+ const mainCode = mainFile ? mainFile.content : "";
+ renderTypst(mainCode, preview, loadingIndicator, sourceFiles, binaryFiles, id, previewContainer);
+ }
+ };
+
+ // Add source file button
+ addSourceFileBtn?.addEventListener("click", () => {
+ const filename = prompt(i18n.get("typst-filename-prompt") || "Enter filename (e.g., helper.typ):");
+ if (filename) {
+ // Validate filename
+ if (!filename.endsWith(".typ") && !filename.endsWith(".typst")) {
+ alert(i18n.get("typst-filename-error") || "Filename must end with .typ or .typst");
+ return;
+ }
+
+ if (sourceFiles.some(f => f.filename === filename)) {
+ alert(i18n.get("typst-filename-exists") || "File already exists");
+ return;
+ }
+
+ // Add new file
+ const newFile = { filename, content: `// ${filename}\n` };
+ sourceFiles.push(newFile);
+ fileContents.set(filename, newFile.content);
+
+ // Switch to new file
+ if (editor) {
+ fileContents.set(currentFile.filename, editor.value);
+ }
+ currentFile = newFile;
+ if (editor) {
+ editor.value = newFile.content;
+ }
+
+ updateTabs();
+ saveState();
+ rerenderTypst();
+ }
+ });
+
+ // Add binary file button
+ addBinaryFileBtn?.addEventListener("click", (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const input = document.createElement("input");
+ input.type = "file";
+ input.accept = "image/*,.pdf";
+ input.addEventListener("change", async (e) => {
+ const file = e.target.files[0];
+ if (file) {
+ const dest = `/${file.name}`;
+
+ // Check if file already exists
+ if (binaryFiles.some(f => f.dest === dest)) {
+ if (!confirm(i18n.get("typst-file-replace") || `Replace existing ${dest}?`)) {
+ return;
+ }
+ binaryFiles = binaryFiles.filter(f => f.dest !== dest);
+ }
+
+ // Read file as data URL
+ const reader = new FileReader();
+ reader.onload = async (e) => {
+ const url = e.target.result;
+ binaryFiles.push({ dest, url });
+ updateBinaryFilesList();
+ saveState();
+ rerenderTypst();
+ };
+ reader.readAsDataURL(file);
+ }
+ });
+ input.click();
+ });
+
+ // 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;
+
+ // Restore sourceFiles and binaryFiles if available
+ if (result.sourceFiles) {
+ sourceFiles = result.sourceFiles;
+ sourceFiles.forEach(f => fileContents.set(f.filename, f.content));
+ }
+ if (result.binaryFiles) {
+ binaryFiles = result.binaryFiles;
+ }
+ if (result.currentFile) {
+ currentFile = sourceFiles.find(f => f.filename === result.currentFile) || sourceFiles[0];
+ editor.value = fileContents.get(currentFile.filename) || "";
+ }
+ }
+ initialCode = editor.value;
+
+ updateTabs();
+ updateBinaryFilesList();
+ rerenderTypst();
+
+ // Listen for input changes
+ editor.addEventListener("input", () => {
+ saveState();
+ rerenderTypst();
+ });
+ });
+ } else if (sourceTextarea) {
+ // Preview mode - code is in hidden textarea
+ initialCode = sourceTextarea.value;
+ loadTypst().then(() => {
+ renderTypst(initialCode, preview, loadingIndicator, sourceFiles, binaryFiles, id, previewContainer);
+ });
+ }
+
+ // Download PDF button
+ downloadBtn?.addEventListener("click", async () => {
+ // Get the main file content
+ const mainFile = sourceFiles.find(f => f.filename === "main.typ" || f.filename === "main.typst");
+ const code = mainFile ? mainFile.content : (editor ? editor.value : initialCode);
+ await exportPdf(code, id, sourceFiles, binaryFiles);
+ });
+
+ // Download Project button (ZIP with all files)
+ downloadProjectBtn?.addEventListener("click", async () => {
+ // Get the main file content
+ const mainFile = sourceFiles.find(f => f.filename === "main.typ" || f.filename === "main.typst");
+ const code = mainFile ? mainFile.content : (editor ? editor.value : initialCode);
+ const encoder = new TextEncoder();
+ const zipFiles = {};
+
+ // Add all source files
+ for (const { filename, content } of sourceFiles) {
+ const path = filename.startsWith('/') ? filename.substring(1) : filename;
+ zipFiles[path] = encoder.encode(content);
+ }
+
+ // Add binary files
+ for (const { dest, url } of binaryFiles) {
+ try {
+ let arrayBuffer;
+
+ // Check if URL is a data URL (user-uploaded file)
+ if (url.startsWith('data:')) {
+ const response = await fetch(url);
+ arrayBuffer = await response.arrayBuffer();
+ } else {
+ // External URL
+ const response = await fetch(url);
+ if (response.ok) {
+ arrayBuffer = await response.arrayBuffer();
+ } else {
+ console.warn(`Failed to load binary file: ${url}`);
+ continue;
+ }
+ }
+
+ const path = dest.startsWith('/') ? dest.substring(1) : dest;
+ zipFiles[path] = new Uint8Array(arrayBuffer);
+ } catch (error) {
+ console.warn(`Error loading binary file ${url}:`, error);
+ }
+ }
+
+ // Create ZIP using UZIP
+ const zipData = UZIP.encode(zipFiles);
+ const zipBlob = new Blob([zipData], { type: "application/zip" });
+ const link = document.createElement("a");
+ link.href = URL.createObjectURL(zipBlob);
+ link.download = `typst-project-${id}.zip`;
+ link.click();
+ URL.revokeObjectURL(link.href);
+ });
+
+ // 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..a1b337c1
--- /dev/null
+++ b/packages/markdown/assets/directive-typst/style.css
@@ -0,0 +1,466 @@
+.directive-typst {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ margin-bottom: 16px;
+ overflow: hidden;
+ gap: 8px;
+ height: calc(100dvh - 128px);
+}
+
+code-input {
+ margin: 0;
+}
+
+.directive-typst .preview-container {
+ width: 100%;
+ border: 1px solid var(--color-spacer);
+ border-radius: 8px;
+ overflow: auto;
+ background-color: white;
+ position: relative;
+}
+
+.directive-typst .typst-preview {
+ padding: 16px;
+ min-height: 100px;
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+}
+
+.directive-typst.preview-only {
+ gap: 0px;
+ align-items: normal;
+ height: auto;
+
+ .download-pdf {
+ border-right: none;
+ }
+
+ .preview-container {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+}
+
+.directive-typst .typst-preview svg {
+ max-width: 100%;
+ height: auto;
+}
+
+.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-brand, #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 .typst-error-overlay {
+ position: absolute;
+ bottom: 16px;
+ left: 16px;
+ right: 16px;
+ z-index: 100;
+ animation: typst-error-slide-in 0.3s ease-out;
+}
+
+@keyframes typst-error-slide-in {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.directive-typst .typst-error-content {
+ background-color: #fef2f2;
+ border: 2px solid #dc2626;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ overflow: hidden;
+}
+
+.directive-typst .typst-error-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 16px;
+ background-color: #dc2626;
+ color: white;
+}
+
+.directive-typst .typst-error-title {
+ font-weight: 600;
+ font-size: 14px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.directive-typst .typst-error-close {
+ background: none;
+ border: none;
+ color: white;
+ cursor: pointer;
+ font-size: 24px;
+ line-height: 1;
+ padding: 0;
+ margin: 0;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 4px;
+ transition: background-color 0.2s;
+ flex: none;
+}
+
+.directive-typst .typst-error-close:hover {
+ background-color: rgba(255, 255, 255, 0.2);
+}
+
+.directive-typst .typst-error-message {
+ padding: 16px;
+ color: #dc2626;
+ font-family: monospace;
+ font-size: 13px;
+ white-space: pre-wrap;
+ word-break: break-word;
+ line-height: 1.5;
+}
+
+.directive-typst .editor-container {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ height: 400px;
+}
+
+.directive-typst .file-tabs {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ background-color: var(--color--background);
+ border: 1px solid var(--color-spacer);
+ border-bottom: none;
+ border-radius: 8px 8px 0 0;
+ padding: 4px;
+ overflow-x: auto;
+}
+
+.directive-typst .tabs-list {
+ display: flex;
+ gap: 4px;
+ flex: 1;
+ overflow-x: auto;
+ padding: 2px;
+}
+
+.directive-typst .file-tab {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 12px;
+ background-color: var(--color--background);
+ border: 1px solid var(--color-spacer);
+ border-radius: 4px;
+ cursor: pointer;
+ white-space: nowrap;
+ font-size: 14px;
+ transition: all 0.2s;
+}
+
+.directive-typst .file-tab:hover {
+ background-color: var(--color-spacer);
+}
+
+.directive-typst .file-tab.active {
+ background-color: var(--color-brand, #3b82f6);
+ color: var(--color-brand-text);
+ border-color: var(--color-brand, #3b82f6);
+}
+
+.directive-typst .tab-name {
+ flex: 1;
+}
+
+.directive-typst .tab-delete {
+ background: none;
+ border: none;
+ color: inherit;
+ cursor: pointer;
+ font-size: 18px;
+ line-height: 1;
+ padding: 0;
+ margin: 0;
+ width: 18px;
+ height: 18px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 2px;
+ flex-shrink: 0;
+}
+
+.directive-typst .tab-delete:hover {
+ background-color: rgba(0, 0, 0, 0.1);
+}
+
+.directive-typst .file-tab.active .tab-delete:hover {
+ background-color: rgba(255, 255, 255, 0.2);
+}
+
+.directive-typst .add-source-file {
+ padding: 6px 12px;
+ font-size: 16px;
+ flex: none;
+ font-weight: bold;
+ white-space: nowrap;
+ border: 1px solid var(--color-spacer);
+ border-radius: 4px;
+ background-color: var(--color--background);
+ color: var(--color-text);
+ cursor: pointer;
+ min-width: 32px;
+}
+
+.directive-typst .add-source-file:hover {
+ background-color: var(--color-spacer);
+}
+
+.directive-typst .binary-files-section {
+ border: 1px solid var(--color-spacer);
+ border-top: none;
+ border-bottom: none;
+ background-color: var(--color--background);
+ margin-bottom: 0;
+}
+
+.directive-typst .binary-files-section summary {
+ padding: 8px 12px;
+ cursor: pointer;
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ user-select: none;
+ background-color: var(--color--background);
+ list-style: none;
+}
+
+.directive-typst .binary-files-section summary::-webkit-details-marker {
+ display: none;
+}
+
+.directive-typst .binary-files-section summary .summary-text {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.directive-typst .binary-files-section summary .summary-indicator {
+ transition: transform 0.2s;
+ display: inline-block;
+ font-size: 12px;
+}
+
+.directive-typst .binary-files-section[open] summary .summary-indicator {
+ transform: rotate(90deg);
+}
+
+.directive-typst .binary-files-section summary:hover {
+ background-color: var(--color-spacer);
+}
+
+.directive-typst .add-binary-file {
+ padding: 6px 12px;
+ font-size: 14px;
+ border: 1px solid var(--color-spacer);
+ border-radius: 4px;
+ background-color: var(--color--background);
+ color: var(--color-text);
+ cursor: pointer;
+ width: 100%;
+}
+
+.directive-typst .add-binary-file:hover {
+ background-color: var(--color-spacer);
+}
+
+.directive-typst .binary-files-actions {
+ padding: 8px;
+}
+
+.directive-typst .binary-files-list {
+ padding: 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.directive-typst .binary-files-empty {
+ padding: 12px;
+ text-align: center;
+ color: var(--color-text);
+ opacity: 0.6;
+ font-style: italic;
+ font-size: 14px;
+}
+
+.directive-typst .binary-file-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ font-size: 14px;
+}
+
+.directive-typst .binary-file-icon {
+ font-size: 16px;
+ flex-shrink: 0;
+}
+
+.directive-typst .binary-file-name {
+ flex: 1;
+ word-break: break-all;
+}
+
+.directive-typst .binary-file-delete {
+ background: none;
+ border: none;
+ color: var(--color-text);
+ cursor: pointer;
+ font-size: 18px;
+ line-height: 1;
+ padding: 0;
+ margin: 0;
+ width: 20px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 2px;
+ flex: none;
+}
+
+.directive-typst .binary-file-delete:hover {
+ background-color: rgba(220, 38, 38, 0.1);
+ color: #dc2626;
+}
+
+.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;
+ justify-content: center;
+ width: 100%;
+}
+
+.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:not(.active) {
+ opacity: 0.6;
+}
+
+.directive-typst .buttons:last-child {
+ border-right: none;
+}
+
+.directive-typst button:hover {
+ background-color: var(--color-spacer);
+}
+
+.directive-typst .buttons-container button {
+ flex: none;
+ border: 1px solid var(--color-spacer);
+ border-radius: 8px;
+ padding: 8px 24px;
+}
+
+.directive-typst .hidden {
+ display: none !important;
+}
+
+@media screen and (min-width: 1024px) {
+ .directive-typst:not(.preview-only) {
+ flex-direction: row;
+ height: calc(100dvh - 128px);
+
+ .preview-container {
+ flex: 1;
+ height: 100% !important;
+ }
+
+ .editor-container {
+ flex: 1;
+ height: 100%;
+ overflow: hidden;
+ }
+ }
+}
diff --git a/packages/markdown/assets/prism/prism-typst.js b/packages/markdown/assets/prism/prism-typst.js
new file mode 100644
index 00000000..aef645ae
--- /dev/null
+++ b/packages/markdown/assets/prism/prism-typst.js
@@ -0,0 +1,182 @@
+
+const comment = [
+ {
+ pattern: /\/\*[\s\S]*?\*\//,
+ greedy: true,
+ },
+ {
+ pattern: /\/\/.*/,
+ greedy: true,
+ },
+];
+
+const raw = [
+ {
+ pattern: /^```[\s\S]*?^```$/m,
+ greedy: true,
+ },
+ {
+ pattern: /`[\s\S]*?`/,
+ greedy: true,
+ }
+];
+
+const label = {
+ pattern: /<[\w\-.:]*>/, // starting with ([^\\]) not necessary anymore when matching "escaped" before
+};
+
+const general_escapes = [
+ /\\u{[\da-fA-F]+?\}/,
+ /\\\S/,
+ /\\\s/,
+]
+
+Prism.languages["typst-math"] = {
+ comment: comment,
+ raw: raw,
+ escaped: general_escapes,
+ operator: [
+ /<=>|<==>|<-->|\[\||\|\]|\|\||:=|::=|\.\.\.|=:|!=|>>>|>=|<<<|<==|<=|\|->|=>|\|=>|==>|-->|~~>|~>|>->|->>|<--|<~~|<~|<-<|<<-|<->|->|<-|<<|>>/,
+ /[_\\\^\+\-\*\/&']/,
+ ],
+ string: [
+ /"(?:\\.|[^\\"])*"/,
+ /\$/
+ ],
+ function: /\b[a-zA-Z][\w-]*(?=\[|\()/,
+ symbol: [
+ /[a-zA-Z][\w]+/,
+ /#[a-zA-Z][\w]*/,
+ ]
+}
+
+const math = [
+ {
+ pattern: /\$(?:\\.|[^\\$])*?\$/,
+ inside: Prism.languages["typst-math"],
+ // greedy: true,
+ },
+]
+
+
+Prism.languages["typst-code"] = {
+ typst: {
+ // pattern: /\[[\s\S]*\]/,
+ pattern: /\[(?:[^\]\[]|\[(?:[^\]\[]|\[(?:[^\]\[]|\[[^\]\[]*\])*\])*\])*\]/,
+ inside: Prism.languages["typst"],
+ greedy: true
+ },
+ comment: comment,
+ math: math,
+ function: [
+ {
+ pattern: /#?[a-zA-Z][\w\-]*?(?=\[|\()/,
+ greedy: true
+ },
+ /(?<=show [\w.]*)[a-zA-Z][\w-]*\s*:/,
+ /(?<=show\s*:\s*)[a-zA-Z][\w-]*/,
+ ],
+ keyword: /(?:#|\b)(?:none|auto|let|return|if|else|set|show|context|for|while|not|in|continue|break|include|import|as)\b/,
+ boolean: /(?:#|\b)(?:true|false)\b/,
+ string: {
+ pattern: /"(?:\\.|[^\\"])*"/,
+ greedy: true,
+ },
+ label: label,
+ number: [
+ /0b[01]+/,
+ /0o[0-7]+/,
+ /0x[\da-fA-F]+/,
+ /(?|<=|\.\.|<|>/,
+ punctuation: /[\{\}\(\):\,\;\.]/
+};
+
+
+Prism.languages.typst = {
+ comment: comment,
+ raw: raw,
+ math: math,
+ "code-mode": [
+ {
+ // enter code mode via #my-func() or #()
+ // pattern: /(?<=#[a-zA-Z][\w-.]*\()(?:[^)(]|\((?:[^)(]|\((?:[^)(]|\([^)(]*\))*\))*\))*\)/,
+ // pattern: /(?<=#([a-zA-Z][\w-.]*)?\()(?:[^)(]|\((?:[^)(]|\((?:[^)(]|\((?:[^)(]|\((?:[^)(]|\([^)(]*\))*\))*\))*\))*\))*\)/,
+ // between # and ( either
+ // - nothing: #(
+ // - Declaration: #let ... (
+ // - Function call: #my-func2(
+ // #
+ pattern: /(?> 8) & 255;
+ },
+ readUint: function (buff, p) {
+ return (
+ buff[p + 3] * (256 * 256 * 256) +
+ ((buff[p + 2] << 16) | (buff[p + 1] << 8) | buff[p])
+ );
+ },
+ writeUint: function (buff, p, n) {
+ buff[p] = n & 255;
+ buff[p + 1] = (n >> 8) & 255;
+ buff[p + 2] = (n >> 16) & 255;
+ buff[p + 3] = (n >> 24) & 255;
+ },
+ readASCII: function (buff, p, l) {
+ var s = "";
+ for (var i = 0; i < l; i++) s += String.fromCharCode(buff[p + i]);
+ return s;
+ },
+ writeASCII: function (data, p, s) {
+ for (var i = 0; i < s.length; i++) data[p + i] = s.charCodeAt(i);
+ },
+ pad: function (n) {
+ return n.length < 2 ? "0" + n : n;
+ },
+ readIBM: function (buff, p, l) {
+ var codes = [
+ 0xc7, 0xfc, 0xe9, 0xe2, 0xe4, 0xe0, 0xe5, 0xe7, 0xea, 0xeb, 0xe8, 0xef,
+ 0xee, 0xec, 0xc4, 0xc5, 0xc9, 0xe6, 0xc6, 0xf4, 0xf6, 0xf2, 0xfb, 0xf9,
+ 0xff, 0xd6, 0xdc, 0xa2, 0xa3, 0xa5, 0xa7, 0x192, 0xe1, 0xed, 0xf3, 0xfa,
+ 0xf1, 0xd1, 0xaa, 0xba, 0xbf, 0x2310, 0xac, 0xbd, 0xbc, 0xa1, 0xab,
+ 0xbb,
+ ];
+ var out = "";
+ for (var i = 0; i < l; i++) {
+ var cc = buff[p + i];
+ if (cc < 0x80) cc = cc;
+ else if (cc < 0xb0) cc = codes[cc - 0x80];
+ else return null;
+ out += String.fromCharCode(cc);
+ }
+ return out;
+ },
+ readUTF8: function (buff, p, l) {
+ var s = "",
+ ns;
+ for (var i = 0; i < l; i++) s += "%" + B.pad(buff[p + i].toString(16));
+ try {
+ ns = decodeURIComponent(s);
+ } catch (e) {
+ return B.readASCII(buff, p, l);
+ }
+ return ns;
+ },
+ writeUTF8: function (buff, p, str) {
+ var strl = str.length,
+ i = 0;
+ for (var ci = 0; ci < strl; ci++) {
+ var code = str.charCodeAt(ci);
+ if ((code & (0xffffffff - (1 << 7) + 1)) == 0) {
+ buff[p + i] = code;
+ i++;
+ } else if ((code & (0xffffffff - (1 << 11) + 1)) == 0) {
+ buff[p + i] = 192 | (code >> 6);
+ buff[p + i + 1] = 128 | ((code >> 0) & 63);
+ i += 2;
+ } else if ((code & (0xffffffff - (1 << 16) + 1)) == 0) {
+ buff[p + i] = 224 | (code >> 12);
+ buff[p + i + 1] = 128 | ((code >> 6) & 63);
+ buff[p + i + 2] = 128 | ((code >> 0) & 63);
+ i += 3;
+ } else if ((code & (0xffffffff - (1 << 21) + 1)) == 0) {
+ buff[p + i] = 240 | (code >> 18);
+ buff[p + i + 1] = 128 | ((code >> 12) & 63);
+ buff[p + i + 2] = 128 | ((code >> 6) & 63);
+ buff[p + i + 3] = 128 | ((code >> 0) & 63);
+ i += 4;
+ } else throw "e";
+ }
+ return i;
+ },
+ sizeUTF8: function (str) {
+ var strl = str.length,
+ i = 0;
+ for (var ci = 0; ci < strl; ci++) {
+ var code = str.charCodeAt(ci);
+ if ((code & (0xffffffff - (1 << 7) + 1)) == 0) {
+ i++;
+ } else if ((code & (0xffffffff - (1 << 11) + 1)) == 0) {
+ i += 2;
+ } else if ((code & (0xffffffff - (1 << 16) + 1)) == 0) {
+ i += 3;
+ } else if ((code & (0xffffffff - (1 << 21) + 1)) == 0) {
+ i += 4;
+ } else throw "e";
+ }
+ return i;
+ },
+ };
+
+ var crc = {
+ table: (function () {
+ var tab = new Uint32Array(256);
+ for (var n = 0; n < 256; n++) {
+ var c = n;
+ for (var k = 0; k < 8; k++) {
+ if (c & 1) c = 0xedb88320 ^ (c >>> 1);
+ else c = c >>> 1;
+ }
+ tab[n] = c;
+ }
+ return tab;
+ })(),
+ update: function (c, buf, off, len) {
+ for (var i = 0; i < len; i++)
+ c = crc.table[(c ^ buf[off + i]) & 0xff] ^ (c >>> 8);
+ return c;
+ },
+ crc: function (b, o, l) {
+ return crc.update(0xffffffff, b, o, l) ^ 0xffffffff;
+ },
+ };
+
+ function adler(data, o, len) {
+ var a = 1,
+ b = 0;
+ var off = o,
+ end = o + len;
+ while (off < end) {
+ var eend = Math.min(off + 5552, end);
+ while (off < eend) {
+ a += data[off++];
+ b += a;
+ }
+ a = a % 65521;
+ b = b % 65521;
+ }
+ return (b << 16) | a;
+ }
+
+ function parseTar(data) {
+ var off = 0,
+ out = {};
+ while (off + 1024 < data.length) {
+ var no = off;
+ while (data[no] != 0) no++;
+ var nam = B.readASCII(data, off, no - off);
+ off += 100;
+ off += 24;
+ var sz = parseInt(B.readASCII(data, off, 12), 8);
+ off += 12;
+ var tm = parseInt(B.readASCII(data, off, 12), 8);
+ off += 12;
+ // console.log(nam, sz, tm);
+ off += 8 + 1 + 100;
+ off += 6 + 2 + 32 + 32 + 8 + 8 + 155 + 12;
+
+ out[nam] = data.slice(off, off + sz);
+ off += sz;
+
+ var ex = off & 0x1ff;
+ if (ex != 0) off += 512 - ex;
+ }
+ return out;
+ }
+ /*
+ var parse7z = function() {
+ var rUs = B.readUshort, rUi = B.readUint;
+ var data, off;
+
+ function NUMBER() {
+ var v = data[off++];
+ if ((v&128)==0) return v;
+ else if((v& 64)==0) return ((((((v&63)<<8)+data[off++]) ) ) );
+ else if((v& 32)==0) return ((((((v&31)<<8)+data[off++])<<8)+data[off++]) );
+ else if((v& 16)==0) return ((((((v&31)<<8)+data[off++])<<8)+data[off++])<<8)+data[off++];
+ else throw v.toString(2);
+ }
+
+ function readProp() {
+ var ID = data[off++]; console.log(ID.toString(16));
+
+ if (ID==0x17) readProp(off); // EncodedHeader
+ else if(ID==0x06) { // PackInfo
+ var pos = NUMBER();
+ var cnt = NUMBER(); if(cnt!=1) throw "e";
+
+ console.log(pos, cnt);
+
+ console.log(data.slice(off));
+
+ var siz = readProp(); console.log(siz);
+
+ console.log(data.slice(off));
+
+ var crc = readProp(); console.log(crc);
+
+
+ console.log(data.slice(off));
+ }
+ else if(ID==0x09) { // Size
+ return NUMBER();
+ }
+ else throw ID;
+
+ }
+
+ function pars(_d) {
+ data = _d; off=8;
+
+ var sign = [55, 122, 188, 175, 39, 28, 0, 4];
+ for(var i=0; i<8; i++) if(data[i]!=sign[i]) throw "e";
+
+ var crc = rUi(data,off); off+=4;
+
+ var nho = rUi(data,off); off+=8;
+ var nhs = rUi(data,off); off+=8;
+ var nhc = rUi(data,off); off+=4;
+ console.log(nho,nhs,nhc);
+
+ //var hdr=data[off++];
+
+ //console.log(hdr);
+
+ off = 32+nho;
+ readProp();
+
+
+
+
+
+ console.log(data.slice(off)); throw "e";
+ }
+
+ return pars;
+ }();
+ */
+
+ function parse(buf, onlyNames) {
+ // ArrayBuffer
+ var rUs = B.readUshort,
+ rUi = B.readUint,
+ o = 0,
+ out = {};
+ var data = new Uint8Array(buf);
+ if (data.length > 257 + 6 && B.readASCII(data, 257, 6) == "ustar ")
+ return parseTar(data);
+ //if(B.readASCII(data,0,2)=="7z") return parse7z(data);
+
+ var eocd = data.length - 4;
+
+ while (rUi(data, eocd) != 0x06054b50) eocd--;
+
+ var o = eocd;
+ o += 4; // sign = 0x06054b50
+ o += 4; // disks = 0;
+ var cnu = rUs(data, o);
+ o += 2;
+ var cnt = rUs(data, o);
+ o += 2;
+
+ var csize = rUi(data, o);
+ o += 4;
+ var coffs = rUi(data, o);
+ o += 4;
+
+ o = coffs;
+ for (var i = 0; i < cnu; i++) {
+ var sign = rUi(data, o);
+ o += 4;
+ o += 4; // versions;
+ o += 4; // flag + compr
+ var time = _readTime(data, o);
+ o += 4; // time
+
+ var crc32 = rUi(data, o);
+ o += 4;
+ var csize = rUi(data, o);
+ o += 4;
+ var usize = rUi(data, o);
+ o += 4;
+
+ var nl = rUs(data, o),
+ el = rUs(data, o + 2),
+ cl = rUs(data, o + 4);
+ o += 6; // name, extra, comment
+ o += 8; // disk, attribs
+ var roff = rUi(data, o);
+ o += 4;
+
+ o += nl;
+
+ var lo = 0;
+ while (lo < el) {
+ var id = rUs(data, o + lo);
+ lo += 2;
+ var sz = rUs(data, o + lo);
+ lo += 2;
+ if (id == 1) {
+ // Zip64
+ if (usize == 0xffffffff) {
+ usize = rUi(data, o + lo);
+ lo += 8;
+ }
+ if (csize == 0xffffffff) {
+ csize = rUi(data, o + lo);
+ lo += 8;
+ }
+ if (roff == 0xffffffff) {
+ roff = rUi(data, o + lo);
+ lo += 8;
+ }
+ } else lo += sz;
+ }
+
+ o += el + cl;
+
+ _readLocal(data, roff, out, csize, usize, onlyNames);
+ }
+ //console.log(out);
+ return out;
+ }
+
+ function _readTime(data, o) {
+ var time = B.readUshort(data, o),
+ date = B.readUshort(data, o + 2);
+ var year = 1980 + (date >>> 9);
+ var mont = (date >>> 5) & 15;
+ var day = date & 31;
+ //console.log(year,mont,day);
+
+ var hour = time >>> 11;
+ var minu = (time >>> 5) & 63;
+ var seco = 2 * (time & 31);
+
+ var stamp = new Date(year, mont, day, hour, minu, seco).getTime();
+
+ //console.log(date,time);
+ return stamp;
+ }
+ function _writeTime(data, o, stamp) {
+ var dt = new Date(stamp);
+ var date =
+ ((dt.getFullYear() - 1980) << 9) |
+ ((dt.getMonth() + 1) << 5) |
+ dt.getDate();
+ var time =
+ (dt.getHours() << 11) | (dt.getMinutes() << 5) | (dt.getSeconds() >>> 1);
+ B.writeUshort(data, o, time);
+ B.writeUshort(data, o + 2, date);
+ }
+
+ function _readLocal(data, o, out, csize, usize, onlyNames) {
+ var rUs = B.readUshort,
+ rUi = B.readUint;
+ var sign = rUi(data, o);
+ o += 4;
+ var ver = rUs(data, o);
+ o += 2;
+ var gpflg = rUs(data, o);
+ o += 2;
+ //if((gpflg&8)!=0) throw "unknown sizes";
+ var cmpr = rUs(data, o);
+ o += 2;
+
+ var time = _readTime(data, o);
+ o += 4;
+
+ var crc32 = rUi(data, o);
+ o += 4;
+ //var csize = rUi(data, o); o+=4;
+ //var usize = rUi(data, o); o+=4;
+ o += 8;
+
+ var nlen = rUs(data, o);
+ o += 2;
+ var elen = rUs(data, o);
+ o += 2;
+
+ var name =
+ (gpflg & 2048) == 0
+ ? B.readIBM(data, o, nlen)
+ : B.readUTF8(data, o, nlen);
+ if (name == null) name = B.readUTF8(data, o, nlen);
+ o += nlen; //console.log(name);
+ o += elen;
+
+ //console.log(sign.toString(16), ver, gpflg, cmpr, crc32.toString(16), "csize, usize", csize, usize, nlen, elen, name, o);
+ if (onlyNames) {
+ out[name] = { size: usize, csize: csize };
+ return;
+ }
+ var file = new Uint8Array(data.buffer, o);
+ if (gpflg & 1) {
+ out[name] = new Uint8Array(0);
+ alert("ZIPs with a password are not supported.", 3000);
+ } else if (cmpr == 0)
+ out[name] = new Uint8Array(file.buffer.slice(o, o + csize));
+ else if (cmpr == 8) {
+ var buf = new Uint8Array(usize);
+ inflateRaw(file, buf);
+ /*var nbuf = pako["inflateRaw"](file);
+ if(usize>8514000) {
+ //console.log(PUtils.readASCII(buf , 8514500, 500));
+ //console.log(PUtils.readASCII(nbuf, 8514500, 500));
+ }
+ for(var i=0; i>> 4;
+ //console.log(CM, CINFO,CMF,FLG);
+ return inflateRaw(
+ new Uint8Array(file.buffer, file.byteOffset + 2, file.length - 6),
+ buf,
+ );
+ }
+ function deflate(data, opts /*, buf, off*/) {
+ if (opts == null) opts = { level: 6 };
+ var off = 0,
+ buf = new Uint8Array(50 + Math.floor(data.length * 1.1));
+ buf[off] = 120;
+ buf[off + 1] = 156;
+ off += 2;
+ off = UZIP["F"]["deflateRaw"](data, buf, off, opts["level"]);
+ var crc = adler(data, 0, data.length);
+ buf[off + 0] = (crc >>> 24) & 255;
+ buf[off + 1] = (crc >>> 16) & 255;
+ buf[off + 2] = (crc >>> 8) & 255;
+ buf[off + 3] = (crc >>> 0) & 255;
+ return new Uint8Array(buf.buffer, 0, off + 4);
+ }
+ function deflateRaw(data, opts) {
+ if (opts == null) opts = { level: 6 };
+ var buf = new Uint8Array(50 + Math.floor(data.length * 1.1));
+ var off = UZIP["F"]["deflateRaw"](data, buf, off, opts["level"]);
+ return new Uint8Array(buf.buffer, 0, off);
+ }
+
+ function encode(obj, noCmpr) {
+ if (noCmpr == null) noCmpr = false;
+ var tot = 0,
+ wUi = B.writeUint,
+ wUs = B.writeUshort;
+ var zpd = {};
+ for (var p in obj) {
+ var cpr = !_noNeed(p) && !noCmpr,
+ buf = obj[p],
+ cr = crc.crc(buf, 0, buf.length);
+ zpd[p] = {
+ cpr: cpr,
+ usize: buf.length,
+ crc: cr,
+ file: cpr ? deflateRaw(buf) : buf,
+ };
+ }
+
+ for (var p in zpd) tot += zpd[p].file.length + 30 + 46 + 2 * B.sizeUTF8(p);
+ tot += 22;
+
+ var data = new Uint8Array(tot),
+ o = 0;
+ var fof = [];
+
+ for (var p in zpd) {
+ var file = zpd[p];
+ fof.push(o);
+ o = _writeHeader(data, o, p, file, 0);
+ }
+ var i = 0,
+ ioff = o;
+ for (var p in zpd) {
+ var file = zpd[p];
+ fof.push(o);
+ o = _writeHeader(data, o, p, file, 1, fof[i++]);
+ }
+ var csize = o - ioff;
+
+ wUi(data, o, 0x06054b50);
+ o += 4;
+ o += 4; // disks
+ wUs(data, o, i);
+ o += 2;
+ wUs(data, o, i);
+ o += 2; // number of c d records
+ wUi(data, o, csize);
+ o += 4;
+ wUi(data, o, ioff);
+ o += 4;
+ o += 2;
+ return data.buffer;
+ }
+ // no need to compress .PNG, .ZIP, .JPEG ....
+ function _noNeed(fn) {
+ var ext = fn.split(".").pop().toLowerCase();
+ return "png,jpg,jpeg,zip".indexOf(ext) != -1;
+ }
+
+ function _writeHeader(data, o, p, obj, t, roff) {
+ // it is a task of a user to provide valid file names
+ //var bad = "#%&{}\<>*?$'\":@+`|=";
+ //for(var i=0; i> 1;
+ var cl = tree[i + 1],
+ val = (lit << 4) | cl; // : (0x8000 | (U.of0[lit-257]<<7) | (U.exb[lit-257]<<4) | cl);
+ var rest = MAX_BITS - cl,
+ i0 = tree[i] << rest,
+ i1 = i0 + (1 << rest);
+ //tree[i]=r15[i0]>>>(15-MAX_BITS);
+ while (i0 != i1) {
+ var p0 = r15[i0] >>> (15 - MAX_BITS);
+ map[p0] = val;
+ i0++;
+ }
+ }
+ }
+ function revCodes(tree, MAX_BITS) {
+ var r15 = U.rev15,
+ imb = 15 - MAX_BITS;
+ for (var i = 0; i < tree.length; i += 2) {
+ var i0 = tree[i] << (MAX_BITS - tree[i + 1]);
+ tree[i] = r15[i0] >>> imb;
+ }
+ }
+
+ // used only in deflate
+ function _putsE(dt, pos, val) {
+ val = val << (pos & 7);
+ var o = pos >>> 3;
+ dt[o] |= val;
+ dt[o + 1] |= val >>> 8;
+ }
+ function _putsF(dt, pos, val) {
+ val = val << (pos & 7);
+ var o = pos >>> 3;
+ dt[o] |= val;
+ dt[o + 1] |= val >>> 8;
+ dt[o + 2] |= val >>> 16;
+ }
+
+ function _bitsE(dt, pos, length) {
+ return (
+ ((dt[pos >>> 3] | (dt[(pos >>> 3) + 1] << 8)) >>> (pos & 7)) &
+ ((1 << length) - 1)
+ );
+ }
+ function _bitsF(dt, pos, length) {
+ return (
+ ((dt[pos >>> 3] |
+ (dt[(pos >>> 3) + 1] << 8) |
+ (dt[(pos >>> 3) + 2] << 16)) >>>
+ (pos & 7)) &
+ ((1 << length) - 1)
+ );
+ }
+
+ function _get17(dt, pos) {
+ // return at least 17 meaningful bytes
+ return (
+ (dt[pos >>> 3] |
+ (dt[(pos >>> 3) + 1] << 8) |
+ (dt[(pos >>> 3) + 2] << 16)) >>>
+ (pos & 7)
+ );
+ }
+ function _get25(dt, pos) {
+ // return at least 17 meaningful bytes
+ return (
+ (dt[pos >>> 3] |
+ (dt[(pos >>> 3) + 1] << 8) |
+ (dt[(pos >>> 3) + 2] << 16) |
+ (dt[(pos >>> 3) + 3] << 24)) >>>
+ (pos & 7)
+ );
+ }
+
+ (function () {
+ var len = 1 << 15;
+ for (var i = 0; i < len; i++) {
+ var x = i;
+ x = ((x & 0xaaaaaaaa) >>> 1) | ((x & 0x55555555) << 1);
+ x = ((x & 0xcccccccc) >>> 2) | ((x & 0x33333333) << 2);
+ x = ((x & 0xf0f0f0f0) >>> 4) | ((x & 0x0f0f0f0f) << 4);
+ x = ((x & 0xff00ff00) >>> 8) | ((x & 0x00ff00ff) << 8);
+ U.rev15[i] = ((x >>> 16) | (x << 16)) >>> 17;
+ }
+
+ function pushV(tgt, n, sv) {
+ while (n-- != 0) tgt.push(0, sv);
+ }
+
+ for (var i = 0; i < 32; i++) {
+ U.ldef[i] = (U.of0[i] << 3) | U.exb[i];
+ U.ddef[i] = (U.df0[i] << 4) | U.dxb[i];
+ }
+
+ pushV(U.fltree, 144, 8);
+ pushV(U.fltree, 255 - 143, 9);
+ pushV(U.fltree, 279 - 255, 7);
+ pushV(U.fltree, 287 - 279, 8);
+ /*
+ var i = 0;
+ for(; i<=143; i++) U.fltree.push(0,8);
+ for(; i<=255; i++) U.fltree.push(0,9);
+ for(; i<=279; i++) U.fltree.push(0,7);
+ for(; i<=287; i++) U.fltree.push(0,8);
+ */
+ makeCodes(U.fltree, 9);
+ codes2map(U.fltree, 9, U.flmap);
+ revCodes(U.fltree, 9);
+
+ pushV(U.fdtree, 32, 5);
+ //for(i=0;i<32; i++) U.fdtree.push(0,5);
+ makeCodes(U.fdtree, 5);
+ codes2map(U.fdtree, 5, U.fdmap);
+ revCodes(U.fdtree, 5);
+
+ pushV(U.itree, 19, 0);
+ pushV(U.ltree, 286, 0);
+ pushV(U.dtree, 30, 0);
+ pushV(U.ttree, 320, 0);
+ /*
+ for(var i=0; i< 19; i++) U.itree.push(0,0);
+ for(var i=0; i<286; i++) U.ltree.push(0,0);
+ for(var i=0; i< 30; i++) U.dtree.push(0,0);
+ for(var i=0; i<320; i++) U.ttree.push(0,0);
+ */
+ })();
+
+ function deflateRaw(data, out, opos, lvl) {
+ var opts = [
+ /*
+ ush good_length; /* reduce lazy search above this match length
+ ush max_lazy; /* do not perform lazy search above this match length
+ ush nice_length; /* quit search above this match length
+ */
+ /* good lazy nice chain */
+ /* 0 */ [0, 0, 0, 0, 0] /* store only */,
+ /* 1 */ [4, 4, 8, 4, 0] /* max speed, no lazy matches */,
+ /* 2 */ [4, 5, 16, 8, 0],
+ /* 3 */ [4, 6, 16, 16, 0],
+
+ /* 4 */ [4, 10, 16, 32, 0] /* lazy matches */,
+ /* 5 */ [8, 16, 32, 32, 0],
+ /* 6 */ [8, 16, 128, 128, 0],
+ /* 7 */ [8, 32, 128, 256, 0],
+ /* 8 */ [32, 128, 258, 1024, 1],
+ /* 9 */ [32, 258, 258, 4096, 1],
+ ]; /* max compression */
+
+ var opt = opts[lvl];
+
+ //var U = UZIP.F.U, goodIndex = UZIP.F._goodIndex, hash = UZIP.F._hash, putsE = UZIP.F._putsE;
+ var i = 0,
+ pos = opos << 3,
+ cvrd = 0,
+ dlen = data.length;
+
+ if (lvl == 0) {
+ while (i < dlen) {
+ var len = Math.min(0xffff, dlen - i);
+ _putsE(out, pos, i + len == dlen ? 1 : 0);
+ pos = _copyExact(data, i, len, out, pos + 8);
+ i += len;
+ }
+ return pos >>> 3;
+ }
+
+ var lits = U.lits,
+ strt = U.strt,
+ prev = U.prev,
+ li = 0,
+ lc = 0,
+ bs = 0,
+ ebits = 0,
+ c = 0,
+ nc = 0; // last_item, literal_count, block_start
+ if (dlen > 2) {
+ nc = _hash(data, 0);
+ strt[nc] = 0;
+ }
+ var nmch = 0,
+ nmci = 0;
+
+ for (i = 0; i < dlen; i++) {
+ c = nc;
+ //*
+ if (i + 1 < dlen - 2) {
+ nc = _hash(data, i + 1);
+ var ii = (i + 1) & 0x7fff;
+ prev[ii] = strt[nc];
+ strt[nc] = ii;
+ } //*/
+ if (cvrd <= i) {
+ if ((li > 14000 || lc > 26697) && dlen - i > 100) {
+ if (cvrd < i) {
+ lits[li] = i - cvrd;
+ li += 2;
+ cvrd = i;
+ }
+ pos = _writeBlock(
+ i == dlen - 1 || cvrd == dlen ? 1 : 0,
+ lits,
+ li,
+ ebits,
+ data,
+ bs,
+ i - bs,
+ out,
+ pos,
+ );
+ li = lc = ebits = 0;
+ bs = i;
+ }
+
+ var mch = 0;
+ //if(nmci==i) mch= nmch; else
+ if (i < dlen - 2)
+ mch = _bestMatch(
+ data,
+ i,
+ prev,
+ c,
+ Math.min(opt[2], dlen - i),
+ opt[3],
+ );
+ /*
+ if(mch!=0 && opt[4]==1 && (mch>>>16)>>16)>(mch>>>16)) mch=0;
+ }//*/
+ var len = mch >>> 16,
+ dst = mch & 0xffff; //if(i-dst<0) throw "e";
+ if (mch != 0) {
+ var len = mch >>> 16,
+ dst = mch & 0xffff; //if(i-dst<0) throw "e";
+ var lgi = _goodIndex(len, U.of0);
+ U.lhst[257 + lgi]++;
+ var dgi = _goodIndex(dst, U.df0);
+ U.dhst[dgi]++;
+ ebits += U.exb[lgi] + U.dxb[dgi];
+ lits[li] = (len << 23) | (i - cvrd);
+ lits[li + 1] = (dst << 16) | (lgi << 8) | dgi;
+ li += 2;
+ cvrd = i + len;
+ } else {
+ U.lhst[data[i]]++;
+ }
+ lc++;
+ }
+ }
+ if (bs != i || data.length == 0) {
+ if (cvrd < i) {
+ lits[li] = i - cvrd;
+ li += 2;
+ cvrd = i;
+ }
+ pos = _writeBlock(1, lits, li, ebits, data, bs, i - bs, out, pos);
+ li = 0;
+ lc = 0;
+ li = lc = ebits = 0;
+ bs = i;
+ }
+ while ((pos & 7) != 0) pos++;
+ return pos >>> 3;
+ }
+ function _bestMatch(data, i, prev, c, nice, chain) {
+ var ci = i & 0x7fff,
+ pi = prev[ci];
+ //console.log("----", i);
+ var dif = (ci - pi + (1 << 15)) & 0x7fff;
+ if (pi == ci || c != _hash(data, i - dif)) return 0;
+ var tl = 0,
+ td = 0; // top length, top distance
+ var dlim = Math.min(0x7fff, i);
+ while (
+ dif <= dlim &&
+ --chain != 0 &&
+ pi != ci /*&& c==UZIP.F._hash(data,i-dif)*/
+ ) {
+ if (tl == 0 || data[i + tl] == data[i + tl - dif]) {
+ var cl = _howLong(data, i, dif);
+ if (cl > tl) {
+ tl = cl;
+ td = dif;
+ if (tl >= nice) break; //*
+ if (dif + 2 < cl) cl = dif + 2;
+ var maxd = 0; // pi does not point to the start of the word
+ for (var j = 0; j < cl - 2; j++) {
+ var ei = (i - dif + j + (1 << 15)) & 0x7fff;
+ var li = prev[ei];
+ var curd = (ei - li + (1 << 15)) & 0x7fff;
+ if (curd > maxd) {
+ maxd = curd;
+ pi = ei;
+ }
+ } //*/
+ }
+ }
+
+ ci = pi;
+ pi = prev[ci];
+ dif += (ci - pi + (1 << 15)) & 0x7fff;
+ }
+ return (tl << 16) | td;
+ }
+ function _howLong(data, i, dif) {
+ if (
+ data[i] != data[i - dif] ||
+ data[i + 1] != data[i + 1 - dif] ||
+ data[i + 2] != data[i + 2 - dif]
+ )
+ return 0;
+ var oi = i,
+ l = Math.min(data.length, i + 258);
+ i += 3;
+ //while(i+4>> 23,
+ end = off + (qb & ((1 << 23) - 1));
+ while (off < end) pos = _writeLit(data[off++], ltree, out, pos);
+
+ if (len != 0) {
+ var qc = lits[si + 1],
+ dst = qc >> 16,
+ lgi = (qc >> 8) & 255,
+ dgi = qc & 255;
+ pos = _writeLit(257 + lgi, ltree, out, pos);
+ _putsE(out, pos, len - U.of0[lgi]);
+ pos += U.exb[lgi];
+
+ pos = _writeLit(dgi, dtree, out, pos);
+ _putsF(out, pos, dst - U.df0[dgi]);
+ pos += U.dxb[dgi];
+ off += len;
+ }
+ }
+ pos = _writeLit(256, ltree, out, pos);
+ }
+ //console.log(pos-opos, fxdSize, dynSize, cstSize);
+ return pos;
+ }
+ function _copyExact(data, off, len, out, pos) {
+ var p8 = pos >>> 3;
+ out[p8] = len;
+ out[p8 + 1] = len >>> 8;
+ out[p8 + 2] = 255 - out[p8];
+ out[p8 + 3] = 255 - out[p8 + 1];
+ p8 += 4;
+ out.set(new Uint8Array(data.buffer, off, len), p8);
+ //for(var i=0; i 4 && U.itree[(U.ordr[numh - 1] << 1) + 1] == 0) numh--;
+ return [ML, MD, MH, numl, numd, numh, lset, dset];
+ }
+ function getSecond(a) {
+ var b = [];
+ for (var i = 0; i < a.length; i += 2) b.push(a[i + 1]);
+ return b;
+ }
+ function nonZero(a) {
+ var b = "";
+ for (var i = 0; i < a.length; i += 2)
+ if (a[i + 1] != 0) b += (i >> 1) + ",";
+ return b;
+ }
+ function contSize(tree, hst) {
+ var s = 0;
+ for (var i = 0; i < hst.length; i++) s += hst[i] * tree[(i << 1) + 1];
+ return s;
+ }
+ function _codeTiny(set, tree, out, pos) {
+ for (var i = 0; i < set.length; i += 2) {
+ var l = set[i],
+ rst = set[i + 1]; //console.log(l, pos, tree[(l<<1)+1]);
+ pos = _writeLit(l, tree, out, pos);
+ var rsl = l == 16 ? 2 : l == 17 ? 3 : 7;
+ if (l > 15) {
+ _putsE(out, pos, rst, rsl);
+ pos += rsl;
+ }
+ }
+ return pos;
+ }
+ function _lenCodes(tree, set) {
+ var len = tree.length;
+ while (len != 2 && tree[len - 1] == 0) len -= 2; // when no distances, keep one code with length 0
+ for (var i = 0; i < len; i += 2) {
+ var l = tree[i + 1],
+ nxt = i + 3 < len ? tree[i + 3] : -1,
+ nnxt = i + 5 < len ? tree[i + 5] : -1,
+ prv = i == 0 ? -1 : tree[i - 1];
+ if (l == 0 && nxt == l && nnxt == l) {
+ var lz = i + 5;
+ while (lz + 2 < len && tree[lz + 2] == l) lz += 2;
+ var zc = Math.min((lz + 1 - i) >>> 1, 138);
+ if (zc < 11) set.push(17, zc - 3);
+ else set.push(18, zc - 11);
+ i += zc * 2 - 2;
+ } else if (l == prv && nxt == l && nnxt == l) {
+ var lz = i + 5;
+ while (lz + 2 < len && tree[lz + 2] == l) lz += 2;
+ var zc = Math.min((lz + 1 - i) >>> 1, 6);
+ set.push(16, zc - 3);
+ i += zc * 2 - 2;
+ } else set.push(l, 0);
+ }
+ return len >>> 1;
+ }
+ function _hufTree(hst, tree, MAXL) {
+ var list = [],
+ hl = hst.length,
+ tl = tree.length,
+ i = 0;
+ for (i = 0; i < tl; i += 2) {
+ tree[i] = 0;
+ tree[i + 1] = 0;
+ }
+ for (i = 0; i < hl; i++) if (hst[i] != 0) list.push({ lit: i, f: hst[i] });
+ var end = list.length,
+ l2 = list.slice(0);
+ if (end == 0) return 0; // empty histogram (usually for dist)
+ if (end == 1) {
+ var lit = list[0].lit,
+ l2 = lit == 0 ? 1 : 0;
+ tree[(lit << 1) + 1] = 1;
+ tree[(l2 << 1) + 1] = 1;
+ return 1;
+ }
+ list.sort(function (a, b) {
+ return a.f - b.f;
+ });
+ var a = list[0],
+ b = list[1],
+ i0 = 0,
+ i1 = 1,
+ i2 = 2;
+ list[0] = { lit: -1, f: a.f + b.f, l: a, r: b, d: 0 };
+ while (i1 != end - 1) {
+ if (i0 != i1 && (i2 == end || list[i0].f < list[i2].f)) {
+ a = list[i0++];
+ } else {
+ a = list[i2++];
+ }
+ if (i0 != i1 && (i2 == end || list[i0].f < list[i2].f)) {
+ b = list[i0++];
+ } else {
+ b = list[i2++];
+ }
+ list[i1++] = { lit: -1, f: a.f + b.f, l: a, r: b };
+ }
+ var maxl = setDepth(list[i1 - 1], 0);
+ if (maxl > MAXL) {
+ restrictDepth(l2, MAXL, maxl);
+ maxl = MAXL;
+ }
+ for (i = 0; i < end; i++) tree[(l2[i].lit << 1) + 1] = l2[i].d;
+ return maxl;
+ }
+
+ function setDepth(t, d) {
+ if (t.lit != -1) {
+ t.d = d;
+ return d;
+ }
+ return Math.max(setDepth(t.l, d + 1), setDepth(t.r, d + 1));
+ }
+
+ function restrictDepth(dps, MD, maxl) {
+ var i = 0,
+ bCost = 1 << (maxl - MD),
+ dbt = 0;
+ dps.sort(function (a, b) {
+ return b.d == a.d ? a.f - b.f : b.d - a.d;
+ });
+
+ for (i = 0; i < dps.length; i++)
+ if (dps[i].d > MD) {
+ var od = dps[i].d;
+ dps[i].d = MD;
+ dbt += bCost - (1 << (maxl - od));
+ } else break;
+ dbt = dbt >>> (maxl - MD);
+ while (dbt > 0) {
+ var od = dps[i].d;
+ if (od < MD) {
+ dps[i].d++;
+ dbt -= 1 << (MD - od - 1);
+ } else i++;
+ }
+ for (; i >= 0; i--)
+ if (dps[i].d == MD && dbt < 0) {
+ dps[i].d--;
+ dbt++;
+ }
+ if (dbt != 0) console.log("debt left");
+ }
+
+ function _goodIndex(v, arr) {
+ var i = 0;
+ if (arr[i | 16] <= v) i |= 16;
+ if (arr[i | 8] <= v) i |= 8;
+ if (arr[i | 4] <= v) i |= 4;
+ if (arr[i | 2] <= v) i |= 2;
+ if (arr[i | 1] <= v) i |= 1;
+ return i;
+ }
+ function _writeLit(ch, ltree, out, pos) {
+ _putsF(out, pos, ltree[ch << 1]);
+ return pos + ltree[(ch << 1) + 1];
+ }
+
+ function inflate(data, buf) {
+ var u8 = Uint8Array;
+ if (data[0] == 3 && data[1] == 0) return buf ? buf : new u8(0);
+ //var F=UZIP.F, bitsF = F._bitsF, bitsE = F._bitsE, decodeTiny = F._decodeTiny, makeCodes = F.makeCodes, codes2map=F.codes2map, get17 = F._get17;
+
+ var noBuf = buf == null;
+ if (noBuf) buf = new u8((data.length >>> 2) << 3);
+
+ var BFINAL = 0,
+ BTYPE = 0,
+ HLIT = 0,
+ HDIST = 0,
+ HCLEN = 0,
+ ML = 0,
+ MD = 0;
+ var off = 0,
+ pos = 0;
+ var lmap, dmap;
+
+ while (BFINAL == 0) {
+ BFINAL = _bitsF(data, pos, 1);
+ BTYPE = _bitsF(data, pos + 1, 2);
+ pos += 3;
+ //console.log(BFINAL, BTYPE);
+
+ if (BTYPE == 0) {
+ if ((pos & 7) != 0) pos += 8 - (pos & 7);
+ var p8 = (pos >>> 3) + 4,
+ len = data[p8 - 4] | (data[p8 - 3] << 8); //console.log(len);//bitsF(data, pos, 16),
+ if (noBuf) buf = _check(buf, off + len);
+ buf.set(new u8(data.buffer, data.byteOffset + p8, len), off);
+ //for(var i=0; i tl) tl = l;
+ }
+ pos += 3 * HCLEN; //console.log(itree);
+ makeCodes(U.itree, tl);
+ codes2map(U.itree, tl, U.imap);
+
+ lmap = U.lmap;
+ dmap = U.dmap;
+
+ pos = _decodeTiny(
+ U.imap,
+ (1 << tl) - 1,
+ HLIT + HDIST,
+ data,
+ pos,
+ U.ttree,
+ );
+ var mx0 = _copyOut(U.ttree, 0, HLIT, U.ltree);
+ ML = (1 << mx0) - 1;
+ var mx1 = _copyOut(U.ttree, HLIT, HDIST, U.dtree);
+ MD = (1 << mx1) - 1;
+
+ //var ml = _decodeTiny(U.imap, (1<>>24))-1; pos+=(ml&0xffffff);
+ makeCodes(U.ltree, mx0);
+ codes2map(U.ltree, mx0, lmap);
+
+ //var md = _decodeTiny(U.imap, (1<>>24))-1; pos+=(md&0xffffff);
+ makeCodes(U.dtree, mx1);
+ codes2map(U.dtree, mx1, dmap);
+ }
+ //var ooff=off, opos=pos;
+ while (true) {
+ var code = lmap[_get17(data, pos) & ML];
+ pos += code & 15;
+ var lit = code >>> 4; //U.lhst[lit]++;
+ if (lit >>> 8 == 0) {
+ buf[off++] = lit;
+ } else if (lit == 256) {
+ break;
+ } else {
+ var end = off + lit - 254;
+ if (lit > 264) {
+ var ebs = U.ldef[lit - 257];
+ end = off + (ebs >>> 3) + _bitsE(data, pos, ebs & 7);
+ pos += ebs & 7;
+ }
+ //UZIP.F.dst[end-off]++;
+
+ var dcode = dmap[_get17(data, pos) & MD];
+ pos += dcode & 15;
+ var dlit = dcode >>> 4;
+ var dbs = U.ddef[dlit],
+ dst = (dbs >>> 4) + _bitsF(data, pos, dbs & 15);
+ pos += dbs & 15;
+
+ //var o0 = off-dst, stp = Math.min(end-off, dst);
+ //if(stp>20) while(off>>3);
+ }
+ //console.log(UZIP.F.dst);
+ //console.log(tlen, dlen, off-tlen+tcnt);
+ return buf.length == off ? buf : buf.slice(0, off);
+ }
+ function _check(buf, len) {
+ var bl = buf.length;
+ if (len <= bl) return buf;
+ var nbuf = new Uint8Array(Math.max(bl << 1, len));
+ nbuf.set(buf, 0);
+ //for(var i=0; i>> 4;
+ if (lit <= 15) {
+ tree[i] = lit;
+ i++;
+ } else {
+ var ll = 0,
+ n = 0;
+ if (lit == 16) {
+ n = 3 + _bitsE(data, pos, 2);
+ pos += 2;
+ ll = tree[i - 1];
+ } else if (lit == 17) {
+ n = 3 + _bitsE(data, pos, 3);
+ pos += 3;
+ } else if (lit == 18) {
+ n = 11 + _bitsE(data, pos, 7);
+ pos += 7;
+ }
+ var ni = i + n;
+ while (i < ni) {
+ tree[i] = ll;
+ i++;
+ }
+ }
+ }
+ return pos;
+ }
+ function _copyOut(src, off, len, tree) {
+ var mx = 0,
+ i = 0,
+ tl = tree.length >>> 1;
+ while (i < len) {
+ var v = src[i + off];
+ tree[i << 1] = 0;
+ tree[(i << 1) + 1] = v;
+ if (v > mx) mx = v;
+ i++;
+ }
+ while (i < tl) {
+ tree[i << 1] = 0;
+ tree[(i << 1) + 1] = 0;
+ i++;
+ }
+ return mx;
+ }
+
+ UZIP["F"] = { inflate: inflate, deflateRaw: deflateRaw };
+})();
diff --git a/packages/markdown/locales/de.json b/packages/markdown/locales/de.json
index e32e85d1..b71698b5 100644
--- a/packages/markdown/locales/de.json
+++ b/packages/markdown/locales/de.json
@@ -53,5 +53,25 @@
"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-download-project": "Projekt herunterladen",
+ "typst-loading": "Typst wird geladen...",
+ "typst-add-source-file": "Quelldatei hinzufügen",
+ "typst-add-binary-file": "Binärdatei hinzufügen",
+ "typst-source": "Quelle",
+ "typst-binary": "Binär",
+ "typst-add": "Hinzufügen",
+ "typst-delete-file": "Datei löschen",
+ "typst-delete-confirm": "Löschen",
+ "typst-filename-prompt": "Dateiname eingeben (z.B. helper.typ):",
+ "typst-filename-error": "Dateiname muss auf .typ oder .typst enden",
+ "typst-filename-exists": "Datei existiert bereits",
+ "typst-file-replace": "Existierende Datei ersetzen?",
+ "typst-binary-files": "Binärdateien",
+ "typst-no-binary-files": "Keine Binärdateien"
}
diff --git a/packages/markdown/locales/en.json b/packages/markdown/locales/en.json
index c9361eb3..d2de1e8a 100644
--- a/packages/markdown/locales/en.json
+++ b/packages/markdown/locales/en.json
@@ -53,5 +53,25 @@
"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-download-project": "Download Project",
+ "typst-loading": "Loading Typst...",
+ "typst-add-source-file": "Add source file",
+ "typst-add-binary-file": "Add binary file",
+ "typst-source": "Source",
+ "typst-binary": "Binary",
+ "typst-add": "Add",
+ "typst-delete-file": "Delete file",
+ "typst-delete-confirm": "Delete",
+ "typst-filename-prompt": "Enter filename (e.g., helper.typ):",
+ "typst-filename-error": "Filename must end with .typ or .typst",
+ "typst-filename-exists": "File already exists",
+ "typst-file-replace": "Replace existing file?",
+ "typst-binary-files": "Binary Files",
+ "typst-no-binary-files": "No binary files"
}
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/rehypeHtmlStructure.ts b/packages/markdown/src/rehypeHtmlStructure.ts
index de5725db..0ee896f3 100644
--- a/packages/markdown/src/rehypeHtmlStructure.ts
+++ b/packages/markdown/src/rehypeHtmlStructure.ts
@@ -435,6 +435,15 @@ window.Prism.manual = true;`,
},
children: [],
},
+ {
+ type: "element",
+ tagName: "script",
+ properties: {
+ src: makeUrl(["prism", "prism-typst.js"], "assets"),
+ defer: true,
+ },
+ children: [],
+ },
{
type: "element",
tagName: "link",
diff --git a/packages/markdown/src/remarkDirectiveTypst.ts b/packages/markdown/src/remarkDirectiveTypst.ts
new file mode 100644
index 00000000..05e47c8f
--- /dev/null
+++ b/packages/markdown/src/remarkDirectiveTypst.ts
@@ -0,0 +1,423 @@
+// Register directive nodes in mdast:
+///
+//
+import { HyperbookContext } from "@hyperbook/types";
+import { Code, Root, Text } 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 = "auto",
+ id = hash(node),
+ mode = "preview",
+ } = 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"]);
+ requestJS(file, ["uzip", "uzip.js"]);
+
+ const sourceFiles: { filename: string; content: string }[] = [];
+ const binaryFiles: { dest: string; url: string }[] = [];
+
+ // Parse @file and @source directives from text nodes (including nested in paragraphs)
+ for (const child of node.children) {
+ if (child.type === "text") {
+ const text = (child as Text).value;
+
+ // Parse @file directives for binary files
+ const fileMatches = text.matchAll(
+ /@file\s+dest="([^"]+)"\s+src="([^"]+)"/g,
+ );
+ for (const match of fileMatches) {
+ const dest = match[1];
+ const src = match[2];
+ const url = ctx.makeUrl(
+ src,
+ "public",
+ ctx.navigation.current || undefined,
+ );
+ binaryFiles.push({ dest, url });
+ }
+
+ // Parse @source directives for typst source files
+ const sourceMatches = text.matchAll(
+ /@source\s+dest="([^"]+)"\s+src="([^"]+)"/g,
+ );
+ for (const match of sourceMatches) {
+ const dest = match[1];
+ const src = match[2];
+ const content = readFile(src, ctx) || "";
+ sourceFiles.push({ filename: dest, content });
+ }
+ } else if (child.type === "paragraph") {
+ // Check text nodes inside paragraphs
+ for (const textNode of child.children) {
+ if (textNode.type === "text") {
+ const text = (textNode as Text).value;
+
+ // Parse @file directives for binary files
+ const fileMatches = text.matchAll(
+ /@file\s+dest="([^"]+)"\s+src="([^"]+)"/g,
+ );
+ for (const match of fileMatches) {
+ const dest = match[1];
+ const src = match[2];
+ const url = ctx.makeUrl(
+ src,
+ "public",
+ ctx.navigation.current || undefined,
+ );
+ binaryFiles.push({ dest, url });
+ }
+
+ // Parse @source directives for typst source files
+ const sourceMatches = text.matchAll(
+ /@source\s+dest="([^"]+)"\s+src="([^"]+)"/g,
+ );
+ for (const match of sourceMatches) {
+ const dest = match[1];
+ const src = match[2];
+ const content = readFile(src, ctx) || "";
+ sourceFiles.push({ filename: dest, content });
+ }
+ }
+ }
+ }
+ }
+
+ // Find all typ/typst code blocks inside the directive
+ const codeBlocks = node.children.filter(
+ (n) => n.type === "code" && (n.lang === "typ" || n.lang === "typst"),
+ ) as Code[];
+
+ if (codeBlocks.length > 0) {
+ // If multiple code blocks exist, use meta for filenames
+ for (const codeBlock of codeBlocks) {
+ const filename = codeBlock.meta?.trim() || "main.typ";
+ // Named file - add to sourceFiles
+ sourceFiles.push({
+ filename: filename,
+ content: codeBlock.value,
+ });
+ }
+ }
+ let mainSource = sourceFiles.find(
+ (f) => f.filename == "main.typ" || f.filename == "main.typst",
+ );
+ if (!mainSource && sourceFiles.length > 0) {
+ mainSource = sourceFiles[0];
+ } else if (!mainSource) {
+ mainSource = { filename: "main.typ", content: "" };
+ }
+
+ const isEditMode = mode === "edit";
+ const isPreviewMode = mode === "preview" || !isEditMode;
+
+ data.hName = "div";
+ data.hProperties = {
+ class: ["directive-typst", isPreviewMode ? "preview-only" : ""]
+ .join(" ")
+ .trim(),
+ "data-id": id,
+ "data-source-files": Buffer.from(
+ JSON.stringify(sourceFiles),
+ ).toString("base64"),
+ "data-binary-files": Buffer.from(
+ JSON.stringify(binaryFiles),
+ ).toString("base64"),
+ };
+
+ const previewContainer: Element = {
+ type: "element",
+ tagName: "div",
+ properties: {
+ class: "preview-container",
+ style: `height: ${height};`,
+ },
+ 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 downloadProjectButton: Element = {
+ type: "element",
+ tagName: "button",
+ properties: {
+ class: "download-project",
+ },
+ children: [
+ {
+ type: "text",
+ value: i18n.get("typst-download-project"),
+ },
+ ],
+ };
+
+ 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: "file-tabs",
+ },
+ children: [
+ {
+ type: "element",
+ tagName: "div",
+ properties: {
+ class: "tabs-list",
+ },
+ children: [],
+ },
+ {
+ type: "element",
+ tagName: "button",
+ properties: {
+ class: "add-source-file",
+ title: i18n.get("typst-add-source-file"),
+ },
+ children: [
+ {
+ type: "text",
+ value: "+",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: "element",
+ tagName: "details",
+ properties: {
+ class: "binary-files-section",
+ },
+ children: [
+ {
+ type: "element",
+ tagName: "summary",
+ properties: {},
+ children: [
+ {
+ type: "element",
+ tagName: "span",
+ properties: {
+ class: "summary-text",
+ },
+ children: [
+ {
+ type: "element",
+ tagName: "span",
+ properties: {
+ class: "summary-indicator",
+ },
+ children: [
+ {
+ type: "text",
+ value: "▶",
+ },
+ ],
+ },
+ {
+ type: "text",
+ value: i18n.get("typst-binary-files"),
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: "element",
+ tagName: "div",
+ properties: {
+ class: "binary-files-list",
+ },
+ children: [],
+ },
+ {
+ type: "element",
+ tagName: "div",
+ properties: {
+ class: "binary-files-actions",
+ },
+ children: [
+ {
+ type: "element",
+ tagName: "button",
+ properties: {
+ class: "add-binary-file",
+ title: i18n.get("typst-add-binary-file"),
+ },
+ children: [
+ {
+ type: "text",
+ value: "+ " + i18n.get("typst-add"),
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: "element",
+ tagName: "code-input",
+ properties: {
+ class: "editor typst active line-numbers",
+ language: "typst",
+ template: "typst-highlighted",
+ },
+ children: [
+ {
+ type: "raw",
+ value: htmlEntities(mainSource.content),
+ },
+ ],
+ },
+ {
+ type: "element",
+ tagName: "div",
+ properties: {
+ class: "buttons bottom",
+ },
+ children: [
+ {
+ type: "element",
+ tagName: "button",
+ properties: {
+ class: "reset",
+ },
+ children: [
+ {
+ type: "text",
+ value: i18n.get("typst-reset"),
+ },
+ ],
+ },
+ downloadProjectButton,
+ downloadButton,
+ ],
+ },
+ ],
+ },
+ ];
+ } else {
+ // Preview mode: show only preview with download button
+ data.hChildren = [
+ previewContainer,
+ {
+ type: "element",
+ tagName: "div",
+ properties: {
+ class: "buttons bottom",
+ },
+ children: [downloadProjectButton, downloadButton],
+ },
+ {
+ type: "element",
+ tagName: "textarea",
+ properties: {
+ class: "typst-source hidden",
+ style: "display: none;",
+ },
+ children: [
+ {
+ type: "text",
+ value: mainSource.content,
+ },
+ ],
+ },
+ ];
+ }
+ }
+ });
+ };
+};
diff --git a/packages/markdown/tests/__snapshots__/process.test.ts.snap b/packages/markdown/tests/__snapshots__/process.test.ts.snap
index a61cb8e7..046935e6 100644
--- a/packages/markdown/tests/__snapshots__/process.test.ts.snap
+++ b/packages/markdown/tests/__snapshots__/process.test.ts.snap
@@ -43,7 +43,7 @@ body {
// Remove no-js class as soon as JavaScript is available
document.documentElement.classList.remove('no-js');
@@ -70,7 +70,7 @@ HYPERBOOK_ASSETS = "assets/"
-