Commit Graph
133 Commits
Author SHA1 Message Date
Steffen Becker 35f3c50541 fix: a column break lands on the line it was measured to
Three measurement bugs in columnsFlow, each worth a page on its own:
rects became lines by their top, so a raised run counted as a line of
its own (192 for 111); the split probed `left + 1`, which is a line's
*end* right-to-left, so it kept a word too many and the flow then shaved
one line per pass; and one median advance under-measured a paragraph
whose lines differ in height (2834px real against 2453 estimated), by
enough to budget 74 lines into room for 64. The chain used to flip
between one fragment and two forever; it now settles after one split.
2026-08-12 20:57:49 +00:00
Steffen Becker 9bfd579d65 fix: hidden text stays off the page, except the heading a running head reads
text:display="none" now drops the paragraph its style hides and the stored
value of a field that carries the attribute — LibreOffice lays out neither.
A hidden heading is the exception and still renders: it is the outline the
running head's chapter field reads, and dropping it blanked every head that
quotes one. The editor has no way to hold a block it does not draw.
2026-08-12 19:49:33 +00:00
Steffen Becker 99a14a1a7f fix: Hebrew and Arabic take the size their style declares for them
ODF carries western, asian and complex-script font properties side by side and
a word processor picks by the character's script; the importer read only the
western pair, so a complex-script run came out at the document default.
Decided per run — the finest split the model has — so a document with no
complex script is untouched, and our own export (all three written alike)
round-trips. Measured: a two-column page's line pitch 7.5mm -> 5.9 against
LibreOffice's 6.0.
2026-08-12 19:49:15 +00:00
Steffen Becker 06343103e4 merge: the Word-style ribbon lands on main
Brings the ribbon chrome, its tabs and controls, the Paragraph/Tabs dialogs
and the shared selection reader over from word_ui. No file overlap with
main's cover-graphic fix, so the merge is clean.
2026-08-12 19:56:19 +02:00
Steffen Becker e58e8a180e fix: a page-anchored cover graphic sits in front, at the page's own corner
Two compounding bugs on a title/back-page background image:
- its host paragraph picks up position:relative from the line-height
  rule, becoming the frame's containing block instead of the page and
  offsetting it by a full page margin — now forced back to static.
- the frame always painted behind text (z-index -1); a graphic that
  declares ODF's style:run-through="foreground" now paints in front,
  as LibreOffice draws it, via a new inFront attr round-tripped through
  ODF export/import.

Confirmed against LibreOffice's own render: the duplicated, ~2cm
displaced cover graphic now matches exactly.
2026-08-11 23:48:52 +00:00
Steffen Becker f7ee9546a4 refactor: what the selection formats becomes a shared reader
Font, size, mark colour and block attributes all answer the same question — the
one value the selection agrees on, blank when it mixes. One `uniform` walk now
serves all four, framework-free, so any chrome can read it inside its own
reactive wrapper.

The classic toolbar keeps its own copies for now: it is the baseline the ribbon
is compared against, and it should adopt these only once that comparison ends.
2026-08-12 01:22:52 +02:00
Steffen Becker b98cc550c3 fix: the suite finds a localStorage again
Node 26 defines its own `localStorage` global — undefined unless the process is
given --localstorage-file — and it shadows the one jsdom installs. Every module
that reads storage at import time threw before a test could run, so 37 of 65
files failed to collect. A setup file defines the property when it is missing.
2026-08-12 01:20:40 +02:00
Steffen Becker 64cc010f01 docs: a real-world document's name stays out of the repo
The parity findings named the six files the corpus is built from, so every
measurement carried a document title into the history. They move to a
gitignored FINDINGS.md beside the corpus itself; the README keeps the harness.

