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

docier-web

v0.9.52

Published

A Word-compatible DOCX editor for the browser. Framework-agnostic, command and event driven, with an optional tokenization module for document templates.

Readme

docier

A DOCX editor for the browser, written in TypeScript.

docier opens a .docx, lets a person edit it, and writes a valid .docx back. It is a library rather than an application: you mount it into an element, configure it with one object, drive it through commands and observe it through events. It is framework-agnostic, has one runtime dependency, and ships an optional module for document templates.

CI

The problem it solves

Most browser editing is HTML editing. You type into a contenteditable, the browser decides where the text goes, and a converter guesses at a .docx afterwards. That works until the document matters: the line breaks are not the ones that print, the page breaks are not the ones Word would choose, and each conversion loses something.

docier treats the .docx as the document and computes the layout itself. Text is positioned from a computed layout rather than from what the browser happened to do, so the page you see and the page that prints are the same page. Documents from Word keep their markup, including the parts docier does not understand, so re-saving a contract does not quietly remove things.

Install

npm install docier-web

Quick start

import { createEditor } from 'docier-web'
import { mountChrome } from 'docier-web/ui'

const bytes = await fetch('/contract.docx').then((r) => r.arrayBuffer())

const editor = createEditor('#editor', {}, { document: bytes })
mountChrome(editor)

editor.events.on('docier:doc:change', () => scheduleSave(editor.document))

createEditor(target, config, options) where target is an element or a selector:

  • config is a deep-partial configuration object; every key has a default.
  • options.document accepts a Uint8Array, ArrayBuffer, Blob, or an already-parsed package or model. options.zoom and options.render are also accepted here.

The call is safe to repeat: mounting twice on the same element returns the first handle, and destroy() is idempotent.

Using it

Everything the user can do is a command. Every state change is an event.

editor.commands.execute('docier.command.format.bold')
editor.commands.execute('docier.command.insert.table', { rows: 3, columns: 4 })

const bold = editor.commands.describe('docier.command.format.bold')
// { enabled: true, active: false, ... }

const blocked = editor.commands.describe('docier.command.table.deleteRow')
// { enabled: false, reason: 'The caret is not inside a table' }

A command that cannot run tells you why. Nothing is silently inert, and nothing reports success while doing nothing: describe returns a reason, and executing a blocked command returns a structured status carrying the same reason.

Events follow the same naming scheme, docier:<area>:<verb>, with the cancellable ones named before<Verb>:

editor.events.on('docier:selection:change', ({ from, to }) => updateToolbar(from, to))
editor.events.on('docier:doc:beforechange', (event) => {
  if (!confirmDiscard()) event.preventDefault()
})

Exporting

PDF export is a separate entry point, so an application that never exports never loads it.

import { exportPdf } from 'docier-web/pdf'

const { bytes, report } = await exportPdf(editor.layout, { pdfa: 'a-2b' })

The exporter renders the same computed layout the screen renders. It does not re-run layout, re-measure text, or consult the document for any position. That is what makes the screen and the printed page agree rather than approximately agree. Glyph advances come from the same font measurement the layout used, and a font whose metrics disagree is reported rather than substituted.

Output is deterministic: the same document produces byte-identical bytes, verified across separate processes. Missing fonts and missing images are reported as losses rather than silently omitted.

Printing is available too, with page ranges shared by the print and PDF paths.

Document templates

Optional, a separate entry point, and off unless enabled. An application that does not use templates never loads it.

A token is a real Word content control. It is the same mechanism Word itself uses for fillable fields, which means a template stays a valid .docx that a person can open and edit in Word, with the fields still working. Placeholders are not a special text syntax that only docier understands.

import { createTokenAttachment, fillTemplate } from 'docier-web/tokens'

const tokens = createTokenAttachment(editor, { catalogue, locale: 'ro-RO' })
tokens.data.setData({ 'employee.surname': 'Popescu' })

const { bytes, issues } = await fillTemplate({ template, catalogue, data, locale: 'ro-RO' })

The catalogue is the backend's authority. A template that references a field the catalogue does not define is reported rather than rendering as blank, and a value that is missing is both reported by code and shown as a visible placeholder. A contract must never print a silently empty name.

Filling runs headlessly: bytes in, bytes out, no DOM and no network, so a server can fill a template without a browser.

What it handles

  • DOCX fidelity. Round-trips a real .docx, preserving markup it does not model as raw bytes rather than dropping it. Unicode is preserved exactly, not normalised.
  • Layout. Line breaking, justification, hyphenation points, widow and orphan control, keep-with-next and keep-lines-together, page breaks, sections, columns, tab stops, borders and shading.
  • Tables. Column resolution, all three row height rules, row splitting across pages, repeating header rows, merged cells, nested tables, and editing inside cells.
  • Headers, footers and page fields. Default, first-page and even/odd variants, with PAGE, NUMPAGES, SECTION and SECTIONPAGES resolved to real values.
  • Styles. The full cascade: document defaults, table styles and conditional formatting, numbering, paragraph and character styles with their basedOn chain, and direct formatting, with Word's toggle semantics.
  • Editing. Caret and selection, Word's navigation keys, typing, splitting and joining, clipboard with a model-content buffer, undo and redo with one entry per gesture, and 112 commands.
  • Chrome. Ribbon with tabs and groups, context menus on every surface, a floating selection toolbar, a ruler, a status bar, and theming through CSS custom properties.

