Commit Graph
238 Commits
Author SHA1 Message Date
Steffen Becker 86cacc40b1 fix: the space below a zone's last paragraph is no part of its band
Probed: a header of one 10pt line with a 12mm bottom margin puts the body at the
band's min-height, not 12mm lower, and a footer's trailing margin is dropped the
same way — while a margin between two of the zone's paragraphs counts in full
(two lines 12mm apart give a 20.16mm band). The collapsed zone paragraph carries
only the margins between its source paragraphs now. One 16-page fixture went
183 reports → 5, its body having sat 10.3mm low on every page.
2026-09-04 19:30:09 +02:00
Steffen Becker 0c1eb2626c fix: a section's zones sit at the distance its own page setup gives them
The edge→zone distance was document-wide, so an index whose header starts 4cm
down had its body open 14.5mm too high and the page under it followed. HfSet
carries the section's own pair (and its first page's, where the page style hands
over): ODF's page-layout margin is that distance, Word's is w:pgMar
w:header/w:footer, and both exports write it back.
2026-09-04 19:30:08 +02:00
Steffen Becker 8b6e9fdfb3 fix: a zone of nothing but empty paragraphs is still that many lines tall
The zone collapses its source paragraphs into one, and a break was pushed only
where something had already come out — so a header of three empty paragraphs
came out empty and reserved a single line. The breaks are counted from the
paragraphs now, and the strut takes the style's own size where no run can say it.
2026-09-04 19:30:08 +02:00
Steffen Becker 34d5d6014d fix: an imported page margin is the one the file declares
Both importers capped every side at 10cm, which is the Layout panel's limit and
no file's: a back cover pushing its last lines to the page foot with a 21cm top
margin rendered 11cm high. fitMargins caps only what would leave the page no
strip of text at all, and the zone distances follow the page's own height.
2026-09-04 19:30:08 +02:00
Steffen Becker b80440de0e fix: a chapter number is set and placed the way the file describes it
The outline level's character style, its indent and the stop its label's tab
runs to were never read, so a 96pt grey chapter number rendered at the
heading's own size and everything under it sat ~30mm high. The label is an
inline block now — one set larger than its heading grows the line as both
products grow it — hung out of the paragraph's indent with the level's stop as
its minimum width. text-indent inherits into that block and would shrink it by
exactly what it hangs out, so the label zeroes it. Both formats carry all of it
back out (ODF's label alignment, Word's w:suff / w:ind / w:rPr).
2026-09-04 19:30:08 +02:00
Steffen Becker 4ea66725ef fix: a dynamic zone spacing is absorbed by the band, not added to it
style:dynamic-spacing="true" — what LibreOffice writes for a Word header —
makes the gap give way as the zone grows, so the band is max(height, content)
where a fixed gap gives max(height, content + gap). Probed at min-height
7.51mm, gap 6.51mm, 12pt lines: false → 11.37 / 21.11 / 35.71mm for 1 / 3 / 6
lines, true → 7.51 / 14.60 / 29.21. The previous note said it changed neither.
2026-09-04 19:30:08 +02:00
Steffen Becker 43d9a864d4 docs: the band is measured, a manual break keeps its space, headings number
Corrects two probed claims that measurement contradicted: a hard page break
does *not* swallow the block's space above (only an automatic one does), and
the header band is max(declared height, content + gap) rather than a line
count. Adds the chapter-numbering model, the mirrored master pair, and the
harness ceiling a CSS-drawn label is.
2026-09-04 19:30:06 +02:00
Steffen Becker 80463883f0 feat: chapter numbering — headings carry the number the file gives them
A numbered thesis lost every "1.", "2.1.4" and "A." on import: the outline
definition was read as a list wrapping headings and dropped. It is now a
document-level definition on the style sheet (format, prefix, suffix,
display-levels, start per level), drawn by CSS counters that reset down the
levels, and carried by both formats — ODF's text:outline-style, Word's
multilevel numbering bound to the heading styles. The contents rows count the
same way, since no text walk can read a CSS counter.
2026-09-04 19:30:06 +02:00
Steffen Becker 430bef5795 fix: two masters naming each other are a mirrored pair, not a title page
LibreOffice's Left Page / Right Page chain into each other through
style:next-style-name, which was read as the different-first-page idiom: one
master's geometry and zones governed the whole section, so a book-style body
lost its running head on every second page and its inner/outer margins with
it. A next-style cycle now becomes one mirrored setup — the right page's
geometry, the left page's zones as the even variant — and style:page-usage
left/right marks the margins as mirrored.
2026-09-04 19:30:06 +02:00
Steffen Becker 862ba22df2 feat: a section numbers its pages in its own format
A roman front matter before a decimal body is one page style before another
in ODF and one w:pgNumType per section in Word, not a document setting — read
as one, its pages printed 1, 3 where the reference prints i, iii. HfSet grows
pageNumberFormat, both exports write it back, and the DOCX importer takes the
document's own format from the first section, as it already does the paper.
2026-09-04 19:30:05 +02:00
Steffen Becker ad12231bf0 fix: a left page keeps the header its master gives every page
Odd/even is per zone in ODF: a master declaring only style:footer-left still
repeats its header on left pages, and reading the pair as one flag blanked
the running head on every even page of a mirrored document. A left element
that is present but empty still blanks its zone.
2026-09-04 19:30:05 +02:00
Steffen Becker ce9dc9bb4d fix: the header/footer band is measured, not counted in lines
The body's margin has to clear the band the zone renders at, and the
estimate counted paragraphs — blind to a line that wraps, to a run smaller
than the 12pt floor, and to the zone's padding. HeaderFooterLayer now lays
each section's zones out off-screen at their text width and reports the
height; the ODF zone's own gap to the body and its padding ride the
collapsed paragraph's spacing, which is where the export already puts them.
Measured on one file: a three-line band read as two, 14.9mm a page.
2026-09-04 19:30:05 +02:00
Steffen Becker 0cf43de1b1 fix: the page geometry is the body's master before the body is read
The dominant master was elected after convertBlocks, so the text width a
table's margins, an inline image's fit and the tab-stop suppression measure
against was whatever master the file declares first (17.0cm against a real
14.7cm on one file). A pre-pass over the body's paragraphs elects it up
front, and the post-conversion election is gone so the two cannot disagree.
2026-09-04 19:30:05 +02:00
Steffen Becker a7cf09a6be docs: the header band is max(declared height, content + its gap)
The note said a header's fo:margin-bottom just hangs into the body and does
not move it. Probed against soffice on one-page fixtures: a 20mm top margin
with a one-line 10pt header puts the body at 24.07mm with no gap and 29.07mm
with a 5mm one, three paragraphs at 37.21, a paragraph wrapping to two lines
at 33.14 (so rendered lines count, not paragraphs), and fo:min-height 20mm at
40.00 — the floor wins, the gap is not added on top. Padding counts too.
2026-09-04 19:30:05 +02:00
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