@keenthekeen/exam-import
v0.1.0
Published
Parse multiple-choice exam items from DOCX (marker template) and XLSX files, in the browser or Node.
Maintainers
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-importSample 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
