mirror of
https://github.com/mii443/obsidian-typst.git
synced 2025-08-22 16:15:34 +00:00
0.5.0 (#8)
Co-authored-by: paulaingate <49568567+paulaingate@users.noreply.github.com>
This commit is contained in:
@ -1,2 +0,0 @@
|
||||
[build]
|
||||
rustflags = ["--cfg=web_sys_unstable_apis"]
|
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
github: fenjalien
|
8
CHANGELOG.md
Normal file
8
CHANGELOG.md
Normal file
@ -0,0 +1,8 @@
|
||||
# 0.5.0
|
||||
- Update Typst version to 0.6.0
|
||||
- Attempt to fix bad canvas output
|
||||
- Move compiler to a web worker
|
||||
- Rework file reading
|
||||
- Add support of reading packages outside of the vault
|
||||
- Improve settings appearance (#7)
|
||||
- Various bug fixes
|
627
Cargo.lock
generated
627
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
30
Cargo.toml
30
Cargo.toml
@ -1,23 +1,21 @@
|
||||
[package]
|
||||
name = "obsidian-typst"
|
||||
version = "0.4.2"
|
||||
version = "0.5.0"
|
||||
authors = ["fenjalien"]
|
||||
edition = "2021"
|
||||
description = "Renders `typst` code blocks to images with Typst."
|
||||
readme = "README.md"
|
||||
rust-version = "1.70"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
# Everything to do with Typst
|
||||
typst = { git = "https://github.com/typst/typst.git", tag = "v0.4.0" }
|
||||
typst-library = { git = "https://github.com/typst/typst.git", tag = "v0.4.0" }
|
||||
typst = { git = "https://github.com/typst/typst.git", tag = "v0.6.0" }
|
||||
typst-library = { git = "https://github.com/typst/typst.git", tag = "v0.6.0" }
|
||||
comemo = "0.3"
|
||||
|
||||
once_cell = "1.17.1"
|
||||
siphasher = "0.3.10"
|
||||
elsa = "1.8.0"
|
||||
|
||||
|
||||
# Everything to do with wasm
|
||||
@ -26,14 +24,14 @@ js-sys = "^0.3"
|
||||
wasm-bindgen-futures = "^0.4"
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde-wasm-bindgen = "^0.5"
|
||||
web-sys = { version = "^0.3", features = ["console", "Window", "FontData", "Blob", "ImageData"] }
|
||||
web-sys = { version = "^0.3", features = [
|
||||
"console",
|
||||
"Window",
|
||||
"FontData",
|
||||
"Blob",
|
||||
"ImageData",
|
||||
] }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
|
||||
# [patch.crates-io]
|
||||
# web-sys = { git = "https://github.com/fenjalien/wasm-bindgen.git" }
|
||||
# js-sys = { git = "https://github.com/fenjalien/wasm-bindgen.git" }
|
||||
# wasm-bindgen-futures = { git = "https://github.com/fenjalien/wasm-bindgen.git" }
|
||||
# wasm-bindgen = { git = "https://github.com/fenjalien/wasm-bindgen.git" }
|
||||
|
||||
# [profile.release]
|
||||
# debug = true
|
||||
# Image handling
|
||||
fast_image_resize = "2.7.3"
|
||||
|
27
README.md
27
README.md
@ -2,15 +2,20 @@
|
||||
|
||||
Renders `typst` code blocks, and optionally math blocks, into images using [Typst](https://github.com/typst/typst) through the power of WASM! This is still very much in development, so suggestions and bugs are welcome!
|
||||
|
||||
## Things to NOTE
|
||||
## Small Things to NOTE
|
||||
- This plugin uses Typst 0.6.0
|
||||
- Typst does not currently support exporting to HTML only PDFs and PNGs. So due to image scaling, the rendered views may look a bit terrible. If you know how to fix this PLEASE HELP.
|
||||
- File paths should be relative to the vault folder.
|
||||
- System fonts are not loaded by default as this takes about 20 seconds (on my machine). There is an option in settings to enable them (requires a reload of the plugin).
|
||||
- You can not import on mobile as file reading is NOT supported on mobile, this is due to `SharedArrayBuffer`s not being available on mobile but is available for some reason on desktop.
|
||||
|
||||
## Using Packages
|
||||
The plugin supports only the reading of packages from the [`@preview`](https://github.com/typst/packages#downloads) and [`@local`](https://github.com/typst/packages#local-packages) namespaces. Please use the Typst cli to download your desired packages. This means the plugin accesses files outside of your vault but only to read them, it does not modify or create files outside of your vault.
|
||||
|
||||
## Math Block Usage
|
||||
The plugin can render `typst` inside math blocks! By default this is off, to enable it set the "Override Math Blocks" setting or use the "Toggle Math Block Override" command. Math block types are conserved between Obsidian and Typst, `$...$` -> `$...$` and `$$...$$` -> `$ ... $`.
|
||||
The plugin can render `typst` inside math blocks! By default this is off, to enable it set the "Override Math Blocks" setting or use the "Toggle math block override" command. Math block types are conserved between Obsidian and Typst, `$...$` -> `$...$` and `$$...$$` -> `$ ... $`.
|
||||
|
||||
From what I've experimented with, normal math blocks are okay with `typst` code but Typst is not happy with any Latex code.
|
||||
From what I've experimented with, normal math blocks are okay with Typst but Typst is not happy with any Latex code.
|
||||
|
||||
For styling and using imports with math blocks see the next section.
|
||||
|
||||
@ -23,10 +28,8 @@ Preamables are prepended to your `typst` code before compiling. There are three
|
||||
- `code`: Prepended to `typst` code only in code blocks.
|
||||
|
||||
## Known Issues
|
||||
### "File Not Found" Error on First Load of Obsidian
|
||||
When Obsidian first loads it sometimes tries to render before its files are resolved and cached.
|
||||
|
||||
To fix, simply select then deselect everything in the file, or close and re-open the file.
|
||||
### Runtime Error Unreachable or Recursive Use Of Object
|
||||
These occur when the Typst compiler panics for any reason and means the compiler cannot be used again until it is restarted. There should be more information in the console log so please create an issue with this error!
|
||||
|
||||
## Example
|
||||
|
||||
@ -61,14 +64,20 @@ The first #count numbers of the sequence are:
|
||||
<img src="assets/example.png">
|
||||
|
||||
## Installation
|
||||
Until this plugin is submitted to the community plugins please install it by copying `main.js`, `styles.css`, and `manifest.json` from the releases tab to the folder `.obsidian/plugins/obsidian-typst` in your vault.
|
||||
Install "Typst Renderer" from the community plugins tab in settings
|
||||
|
||||
or
|
||||
|
||||
Install it by copying `main.js`, `styles.css`, and `manifest.json` from the releases tab to the folder `.obsidian/plugins/obsidian-typst` in your vault.
|
||||
|
||||
## TODO / Goals (In no particular order)
|
||||
- [x] Better font loading
|
||||
- [x] Fix importing
|
||||
- [x] Fix Github Actions
|
||||
- [ ] Better error handling
|
||||
- [ ] Fix output image scaling
|
||||
- [x]? Fix output image scaling
|
||||
- [ ] Use HTML output
|
||||
- [x] Override default equation rendering
|
||||
- [ ] Custom editor for `.typ` files
|
||||
- [ ] Mobile file reading
|
||||
- [ ] Automate package downloading
|
||||
|
68
compiler.worker.ts
Normal file
68
compiler.worker.ts
Normal file
@ -0,0 +1,68 @@
|
||||
//@ts-ignore
|
||||
import wasmBin from './pkg/obsidian_typst_bg.wasm'
|
||||
import * as typst from './pkg'
|
||||
|
||||
import { CompileCommand, WorkerRequest } from "types";
|
||||
|
||||
typst.initSync(wasmBin);
|
||||
|
||||
let canUseSharedArrayBuffer = new Boolean(false);
|
||||
|
||||
// let buffer: Int32Array;
|
||||
let decoder = new TextDecoder()
|
||||
|
||||
function requestData(path: string): string {
|
||||
if (!canUseSharedArrayBuffer) {
|
||||
throw "Cannot read files on mobile"
|
||||
}
|
||||
// @ts-expect-error
|
||||
let buffer = new Int32Array(new SharedArrayBuffer(4, { maxByteLength: 1e8 }))
|
||||
buffer[0] = 0;
|
||||
postMessage({ buffer, path })
|
||||
const res = Atomics.wait(buffer, 0, 0);
|
||||
if (buffer[0] == 0) {
|
||||
return decoder.decode(Uint8Array.from(buffer.slice(1)))
|
||||
}
|
||||
|
||||
throw buffer[0]
|
||||
}
|
||||
|
||||
let compiler = new typst.SystemWorld("", requestData)
|
||||
const compileAttempts = 5;
|
||||
|
||||
// function compile(data: CompileCommand) {
|
||||
// for (let i = 1; i <= compileAttempts; i += 1) {
|
||||
// try {
|
||||
// return compiler.compile(data.source, data.path, data.pixel_per_pt, data.fill, data.size, data.display)
|
||||
// } catch (error) {
|
||||
// if (i < compileAttempts && ((error.name == "RuntimeError" && error.message == "unreachable") || (error.name == "Uncaught Error"))) {
|
||||
// console.warn("Typst compiler crashed, attempting to restart: ", i);
|
||||
// console.log(data);
|
||||
|
||||
// compiler.free()
|
||||
// compiler = new typst.SystemWorld("", requestData)
|
||||
// } else {
|
||||
// console.log("name", error.name);
|
||||
// console.log("message", error.message);
|
||||
|
||||
// throw error;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
onmessage = (ev: MessageEvent<CompileCommand | true>) => {
|
||||
if (ev.data == true) {
|
||||
canUseSharedArrayBuffer = ev.data
|
||||
} else if ("source" in ev.data) {
|
||||
const data: CompileCommand = ev.data;
|
||||
postMessage(compiler.compile(data.source, data.path, data.pixel_per_pt, data.fill, data.size, data.display))
|
||||
|
||||
// postMessage(compile(ev.data))
|
||||
} else {
|
||||
throw ev;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Typst compiler worker loaded!");
|
@ -2,6 +2,8 @@ import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
|
||||
import inlineWorkerPlugin from "esbuild-plugin-inline-worker";
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
@ -69,13 +71,16 @@ const context = await esbuild.context({
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
plugins: [
|
||||
wasmPlugin
|
||||
inlineWorkerPlugin({ format: "cjs", target: "es2018", plugins: [wasmPlugin] })
|
||||
]
|
||||
})
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
await context.rebuild();
|
||||
process.exit(0)
|
||||
|
||||
// if (prod) {
|
||||
// await context.rebuild();
|
||||
// process.exit(0);
|
||||
// } else {
|
||||
// await context.watch();
|
||||
// }
|
267
main.ts
267
main.ts
@ -1,11 +1,10 @@
|
||||
import { App, HexString, Notice, Plugin, PluginSettingTab, Setting, Workspace, loadMathJax } from 'obsidian';
|
||||
import { App, renderMath, HexString, Platform, Plugin, PluginSettingTab, Setting, loadMathJax, normalizePath } from 'obsidian';
|
||||
|
||||
// @ts-ignore
|
||||
import typst_wasm_bin from './pkg/obsidian_typst_bg.wasm'
|
||||
import typstInit, * as typst from './pkg/obsidian_typst'
|
||||
import TypstCanvasElement from 'typst-canvas-element';
|
||||
import Worker from "./compiler.worker.ts"
|
||||
|
||||
// temp.track()
|
||||
import TypstCanvasElement from 'typst-canvas-element';
|
||||
import { WorkerRequest } from 'types.js';
|
||||
|
||||
interface TypstPluginSettings {
|
||||
noFill: boolean,
|
||||
@ -27,85 +26,218 @@ const DEFAULT_SETTINGS: TypstPluginSettings = {
|
||||
search_system: false,
|
||||
override_math: false,
|
||||
preamable: {
|
||||
shared: "#let pxToPt = (p) => if p == auto {p} else {p * DPR * (72/96) * 1pt}\n#set text(fill: white, size: pxToPt(SIZE))",
|
||||
math: "#set page(width: pxToPt(WIDTH), height: pxToPt(HEIGHT), margin: 0pt)\n#set align(horizon)",
|
||||
code: "#set page(width: auto, height: auto, margin: 1em)"
|
||||
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;
|
||||
compiler: typst.SystemWorld;
|
||||
files: Map<string, string>;
|
||||
|
||||
compilerWorker: Worker;
|
||||
|
||||
tex2chtml: any;
|
||||
resizeObserver: ResizeObserver;
|
||||
|
||||
prevCanvasHeight: number = 0;
|
||||
textEncoder: TextEncoder
|
||||
fs: any;
|
||||
|
||||
async onload() {
|
||||
await typstInit(typst_wasm_bin)
|
||||
this.compilerWorker = new Worker();
|
||||
if (!Platform.isMobileApp) {
|
||||
this.compilerWorker.postMessage(true);
|
||||
this.fs = require("fs")
|
||||
}
|
||||
|
||||
this.textEncoder = new TextEncoder()
|
||||
await this.loadSettings()
|
||||
let notice = new Notice("Loading fonts for Typst...");
|
||||
this.compiler = await new typst.SystemWorld("", (path: string) => this.get_file(path), this.settings.search_system);
|
||||
notice.hide();
|
||||
notice = new Notice("Finished loading fonts for Typst", 5000);
|
||||
|
||||
this.registerEvent(
|
||||
this.app.metadataCache.on("resolved", () => this.updateFileCache())
|
||||
)
|
||||
|
||||
TypstCanvasElement.compile = (a, b, c, d) => this.typstToSizedImage(a, b, c, d)
|
||||
customElements.define("typst-canvas", TypstCanvasElement, { extends: "canvas" })
|
||||
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: "math-override",
|
||||
name: "Toggle Math Block Override",
|
||||
id: "toggle-math-override",
|
||||
name: "Toggle math block override",
|
||||
callback: () => this.overrideMathJax(!this.settings.override_math)
|
||||
})
|
||||
this.addCommand({
|
||||
id: "update-files",
|
||||
name: "Update Cached .typ Files",
|
||||
callback: () => this.updateFileCache()
|
||||
})
|
||||
|
||||
|
||||
this.addSettingTab(new TypstSettingTab(this.app, this));
|
||||
this.registerMarkdownCodeBlockProcessor("typst", (source, el, ctx) => {
|
||||
el.appendChild(this.typstToCanvas(`${this.settings.preamable.code}\n${source}`, true))
|
||||
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");
|
||||
|
||||
console.log("loaded Typst Renderer");
|
||||
}
|
||||
|
||||
typstToImage(source: string) {
|
||||
return this.compiler.compile(source, this.settings.pixel_per_pt, `${this.settings.fill}${this.settings.noFill ? "00" : "ff"}`)
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
typstToSizedImage(source: string, size: number, display: boolean, fontSize: number) {
|
||||
const sizing = `#let (WIDTH, HEIGHT, SIZE, DPR) = (${display ? size : "auto"}, ${!display ? size : "auto"}, ${fontSize}, ${window.devicePixelRatio})`
|
||||
return this.typstToImage(
|
||||
`${sizing}\n${this.settings.preamable.shared}\n${source}`
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
typstToCanvas(source: string, display: boolean) {
|
||||
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
|
||||
}
|
||||
|
||||
typstMath2Html(source: string, r: { display: boolean }) {
|
||||
createTypstMath(source: string, r: { display: boolean }) {
|
||||
const display = r.display;
|
||||
source = `${this.settings.preamable.math}\n${display ? `$ ${source} $` : `$${source}$`}`
|
||||
return this.typstToCanvas(source, display)
|
||||
|
||||
return this.createTypstCanvas("/586f8912-f3a8-4455-8a4a-3729469c2cc1.typ", source, display, true)
|
||||
}
|
||||
|
||||
onunload() {
|
||||
//@ts-expect-error
|
||||
// @ts-expect-error
|
||||
MathJax.tex2chtml = this.tex2chtml
|
||||
this.compilerWorker.terminate()
|
||||
}
|
||||
|
||||
async overrideMathJax(value: boolean) {
|
||||
@ -113,7 +245,7 @@ export default class TypstPlugin extends Plugin {
|
||||
await this.saveSettings();
|
||||
if (this.settings.override_math) {
|
||||
// @ts-expect-error
|
||||
MathJax.tex2chtml = (e, r) => this.typstMath2Html(e, r)
|
||||
MathJax.tex2chtml = (e, r) => this.createTypstMath(e, r)
|
||||
} else {
|
||||
// @ts-expect-error
|
||||
MathJax.tex2chtml = this.tex2chtml
|
||||
@ -127,23 +259,6 @@ export default class TypstPlugin extends Plugin {
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
get_file(path: string) {
|
||||
if (this.files.has(path)) {
|
||||
return this.files.get(path)
|
||||
}
|
||||
console.error(`'${path}' is a folder or does not exist`, this.files.keys());
|
||||
throw `'${path}' is a folder or does not exist`
|
||||
}
|
||||
|
||||
async updateFileCache() {
|
||||
this.files = new Map()
|
||||
for (const file of this.app.vault.getFiles()) {
|
||||
if (file.extension == "typ") {
|
||||
this.files.set(file.path, await this.app.vault.cachedRead(file))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TypstSettingTab extends PluginSettingTab {
|
||||
@ -159,18 +274,7 @@ class TypstSettingTab extends PluginSettingTab {
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
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("No Fill (Transparent)")
|
||||
.addToggle((toggle) => {
|
||||
@ -183,6 +287,20 @@ class TypstSettingTab extends PluginSettingTab {
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
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) =>
|
||||
@ -193,7 +311,10 @@ class TypstSettingTab extends PluginSettingTab {
|
||||
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.
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "typst",
|
||||
"name": "Typst Renderer",
|
||||
"version": "0.4.2",
|
||||
"version": "0.5.0",
|
||||
"minAppVersion": "1.0.0",
|
||||
"description": "Renders `typst` code blocks and math blocks to images with Typst.",
|
||||
"author": "fenjalien",
|
||||
|
741
package-lock.json
generated
741
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-typst-plugin",
|
||||
"version": "0.4.2",
|
||||
"version": "0.5.0",
|
||||
"description": "Renders `typst` code blocks to images with Typst.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"wasm": "wasm-pack build --target web",
|
||||
"dev": "wasm-pack build --target web --dev && node esbuild.config.mjs",
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"wasm-dev": "wasm-pack build --target web --dev && node esbuild.config.mjs",
|
||||
"wasm-build": "wasm-pack build --target web && tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
@ -14,23 +15,17 @@
|
||||
"author": "fenjalien",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"@types/temp": "^0.9.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.17.3",
|
||||
"esbuild-plugin-wasm": "^1.0.0",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
"@types/node": "^20",
|
||||
"@typescript-eslint/eslint-plugin": "^5",
|
||||
"@typescript-eslint/parser": "^5",
|
||||
"builtin-modules": "^3",
|
||||
"esbuild": "^0.18",
|
||||
"esbuild-plugin-inline-worker": "^0.1.1",
|
||||
"typescript": "^5.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/state": "^6.2.0",
|
||||
"@codemirror/view": "^6.10.0",
|
||||
"@lezer/common": "^1.0.2",
|
||||
"obsidian": "latest",
|
||||
"obsidian-typst": "file:pkg",
|
||||
"temp": "^0.9.4"
|
||||
"tslib": "^2"
|
||||
}
|
||||
}
|
||||
|
49
src/fonts.rs
Normal file
49
src/fonts.rs
Normal file
@ -0,0 +1,49 @@
|
||||
use typst::{
|
||||
font::{Font, FontBook},
|
||||
util::Bytes,
|
||||
};
|
||||
|
||||
/// Searches for fonts.
|
||||
pub struct FontSearcher {
|
||||
/// Metadata about all discovered fonts.
|
||||
pub book: FontBook,
|
||||
/// Slots that the fonts are loaded into.
|
||||
pub fonts: Vec<Font>,
|
||||
}
|
||||
|
||||
impl FontSearcher {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
book: FontBook::new(),
|
||||
fonts: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_embedded(&mut self) {
|
||||
let mut process = |bytes: &'static [u8]| {
|
||||
let buffer = Bytes::from_static(bytes);
|
||||
for font in Font::iter(buffer) {
|
||||
self.book.push(font.info().clone());
|
||||
self.fonts.push(font);
|
||||
}
|
||||
};
|
||||
|
||||
macro_rules! add {
|
||||
($filename:literal) => {
|
||||
process(include_bytes!(concat!("../assets/fonts/", $filename)));
|
||||
};
|
||||
}
|
||||
|
||||
// Embed default fonts.
|
||||
add!("LinLibertine_R.ttf");
|
||||
add!("LinLibertine_RB.ttf");
|
||||
add!("LinLibertine_RBI.ttf");
|
||||
add!("LinLibertine_RI.ttf");
|
||||
add!("NewCMMath-Book.otf");
|
||||
add!("NewCMMath-Regular.otf");
|
||||
add!("DejaVuSansMono.ttf");
|
||||
add!("DejaVuSansMono-Bold.ttf");
|
||||
add!("DejaVuSansMono-Oblique.ttf");
|
||||
add!("DejaVuSansMono-BoldOblique.ttf");
|
||||
}
|
||||
}
|
434
src/lib.rs
434
src/lib.rs
@ -1,299 +1,279 @@
|
||||
use comemo::Prehashed;
|
||||
use elsa::FrozenVec;
|
||||
use once_cell::unsync::OnceCell;
|
||||
use siphasher::sip128::{Hasher128, SipHasher};
|
||||
use fast_image_resize as fr;
|
||||
use std::{
|
||||
cell::{RefCell, RefMut},
|
||||
cell::{OnceCell, RefCell, RefMut},
|
||||
collections::HashMap,
|
||||
hash::Hash,
|
||||
num::NonZeroU32,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
use typst::{
|
||||
diag::{FileError, FileResult},
|
||||
eval::Library,
|
||||
font::{Font, FontBook, FontInfo},
|
||||
diag::{EcoString, FileError, FileResult, PackageError, PackageResult},
|
||||
eval::{Datetime, Library},
|
||||
file::{FileId, PackageSpec},
|
||||
font::{Font, FontBook},
|
||||
geom::{Color, RgbaColor},
|
||||
syntax::{Source, SourceId},
|
||||
util::{Buffer, PathExt},
|
||||
syntax::Source,
|
||||
util::{Bytes, PathExt},
|
||||
World,
|
||||
};
|
||||
use wasm_bindgen::{prelude::*, Clamped};
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::{console, Blob, FontData, ImageData};
|
||||
use web_sys::ImageData;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
fn alert(s: &str);
|
||||
}
|
||||
mod fonts;
|
||||
mod paths;
|
||||
|
||||
#[wasm_bindgen(module = "fs")]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(catch)]
|
||||
fn readFileSync(path: &str) -> Result<JsValue, JsValue>;
|
||||
}
|
||||
use crate::fonts::FontSearcher;
|
||||
use crate::paths::{PathHash, PathSlot};
|
||||
|
||||
/// A world that provides access to the operating system.
|
||||
#[wasm_bindgen]
|
||||
pub struct SystemWorld {
|
||||
/// The root relative to which absolute paths are resolved.
|
||||
root: PathBuf,
|
||||
/// The input source.
|
||||
main: FileId,
|
||||
/// Typst's standard library.
|
||||
library: Prehashed<Library>,
|
||||
/// Metadata about discovered fonts.
|
||||
book: Prehashed<FontBook>,
|
||||
fonts: Vec<FontSlot>,
|
||||
hashes: RefCell<HashMap<PathBuf, PathHash>>,
|
||||
/// Storage of fonts
|
||||
fonts: Vec<Font>,
|
||||
/// Maps package-path combinations to canonical hashes. All package-path
|
||||
/// combinations that point to thes same file are mapped to the same hash. To
|
||||
/// be used in conjunction with `paths`.
|
||||
hashes: RefCell<HashMap<FileId, FileResult<PathHash>>>,
|
||||
/// Maps canonical path hashes to source files and buffers.
|
||||
paths: RefCell<HashMap<PathHash, PathSlot>>,
|
||||
sources: FrozenVec<Box<Source>>,
|
||||
main: SourceId,
|
||||
js_read_file: js_sys::Function,
|
||||
/// The current date if requested. This is stored here to ensure it is
|
||||
/// always the same within one compilation. Reset between compilations.
|
||||
today: OnceCell<Option<Datetime>>,
|
||||
|
||||
packages: RefCell<HashMap<PackageSpec, PackageResult<PathBuf>>>,
|
||||
|
||||
resizer: fr::Resizer,
|
||||
|
||||
js_request_data: js_sys::Function,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl SystemWorld {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub async fn new(
|
||||
root: String,
|
||||
js_read_file: &js_sys::Function,
|
||||
search_system: bool,
|
||||
) -> Result<SystemWorld, JsValue> {
|
||||
pub fn new(root: String, js_read_file: &js_sys::Function) -> SystemWorld {
|
||||
console_error_panic_hook::set_once();
|
||||
let mut searcher = FontSearcher::new();
|
||||
if search_system {
|
||||
searcher.search_system().await?;
|
||||
} else {
|
||||
searcher.add_embedded();
|
||||
}
|
||||
searcher.add_embedded();
|
||||
|
||||
Ok(Self {
|
||||
Self {
|
||||
root: PathBuf::from(root),
|
||||
main: FileId::detached(),
|
||||
library: Prehashed::new(typst_library::build()),
|
||||
book: Prehashed::new(searcher.book),
|
||||
fonts: searcher.fonts,
|
||||
hashes: RefCell::default(),
|
||||
paths: RefCell::default(),
|
||||
sources: FrozenVec::new(),
|
||||
main: SourceId::detached(),
|
||||
js_read_file: js_read_file.clone(),
|
||||
})
|
||||
today: OnceCell::new(),
|
||||
packages: RefCell::default(),
|
||||
resizer: fr::Resizer::default(),
|
||||
js_request_data: js_read_file.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.hashes.borrow_mut().clear();
|
||||
self.paths.borrow_mut().clear();
|
||||
self.today.take();
|
||||
}
|
||||
|
||||
pub fn compile(
|
||||
&mut self,
|
||||
source: String,
|
||||
text: String,
|
||||
path: String,
|
||||
pixel_per_pt: f32,
|
||||
fill: String,
|
||||
size: u32,
|
||||
display: bool,
|
||||
) -> Result<ImageData, JsValue> {
|
||||
self.sources.as_mut().clear();
|
||||
self.hashes.borrow_mut().clear();
|
||||
self.paths.borrow_mut().clear();
|
||||
self.reset();
|
||||
|
||||
// Insert the main path slot
|
||||
let system_path = PathBuf::from(path);
|
||||
let hash = PathHash::new(&text);
|
||||
self.main = FileId::new(None, &system_path);
|
||||
self.hashes.borrow_mut().insert(self.main, Ok(hash));
|
||||
self.paths.borrow_mut().insert(
|
||||
hash,
|
||||
PathSlot {
|
||||
id: self.main,
|
||||
system_path,
|
||||
buffer: OnceCell::new(),
|
||||
source: Ok(Source::new(self.main, text)),
|
||||
},
|
||||
);
|
||||
|
||||
self.main = self.insert("<user input>".as_ref(), source);
|
||||
match typst::compile(self) {
|
||||
Ok(document) => {
|
||||
let render = typst::export::render(
|
||||
let mut pixmap = typst::export::render(
|
||||
&document.pages[0],
|
||||
pixel_per_pt,
|
||||
Color::Rgba(RgbaColor::from_str(&fill)?),
|
||||
);
|
||||
Ok(ImageData::new_with_u8_clamped_array_and_sh(
|
||||
Clamped(render.data()),
|
||||
render.width(),
|
||||
render.height(),
|
||||
)?)
|
||||
|
||||
let width = pixmap.width();
|
||||
let height = pixmap.height();
|
||||
// Create src image
|
||||
let mut src_image = fr::Image::from_slice_u8(
|
||||
NonZeroU32::new(width).unwrap(),
|
||||
NonZeroU32::new(height).unwrap(),
|
||||
pixmap.data_mut(),
|
||||
fr::PixelType::U8x4,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Multiple RGB channels of source image by alpha channel
|
||||
let alpha_mul_div = fr::MulDiv::default();
|
||||
alpha_mul_div
|
||||
.multiply_alpha_inplace(&mut src_image.view_mut())
|
||||
.unwrap();
|
||||
|
||||
let dst_width = NonZeroU32::new(if display {
|
||||
size
|
||||
} else {
|
||||
((size as f32 / height as f32) * width as f32) as u32
|
||||
})
|
||||
.unwrap_or(NonZeroU32::MIN);
|
||||
let dst_height = NonZeroU32::new(if display {
|
||||
((size as f32 / width as f32) * height as f32) as u32
|
||||
} else {
|
||||
size
|
||||
})
|
||||
.unwrap_or(NonZeroU32::MIN);
|
||||
|
||||
// Create container for data of destination image
|
||||
let mut dst_image = fr::Image::new(dst_width, dst_height, src_image.pixel_type());
|
||||
// Get mutable view of destination image data
|
||||
let mut dst_view = dst_image.view_mut();
|
||||
|
||||
// Resize source image into buffer of destination image
|
||||
self.resizer
|
||||
.resize(&src_image.view(), &mut dst_view)
|
||||
.unwrap();
|
||||
|
||||
alpha_mul_div.divide_alpha_inplace(&mut dst_view).unwrap();
|
||||
|
||||
ImageData::new_with_u8_clamped_array_and_sh(
|
||||
Clamped(dst_image.buffer()),
|
||||
dst_width.get(),
|
||||
dst_height.get(),
|
||||
)
|
||||
}
|
||||
Err(errors) => Err(format!("{:?}", *errors).into()),
|
||||
Err(errors) => Err(format!(
|
||||
"{:?}",
|
||||
errors
|
||||
.into_iter()
|
||||
.map(|e| e.message)
|
||||
.collect::<Vec<EcoString>>()
|
||||
)
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl World for SystemWorld {
|
||||
fn root(&self) -> &Path {
|
||||
&self.root
|
||||
}
|
||||
|
||||
fn library(&self) -> &Prehashed<Library> {
|
||||
&self.library
|
||||
}
|
||||
|
||||
fn main(&self) -> &Source {
|
||||
self.source(self.main)
|
||||
}
|
||||
|
||||
fn resolve(&self, path: &Path) -> FileResult<SourceId> {
|
||||
let path = self.root.join(path);
|
||||
let path = path.as_path();
|
||||
self.slot(path)?
|
||||
.source
|
||||
.get_or_init(|| {
|
||||
let buf = self.read_file(path)?;
|
||||
let text = String::from_utf8(buf)?;
|
||||
Ok(self.insert(path, text))
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn source(&self, id: SourceId) -> &Source {
|
||||
&self.sources[id.into_u16() as usize]
|
||||
}
|
||||
|
||||
fn book(&self) -> &Prehashed<FontBook> {
|
||||
&self.book
|
||||
}
|
||||
|
||||
fn font(&self, id: usize) -> Option<Font> {
|
||||
let slot = &self.fonts[id];
|
||||
slot.font
|
||||
.get_or_init(|| Font::new(slot.buffer.clone(), slot.index))
|
||||
.clone()
|
||||
fn main(&self) -> Source {
|
||||
self.source(self.main).unwrap()
|
||||
}
|
||||
|
||||
fn file(&self, path: &Path) -> FileResult<Buffer> {
|
||||
let path = self.root.join(path);
|
||||
let path = path.as_path();
|
||||
self.slot(path)?
|
||||
.buffer
|
||||
.get_or_init(|| self.read_file(path).map(Buffer::from))
|
||||
.clone()
|
||||
fn source(&self, id: FileId) -> FileResult<Source> {
|
||||
self.slot(id)?.source()
|
||||
}
|
||||
|
||||
fn file(&self, id: FileId) -> FileResult<Bytes> {
|
||||
self.slot(id)?.file()
|
||||
}
|
||||
|
||||
fn font(&self, index: usize) -> Option<Font> {
|
||||
Some(self.fonts[index].clone())
|
||||
}
|
||||
|
||||
fn today(&self, _: Option<i64>) -> Option<Datetime> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl SystemWorld {
|
||||
fn slot(&self, path: &Path) -> FileResult<RefMut<PathSlot>> {
|
||||
let mut hashes = self.hashes.borrow_mut();
|
||||
let hash = match hashes.get(path).cloned() {
|
||||
Some(hash) => hash,
|
||||
None => {
|
||||
let hash = PathHash::new(Buffer::from(self.read_file(path)?));
|
||||
if let Ok(canon) = path.canonicalize() {
|
||||
hashes.insert(canon.normalize(), hash);
|
||||
}
|
||||
hashes.insert(path.into(), hash);
|
||||
hash
|
||||
}
|
||||
};
|
||||
fn read_file(&self, path: &Path) -> FileResult<String> {
|
||||
let f = |_e: JsValue| FileError::Other;
|
||||
Ok(self
|
||||
.js_request_data
|
||||
.call1(&JsValue::NULL, &path.to_str().unwrap().into())
|
||||
.map_err(f)?
|
||||
.as_string()
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
Ok(std::cell::RefMut::map(self.paths.borrow_mut(), |paths| {
|
||||
paths.entry(hash).or_default()
|
||||
fn prepare_package(&self, spec: &PackageSpec) -> PackageResult<PathBuf> {
|
||||
let f = |e: JsValue| {
|
||||
if let Some(num) = e.as_f64() {
|
||||
if num == -2.0 {
|
||||
return PackageError::NotFound(spec.clone());
|
||||
}
|
||||
}
|
||||
PackageError::Other
|
||||
};
|
||||
self.packages
|
||||
.borrow_mut()
|
||||
.entry(spec.clone())
|
||||
.or_insert_with(|| {
|
||||
Ok(self
|
||||
.js_request_data
|
||||
.call1(
|
||||
&JsValue::NULL,
|
||||
&format!("@{}/{}-{}", spec.namespace, spec.name, spec.version).into(),
|
||||
)
|
||||
.map_err(f)?
|
||||
.as_string()
|
||||
.unwrap()
|
||||
.into())
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn slot(&self, id: FileId) -> FileResult<RefMut<PathSlot>> {
|
||||
let mut system_path = PathBuf::new();
|
||||
let mut text = String::new();
|
||||
let hash = self
|
||||
.hashes
|
||||
.borrow_mut()
|
||||
.entry(id)
|
||||
.or_insert_with(|| {
|
||||
let root = match id.package() {
|
||||
Some(spec) => self.prepare_package(spec)?,
|
||||
None => self.root.clone(),
|
||||
};
|
||||
|
||||
system_path = root.join_rooted(id.path()).ok_or(FileError::AccessDenied)?;
|
||||
text = self.read_file(&system_path)?;
|
||||
|
||||
Ok(PathHash::new(&text))
|
||||
})
|
||||
.clone()?;
|
||||
|
||||
Ok(RefMut::map(self.paths.borrow_mut(), |paths| {
|
||||
paths.entry(hash).or_insert_with(|| PathSlot {
|
||||
id,
|
||||
source: Ok(Source::new(id, text)),
|
||||
buffer: OnceCell::new(),
|
||||
system_path,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn insert(&self, path: &Path, text: String) -> SourceId {
|
||||
let id = SourceId::from_u16(self.sources.len() as u16);
|
||||
let source = Source::new(id, path, text);
|
||||
self.sources.push(Box::new(source));
|
||||
id
|
||||
}
|
||||
|
||||
fn read_file(&self, path: &Path) -> FileResult<Vec<u8>> {
|
||||
let f1 = |e: JsValue| {
|
||||
console::error_1(&e);
|
||||
FileError::Other
|
||||
};
|
||||
Ok(self
|
||||
.js_read_file
|
||||
.call1(&JsValue::NULL, &path.to_str().unwrap().into())
|
||||
.map_err(f1)?
|
||||
.as_string()
|
||||
.unwrap()
|
||||
.into_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds details about the location of a font and lazily the font itself.
|
||||
struct FontSlot {
|
||||
buffer: Buffer,
|
||||
index: u32,
|
||||
font: OnceCell<Option<Font>>,
|
||||
}
|
||||
|
||||
/// A hash that is the same for all paths pointing to the same entity.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
struct PathHash(u128);
|
||||
|
||||
impl PathHash {
|
||||
fn new(handle: Buffer) -> Self {
|
||||
// let handle = Buffer::from(read(path)?);
|
||||
let mut state = SipHasher::new();
|
||||
handle.hash(&mut state);
|
||||
Self(state.finish128().as_u128())
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds canonical data for all paths pointing to the same entity.
|
||||
#[derive(Default)]
|
||||
struct PathSlot {
|
||||
source: OnceCell<FileResult<SourceId>>,
|
||||
buffer: OnceCell<FileResult<Buffer>>,
|
||||
}
|
||||
|
||||
struct FontSearcher {
|
||||
book: FontBook,
|
||||
fonts: Vec<FontSlot>,
|
||||
}
|
||||
|
||||
impl FontSearcher {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
book: FontBook::new(),
|
||||
fonts: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn add_embedded(&mut self) {
|
||||
let mut add = |bytes: &'static [u8]| {
|
||||
let buffer = Buffer::from_static(bytes);
|
||||
for (i, font) in Font::iter(buffer.clone()).enumerate() {
|
||||
self.book.push(font.info().clone());
|
||||
self.fonts.push(FontSlot {
|
||||
buffer: buffer.clone(),
|
||||
index: i as u32,
|
||||
font: OnceCell::from(Some(font)),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Embed default fonts.
|
||||
add(include_bytes!("../assets/fonts/LinLibertine_R.ttf"));
|
||||
add(include_bytes!("../assets/fonts/LinLibertine_RB.ttf"));
|
||||
add(include_bytes!("../assets/fonts/LinLibertine_RBI.ttf"));
|
||||
add(include_bytes!("../assets/fonts/LinLibertine_RI.ttf"));
|
||||
add(include_bytes!("../assets/fonts/NewCMMath-Book.otf"));
|
||||
add(include_bytes!("../assets/fonts/NewCMMath-Regular.otf"));
|
||||
add(include_bytes!("../assets/fonts/DejaVuSansMono.ttf"));
|
||||
add(include_bytes!("../assets/fonts/DejaVuSansMono-Bold.ttf"));
|
||||
add(include_bytes!("../assets/fonts/DejaVuSansMono-Oblique.ttf"));
|
||||
add(include_bytes!(
|
||||
"../assets/fonts/DejaVuSansMono-BoldOblique.ttf"
|
||||
));
|
||||
}
|
||||
|
||||
async fn search_system(&mut self) -> Result<(), JsValue> {
|
||||
if let Some(window) = web_sys::window() {
|
||||
for fontdata in JsFuture::from(window.query_local_fonts()?)
|
||||
.await?
|
||||
.dyn_into::<js_sys::Array>()?
|
||||
.to_vec()
|
||||
{
|
||||
let buffer = Buffer::from(
|
||||
js_sys::Uint8Array::new(
|
||||
&JsFuture::from(
|
||||
JsFuture::from(fontdata.dyn_into::<FontData>()?.blob())
|
||||
.await?
|
||||
.dyn_into::<Blob>()?
|
||||
.array_buffer(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
.to_vec(),
|
||||
);
|
||||
for (i, info) in FontInfo::iter(buffer.as_slice()).enumerate() {
|
||||
self.book.push(info);
|
||||
self.fonts.push(FontSlot {
|
||||
buffer: buffer.clone(),
|
||||
index: i as u32,
|
||||
font: OnceCell::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
56
src/paths.rs
Normal file
56
src/paths.rs
Normal file
@ -0,0 +1,56 @@
|
||||
use std::{cell::OnceCell, hash::Hash, path::PathBuf};
|
||||
|
||||
use siphasher::sip128::{Hasher128, SipHasher13};
|
||||
use typst::{diag::FileResult, file::FileId, syntax::Source, util::Bytes};
|
||||
|
||||
/// Holds canonical data for all pahts pointing to the same entity.
|
||||
///
|
||||
/// Both fields can be populated if the file is both imported and read().
|
||||
pub struct PathSlot {
|
||||
/// The slot's canonical file id.
|
||||
pub id: FileId,
|
||||
/// The slot's path on the system.
|
||||
pub system_path: PathBuf,
|
||||
/// The loaded buffer for a path hash.
|
||||
pub buffer: OnceCell<FileResult<Bytes>>,
|
||||
/// The lazily loaded source file for a path hash.
|
||||
pub source: FileResult<Source>,
|
||||
}
|
||||
|
||||
impl PathSlot {
|
||||
pub fn source(&self) -> FileResult<Source> {
|
||||
self.source.clone()
|
||||
}
|
||||
|
||||
pub fn file(&self) -> FileResult<Bytes> {
|
||||
self.buffer
|
||||
.get_or_init(|| Ok(Bytes::from(self.source()?.text().as_bytes())))
|
||||
.clone()
|
||||
}
|
||||
// pub fn source(&self) -> FileResult<Source> {
|
||||
// self.source
|
||||
// .get_or_init(|| {
|
||||
// Ok(Source::new(
|
||||
// self.id,
|
||||
// String::from_utf8(self.buffer.clone()?.to_vec())?,
|
||||
// ))
|
||||
// })
|
||||
// .clone()
|
||||
// }
|
||||
|
||||
// pub fn file(&self) -> FileResult<Bytes> {
|
||||
// self.buffer.clone()
|
||||
// }
|
||||
}
|
||||
|
||||
/// A hash that is the same for all paths pointing to the same entity.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct PathHash(u128);
|
||||
|
||||
impl PathHash {
|
||||
pub fn new(source: &str) -> Self {
|
||||
let mut state = SipHasher13::new();
|
||||
source.hash(&mut state);
|
||||
Self(state.finish128().as_u128())
|
||||
}
|
||||
}
|
16
styles.css
16
styles.css
@ -1 +1,15 @@
|
||||
/* styles */
|
||||
/* styles */
|
||||
textarea {
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.setting-item:has(textarea) {
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.is-disabled {
|
||||
display: none;
|
||||
}
|
@ -10,7 +10,7 @@
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES5",
|
||||
@ -21,4 +21,4 @@
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
||||
}
|
13
types.d.ts
vendored
Normal file
13
types.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
export interface CompileCommand {
|
||||
source: string;
|
||||
path: string;
|
||||
pixel_per_pt: number;
|
||||
fill: string;
|
||||
size: number;
|
||||
display: boolean;
|
||||
}
|
||||
|
||||
export interface WorkerRequest {
|
||||
buffer: Int32Array,
|
||||
path: string
|
||||
}
|
@ -1,17 +1,31 @@
|
||||
export default class TypstCanvasElement extends HTMLCanvasElement {
|
||||
static compile: (source: string, size: number, display: boolean, fontSize: number) => ImageData;
|
||||
static compile: (path: string, source: string, size: number, display: boolean, fontSize: number) => Promise<ImageData>;
|
||||
static nextId = 0;
|
||||
static prevHeight = 0;
|
||||
|
||||
id: string
|
||||
abortController: AbortController
|
||||
source: string
|
||||
path: string
|
||||
display: boolean
|
||||
resizeObserver: ResizeObserver
|
||||
size: number
|
||||
math: boolean
|
||||
|
||||
connectedCallback() {
|
||||
async connectedCallback() {
|
||||
if (!this.isConnected) {
|
||||
console.log("called before connection");
|
||||
console.warn("Typst Renderer: Canvas element has been called before connection");
|
||||
return;
|
||||
}
|
||||
this.draw()
|
||||
|
||||
// if (this.display && this.math) {
|
||||
this.height = TypstCanvasElement.prevHeight;
|
||||
// }
|
||||
|
||||
this.id = "TypstCanvasElement-" + TypstCanvasElement.nextId.toString()
|
||||
TypstCanvasElement.nextId += 1
|
||||
this.abortController = new AbortController()
|
||||
|
||||
if (this.display) {
|
||||
this.resizeObserver = new ResizeObserver((entries) => {
|
||||
if (entries[0]?.contentBoxSize[0].inlineSize !== this.size) {
|
||||
@ -20,43 +34,58 @@ export default class TypstCanvasElement extends HTMLCanvasElement {
|
||||
})
|
||||
this.resizeObserver.observe(this.parentElement!.parentElement!)
|
||||
}
|
||||
await this.draw()
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.display) {
|
||||
TypstCanvasElement.prevHeight = this.height
|
||||
if (this.display && this.resizeObserver != undefined) {
|
||||
this.resizeObserver.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
draw() {
|
||||
|
||||
let fontSize = parseFloat(this.getCssPropertyValue("--font-text-size"))
|
||||
this.size = this.display ? this.parentElement!.parentElement!.innerWidth : parseFloat(this.getCssPropertyValue("--line-height-normal")) * fontSize
|
||||
// console.log(size, this.parentElement);
|
||||
|
||||
|
||||
if (this.display) {
|
||||
this.style.width = "100%"
|
||||
} else {
|
||||
this.style.verticalAlign = "bottom"
|
||||
this.style.height = `${this.size}px`
|
||||
}
|
||||
|
||||
let image: ImageData;
|
||||
let ctx = this.getContext("2d")!;
|
||||
async draw() {
|
||||
this.abortController.abort()
|
||||
this.abortController = new AbortController()
|
||||
try {
|
||||
image = TypstCanvasElement.compile(this.source, this.size, this.display, fontSize)
|
||||
await navigator.locks.request(this.id, { signal: this.abortController.signal }, async () => {
|
||||
let fontSize = parseFloat(this.getCssPropertyValue("--font-text-size"))
|
||||
this.size = this.display ? this.parentElement!.parentElement!.innerWidth : parseFloat(this.getCssPropertyValue("--line-height-normal")) * fontSize
|
||||
|
||||
// resizeObserver can trigger before the element gets disconnected which can cause the size to be 0
|
||||
// which causes a NaN. size can also sometimes be -ve so wait for resize to draw it again
|
||||
if (this.size <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let image: ImageData;
|
||||
let ctx = this.getContext("2d")!;
|
||||
try {
|
||||
image =
|
||||
await TypstCanvasElement.compile(this.path, this.source, this.size, this.display, fontSize)
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.outerText = error
|
||||
return
|
||||
}
|
||||
|
||||
if (this.display) {
|
||||
this.style.width = "100%"
|
||||
this.style.height = ""
|
||||
} else {
|
||||
this.style.verticalAlign = "bottom"
|
||||
this.style.height = `${this.size}px`
|
||||
}
|
||||
this.width = image.width
|
||||
this.height = image.height
|
||||
|
||||
ctx.imageSmoothingEnabled = true
|
||||
ctx.imageSmoothingQuality = "high"
|
||||
ctx.putImageData(image, 0, 0);
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.outerText = error
|
||||
return
|
||||
}
|
||||
|
||||
this.width = image.width
|
||||
this.height = image.height
|
||||
|
||||
ctx.imageSmoothingEnabled = true
|
||||
ctx.imageSmoothingQuality = "high"
|
||||
ctx.putImageData(image, 0, 0);
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"0.5.0": "1.0.0",
|
||||
"0.4.2": "1.0.0",
|
||||
"0.4.1": "1.0.0",
|
||||
"0.4.0": "1.0.0",
|
||||
|
Reference in New Issue
Block a user