122 lines
6.9 KiB
Markdown
122 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.
|