Skip to content

Repository files navigation

@ariadng/office

A TypeScript library that reads and writes modern Microsoft Office files — Word, Excel, and PowerPoint, plus every macro, template, slideshow, and add-in variant. Its core promise is simple: anything you don't touch comes back exactly as it went in, byte for byte.

npm install @ariadng/office
  • Zero runtime dependencies. ZIP and XML are implemented in-house on platform primitives (Uint8Array, TextEncoder/TextDecoder, CompressionStream/DecompressionStream('deflate-raw')). Runs on Node ≥ 20 and evergreen browsers; Deno and Bun ride along. ESM only.
  • Open → edit → save without collateral damage. Parts you never touch are copied byte-for-byte from the source file. Content the library has no API for — pivot tables, animations, macros, vendor extensions — survives automatically, because nothing ever rebuilds it.
  • Create from scratch. Document.create(), Workbook.create(), and Presentation.create() produce minimal files that real Office opens with no repair prompt.
  • Content-based detection. The format is detected from the package bytes (main-part content type), never from the file extension.

Scope

All 16 modern OOXML extensions are supported for open → edit → save:

App Formats
Word .docx .docm .dotx .dotm
Excel .xlsx .xlsm .xltx .xltm .xlam
PowerPoint .pptx .pptm .potx .potm .ppsx .ppsm .ppam

Macro-enabled variants keep their vbaProject.bin as an opaque part — it is preserved bit-for-bit, never parsed.

Deliberately out of scope:

  • Legacy binary formats (.doc, .xls, .ppt) — permanently.
  • .xlsb (Excel Binary Workbook) — permanently.
  • Rendering, layout, PDF export, page counting.
  • Formula evaluation (formula parsing to a typed AST is fully supported; computing values never will be).
  • Reading/writing VBA module source (preserve-only).

The fidelity ladder

No library can promise byte-identical output for XML it has edited — attribute order, whitespace, and ZIP metadata all shift. So fidelity is defined as three concrete levels, and every conformance test says which level it checks.

Level Name Guarantee
L1 Semantic equivalence A reserialized part compares structurally equal to its source (namespace-aware, prefix-insensitive, attribute-order-insensitive). Minimum bar for every part we reserialize.
L2 Office-clean The output opens in real Word/Excel/PowerPoint with no repair prompt. Acceptance bar for create() outputs and edited files, enforced by the COM harness.
L3 Byte-stable Every part not touched since open is written byte-for-byte identical (its compressed stream is copied straight through the zip layer). Mandatory behavior of save().

Open → save with zero mutations is L3 for all parts — you can prove it yourself:

import { readFile } from 'node:fs/promises';
import { OfficeDocument } from '@ariadng/office/document';
import { comparePackages } from '@ariadng/office/preservation';

const bytes = await readFile('budget.xlsx');
const office = await OfficeDocument.open(bytes);
console.log(office.format.extension, office.family); // 'xlsx' 'excel'

// Zero mutations, so every part raw-copies at save (L3):
const out = await office.save();
const report = await comparePackages(bytes, out);
console.log(report.equal); // true — every entry byte-identical

The normative rules (L1 comparison, dirty tracking, the L3 save algorithm) live in PRESERVATION.md.

Architecture: lenses over the DOM

One rule underpins the whole library:

High-level models (Paragraph, Worksheet, Slide, …) are lenses over the parsed XML tree. They read and edit the live tree in place. They never copy it into their own data structures and rebuild the XML from those.

Consequences:

  • Unknown content survives by construction. Markup the model layer has never heard of is just a tree node nobody touches; saving reserializes the same tree.
  • Reading is free. Walking the DOM, resolving namespaces, or computing MCE reading views mutates nothing — the part stays on the byte-copy path.
  • One tree, many views. A mutation through any lens is immediately visible through the raw DOM and every other lens over the same nodes.
  • Dirtiness is a per-part bit. Every mutating lens method marks the affected part(s) dirty; save() reserializes dirty parts and raw-copies the rest.

