Typst-side toolchain for myform (as of 2026-07-20)#

Question: What is the Python-accessible Typst analogue of the markdown-it-py / mdformat ecosystem myform already joins — for parsing .typ into an AST and for formatting/pretty-printing Typst source?

Thesis: There is no such ecosystem to join, on either side. Nothing in the Typst world plays the role markdown-it-py/mdformat play for Markdown. Emit directly from myform’s own AST as hand-written Typst source strings (the same architecture myform/Renderer.py already uses for Markdown, and the same architecture mystmd’s official Typst exporter uses — this is confirmed convergent prior art, not a novel choice). Format by shelling out to the typstyle Rust binary as an optional external dependency, the same way one might shell out to black or prettier, because no Python-native or plugin-extensible Typst formatter exists. Read/parse .typ has no good Python option at all right now; treat it as future scope gated on either a maintained tree-sitter grammar reappearing or hand-rolling a minimal recursive-descent parser against typst-syntax’s node inventory as a spec.

All claims below are dated 2026-07-20 (retrieval date); Typst itself is versioned 0.15.x pre-1.0, so all of this is subject to breaking change on any Typst point release.

1. Typst parsing from Python#

No option produces a usable source-level AST inside a Python process today, per a Typst community-forum thread devoted to exactly this question that concludes there is “no turnkey solution” and that typst-py is merely “the most mature effort” without actually exposing the AST — Typst Forum: “How can I access the Typst AST from Python?” (2026-07-20; a forum thread is a weak source in isolation, but here it is corroborated by every primary source below, so I’m treating the conclusion as reliable even though the thread itself isn’t citable for facts).

  • typst PyPI package (PyPI, GitHub messense/typst-py; v0.15.0, released 2026-06-16, actively maintained, 24 releases) is a PyO3 binding to the real Rust typst compiler. It exposes compile() (to PDF/PNG/SVG/HTML), a Compiler class, query(), and eval(). query() only returns “locatable elements of the compiled document” (post-layout content, i.e. the Content/element model, not source syntax); functions and macros aren’t locatable this way. It does not expose typst-syntax’s SyntaxNode/AST at all — confirmed by the PyPI/GitHub descriptions and by the forum thread. Import cost is real: the Linux manylinux wheel is 34.2 MB (PyPI file listing, retrieved 2026-07-20) because it embeds the full compiler, layout engine, and fonts — not appropriate for a formatter that must start fast.

  • tree-sitter-typst grammars: two independent, uncoordinated community grammars, neither current relative to Typst’s own pace of change:

    • frozolotl/tree-sitter-typst — last commit 2024-01-29 (retrieved via commit history, 2026-07-20); explicitly lists missing coverage for “Code, Lists, Enums, Terms.”

    • uben0/tree-sitter-typst — last commit 2025-02-10; the maintainer’s own README calls the scanner code “a spaghetti mess” awaiting a rewrite that (per the README) never shipped.

    • Neither publishes a Python wheel; both would need on-machine grammar compilation via py-tree-sitter’s build step. Generic tree-sitter core itself is light — 668 KB manylinux wheel (PyPI, retrieved 2026-07-20), ~50x lighter than typst-py — so if a maintained grammar existed, this would be the right-weight answer. It doesn’t.

    • Tellingly, Typst’s own tooling (the tinymist LSP, typstyle) is built on the native typst-syntax Rust crate, not tree-sitter — tree-sitter-typst is a community side effort, not something the core project uses or feeds.

  • No pure-Python Typst parser exists. Search turned up only Python→Typst emitters (pypst, typstpy, md2typst; see §4/§5) and the typst-py compiler binding — no project that parses .typ source into a tree in pure Python.

  • No published WASM build of typst-syntax alone. Myriad-Dreamin/typst.ts (“Run Typst in JavaScript World”) ships WASM compiler/renderer modules on npm (@myriaddreamin/typst-ts-web-compiler) for JS/browser use, not Python, and compiles the whole engine, not a standalone syntax-tree module.

2. Typst formatting/pretty-printing#

