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

@docx-pages/core

v0.2.0

Published

Lay out a .docx the way Word does, page by page. Isomorphic, and measured against Word.

Readme

@docx-pages/core

Reads a .docx and works out where Word would put everything, page by page. Isomorphic: it touches no disk and no network, so the caller hands in the bytes and the fonts.

Draw the result with @docx-pages/viewer, or with your own renderer against the same types.

npm install @docx-pages/core

Laying a document out

import { layOutDocument, openDocx, substitutingMetrics } from "@docx-pages/core";

const faces = substitutingMetrics(suppliedFaces, ["Cambria"]);
const layout = layOutDocument(openDocx(bytes), faces);

if (layout.kind === "laid-out") {
  for (const page of layout.pages) {
    // page.body   paragraphs, each with its lines and where they sit
    // page.cells  table cells
    // page.floats anchored objects, in the order Word draws them
    // page.inlines drawings that stand in the flow of the text
  }
}

A document is refused rather than drawn wrongly. Where layout.kind is "blocked", layout.blocker says which of four things went wrong: the style cascade could not name a run's face (unresolved-font), nothing supplies a name and no fallback answers for it (unknown-font-metrics), a face was supplied in a form the reader cannot measure with (unmeasurable-text), or no face on the machine has a glyph for some character (unmapped-character).

Supplying faces

Nothing is guessed. A face is handed in carrying its metrics and its glyph advances, both read out of the file:

import { readFontFile, type SuppliedFace } from "@docx-pages/core";

const read = readFontFile(bytes); // ttf, otf, ttc or woff. Not woff2.
const face: SuppliedFace = {
  name: "Calibri",
  bold: false,
  italic: false,
  metrics: read.metrics,
  advances: read.advances,
  sansSerif: read.sansSerif,
};

readFontFile(bytes, faceName) picks one face out of a collection by name, since a .ttc holds several.

substitutingMetrics resolves a face the way Word does and is never quiet about it. Read both after laying out, not before, since nothing is known until the layout has asked for the faces it needs:

  • substitutions() names every face the document asked for that another one answered for. Every line drawn in it may break where Word did not break it.
  • fallbackCharacters() names every character drawn out of a face the document never mentioned. The room it takes is Word's, so nothing moves, but the glyph drawn in that room is whatever the renderer finds.

Which face answers for a character its own has no glyph for turns on the kind of face that asked and then on the character. WORD_SANS_FALLBACK_FACE, WORD_SERIF_FALLBACK_FACE, WORD_EMOJI_FACE and WORD_CHARACTER_FALLBACK_FACES say which faces are reached for, so a caller can supply them. A face the machine has not got is passed over rather than refused.

What the document asked for and did not get

for (const entry of layout.unhonoured) {
  entry.kind; // "character-spacing", "keep-with-next", "footnote", ...
  entry.effect; // "moves-text" or "changes-paint"
}

moves-text puts every page below it in doubt; changes-paint is wrong only where it stands. The report is not a list of known-wrong drawings: it is what the document asks for that nothing here answers, whether or not it showed.

Limits worth knowing

  • woff2 is refused. It needs brotli, which is not in every runtime.
  • cmap formats 4 and 12 only. Anything else refuses the document rather than guessing a width.
  • No kerning and no ligatures. A line is the sum of its characters' advances.
  • Bold and italic are separate faces, each needing its own file.

Licence

MIT.