The module layering (each is also a subpath export, e.g. @ariadng/office/opc):

zip ─── opc ─── document ─── docx / xlsx / pptx     (task-shaped lenses)
xml ─── opc, mce, preservation, document, docx, xlsx, pptx
mce ─── non-destructive MCE reading views (AlternateContent, Ignorable)
preservation ─── the L1 comparator + package-level round-trip oracle

The lens API never locks you out of the underlying XML — drop down whenever the task-shaped surface runs out, and take over the dirty flag yourself:

import { readFile, writeFile } from 'node:fs/promises';
import { Document, WML_NAMESPACE } from '@ariadng/office/docx';

const doc = await Document.open(await readFile('report.docx'));

// The live parsed w:body of /word/document.xml — not a copy.
const body = doc.body();
const pgSz = body.find(WML_NAMESPACE, 'sectPr')?.find(WML_NAMESPACE, 'pgSz');
console.log('page width (twips):', pgSz?.getAttributeNs(WML_NAMESPACE, 'w'));

// Direct DOM mutations are allowed — you then own the dirty flag:
pgSz?.setAttributeNs(WML_NAMESPACE, 'w', '12240'); // resize to US Letter
pgSz?.setAttributeNs(WML_NAMESPACE, 'h', '15840');
doc.office.markDirty(doc.office.mainPart.name);
await writeFile('report-edited.docx', await doc.save());

The full per-module behavior contract is in CONTRACTS.md.

Quickstart

Word (@ariadng/office/docx)

import { readFile, writeFile } from 'node:fs/promises';
import { Document } from '@ariadng/office/docx';

// Open → edit → save. Styles, numbering, images, themes, and unknown
// markup you don't touch round-trip byte-identically.
const doc = await Document.open(await readFile('report.docx'));

for (const p of doc.paragraphs()) {
  console.log(p.styleId() ?? '(default)', JSON.stringify(p.text()));
}

doc.addParagraph('Reviewed and approved.', { bold: true });
await writeFile('report-reviewed.docx', await doc.save());
import { writeFile } from 'node:fs/promises';
import { Document } from '@ariadng/office/docx';

// Create from scratch — opens in Word with no repair prompt.
const doc = Document.create();
doc.addParagraph('Quarterly Report');
doc.addParagraph('Everything is on track.', { italic: true });
await writeFile('fresh.docx', await doc.save());

Text extraction is MCE-aware: content inside mc:AlternateContent is read through the branch a current Office build would select, never both.

Word modeling goes well beyond plain text. format() resolves the full style cascade — docDefaults → the w:basedOn style chain → numbering → direct formatting — with correct ECMA-376 §17.7.3 toggle (XOR) semantics and theme fonts resolved at open. list() reports a paragraph's numbering:

import { readFile } from 'node:fs/promises';
import { Document } from '@ariadng/office/docx';

const doc = await Document.open(await readFile('report.docx'));

const heading = doc.paragraphs()[0];
const pf = heading.format(); // resolved paragraph facts
console.log(pf.styleName, pf.spacingBeforeTwips); // 'heading 1' 360

const rf = heading.runs()[0].format(); // resolved run facts
console.log(rf.sizePt, rf.color, rf.font); // 20 '0F4761' 'Aptos Display'

console.log(heading.list()); // null — the heading is not in a list

Tables, inline images, headers/footers, and lists all have task-shaped create/read APIs:

import { writeFile } from 'node:fs/promises';
import { Document } from '@ariadng/office/docx';

const doc = Document.create();
doc.addParagraph('Quarterly Report', { style: 'Heading1' });

// A visible table (single-line borders when no style is given):
const table = doc.addTable(2, 3);
['Region', 'Product', 'Revenue'].forEach((h, c) => table.cell(0, c).setText(h));
table.cell(1, 0).setText('North');
const row = table.addRow(); // copies the last row's cell widths
row.cells()[0].setText('South');

