npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@devmm/puredocs-word

v1.0.0

Published

TypeScript library for reading and writing Word (.docx) files — zero deps except fflate

Readme

@devmm/puredocs-word

TypeScript library for reading, writing and editing Word (.docx) files. Zero runtime dependencies except fflate. Works in Node.js, browser, Deno, and Bun.

License: MIT


Packages

| Package | Description | |---------|-------------| | @devmm/puredocs-word | Core: read/write/edit .docx — model, lossless round-trip, styles, tables, images, lists, find & replace, comments, track changes, fields, protection | | @devmm/puredocs-word-mail-merge | Template engine: {{field}} and MERGEFIELD binding (lossless, cross-run) | | @devmm/puredocs-word-html-import | HTML/Markdown → .docx and .docx → HTML/Markdown | | @devmm/puredocs-word-angular | Angular 17+ WordService + provideWord() | | @devmm/puredocs-word-react | React 18+ hooks: useDocument(), useTemplate() |


Installation

npm install @devmm/puredocs-word

Architecture

The library is built on three layers you can drop into at any level:

  1. XML DOM (parseXml / serializeXml) — a faithful, round-trip XML parser.
  2. OPC package (OoxmlPackage) — holds every part of a .docx; edits to one part leave every other part byte-identical on save (lossless round-trip).
  3. Rich model (Document, Paragraph, Run, DocTable, …) — an ergonomic builder/reader for authoring and inspecting content.

Lossless editing rule of thumb: to open a real Word file, tweak it, and keep everything else exactly as it was, edit at the package/XML layer (OoxmlPackage). The rich-model writer (Document.saveAsBuffer) regenerates the document from scratch and is best for authoring new documents.


Quick Start

Create a document

import { Document, WordColor, WordBorderStyle } from '@devmm/puredocs-word';

const doc = Document.create();

doc.addHeading('Sales Report Q2', 1);

doc.addParagraph(p => p
  .setAlignment('justify')
  .addRun('Revenue increased by ')
  .addRun('42%', r => r.setBold(true).setColor(WordColor.green))
  .addRun(' compared to last quarter.'));

doc.addList(['APAC up 15%', 'EMEA up 8%', 'Americas up 19%'], 'bullet');

doc.addTable(t => {
  t.setColumnWidths([2000, 3000, 2000]);
  t.setAllBorders(WordBorderStyle.Single);
  t.addHeaderRow(['Product', 'Price', 'Stock']);
  t.addRow(r => { r.addTextCell('Laptop Pro'); r.addTextCell('$1,499'); r.addTextCell('42'); });
});

doc.setHeader(h => { h.setAlignment('right'); h.addRun('devmm Company — '); h.addPageNumber(); });
doc.setFooter(f => { f.setAlignment('center'); f.addRun('Confidential'); });

const buffer = doc.saveAsBuffer();       // Uint8Array — browser + Node
await doc.saveToFile('./report.docx');   // Node.js only

Read an existing file

The reader parses the full document into the model — paragraphs with formatting, tables (incl. merged cells), hyperlinks, inline images, and section/page setup.

import { Document, Paragraph } from '@devmm/puredocs-word';

const doc = await Document.fromFile('./existing.docx');            // Node
// const doc = Document.fromBuffer(await file.arrayBuffer());      // browser

console.log(doc.getText());                       // full plain text
for (const p of doc.getParagraphs()) {            // deep (descends into tables)
  console.log(p.style.styleName, '→', p.getText());
}

Find & Replace (across runs)

Matches straddling multiple runs (as Word emits) are handled correctly.

const doc = await Document.fromFile('./letter.docx');

doc.replaceText('{{CustomerName}}', 'Nguyen Van A');           // plain string
doc.replaceText(/\d{4}-\d{2}-\d{2}/g, m => formatDate(m[0]));   // regex + function
doc.replaceText('cat', 'dog', { matchCase: false, wholeWord: true });

await doc.saveToFile('./out.docx');

Lossless package editing

Open a real document, change one thing, and keep everything else intact:

