Skip to content

feat(doc-codec): add MS-DOC structure writer (partial write support) - #876

Merged
Mearman merged 4 commits into
mainfrom
worktree-agent-a4a5c5b3fc9f3a3f3
Sep 3, 2026
Merged

feat(doc-codec): add MS-DOC structure writer (partial write support)#876
Mearman merged 4 commits into
mainfrom
worktree-agent-a4a5c5b3fc9f3a3f3

Conversation

@Mearman

@Mearman Mearman commented Sep 3, 2026

Copy link
Copy Markdown
Member

Summary

Adds writeDocContent to doc-codec: a genuine, spec-grounded [MS-DOC] writer for a single-section wordprocessing ContentDocument, producing real Word Binary File bytes wrapped in a real [MS-CFB] compound file via archive-codec's writeCompoundFile. This is progress on #816 but does not claim full read+write parity — see Scope below.

What's written, all inverting real structures the package's own reader (readDocContent) already consumes:

  • A piece table (Clx), always one uncompressed 16-bit piece.
  • Sprm-encoded ChpxFkp/PapxFkp formatting exceptions, packed and split across as many 512-byte pages as the content needs (not just the common one-page case).
  • A spec-conformant style sheet carrying zero styles (FibRgFcLcb97.lcbStshf "MUST be a nonzero value").
  • A font table (SttbfFfn/FFN) when a run names a font — this needed a small reader-side addition too (sprmCRgFtc0 resolution into ContentRun.fontFamily), since round-tripping a font needed the reader to understand the same table the writer produces.

Character formatting: bold, italic, strike, underline, sizePt, color (exact sprmCCv COLORREF, not the lossy 17-entry sprmCIco palette), fontFamily. Paragraph formatting: alignment, indentLeftPt, indentFirstLinePt, spacingBeforePt, spacingAfterPt, lineSpacing (multiplier form), pageBreakBefore.

Scope — what this PR does NOT cover

Refused with DocUnsupportedError rather than approximated:

  • More than one section (the reader itself never distinguishes more than one anyway).
  • Any non-paragraph block: tables, images, page breaks, embedded objects, construct markers.
  • Paragraph styles (styleId/headingLevel are not written; every paragraph is istd 0).
  • Numbering/lists, hyperlinks, fields, footnotes/headers/comments, section geometry, metadata, encryption — none of these are written, matching what the reader already doesn't support.
  • ContentParagraph's right-margin indent doesn't exist as a schema field at all (a pre-existing gap, not new to this PR).
  • FIB fields beyond what this package's own reader consults (SttbfAssoc, Dop, printer-driver structures, ~140 others) are left zero — conformant for this package's own round trip, not a certification that every third-party reader accepts the result.

Full details and rationale are in packages/doc-codec/README.md's new "Writing" section and "Not built, and not approximated" table.

Verification

  1. Round-trip through this package's own reader (src/write.test.ts, 18 tests): every supported property, an empty section, a paragraph with no runs, non-Latin-1/non-BMP text, and forced multi-page ChpxFkp/PapxFkp splitting (150 distinctly-formatted runs, 60 distinctly-indented paragraphs).
  2. Independent real-world validation: a writeDocContent sample exercising every supported property was opened, rendered, and re-exported by LibreOffice (soffice --headless --convert-to txt and --convert-to pdf) without error or content loss — bold/italic/underline/strike/colour/font-family runs, centred alignment, indentation, and Unicode (café, 中文, an emoji surrogate pair) all rendered correctly in the PDF output. This is a real, independent [MS-DOC] implementation, not this package's own reader.
  3. pnpm exec turbo run _lint _typecheck _test _test:workers --filter=doc-codec — all green (127 unit tests, 4 workerd tests).
  4. Full-workspace pnpm exec turbo run _lint _typecheck — all 51 tasks green.

Test plan

  • pnpm exec turbo run _lint _typecheck _test _test:workers --filter=doc-codec
  • Full-workspace pnpm exec turbo run _lint _typecheck
  • Manual LibreOffice round trip (soffice --headless --convert-to pdf) on a sample exercising every writer feature
  • CI green (pending)

@Mearman
Mearman marked this pull request as ready for review September 3, 2026 12:13
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review Completed 2026-09-03T12:21:37.661610Z 3516232 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Implements writeDocContent, taking a single-section wordprocessing
ContentDocument and producing real Word Binary File bytes wrapped in
a real MS-CFB compound file: a piece table (always one uncompressed
16-bit piece, sidestepping the compressed-character mapping table
entirely), Sprm-encoded grpprls for direct character and paragraph
formatting, ChpxFkp/PapxFkp pages that split across as many 512-byte
pages as the content needs, a spec-conformant style sheet carrying
zero styles (FibRgFcLcb97.lcbStshf must be nonzero even though this
package's own reader tolerates a missing one), and a font table
(SttbfFfn/FFN) when a run names a font.

Character formatting: bold, italic, strike, underline, sizePt, color
(via sprmCCv's exact COLORREF rather than the lossy Ico palette), and
fontFamily. Paragraph formatting: alignment, indentLeftPt,
indentFirstLinePt, spacingBeforePt, spacingAfterPt, lineSpacing
(multiplier form), and pageBreakBefore.

The font table is new on the read side too: chp.ts now resolves
sprmCRgFtc0 through a document's own SttbfFfn into ContentRun's
fontFamily field, since round-tripping a run's font needed the reader
to understand the same structure the writer produces.

Refuses, rather than approximates, a document with more than one
section, a non-paragraph block (table, image, embedded object,
construct marker), and any property this package's own reader does
not itself convert.
Round-trips every property the writer supports through readDocContent:
plain and formatted runs, every paragraph property, an empty section,
a paragraph with no runs, non-Latin-1 and non-BMP text, and enough
distinct formatting exceptions and paragraphs to force ChpxFkp/PapxFkp
page-splitting rather than exercising only the common one-page case.
Also asserts writeDocContent refuses a non-wordprocessing document, a
multi-section document, and an unsupported block kind.

Adds a workers-runtime write+read round trip alongside the existing
read-only workers test, so the writer's own Worker-isomorphism is a
runtime-checked fact rather than an assertion, matching the reader.
Updates the Status section's read-only claim, adds a Writing section
covering exactly what writeDocContent converts and what it refuses,
documents the shared font-table module (SttbfFfn/FFN) and why the
writer always emits uncompressed 16-bit text, extends the module and
specification-citation tables, and adds a writeDocContent usage
example alongside the existing read one.
lint-staged's prettier pass wasn't run before the writer PR's last
commit, leaving the "Not built, and not approximated" table's column
widths out of step with its own content.
@Mearman
Mearman force-pushed the worktree-agent-a4a5c5b3fc9f3a3f3 branch from 3516232 to b57c3df Compare September 3, 2026 12:16
@Mearman
Mearman merged commit e97219f into main Sep 3, 2026
17 checks passed
@Mearman
Mearman deleted the worktree-agent-a4a5c5b3fc9f3a3f3 branch September 3, 2026 12:19
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.1.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant