Moving it out of the build left it with no way in. Settings can only install from the community catalogue, so a first-party extension that isn't bundled has a working update channel and no first copy for anyone to update — the mechanism was all there and the front door was missing. So it goes back beside screenshot, aegis and pdf-editor: seeded into every profile by the build, listed under "Built into Theseus", and kept current between releases by the operator-signed channel at theseus.x/extensions/docx-editor/. That is the arrangement docs/ADDON-UPDATES.md describes, and the one the signing script was written for. About 400 KB compressed in the installer, most of it the vendored editor libraries — next to the ~4 MB of pdf.js that pdf-editor already ships, the weight argument for keeping it out didn't survive contact with the numbers. The end-to-end driver goes back to checking that a fresh profile seeds it, which is the property that actually matters now.
121 lines
6.9 KiB
Markdown
121 lines
6.9 KiB
Markdown
# What survives a round trip
|
||
|
||
The editor rebuilds a document's body from what you see on screen and carries
|
||
the rest of the original package across. This is the ledger of what that costs.
|
||
|
||
Measured over 66 real-world Word documents found on a working machine (CVs,
|
||
contracts, invoices, forms, letters — Greek, Russian, German and English):
|
||
|
||
clean round-trip : 65
|
||
content drift : 1 (a 7 MB WMF picture, see "Dropped")
|
||
invalid package : 0
|
||
threw : 0
|
||
|
||
"Clean" means: read the file, save it, read it again, and the two editor
|
||
documents are identical — same blocks, same attributes, same marks on the same
|
||
text. The saved package is also checked for well-formed XML, no dangling
|
||
relationship ids and no relationships pointing at parts that aren't there.
|
||
|
||
Reproduce with:
|
||
|
||
cd addon-build/docx-editor
|
||
npm install && npm run build
|
||
node test/roundtrip.mjs # the built-in fixture
|
||
node test/corpus.mjs <a folder of .docx> # a real corpus
|
||
|
||
## How the two halves work
|
||
|
||
Reading uses [mammoth](https://github.com/mwilliamson/mammoth.js), but not its
|
||
HTML. mammoth's converter is deliberately semantic, and HTML has nowhere to put
|
||
a run's colour or a paragraph's line spacing, so it drops them. We take its
|
||
parsed *document model* instead, through the public `transformDocument` hook,
|
||
and walk that into the editor's model. See `addon-build/docx-editor/patches.mjs`
|
||
for the six properties we taught that model to carry: run colour, paragraph
|
||
spacing, paragraph bottom border, image display size, numbering format and
|
||
numbering id.
|
||
|
||
Writing uses [docx](https://www.npmjs.com/package/docx), which always builds a
|
||
brand-new package. Anything living outside the document body would therefore
|
||
vanish, so `lib/pkg.js` grafts it back: headers, footers, footnotes, endnotes,
|
||
the style catalogue, the theme and the page setup, re-wiring relationship ids
|
||
and content types as it goes.
|
||
|
||
## Kept
|
||
|
||
| | How |
|
||
|---|---|
|
||
| Headers and footers | The parts are copied across with their own relationships and images, and re-referenced from the new `sectPr`. |
|
||
| Footnotes and endnotes | The markers survive in the body as their own node; `footnotes.xml` is copied wholesale so the ids still match. |
|
||
| Page size, orientation, margins, gutter, title page | Read off the first `sectPr` and handed to the builder. |
|
||
| The document's styles | The original `styles.xml` is merged over the builder's. Where both define a style id the original wins — it is what the document actually looked like. `docDefaults` comes across too, so unstyled paragraphs don't shift. |
|
||
| Theme, fonts | `theme1.xml` is copied. |
|
||
| Title, author, subject, keywords | From `docProps/core.xml`. |
|
||
| Bold, italic, underline, strike, super/subscript, all-caps, small-caps | |
|
||
| Font family, size, colour, highlight | Highlight is Word's 15-value enum, not a hex colour, so it passes through exactly. |
|
||
| Alignment, indent, line spacing, space before/after | |
|
||
| Headings 1–6, quotes, code blocks | Code blocks ride on a `SourceCode` paragraph style. |
|
||
| Bulleted and numbered lists, nested, with their numbering format | Format is kept per level, so a list that is decimal at the top and lettered underneath stays that way. |
|
||
| Where one list ends and the next begins | Tracked by Word's `numId`, so a second list still restarts at 1. |
|
||
| Tables, including merged cells | Both directions. A 12-row vertical merge comes back as a 12-row vertical merge. |
|
||
| Images | At the size Word was displaying them, to EMU precision, not the file's natural size. |
|
||
| Links, internal anchors | |
|
||
| Page breaks, horizontal rules | A rule is Word's empty paragraph with a bottom border, and is written back as one. |
|
||
|
||
## Dropped
|
||
|
||
These are detected when the file opens and named in a banner before any
|
||
editing, and again in the About dialog. The original file on disk is never
|
||
overwritten — a save downloads `<name>-edited.docx`.
|
||
|
||
- **Tracked changes.** mammoth renders insertions as ordinary text and drops
|
||
deletions, so a save would silently accept every pending revision. A document
|
||
with them opens read-only until you explicitly choose "Accept all and edit".
|
||
- **Comments.** Same gate as tracked changes.
|
||
- **Equations** (OMML), **shapes, text boxes and WordArt**, **content controls**.
|
||
- **Fields** — page numbers, tables of contents, cross-references. The text Word
|
||
last calculated is kept; the field code that would recalculate it is not.
|
||
- **Bookmarks.**
|
||
- **Section breaks and multi-column layout.** Only the first section's page
|
||
setup is kept.
|
||
- **Metafile pictures (WMF/EMF).** Word's vector picture format: browsers can't
|
||
display it and the builder can't write it. This is the single drift in the
|
||
corpus above — one CV with a 7 MB WMF.
|
||
|
||
## Kept, but not exactly
|
||
|
||
- **Paragraph borders and shading.** Only the rule under an empty paragraph
|
||
round-trips. A box around a paragraph, or a shaded paragraph, is lost.
|
||
- **Custom tab stops.** Tab characters are kept; the stop positions are not.
|
||
- **Exact line spacing.** `atLeast` and `exact` line rules are read but the
|
||
editor has no control for them, so they are written back as-is only when the
|
||
paragraph is untouched.
|
||
- **Table borders.** mammoth doesn't report the borders it read, so every table
|
||
is written with a plain single-line border. A borderless table gains lines.
|
||
- **List indentation depth.** A list Word started at level 2 with no level 0 or
|
||
1 above it becomes a top-level list, and is written at level 0.
|
||
- **An empty paragraph after a table inside a cell.** OOXML forbids a cell that
|
||
ends with a table, so every such document carries a paragraph the author
|
||
never typed. It is dropped on read and put back on write.
|
||
|
||
## Decisions worth knowing about
|
||
|
||
**Why not preserve the dropped features as raw XML?** `docx` can embed raw
|
||
OOXML (`ImportedXmlComponent`), so it is technically possible. What makes it
|
||
expensive is position: mammoth silently discards the elements it can't model
|
||
and reports no location for them, so anchoring a passthrough node in the right
|
||
place needs a second OOXML reader running alongside mammoth purely to recover
|
||
block order. That is a large amount of machinery whose failure mode is a subtly
|
||
corrupt package, which is worse than an honest warning. The grafting approach
|
||
gets headers, footers, notes, page setup and styles — the things most real
|
||
documents actually have — without that risk.
|
||
|
||
**Why patch mammoth instead of using it as shipped?** Colour, line spacing and
|
||
numbering format are all editable in this editor's ribbon. Shipping without the
|
||
patches would mean the editor shows a control for something it silently eats on
|
||
the next save. The patches are six string replacements applied at build time
|
||
and each one asserts its anchor, so a mammoth upgrade that moves the code fails
|
||
the build instead of quietly shipping a lossy reader.
|
||
|
||
**Why is the original file never overwritten?** Because of everything on the
|
||
"Dropped" list. A save is a download of a new file, so the original is always
|
||
still there to fall back on.
|