import { OoxmlPackage, acceptAllRevisions } from '@devmm/puredocs-word';
import { readFileSync } from 'fs';

const pkg = OoxmlPackage.open(readFileSync('./review.docx'));

// Every part is preserved; only what you touch is re-serialized.
const mainDoc = pkg.getMainDocument()!;   // parsed word/document.xml (XML tree)
acceptAllRevisions(mainDoc);              // resolve tracked changes in place

const out = pkg.toBuffer();

Feature guide

Page-break constraints (extractPageConstraints)

Pagination engines that measure rendered DOM can detect overflow breaks but not the breaks Word stores explicitly. This returns those exact constraints so a hybrid engine can combine them with its own measurements.

import { OoxmlPackage, extractPageConstraints } from '@devmm/puredocs-word';

const pkg = OoxmlPackage.open(readFileSync('./report.docx'));

for (const c of extractPageConstraints(pkg)) {
  // c.blockIndex     — 0-based index of a direct <w:body> child
  // c.breakBefore    — this block must start a new page
  // c.keepWithNext   — w:keepNext: do not orphan from the next block
  // c.keepTogether   — w:keepLines: do not split this block's lines
}

blockIndex counts both <w:p> and <w:tbl> children of <w:body> in document order; the trailing body-level <w:sectPr> is not a block. Blocks with no constraints are omitted from the result.

breakBefore merges three sources: a <w:br w:type="page"/> run (the containing block is marked — mid-paragraph breaks are approximated), w:pageBreakBefore, and section breaks (the block after a w:sectPr paragraph, unless the section type is continuous; a missing type means nextPage). Word's stale lastRenderedPageBreak hints are deliberately ignored.

Headers, footers & document defaults

The reader resolves header/footer references and parses the referenced parts, so their content is available as ordinary model blocks.

const { sections, docDefaults } = readPackage(pkg);

const header = sections[0].headers?.default;   // (Paragraph | DocTable)[]
const footer = sections[0].footers?.default;
sections[0].headers?.first;                    // title-page header, if any
sections[0].headers?.even;                     // even-page header, if any

docDefaults?.fontFamily;   // from w:docDefaults/w:rPrDefault
docDefaults?.fontSizePt;

Stylesheet.docDefaults() exposes the same values when you already hold a stylesheet.

Text boxes & shapes (DocShape)

Text boxes and vector shapes are parsed instead of dropped. Pictures — inline, floating (wp:anchor) or VML (v:imagedata) — remain DocImage.

import { DocShape, DocImage } from '@devmm/puredocs-word';

for (const child of paragraph.children) {
  if (child instanceof DocShape) {
    child.kind;        // 'textBox' | 'shape'
    child.placement;   // 'inline' | 'floating'
    child.geometry;    // 'rect', 'ellipse', … (preset or VML element name)
    child.content;     // parsed blocks inside a text box
    child.getText();
  } else if (child instanceof DocImage) {
    child.placement;   // floating pictures are flagged, not silently inlined
  }
}

paragraph.shapes;      // just the DocShapes
paragraph.getText();   // text box content is NOT included here

Write path: shapes are read-only in the model writer. Their raw XML (kept on shape.rawXml) references relationship ids from the source package, and the model writer rebuilds relationships from scratch, so re-emitting it verbatim would point at unrelated parts. To preserve shapes, edit through OoxmlPackage instead. Floating pictures round-trip as inline pictures.

Styles (Stylesheet)

Read, query, resolve inheritance, and add custom styles.

import { OoxmlPackage, Stylesheet, RunStyle, WordColor } from '@devmm/puredocs-word';

const pkg = OoxmlPackage.open(buffer);
const styles = Stylesheet.parse(pkg.getText('word/styles.xml')!);

styles.get('Heading1')?.name;                 // "heading 1"
styles.basedOnChain('Heading1');              // ['Heading1', 'Normal']
styles.effectiveRunStyle('Heading1').bold;    // merges docDefaults + basedOn chain

const warn = new RunStyle();
warn.bold = true; warn.color = WordColor.fromHex('C00000');
styles.addStyle({ styleId: 'Warning', name: 'Warning', basedOn: 'Normal', run: warn });

