ODF's table:align="margins" means the table spans between its own margins, and
style:width is then the width the producing page had. Read as a left-aligned
table, the undeclared right margin was derived from that width against ours and
the table came out narrow — measured 12.4cm on a 14.7cm text width, its column
rule 10.4mm left of where LibreOffice draws it.
Five legs in tests/lo-password.test.ts, driven through the repo's first UNO
helper because --convert-to cannot pass a password; self-skips without soffice
or python3-uno and runs with npm run test:lo. The probed format details live in
docs/architecture/encryption.md.
The thread rides the comment mark's attrs, so two runs of one comment still
merge back together and no second store is needed. ODF writes each answer as
its own annotation over the same range, naming its comment in
loext:parent-name; Word writes its own w:comment tied by w15:paraIdParent in
commentsExtended.xml (both probed). Either importer folds the answers back
into the comment they belong to, and the printed list carries them indented.
A TOC field regenerates its rows from the styles named `toc 1`…`toc 9`, and its
cached rows are skipped on import — so those styles were never seen and the index
fell back to the editor's 0.5cm per level, dropping the file's space-after with it.
Measured on a 60-row index: a 5.1mm row pitch against LibreOffice's 6.9mm.
They now land as Contents 1…10, and the DOCX export writes the rows back under
them instead of the fallback indent.
Word caches a BIBLIOGRAPHY field's rows as a w:tbl inside the field, and
only the field's paragraphs were skipped — so the list arrived once as the
regenerated index node and once as that table. Measured on a 49-page
document: two pages of duplicate. StyleName IEEE now reads as the numbered
style it is, so its citations show [1] instead of the source key.
textBox becomes an inline node carrying block content, like the frame both
formats write, so "in line with text" really is in the line and a box reaches
a table cell or a list item through their paragraphs. Both exports write it
where it sits (an as-char draw:frame / a wp:inline drawing) and both imports
leave it there, which drops the hoisting machinery on either side.
Two guards the schema cannot express: no box inside a box, and a selection
reaching into a box grows to the box's bounds rather than dissolving it.
wrapAlign only ever came from a file, so a box inserted here stayed at
the left in both modes that leave it in flow. The floating toolbar now
offers left/centre/right for `inline` and `topBottom` (a side wrap names
the side itself) and drops a coordinate placement when one is picked.
DOCX carried neither: `topBottom` now writes wp:align and an as-char box
the w:jc of its rebuilt marker paragraph, both read back on import. ODF
already wrote both. Measured centred at 297px of a 76…718px column, and
rendered through LibreOffice from .odt and .docx alike.
The DOCX import dropped left as the default and kept right regardless
of w:bidi; an rtl paragraph's default edge is right. start/end read as
physical left/right — LibreOffice spells an rtl paragraph's physical
left as w:jc="start" (probed via its own DOCX export).
Without a parent chain to the Frame style LibreOffice read the box as a
drawing object, whose text loses lists, nested images and borders. The
frame takes fo:background-color/fo:border and its writing mode in the
graphic properties; an inline box's centring rides the anchor paragraph.
The import reads the frame spellings back and drops LO's from-top/0
respelling of the default alignment.
ODF lays a page border and its padding out inside the margin, so the
bordered ODT rendered every line narrower than the editor and Word
(offsetFrom="text") and paginated a page long. The export shrinks the
fo:margin-* by border + padding, the import grows them back.
The three index kinds exported as fields with an empty result; Word for
Mac never updates fields on open, so every index rendered as nothing.
They are now complex fields whose result is the editor's cached entries
(indent per level, leader tab, pages), the form Word itself writes; the
importer recognizes the complex INDEX/BIBLIOGRAPHY form and keeps the
attrs the fldSimple path set.
The decor shapes now ride every header variant: DOCX gets an empty
w:type=first/even header part for the post-passes to fill, ODT the
same splice into style:header-first/-left. The editor's page border
now starts at the zone's edge distance where a header/footer exists —
both word processors draw the band inside the border (probed).
Anchored exactly on the paragraph top (posOffset 0), LibreOffice's wrap
layout derails and paints a neighbouring inline text box's content
outside its shape; LO itself never writes less than a twip there.
Probed by bisecting a broken box against LO's own DOCX export of the
same document. The importer rounds the sub-visible offset back to none,
so nothing accretes on the round trip.
FootnoteText/EndnoteText ride the factory slots with the probed 0.6cm
start/hanging pair, and the note body puts a tab run after the marker,
matching LibreOffice's own DOCX export; Word no longer glues the number
to the text. The importer strips the tab (also behind a custom mark on
a run of its own) and counts an equal start/hanging pair as the stock
note look, so neither a styleName nor an indent accretes on re-import.
style:writing-mode alone leaves the paragraph left-aligned in
LibreOffice; the mode+end pair is what it writes itself (probed), so
both export paths add it where the block sets no alignment of its own,
and the importer suppresses end/right against an RTL block's default —
the pair is its normal look, not direct formatting. The render-
consistency leg now compares under spacing model 'max', the one both
formats express; thresholds re-measured.
ODF: every part declares 1.3 (the version LibreOffice writes; header-first
is a 1.3 element), draw:image gains the mandatory xlink:type trio,
text:time-value becomes an xsd dateTime (import still reads the legacy
duration), index-entry-link-start/-end stay in TOC templates only, and
dont-balance-text-columns moves onto style:section-properties (probed: LO
reads and re-writes it there). DOCX: a final pass re-sorts w:settings into
the CT_Settings sequence, and comments.xml declares w14 mc:Ignorable.
One shared abstract per style (each list its own w:num instance, so numbering
restarts); a post-pack pass injects w:styleLink and the numbering style's real
numId, which the docx library cannot express. Import follows w:styleLink and
w:numStyleLink — a linked abstract used to resolve to {} and silently import
as a bullet list — maps the style onto listStyleName + sheet.list, and derives
no per-level attrs. Probed: LO ignores DOCX numbering styles both ways (mints
WWNum# on import, flattens to the look on export); ODF carries the identity.
Fourth style family (LibreOffice's Listenformatvorlagen): per-level model in
styles/listStyles.ts, five probed LO built-ins, listStyleName on the outermost
list, one effective-level resolver shared by the decoration walk and the export.
ODF writes the full named text:list-style into styles.xml and repoints
unoverridden lists at it (overrides keep the resolved automatic clone);
import maps named list styles back to the attr + registry, attr-free.
Probed: LO keeps the named reference and display-name across a re-save.
edentext-fold-marks flag: FoldMarkLayer draws the 87/192mm folds and the
148.5mm punch mark per page inside .paper, so print and PDF include them.
Toggled in the Layout tab. Exported as named EdenFoldMark* lines in the
master page's header (ODF page-relative draw:line, DOCX VML v:line);
both importers read the names back into the flag. Roundtrip Leg 32.
An inline atom drawn gray as ‹label›: a click selects the whole field,
typing replaces it, Tab/Shift-Tab jump between fields. Exports to ODF
<text:placeholder> (PLH sentinel U+E022) and a DOCX w:sdt content
control tagged edentext-placeholder; both importers read it back, the
export-minted gray stripped. Covered in roundtrip Leg 31 + fuzzDoc.
Word's in-front-of / behind-text (wp:wrapNone + behindDoc) and ODF's
style:wrap="run-through" both read as a side float, so a near-column-wide
picture reserved its whole height and pushed the text 23-52mm down. They
are a wrap mode of their own now: out of flow at the static position it
was anchored at, z-order from the file (inFront). Both exports carry it
back; tests/unit/wrap-through.test.ts covers the round trip.
The image fixture 72 -> 38 over both formats and 7 pages -> 6, matching
the reference; corpus 1108 -> 1074, no other fixture moved.
style:contextual-spacing drops a paragraph's own spacing towards a neighbour of
the same style; LibreOffice's List styles carry it, and only the docx path read
its w:contextualSpacing counterpart. Unread, every list point kept the 8pt its
style declares and a long document accumulated a page every dozen items.
The editor has no such mode, so the suppressed side becomes an explicit 0. In a
list each item wraps its own paragraph, so the neighbour is the next item's.
The ODF importer's yardstick for a block that can carry no style name was the
editor's own fallback, not the default paragraph style — which is what
`p:not([data-style])` re-applies. So a cell paragraph overriding its 115% line
height back to 100% was dropped as "same as the default" and rendered at 115%
anyway: 0.7mm too much on every table row. Its spacing is the one exception,
zeroed by a rule that outranks the style's, for a paragraph but not a heading.
Two fixtures' table rows: pitch 5.8mm against LibreOffice's 5.1, then 5.1.
Word's TOC \n and an ODF entry template naming no index-entry-page-number both
say the rows are text alone; we drew leaders and live numbers over them either
way. Carried as the node's own pageNumbers attr, off only — the node view had
the page-less row already, for the bibliography. The exports write the switch
back out: \n over the whole level range, and the ODF template drops the tab
stop with the number, which is the pair LibreOffice's dialog switches together.
Both importers drop a mark the block's style supplies, and a heading's bold was
taken as given. The registry style that renders it is the file's, though —
re-parented to Standard wherever the file declares a basedOn / parent-style-name
— so nothing supplied what the mark had been deleted for, and a 23pt title line
came out 149.7mm against LibreOffice's 161.7. Gated on that parent now; a
LibreOffice file, which writes fo:font-weight on every Heading_20_N, is
untouched. Parity corpus 1892 → 1889.
A v:rect flagged o:hr is the line itself, not a shape: its height and
fillcolor become the paragraph's borderBottom (1.5pt/#A0A0A0 where the file
names none), a real w:pBdr keeping precedence — only one line can be drawn.
It was dropped with the "Drawings were removed" warning until now, which the
changelog's gap list did not admit to either.
Both importers resolve the table's style name + look + grid position through
resolveTableCell: the region attr comes back on each cell, a bold region's
runs convert like headings (boldByDefault) and a run matching the region's
color loses that mark — the style's own text no longer lands as direct
formatting on reopen. A DOCX file carrying its own w:tblStylePr keeps the
baked areas instead; a style name not in the registry stays untouched.
The b:Sources part is also the citation style's only carrier, so a document
whose bibliography index sets a non-default style writes it even with no
source cited. The importer keeps a declared all-off w:tblLook ('') instead
of dropping it back to the default look.
The TOC importer no longer fabricates settings our own export mints: a right
stop at the column's end is the attr's default (null), and an entry template
resolving to the default style names no levelStyles entry. A custom-shape box
keeps its vertical text (the writing-mode read only ran for plain frames),
and a declared all-off tableLook ('') survives instead of reverting to the
default look.
- a continuous break that only encodes a columns region no longer
fabricates a section break or its own header/footer set
- a topBottom text box keeps its x offset (posOffset now written)
- cell margins read back in hundredth-cm, so twips don't show through
- a table margin's zero side stays the default instead of -0
- what a block's named style supplies (Title's centring, keep-with-next,
its size as a raised run's percent reference) is direct formatting no more
- a topBottom box keeps its left place instead of gaining a fabricated
centring; horizontal-pos right reads back as the box's wrapAlign
- a table margin's zero side stays the default instead of an explicit 0
An XE field's literal quote is backslash-escaped (Word's own escaping;
import already unescaped it) instead of flattened to an apostrophe. A
w:ins/w:del inside w:hyperlink imports (shared handleRevision closure).
Text boxes carry charStyle (w:rStyle), index entries, citations,
date/time fields and revision marks (w:delText for deletions);
dateTimeFieldParts/xeInstr are shared with the body path.
The deleted runs are hoisted through every serialization pass and spliced
into the tracked-changes registry as styled runs (as LibreOffice stores
them); import converts them back as real inline content. One point marker
per change instead of one per fragment. A deletion's marker also no longer
makes a formula beside it display (aloneInParagraph counts text:change).
applyComments now declares dc:/loext: only when absent — beside a revision
the duplicate xmlns:dc made content.xml invalid.
Word keeps a comment's resolved state as w15:done in
word/commentsExtended.xml, keyed by the w14:paraId of the comment
body's last paragraph. The docx package knows neither part, so a
post-pack pass stamps the paraId, mints the part with its content
type and relationship, and the importer reads it back.
A row holding only covered cells cannot exist in the editor and was
dropped on ODT import, but every rowspan reaching over it kept its
full count, leaving an inconsistent grid. Spans now shrink by the
dropped rows they covered.
The docx package cannot express a custom footnote/endnote mark, so the anchor
always went out as an automatic reference and the label was lost — and the
importer never read one either, so a Word file's custom mark leaked into the
note's body text. A post-pack pass now rewrites the anchor (flag + literal
run) and the note part's <w:footnoteRef/>; the importer reads the flag, takes
the run's text as the label and strips its repeat off the note body.
Both importers stamped the exporter's own note style back onto the note
(styleName "Footnote" from ODT, "footnote text" from DOCX), so an
editor-authored document accreted a foreign attr on every round trip and the
two formats disagreed. A style that only re-states the stock note look is now
suppressed like any other default-equal value; a customized one still lands.
Which importer ran was decided by the extension alone, so a .docx saved or
renamed as .odt (and the reverse) was reported as a broken file. A failed
read is retried as the other format; a file that is neither still reports
the error for the extension it carries.
Found by the cross-format fuzz leg. importDocx baked the resolved cell
chain in as direct formatting even where it equals what an unset attr
already renders (0/0 spacing; single line height over a single default) —
every re-imported own file grew spacing attrs on every cell paragraph.
And a numbering level kept the first list kind it saw, so a nested bullet
list beside a nested ordered one came back ordered; a kind conflict now
forks its own numbering reference, as the ODT side mints a list style.
A new document starts with it off and an import adopts the file's own
flag, so an editor left recording no longer marks the next document it
opens. Both formats carry it, probed against soffice: ODF on the change
registry (text:track-changes, written for the flag alone where nothing
is recorded yet), Word as w:trackRevisions in settings.xml — LibreOffice
writes and reads exactly those two, and reading w:trackChanges instead
would have found nothing.
A polygon, a polyline, a curve or a connector's elbow used to be dropped
with a warning; it now rides the box as an SVG path in the 0…100 box a
preset's points use, and the editor draws it. Read from all four dialects
the two products write (ODF points and svg:d, a non-primitive
enhanced-path, DrawingML custGeom, VML's reversed-case path), written
back as enhanced-path and custGeom — LibreOffice renders both exactly as
the editor draws them. An open outline is stroked, never filled.
Both come from the document's language, read off Intl: the symbol's side
and spacing, the short date's order and padding. Each minted data style
names that language — without it LibreOffice prints its own locale's
separators (probed: a German document's 1.234,00 € came out $1,234.00).
A date is a serial day from LibreOffice's day 0, 1899-12-30. Word's
formula dialog offers no date, so the DOCX leg uses the field's \@
picture. Also drops a document's name from three older comments.
Word's Text Direction and LibreOffice's, on the frame's own toolbar: the
browser lays the vertical flow out, so the box needs nothing but the mode.
ODF keeps it in the frame style's *paragraph* properties — probed: in the
graphic properties LibreOffice drops it, there it keeps it and lays the box
out — and Word in the shape body's `vert`, the VML fallback's
`layout-flow:vertical` read too. Both survive a LibreOffice re-save.
The Formula dialog gains the number format Word's has: whole numbers, two
decimals, grouped thousands and the two percentages beside the general
one.
ODF keeps it on the cell *style* as a reference to a data style — odf-kit
shares one cell style across every cell, so a formatted cell gets a copy
of it — and LibreOffice writes both back unchanged. Word keeps it on the
field as its \# switch; LibreOffice's DOCX filter neither reads nor writes
that switch, so that leg holds through our own round trip.
Ruby, as both products' phonetic guide writes it: one element holding a
base and its reading, so the editor keeps it as one inline atom and the
browser sets it as a real <ruby>.
ODF gets <text:ruby> plus a minted ruby-family style (centred, above —
probed: LibreOffice writes that style back unchanged); Word a <w:ruby> run
from a post-pack pass, the package having none. Both legs verified through
a LibreOffice re-save, our .docx to .odt included.
Cite by short name (LibreOffice's own), numbered, APA, MLA or Chicago:
the style sets the citation's label and the row's shape, and rides the
bibliography index node — ODF as the entry template LibreOffice
regenerates its rows from (verified through a re-save), Word as the
sources part's StyleName. Labels are derived and kept true by an
appendTransaction, so adding a citation renumbers the rest.
Word's Start at, per section: ODF writes style:page-number on the
paragraph that switches master page, DOCX a w:pgNumType w:start. That
section is no longer exported as a continuous break — probed, LibreOffice
drops the start off a continuous one, and neither word processor honours
it there. The footer counts from the nearest restart at or above the page.
HTML stops at h6, so 7-10 render as unknown elements editor.css makes
blocks; their built-in styles continue level 6 at 12pt, since LibreOffice
writes these styles with no properties and resolves them from its own
pool. Both importers read a two-digit level again (the style-name regexes
stopped at one digit). Verified through a LibreOffice re-save of both
formats and in the live editor.
LibreOffice's Format > Paragraph > Text Flow checkbox. Only "off"
travels (Word has no per-paragraph "on") and only under a document that
hyphenates, or it would say what is already true. ODF writes
fo:hyphenate="false" into the paragraph's text properties, DOCX gains
w:suppressAutoHyphens through a post-pack pass — the docx package
exposes no option for it. Both survive a LibreOffice re-save.