floodtide/dom-docx: Convert semantic HTML fragments to native, editable Word documents (OOXML) · GitHub

🔥 Discover this must-read post from Hacker News 📖

📂 **Category**:

💡 **What You’ll Learn**:

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.

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 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.

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.

import 💬 from "dom-docx/browser";

const html = `

Revenue grew 12% year over year.

  • North America
  • EMEA
  • `; 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.

    import { writeFile } from "node:fs/promises";
    import { convertHtmlToDocx } from "dom-docx";
    
    const html = `
    
    

    Revenue grew 12% year over year.

  • North America
  • EMEA
  • `; const docx = await convertHtmlToDocx(html); await writeFile("output.docx", docx);

    Pass a body fragment only (no / / required). Defaults: US Letter, 1″ margins, Arial 10.5pt body text.

    Supported (default styleSource: "inline"):

    • Headings, paragraphs, lists (/
        including list-style-type), tables, links, inline formatting
      1. Block backgrounds, blockquotes,

        , simple flex rows (≤4 items)

      2. data: images; remote images via your imageResolver
      3. Page size/orientation/margins, default font, metadata, header/footer HTML, page numbers, table of contents, lang/direction
      4. Low-complexity inline SVG (bars + text)
      5. CSS bar divs in table cells (background + height/width → native shaded bands)

    Advanced (optional styleSource: "computed"):

    • Resolves blocks and class/#id selectors via getComputedStyle
    • 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) or rootSelector (Node + live page) when converting element.innerHTML so 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 and complex (e.g. Highcharts) to PNG before conversion
    • Recommended for charts: rasterizeInPlace: { scale: 2 } — supersamples at 2× density for sharper images in Word (default scale: 1; max 4)
    • 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,
       polish, 
      , table rowspan
    • Header/footer first/even page variants; guaranteed multi-page layout fidelity
    • Complex SVG (paths, gradients, ) unless rasterizeInPlace is used on a live rendered page

    See AGENTS.md for HTML authoring tiers and API.md for full options.

    convertHtmlToDocx(html, options?)

    Returns Promise (Node) with a valid .docx file.

    Confidential”, footerHtml: “

    © 2026 ACME

    “, pageNumber: true, lang: “en-US”, direction: “ltr”, coverHtml: “”, // page 1, before the TOC tocHtml: “
    1. Introduction
    “, // your TOC; links jump to id=”intro” });”>
    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: "

    Confidential

    "
    , footerHtml: "

    © 2026 ACME

    "
    , pageNumber: true, lang: "en-US", direction: "ltr", coverHtml: "", // page 1, before the TOC tocHtml: "
    1. Introduction
    "
    , // your TOC; links jump to id="intro" });

    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 / chart to PNG before conversion. Recommended: { scale: 2 } for chart exports. Node: Playwright page; browser: requires root. See API.md.
    imageResolver Hook to fetch non-data: (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[], descriptiondocProps/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 () jump to the matching id in the body. Add a trailing to put it on its own page.

    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" };
      },
    });

    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  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:browserdist/browser/dom-docx.browser.js.

    For advanced usage (buildDocxBuffer, custom StyleResolver, engine architecture), see API.md.


    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,


    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.


    dom-docx maps a practical HTML subset to native OOXML through a three-stage pipeline:

    1. Style resolution: inline style="" (default) or browser computed styles
    2. Visitor: Cheerio walk → docx paragraphs, tables, numbering, hyperlinks
    3. 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 via npm 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.


    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).


    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

    MIT

    {💬|⚡|🔥} **What’s your take?**
    Share your thoughts in the comments below!

    #️⃣ **#floodtidedomdocx #Convert #semantic #HTML #fragments #native #editable #Word #documents #OOXML #GitHub**

    🕒 **Posted on**: 1783950521

    🌟 **Want more?** Click here for more info! 🌟

    By

    Leave a Reply

    Your email address will not be published. Required fields are marked *