Brute forcing my way to better HTML → DOCX conversion

By Blair Googer · The dom-docx origin story · July 2026 · also on dev.to

Ten years ago Vue.js hooked me on front-end JavaScript. The reactivity, the hot reload and the whole developer experience. Thousands of hours later I'm still in love. But much of my work is backend, and one task I always dread is updating Word document templates and the related generation code. The feedback loop is slow and debugging is painful. I would much rather build documents the way I build web pages. Unfortunately, the existing HTML-to-DOCX libraries leave a lot to be desired, and I never had the motivation to build something better.

Then I started having success with Karpathy Autoresearch loops on a couple of other things, and the idea hit me. Can I brute-force my way to a higher-fidelity HTML-to-DOCX converter?

Autoresearch loops are a general concept. You 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:

We are using autoresearch loops to improve SQL performance. 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 autonomous iterations trying to improve within a constrained task.

I sat on the idea for a few days, then just decided to try it. The result is my first open source package, dom-docx.

How the dom-docx 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:

  1. Render a batch of HTML test cases (headings, lists, tables, flex rows, images, inline formatting, style sheets, etc.) in Playwright and take a screenshot
  2. Convert each to .docx and take another screenshot via LibreOffice
  3. Compare the Playwright rendered screenshot against the screenshot of the dom-docx output
  4. Score each case as 50% visual/layout fidelity, 35% editability (real Word structure, not a 1×1 layout table pretending to be a document), 15% compile speed
  5. Feed results back in, adjust the conversion logic, repeat
Browser HTML A table with a dark header row, a shaded subtotal row and a teal delta value, rendered in a browser
Converted DOCX The same table after conversion to DOCX, visually near-identical
One frame from the loop, exactly as the scorer sees it. This is the table-row-backgrounds case, a dark header row, a shaded subtotal row and a colored delta cell all surviving the trip into Word format at 98.7 percent layout fidelity. The same case shows up in the chart below, where the incumbent libraries score 29.8 and 72.2.

Part of what I love about autoresearch is that it's very accessible machine learning. There's no training run and no GPUs, just a metric, a loop and some monitoring. Two classic ML failure modes still apply, though. One is the asymptote. Early iterations buy quick gains, then scores flatten and the juice stops being worth the squeeze. The other is overfitting. Optimize against a fixed suite long enough and you risk building a converter that's great at 44 test cases and nothing else.

The guard against overfitting was a wild corpus of 11 pages pulled from the live web that the converter was never tuned on (a news article, a Craigslist page, marketing email templates, a Project Gutenberg novel, a Hacker News thread, an RFC and Wikipedia tables), all scored with the same harness. After further iterations, most of those scored in the 90s.

The results

I benchmarked dom-docx against the established OSS HTML-to-DOCX converters on npm: html-to-docx (~337k weekly downloads) and its actively maintained fork @turbodocx/html-to-docx, both solid, widely used pure-JS libraries with no headless browser requirement, using the same scoring harness across 40+ real-world test cases.

A few things stood out once the numbers came in:

  • dom-docx produced schema-valid OOXML on every case in this harness, which held up as the clearest gap.
  • Average layout-fidelity score in the mid-90s percent, 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.

Here are five example cases from the scoring suite:

dom-docx html-to-docx @turbodocx/html-to-docx
inline-svg-chart
dom-docx97.0
html-to-docx7.2
@turbodocx/html-to-docx9.8
nested-blockquotes-lists
dom-docx91.5
html-to-docx12.1
@turbodocx/html-to-docx11.5
table-row-backgrounds
dom-docx98.7
html-to-docx29.8
@turbodocx/html-to-docx72.2
table-cell-padding
dom-docx97.2
html-to-docx62.8
@turbodocx/html-to-docx61.6
plain-paragraph
dom-docx96.8
html-to-docx97.0
@turbodocx/html-to-docx97.0
Layout-fidelity score per test case (0–100, higher is better), 5 of the 44 suite cases. plain-paragraph is included deliberately as one of the three cases where the incumbents edge dom-docx, all by less than a fifth of a point.
View this data as a table
Test case dom-docx html-to-docx @turbodocx
inline-svg-chart 97.03 7.21 9.83
nested-blockquotes-lists 91.48 12.11 11.51
table-row-backgrounds 98.73 29.81 72.19
table-cell-padding 97.15 62.83 61.61
plain-paragraph 96.84 96.98 96.99

The full per-case breakdown, named comparisons and methodology are public: BENCHMARK.md and SCORING.md.

Try it

Terminal
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):

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

CLI
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 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, retrieve-augment-generate 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.

The project lives at github.com/floodtide/dom-docx. MIT licensed. Stars, issues and PRs welcome. I'd genuinely like to hear where it breaks for you.

Keep going