// Each list call is its own numbering definition (numbered lists restart at 1):
doc.addNumberedList(['Prepare', 'Execute', 'Verify']);
const bullets = doc.addBulletList(['Alpha', { text: 'Nested', level: 1 }, 'Beta']);
console.log(bullets.at(-1)?.list()?.isBullet); // true — round-trips through the resolver

// An inline image — PNG/JPEG sniffed from magic bytes, sized at 96 DPI:
const png = Uint8Array.from(
  atob('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='),
  (c) => c.charCodeAt(0),
);
const img = doc.addImage(png, { widthPx: 120, altText: 'logo' });
console.log(img.format, img.partName); // 'png' '/word/media/image1.png'

// Headers/footers are wired into every section reference for you:
doc.setHeader('ACME Corp — Confidential');
doc.setFooter('Page footer', { type: 'first' }); // sets w:titlePg

await writeFile('built.docx', await doc.save());

Excel (@ariadng/office/xlsx)

import { readFile, writeFile } from 'node:fs/promises';
import { Workbook } from '@ariadng/office/xlsx';

const wb = await Workbook.open(await readFile('budget.xlsx'));

const data = wb.sheet(0); // by index — or wb.sheet('Data') by name
if (data === undefined) throw new Error('workbook has no sheets');
console.log(data.name, data.getCell('A1'));

// Typed reads: shared/inline strings, numbers, booleans, Dates for
// date-formatted cells, cached formula results:
console.log(data.getCellInfo('B2'));

data.setCell('B2', 1234.5);
data.setCell('B3', 'paid'); // interned in the shared string table
data.setCell('B4', new Date(Date.UTC(2026, 6, 19))); // serial + date format
await writeFile('budget-edited.xlsx', await wb.save());
import { writeFile } from 'node:fs/promises';
import { Workbook } from '@ariadng/office/xlsx';

// Create from scratch — one empty 'Sheet1', minimal stylesheet.
const wb = Workbook.create();
const sheet = wb.sheet('Sheet1');
if (sheet === undefined) throw new Error('unreachable');
sheet.setCell('A1', 'Item');
sheet.setCell('B1', 'Price');
sheet.setCell('A2', 'Rice (5 kg)');
sheet.setCell('B2', 79000);

const notes = wb.addSheet('Notes');
notes.setCell('A1', 'Generated by @ariadng/office');
await writeFile('fresh.xlsx', await wb.save());

Writes match what Excel itself produces: strings go through /xl/sharedStrings.xml (deduplicated), dates get a cell format derived from the cell's current style so fills/borders/fonts survive, and rows/cells are kept in the order Excel requires.

Styling, merges, real Excel tables, defined names, formulas, and constant-memory row streaming:

import { writeFile } from 'node:fs/promises';
import { Workbook, readRows } from '@ariadng/office/xlsx';

const wb = Workbook.create();
const sheet = wb.sheet('Sheet1');
if (sheet === undefined) throw new Error('unreachable');

const rows = [
  ['Region', 'Product', 'Revenue'],
  ['North', 'Widget', 5300],
  ['South', 'Gadget', 4100],
] as const;
rows.forEach(([region, product, revenue], i) => {
  sheet.setCell(`A${i + 1}`, region);
  sheet.setCell(`B${i + 1}`, product);
  sheet.setCell(`C${i + 1}`, revenue);
});

// Styles are flat, partial writes — only what you name changes, and
// repeated styling never bloats styles.xml (everything is deduplicated).
sheet.setCellStyle('A1:C1', { bold: true, fill: '#4472C4', color: '#FFFFFF' });
sheet.setCellStyle('C2:C3', { numberFormat: '#,##0.00' });
sheet.setColumnWidth('B', 14.5);
console.log(sheet.getCellStyle('A1').font.bold); // true — fully resolved read

