dom-typst
v0.1.14
Published
Convert HTML to Typst markup — native Node.js addon
Maintainers
Readme
dom-typst
Convert HTML markup into Typst markup — fast, native Node.js addon written in Rust. No build step, no native dependencies to install.
HTML string ──► dom-typst ──► Typst markup string ──► typst compile ──► PDFWhy dom-typst?
| | dom-typst | Manual conversion | |---|---|---| | HTML sanitisation | ✅ automatic (ammonia) | ❌ manual | | CSS inlining | ✅ automatic | ❌ manual | | SVG support | ✅ inline, no files | ❌ complex | | Cross-platform | ✅ prebuilt binaries | — | | Performance | ✅ native Rust | — |
Install
npm install dom-typstNo build step required. Prebuilt native binaries are bundled for all major platforms — just npm install and use.
| Platform | Architectures | |----------|--------------| | Linux (glibc) | x64, arm64, armv7 | | Linux (musl / Alpine) | x64, arm64 | | macOS | x64, arm64 (Apple Silicon) | | Windows | x64, ia32 |
Quick start
CommonJS
const { parse } = require('dom-typst');
const html = `
<h1>My Document</h1>
<p>Hello <strong>world</strong>! This is <em>dom-typst</em>.</p>
<ul>
<li>Fast — native Rust core</li>
<li>Zero runtime dependencies</li>
<li>Works on all major platforms</li>
</ul>
`;
const { typstBody } = parse(html);
console.log(typstBody);Output:
#set par(justify: true)
#heading(level: 1)[My Document]
Hello *world*! This is _dom-typst_.
- Fast — native Rust core
- Zero runtime dependencies
- Works on all major platformsTypeScript / ESM
import { parse, Config } from 'dom-typst';
const config: Config = {
pageSize: 'a4', // named page size (sets #set page automatically)
editorCanvasWidthPx: 1200, // width of your HTML viewport in px
rootFontSizePx: 16, // 1rem = 16px
};
const { typstBody } = parse('<h2>Report</h2><p>Content here.</p>', config);Full pipeline example — HTML file → PDF
const { parse } = require('dom-typst');
const { execSync } = require('child_process');
const fs = require('fs');
// 1. Read your HTML
const html = fs.readFileSync('report.html', 'utf8');
// 2. Convert to Typst — page setup is auto-generated
const { typstBody } = parse(html, {
pageSize: 'a4', // sets #set page(width: 595.28pt, height: 841.89pt, margin: 2cm)
editorCanvasWidthPx: 960,
justify: true, // default, adds #set par(justify: true)
});
// 3. Wrap in any extra preamble you need
const typstDoc = [
'#set text(font: "Linux Libertine", size: 11pt)',
'#set heading(numbering: "1.")',
'',
typstBody,
].join('\n');
// 4. Write .typ file
fs.writeFileSync('report.typ', typstDoc, 'utf8');
// 5. Compile to PDF (requires typst CLI: https://github.com/typst/typst)
execSync('typst compile report.typ report.pdf');
console.log('PDF generated: report.pdf');API Reference
parse(html, config?)
Converts an HTML string to Typst markup.
function parse(html: string, config?: Config): ParseResult| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| html | string | ✅ | Raw HTML string. Full documents (<!DOCTYPE html>) and fragments both work. |
| config | Config | ❌ | Optional scaling and font configuration. |
Returns ParseResult:
interface ParseResult {
typstBody: string // ready-to-embed Typst markup
}Config — all fields optional
interface Config {
// Scaling
editorCanvasWidthPx?: number // default: 800 — pixel width of your HTML viewport
targetPageWidthPt?: number // default: 595.28 — overridden when pageSize is set
rootFontSizePx?: number // default: 16 — resolves rem units
scaleFactor?: number // direct px→pt ratio override
// Page layout
pageSize?: string // named size: 'a4', 'a3', 'a5', 'letter', 'legal', etc.
pageSetup?: PageSetup // fine-grained #set page(…) control
justify?: boolean // default: true — prepends #set par(justify: true)
}| Field | Default | Description |
|-------|---------|-------------|
| editorCanvasWidthPx | 800 | Pixel width of the HTML rendering context. All px sizes are scaled relative to this. |
| targetPageWidthPt | 595.28 | Output page width in points. Overridden automatically when pageSize is set. |
| rootFontSizePx | 16 | Base font size for resolving rem CSS values into Typst pt. |
| scaleFactor | computed | Direct scale-factor override. Bypasses editorCanvasWidthPx / targetPageWidthPt. E.g. 0.75 → 1 px = 0.75 pt. |
| pageSize | none | Named page size — sets both the #set page(…) dimensions and targetPageWidthPt for scaling. Accepts 'a0'–'a6', 'b4', 'b5', 'letter', 'legal', 'tabloid'. Case-insensitive. |
| pageSetup | see below | Fine-grained #set page(…) control. Pass {} for A4 defaults. Omit the field to disable the directive entirely. |
| justify | true | When true, prepends #set par(justify: true). Set false to disable. |
How scaling works
scale_factor = targetPageWidthPt / editorCanvasWidthPx
value_pt = value_px × scale_factorExample: canvas 800 px → A4 595.28 pt → scale = 0.744
A 20px font becomes 20 × 0.744 = 14.88pt.
If your HTML is rendered at 1200 px wide, set editorCanvasWidthPx: 1200 so font and layout sizes scale correctly to your target page.
Page sizes
Pass pageSize as a string to avoid specifying raw pt values:
// Named size — auto-generates #set page(…) and sets scaling
parse(html, { pageSize: 'a4' }) // 595.28 × 841.89 pt (default)
parse(html, { pageSize: 'a3' }) // 841.89 × 1190.55 pt
parse(html, { pageSize: 'a5' }) // 419.53 × 595.28 pt
parse(html, { pageSize: 'letter' }) // 612 × 792 pt
parse(html, { pageSize: 'legal' }) // 612 × 1008 pt
// Named size + custom margin
parse(html, { pageSize: 'a4', pageSetup: { margin: '1cm' } })
// Raw pt values (when you need non-standard dimensions)
parse(html, { pageSetup: { widthPt: 500, heightPt: 700, margin: '1.5cm' } })
// Disable #set page(…) entirely — embed in your own preamble
parse(html, { justify: false })PageSetup object
Controls the #set page(…) directive. All fields optional.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| size | string | — | Named size (same list as pageSize). Overrides widthPt/heightPt. |
| widthPt | number | 595.28 | Page width in points. Ignored when size is set. |
| heightPt | number | 841.89 | Page height in points. Ignored when size is set. |
| margin | string | '2cm' | Margin passed verbatim to Typst: '1cm', '1in', '(top: 2cm, rest: 1.5cm)', etc. |
HTML element support
Headings
<h1>Title</h1>
<h2>Section</h2>
<h3>Subsection</h3>#heading(level: 1)[Title]
#heading(level: 2)[Section]
#heading(level: 3)[Subsection]Inline formatting
| HTML | Typst |
|------|-------|
| <b>, <strong> | *bold* |
| <i>, <em>, <cite> | _italic_ |
| <u>, <ins> | #underline[…] |
| <s>, <del>, <strike> | #strike[…] |
| <mark> | #highlight[…] |
| <small> | #text(size: 0.85em)[…] |
| <sup> | superscript |
| <sub> | subscript |
| <q> | "…" (curly quotes) |
| <code>, <kbd>, <samp>, <var> | verbatim |
Links
<a href="https://typst.app">Typst</a>#link("https://typst.app")[Typst]Lists
<ul>
<li>Unordered item</li>
</ul>
<ol>
<li>Ordered item</li>
</ol>- Unordered item
+ Ordered itemDescription lists
<dl>
<dt>Term</dt>
<dd>Definition of the term.</dd>
</dl>*Term*
Definition of the term.Blockquote
<blockquote>A wise quote.</blockquote>#quote[
A wise quote.
]Code
<!-- Block -->
<pre><code>fn main() {
println!("Hello");
}</code></pre>
<!-- Inline -->
<p>Run <code>cargo build</code>.</p>fn main() { println!("Hello"); }
Run `cargo build`.Tables
<table>
<caption>Sales Data</caption>
<thead>
<tr><th>Quarter</th><th>Revenue</th><th>Growth</th></tr>
</thead>
<tbody>
<tr><td>Q1</td><td>$1.2M</td><td>+5%</td></tr>
<tr><td>Q2</td><td>$1.5M</td><td>+12%</td></tr>
</tbody>
</table>*Sales Data*
#table(
columns: 3,
[Quarter], [Revenue], [Growth],
[Q1], [$1.2M], [+5%],
[Q2], [$1.5M], [+12%],
)
colspan/rowspanare not supported. Short rows are padded with empty[]cells.
Horizontal rule
<hr>#line(length: 100%)SVG (inline, no external files)
<svg> elements are extracted, base64-encoded, and re-embedded:
#figure(
image(bytes("…svg content…"), format: "svg"),
caption: [Inline SVG 1],
)The returned Typst string is fully self-contained — no external files needed.
CSS styles
<style> blocks are automatically inlined onto elements before processing.
| CSS property | Typst output |
|-------------|--------------|
| color: #ff0000 | #text(fill: rgb("#ff0000"))[…] |
| color: red (named) | #text(fill: rgb("#ff0000"))[…] (resolved) |
| font-size: 20px | #text(size: 14.88pt)[…] (scaled) |
| font-size: 1.5rem | #text(size: Npt)[…] (resolved) |
| font-size: 1.2em | #text(size: 1.2em)[…] |
| font-weight: bold / 700 | *bold* wrapping |
| font-style: italic | _italic_ wrapping |
| letter-spacing: Npx | #text(tracking: N.NNpt)[…] |
| text-transform: uppercase | #upper[…] |
| text-transform: lowercase | #lower[…] |
| text-align: center | #align(center)[…] (block elements only) |
| text-align: right | #align(right)[…] (block elements only) |
| background-color: <color> | #block(fill: rgb(…), …)[…] |
| border: Npx solid <color> | #block(stroke: N.NNpt + rgb(…), …)[…] |
| padding: Npx | #block(inset: N.NNpt, …)[…] |
| margin-top: Npx | #v(N.NNpt) before element |
| margin-bottom: Npx | #v(N.NNpt) after element |
| column-count: N | #columns(N, …)[…] (block elements only) |
| column-gap: Ncm | gutter in #columns(…, gutter: …) |
Supported named colors: white, black, red, lime, blue, yellow, cyan, magenta, silver, gray, maroon, olive, green, purple, teal, navy, orange.
CSS var(--…) values and transparent are silently skipped.
Stripped / ignored elements
| Category | Elements |
|----------|----------|
| Scripts & metadata | <script>, <style>, <head>, <meta>, <link>, <title>, <noscript> |
| Form controls | <form>, <input>, <select>, <button>, <option>, <datalist>, <output> |
| Media | <audio>, <video>, <source>, <track>, <embed>, <object> |
| Progress | <progress>, <meter> |
| Graphics / math | <canvas>, <math> (<svg> is handled separately) |
Pass-through containers
These elements render their children as-is without any wrapping:
<div>, <span>, <section>, <article>, <main>, <header>, <footer>, <nav>, <aside>, <figure>, <details>, <fieldset>, <address>, <time>, <abbr>, <label>
Text escaping
Plain text nodes automatically escape characters that conflict with Typst syntax:
| Character | Escaped as |
|-----------|-----------|
| # | \# |
| $ | \$ |
| * | \* |
| _ | \_ |
| @ | \@ |
| ` | \` |
Processing pipeline
Internally dom-typst runs the following steps on every call:
- SVG extraction — all
<svg>…</svg>blocks are pulled out and replaced with<img>placeholders rem→pxconversion —remunits in<style>blocks are rewritten topxbefore CSS inlining (workaround for acss-inlinelimitation)- CSS inlining —
<style>rules are applied asstyle=""attributes using css-inline - HTML sanitisation —
<script>,<style>,<head>and other noise is stripped using ammonia - DOM traversal — the sanitised document is walked and each element is mapped to its Typst equivalent
- SVG substitution — placeholders are replaced with
image(bytes(…), format: "svg")calls
Frequently asked questions
Q: Can I use this with a full HTML file (<!DOCTYPE html> …)?
Yes. Pass the full document string — the library parses the <body> contents and ignores everything else.
Q: Does it support CSS classes and stylesheets?
Yes. CSS from <style> blocks is automatically inlined onto elements before conversion, so class-based styling is preserved.
Q: What happens with images (<img>)?<img> tags are converted to _[Image: alt text]_ using the alt attribute as a fallback. Inline <svg> elements are fully embedded.
Q: Can I control the output page size?
Use the targetPageWidthPt config option. This doesn't add a #set page(…) to the output — it just controls how pixel sizes are scaled. Add your own #set page(…) to the document preamble.
Q: Does this compile to PDF?
No — dom-typst only produces the Typst markup string. To compile to PDF, use the Typst CLI:
typst compile document.typ document.pdfQ: Is it safe to pass untrusted HTML?
Yes. All HTML is sanitised with ammonia before processing, which strips scripts, event handlers, and dangerous attributes.
License
MIT © dom-typst contributors
