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-doctor

v0.3.0

Published

Detect and repair structural defects in real-world Word (.docx) templates.

Readme

docx-doctor

Detect and repair structural defects in real-world Word (.docx) templates — the numbering restarts, silently-dropped heading spacing, and stray trailing blank pages that accumulate in a template that's been hand-edited in Word for years.

Why

Most docx tooling (docxtemplater, Aspose.Words, the OpenXML SDK) assumes you already have a clean template and just want to fill it with data. In practice, a firm's actual "standard template" file has a decade of hand-edits, copy-pasted sections, and Word's own UI silently corrupting structure along the way. docx-doctor's job is that different, earlier problem: diagnose and fix the template itself, before you templatize or mail-merge it with something else.

Non-goals

  • Not a templating/mail-merge engine. Use docxtemplater (or your own token-replacement) for filling — docx-doctor is a pre-processing step, not a replacement.
  • Not a general-purpose docx editor. It only understands a specific, growing set of known defect patterns.
  • No AI/LLM involved — this is deterministic OOXML structure analysis.

Install

npm install -g docx-doctor

or run it without installing via npx docx-doctor ....

CLI

docx-doctor scan template.docx
docx-doctor scan template.docx --rules trailing-blank-pages,numbering-restart
docx-doctor scan template.docx --ci        # nonzero exit only on error-severity findings
docx-doctor fix template.docx -o fixed.docx
docx-doctor fix template.docx -o fixed.docx --rules heading-ppr-order

--ci is the intended CI-pipeline hook: it fails the build on a error-severity finding (a defect docx-doctor is confident about) but not on a warning (like split-run-risk, which is informational — see below), so it doesn't block a build over something cosmetic.

Library

import { openDocx, scan, fix } from "docx-doctor";

const doc = await openDocx(buffer);
const { findings } = scan(doc);                          // read-only
const { doc: fixed, applied, skipped } = fix(doc, { rules: ["numbering-restart"] });
await fixed.save("out.docx");

What it catches (v0.1.0)

| Rule | Severity | Fixes it? | What it catches | |---|---|---|---| | trailing-blank-pages | warn | yes | A run of 2+ empty paragraphs left at the end of the document body from repeated save/edit cycles. | | heading-pPr-order | error | yes | Paragraph properties inserted after the paragraph-mark <w:rPr> inside <w:pPr> — Word silently ignores them (ECMA-376 §17.3.1.29), a common result of naive "append before </w:pPr>" template code. | | numbering-restart | error | yes | A list that reads as one continuous list but silently restarts its count, because a paragraph got pointed at a second <w:num> entry that points at the same underlying list definition. | | split-run-risk | warn | no (linter only) | A {{TOKEN}} or ${TOKEN} placeholder whose text is split across multiple <w:r> runs — invisible on screen, but it'll silently defeat a naive find-replace on the raw XML. Detect-only: fixing it is a templating decision, not a structural repair. |

Every rule is built to never false-positive on a clean template — each has a test proving zero findings on an undamaged fixture, not just that it catches the defect it's looking for.

Building a document from scratch (docx-doctor/build)

Everything above repairs an existing file. docx-doctor/build is the other direction: a small API for constructing a new .docx where three of the four defects above are structurally impossible to produce, not just detectable afterward.

import { createDocument, paragraph, token, table, pageBreak } from "docx-doctor/build";

const doc = createDocument();                          // a valid, empty .docx
const scope = doc.numberedList({ format: "decimal" });  // one list definition
const link = doc.hyperlink("our website", "https://example.com");

doc.append(
  paragraph({ text: "Scope of Work", bold: true, spacing: { before: 240, after: 120 } }),
  paragraph({ text: "First deliverable", list: scope }),
  paragraph({ text: "Second deliverable", list: scope }),   // same handle -> continuous numbering, guaranteed
  table({ rows: [["Item", "Price"], ["Widget", "$5"]] }),
  pageBreak(),
  paragraph({ children: ["The project is ", token("PROJECT_NAME"), ". See ", link, " for details."] }),
);

const buffer = doc.build();   // runs docx-doctor's own scan() first; throws if it finds anything
  • paragraph() always places the paragraph-mark <w:rPr> last inside <w:pPr>, regardless of what order you pass options in — the heading-pPr-order bug can't happen through this function. Also takes pageBreakBefore: true, correctly ordered ahead of numPr/spacing/etc.
  • numberedList() returns a handle; reusing it keeps a list continuous, and getting a new list is a separate, deliberate call — the numbering-restart bug (an accidental second <w:num> duplicating a list) requires calling numberedList() again on purpose, not tripping into it by accident.
  • token() always becomes one complete, unsplit run — the only way to actually guarantee split-run-risk can't happen, since that rule has no automatic fix.
  • table() builds a <w:tbl> from a rows array of cells (plain text, a pre-built paragraph() for formatting, or an array for multiple paragraphs in one cell). Known limitation: 2+ consecutive empty trailing cells (e.g. an entirely empty last row) placed at the very end of the document can trip the trailing-blank-pages self-scan in build(), since that rule doesn't yet know a paragraph is inside a table cell rather than the body. Workaround (also just normal Word convention): end with a real paragraph after a trailing table.
  • pageBreak() is a standalone page-break paragraph, deliberately recognized as non-empty by trailing-blank-pages — using it near the end of a document won't trip the self-scan.
  • doc.hyperlink(text, url) registers the relationship and returns a handle to drop into paragraph({ children: [...] }).
  • trailing-blank-pages isn't otherwise preventable by construction (nothing stops you from appending empty paragraphs on purpose) — build()'s self-scan is the backstop for that one. Pass { allowFindings: true } to opt out.

Status

v0.1.0. All four scan/fix rules are real (not stubs), with tests including a CLI test that drives the actual subprocess, plus docx-doctor/build for constructing new documents defect-free. See ROADMAP.md for what's explicitly deferred (multi-section sectPr, startOverride-only restarts, runs wrapped in <w:hyperlink>/<w:ins>/<w:del>) — each one is a documented scope decision, not an unknown gap.

License

MIT — see LICENSE.