Commit Graph
224 Commits
Author SHA1 Message Date
Steffen Becker 784dd5e06f fix: a table aligned to the margins fills the text width
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.
2026-09-04 19:30:05 +02:00
Steffen Becker b3b8e58bd4 test: LibreOffice opens what we encrypt, and we open what it encrypts
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.
2026-09-04 19:30:04 +02:00
Steffen Becker f338aec89c feat: a comment carries the answers to it
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.
2026-09-04 19:30:02 +02:00
Steffen Becker 8c080b1d16 fix: a Word index takes the file's own entry styles
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.
2026-09-04 19:30:02 +02:00
Steffen Becker 198c16f7ab fix: a Word bibliography no longer imports twice
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.
2026-09-04 19:30:02 +02:00
Steffen Becker 060dcd7380 feat: a text box sits in the line of text
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.
2026-09-04 19:30:01 +02:00
Steffen Becker 6081a85626 feat: set where an in-flow text box sits across the column
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.
2026-09-04 19:30:00 +02:00
Steffen Becker 104ec2366c fix: suppress the base direction's own edge when reading w:jc
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).
2026-08-30 10:06:43 +00:00
Steffen Becker 35059d8126 fix: export plain text boxes as Writer text frames
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.
2026-08-30 10:06:43 +00:00
Steffen Becker 6d85a88907 fix: keep the text area at the page margins under a page border
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.
2026-08-30 10:06:43 +00:00
Steffen Becker 15049377a0 fix: cache the index rows in the DOCX TOC/INDEX/BIBLIOGRAPHY fields
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.
2026-08-30 10:06:42 +00:00
Steffen Becker 9578dea57e fix: watermark and fold marks on first/even pages, border wraps the hf band
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).
2026-08-30 10:06:42 +00:00
Steffen Becker 9a6282fce9 fix: floor a DOCX anchor's paragraph posOffset at one twip
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.
2026-08-30 10:06:42 +00:00
Steffen Becker aac93e22e8 fix: a note opens with a tab into LibreOffice's hanging pair in DOCX
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.
2026-08-30 10:06:41 +00:00
Steffen Becker 7ce97b1d50 fix: an RTL block also carries fo:text-align="end" in ODT
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.
2026-08-30 10:06:41 +00:00
Steffen Becker 3a6bc336f7 fix: schema-valid ODF and DOCX output
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.
2026-08-26 15:41:12 +00:00
Steffen Becker b0de0311a8 feat: named list styles — DOCX round-trip
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.
2026-08-25 22:19:14 +00:00
Steffen Becker e2525237de feat: named list styles — model, editor resolution, ODF round-trip
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.
2026-08-25 20:38:15 +00:00
Steffen Becker a5c072db23 feat: fold and punch marks (Falzmarken, DIN 5008)
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.
2026-08-24 16:27:27 +00:00
Steffen Becker 0753caa5a9 feat: placeholder fields (inline atom + ODF/DOCX round trip)
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.
2026-08-24 16:16:24 +00:00
Steffen Becker 847fe59d3d feat: a frame the text runs through reserves no space at all
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.
2026-08-17 13:45:39 +02:00
Steffen Becker b1b8b9de63 docs: the ODF import yardstick is the default style, and its spacing exception 2026-08-17 09:18:16 +02:00
Steffen Becker b3c52e53fc feat: an ODF paragraph reads the contextual spacing its style declares
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.
2026-08-17 09:17:33 +02:00
Steffen Becker b65920f7b7 fix: a cell or list paragraph is measured against the default style
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.
2026-08-17 09:17:05 +02:00
Steffen Becker ac5b05e0fe feat: an index the file writes without page numbers keeps its rows text-only
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.
2026-08-16 23:49:53 +02:00
Steffen Becker 8a9b391d76 fix: a heading keeps the bold its own run declares
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.
2026-08-16 23:12:34 +02:00
Steffen Becker eb3a9f95c1 feat: Word's horizontal line imports as the paragraph's rule
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.
2026-08-16 22:44:40 +02:00
Steffen Becker 4c55e9b9f5 fix: imports re-derive a registry table style's regions and unbake its text
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.
2026-08-16 13:42:24 +02:00
Steffen Becker 6789377b48 fix: DOCX carries the citation style and an all-off table look
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.
2026-08-16 13:17:47 +02:00
Steffen Becker 6a36b89c18 fix: ODT indexes and shape boxes read back clean
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.
2026-08-16 13:17:38 +02:00
Steffen Becker db3fbe32c1 fix: DOCX sections, box offsets and cell margins round clean
- 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
2026-08-16 12:50:10 +02:00
Steffen Becker d32d40be3c fix: named styles and floating boxes read clean through ODT
- 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
2026-08-16 12:50:04 +02:00
Steffen Becker 205ae72ecf fix: DOCX closes round-5 gaps — XE quotes, linked revisions, box features
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.
2026-08-16 12:07:14 +02:00
Steffen Becker afec07579a fix: a deletion's text keeps its formatting through ODT
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.
2026-08-16 12:07:07 +02:00
Steffen Becker 8e6616c562 feat: resolved comments travel through DOCX (commentsExtended.xml)
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.
2026-08-16 11:22:21 +02:00
Steffen Becker 1264f9742c fix: dropping a fully covered table row shrinks the spans across it
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.
2026-08-16 11:21:13 +02:00
Steffen Becker 044c59d895 feat: custom note marks travel through DOCX (w:customMarkFollows)
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.
2026-08-16 10:30:00 +02:00
Steffen Becker 97f9016369 fix: a note's stock body style no longer imports as direct formatting
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.
2026-08-16 07:35:42 +02:00
Steffen Becker 273c533fce fix: open a file whose extension names the wrong format
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.
2026-08-16 03:00:27 +02:00
Steffen Becker 76856f4aff fix: DOCX cell spacing accretion and nested-list kind flips
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.
2026-08-15 22:58:29 +02:00
Steffen Becker ad2a01ba75 feat: recording revisions belongs to the document, not the editor
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.
2026-08-15 17:07:06 +02:00
Steffen Becker 503c9821d6 feat: a freeform drawing keeps its own outline
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.
2026-08-15 12:09:12 +02:00
Steffen Becker 9be1578b97 feat: a formula cell prints a currency and a date too
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.
2026-08-15 11:44:43 +02:00
Steffen Becker c800618299 feat: a text box can run its text top-to-bottom
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.
2026-08-15 11:13:46 +02:00
Steffen Becker eda134f162 feat: a formula cell prints its result in its own number format
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.
2026-08-15 10:53:30 +02:00
Steffen Becker 20e6dcad0e feat: a reading can be printed over its base text
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.
2026-08-15 10:40:52 +02:00
Steffen Becker 24402dd372 feat: the bibliography gets a citation style
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.
2026-08-15 10:02:54 +02:00
Steffen Becker 5f2e7fcbcf feat: a section can restart the page numbering
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.
2026-08-15 09:49:20 +02:00
Steffen Becker 147034730f feat: headings go to level 10, as both word processors do
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.
2026-08-15 01:21:23 +02:00
Steffen Becker 4afd5cd1d5 feat: a paragraph can opt out of automatic hyphenation
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.
2026-08-15 01:09:55 +02:00