// A real Excel table (with autofilter) over the data:
const table = await sheet.addTable('A1:C3', { name: 'Sales' });
console.log(table.columns); // ['Region', 'Product', 'Revenue']

// Formulas are validated by a real parser before anything is written:
sheet.setFormula('C4', '=SUM(Sales[Revenue])');
console.log(sheet.getFormula('C4')); // 'SUM(Sales[Revenue])'

wb.setDefinedName('Total', 'Sheet1!$C$4');
console.log(wb.getDefinedName('Total')?.ref); // 'Sheet1!$C$4'

const bytes = await wb.save();
await writeFile('sales.xlsx', bytes);

// Read rows back without ever building the sheet DOM (million-row safe):
for await (const row of readRows(bytes, { sheet: 'Sheet1', range: 'A2:C3' })) {
  console.log(row.index, row.values); // 2 ['North', 'Widget', 5300] …
}

The formula toolkit is available standalone — parse to a typed AST, serialize back, convert between A1 and R1C1:

import { parseFormula, formulaToText, a1ToR1C1, r1c1ToA1 } from '@ariadng/office/xlsx';

const ast = parseFormula('=XLOOKUP(A2,Table1[SKU],Table1[Price])');
console.log(ast.kind); // 'call' — stored XML says _xlfn.XLOOKUP; the AST says XLOOKUP
console.log(formulaToText(ast)); // 'XLOOKUP(A2,Table1[SKU],Table1[Price])'

console.log(a1ToR1C1('SUM(B2:C2)', 'D2')); // 'SUM(RC[-2]:RC[-1])'
console.log(r1c1ToA1('SUM(RC[-2]:RC[-1])', 'D2')); // 'SUM(B2:C2)'

PowerPoint (@ariadng/office/pptx)

import { readFile, writeFile } from 'node:fs/promises';
import { Presentation } from '@ariadng/office/pptx';

const deck = await Presentation.open(await readFile('deck.pptx'));

for (const slide of deck.slides()) {
  console.log(slide.partName, slide.text()); // one string per a:p
}

deck.slides()[0].setTitle('FY26 Kickoff');
deck.addSlide(); // new slide referencing the deck's first layout
await writeFile('deck-edited.pptx', await deck.save());
import { writeFile } from 'node:fs/promises';
import { Presentation } from '@ariadng/office/pptx';

// Create from scratch: master (with text styles + title/body placeholders),
// a "Title and Content" layout, and a complete theme — all wired so
// PowerPoint opens it with no repair prompt.
const deck = Presentation.create();

// The fresh deck is immediately usable — its first slide ships title + body
// placeholders, exactly like Document.create()/Workbook.create().
const slide = deck.slides()[0];
slide.setTitle('FY26 Kickoff');
slide.placeholder('body')!.setText('Revenue up 12%\nMargins steady');

deck.addSlide().setTitle('Agenda'); // new slides get placeholders too
console.log(deck.slides().length);  // 2
await writeFile('fresh.pptx', await deck.save());

Deep slide modeling — shapes, effective (resolved) formatting, text boxes, pictures, and speaker notes:

import { readFile, writeFile } from 'node:fs/promises';
import { Presentation } from '@ariadng/office/pptx';

const deck = await Presentation.open(await readFile('deck.pptx'));
const slide = deck.slides()[0];

// Enumerate every shape with its placeholder identity and text.
for (const shape of slide.shapes()) {
  console.log(shape.name, shape.placeholder?.type ?? '(none)', shape.text());
}

// Find a placeholder (forgiving: 'title' also matches 'ctrTitle', 'body' the
// obj/subTitle family) and read what PowerPoint actually renders for it —
// resolved up slide → layout → master → theme.
const title = slide.placeholder('title');
if (title !== null) {
  const rf = title.runFormat(); // run 0 of paragraph 0
  console.log(rf.bold, rf.font, rf.sizePt, rf.color); // e.g. true 'Calibri Light' 44 '1F4E79'
  console.log(title.paragraphFormat().alignment);     // e.g. 'center'
  title.setText('Q3 Results'); // preserves the first run's a:rPr, splits '\n' into paragraphs
}

// A free-floating text box positioned in px (converted to EMU).
slide.addTextBox('Draft — do not circulate', { xPx: 40, yPx: 520, wPx: 360, hPx: 40 });

// A picture: PNG/JPEG sniffed from magic bytes; default size = intrinsic
// pixels at 96 DPI (here a 1×1 PNG scaled by an explicit width).
const png = Buffer.from(
  'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==',
  'base64',
);
const pic = slide.addPicture(png, { xPx: 600, yPx: 40, wPx: 120, altText: 'Logo' });
console.log(pic.partName, pic.format, pic.wEmu); // '/ppt/media/imageN.png' 'png' 1143000

// Speaker notes — creates the notesSlide (and a notesMaster when the deck has
// none) on first use, wired so PowerPoint opens it repair-free.
await slide.setNotes('Slow down here.\nAsk for questions.');
console.log(await slide.notes()); // 'Slow down here.\nAsk for questions.'

await writeFile('deck-p5.pptx', await deck.save());

Animations, transitions, and any content without a modeled API are untouched by these edits and preserved byte-for-byte.

Support matrix

Two tiers of support for every feature of every format:

  • Modeled — a task-shaped API reads and writes it. Mutations mark only the affected part(s) dirty.
  • Preserve-only — no API surface yet, but the content survives open → edit → save byte-for-byte (see why this is safe below). You can still reach it through the raw XML DOM (body(), root(), OfficeDocument.xml()) and own the dirty flag yourself.

Word (@ariadng/office/docx)

Modeled Preserve-only
open / create / save Style definitions authoring (no new w:style elements; existing styles are read and resolved)
Paragraph enumeration (paragraphs()), body text extraction (text(), MCE-aware) Footnotes, endnotes, comments, tracked changes, fields, content controls
addParagraph(text, { style, bold, italic }); per-paragraph text() / styleId() / runs(); per-run text() / bold() / italic() / setText() Anchored/floating image creation (existing anchored images are listed read-only; inline creation is supported)
Effective formatting: Paragraph.format() / Run.format() — full cascade (docDefaults → w:basedOn chain → numbering → direct), toggle XOR (§17.7.3), theme fonts Cell merging creation (w:gridSpan / w:vMerge), table-style authoring, w:tblStylePr folding into effective formats
Numbering: Paragraph.list() read; list creation addBulletList / addNumberedList Section insertion & page setup editing (page size/margins/columns)
Tables: tables() / addTable; Table / TableRow / TableCell (read, setText, addRow) Themes beyond font resolution, drawings/shapes/text boxes, settings, macros (vbaProject.bin)
Images: images() / addImage (inline PNG/JPEG, magic-byte sniffed); pxToEmu / emuToPx / EMU_PER_PIXEL. Headers/footers: headers() / footers() / setHeader / setFooter. Raw DOM: body(), WML_NAMESPACE

Excel (@ariadng/office/xlsx)

Modeled Preserve-only
open / create / save Formula evaluation (permanent non-goal; cached results are readable)
Sheet enumeration/lookup (sheets(), sheet(nameOrIndex)), addSheet(name) Theme & indexed colors (read as color: undefined; survive untouched unless that property is overwritten)
Typed cell reads: getCell, getCellInfo — shared/inline strings, numbers, booleans, dates (via number formats), errors, formula text + cached results Conditional formatting, data validation, comments/notes
setCell(ref, value) — string (shared-string interned), number, boolean, Date, null to clear Pivot tables, charts, rich-text run styling inside cells
Cell styling: getCellStyle (fully resolved read) / setCellStyle (flat partial writes: font, fill, border, number format, alignment — deduplicated into styles.xml) Table totals rows & table style definitions (referenced by name only)
Column widths / row heights: getColumnWidth / setColumnWidth, getRowHeight / setRowHeight Shared-formula group rewriting, array-formula creation (existing ones preserved)
Formulas: setFormula (parser-validated, calc chain handled) / getFormula (shared-formula dependents translated); AST toolkit parseFormula / formulaToText, a1ToR1C1 / r1c1ToA1 Sheet rename/remove/reorder, freeze panes, print setup, other row/column properties (hidden, outline), macros
Merged cells (merge / unmerge / merges); Excel tables (addTable / tables); defined names (definedNames / getDefinedName / setDefinedName / removeDefinedName)
Streaming row reads: readRows — iterate any sheet without building its DOM
A1/range helpers: parseCellRef / formatCellRef / parseRangeRef / formatRangeRef; 1904 date system; calcChain invalidation on formula overwrite

PowerPoint (@ariadng/office/pptx)

Modeled Preserve-only
open / create / save Animations, transitions
Slide enumeration (slides()), per-slide text() (one string per a:p, MCE-aware); addSlide({ layoutIndex | layoutPartName }) / removeSlide(index); layouts() introspection Text-run authoring beyond setText (per-run bold/color, mixed-format runs, bullets/numbering)
Shapes & text: slide.shapes(); Shape (id / name / placeholder / shapeType / hasText / text / setText); slide.placeholder(type); setTitle(text) Paragraph-property writes (alignment/indent/spacing — read-only via paragraphFormat())
Effective formatting (read): Shape.runFormat() / paragraphFormat() / transform() — resolved slide → layout → master → theme (fonts, scheme colors, sizes, alignment, EMU geometry) Comments, hyperlinks, sections, header/footer text
Text boxes: slide.addTextBox(text, opts) (explicit EMU/px geometry) Themes, masters & layout editing; SmartArt, charts, OLE objects
Pictures: slide.pictures() / slide.addPicture(bytes, opts) (PNG/JPEG, magic-byte sniffed, intrinsic-size default); unit helpers pxToEmu / emuToPx / ptToEmu / emuToPt / EMU_PER_PIXEL / EMU_PER_POINT Macros (vbaProject.bin)
Speaker notes: slide.notes() / slide.setNotes(text) (creates the notesSlide + notesMaster when the deck has none)
Raw DOM access: root(), partName / layoutPartName; PML_NAMESPACE / DRAWINGML_NAMESPACE

Why preserve-only content is safe

The fidelity ladder is what makes "no API yet" different from "unsupported":

  • L3 — untouched parts never get rewritten. Dirtiness is tracked per part; save() copies every non-dirty part's compressed bytes straight through the ZIP layer. A pivot table, animation, or macro lives in parts your edits never dirty, so it comes back byte-identical — not "re-emitted and hopefully equivalent".
  • L1 — within a dirty part, unknown markup survives by construction. Models are lenses over the parsed XML DOM: adding a paragraph appends nodes to the live tree, and everything else in that tree (unknown elements, attributes, namespaces, vendor extensions) reserializes with structural equality guaranteed by the L1 comparator.
  • L2 — real Office is the referee. The conformance suite opens every runner output in actual Word/Excel/PowerPoint via COM with auto-repair disabled; a repair prompt is a test failure.

So a file full of features this library has never heard of can be opened, edited at the cells/paragraphs/slides level, and saved — and every one of those features survives.

Repository layout: two repos, two products

This project ships as two sibling git repositories:

