Show HN: DOM-docx – HTML to native, editable Word docs (MIT)
Summary
DOM-docx is an open-source library that converts semantic HTML fragments into native, editable Word documents (OOXML) without screenshots or layout hacks, supporting Node.js and browser environments.
View Cached Full Text
Cached at: 07/13/26, 01:52 PM
floodtide/dom-docx
Source: https://github.com/floodtide/dom-docx
dom-docx
Convert semantic HTML fragments to native, editable Word documents (OOXML): paragraphs, runs, lists, tables, images. Not screenshots or layout hacks.
Live demo: dom-docx.com. Try the converter, browse showcases, read the learn guide.
Built with a visual regression loop: render HTML in Chromium, convert to docx, rasterize via LibreOffice, score layout + structural fidelity against a human-validated metric, iterate. Latest scores: TEST-SCORES.md · methodology: SCORING.md.
Install
npm install dom-docx
Requires Node.js ≥ 20. No browser or Playwright is needed for the default inline path.
When is Playwright needed?
| Entry | styleSource: "inline" | styleSource: "computed" | rasterizeInPlace |
|---|---|---|---|
Node (dom-docx) | Pure JS, no browser | Playwright + Chromium | Playwright + Chromium (same headless page) |
Browser (dom-docx/browser) | Pure JS, no live DOM | Live page: native getComputedStyle | Live page: canvas/SVG → PNG <img> in the tab |
On Node, playwright is an optional peer dependency. npm install dom-docx pulls only docx, cheerio and fflate, nothing heavy. It is loaded lazily when you pass styleSource: "computed" or rasterizeInPlace. To use those paths, install Playwright and Chromium yourself, once:
npm install playwright
npx playwright install chromium
Playwright is also used by the dev test harness (not required to use the library). Contributors: npm run setup after clone.
LibreOffice is not needed to convert. It is only used for the visual test harness.
CLI
Convert a file without writing any code:
npx dom-docx input.html -o output.docx
npx dom-docx input.html # writes input.docx next to it
cat fragment.html | npx dom-docx - -o - # stdin → binary stdout (pipelines)
npx dom-docx input.html -s computed # stylesheet/class HTML (needs playwright installed)
npm install -g dom-docx # optional: install globally, then run "dom-docx" without npx
Input is a body HTML fragment, same as the API. --help for all options.
Quick start
Browser
import { convertHtmlToDocx } from "dom-docx/browser";
const html = `
<h1 style="color:#1a1a2e">Quarterly Report</h1>
<p>Revenue grew <strong>12%</strong> year over year.</p>
<ul>
<li>North America</li>
<li>EMEA</li>
</ul>
`;
const blob = await convertHtmlToDocx(html);
// e.g. trigger a download in the browser
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "output.docx";
a.click();
No Playwright, no Node. This runs entirely in the user’s tab. See Browser bundle below.
Node
import { writeFile } from "node:fs/promises";
import { convertHtmlToDocx } from "dom-docx";
const html = `
<h1 style="color:#1a1a2e">Quarterly Report</h1>
<p>Revenue grew <strong>12%</strong> year over year.</p>
<ul>
<li>North America</li>
<li>EMEA</li>
</ul>
`;
const docx = await convertHtmlToDocx(html);
await writeFile("output.docx", docx);
Pass a body fragment only (no <!DOCTYPE> / <html> / <body> required). Defaults: US Letter, 1″ margins, Arial 10.5pt body text.
v0.1.x capability
Supported (default styleSource: "inline"):
- Headings, paragraphs, lists (
<ul>/<ol>includinglist-style-type), tables, links, inline formatting - Block backgrounds, blockquotes,
<hr>, simple flex rows (≤4 items) data:images; remote images via yourimageResolver- Page size/orientation/margins, default font, metadata, header/footer HTML, page numbers, table of contents, lang/direction
- Low-complexity inline SVG (bars + text)
- CSS bar divs in table cells (background + height/width → native shaded bands)
Advanced (optional styleSource: "computed"):
- Resolves
<style>blocks and class/#idselectors viagetComputedStyle - Node: requires
playwright(optional peer dependency, installed separately) + Chromium. The library launches headless Chromium to render the fragment - Browser bundle: uses the live DOM in the user’s tab. No Playwright, no extra install
- SPA fragment export: pass
root(browser) orrootSelector(Node + livepage) when convertingelement.innerHTMLso computed-style paths match the fragment tree - Inline is the supported default for npm installs; computed is for stylesheets/classes or when you already have a rendered page
Charts & complex SVG (optional rasterizeInPlace):
- Rasterizes
<canvas>and complex<svg>(e.g. Highcharts) to PNG<img>before conversion - Recommended for charts:
rasterizeInPlace: { scale: 2 }— supersamples at 2× density for sharper images in Word (defaultscale: 1; max4) - Browser: requires
root. Clones off-screen by default so the live page is not mutated - Node: uses the same Playwright/Chromium context as computed styles; ephemeral spawn pages mutate in place by default
- Simple inline SVG (rect + text bar charts) still converts natively without rasterization
Not supported in v0.1.x:
- External stylesheets on the inline path (use computed or inline all styles)
- Web fonts, CSS grid/float layout, forms,
<pre>polish,<dl>, tablerowspan - Header/footer first/even page variants; guaranteed multi-page layout fidelity
- Complex SVG (paths, gradients,
<use>) unlessrasterizeInPlaceis used on a live rendered page
See AGENTS.md for HTML authoring tiers and API.md for full options.
API
convertHtmlToDocx(html, options?)
Returns Promise<Buffer> (Node) with a valid .docx file.
import { convertHtmlToDocx, type ConvertOptions } from "dom-docx";
const docx = await convertHtmlToDocx(html, {
pageSize: "a4",
orientation: "landscape",
margins: { top: 0.75, bottom: 0.75 }, // inches; omitted sides default to 1
defaultFont: { family: "Georgia", sizePt: 11 },
metadata: { title: "Q3 Report", creator: "Finance" },
headerHtml: "<p style='font-size:12px;color:#666'>Confidential</p>",
footerHtml: "<p style='font-size:12px'>© 2026 ACME</p>",
pageNumber: true,
lang: "en-US",
direction: "ltr",
coverHtml: "<h1 style='text-align:center'>Quarterly Review</h1>", // page 1, before the TOC
tocHtml: "<ol><li><a href='#intro'>Introduction</a></li></ol>", // your TOC; links jump to id="intro"
});
Options
| Option | Default | Description |
|---|---|---|
styleSource | "inline" | "inline" parses style="" only (pure JS, fast). "computed" uses getComputedStyle. On Node this requires Playwright + Chromium; in the browser bundle it reads from the live DOM (no Playwright). |
browser / page | — | Node only. Reuse an open Playwright browser or page (computed styles and/or rasterizeInPlace). Not used by dom-docx/browser. |
rootSelector | — | Node only. CSS selector for the export root when converting element.innerHTML from a live Playwright page. Must match the node whose HTML you pass. |
rasterizeInPlace | — | Rasterize <canvas> / chart <svg> to PNG <img> before conversion. Recommended: { scale: 2 } for chart exports. Node: Playwright page; browser: requires root. See API.md. |
imageResolver | — | Hook to fetch non-data: <img src> (library never fetches on its own). |
pageSize | "letter" | "letter", "a4" or { width, height } in inches. |
orientation | "portrait" | "landscape" swaps dimensions. |
margins | 1 inch each | Per-side overrides in inches. |
defaultFont | Arial 10.5pt | { family, sizePt } for body text without explicit CSS. |
metadata | — | title, subject, creator, keywords[], description → docProps/core.xml. |
headerHtml / footerHtml | — | HTML fragments for page header/footer. |
pageNumber | false | Appends centered Page N field to footer. |
lang / direction | — | Spell-check locale; "rtl" for right-to-left. |
coverHtml | — | HTML fragment rendered as a cover page — the first content, before the TOC, followed by an automatic page break. Inline styles + data: images (e.g. a logo). Header/footer/page number are suppressed on the cover page. |
tocHtml | — | HTML fragment rendered as a table-of-contents “slot” — placed after the cover, before the body. You control the markup/styling (numbered, boxed, columns…); in-page links (<a href="#id">) jump to the matching id in the body. Add a trailing <div style="break-after:page"></div> to put it on its own page. |
Images
Only data: URLs embed automatically. For http(s): or file paths, supply a resolver. You control fetch policy and security:
const docx = await convertHtmlToDocx(html, {
imageResolver: async (src) => {
const res = await fetch(src); // your allowlist / SSRF checks
if (!res.ok) return null;
return { data: new Uint8Array(await res.arrayBuffer()), type: "png" };
},
});
Browser bundle
For client-side conversion (returns a Blob). No Playwright. The bundle runs entirely in the user’s browser.
import { convertHtmlToDocx } from "dom-docx/browser";
// Inline styles: pass HTML string directly
const blob = await convertHtmlToDocx(htmlFragment, { styleSource: "inline" });
// Computed styles: render the fragment in the live DOM first, then convert
document.body.innerHTML = htmlFragment;
const blob2 = await convertHtmlToDocx(htmlFragment, { styleSource: "computed" });
// SPA export: pass the live root so computed paths match element.innerHTML
const root = document.querySelector(".page-body")!;
const blob3 = await convertHtmlToDocx(root.innerHTML, {
styleSource: "computed",
root,
});
// Charts (Highcharts, canvas): rasterize to PNG <img> on a clone, then convert
const blob4 = await convertHtmlToDocx(root.innerHTML, {
styleSource: "computed",
root,
rasterizeInPlace: { scale: 2 }, // recommended for charts; default scale is 1
});
Browser-only options: root, document, rasterizeInPlace. Build: npm run build:browser → dist/browser/dom-docx.browser.js.
For advanced usage (buildDocxBuffer, custom StyleResolver, engine architecture), see API.md.
What converts well
Optimized for Word-friendly semantic HTML: headings, paragraphs, lists, data tables, inline formatting, shaded callouts, simple flex rows.
| Excellent | Good | Avoid (or use rasterizeInPlace) |
|---|---|---|
| Headings, lists, simple tables | Shaded banners, flex (≤4 items) | Live chart libraries (Highcharts, canvas) unless rasterized |
Inline strong / em / links | Table row/cell backgrounds | Complex SVG paths/gradients unless rasterized |
Explicit page breaks (break-before: page, break-after: page) | CSS grid, floats, absolute layout | |
| Short span highlights | Blockquotes, <hr> | External stylesheets (inline path) |
| Simple SVG bars (rect + text) | data: images | Forms, web fonts, CSS grid/float layout |
Full authoring guide for agents: AGENTS.md.
How it was built
dom-docx maps a practical HTML subset to native OOXML through a three-stage pipeline:
- Style resolution: inline
style=""(default) or browser computed styles - Visitor: Cheerio walk →
docxparagraphs, tables, numbering, hyperlinks - Pack + patch: generate OOXML, patch list numbering and shaded-block alignment for LibreOffice/Word PDF export
Quality is driven by an autonomous loop rather than one-off visual checks:
- 30+ regression cases (defined in
tools/generator.ts, run vianpm run score:suite): human-validated layout fidelity (ink-projection structure comparison, 85.6% concordance with blind human quality ratings), plus guards for bad contrast, missing list markers, wrong text and imbalanced shaded blocks; raw pixel match is recorded as a regression tripwire - Engine score: 50% visual (layout-based) + 35% editability (native structure, not 1×1 layout tables) + 15% compile speed
- OSS benchmark: same harness scores html-to-docx and @turbodocx/html-to-docx for ongoing comparison (BENCHMARK.md)
The default inline path is pure JavaScript (docx + cheerio + fflate) with no browser required. Playwright is Node-only: for styleSource: "computed" on the server and for the dev harness. The dom-docx/browser bundle never uses Playwright; computed styles come from the live page’s getComputedStyle.
Full scoring formulas, subscores, calibration and the agent iteration workflow: SCORING.md.
Development
For contributors and harness runs (not required to use the library):
git clone … && npm install && npm run setup
npm run build # dist/ for npm pack
npm run typecheck
npm run score:suite # full visual + XML regression suite (cases: tools/generator.ts)
npm run score:suite:priority # fast subset of the same cases
# Run one case: SUITE_ONLY=rasterize-in-place-chart npm run score:suite
npm run score:benchmark # vs html-to-docx + TurboDocx
npm run guard:config # ConvertOptions OOXML checks
Prerequisites for the harness: LibreOffice (soffice) for PDF rasterization, Playwright Chromium (npm run setup).
Documentation
| Doc | Contents |
|---|---|
| API.md | Full API reference, engine architecture, usage patterns |
| AGENTS.md | HTML authoring guide for AI agents |
| SCORING.md | Validation methodology and engine score |
| TEST-SCORES.md | Latest suite metrics and per-case scores |
| BENCHMARK.md | Comparison vs OSS html-to-docx libraries |
| examples/ | Sample HTML, DOCX output and side-by-side previews |
| SHOWCASE.md | How to run and extend showcase examples |
| CONTRIBUTING.md | Library vs harness layout, dev setup |
License
Similar Articles
Office-open-xml-viewer: Office XML document viewer that renders to HTML Canvas
A browser-based viewer for Office Open XML documents (DOCX, XLSX, PPTX) that renders to HTML Canvas, built with Rust/WASM parsers and TypeScript renderers, and generated by Anthropic's Claude AI.
Show HN: Docx-CLI: agents read/edit Word docs using 1/2 the time and tokens
docx-cli is a CLI tool that enables AI agents to read and edit Word documents efficiently by using stable locators and mutating XML in place, reducing token usage and time by half compared to default methods.
@QingQ77: 一个 Open WebUI 工具,用 python-docx 把模型产出的 Markdown(含 YAML 头)或 JSON 规范直接生成为原生、可编辑的 Word(.docx)文档。 https://github.com/ianuste…
一个 Open WebUI 工具,使用 python-docx 将模型产出的 Markdown(含 YAML 头)或 JSON 规范直接生成为原生、可编辑的 Word (.docx) 文档,支持多种模板和样式。
Show HN: Writemark, a dependency free web component for inline Markdown editing
Writemark is a dependency-free web component for inline Markdown editing that renders as you type, supports source/split/preview modes, slash commands, tables, task lists, code blocks, and native form integration, all without a framework or toolbar.
@MindfulReturn: I've decided that from now on, my output won't be docx, PPT, pdf, or md—it will be HTML. With this html-anything, I can turn all the above into HTML. Sharing only takes a few hundred KB, whereas sharing the same content as docx before was at least 2MB.
This tool allows users to convert documents (docx, PPT, pdf, md) into HTML format, and edit them using an AI agent CLI. It supports multiple output formats, is local-first, and open source.