Requirements

Evergreen browsers: Chrome and Edge 120+, Firefox 121+, Safari 17.4+.

The floor is set by capability rather than version. docier uses CompressionStream, Intl.Segmenter for locale-aware line and word breaking, the Popover API, structuredClone, and ES modules with top-level await. There are no polyfills for older browsers.

Fonts are not bundled. You supply them. This keeps the package small and leaves font licensing with you, but it means text will not render correctly until you wire up the fonts your documents use. See docs/ for the requirement.

The font integration contract

Layout happens before paint and without a browser, so the engine cannot ask a font file how wide a run is. It measures through a TextMeasurer, and the widths it returns are the widths the renderer has to produce. That makes three things your responsibility, and they have to agree with each other:

  1. Supply the face bytes. Read the TTF or OTF files you are licensed to serve, keyed by the family names and the four styles (regular, bold, italic, bold italic) the documents actually ask for.

  2. Register screen faces for every document family name. The browser paints with fonts resolved from CSS, so a family the page does not know falls back to something else and the painted run will not be the width the engine reserved. A document asks for Calibri, Times New Roman or Arial, not for the name of the file you happen to ship. Declare only the styles you can actually serve: a @font-face with no file behind it makes the browser substitute a font the measurer never saw, which is worse than leaving the style out and letting the family's regular face answer for it.

  3. Build the measurer from the same bytes. createFontMeasurer({ faces }) takes { family, bold, italic, bytes } records and returns a TextMeasurer that reads real hmtx advances and cmap coverage, segments text exactly like the built-in model (combining marks and variation selectors stay with their base), and reports a face id the PDF exporter can verify against the font it embeds. Pass it as layout.measurer:

    import { createEditor, createFontMeasurer } from 'docier-web';
    
    const measurer = createFontMeasurer({
      faces: [
        { family: 'Calibri', bold: false, italic: false, bytes: regularBytes },
        { family: 'Calibri', bold: true, italic: false, bytes: boldBytes },
      ],
      fallbackFamily: 'Calibri',
    });
    
    const handle = createEditor(host, { layout: { measurer }, /* ... */ });

layout.measurer is a reload key, so setting a different measurer lays the document out again with it; updateConfig({ layout: { measurer: other } }) takes effect on the next layout rather than mutating the painted page in place.

If no measurer is supplied the engine falls back to a deterministic advance model, which is legible in tests but is not a font: real faces are 14 to 30 percent wider, so with that model adjacent runs overlap and right-aligned text runs past the margin. That is the failure you see when only the screen @font-face rules were wired up.

Check that what you fetched really is a font. A development server that answers a missing file with its own HTML page returns 200 OK and markup, and font bytes are parsed rather than trusted, so a placeholder page in the place of a face is an error rather than a silent fallback.

The full worked example is the demo host in example/src/fonts.ts and example/src/main.ts: it loads the faces, registers the screen aliases, and hands the same bytes to createFontMeasurer.

Status

Version 0.x. Under active development, CI on every push, 1,200+ assertions.

Working: everything listed under "What it handles".

Not yet: templates with repeating sections and conditionals; inserting images and shapes, as opposed to rendering ones already in a document; comments; footnotes; find and replace; proofing; and editing a header or footer from the UI. See agent_progress.md for the current state and docs/SPEC.md for the full design.

Architecture

For contributors, and for anyone who wants to know why the page you see is the page that prints.

There is one layout engine and two renderers. The engine produces an immutable layout result in millipoints; the screen renderer paints it into the DOM and the PDF exporter paints it into a file. No CSS takes part in layout, the point-to-pixel conversion happens in exactly one place, and zoom is a paint scale that never triggers a re-layout. The constraint is enforced by a test that fails if the renderer imports the layout layer, so the two cannot drift apart through inattention.

The document model is a typed view over the parsed XML rather than a structure rebuilt from it. That is what lets unmodelled markup survive an edit, and it is why an unknown element in a contract is still there after a save.

See docs/SPEC.md for the full design and docs/adr/ for the decisions behind it.

Licence

Dual-licensed: AGPL-3.0-only for open source, or a commercial licence for proprietary use.

The AGPL covers use over a network, so it applies to a hosted product as well as a distributed one. If that does not suit your project, a commercial licence is available. See LICENSE and COMMERCIAL.md.