typstyle is the maintained formatter; typstfmt is dead.

  • typstyle-rs/typstyle — v0.15.0, released 2026-06-20 (retrieved 2026-07-20). Parses to AST via typst-syntax, formats with a Wadler-style pretty-printer (a fork of pretty.rs called prettyless), and ships correctness guarantees (idempotence + render-identical output tested against real repos). It is opinionated by design and has no plugin/config-rule extensibility — the maintainer’s stated rationale is explicitly against the config-sprawl model mdformat plugins rely on. Distribution: CLI binary, a Rust library crate (typstyle-core on crates.io), and a WASM/npm build (@typstyle/typstyle-wasm-bundler) for JS. No Python binding exists. It’s the formatter wired into tinymist (the standard Typst LSP/editor extension).

  • typstfmt is abandoned, twice over, and the community has already redirected to typstyle:

    • astrale-sharp/typstfmt (original) — archived 2024-06-08; maintainer’s own note points users to typstyle.

    • jeffa5/typstfmt (a rewrite of the above, aiming for more config flexibility) — archived 2025-12-11, development moved to a private self-hosted git instance.

  • Nothing mdformat-like exists in the Typst world — i.e. no plugin-extensible formatter where third parties register custom node handlers into a shared pretty- printing pipeline. typstyle is a closed, opinionated Rust binary; there is no equivalent of mdformat’s Plugin/extension-point model on the Typst side.

3. Typst AST / content-model specs#

Two distinct things exist, neither an external stability contract:

  • Source syntax tree — typst-syntax crate (v0.15.1): SyntaxNode/SyntaxKind (untyped tree), a typed ast module, Source/Span/LinkedNode. Reasonably documented (97% doc coverage per docs.rs) but its own top-of-file doc comment is a one-line “Parser and syntax tree for Typst” (source, retrieved 2026-07-20) — no explicit stability/semver statement either way. Given Typst itself is pre-1.0 (0.15.x), treat every node name as liable to change on any release; that caution is an inference from the version number, not a quoted commitment from the crate.

  • Content/document model — typst.app/docs/reference/foundations/content/ and typst.app/docs/reference/model/ document the runtime Content value (elements + fields, compositional via +) and ~24 structural elements (heading, list, figure, table, bibliography, footnote, quote, etc.). This is a post-compilation semantic model, not a source-syntax spec, and it’s documentation-by-reference-page rather than one canonical schema document.

Recommendation for node-type alignment: use typst-syntax’s SyntaxKind enum (read via docs.rs, no install required) as the informal vocabulary to name myform’s Typst-facing AST nodes, and the foundations/model reference pages as the vocabulary for block/inline semantic categories (heading vs. list vs. figure) — but do not treat either as a frozen external contract; snapshot the version consulted.

4. Markdown↔Typst converters as prior art#

  • Pandoc: writer is mature, reader is young.

    • Typst writer shipped in pandoc 3.1.2 (per web search corroborated by third-party Typst/pandoc integration repos noting they stopped maintaining custom extensions once “native support for Typst” landed) and has continued to receive fixes through 3.10 (released 2026-06-04, added --typst-input for sys.inputs).

    • Typst reader (Text.Pandoc.Readers.Typst) exists and evaluates .typ into the Pandoc AST, but its Hackage module doc explicitly marks stability as “alpha” (Hackage pandoc-3.6.3 docs, retrieved 2026-07-20) with no documented list of unsupported syntax — read this as “don’t lean on it as a spec of correct Typst parsing yet.” Two third-party custom reader/writer projects (BroadbandForum/pandoc-typst-writer, lvignoli/typst-pandoc) predate native support and are now superseded.

    • Architecturally, pandoc’s Typst reader is a Haskell implementation independent of typst-syntax — it is not itself Python-accessible and wouldn’t help myform directly even if more mature; it’s useful as a second data point on what “supported Typst syntax” looks like for a document converter, not as a dependency.

  • mystmd’s Typst exporter is the most directly relevant prior art, and it validates the recommended architecture. packages/myst-to-typst in the mystmd monorepo is a hand-written visitor/handler pattern (TypstSerializer class, handlers object keyed by mdast/MyST node type — confirmed by reading src/index.ts, retrieved 2026-07-20) covering ~50 node types (paragraph, heading, emphasis/strong, code, math via MATH_HANDLERS, lists, tables, links, cross-references, citations, footnotes, images, admonitions, tabs, etc.). It writes Typst markup as raw strings directly via state.write() — it imports no Typst-specific parsing/AST library at all. This is exactly the architecture myform’s own Renderer.py already uses for Markdown, just aimed at a different output grammar. No published node-mapping spec document exists beyond the source itself; the README is a one-line stub (“Convert a MyST AST to Typst”).

  • Minor Python-native prior art, emit-only, none general-purpose enough to build on:

    • pypst (GitHub tilman151/pypst, latest release 2025-12-28) — a declarative “build Typst objects in Python, get Typst source out” library, MIT-licensed. Covers headings, lists, tables (with strong Pandas-dataframe-to-table support as its actual headline use case), figures, images, imports. No parsing capability, no math support, narrow node coverage — it’s closer to a report-generation DSL than a general Markdown-superset emitter. Worth knowing about, not worth depending on.

    • typstpy (PyPI) — functional-style Typst code generation, similarly emit-only and narrower in scope than what myform needs.

    • md2typst (PyPI) — Markdown→Typst string converter; another single-purpose converter, not a reusable AST/renderer library.

