@file-viewer/docx
v0.3.36
Published
Pure HTML DOCX renderer fork for browser-based Word previews.
Maintainers
Readme
@file-viewer/docx
Pure HTML DOCX renderer for browser-based Word previews. This package preserves the final non-canvas docx-viewer line as a stable maintenance track, converting WordprocessingML into HTML while keeping semantic structure, page layout, headers/footers, numbering, fields, tables, images and common DrawingML content as closely as browser layout allows.
This fork includes production hardening for large Word documents: optional Worker parsing with a main-thread fallback, asynchronous rendering yields, Word-saved pagination support, dynamic overflow pagination and layout telemetry.
Installation
npm install @file-viewer/docxFor the standalone browser build, ship these files together and keep the Worker URL same-origin:
dist/docx-preview.js
dist/docx-preview.worker.js
dist/jszip.min.jsBasic usage
<script src="dist/jszip.min.js"></script>
<script src="dist/docx-preview.js"></script>
<div id="container"></div>
<script>
const options = {
useWorker: true,
workerUrl: "dist/docx-preview.worker.js",
awaitLayout: true,
strictWordCompatibility: true,
ignoreLastRenderedPageBreak: false,
preserveComplexFieldResults: true,
updatePageReferences: false,
hideWebHiddenContent: false,
externalLinkPolicy: "allow",
externalResourcePolicy: "block",
progress: ev => console.log(ev.phase, ev.current, ev.total, ev.message)
};
docx.renderAsync(fileOrArrayBuffer, document.getElementById("container"), null, options)
.then(() => console.log("docx: finished"));
</script>ES module usage:
import { renderAsync } from "@file-viewer/docx";
await renderAsync(fileOrArrayBuffer, container, null, {
useWorker: true,
workerUrl: new URL("@file-viewer/docx/worker", import.meta.url).toString(),
awaitLayout: true
});Under a CSP with require-trusted-types-for 'script', create the Worker at the
application boundary and pass it through workerFactory:
const workerPolicy = trustedTypes.createPolicy("app-docx-worker", {
createScriptURL(value) {
const url = new URL(value, location.href);
if (url.origin !== location.origin)
throw new TypeError("DOCX Worker must be same-origin");
return url.href;
}
});
await renderAsync(fileOrArrayBuffer, container, null, {
useWorker: true,
workerFactory: () => new Worker(
workerPolicy.createScriptURL("/assets/docx-preview.worker.js")
)
});Copy dist/docx-preview.worker.js to that same-origin application asset path as
part of your build or deployment.
Allow the application-owned Worker policy and the renderer's three narrow HTML
policies in the trusted-types CSP directive:
trusted-types app-docx-worker file-viewer-docx-xml-parser file-viewer-docx-altchunk-parser file-viewer-docx-altchunk-srcdoc;
require-trusted-types-for 'script';The renderer policies brand only the exact value being sent to an inert XML/HTML
parser or the sanitized altChunk srcdoc; no default policy is installed.
Read-only review preview
Tracked changes remain disabled by default. Pass reviewMode to keep Word revision
markup in the DOM, show comments by default, and enable the four read-only views:
import {
collectDocxReviewChanges,
renderAsync,
renderDocument
} from "@file-viewer/docx";
const document = await renderAsync(file, container, null, {
reviewMode: "all" // "final" | "original" | "simple" | "all"
});
const changes = collectDocxReviewChanges(document);
await renderDocument(document, container, null, { reviewMode: "original" });renderChanges: true is retained as a compatibility alias for reviewMode: "all".
Set renderComments: false explicitly when tracked changes are required without
the comments surface.
The default reviewPresentation: "balloon" keeps deleted text as anchors for a
margin review rail. Hosts without a rail can pass reviewPresentation: "inline"
to show deletions with strikethrough using the engine's own styles.
Measured tabs, DrawingML/VML anchor origins, VML text viewports, merged-cell
edges and page backgrounds are handled by the engine. Drawing text boxes use
independent HTML stories so serialization preserves their paragraph boundaries;
exporters do not need to replace or repair generated elements. renderAsync()
updates coordinates after fonts, images, pagination and container resizing.
Call disposeRenderedDocument(container) when unmounting to release document
observers and bookmark handlers. Rendering again in the same container releases
the previous handlers automatically.
The mode values follow Word's Display for Review semantics: simple keeps
the current text with compact change indicators, all exposes every supported
revision and comment anchor, final is the current document with markup hidden,
and original restores the pre-revision text with markup hidden. A host that
wants Word-style margin balloons can use collectDocxReviewChanges() together
with the stable data-docx-change-* anchors; the renderer does not force that
optional review UI on normal document previews.
Paragraph-mark revisions are resolved when rendering a review mode: deleted
marks join paragraphs in final/simple, and inserted marks join paragraphs
in original. Content stays within its table cell, section, or positioned
frame. Native Word-created fixtures verify paragraph boundaries, tabs, soft
breaks, mixed bold runs, CJK replacements, and table cell text against Word's
Accept All and Reject All files. The browser gate repeats these checks in
the main thread and a real Worker while reusing the parsed document.
Use renderDocument() with the returned parsed document when switching a view
that can change paragraph boundaries; it does not parse or upload the file again.
The synchronous setDocxReviewMode() helper changes CSS visibility only. It does
not rebuild paragraph structure or pagination and is not a substitute for that
render step. File Viewer's Word settings perform the render step automatically.
collectDocxReviewChanges() returns logical Word review items rather than raw
OOXML wrappers. Adjacent/nested revision fragments from the same paragraph,
author, and edit session are coalesced; ids lists every source revision id.
The presentation field is inline for insert/move-to items and balloon for
delete/move-from/format/comment items. Formatting items include structured
formatChanges and a semantic description such as 无下划线.
API
renderAsync(
document: Blob | ArrayBuffer | Uint8Array,
bodyContainer: HTMLElement,
styleContainer?: HTMLElement,
options?: Partial<Options>
): Promise<WordDocument>
parseAsync(
document: Blob | ArrayBuffer | Uint8Array,
options?: Partial<Options>
): Promise<WordDocument>
parseAsyncInWorker(
document: Blob | ArrayBuffer | Uint8Array,
options?: Partial<Options>
): Promise<WordDocument>
renderDocument(
wordDocument: WordDocument,
options?: Partial<Options>
): Promise<Node[]>
awaitRenderedLayout(
container: HTMLElement,
options?: Partial<Options>
): Promise<LayoutSnapshot>
collectLayoutSnapshot(
container: HTMLElement,
options?: Partial<Options>
): LayoutSnapshotImportant options
{
className: "docx",
inWrapper: true,
hideWrapperOnPrint: false,
ignoreWidth: false,
ignoreHeight: false,
ignoreFonts: false,
breakPages: true,
// Appearance
darkMode: false,
// Word fidelity / pagination
ignoreLastRenderedPageBreak: false,
strictWordCompatibility: true,
paginationTolerance: 2,
maxDynamicPaginationPasses: 1000,
awaitLayout: true,
// Worker and responsiveness
useWorker: true,
workerUrl: "dist/docx-preview.worker.js",
workerFallback: true,
workerTimeout: 120000,
renderPageBatchSize: 2,
renderYieldEveryMs: 16,
progress: ev => void,
// Field and print-layout compatibility
preserveComplexFieldResults: true,
updatePageReferences: false,
hideWebHiddenContent: false,
// Content switches
renderHeaders: true,
renderFooters: true,
renderFootnotes: true,
renderEndnotes: true,
renderComments: false,
renderAltChunks: true,
reviewMode: undefined, // opt-in: "final" | "original" | "simple" | "all"
renderChanges: false,
experimental: false,
trimXmlDeclaration: true,
useBase64URL: false,
debug: false
}Large-document rendering path
The production path is intentionally split into stages:
- The main thread starts a Worker using
workerUrl. - The self-contained Worker uses its bundled JSZip and XML parser imports to unzip the package, parse XML parts, resolve relationships and serialize a compact document snapshot. The default needs no external script, global XML shim or separately appended bootstrap.
- The main thread restores the snapshot into
WordDocumentand renders HTML pages in batches, yielding torequestIdleCallback,requestAnimationFrameorsetTimeoutbetween page batches. awaitRenderedLayoutwaits for images and fonts, runs dynamic overflow pagination and returns aLayoutSnapshot.collectLayoutSnapshotcan be used in CI or telemetry to detect overflow pages, unresolved media, page count, aggregate text length, fields and floating objects.
An awaited layout includes font/image settlement and the resulting tab, drawing
and pagination measurements. It rejects if that render is replaced, its mounted
container is removed, or pagination cannot settle within
maxDynamicPaginationPasses. An AbortError identifies an obsolete or removed
render. With awaitLayout: false, rendering can return before these measurements
finish; call awaitRenderedLayout before measuring or exporting the result.
A detached renderDocument result may still be mounted and awaited later.
Batch rendering and browser yields avoid the former demo-page freeze caused by inserting very large DOM trees in one task. The supplied Worker includes its XML APIs. workerFallback: true remains useful when Worker startup is blocked by the host environment. If workerJsZipUrl is explicitly set, the Worker loads and uses that JSZip asset. The selection must be a non-empty same-origin HTTP(S) or Blob URL without credentials. Relative URLs resolve against the Worker script URL; a Blob Worker requires an absolute URL. Cross-origin URLs and opaque origins are rejected before import, even when a global JSZip implementation is already present. Omit this option to use the bundled dependency; an explicit invalid value does not select the default. A failed load or invalid JSZip export is reported as an error; it never silently substitutes the bundled dependency. workerFallback continues to control main-thread fallback; set it to false when any Worker or selected-dependency failure must reject the call.
Pagination, headers/footers and TOC
The renderer supports:
- explicit page breaks (
w:br w:type="page"); - Word-saved page break positions (
w:lastRenderedPageBreak), enabled by default; - paragraph
pageBreakBefore,keepNext,keepLinesandwidowControlhints; - section page size, orientation, columns, margins, headers and footers;
- table row splitting with
w:tblHeaderrepeat-header preservation; - post-render dynamic pagination for content that still overflows after structural page splitting;
- layout-time updates for truly page-local fields such as
PAGE,NUMPAGES,SECTIONPAGESandSECTION; - preservation of Word's stored complex-field results for
TOC,PAGEREF,REF,SEQ,IF,MERGEFIELDand related fields by default, so a Word-authored table of contents keeps its cached page numbers and tab leaders instead of being recalculated incorrectly by the browser.
For Word-authored documents, keep ignoreLastRenderedPageBreak: false. Those markers preserve the positions calculated by the Word-compatible editor that last saved the document and are especially important for long tables and large Chinese technical documents. Keep preserveComplexFieldResults: true unless you intentionally want to recompute cross-reference fields; the default matches Word print-layout behavior for existing TOC/PAGEREF results.
w:webHidden is not hidden by default in this renderer because it only applies to Word's Web Layout view. In Print Layout, Word still displays the TOC tab before the page number and the cached PAGEREF result even when those runs are marked w:webHidden. Set hideWebHiddenContent: true only for an explicit Web Layout style preview.
External hyperlinks remain active by default, but only HTTP(S), mailto:, tel:
and safe relative targets receive an href. Script-capable, local-file, blob,
protocol-relative and unknown schemes are rejected in every mode. Set
externalLinkPolicy: "block" to omit safe external href values while preserving
their text and sanitized target metadata. Document-internal bookmark links remain
active in either mode.
External image relationships are blocked by default. Set externalResourcePolicy: "allow" only when the document is trusted to load HTTP(S) image resources. Package-embedded images and local data:image/... or blob: image URLs remain available without enabling network access; unknown and script-capable protocols are rejected in either mode.
renderAltChunks remains compatible with existing integrations. Embedded HTML is
parsed inertly and stripped of scripts, active elements, event handlers, CSS and
URL-bearing attributes before it reaches a maximally restricted sandboxed iframe.
Set darkMode: true to reproduce Word Online's "Switch Modes" dark appearance. Every color the renderer emits — text, backgrounds/highlights, borders, underlines, theme colors and DrawingML/VML shape fills and strokes — is converted with the same lightness-inversion algorithm Microsoft uses for this exact purpose (see roosterjs's getDarkColor): the color is converted to CIE Lab, only the lightness (L) channel is inverted around a fixed base value (the L of #333333), and hue/chroma (a/b) are left untouched. That guarantees every color stays legible on a dark page — pure black becomes pure white, pure white becomes #333333, and an arbitrary color such as rgb(99, 36, 35) keeps its reddish hue while becoming light enough to read — without the hue-flipping side effects of a naive per-channel RGB invert. The toggle is purely a rendering-time CSS transform — it does not change the parsed document model — and every transformed color is wrapped in a CSS variable so that @media print can restore the exact original light-mode value; print output always renders with Word's normal light appearance regardless of darkMode.
Header/footer selection follows the WordprocessingML print-layout rules: first references apply to the first page of a section with w:titlePg, and even references apply only when settings contain w:evenAndOddHeaders. Each omitted reference inherits its own variant from the previous section. If a selected variant is absent in the first section, its header/footer is blank. Dynamic continuation pages select their own first/even/default variant instead of repeating a cloned first-page header/footer.
Adjacent saved page markers do not create extra blank pages, including a saved marker immediately after an authored page break. Repeated authored w:br w:type="page" breaks still preserve an intentional blank page.
Footnotes begin on the page containing their first visible reference. Long note text continues in order across pages, preserving authored text, formatting and embedded resources. Endnotes follow the final body content. Custom note marks and section/page restart settings are kept separate from generated numbering. An indivisible note object that cannot fit the printable page is retained and reported as an unresolved layout error instead of being silently clipped.
Complex-script formatting is kept script-aware. w:iCs and w:bCs now affect RTL/complex-script spans, but they are not applied to East Asian TOC text; this avoids turning Chinese TOC level 3 entries italic when the DOCX only specified complex-script italics.
Regression fixture
tests/regression/database-design.docx is a large Chinese database-design document used to validate production behavior. The fixture has a large word/document.xml, thousands of paragraphs, many tables, TOC/PAGEREF fields, saved page breaks and multiple header/footer references.
Open this page in a local server to run the browser regression:
tests/regression/database-design-render.htmlThe Node regression can be run with:
node tests/regression/check-database-design.cjsIt renders tests/regression/database-design.docx and asserts that TOC entries keep Word's cached page numbers such as 引言 5, 各系统与数据库对应关系 6, 表间关系 16 and 数据恢复策略 103 instead of collapsing to page 1. It also verifies that w:tab right-tab leaders are emitted as measurable docx-tab-stop elements with dotted leader styling, so TOC and figure-list page-number dot leaders remain visible. The regression also checks print-layout TOC hyperlink styling: TOC hyperlinks keep their anchors, but nested Hyperlink character-style runs are forced to inherit the paragraph color and text decoration, matching Word's black print-layout TOC rather than a browser-blue link. A successful browser run updates the fixed status banner with page count, overflow pages, unresolved media and text length.
Build
All JavaScript is generated by Rollup from the TypeScript source entry points. The build emits readable and minified UMD/CommonJS-compatible and ESM bundles, a self-contained Worker, and declarations generated by the local TypeScript compiler. Existing output is cleaned first, and npm pack rebuilds it automatically:
npm run buildInstall the lockfile dependencies with npm ci. No global compiler or NODE_PATH is required. dist/jszip.min.js is copied unchanged from the declared JSZip dependency for standalone main-thread use; bundled dependency licenses are retained in dist/THIRD_PARTY_NOTICES.txt.
Run the complete build, semantic regressions and package contract checks with:
npm run verifyRun npm run test:worker for the isolated Node VM protocol checks. This is not a browser test. Run npm run e2e:worker (Chromium) and npm run e2e:worker -- webkit to verify the generated Worker, main-thread parity, genuine selected JSZip fetch/hash, and dependency/XML failure controls in real browsers. Set DOCX_WORKER_JSZIP_PATH to another installed JSZip browser asset when checking a consumer-resolved version. These checks never append parser code to the Worker.
Browser-focused checks remain available through npm run e2e, npm run e2e:chart and npm run e2e:header-guard.
These commands run the existing Jasmine specs in the pinned Playwright browser,
served only on loopback. Install Chromium and Firefox with
npx playwright install chromium firefox before running them; e2e:security
runs both browsers with the strict CSP and Trusted Types fixture. Pass
-- --browsers chromium,webkit to select other installed engines.
npm run e2e-watch reruns the same specs after changes to tests or dist.
The runner uses native file enumeration and watching instead of the legacy
Karma glob dependencies, which included the unpatched braces recursion flaw.
Local demo
Serve the repository over HTTP and open the root page:
python3 -m http.server 8000The demo is self-contained for normal DOCX files, includes sample documents, page navigation, light/dark rendering and an options panel. TIFF support is loaded only when a document actually contains TIFF media.
Optional display targets
Pass exposeDisplayTargets: true to renderAsync (or to both parseAsync
and renderDocument when used separately) to expose paragraph identities as
data-office-target attributes. The value is the OOXML part path followed by
#p:<w14:paraId>, or #xpath:<structural-path> when no paragraph ID is stored.
Repeated headers and split paragraph fragments may share an identity.
This option is disabled by default and works with the matching Worker bundle. It adds read-only identity metadata, not an editing or document write-back API. An external display adapter may use it to locate rendered text. Reloading or repaginating the document can replace DOM nodes; adapters must reapply their own cached display state. Identities are scoped to the source package and are not guaranteed across structural edits to the original file.
Notes on fidelity
The renderer follows OOXML semantics and uses browser-native layout. It is designed for production preview and regression testing, not for claiming byte-for-byte or pixel-perfect identity with Microsoft Word's private layout engine. Use LayoutSnapshot and the supplied regression fixture to monitor the remaining differences that depend on browser fonts, font fallback and shaping engines.
Numbering suffix and heading/list numbering
OOXML separates numbering text from the suffix after the numbering symbol. w:lvlText contains the numbering pattern, w:numFmt defines the format used to expand %1, %2, etc., and w:suff defines the content inserted between the generated number and paragraph text. When w:suff is omitted, OOXML treats it as tab. The renderer emits this suffix as a valid CSS unicode separator token, not as escaped visible text, so headings/lists no longer show literal \9 or \a0 after the numbering.
Run the focused regression with:
node tests/regression/check-numbering-suffix.cjsThe large database-design regression uses the cached rendered snapshot by default to avoid spending minutes rebuilding the full document in constrained CI. Set DOCXJS_RERENDER_REGRESSION=1 when you need to regenerate it from the DOCX.
Maintainer releases
publish.yml checks out the exact stable release tag, builds the package, runs
semantic and native Word browser regressions, then publishes with npm provenance.
The npm trusted publisher must bind @file-viewer/docx to
flyfish-dev/docxjs and publish.yml; no long-lived npm token is required.
The workflow alone does not create that npm permission.
Manual dispatch defaults to a dry run and requires an existing release tag. Retries verify the packed SHA-512 against the registry before skipping an existing version. Different bytes or an uncertain write stop the run rather than changing the version or repeating an authentication prompt.