A test that needs a document builds one, so nothing under src/ or tests/
reads the gitignored corpus — CI greps for exactly that.
2026-08-11 22:25:33 +00:00
Steffen Becker f055fc4850 docs: every comment back inside the three-line rule
A sweep of the whole tree: 27 blocks ran to four lines or more. Trimmed
to what the code can't say, with two of them split back onto the
function each was really about.
2026-08-11 21:22:13 +00:00
Steffen Becker 2a8f0675bc fix: Word's hollow bullet is the letter it is
A Courier New "o" was mapped to ◦ for looks. LibreOffice and Word both
draw the glyph the numbering names, so the nested lists of the gravity
summary now read like theirs.
2026-08-11 21:22:13 +00:00
Steffen Becker 95d174501d fix: a Word hyperlink keeps the look its file gives it
Word paints a link through its Hyperlink character style, never for
being one, so the editor's own blue overwrote both the file's #0000FF
and the plain black of a link carrying no style at all. The DOCX
importer marks those `plain` and the CSS leaves them alone; LibreOffice
paints every ODF text:a, so those keep the visual.
2026-08-11 20:52:12 +00:00
Steffen Becker 8db6214c11 fix: a CMYK image keeps the colours its file draws
Chromium colour-manages a CMYK JPEG through its embedded ICC profile
where LibreOffice and Word convert naively, so a fixture's
logo arrived dull teal on grey instead of vivid blue on black. The
importer drops the profile — measured back onto LibreOffice's own
values, and 655 KB smaller for that one logo.
2026-08-11 20:42:00 +00:00
Steffen Becker 40d58463c7 docs: per-section margins ship, and what the corpus reports
Also records the harness change that made the longest fixture measurable at all.
2026-08-11 19:52:39 +00:00
Steffen Becker 860e25d8c8 fix: saving keeps a section's own page margins
Both exports wrote one geometry for the whole document, so a title page's
margins survived import and were lost on the next save. Each ODF section master
now names a page layout of its own — the document's, shifted by the section's
difference, so what the header/footer pass folded into it survives — and every
w:sectPr carries its section's w:pgMar. DOCX import takes the document pair from
the first section, the one .tiptap's padding draws.