Repo Product
office (this repo) Product 1 — the TypeScript reference implementation: src/**, the two conformance runners (conformance/runner, conformance/runner-ts), and the library docs (CONTRACTS.md, PRESERVATION.md).
office-spec Product 2 — the language-agnostic specification (spec/ chapters), the machine-readable conformance suite (conformance/cases, conformance/corpus), the fixture generators (tools/gen-fixtures), and the real-Office L2 harness (tools/office-harness).

Everything here that needs spec-repo files (fixtures, cases, the harness) locates the spec repo through one module — src/test-support/spec-dir.ts:

  1. OFFICE_SPEC_DIR environment variable, if set, is the spec repo root;
  2. otherwise the sibling checkout <thisRepo>/../office-spec is used.

So the default setup is simply two sibling clones:

<parent>/office        this repo
<parent>/office-spec   the spec + conformance suite

Set OFFICE_SPEC_DIR only when the spec repo lives somewhere else.

Testing, conformance & the Office harness

Three layers of validation, all run from the repo root (all three need the spec repo checkout — see the layout section above):

npx tsc --noEmit          # strict typecheck
npx vitest run            # unit tests, colocated as src/**/*.test.ts
npm run conformance:cases # machine-readable conformance cases (spec 14)
npm run conformance       # round-trip matrix over the spec repo's corpus

The conformance runner exercises every package in the spec repo's conformance/corpus/** (hand-crafted minimal packages plus files generated by COM-driving real Office) through three steps:

  • A — untouched round-trip: open → save → every part must be byte-identical (L3).
  • B — trivial edit per family: add a paragraph / set a cell / add a slide → save → reopen → the edit is visible and only the documented parts differ.
  • C — L2 harness: every output is opened in real Word, Excel, and PowerPoint via COM, with auto-repair disabled so corruption fails loudly.

Step C needs Windows with Microsoft Office installed; elsewhere it is skipped automatically (or explicitly with npm run conformance -- --skip-harness). The harness (which lives in the spec repo) can also be pointed at any folder directly:

powershell -NoProfile -ExecutionPolicy Bypass -File ../office-spec/tools/office-harness/check.ps1 -Folder conformance/out

It prints a JSON report per file and exits with the number of files that failed to open clean. Corpus regeneration scripts (../office-spec/tools/gen-fixtures/) and harness details are documented in the spec repo's conformance/README.md; the runner contract is in conformance/runner/README.md.

The spec repo's T3 adversarial corpus is generated from this repo: npx tsx tools/gen-t3/generate.ts deterministically (byte-identically) rewrites ../office-spec/conformance/corpus/t3-adversarial/ using this library's ZIP writer, locating the spec repo via the same src/test-support/spec-dir.ts contract.

Every ts code block in this README is executable: scratch/readme-check.mts extracts and runs them all against the real corpus fixtures (npx tsx scratch/readme-check.mts from the repo root).

Roadmap

This repo is the TypeScript reference implementation of a larger phased plan:

  • Here today: the container core (zip, xml, opc, mce, preservation), format detection and the L3 save pipeline, a schema layer generated from the ECMA-376 XSDs (element ordering), task-shaped lenses for Word, Excel, and PowerPoint, deep Word modeling — the style-cascade and numbering resolvers (format() / list()), tables, inline images, and headers/footers — deep Excel modeling — cell styles, column/row sizing, merged cells, tables, defined names, a full formula parser (typed AST, A1↔R1C1) and constant-memory row streaming — plus fuzzing gates, all validated against a real-Office corpus.
  • Next, per the plan: Word tracked changes and section/page-setup editing; PowerPoint placeholder/property inheritance resolution and slide copy/reorder; Excel conditional formatting and data validation.
  • Post-1.0: encryption ([MS-OFFCRYPTO]), digital signatures, deeper chart/pivot/animation models.

The companion product is a language-agnostic specification plus a machine-readable conformance suite (the sibling office-spec repo: spec/ chapters and conformance/cases/) so the same behavior can be re-implemented in any language and proven against the same fixtures.

Reference documents:

Document Contents
CONTRACTS.md Normative per-module API behavior contract (this repo)
PRESERVATION.md Fidelity ladder, dirty tracking, L1 comparison rules (this repo)
../office-spec/conformance/README.md Corpus tiers, fixture provenance, harness usage (spec repo)
../office-spec/spec/ Language-agnostic spec chapters (spec repo)

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages