Rendering to HTML
Render DOCX to reflowing semantic HTML with lit-docx-html, replace the rendering of any node, and get an outline and the bold or underlined passages.
@quirekit/lit-docx-html renders DOCX as HTML that reflows to the screen width. It suits Word-authored content shown
on websites or in apps, such as user agreements, privacy policies and announcements. It does not paginate or
reproduce page size, fonts or paragraph spacing; the Preview of the Quick start example is rendered by it.
pnpm add @quirekit/lit-docx-html
Convert to HTML
docxToHtml runs in browsers and in Node. Converting at build time lets pages ship static HTML without loading
the parser in the browser:
import { readFile } from "node:fs/promises";
import { docxToHtml } from "@quirekit/lit-docx-html";
const { html, outline, emphasis } = await docxToHtml(new Uint8Array(await readFile("agreement.docx")));
html: the body as an HTML fragment. Text and attributes are escaped; links keep onlyhttp,https,mailtoand in-page anchors, and images keep only embedded raster data.outline: headings and clauses with anchor, level, number text (such as1.1) and text, for a table of contents.emphasis: bold or underlined passages with the anchor of their clause. Agreements usually mark terms that need special attention this way, so they can be listed together.title: the document title, taken from the first centered line before any clause or the first heading.
Clause numbers
Paragraphs numbered by Word get their number text when rendered. Many agreements type the numbers instead, such as
第三条, 一、, (一), 4.1, (1) or ①, or Section 2; these are recognized as clauses too, enter the outline
and get anchors, while the text stays as written.
- Levels follow the order in which each numbering style first appears: when
一、comes before第一条, articles nest under sections, and the other way round. Word numbering and typed numbers are ranked together. - A style used only once in the document (such as
1.5 times spacing) is not a clause, and paragraphs inside tables never are. outlineDepthsets how many levels the outline lists, 2 by default;typedClauses: falseturns typed numbers off.
Bold or underlined text counts toward emphasis only when at least 8 letters remain after removing the clause number
and {{…}} placeholders, so bold clause numbers and underlined blanks do not enter the key terms.
Custom rendering
renderers replaces the default rendering by node type and markRenderers by mark type. A renderer returns a tree
of { tag, attrs, children } and strings; null omits the node and undefined uses the default:
const { html } = await docxToHtml(bytes, {
renderers: {
// Numbered clauses become sections with a number badge
paragraph: (node, context) => context.number
? { tag: "section", attrs: { class: "clause", id: context.anchor },
children: [{ tag: "b", children: [context.number] }, ...context.children()] }
: undefined,
// Hide tables
table: () => null,
},
markRenderers: {
// Mark all bold text as key terms
bold: (_mark, content) => ({ tag: "mark", attrs: { class: "key-term" }, children: content }),
},
});
The context provides children() (rendered children), fallback() (the default rendering), number, anchor and the
ancestors. See the LitAST node model for node types and attributes. Parsed LitAST can go straight
to renderLitDocx and be serialized with toHtml; parse with numberingMode: "flat-numbered-paragraphs" so number
text follows Word's rules.
In Vue
<script setup lang="ts">
import { ref } from "vue";
import { LitDocxView } from "@quirekit/lit-docx-html/vue";
defineProps<{ bytes: Uint8Array }>();
const agreed = ref(false);
</script>
<template>
<LitDocxView :source="bytes" @rendered="result => console.log(result.outline)" @read-end="agreed = true" />
</template>
source accepts DOCX bytes or LitAST, and options takes the custom renderers above. read-end fires the first time
the end of the document enters the viewport, for “agree after reading” checkboxes.
Theme and static pages
@quirekit/lit-docx-html/styles.css is the default theme. It applies to the .lit-docx container, exposes colors and
fonts as CSS variables (such as --lit-docx-accent and --lit-docx-font), follows the system dark mode and can be
fixed with data-theme="light" or "dark". The root element of LitDocxView is .lit-docx.
At build time a command generates a complete page:
npx lit-docx-html agreement.docx -o agreement.html --toc --key-terms --outline agreement.json
--toc adds the table of contents, --key-terms the key terms, --fragment outputs only the body, --lang en switches
the interface text, and --outline saves the outline and key terms as JSON. In code, renderHtmlPage, renderToc and
renderKeyTerms compose the same page.
Compare two versions
After an agreement changes, users usually need to know what changed. compareDocx compares paragraphs, headings and
table rows of two versions and returns additions, removals and modifications with their clause:
import { compareDocx } from "@quirekit/lit-docx-html";
const changes = await compareDocx(oldBytes, newBytes);
// [{ kind: "added", clause: { number: "Section 2", text: "Personal data" }, after: "…" },
// { kind: "modified", clause: { number: "Section 3", text: "Services" }, before: "…billed yearly.", after: "…billed monthly." }]
Typed clause numbers are ignored when matching, so inserting a clause does not mark every renumbered clause after it
as modified. Adjacent removed and added text whose similarity reaches similarity (0.4 by default) becomes one
modification. Removals carry the clause of the old version; other changes carry the clause of the new version.
License
@quirekit/lit-docx-html is a QuireKit commercial component under the same license terms as lit-docx.