pkg.setText('word/styles.xml', styles.toXmlString());

Document metadata (DocumentProperties)

import { OoxmlPackage, DocumentProperties } from '@devmm/puredocs-word';

const pkg = OoxmlPackage.open(buffer);
const props = DocumentProperties.parse(pkg.getText('docProps/core.xml')!);
props.title = 'Quarterly Report';
props.creator = 'Bill';
props.keywords = 'finance, q2';
pkg.setText('docProps/core.xml', props.toXmlString());

Comments, footnotes & endnotes

import { OoxmlPackage, readComments, readFootnotes } from '@devmm/puredocs-word';

const pkg = OoxmlPackage.open(buffer);
readComments(pkg.getText('word/comments.xml')!);   // [{ id, author, date, initials, text }]
readFootnotes(pkg.getText('word/footnotes.xml')!); // [{ id, text }]  (separators skipped)

Track changes (readRevisions / accept / reject)

import { OoxmlPackage, readRevisions, acceptAllRevisions, rejectAllRevisions } from '@devmm/puredocs-word';

const pkg = OoxmlPackage.open(buffer);
const doc = pkg.getMainDocument()!;

readRevisions(doc);       // [{ type: 'insertion'|'deletion', author, date, text }]
acceptAllRevisions(doc);  // keep insertions, drop deletions
// rejectAllRevisions(doc); // drop insertions, restore deletions

const out = pkg.toBuffer();

Content controls (SDT) & fields

import { OoxmlPackage, readContentControls, readFields, insertTableOfContents } from '@devmm/puredocs-word';

const pkg = OoxmlPackage.open(buffer);
const doc = pkg.getMainDocument()!;

readContentControls(doc);   // [{ tag, alias, type, text }]
readFields(doc);            // [{ instruction: 'MERGEFIELD Name', result: '…' }]

// Insert a Table of Contents (dirty field → Word rebuilds it on open).
insertTableOfContents(doc, { title: 'Table of Contents' });
const out = pkg.toBuffer();

To make Word update the TOC automatically on open, set the flag in settings:

import { Settings } from '@devmm/puredocs-word';
const settings = Settings.parse(pkg.getText('word/settings.xml')!);
settings.updateFieldsOnOpen = true;
pkg.setText('word/settings.xml', settings.toXmlString());

Document protection (Settings)

import { Settings } from '@devmm/puredocs-word';

const settings = Settings.parse(pkg.getText('word/settings.xml')!);
settings.setProtection('readOnly');   // 'readOnly' | 'comments' | 'trackedChanges' | 'forms'
settings.getProtection();             // { edit: 'readOnly', enforced: true }
pkg.setText('word/settings.xml', settings.toXmlString());