5. Recommendation#

(a) EMIT path — build now, no new runtime dependency:

  1. Add a Renderer-style Typst emitter inside myform’s own AST→text pipeline, symmetric to the existing Markdown Renderer.py/Postprocessor.py split. This is the architecture both mystmd’s official exporter and myform itself already use — not a novel bet.

  2. For final normalization/pretty-printing, shell out to the typstyle CLI binary as an optional extra (e.g. myform[typst-format] installs/requires the binary on $PATH, or vendor via the WASM/npm build if a pure-binary dependency is unwelcome). Do not attempt to reimplement typstyle’s pretty-printing rules in Python; it has no plugin surface to extend anyway, so there’s nothing to “join,” only a binary to call.

  3. Do not take a typst-py dependency for the emit path — 34 MB of compiler+font payload for output you’re generating yourself is pure import-time cost with no benefit.

(b) READ path — defer, no good option today: Ranked, worst-case-first:

  1. Best-available-but-unmaintained: fork/revive uben0/tree-sitter-typst (more recently touched, 2025-02-10, but self-described as needing a rewrite) or frozolotl/tree-sitter-typst (closer to spec-correctness goal but stale since 2024-01-29 and missing whole node categories). Either requires real upstream investment before it’s trustworthy, and neither has a Python wheel today — you’d own packaging via py-tree-sitter’s build step.

  2. Heaviest but most correct: wrap typst-syntax directly via a small PyO3 binding (something typst-py does not currently expose per the forum thread) — this is “write the binding yourself,” the community’s own fallback suggestion, and the only option that tracks the real parser exactly. Costly in maintenance, not runtime weight (a thin binding need not carry the whole compiler/font payload typst-py does).

  3. Cheapest, narrowest: hand-roll a minimal recursive-descent Typst parser in Python scoped only to the markup subset myform actually needs to round-trip (headings, emphasis, lists, links, math, code, tables) — explicitly not general Typst, using typst-syntax’s SyntaxKind names as the target vocabulary. Realistic if the read path only ever needs to ingest myform-authored or otherwise constrained .typ files, not arbitrary Typst in the wild.

  4. Do not depend on pandoc’s Typst reader for anything load-bearing; it’s explicitly alpha and not Python-native anyway.

Follow-ups not chased here (in-scope-adjacent, out of this memo’s bound): (i) whether typstyle’s WASM build could be driven from Python via wasmtime-py instead of a subprocess call — worth a spike if subprocess overhead becomes a measured problem, not before; (ii) whether contributing fixes upstream to one of the two stale tree-sitter grammars is more efficient than a from-scratch PyO3 binding, once the read path is actually prioritized; (iii) re-check typst-py for AST exposure at its next release — the forum thread notes the original poster was considering contributing an “AST wrapper” themselves, so this could change without much warning.

Source-disagreement note: none of substance — every source (forum thread, PyPI descriptions, GitHub repos, docs.rs, mystmd source) converges on the same conclusion: no Python-accessible Typst AST exists today. The only soft disagreement is stylistic — typstfmt’s now-archived README frames itself as more flexible/configurable than typstyle, while typstyle’s README frames flexibility as the wrong goal — but this is now moot since typstfmt is dead on both forks.

This question was well-bounded to a single-charter survey (a handful of primary repos and docs, no adversarial fact-checking or compliance stakes); the deep-research skill is not warranted here.