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

word-toolkit

v1.2.2

Published

Battle-tested toolkit for Microsoft Word add-in development — safe execution, track changes, document extraction, and more.

Readme

word-toolkit

npm version license npm downloads

Battle-tested toolkit for building Microsoft Word add-ins with the Office.js API.

Handles the hardest parts of Word add-in development so you can focus on your product: safe execution, track changes, document extraction, selection management, and more.


Why this exists

Building a Word add-in with Office.js is painful. The API is async, stateful, and fragile:

  • Concurrent Word.run calls crash the add-in because there's no built-in locking
  • Calling context.sync() too fast overwhelms the API and Word stops responding
  • Transient errors are silent and random: "service is busy", "GeneralException", timeouts with no recovery
  • Track changes via the API don't exist, so you have to fake them with search/delete/insert at the word level
  • OOXML with REF fields + tracked changes crashes Word Desktop with no warning
  • Paragraph IDs have inconsistent casing between Word Desktop and Word Online
  • Emoji, smart quotes, zero-width chars, and field codes break search operations silently

This toolkit handles all of it. Every function goes through a single execution engine with locking, retry, circuit breaking, throttling, and timeout protection. Track changes are applied using word-level diffs so they appear as native insertions and deletions. OOXML is pre-scanned for crash-inducing structures before any edit touches the document.

The result: you call getContextWithComments() to extract the full document — paragraphs, tables, headers, footers, footnotes, and endnotes — in one batch, and applyBatchChanges(changes) to apply dozens of edits, inserts, and deletes as native tracked changes, accurate, fast, and in a single Word.run. It just works on Desktop, Mac, and Web, with or without problematic document structures.


Installation

npm install word-toolkit

All dependencies are bundled automatically.


Quick Start

import {
  getContextWithComments,
  getSelectedParagraphIds,
  shapeContextForBackend,
} from "word-toolkit";

// Extract paragraphs, tables, headers, footers, footnotes, endnotes, tracked changes, and comments
const { paragraphs, comments } = await getContextWithComments();

// Get what the user has selected
const selectedIds = getSelectedParagraphIds(paragraphs);

// Shape for your backend API
const shaped = shapeContextForBackend(paragraphs);

Every call is automatically protected by the runner: locking, retry, circuit breaker, throttling, and timeout are all handled for you.


Usage Examples

Extract document content

import {
  getContext,
  shapeContextForBackend,
  getSelectedParagraphIds,
} from "word-toolkit";

const blocks = await getContext();
const selectedIds = getSelectedParagraphIds(blocks);
const shaped = shapeContextForBackend(blocks);

await fetch("/api/analyze", {
  method: "POST",
  body: JSON.stringify({ context: shaped, selectedParagraphIds: selectedIds }),
});

Extract content with comments

import { getContextWithComments } from "word-toolkit";

const { paragraphs, comments } = await getContextWithComments();

Apply changes with track changes

import { applyBatchChanges } from "word-toolkit";

const results = await applyBatchChanges([
  { paragraphId: "abc123", newText: "Improved sentence.", operation: "edit" },
  { paragraphId: "def456", newText: "New paragraph.", operation: "insert" },
  { paragraphId: "ghi789", operation: "delete" },
]);

results.forEach((r) => {
  console.log(r.success ? `Applied: ${r.paragraphId}` : `Failed: ${r.error}`);
});

Edit a single paragraph

import { replaceTextByParagraphId } from "word-toolkit";

await replaceTextByParagraphId("abc123", "Updated text", "AI suggestion");

Listen for selection changes

import { setupSelectionChangeListener } from "word-toolkit";

const cleanup = setupSelectionChangeListener((selectedText) => {
  console.log("Selected:", selectedText);
});

// Call cleanup() when done (e.g. React useEffect return)

Revert a change

import { rejectTrackedChanges } from "word-toolkit";

const result = await rejectTrackedChanges("abc123");
// { success: true, rejectedCount: 2, commentsRemoved: 1 }

Check document health

import { probeWordApi, checkDocumentEditable } from "word-toolkit";

if (!(await probeWordApi())) return showError("Word is not responding");

const { editable, reason } = await checkDocumentEditable();
if (!editable) return showError(`Read-only: ${reason}`);

Insert markdown as HTML at cursor

import { insertTextToCursor } from "word-toolkit";

await insertTextToCursor("**Bold** and _italic_ text");

Custom text preprocessing

import { setTextPreprocessor } from "word-toolkit";

setTextPreprocessor((text) => {
  return text
    .replace(/\[suggestion\](.*?)\[\/suggestion\]/gi, "$1")
    .replace(/\[delete\].*?\[\/delete\]/gi, "");
});

How the Runner Protects You

Every function in this package goes through safeWordRun:

| Protection | What it does | | -------------------- | ------------------------------------------------------------ | | FIFO Lock | Only one Word.run at a time, prevents concurrent crashes | | Retry | Transient errors retried up to 3x with exponential back-off | | Circuit Breaker | After 15 consecutive failures, pauses 5s to let Word recover | | Timeout | Kills hung operations after 30s | | Generation Abort | Superseded callbacks bail out immediately | | Throttled Sync | context.sync() spaced minimum 16ms apart | | Busy Tracking | Detects Office stress, adds cooldown periods |


API Reference

Execution

| Function | Description | | ----------------------------------- | ------------------------------------------- | | safeWordRun(fn, options?) | Execute Word.run with full protection | | throttledSync(context, options?) | Throttled context.sync() | | waitForOfficeFree(options?) | Wait until Office stress level drops | | ensureOfficeReady() | Wait for Office.onReady | | probeWordApi(timeoutMs?) | Health check, returns true/false | | checkDocumentEditable(timeoutMs?) | Write test, returns { editable, reason } | | getWordApi() | Returns global Word object or undefined |