Password-based protection (Word's hash scheme) is not implemented; this enforces the edit mode without a password.

Numbering definitions (NumberingDefinitions)

import { OoxmlPackage, NumberingDefinitions } from '@devmm/puredocs-word';

const pkg = OoxmlPackage.open(buffer);
const nd = NumberingDefinitions.parse(pkg.getText('word/numbering.xml')!);
nd.isBullet(1);            // true / false
nd.level(2, 0)?.numFmt;    // 'decimal', 'lowerRoman', …

Mail merge

Lossless template substitution — the template's styling and layout are preserved, and placeholders split across runs are handled.

import { MailMerge } from '@devmm/puredocs-word-mail-merge';
import { readFileSync, writeFileSync } from 'fs';

const template = readFileSync('./template.docx');

// {{CustomerName}} in the template, or Word «MERGEFIELD» display text
const result = MailMerge.merge(template, {
  CustomerName: 'Nguyen Van A',
  InvoiceDate: '2026-04-01',
  TotalAmount: '1,500,000 VND',
});
writeFileSync('./invoice.docx', result);

const buffers = MailMerge.mergeAll(template, records);      // one file per record
const combined = MailMerge.mergeAllInOne(template, records); // one file, page breaks

// Options: keepUnmatched leaves {{x}} in place when no data key matches
MailMerge.merge(template, data, { keepUnmatched: true });

HTML & Markdown conversion

Import (HTML/Markdown → .docx)

import { HtmlToDocx, MarkdownToDocx } from '@devmm/puredocs-word-html-import';

const doc = new HtmlToDocx().convert('<h1>Title</h1><p><strong>Bold</strong></p>');
await doc.saveToFile('./from-html.docx');

const doc2 = await MarkdownToDocx.fromString('# Hello\n\n- a\n- b');
await doc2.saveToFile('./from-markdown.docx');

Export (.docx → HTML/Markdown)

import { Document } from '@devmm/puredocs-word';
import { DocxToHtml, DocxToMarkdown } from '@devmm/puredocs-word-html-import';

const doc = await Document.fromFile('./report.docx');

const html = new DocxToHtml().convert(doc, { fullDocument: true, title: 'Report' });
const md = new DocxToMarkdown().convert(doc);

Angular

// app.config.ts
import { provideWord } from '@devmm/puredocs-word-angular';
export const appConfig = { providers: [provideWord()] };

// component.ts
import { Component, inject } from '@angular/core';
import { WordService } from '@devmm/puredocs-word-angular';

@Component({ selector: 'app-export', template: `<button (click)="export()">Export</button>` })
export class ExportComponent {
  private word = inject(WordService);
  async export() {
    const buffer = await this.word.createDocument(doc => {
      doc.addHeading('Report', 1);
      doc.addParagraph(p => p.addRun('Generated by Angular'));
    });
    this.word.downloadBlob(buffer, 'report.docx');
  }
}

React

import { useDocument } from '@devmm/puredocs-word-react';

function ExportButton() {
  const { createDocument, downloadBlob, loading } = useDocument();
  const handleExport = async () => {
    const buffer = await createDocument(doc => {
      doc.addHeading('Hello from React', 1);
      doc.addParagraph(p => p.addRun('Generated by useDocument hook'));
    });
    downloadBlob(buffer, 'export.docx');
  };
  return <button onClick={handleExport} disabled={loading}>Export Word</button>;
}

Units Reference

Word OOXML uses two unit systems — use the provided helpers:

import { inchToTwip, cmToTwip, ptToTwip, inchToEmu, cmToEmu } from '@devmm/puredocs-word';

inchToTwip(1)   // → 1440   (margins, spacing, indents, column widths)
cmToTwip(2.54)  // ≈ 1440
ptToTwip(12)    // → 240
inchToEmu(1)    // → 914400 (image dimensions)
cmToEmu(10)     // → 3600000

Capability status

| Area | Status | |------|--------| | Read / write / lossless round-trip | ✅ | | Formatting (runs, paragraphs, sections) | ✅ | | Tables (merged cells), images, hyperlinks, lists | ✅ | | Header/footer content, docDefaults | ✅ | | Page-break constraints for pagination engines | ✅ | | Text boxes, floating & VML pictures, vector shapes | ✅ read-only | | Styles (+ basedOn inheritance) | ✅ | | Find & Replace (cross-run, regex) | ✅ | | Mail merge | ✅ | | Metadata, comments, footnotes/endnotes | ✅ | | Track changes (accept / reject) | ✅ | | Content controls, fields, Table of Contents | ✅ | | Document protection | ✅ (no password hash) | | Export to HTML / Markdown | ✅ | | PDF / per-page image rendering | ⏳ planned — see ROADMAP | | Shape / chart / SmartArt authoring, digital signatures | ⏳ planned |

PDF and image output require a layout engine (line breaking, pagination, font metrics) and are tracked as a separate, larger effort in docs/ROADMAP.md.


Development

pnpm install
pnpm build     # build all packages (turbo)
pnpm test      # run all tests (vitest)

Project structure

packages/
├── core/         @devmm/puredocs-word          ← main package
├── mail-merge/   @devmm/puredocs-word-mail-merge
├── html-import/  @devmm/puredocs-word-html-import
├── angular/      @devmm/puredocs-word-angular
└── react/        @devmm/puredocs-word-react

License

MIT © devmm