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

@keenthekeen/exam-import

v0.1.0

Published

Parse multiple-choice exam items from DOCX (marker template) and XLSX files, in the browser or Node.

Readme

@keenthekeen/exam-import

Parse multiple-choice exam items from DOCX (marker template) and XLSX files — entirely locally, in the browser or Node.js. Nothing is uploaded anywhere, which makes it suitable for confidential exam content (e.g. parse, then encrypt client-side).

pnpm add @keenthekeen/exam-import

Sample files for every supported format are shipped in examples/ (also at node_modules/@keenthekeen/exam-import/examples/) — hand them to authors as templates.

Usage

import { parseDocx, parseXlsxItems, DocxParseError } from "@keenthekeen/exam-import";

input.addEventListener("change", async () => {
  const file = input.files![0]!;
  try {
    const items = file.name.endsWith(".docx")
      ? await parseDocx(file)
      : await parseXlsxItems(file);
    // items: { item_number, sort_order, question, choices, correct, topics, tags, image? }[]
  } catch (err) {
    // DocxParseError messages are written for exam authors, e.g.
    // `Question 3: $ANSWER F has no matching $F choice (the question begins "Which of…")`
    if (err instanceof DocxParseError) console.log(err.questionOrdinal, err.questionStart);
    alert((err as Error).message);
  }
});

Every parser accepts a File/Blob, ArrayBuffer or Uint8Array (including a Node Buffer). Import only one format to keep the other's dependency out of your bundle — both are loaded lazily anyway:

import { parseDocx } from "@keenthekeen/exam-import/docx";
import { parseXlsxItems } from "@keenthekeen/exam-import/xlsx";

DOCX in Node.js

The DOCX parser walks HTML with DOMParser, which Node lacks. Pass one from jsdom or linkedom:

import { JSDOM } from "jsdom";
const { window } = new JSDOM();
const items = await parseDocx(buffer, { domParser: new window.DOMParser() });

DOCX format — examples/mcq-template.docx

Each paragraph (or soft-broken line) of a question starts with a marker:

| Marker | Meaning | | --------------- | ----------------------------------------------------------------------------------------- | | $topic | Topic(s), comma-separated; "quote, a topic" containing a comma. Optional, repeatable. | | $tag | Free-form tag. Optional, repeatable. | | $Q | Question stem. Continues over following unmarked paragraphs; may hold one JPEG/PNG image. | | $A … $E | Choices — at least two. | | $ANSWER | The correct choice: letter (B, (b), **B**) or the choice's full text. | | any other $x | Ignored together with its text (e.g. $reference). |

Text before the first marker is ignored, so the file can start with instructions. Blank lines are ignored too, so items can be separated by them for readability. Tolerated quirks: $A:/$A)/$A. separators, no space after the marker (Thai), bold/italic markers, non-breaking and zero-width spaces, full-width $, Word multi-level numbering. Bold, italic, superscript and subscript become markdown (**b**, *i*, ^sup^, ~sub~). Tables are not imported.

Options: maxImageBytes (default 5 MB), domParser.

Lower-level building blocks are exported for testing or custom pipelines: htmlToParagraphs(html, images) and parseParagraphs(paragraphs).

XLSX formats

The first sheet is read; headers are case-insensitive.

Items — wide (items-wide.xlsx), one row per item:

| # | Question | Choice_A | Choice_B | … | Correct | Topic… | Tag… | |---|----------|----------|----------|---|---------|--------|------|

Correct names the correct choice by its column letter (B, b, (B), B.) or by its text (case-insensitive; text is tried first). Leave it blank for no answer key; a value matching no choice is an error. Any number of Topic*/Tag* columns.

Items — tall (items-tall.xlsx), one row per choice; # only on an item's first row:

| # | Question | Choice | Correct | Topic… | Tag… | |---|----------|--------|---------|--------|------|

Mark the correct choice's row with ✓, ✔, x, true, yes, y or 1 (any case), or repeat the choice's text. Any other value (including blank, false, no) means incorrect.

Both are parsed by parseXlsxItems.

Topics only — parseXlsxTopics (topics.xlsx): # plus one or more Topic* columns.

Item statistics — parseXlsxPValues (p-values.xlsx): # plus an All (N) column, where N is the number of test takers and each cell is how many answered the item correctly. Returns { totalTestTakers, rows: [{ item_number, correctCount }] }.

Development

pnpm install
pnpm test        # vitest (jsdom)
pnpm typecheck
pnpm build       # → dist/
pnpm examples    # regenerate examples/ (test/examples.spec.ts checks them)

License

MIT