The parity harness spread a 458-page document's word list into Math.max and blew
the stack; the longest fixture was never measured.
2026-08-11 19:35:55 +00:00
Steffen Becker 71cf17f47a docs: what the corpus still reports, and why it stays
Three probed findings written down rather than fixed: an outside ::marker has
no metric influence in CSS (a bulleted line is 0.12mm short of LibreOffice's),
an unbreakable run longer than the line moves whole instead of breaking at the
margin, and a typeset formula keeps its own baseline rather than its frame's —
honouring the frame measured worse. Plus the live-vs-cached index numbers.
2026-08-11 18:06:42 +00:00
Steffen Becker e2ee55cf35 fix: a header/footer band is as tall as its own font, not the body's
The zone is one paragraph, so its mark sizes every line of the band. Runs
that agree on a font now set that strut (applyUniformRunFont, as the body
already does), the zone schema carries blockFontSize so it renders, and
hfReachPx measures a line by the family's natural height instead of a flat
1.15. A 10pt footer reserved 12pt lines and sat 1.6mm high on every page.
2026-08-11 17:10:08 +00:00
Steffen Becker 361ce14864 feat: pair kerning follows the file, not the browser
A browser always kerns; ODF kerns unless told not to and Word kerns only
above the size w:kern names — nothing, in most files. Measured 5% on a
kerning-heavy string, enough to break a line a word early throughout every
Word document in the corpus. Carried on the style, since that is where
w:docDefaults and the default paragraph style put it.
2026-08-11 07:23:20 +00:00
Steffen Becker db8f4439b1 feat: a table that may not break between rows moves whole
ODF style:may-break-between-rows="false" — probed: LibreOffice leaves four
lines of a page empty rather than split such a table, and splits the identical
one when the attribute says "true". pageBreaks now emits a single atomic leaf
for the wrapper unless the table is taller than a page, where the rule cannot
be met. Imported, exported and round-tripped; Word has no equivalent.
2026-08-11 00:02:31 +00:00
Steffen Becker 13e026fb8b fix: a zone's own margins survive the ODF round trip
The Header/Footer style carries them, and the band that grows to hold them
is rebuilt from there on the way back in.
2026-08-10 21:31:38 +00:00
Steffen Becker af144362fe feat: an as-char frame sits where the file says it does
ODF style:vertical-pos/-rel on an inline frame, which every LibreOffice
formula image carries: baseline, character area, or a plain offset from the
baseline. The default moves from the line-box bottom to the baseline, so a
line with a picture in it keeps the descent below it, as LibreOffice and
Word both do.
2026-08-10 20:48:31 +00:00
Steffen Becker 333ca80b7c docs: what the parity corpus still reports after the spacing round 2026-08-10 19:35:38 +00:00
Steffen Becker 71ecc17a9e fix: a header/footer's gap to the body is inside its band, not above it
The body margin reserved the zone's height *plus* its fo:margin-top/-bottom, so
every page with a footer lost a line — a fixture ran a page over. Probed against
soffice: moving a footer's fo:margin-top leaves the last body line exactly where
it was and only shifts the footer text down its band; style:dynamic-spacing
changes neither side. The band is the declared height, the gap sits in it.

One fixture paginates to LibreOffice's 63 pages again.
2026-08-10 19:34:53 +00:00
Steffen Becker d6c3602429 fix: a lifted text box keeps its anchor paragraph's spacing
An as-char figure frame sits in a paragraph of its own; the importer lifts the
box to top level and dropped that paragraph whole, so its space above and below
went with it — every figure in a fixture sat 2.1mm too tight. The box now carries
them (ParagraphSpacing covers textBox), space above as padding so it adds to the
block above, and the export mints an anchor style to write both back.

One fixture: 142 → 100 parity issues.
2026-08-10 18:54:37 +00:00
Steffen Becker 819f51587e fix: direct formatting on a run beats its named character style
A character style is a stylesheet rule, direct formatting an inline style on the
run's span. The charStyle mark ranked below TextStyle, so its span sat *inside*
and the rule won over the ancestor's inline style — a 9pt style silently undid a
run's own 10pt. Priority 102 lifts it above TextStyle's 101.

One fixture: 259 → 172 parity issues, lineEnd 49 → 7.
2026-08-10 18:08:13 +00:00
Steffen Becker 3153c2f7cc feat: the running head names the chapter the page is in
A manual's footer carries text:chapter, which we imported as the string the
producer happened to cache — so every page of a fixture claimed the last chapter
of the book. It is now a live field: the layer resolves the last heading at
or above its outline level to have started by that page. Round-trips to ODF
text:chapter and Word STYLEREF; PDF keeps the cached name (Chrome's print
engine has no named strings).
2026-08-10 13:04:25 +00:00
Steffen Becker a8dcd9b897 feat: a cell's vertical alignment survives saving
Import read style:vertical-align but neither exporter wrote it, so a
mid-cell callout label jumped to the row's top on every save. odf-kit's
CellOptions and docx's TableCell both take it natively; DOCX import maps
w:vAlign in as well.
2026-08-10 12:51:58 +00:00
Steffen Becker aa2f680ad7 docs: what the corpus still reports, and why 2026-08-10 11:42:41 +00:00
Steffen Becker ab83ad3c58 test: the parity runner compares a leader as one token, and polls
How many dots fill a tab or index leader is the fill's own business —
both engines end it at the same stop. The settle predicate now polls on a
timer: walking every spacer and index row each frame starved the layout
it was waiting for, and half the runs timed out.
2026-08-10 11:32:40 +00:00
Steffen Becker c26aac1bee fix: the index's leader fills its own gap, at LibreOffice's indents
Every row carried 200 dots — clipped on screen, but the clipboard, the
PDF and any measurement got all of them. Cut each to what its gap holds,
and step the levels by 0.5cm, which is what LibreOffice's Contents styles
use. The parity runner now waits for the page numbers to settle too.
2026-08-10 09:32:49 +00:00
Steffen Becker 4ad743dc28 test: the parity runner waits for the import, not just for a blank page
A 3.5MB fixture takes seconds to parse; until then the editor holds the
empty document, whose layout is stable enough to pass the settle window.
2026-08-10 08:17:40 +00:00
Steffen Becker 2e70e47912 test: the parity runner can reuse a dev server already listening
PARITY_PORT points it at an existing npm run dev instead of starting a
second one on 5199.
2026-08-10 07:38:31 +00:00
Steffen Becker aff3eaa812 fix: a named style's line spacing is the percentage the file declares
The style-registry path divided an ODF fo:line-height by 1.15 where the
block path, both DOCX paths and the export all treat it as the plain
percentage, so every paragraph whose spacing came from its style
rendered 15% too tight. Its suppression yardstick is the block's own
style too, so a 100% under a 115% style stays as direct formatting.
2026-08-09 20:43:34 +00:00
Steffen Becker d6bc656c83 feat: the space between two blocks follows the document's own model
LibreOffice adds the upper block's space-below to the lower one's
space-above, and drops the space-above where a page break put the block
at a page top — but for a Word document it takes only the larger of the
two (all probed against soffice). CSS margins can only collapse, so
space-before rides --space-before and editor.css renders it as padding
('add') or margin ('max'); the model is read from settings.xml, assumed
for DOCX, and written back on ODF export.
2026-08-09 16:53:13 +00:00
Steffen Becker af299d8b9f fix: a minted automatic style reaches a document that had none
odf-kit writes `<office:automatic-styles/>` self-closed when it minted
nothing, and every post-pass injected by replacing the closing tag — so
in such a document the page-break, spacing, list and text-effect styles
were dropped and only the style-name reference survived. One
injectAutomaticStyles helper now serves all of them, content.xml and
styles.xml alike.
2026-08-09 15:28:48 +00:00
Steffen Becker 84eb7d6bf3 fix: an index lists the levels it declares, not all five
The TOC node regenerated over every heading level and minted a title of its
own. A file asking for level 1 only (text:outline-level, Word's TOC \o range)
got a 43-entry index instead of 12 — a full page where LibreOffice needs a
quarter, plus a second "Table of Contents" under the file's own heading.

maxLevel and an empty title now round-trip through both formats.
2026-08-09 14:54:58 +00:00
Steffen Becker 28a39e017a feat: a document that blocks the start is not loaded twice
An autosaved document that hangs or throws while the editor comes up was
reloaded from localStorage on every refresh, leaving no way back into the app.
Loading now raises a boot flag that only a completed startup clears; finding it
still set means the last attempt never finished, so the document moves to
odf-editor-doc-broken and the app starts empty with a message.
2026-08-09 14:13:17 +00:00
Steffen Becker 78e35cef2c feat: a reference finds the text it points at, and follows it
Bookmarks are a mark, not a point: both formats store a range, and only a
range lets a REF recompute its text. A cross-reference caches its display
like a Word field and resolves live on each pagination settle — the
bookmark's text, or the page it renders on — keeping the cached value when
the target is gone. Ctrl+click jumps to it, internal #name links too.

ODF splices its sentinels into the run text so cells and custom paragraphs
need no separate emitter; DOCX needs no post-pass at all.
2026-08-09 13:56:22 +00:00
Steffen Becker 46ae0211af fix: a Word table draws only the borders it declares
The DOCX importer read w:tblBorders off the table but never off its table
style, so a table whose style declares none (Word's Normal Table, and the
thesis' Formel-tab around every formula) fell back to the editor's 0.5pt
black default. Resolved per side along the w:basedOn chain, and an
undeclared side is not drawn — LibreOffice writes fo:border="none" for
exactly those tables when it re-saves the file. The ODF importer already
treated an undeclared side that way.
2026-08-09 08:54:59 +00:00
Steffen Becker 2f6e771f06 docs: record what a metafile brings and why a line still breaks early
The line-break finding is sharpened: LibreOffice compresses inter-word
spaces to fit a line (91 of 349 full-width justified lines on the
thesis, up to 0.83px per space), which CSS justification cannot. A
matching negative word-spacing was measured and reverted.
2026-08-09 05:59:58 +00:00
Steffen Becker 3be4333517 feat: two frames set against opposite ends share one band
A top-and-bottom frame carrying positionH align / style:horizontal-pos
keeps it only where a nearby frame is set against the other end
(pairAlignedFrames, both importers); such a pair floats to its own side
instead of filling the band, so the two sit side by side as they do in
LibreOffice. A pair slightly wider than the column is scaled to it —
Word lets the two overlap, two floats cannot.
2026-08-09 05:59:58 +00:00
Steffen Becker 29d82ee657 feat: a metafile is drawn instead of a grey placeholder
An EMF is replayed into SVG (import/emf.ts): window/viewport plus world
transform, a DC stack, pens/brushes/fonts, the poly and path records,
EXTTEXTOUT and embedded DIBs. It rides the lazy pre-pass both importers
already use for TIFF, so a document without one pays nothing.

a fixture fixture's seven MATLAB plots arrive as pictures now.
2026-08-09 05:59:44 +00:00
Steffen Becker c53bef8cae docs: record why a hoisted text box's offset was not measured back 2026-08-09 00:13:41 +00:00
Steffen Becker 2380ed699b feat: an ODF chart object is drawn too
<chart:chart> in the object's sub-document, read through the same picture as
the DOCX leg: series values by cell range out of its own local-table, colours
from the series style, bounds from the axis style.
2026-08-08 23:54:59 +00:00
Steffen Becker bf98014a3a feat: a chart is drawn from its own part instead of a placeholder
word/charts/chartN.xml → an SVG picture of the frame's size: series from the
producer's numCache, theme accent colours, titles, gridlines and the axis
bounds the file fixes. Bar, line, area, scatter and pie; the rest keep the box.
2026-08-08 23:49:13 +00:00
Steffen Becker 2fe762a594 fix: a citation inside a content control keeps its text
Word wraps every citation in a w:sdt; the inline walk ignored the wrapper, so
its runs never reached the paragraph. a fixture fixture lost every [7]-style
reference with them, and now matches LibreOffice's page count.
2026-08-08 23:17:47 +00:00
Steffen Becker 3154a9b7a2 feat: a list label takes the alignment the file gives it
Word's w:lvlJc / ODF's fo:text-align on the level: set right, the label grows
into the margin instead of over the text it labels. That is what the built-in
Roman numberings use, and a fixture's numerals collided without it.
2026-08-08 23:09:58 +00:00
Steffen Becker 2830e8a9d4 feat: a top-and-bottom frame is drawn where the file places it
Both anchor offsets now render: the x moves the rotor inside the full-width
float, the y the float's top. Chromium puts every line after such a float
below it, so a frame set below the paragraph top is sunk behind the text at
import and the node view measures what those lines already cover.
2026-08-08 22:51:39 +00:00
Steffen Becker ee56a348ee feat: formulas can be inserted and edited, and survive both formats
A formula node stores only its LaTeX source; the MathML on screen, the ODF
formula object and Word's OMML are derived from it, and both importers parse
foreign markup back into that source, so an imported formula stays editable.

Word documents were losing their formulas outright: m:oMath sits beside the w:r
runs in its own namespace, past convertInline's `namespaceURI !== W` guard.
2026-08-08 21:50:36 +00:00
Steffen Becker 412701cdec docs: record what LibreOffice does with a nested level's text-indent 2026-08-08 17:47:17 +00:00
Steffen Becker 0af3488e6e feat: every list level takes its indent from the file
Only the top level's own w:ind w:left (ODF: the label-alignment margin)
was read; every level below it was forced to the editor's 1.27cm step.

Both formats store a level's indent absolutely while the editor nests one
step per level, so import now subtracts the level above and the ODF
export adds the running sum back per level (DOCX already accumulated).

Measured: a 0.5cm/-0.6cm pair puts LibreOffice's two levels at 1.77cm and
2.44cm, and the editor draws them at 1.769 and 2.44. Parity corpus
unchanged.
2026-08-08 17:47:17 +00:00