About 10 years ago I fell in love with Vue.js. The easy-to-learn syntax and reactivity were a joy to work with, and HMR (hot module replacement) added up to an incredible developer experience (DX).
At this point I've spent thousands of hours working with front-end JavaScript, and I'm still in love with it, but I also have to do a lot of backend work.
As joyful as the modern frontend DX is (shoutout Vite), working on the backend feels cumbersome by comparison. One of the tasks I like least is backend generation of Word documents using templates and server-side libraries. The iteration loop is slow, the errors are cryptic and debugging is painful.
I'd much rather build documents in HTML. But the existing HTML-to-docx libraries leave a lot to be desired and I never had the motivation to build something better, until now. Enter Karpathy Autoresearch loops.
Autoresearch loops are a general concept: use an agent to kick off a recursive self-improvement loop against something that can be objectively scored. For example, say you have a slow SQL query (speed being the metric). You could kick off a loop with a prompt like:
You are a SQL performance researcher. Run the following SQL query to establish a baseline, then come up with hypotheses to improve performance. Score each result and run 5 iterations. Avoid any regressions, each result must contain the exact same rows and columns.
This isn't exactly brute forcing, but it's a similar concept of autonomously run iterations trying to improve within a constrained task.
After having success with Autoresearch loops on a couple of other things, the idea hit me. Can I brute-force my way to a higher-fidelity HTML-to-DOCX converter?
I sat on the idea for a few days, then just decided to try it. Enter my first open source package, dom-docx.
How the loop actually worked
The scoring loop needed a way to judge "is this a good Word document" that a machine could evaluate without a human eyeballing every output. The approach:
- Render a batch of HTML test cases (headings, lists, tables, flex rows, images, inline formatting, real patterns, not toy examples)
- Convert each to
.docx - Rasterize the result via LibreOffice and compare layout structure against a fidelity metric comparing the rendered HTML vs the converted docx file
- Score each case: 50% visual/layout fidelity, 35% editability (real Word structure, not a 1Γ1 layout table pretending to be a document), 15% compile speed
- Feed regressions back in, adjust the conversion logic, repeat
No single clever trick made this work. It was mostly refusing to accept "good enough" and letting the loop grind down edge cases one at a time: nested lists, table backgrounds, flex layouts, blockquotes, the stuff that actually breaks HTML-to-docx converters in practice.
The results
I benchmarked dom-docx against a couple of the established OSS HTML-to-docx converters on npm, solid, widely-used pure-JS libraries with no headless browser requirement, using the same scoring harness across 37 real-world test cases.
A few things stood out once the numbers came in:
- dom-docx produced schema-valid OOXML on all 37 cases in this harness, which held up as the clearest gap.
- Average layout-fidelity score in the mid-90s%, versus the mid-60s% for the alternatives tested.
- The biggest gaps show up exactly where you'd expect: nested lists inside blockquotes, table row backgrounds, flex layouts, inline SVG. Simple stuff (a plain paragraph, a centered paragraph) is roughly a wash across the board, which makes sense since that's the easy 20% every converter gets right.
- dom-docx trades some raw compile speed for it (~50ms avg vs the high-teens ms range), worth knowing if you're converting at very high volume, though for most document-generation use cases that's not the bottleneck.
Full per-case breakdown, named comparisons and methodology are public if you want the receipts: BENCHMARK.md and SCORING.md.
Try it
npm install dom-docx
Requires Node β₯ 20. The default path is pure JS, no browser or Playwright needed.
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);
Browser (runs entirely client-side, no Playwright):
import { convertHtmlToDocx } from "dom-docx/browser";
const blob = await convertHtmlToDocx(html);
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "output.docx";
a.click();
CLI, no code required:
npx dom-docx input.html -o output.docx
cat fragment.html | npx dom-docx - -o - # stdin to binary stdout, for pipelines
There's also a live demo at dom-docx.com if you want to test HTML with it in the browser first.
Where this is headed
A growing share of the HTML hitting a converter like this is LLM-generated: agents building reports, RAG pipelines writing summaries and shipping them as Word docs. I added an AGENTS.md that documents which HTML patterns convert cleanly and which to avoid, so an agent authoring the HTML can get it right the first time instead of producing DOCX-breaking markup and finding out after the fact.
Repo's here: github.com/floodtide/dom-docx. MIT licensed, stars, issues and PRs welcome. I'd genuinely like to hear where it breaks for you.
Top comments (0)