mirror of
https://github.com/mii443/obsidian-typst.git
synced 2025-08-22 16:15:34 +00:00
349 lines
12 KiB
TypeScript
349 lines
12 KiB
TypeScript
import { App, renderMath, HexString, Platform, Plugin, PluginSettingTab, Setting, loadMathJax, normalizePath } from 'obsidian';
|
|
|
|
// @ts-ignore
|
|
import Worker from "./compiler.worker.ts"
|
|
|
|
import TypstCanvasElement from 'typst-canvas-element';
|
|
import { WorkerRequest } from 'types.js';
|
|
|
|
interface TypstPluginSettings {
|
|
noFill: boolean,
|
|
fill: HexString,
|
|
pixel_per_pt: number,
|
|
search_system: boolean,
|
|
override_math: boolean,
|
|
preamable: {
|
|
shared: string,
|
|
math: string,
|
|
code: string,
|
|
}
|
|
}
|
|
|
|
const DEFAULT_SETTINGS: TypstPluginSettings = {
|
|
noFill: true,
|
|
fill: "#ffffff",
|
|
pixel_per_pt: 3,
|
|
search_system: false,
|
|
override_math: false,
|
|
preamable: {
|
|
shared: "#set text(fill: white, size: SIZE)\n#set page(width: WIDTH, height: HEIGHT)",
|
|
math: "#set page(margin: 0pt)\n#set align(horizon)",
|
|
code: "#set page(margin: (y: 1em, x: 0pt))"
|
|
}
|
|
}
|
|
|
|
export default class TypstPlugin extends Plugin {
|
|
settings: TypstPluginSettings;
|
|
|
|
compilerWorker: Worker;
|
|
|
|
tex2chtml: any;
|
|
|
|
prevCanvasHeight: number = 0;
|
|
textEncoder: TextEncoder
|
|
fs: any;
|
|
|
|
async onload() {
|
|
this.compilerWorker = new Worker();
|
|
if (!Platform.isMobileApp) {
|
|
this.compilerWorker.postMessage(true);
|
|
this.fs = require("fs")
|
|
}
|
|
|
|
this.textEncoder = new TextEncoder()
|
|
await this.loadSettings()
|
|
|
|
TypstCanvasElement.compile = (a, b, c, d, e) => this.processThenCompileTypst(a, b, c, d, e)
|
|
if (customElements.get("typst-renderer") == undefined) {
|
|
customElements.define("typst-renderer", TypstCanvasElement, { extends: "canvas" })
|
|
}
|
|
|
|
await loadMathJax()
|
|
renderMath("", false);
|
|
// @ts-expect-error
|
|
this.tex2chtml = MathJax.tex2chtml
|
|
this.overrideMathJax(this.settings.override_math)
|
|
|
|
this.addCommand({
|
|
id: "toggle-math-override",
|
|
name: "Toggle math block override",
|
|
callback: () => this.overrideMathJax(!this.settings.override_math)
|
|
})
|
|
|
|
|
|
this.addSettingTab(new TypstSettingTab(this.app, this));
|
|
this.registerMarkdownCodeBlockProcessor("typst", async (source, el, ctx) => {
|
|
el.appendChild(this.createTypstCanvas("/" + ctx.sourcePath, `${this.settings.preamable.code}\n${source}`, true, false))
|
|
})
|
|
|
|
|
|
console.log("loaded Typst Renderer");
|
|
}
|
|
|
|
// async loadCompilerWorker() {
|
|
// this.compilerWorker.
|
|
// }
|
|
|
|
async compileToTypst(path: string, source: string, size: number, display: boolean): Promise<ImageData> {
|
|
return await navigator.locks.request("typst renderer compiler", async (lock) => {
|
|
this.compilerWorker.postMessage({
|
|
source,
|
|
path,
|
|
pixel_per_pt: this.settings.pixel_per_pt,
|
|
fill: `${this.settings.fill}${this.settings.noFill ? "00" : "ff"}`,
|
|
size,
|
|
display
|
|
});
|
|
while (true) {
|
|
let result: ImageData | WorkerRequest = await new Promise((resolve, reject) => {
|
|
const listener = (ev: MessageEvent<ImageData>) => {
|
|
remove();
|
|
resolve(ev.data);
|
|
}
|
|
const errorListener = (error: ErrorEvent) => {
|
|
remove();
|
|
reject(error.message)
|
|
}
|
|
const remove = () => {
|
|
this.compilerWorker.removeEventListener("message", listener);
|
|
this.compilerWorker.removeEventListener("error", errorListener);
|
|
}
|
|
this.compilerWorker.addEventListener("message", listener);
|
|
this.compilerWorker.addEventListener("error", errorListener);
|
|
})
|
|
|
|
if (result instanceof ImageData) {
|
|
return result
|
|
}
|
|
// Cannot reach this point when in mobile app as the worker should
|
|
// not have a SharedArrayBuffer
|
|
await this.handleWorkerRequest(result)
|
|
}
|
|
})
|
|
}
|
|
|
|
async handleWorkerRequest({ buffer: wbuffer, path }: WorkerRequest) {
|
|
try {
|
|
let s = await (
|
|
path.startsWith("@")
|
|
? this.preparePackage(path)
|
|
: this.getFileString(path)
|
|
);
|
|
if (s) {
|
|
|
|
|
|
let buffer = Int32Array.from(this.textEncoder.encode(
|
|
s
|
|
));
|
|
if (wbuffer.byteLength < (buffer.byteLength + 4)) {
|
|
//@ts-expect-error
|
|
wbuffer.buffer.grow(buffer.byteLength + 4)
|
|
}
|
|
wbuffer.set(buffer, 1)
|
|
wbuffer[0] = 0
|
|
} else {
|
|
wbuffer[0] = -2
|
|
}
|
|
} catch (error) {
|
|
wbuffer[0] = -1
|
|
throw error
|
|
} finally {
|
|
Atomics.notify(wbuffer, 0)
|
|
}
|
|
}
|
|
|
|
async getFileString(path: string): Promise<string> {
|
|
if (require("path").isAbsolute(path)) {
|
|
return await this.fs.promises.readFile(path, { encoding: "utf8" })
|
|
} else {
|
|
return await this.app.vault.adapter.read(normalizePath(path))
|
|
}
|
|
}
|
|
|
|
async preparePackage(spec: string): Promise<string | undefined> {
|
|
spec = spec.slice(1)
|
|
let subdir = "/typst/packages/" + spec
|
|
|
|
let dir = normalizePath(this.getDataDir() + subdir)
|
|
if (this.fs.existsSync(dir)) {
|
|
return dir
|
|
}
|
|
|
|
dir = normalizePath(this.getCacheDir() + subdir)
|
|
|
|
if (this.fs.existsSync(dir)) {
|
|
return dir
|
|
}
|
|
}
|
|
|
|
getDataDir() {
|
|
if (Platform.isLinux) {
|
|
if ("XDG_DATA_HOME" in process.env) {
|
|
return process.env["XDG_DATA_HOME"]
|
|
} else {
|
|
return process.env["HOME"] + "/.local/share"
|
|
}
|
|
} else if (Platform.isWin) {
|
|
return process.env["APPDATA"]
|
|
} else if (Platform.isMacOS) {
|
|
return process.env["HOME"] + "/Library/Application Support"
|
|
}
|
|
throw "Cannot find data directory on an unknown platform"
|
|
}
|
|
|
|
getCacheDir() {
|
|
if (Platform.isLinux) {
|
|
if ("XDG_CACHE_HOME" in process.env) {
|
|
return process.env["XDG_DATA_HOME"]
|
|
} else {
|
|
return process.env["HOME"] + "/.cache"
|
|
}
|
|
} else if (Platform.isWin) {
|
|
return process.env["LOCALAPPDATA"]
|
|
} else if (Platform.isMacOS) {
|
|
return process.env["HOME"] + "/Library/Caches"
|
|
}
|
|
throw "Cannot find cache directory on an unknown platform"
|
|
}
|
|
|
|
async processThenCompileTypst(path: string, source: string, size: number, display: boolean, fontSize: number) {
|
|
const dpr = window.devicePixelRatio;
|
|
const pxToPt = (px: number) => (px * dpr * (72 / 96)).toString() + "pt"
|
|
const sizing = `#let (WIDTH, HEIGHT, SIZE) = (${display ? pxToPt(size) : "auto"}, ${!display ? pxToPt(size) : "auto"}, ${pxToPt(fontSize)})`
|
|
return this.compileToTypst(
|
|
path,
|
|
`${sizing}\n${this.settings.preamable.shared}\n${source}`,
|
|
size,
|
|
display
|
|
)
|
|
}
|
|
|
|
createTypstCanvas(path: string, source: string, display: boolean, math: boolean) {
|
|
let canvas = new TypstCanvasElement();
|
|
canvas.source = source
|
|
canvas.path = path
|
|
canvas.display = display
|
|
canvas.math = math
|
|
return canvas
|
|
}
|
|
|
|
createTypstMath(source: string, r: { display: boolean }) {
|
|
const display = r.display;
|
|
source = `${this.settings.preamable.math}\n${display ? `$ ${source} $` : `$${source}$`}`
|
|
|
|
return this.createTypstCanvas("/586f8912-f3a8-4455-8a4a-3729469c2cc1.typ", source, display, true)
|
|
}
|
|
|
|
onunload() {
|
|
// @ts-expect-error
|
|
MathJax.tex2chtml = this.tex2chtml
|
|
this.compilerWorker.terminate()
|
|
}
|
|
|
|
async overrideMathJax(value: boolean) {
|
|
this.settings.override_math = value
|
|
await this.saveSettings();
|
|
if (this.settings.override_math) {
|
|
// @ts-expect-error
|
|
MathJax.tex2chtml = (e, r) => this.createTypstMath(e, r)
|
|
} else {
|
|
// @ts-expect-error
|
|
MathJax.tex2chtml = this.tex2chtml
|
|
}
|
|
}
|
|
|
|
async loadSettings() {
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
|
}
|
|
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
}
|
|
}
|
|
|
|
class TypstSettingTab extends PluginSettingTab {
|
|
plugin: TypstPlugin;
|
|
|
|
constructor(app: App, plugin: TypstPlugin) {
|
|
super(app, plugin);
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
display(): void {
|
|
const { containerEl } = this;
|
|
|
|
containerEl.empty();
|
|
|
|
|
|
new Setting(containerEl)
|
|
.setName("No Fill (Transparent)")
|
|
.addToggle((toggle) => {
|
|
toggle.setValue(this.plugin.settings.noFill)
|
|
.onChange(
|
|
async (value) => {
|
|
this.plugin.settings.noFill = value;
|
|
await this.plugin.saveSettings();
|
|
fill_color.setDisabled(value)
|
|
}
|
|
)
|
|
});
|
|
|
|
let fill_color = new Setting(containerEl)
|
|
.setName("Fill Color")
|
|
.setDisabled(this.plugin.settings.noFill)
|
|
.addColorPicker((picker) => {
|
|
picker.setValue(this.plugin.settings.fill)
|
|
.onChange(
|
|
async (value) => {
|
|
this.plugin.settings.fill = value;
|
|
await this.plugin.saveSettings();
|
|
}
|
|
)
|
|
})
|
|
|
|
new Setting(containerEl)
|
|
.setName("Pixel Per Point")
|
|
.addSlider((slider) =>
|
|
slider.setValue(this.plugin.settings.pixel_per_pt)
|
|
.setLimits(1, 5, 1)
|
|
.onChange(
|
|
async (value) => {
|
|
this.plugin.settings.pixel_per_pt = value;
|
|
await this.plugin.saveSettings();
|
|
}
|
|
)
|
|
.setDynamicTooltip()
|
|
)
|
|
|
|
new Setting(containerEl)
|
|
.setName("Search System Fonts")
|
|
.setDesc(`Whether the plugin should search for system fonts.
|
|
This is off by default as it takes around 20 seconds to complete but it gives access to more fonts.
|
|
Requires reload of plugin.`)
|
|
.addToggle((toggle) => {
|
|
toggle.setValue(this.plugin.settings.search_system)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.search_system = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
})
|
|
|
|
new Setting(containerEl)
|
|
.setName("Override Math Blocks")
|
|
.addToggle((toggle) => {
|
|
toggle.setValue(this.plugin.settings.override_math)
|
|
.onChange((value) => this.plugin.overrideMathJax(value))
|
|
});
|
|
|
|
new Setting(containerEl)
|
|
.setName("Shared Preamable")
|
|
.addTextArea((c) => c.setValue(this.plugin.settings.preamable.shared).onChange(async (value) => { this.plugin.settings.preamable.shared = value; await this.plugin.saveSettings() }))
|
|
new Setting(containerEl)
|
|
.setName("Code Block Preamable")
|
|
.addTextArea((c) => c.setValue(this.plugin.settings.preamable.code).onChange(async (value) => { this.plugin.settings.preamable.code = value; await this.plugin.saveSettings() }))
|
|
new Setting(containerEl)
|
|
.setName("Math Block Preamable")
|
|
.addTextArea((c) => c.setValue(this.plugin.settings.preamable.math).onChange(async (value) => { this.plugin.settings.preamable.math = value; await this.plugin.saveSettings() }))
|
|
}
|
|
}
|