Skip to content

typst-ide-wasm: library images and font face metadata - #5

Closed
Maxanox wants to merge 10 commits into
mainfrom
feat/ide-library-fallback
Closed

Maxanox wants to merge 10 commits into
mainfrom
feat/ide-library-fallback

Conversation

@Maxanox

@Maxanox Maxanox commented Sep 26, 2026

Copy link
Copy Markdown

Summary

  • Library images. A project file that does not exist is looked up by its bare file name among host-supplied images — the same rule as typst compile --fallback-dir (project files win, packages never fall back). JS API: IdeSession.setLibraryImages(names), addLibraryImage(name, bytes), takeMissingLibraryImages() (the host fetches what a compile needed and was not given).
  • Completions. Library image names are offered in path completions from any directory, deduplicated against project files, without interning file ids.
  • fontFaces(bytes). Reads every face of a font file with the compiler's own parser ({ index, family, style, weight, stretch, monospace, serif, math, variable }), so a host can record at upload time the family text(font: ..) will match. Work is bounded against hostile files: forged collection headers, and cmap tables whose declared codepoints (all formats, every encoding record, located exactly as ttf-parser's collect_tables does) exceed a per-face and per-file budget are rejected before parsing.
  • Only crates/typst-ide-wasm changes (plus one Cargo.lock edge: ttf-parser becomes a direct dependency at the version already in the tree). Nothing panics on host input.

Test plan

  • cargo test -p typst-ide-wasm (48 tests: fallback rule, missing-name reporting and cache invalidation, completions incl. interner-exhaustion regression, font faces incl. .ttc, forged headers, hostile cmaps, unsorted/duplicate table directories, format 2 amplification)
  • cargo clippy -p typst-ide-wasm --all-targets -- -D warnings, cargo check -p typst-ide-wasm --target wasm32-unknown-unknown, cargo fmt --all -- --check
  • CI

🤖 Generated with Claude Code

Maxanox and others added 10 commits September 26, 2026 01:04
…by file name

A project file that does not exist is looked up by its bare file name in
a host-supplied set of library images, the same rule as
`typst compile --fallback-dir`. Project files always win, package files
never fall back, and names the host hands in are accepted only as bare
file names. Lookups that need bytes not supplied yet are reported through
take_missing_library_images.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IdeWorld::files now also yields, for each known library image name, the
path it would resolve to from the completing file's directory — unless
a project file already sits there, which wins and is already listed.

Carried from the prior review: library_name gets direct unit coverage
(bare names accepted, anything else rejected), and World::source notes
that sources deliberately never fall back to the library, unlike the
CLI's own fallback which serves both.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reading a face lists every codepoint its cmap declares, and a single format 12 group can declare four billion. Count the declared codepoints from the raw table first and reject reversed or out-of-range groups and faces or files that declare too many.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The table directory need not be sorted and may list cmap twice; the parser takes the last cmap record in a linear pass, so the coverage check now locates the table the same way instead of by binary search.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
File ids come from a global interner that never frees one and holds at
most 65,535. Listing files interned a path for every library name in
every directory completions were asked about, so a long session with a
large library ran out of ids and panicked. Compare virtual paths against
the project's files instead, and leave sorting to the IDE, which sorts
what it gets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Supplying bytes for a name outside the set, or setting the same set of
library images again, changes nothing the compiler sees, so it no longer
marks the session for recompilation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The font parser walks all 256 keys of a format 2 subtable even when its
sub-headers list nothing, but such a subtable counted as empty, so a face
with thousands of encoding records pointing at one could take seconds to
read. Count every key as at least one and every encoding record as at
least one, so the count bounds the parser's work and not only its
output. Each subtable is counted once and charged to every record that
points at it, which keeps the check itself linear in the table's size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Maxanox

Maxanox commented Sep 26, 2026

Copy link
Copy Markdown
Author

Superseded: this work continues outside this repository.

@Maxanox Maxanox closed this Sep 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant