npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dom-typst

v0.1.14

Published

Convert HTML to Typst markup — native Node.js addon

Readme

dom-typst

npm version npm downloads license

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  ──►  PDF

Why 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-typst

No 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 platforms

TypeScript / 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_factor

Example: 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 item

Description 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 / rowspan are 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:

  1. SVG extraction — all <svg>…</svg> blocks are pulled out and replaced with <img> placeholders
  2. rempx conversionrem units in <style> blocks are rewritten to px before CSS inlining (workaround for a css-inline limitation)
  3. CSS inlining<style> rules are applied as style="" attributes using css-inline
  4. HTML sanitisation<script>, <style>, <head> and other noise is stripped using ammonia
  5. DOM traversal — the sanitised document is walked and each element is mapped to its Typst equivalent
  6. 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.pdf

Q: 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