Track Changes

| Function | Description | | ------------------------------------------------ | ------------------------------------------------- | | applyBatchChanges(changes, options?, onError?) | Apply multiple edits/inserts/deletes in one batch | | applyParagraphChange(change) | Apply a single edit, insert, or delete | | replaceTextByParagraphId(id, text, comment?) | Edit a paragraph with tracked changes | | insertTextAtCursorTracked(text) | Insert plain text at cursor with tracking | | insertTextToCursor(markdown, options?) | Insert markdown as formatted HTML at cursor | | checkIfReverted(paragraphId) | Check if changes were reverted | | setTextPreprocessor(fn) | Override text preprocessing | | selectTextByParagraphId(fromId, toId?) | Scroll to and select a paragraph |

Document Context

| Function | Description | | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | getContext(options?) | Extract paragraphs, tables, headers, footers, footnotes, and endnotes as normalised blocks | | getContextWithComments(options?) | Full context + comments in one batch | | getSelectedParagraphIds(blocks) | Get highlighted paragraph IDs | | shapeContextForBackend(blocks) | Validate and shape blocks for your API | | ensureContextForBackend(blocks) | Validate with issue reporting | | shapeAttachmentsForBackend(attachments) | Normalise file attachments | | ensureContentIdMap() | Populate content ID map if empty | | getContentIdMap() / setContentIdMap() / clearContentIdMap() | Manage content ID cache | | setLargeDocumentWarningCallback(cb) | Register large document warning |

Selection

| Function | Description | | ----------------------------------------- | --------------------------------------- | | getSelectedText() | Get selected text (cleaned) | | getDocumentText() | Get full document text | | clearSelection() | Collapse selection to end | | replaceHighlightedText(text, fontSize?) | Replace current selection | | insertTextToBody(text, fontSize?) | Insert at selection or end of document | | isHighlighting() | Selected text or empty string | | setupSelectionChangeListener(callback) | Debounced listener (returns cleanup fn) | | removeSelectionChangeListener() | Remove listener manually |

Comments

| Function | Description | | ----------------------------------------- | -------------------------------- | | getCommentsWithParaId() | All comments with paragraph IDs | | getCommentsWithParaIdInContext(context) | Same, using existing context | | removeCommentsFromParagraph(id) | Remove comments from a paragraph | | rejectTrackedChanges(id) | Reject changes + remove comments |

Paragraph Lookup

| Function | Description | | ---------------------------------------- | -------------------------------------------- | | getParagraphById(id) | Find paragraph by ID | | getParagraphTextById(id, options?) | Get text (supports acceptRevisions) | | getParagraphRange(id) | Get Word.Range for paragraph or table cell | | getParagraphByIdInContext(context, id) | Find paragraph in existing context | | getRangeByIdInContext(context, id) | Get range in existing context | | scrollToParagraph(fromId, toId?) | Scroll to and select paragraph(s) | | selectContentById(id) | Select a single paragraph |

Word Service (low-level)

| Function | Description | | ------------------------------------------------- | --------------------------------------------------- | | ApplyMode | Enum: WITH_TRACKED_CHANGES, WITH_COMMENT, RAW | | run(callback) / runResult(callback) | Safe Word.run wrappers | | withTracking(callback) | Run with track changes ON (restores original) | | withoutTracking(callback) | Run with track changes OFF (restores original) | | applyModifications(mode, callback) | Dispatch by ApplyMode | | replaceSingleParagraph(id, text, comment, mode) | Word-level diff replace | | deleteSingleParagraph(id, mode, comment) | Delete with tracking | | insertComment(ctx, comment, range) | Add comment to a range | | getRangeBetween(from, to) | Range spanning two endpoints |

OOXML

| Function | Description | | ------------------------------------------------ | -------------------------------------------- | | invalidateOoxmlCache() | Clear cached analysis | | checkForProblematicTrackedChanges(ctx) | Detect crash-inducing REF fields | | checkOoxmlForMoveOperations(ctx) | Detect move operations | | parseParagraphOoxml(pXml, index) | Parse paragraph XML to { id, text, diffs } | | extractParaId(pXml) / extractRunText(runXml) | XML element helpers |

Text Utilities

| Function | Description | | ------------------------------ | -------------------------------------------------- | | cleanText(text) | Strip control chars, field codes, zero-width chars | | cleanUnicode(text) | Strip chars that break Word search | | normalizeQuotes(text) | Smart quotes to ASCII | | textsMatch(a, b, strict?) | Fuzzy comparison (90% threshold) | | hashText(text) | Fast DJB2 hash | | normalizeForComparison(text) | Full normalisation for comparison | | isEmptyText(text) | Check if empty after cleaning | | toChunks(input, charCount) | Fixed-size substring generator |

Diff Utilities

| Function | Description | | --------------------------------- | -------------------------------------- | | DiffType | Constants: ADDED, REMOVED, EQUAL | | getDiff(oldText, newText) | Diff optimised for Word track changes | | diffWords(oldText, newText) | Raw word-level diff | | getCurrentTextFromDiffs(diffs) | Reconstruct new text | | getOriginalTextFromDiffs(diffs) | Reconstruct original text |

Emoji

| Function | Description | | ------------------------------------ | ----------------------------------- | | renderEmojiToDataUri(grapheme) | Emoji to PNG data-URI | | isEmojiGrapheme(grapheme) | Detect emoji grapheme | | replaceEmojiWithInlineImages(html) | Replace emoji with <img> for Word |


Dependencies

All dependencies install automatically.

| Package | Purpose | | ----------- | --------------------------- | | diff | Word-level text diffing | | uuid | Unique ID generation | | marked | Markdown to HTML conversion | | dompurify | HTML sanitisation |


License

MIT