chore: inital commit
This commit is contained in:
11
.obsidian/plugins/obsidian-typst-math/data.json
vendored
Normal file
11
.obsidian/plugins/obsidian-typst-math/data.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"typstPath": "typst",
|
||||
"enableInlineMath": true,
|
||||
"enableDisplayMath": true,
|
||||
"debugMode": false,
|
||||
"showDetailedErrors": true,
|
||||
"useWatchMode": false,
|
||||
"enableLivePreview": false,
|
||||
"forceSyncCompile": true,
|
||||
"syncOnlyDuringExport": true
|
||||
}
|
||||
805
.obsidian/plugins/obsidian-typst-math/main.js
vendored
Normal file
805
.obsidian/plugins/obsidian-typst-math/main.js
vendored
Normal file
@@ -0,0 +1,805 @@
|
||||
/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// main.ts
|
||||
var main_exports = {};
|
||||
__export(main_exports, {
|
||||
default: () => TypstMathPlugin
|
||||
});
|
||||
module.exports = __toCommonJS(main_exports);
|
||||
var import_child_process = require("child_process");
|
||||
var import_fs = require("fs");
|
||||
var import_obsidian = require("obsidian");
|
||||
var os = __toESM(require("os"));
|
||||
var path = __toESM(require("path"));
|
||||
var DEFAULT_SETTINGS = {
|
||||
typstPath: "typst",
|
||||
enableInlineMath: true,
|
||||
enableDisplayMath: true,
|
||||
debugMode: false,
|
||||
showDetailedErrors: false,
|
||||
useWatchMode: false,
|
||||
enableLivePreview: true,
|
||||
forceSyncCompile: false,
|
||||
syncOnlyDuringExport: true
|
||||
};
|
||||
var TypstMathPlugin = class extends import_obsidian.Plugin {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
// Internal flag set when a print/export flow is active
|
||||
this.exportInProgress = false;
|
||||
// Hold original window.print so we can restore it on unload
|
||||
this._originalWindowPrint = null;
|
||||
// Keep reference to matchMedia listener so we can remove it
|
||||
this._printMediaListener = null;
|
||||
this.watchers = /* @__PURE__ */ new Map();
|
||||
// Store typst source content for blocks that are pending compilation
|
||||
this.pendingTypstContent = /* @__PURE__ */ new Map();
|
||||
this.renderCache = /* @__PURE__ */ new Map();
|
||||
this.handleBeforePrint = () => {
|
||||
this.exportInProgress = true;
|
||||
if (this.settings.debugMode)
|
||||
console.log("Typst plugin: export/print started");
|
||||
};
|
||||
this.handleAfterPrint = () => {
|
||||
this.exportInProgress = false;
|
||||
if (this.settings.debugMode)
|
||||
console.log("Typst plugin: export/print finished");
|
||||
};
|
||||
}
|
||||
async onload() {
|
||||
var _a;
|
||||
await this.loadSettings();
|
||||
this.tempDir = path.join(os.tmpdir(), "obsidian-typst-math");
|
||||
await this.ensureTempDir();
|
||||
this.registerMarkdownCodeBlockProcessor(
|
||||
"math",
|
||||
this.processMathBlock.bind(this)
|
||||
);
|
||||
this.registerMarkdownCodeBlockProcessor(
|
||||
"typst",
|
||||
this.processMathBlock.bind(this)
|
||||
);
|
||||
await (0, import_obsidian.loadMathJax)();
|
||||
if (!globalThis.MathJax) {
|
||||
new import_obsidian.Notice("MathJax failed to load. Math rendering may not work.");
|
||||
console.error("MathJax failed to load.");
|
||||
} else {
|
||||
this.originalTex2chtml = globalThis.MathJax.tex2chtml;
|
||||
globalThis.MathJax.tex2chtml = (latex, options) => {
|
||||
return this.renderWithTypst(latex, options);
|
||||
};
|
||||
const activeView = this.app.workspace.getActiveViewOfType(import_obsidian.MarkdownView);
|
||||
if (activeView) {
|
||||
(_a = activeView.previewMode) == null ? void 0 : _a.rerender(true);
|
||||
if (this.settings.enableLivePreview) {
|
||||
const editor = activeView.editor;
|
||||
if (editor) {
|
||||
const cursor = editor.getCursor();
|
||||
editor.setCursor(cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("beforeprint", this.handleBeforePrint);
|
||||
window.addEventListener("afterprint", this.handleAfterPrint);
|
||||
try {
|
||||
if (typeof window.print === "function") {
|
||||
this._originalWindowPrint = window.print.bind(window);
|
||||
window.print = (...args) => {
|
||||
this.handleBeforePrint();
|
||||
try {
|
||||
const res = this._originalWindowPrint(...args);
|
||||
setTimeout(() => this.handleAfterPrint(), 1500);
|
||||
return res;
|
||||
} catch (e) {
|
||||
this.handleAfterPrint();
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
if (this.settings.debugMode)
|
||||
console.warn("Failed to patch window.print", e);
|
||||
}
|
||||
try {
|
||||
const mql = window.matchMedia && window.matchMedia("print");
|
||||
if (mql) {
|
||||
this._printMediaListener = (ev) => {
|
||||
if (ev.matches)
|
||||
this.handleBeforePrint();
|
||||
else
|
||||
this.handleAfterPrint();
|
||||
};
|
||||
if (typeof mql.addEventListener === "function") {
|
||||
mql.addEventListener("change", this._printMediaListener);
|
||||
} else if (typeof mql.addListener === "function") {
|
||||
mql.addListener(this._printMediaListener);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (this.settings.debugMode)
|
||||
console.warn("Failed to attach matchMedia print listener", e);
|
||||
}
|
||||
}
|
||||
this.addSettingTab(new TypstMathSettingTab(this.app, this));
|
||||
this.addCommand({
|
||||
id: "typst-prepare-export",
|
||||
name: "Typst: Prepare export (sync compile all math)",
|
||||
callback: () => {
|
||||
void this.prepareExport();
|
||||
}
|
||||
});
|
||||
console.log("Typst Math Plugin loaded");
|
||||
}
|
||||
async onunload() {
|
||||
var _a;
|
||||
if (this.originalTex2chtml && globalThis.MathJax) {
|
||||
globalThis.MathJax.tex2chtml = this.originalTex2chtml;
|
||||
(_a = this.app.workspace.getActiveViewOfType(import_obsidian.MarkdownView)) == null ? void 0 : _a.previewMode.rerender(true);
|
||||
}
|
||||
for (const [id, process] of this.watchers) {
|
||||
process.kill();
|
||||
}
|
||||
this.watchers.clear();
|
||||
try {
|
||||
await import_fs.promises.rm(this.tempDir, { recursive: true, force: true });
|
||||
} catch (error) {
|
||||
console.error("Error cleaning up temp directory:", error);
|
||||
}
|
||||
try {
|
||||
if (typeof window !== "undefined") {
|
||||
window.removeEventListener("beforeprint", this.handleBeforePrint);
|
||||
window.removeEventListener("afterprint", this.handleAfterPrint);
|
||||
if (this._printMediaListener && window.matchMedia) {
|
||||
const mql = window.matchMedia("print");
|
||||
if (mql) {
|
||||
if (typeof mql.removeEventListener === "function") {
|
||||
mql.removeEventListener("change", this._printMediaListener);
|
||||
} else if (typeof mql.removeListener === "function") {
|
||||
mql.removeListener(this._printMediaListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this._originalWindowPrint) {
|
||||
try {
|
||||
window.print = this._originalWindowPrint;
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (this.settings.debugMode)
|
||||
console.warn("Error restoring print handlers", e);
|
||||
}
|
||||
console.log("Typst Math Plugin unloaded");
|
||||
}
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
async ensureTempDir() {
|
||||
try {
|
||||
await import_fs.promises.mkdir(this.tempDir, { recursive: true });
|
||||
} catch (error) {
|
||||
console.error("Error creating temp directory:", error);
|
||||
}
|
||||
}
|
||||
formatError(error, isInline = false) {
|
||||
const errorMsg = typeof error === "string" ? error : error.message;
|
||||
let friendlyMsg = "There was an error with your Typst code";
|
||||
if (errorMsg.includes("ENOENT") || errorMsg.includes("not found")) {
|
||||
friendlyMsg = "Typst CLI not found";
|
||||
} else if (errorMsg.includes("syntax error") || errorMsg.includes("unexpected")) {
|
||||
friendlyMsg = "Syntax error in Typst code";
|
||||
} else if (errorMsg.includes("undefined")) {
|
||||
friendlyMsg = "Undefined symbol or function";
|
||||
} else if (errorMsg.includes("type")) {
|
||||
friendlyMsg = "Type error in expression";
|
||||
}
|
||||
const errorClass = isInline ? "typst-error-inline" : "typst-error";
|
||||
if (this.settings.showDetailedErrors) {
|
||||
return `<span class="${errorClass}">${friendlyMsg}</span><span class="typst-error-details">${errorMsg}</span>`;
|
||||
} else {
|
||||
return `<span class="${errorClass}">${friendlyMsg}</span>`;
|
||||
}
|
||||
}
|
||||
async processMathBlock(source, el, ctx) {
|
||||
const blockId = this.generateBlockId(source, ctx.sourcePath);
|
||||
if (this.renderCache.has(blockId)) {
|
||||
el.empty();
|
||||
const container2 = el.createDiv({ cls: "typst-math-container" });
|
||||
const cached = this.renderCache.get(blockId);
|
||||
container2.innerHTML = cached != null ? cached : "";
|
||||
return;
|
||||
}
|
||||
el.empty();
|
||||
const container = el.createDiv({ cls: "typst-math-container" });
|
||||
container.innerHTML = '<div class="typst-loading">Rendering with Typst...</div>';
|
||||
container.setAttribute("data-typst-blockid", blockId);
|
||||
try {
|
||||
const typstContent = this.wrapInTypstDocument(source);
|
||||
const typstFile = path.join(this.tempDir, `${blockId}.typ`);
|
||||
const htmlFile = path.join(this.tempDir, `${blockId}.html`);
|
||||
this.pendingTypstContent.set(blockId, typstContent);
|
||||
await import_fs.promises.writeFile(typstFile, typstContent, "utf-8");
|
||||
if (this.settings.useWatchMode) {
|
||||
await this.renderTypstWithWatch(typstFile, htmlFile, container, blockId);
|
||||
} else {
|
||||
await this.renderTypstToHtml(typstFile, htmlFile, container, blockId);
|
||||
}
|
||||
} catch (error) {
|
||||
container.innerHTML = this.formatError(error, false);
|
||||
if (this.settings.debugMode) {
|
||||
console.error("Typst rendering error:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
renderWithTypst(latex, options) {
|
||||
const isBlock = options.display || false;
|
||||
if (isBlock && !this.settings.enableDisplayMath) {
|
||||
return this.originalTex2chtml(latex, options);
|
||||
}
|
||||
if (!isBlock && !this.settings.enableInlineMath) {
|
||||
return this.originalTex2chtml(latex, options);
|
||||
}
|
||||
if (this.hasLatexCommand(latex)) {
|
||||
}
|
||||
const typstContent = this.convertLatexToTypst(latex);
|
||||
const blockId = this.generateBlockId(typstContent, `math-${Date.now()}`);
|
||||
const container = document.createElement(isBlock ? "div" : "span");
|
||||
container.className = isBlock ? "typst-math-container" : "typst-math-inline";
|
||||
container.innerHTML = '<span class="typst-loading">Rendering with Typst...</span>';
|
||||
try {
|
||||
container.setAttribute("data-typst-blockid", blockId);
|
||||
this.pendingTypstContent.set(blockId, typstContent);
|
||||
} catch (e) {
|
||||
}
|
||||
this.renderTypstMath(typstContent, container, blockId, isBlock);
|
||||
return container;
|
||||
}
|
||||
hasLatexCommand(expr) {
|
||||
return false;
|
||||
}
|
||||
convertLatexToTypst(latex) {
|
||||
let content = latex.trim();
|
||||
content = content.replace(/^\$\$/, "").replace(/\$\$$/, "");
|
||||
content = content.replace(/^\$/, "").replace(/\$$/, "");
|
||||
const conversions = [
|
||||
// Fractions
|
||||
[/\\frac\{([^}]+)\}\{([^}]+)\}/g, "($1)/($2)"],
|
||||
// Superscripts and subscripts (already mostly compatible)
|
||||
// Greek letters (mostly the same, just remove backslash)
|
||||
[/\\alpha\b/g, "alpha"],
|
||||
[/\\beta\b/g, "beta"],
|
||||
[/\\gamma\b/g, "gamma"],
|
||||
[/\\delta\b/g, "delta"],
|
||||
[/\\epsilon\b/g, "epsilon"],
|
||||
[/\\zeta\b/g, "zeta"],
|
||||
[/\\eta\b/g, "eta"],
|
||||
[/\\theta\b/g, "theta"],
|
||||
[/\\iota\b/g, "iota"],
|
||||
[/\\kappa\b/g, "kappa"],
|
||||
[/\\lambda\b/g, "lambda"],
|
||||
[/\\mu\b/g, "mu"],
|
||||
[/\\nu\b/g, "nu"],
|
||||
[/\\xi\b/g, "xi"],
|
||||
[/\\pi\b/g, "pi"],
|
||||
[/\\rho\b/g, "rho"],
|
||||
[/\\sigma\b/g, "sigma"],
|
||||
[/\\tau\b/g, "tau"],
|
||||
[/\\phi\b/g, "phi"],
|
||||
[/\\chi\b/g, "chi"],
|
||||
[/\\psi\b/g, "psi"],
|
||||
[/\\omega\b/g, "omega"],
|
||||
// Capital Greek
|
||||
[/\\Gamma\b/g, "Gamma"],
|
||||
[/\\Delta\b/g, "Delta"],
|
||||
[/\\Theta\b/g, "Theta"],
|
||||
[/\\Lambda\b/g, "Lambda"],
|
||||
[/\\Xi\b/g, "Xi"],
|
||||
[/\\Pi\b/g, "Pi"],
|
||||
[/\\Sigma\b/g, "Sigma"],
|
||||
[/\\Phi\b/g, "Phi"],
|
||||
[/\\Psi\b/g, "Psi"],
|
||||
[/\\Omega\b/g, "Omega"],
|
||||
// Common functions
|
||||
[/\\sin\b/g, "sin"],
|
||||
[/\\cos\b/g, "cos"],
|
||||
[/\\tan\b/g, "tan"],
|
||||
[/\\log\b/g, "log"],
|
||||
[/\\ln\b/g, "ln"],
|
||||
[/\\exp\b/g, "exp"],
|
||||
// Sums and integrals
|
||||
[/\\sum/g, "sum"],
|
||||
[/\\prod/g, "product"],
|
||||
[/\\int/g, "integral"],
|
||||
[/\\infty\b/g, "oo"],
|
||||
// Arrows
|
||||
[/\\rightarrow\b/g, "->"],
|
||||
[/\\leftarrow\b/g, "<-"],
|
||||
[/\\Rightarrow\b/g, "=>"],
|
||||
[/\\Leftarrow\b/g, "<="],
|
||||
// Operators
|
||||
[/\\times\b/g, "times"],
|
||||
[/\\cdot\b/g, "dot"],
|
||||
[/\\pm\b/g, "plus.minus"],
|
||||
[/\\mp\b/g, "minus.plus"],
|
||||
// Special sets
|
||||
[/\\mathbb\{R\}/g, "RR"],
|
||||
[/\\mathbb\{N\}/g, "NN"],
|
||||
[/\\mathbb\{Z\}/g, "ZZ"],
|
||||
[/\\mathbb\{Q\}/g, "QQ"],
|
||||
[/\\mathbb\{C\}/g, "CC"],
|
||||
// Limits
|
||||
[/\\lim/g, "lim"],
|
||||
[/\\to\b/g, "->"],
|
||||
// Parentheses (mostly the same)
|
||||
[/\\left\(/g, "("],
|
||||
[/\\right\)/g, ")"],
|
||||
[/\\left\[/g, "["],
|
||||
[/\\right\]/g, "]"],
|
||||
[/\\left\{/g, "{"],
|
||||
[/\\right\}/g, "}"],
|
||||
// Sqrt
|
||||
[/\\sqrt\{([^}]+)\}/g, "sqrt($1)"],
|
||||
// Text
|
||||
[/\\text\{([^}]+)\}/g, '"$1"']
|
||||
];
|
||||
for (const [pattern, replacement] of conversions) {
|
||||
content = content.replace(pattern, replacement);
|
||||
}
|
||||
return content.trim();
|
||||
}
|
||||
async renderTypstMath(mathContent, container, blockId, isBlock) {
|
||||
if (this.renderCache.has(blockId)) {
|
||||
container.innerHTML = this.renderCache.get(blockId);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const wrappedContent = `$ ${mathContent} $`;
|
||||
const typstContent = this.wrapInTypstDocument(wrappedContent, isBlock);
|
||||
const typstFile = path.join(this.tempDir, `${blockId}.typ`);
|
||||
const htmlFile = path.join(this.tempDir, `${blockId}.html`);
|
||||
this.pendingTypstContent.set(blockId, typstContent);
|
||||
await import_fs.promises.writeFile(typstFile, typstContent, "utf-8");
|
||||
await this.renderTypstToHtml(typstFile, htmlFile, container, blockId);
|
||||
} catch (error) {
|
||||
container.innerHTML = this.formatError(error, true);
|
||||
if (this.settings.debugMode) {
|
||||
console.error("Typst rendering error:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Synchronously compile a single block (used by prepareExport)
|
||||
async syncCompileBlock(blockId) {
|
||||
try {
|
||||
const typstContent = this.pendingTypstContent.get(blockId);
|
||||
if (!typstContent)
|
||||
return;
|
||||
const typstFile = path.join(this.tempDir, `${blockId}.typ`);
|
||||
const htmlFile = path.join(this.tempDir, `${blockId}.html`);
|
||||
(0, import_fs.writeFileSync)(typstFile, typstContent, "utf-8");
|
||||
const args = [
|
||||
"compile",
|
||||
typstFile,
|
||||
htmlFile,
|
||||
"--features",
|
||||
"html",
|
||||
"--format",
|
||||
"html"
|
||||
];
|
||||
if (this.settings.debugMode) {
|
||||
console.log("Typst sync compile (prepareExport):", this.settings.typstPath, args.join(" "));
|
||||
}
|
||||
const res = (0, import_child_process.spawnSync)(this.settings.typstPath, args, { encoding: "utf-8" });
|
||||
if (res.error || res.status !== 0) {
|
||||
const errStr = res.stderr || res.error && res.error.message || `Typst exited ${res.status}`;
|
||||
throw new Error(errStr);
|
||||
}
|
||||
const html = (0, import_fs.readFileSync)(htmlFile, "utf-8");
|
||||
const bodyMatch = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
||||
const content = bodyMatch ? bodyMatch[1] : html;
|
||||
this.renderCache.set(blockId, content);
|
||||
try {
|
||||
const el = document.querySelector(`[data-typst-blockid="${blockId}"]`);
|
||||
if (el instanceof HTMLElement)
|
||||
el.innerHTML = content;
|
||||
} catch (e) {
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.settings.debugMode)
|
||||
console.error("Error in syncCompileBlock:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// Prepare the document for export by sync-compiling all pending Typst blocks
|
||||
async prepareExport() {
|
||||
const pending = Array.from(this.pendingTypstContent.keys());
|
||||
if (pending.length === 0) {
|
||||
new import_obsidian.Notice("No pending Typst math blocks to prepare");
|
||||
return;
|
||||
}
|
||||
new import_obsidian.Notice(`Preparing ${pending.length} Typst math blocks for export...`);
|
||||
const prevForce = this.settings.forceSyncCompile;
|
||||
const prevSyncOnly = this.settings.syncOnlyDuringExport;
|
||||
this.settings.forceSyncCompile = true;
|
||||
this.settings.syncOnlyDuringExport = false;
|
||||
this.exportInProgress = true;
|
||||
let success = 0;
|
||||
for (const id of pending) {
|
||||
try {
|
||||
await this.syncCompileBlock(id);
|
||||
success++;
|
||||
} catch (e) {
|
||||
console.error("prepareExport: failed to compile block", id, e);
|
||||
}
|
||||
}
|
||||
this.settings.forceSyncCompile = prevForce;
|
||||
this.settings.syncOnlyDuringExport = prevSyncOnly;
|
||||
this.exportInProgress = false;
|
||||
new import_obsidian.Notice(`Prepared ${success}/${pending.length} Typst math blocks for export`);
|
||||
}
|
||||
wrapInTypstDocument(mathContent, isBlock = true) {
|
||||
const sizeConfig = isBlock ? "16pt" : "14pt";
|
||||
return `
|
||||
#set text(size: ${sizeConfig})
|
||||
#show math.equation: html.frame
|
||||
#show math.equation.where(block: false): box
|
||||
|
||||
${mathContent}
|
||||
`;
|
||||
}
|
||||
generateBlockId(source, sourcePath) {
|
||||
const hash = this.simpleHash(source + sourcePath);
|
||||
return `block-${hash}`;
|
||||
}
|
||||
simpleHash(str) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
return Math.abs(hash).toString(36);
|
||||
}
|
||||
/**
|
||||
* Parse the full Typst-generated HTML and return a cleaned HTML string.
|
||||
* - Merges paginated pages if present
|
||||
* - Removes placeholder ellipsis-only nodes
|
||||
* - Strips scripts/styles that could interfere
|
||||
*/
|
||||
parseTypstHtml(html) {
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, "text/html");
|
||||
doc.querySelectorAll("script, style").forEach((n) => n.remove());
|
||||
const pageSelectors = [".typst-page", ".page", ".typst-doc", "#content", "body > div"];
|
||||
let parts = [];
|
||||
const typstDoc = doc.querySelector(".typst-doc");
|
||||
if (typstDoc) {
|
||||
const children = Array.from(typstDoc.children);
|
||||
if (children.length > 1) {
|
||||
for (const c of children)
|
||||
parts.push(c.innerHTML);
|
||||
} else {
|
||||
parts.push(typstDoc.innerHTML);
|
||||
}
|
||||
} else {
|
||||
for (const sel of pageSelectors) {
|
||||
const nodes = Array.from(doc.querySelectorAll(sel));
|
||||
if (nodes.length > 1) {
|
||||
for (const n of nodes)
|
||||
parts.push(n.innerHTML);
|
||||
break;
|
||||
} else if (nodes.length === 1 && parts.length === 0) {
|
||||
parts.push(nodes[0].innerHTML);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parts.length === 0) {
|
||||
const bodyHtml = doc.body ? doc.body.innerHTML : html;
|
||||
parts = [bodyHtml];
|
||||
}
|
||||
parts = parts.map((p) => {
|
||||
var _a;
|
||||
const partDoc = parser.parseFromString(`<div>${p}</div>`, "text/html");
|
||||
const walker = document.createTreeWalker(partDoc.body, NodeFilter.SHOW_ELEMENT, null);
|
||||
const toRemove = [];
|
||||
let node = walker.nextNode();
|
||||
while (node) {
|
||||
const el = node;
|
||||
const txt = (_a = el.textContent) == null ? void 0 : _a.trim();
|
||||
if (txt && (/^\.{3,}$/.test(txt) || /^…+$/.test(txt) || txt === "\u22EF")) {
|
||||
toRemove.push(el);
|
||||
}
|
||||
node = walker.nextNode();
|
||||
}
|
||||
toRemove.forEach((e) => e.remove());
|
||||
return partDoc.body.innerHTML;
|
||||
});
|
||||
const joined = parts.map((p, i) => `<div class="typst-merged-page">${p}</div>`).join('\n<hr class="typst-page-break">\n');
|
||||
return joined;
|
||||
} catch (e) {
|
||||
return html;
|
||||
}
|
||||
}
|
||||
async renderTypstToHtml(typstFile, htmlFile, container, blockId) {
|
||||
const useSync = this.settings.forceSyncCompile || this.settings.syncOnlyDuringExport && this.exportInProgress;
|
||||
if (useSync) {
|
||||
try {
|
||||
const args = [
|
||||
"compile",
|
||||
typstFile,
|
||||
htmlFile,
|
||||
"--features",
|
||||
"html",
|
||||
"--format",
|
||||
"html"
|
||||
];
|
||||
if (this.settings.debugMode) {
|
||||
console.log("Running Typst (sync):", this.settings.typstPath, args.join(" "));
|
||||
}
|
||||
const res = (0, import_child_process.spawnSync)(this.settings.typstPath, args, { encoding: "utf-8" });
|
||||
if (res.error) {
|
||||
const errMsg = res.error.message || String(res.error);
|
||||
container.innerHTML = this.formatError(errMsg, false);
|
||||
throw res.error;
|
||||
}
|
||||
if (res.status !== 0) {
|
||||
const errStr = res.stderr || `Typst process exited with code ${res.status}`;
|
||||
container.innerHTML = this.formatError(errStr, false);
|
||||
throw new Error(errStr);
|
||||
}
|
||||
const html = (0, import_fs.readFileSync)(htmlFile, "utf-8");
|
||||
const bodyMatch = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
||||
const content = bodyMatch ? bodyMatch[1] : html;
|
||||
this.renderCache.set(blockId, content);
|
||||
container.innerHTML = content;
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
if (this.settings.debugMode)
|
||||
console.error("Typst sync render error:", err);
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
var _a;
|
||||
if (this.watchers.has(blockId)) {
|
||||
(_a = this.watchers.get(blockId)) == null ? void 0 : _a.kill();
|
||||
this.watchers.delete(blockId);
|
||||
}
|
||||
const args = [
|
||||
"compile",
|
||||
typstFile,
|
||||
htmlFile,
|
||||
"--features",
|
||||
"html",
|
||||
"--format",
|
||||
"html"
|
||||
];
|
||||
if (this.settings.debugMode) {
|
||||
console.log(
|
||||
"Running Typst command:",
|
||||
this.settings.typstPath,
|
||||
args.join(" ")
|
||||
);
|
||||
}
|
||||
const typstProcess = (0, import_child_process.spawn)(this.settings.typstPath, args);
|
||||
let stderr = "";
|
||||
typstProcess.stderr.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
typstProcess.on("close", async (code) => {
|
||||
if (code === 0) {
|
||||
try {
|
||||
const html = await import_fs.promises.readFile(htmlFile, "utf-8");
|
||||
const bodyMatch = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
||||
const content = bodyMatch ? bodyMatch[1] : html;
|
||||
this.renderCache.set(blockId, content);
|
||||
container.innerHTML = content;
|
||||
resolve();
|
||||
} catch (error) {
|
||||
container.innerHTML = this.formatError(error, false);
|
||||
reject(error);
|
||||
}
|
||||
} else {
|
||||
const errorMsg = stderr || `Typst process exited with code ${code}`;
|
||||
container.innerHTML = this.formatError(errorMsg, false);
|
||||
reject(new Error(errorMsg));
|
||||
}
|
||||
});
|
||||
typstProcess.on("error", (error) => {
|
||||
container.innerHTML = this.formatError(error, false);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
async renderTypstWithWatch(typstFile, htmlFile, container, blockId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var _a;
|
||||
if (this.watchers.has(blockId)) {
|
||||
(_a = this.watchers.get(blockId)) == null ? void 0 : _a.kill();
|
||||
this.watchers.delete(blockId);
|
||||
}
|
||||
const args = [
|
||||
"watch",
|
||||
typstFile,
|
||||
htmlFile,
|
||||
"--features",
|
||||
"html",
|
||||
"--format",
|
||||
"html"
|
||||
];
|
||||
if (this.settings.debugMode) {
|
||||
console.log(
|
||||
"Running Typst watch:",
|
||||
this.settings.typstPath,
|
||||
args.join(" ")
|
||||
);
|
||||
}
|
||||
const typstProcess = (0, import_child_process.spawn)(this.settings.typstPath, args);
|
||||
this.watchers.set(blockId, typstProcess);
|
||||
let stderr = "";
|
||||
let hasRendered = false;
|
||||
const checkForUpdates = async () => {
|
||||
try {
|
||||
const html = await import_fs.promises.readFile(htmlFile, "utf-8");
|
||||
const bodyMatch = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
||||
const content = bodyMatch ? bodyMatch[1] : html;
|
||||
this.renderCache.set(blockId, content);
|
||||
container.innerHTML = content;
|
||||
if (!hasRendered) {
|
||||
hasRendered = true;
|
||||
resolve();
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.settings.debugMode) {
|
||||
console.log("Waiting for Typst to generate output...");
|
||||
}
|
||||
}
|
||||
};
|
||||
typstProcess.stderr.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
if (stderr.includes("error:")) {
|
||||
container.innerHTML = this.formatError(stderr, false);
|
||||
}
|
||||
});
|
||||
typstProcess.stdout.on("data", (data) => {
|
||||
const output = data.toString();
|
||||
if (this.settings.debugMode) {
|
||||
console.log("Typst watch output:", output);
|
||||
}
|
||||
if (output.includes("written to") || output.includes("compiled")) {
|
||||
setTimeout(checkForUpdates, 100);
|
||||
}
|
||||
});
|
||||
setTimeout(checkForUpdates, 500);
|
||||
typstProcess.on("close", (code) => {
|
||||
this.watchers.delete(blockId);
|
||||
if (code !== 0 && code !== null && !hasRendered) {
|
||||
const errorMsg = stderr || `Typst watch exited with code ${code}`;
|
||||
container.innerHTML = this.formatError(errorMsg, false);
|
||||
reject(new Error(errorMsg));
|
||||
}
|
||||
});
|
||||
typstProcess.on("error", (error) => {
|
||||
this.watchers.delete(blockId);
|
||||
container.innerHTML = this.formatError(error, false);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
var TypstMathSettingTab = class extends import_obsidian.PluginSettingTab {
|
||||
constructor(app, plugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
display() {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
containerEl.createEl("h2", { text: "Typst Math Renderer Settings" });
|
||||
new import_obsidian.Setting(containerEl).setName("Typst CLI path").setDesc('Path to the Typst executable (e.g., "typst" or full path)').addText(
|
||||
(text) => text.setPlaceholder("typst").setValue(this.plugin.settings.typstPath).onChange(async (value) => {
|
||||
this.plugin.settings.typstPath = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new import_obsidian.Setting(containerEl).setName("Enable inline math").setDesc("Process inline math blocks ($...$)").addToggle(
|
||||
(toggle) => toggle.setValue(this.plugin.settings.enableInlineMath).onChange(async (value) => {
|
||||
this.plugin.settings.enableInlineMath = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new import_obsidian.Setting(containerEl).setName("Enable display math").setDesc("Process display math blocks ($$...$$)").addToggle(
|
||||
(toggle) => toggle.setValue(this.plugin.settings.enableDisplayMath).onChange(async (value) => {
|
||||
this.plugin.settings.enableDisplayMath = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new import_obsidian.Setting(containerEl).setName("Debug mode").setDesc("Enable debug logging in the console").addToggle(
|
||||
(toggle) => toggle.setValue(this.plugin.settings.debugMode).onChange(async (value) => {
|
||||
this.plugin.settings.debugMode = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new import_obsidian.Setting(containerEl).setName("Show detailed errors").setDesc("Display detailed Typst error messages (useful for debugging)").addToggle(
|
||||
(toggle) => toggle.setValue(this.plugin.settings.showDetailedErrors).onChange(async (value) => {
|
||||
this.plugin.settings.showDetailedErrors = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new import_obsidian.Setting(containerEl).setName("Use watch mode").setDesc("Enable watch mode for code blocks (auto-recompile on changes) - experimental").addToggle(
|
||||
(toggle) => toggle.setValue(this.plugin.settings.useWatchMode).onChange(async (value) => {
|
||||
this.plugin.settings.useWatchMode = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new import_obsidian.Setting(containerEl).setName("Force synchronous compile (for exports)").setDesc("Use a synchronous Typst compile which can improve reliability when exporting to PDF. This will block rendering while Typst runs.").addToggle(
|
||||
(toggle) => toggle.setValue(this.plugin.settings.forceSyncCompile).onChange(async (value) => {
|
||||
this.plugin.settings.forceSyncCompile = value;
|
||||
await this.plugin.saveSettings();
|
||||
new import_obsidian.Notice("Force sync compile setting updated");
|
||||
})
|
||||
);
|
||||
new import_obsidian.Setting(containerEl).setName("Sync only during print/export").setDesc("If enabled, synchronous compilation will only be used during a print/export flow. Disable to always use sync compilation when enabled.").addToggle(
|
||||
(toggle) => toggle.setValue(this.plugin.settings.syncOnlyDuringExport).onChange(async (value) => {
|
||||
this.plugin.settings.syncOnlyDuringExport = value;
|
||||
await this.plugin.saveSettings();
|
||||
new import_obsidian.Notice("Sync-only-during-export setting updated");
|
||||
})
|
||||
);
|
||||
new import_obsidian.Setting(containerEl).setName("Enable live preview").setDesc("Render Typst math in live preview mode (works automatically via MathJax override)").addToggle(
|
||||
(toggle) => toggle.setValue(this.plugin.settings.enableLivePreview).onChange(async (value) => {
|
||||
this.plugin.settings.enableLivePreview = value;
|
||||
await this.plugin.saveSettings();
|
||||
new import_obsidian.Notice("Reload Obsidian for this change to take full effect");
|
||||
})
|
||||
);
|
||||
containerEl.createEl("h3", { text: "Usage" });
|
||||
containerEl.createEl("p", {
|
||||
text: "Create a code block with ```math or ```typst and write your Typst math syntax inside."
|
||||
});
|
||||
containerEl.createEl("pre", {
|
||||
text: "```math\n$ sum_(i=1)^n i = (n(n+1))/2 $\n```"
|
||||
});
|
||||
containerEl.createEl("h3", { text: "Installation" });
|
||||
containerEl.createEl("p", {
|
||||
text: "Make sure you have Typst CLI installed. Visit https://github.com/typst/typst for installation instructions."
|
||||
});
|
||||
}
|
||||
};
|
||||
10
.obsidian/plugins/obsidian-typst-math/manifest.json
vendored
Normal file
10
.obsidian/plugins/obsidian-typst-math/manifest.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"id": "obsidian-typst-cli",
|
||||
"name": "Typst CLI Math Integration",
|
||||
"version": "1.3.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Replaces default math blocks with Typst-rendered math blocks using the Typst CLI",
|
||||
"author": "Your Name",
|
||||
"authorUrl": "https://yourwebsite.com",
|
||||
"isDesktopOnly": true
|
||||
}
|
||||
176
.obsidian/plugins/obsidian-typst-math/styles.css
vendored
Normal file
176
.obsidian/plugins/obsidian-typst-math/styles.css
vendored
Normal file
@@ -0,0 +1,176 @@
|
||||
/* Styles for Typst Math Renderer Plugin */
|
||||
|
||||
.typst-math-container {
|
||||
margin: 1em 0;
|
||||
padding: 0.5em;
|
||||
background-color: var(--background-primary);
|
||||
border-radius: 4px;
|
||||
color: #deb4ae;
|
||||
}
|
||||
|
||||
.typst-math-inline {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
margin: 0;
|
||||
padding: 0 0.2em;
|
||||
color: #deb4ae;
|
||||
}
|
||||
|
||||
.typst-loading {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.typst-error {
|
||||
color: #e74c3c;
|
||||
background-color: transparent;
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 3px;
|
||||
font-family: var(--font-interface);
|
||||
font-size: 0.95em;
|
||||
font-weight: 500;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.typst-error-inline {
|
||||
display: inline;
|
||||
color: #e74c3c;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.typst-error-details {
|
||||
display: block;
|
||||
margin-top: 0.3em;
|
||||
padding: 0.5em;
|
||||
background-color: rgba(231, 76, 60, 0.1);
|
||||
border-radius: 3px;
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 0.85em;
|
||||
color: var(--text-muted);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Style the Typst output - inherit theme colors */
|
||||
.typst-math-container svg,
|
||||
.typst-math-inline svg,
|
||||
.typst-math-container .typst-doc,
|
||||
.typst-math-inline .typst-doc {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.typst-math-inline svg,
|
||||
.typst-math-inline .typst-doc {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Override Typst's default black text in SVG */
|
||||
.typst-math-container svg *[fill="#000000"],
|
||||
.typst-math-inline svg *[fill="#000000"],
|
||||
.typst-math-container svg *[fill="black"],
|
||||
.typst-math-inline svg *[fill="black"] {
|
||||
fill: #deb4ae !important;
|
||||
}
|
||||
|
||||
.typst-math-container svg *[stroke="#000000"],
|
||||
.typst-math-inline svg *[stroke="#000000"],
|
||||
.typst-math-container svg *[stroke="black"],
|
||||
.typst-math-inline svg *[stroke="black"] {
|
||||
stroke: #deb4ae !important;
|
||||
}
|
||||
|
||||
/* Also target when SVGs are directly in code blocks */
|
||||
.markdown-preview-view .cm-preview-code-block svg *[fill="#000000"],
|
||||
.markdown-preview-view .cm-preview-code-block svg *[fill="black"] {
|
||||
fill: #deb4ae !important;
|
||||
}
|
||||
|
||||
.markdown-preview-view .cm-preview-code-block svg *[stroke="#000000"],
|
||||
.markdown-preview-view .cm-preview-code-block svg *[stroke="black"] {
|
||||
stroke: #deb4ae !important;
|
||||
}
|
||||
|
||||
/* Ensure the typst-doc class gets color */
|
||||
.typst-doc {
|
||||
color: #deb4ae;
|
||||
}
|
||||
|
||||
/* Override any remaining color specifications */
|
||||
.typst-math-container *,
|
||||
.typst-math-inline * {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Center block math */
|
||||
.typst-math-container p {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Ensure proper spacing for math content */
|
||||
.typst-math-container > * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Print-specific rules: when exporting to PDF, use document/default colors
|
||||
and avoid any truncation/ellipsis or SVG recoloring that may break prints */
|
||||
@media print {
|
||||
/* Restore default text color so printed PDFs look like the document */
|
||||
.typst-math-container,
|
||||
.typst-math-inline,
|
||||
.typst-doc,
|
||||
.typst-math-container *,
|
||||
.typst-math-inline * {
|
||||
color: initial !important;
|
||||
-webkit-text-fill-color: initial !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Ensure SVGs keep their original fills/strokes in print (do not force plugin color) */
|
||||
.typst-math-container svg *[fill="#000000"],
|
||||
.typst-math-inline svg *[fill="#000000"],
|
||||
.typst-math-container svg *[fill="black"],
|
||||
.typst-math-inline svg *[fill="black"],
|
||||
.markdown-preview-view .cm-preview-code-block svg *[fill="#000000"],
|
||||
.markdown-preview-view .cm-preview-code-block svg *[fill="black"] {
|
||||
fill: initial !important;
|
||||
}
|
||||
|
||||
.typst-math-container svg *[stroke="#000000"],
|
||||
.typst-math-inline svg *[stroke="#000000"],
|
||||
.typst-math-container svg *[stroke="black"],
|
||||
.typst-math-inline svg *[stroke="black"],
|
||||
.markdown-preview-view .cm-preview-code-block svg *[stroke="#000000"],
|
||||
.markdown-preview-view .cm-preview-code-block svg *[stroke="black"] {
|
||||
stroke: initial !important;
|
||||
}
|
||||
|
||||
/* Avoid text-overflow/ellipsis in printed output */
|
||||
.typst-math-container,
|
||||
.typst-math-inline,
|
||||
.typst-math-container *,
|
||||
.typst-math-inline * {
|
||||
white-space: normal !important;
|
||||
text-overflow: clip !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Remove any max-width/inline-block constraints that might clip content */
|
||||
.typst-math-inline svg,
|
||||
.typst-math-inline .typst-doc,
|
||||
.typst-math-container svg,
|
||||
.typst-math-container .typst-doc {
|
||||
max-width: none !important;
|
||||
height: auto !important;
|
||||
display: inline !important;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user