Two marks carry a revision with its author and date: an insertion is
marked text, a deletion is text kept and struck through. Consecutive
typing joins one change; accept/reject work at the cursor or on all.
ODF splits the pair — text:change-start/-end around an insertion, a
text:change where a deletion was, its text in the tracked-changes
registry. DOCX uses w:ins/w:del with w:delText. Both round-trip, and
accepting every change gives the document an unrecorded edit would.
The same generated index the table of contents is, over the captions of one
category instead of the headings — an `index` attr picks the family, so the
node view, the page numbers and the leader dots are shared. Round-trips as
ODF text:illustration-index / text:table-index, found by the counter's name
the way LibreOffice finds them, and Word's TOC \c "Figure" field.
A caption is a paragraph in the Caption style holding a sequence field —
one counter per category, in document order, renumbered as the document is
edited and on load (a file's cached numbers are unreadable wherever the
format is not arabic). Round-trips as an ODF text:sequence with
LibreOffice's own ooow:Illustration+1 formula and a Word SEQ field.
Caption style values probed off LibreOffice: 10pt italic, 0.212cm around.
The block's own direction, set in the Paragraph dialog where LibreOffice
keeps it, so a quoted Hebrew or Arabic paragraph turns inside a left-to-right
document. Rides ODF style:writing-mode / Word w:bidi through cells and list
items as well as top-level blocks; the direction the page already has is
inheritance, so neither importer stamps it on every block.
The structural header, beside the styling one: pageBreaks grows the
between-rows spacer by the first row's height and draws a copy of that
row — colgroup included, so the columns line up — at the foot of the
gap, which is the top of the continuation. Round-trips as ODF
table:table-header-rows and Word's w:tblHeader; LibreOffice keeps both.
Word's "don't allow row to break" needs nothing: a row that fits a page
is already an atomic leaf here.
The comment is a mark carrying its own author, date, body and resolved
flag — the bookmark pattern, so no second store and no schema node. The
pane lists them in document order and jumps to the text each marks.
ODF gets office:annotation + office:annotation-end and LibreOffice's
loext:resolved, DOCX the range pair plus word/comments.xml; a point
comment, which LibreOffice writes without a range, attaches to the run
before it. Ctrl+M no longer strips a comment, a bookmark or a link.
A .* toggle beside match-case and whole-word takes the term as a
pattern; a pattern that will not compile simply finds nothing. Each
match keeps its captures, so the replacement expands $1…$9 and $&
the way LibreOffice does — Word has no equivalent.
LibreOffice's rule set: typographic quotes per document language, its
dash matrix, its replacement table, sentence capitalization with an
abbreviation exception list, and TWo INitials. One handleTextInput
plugin listed last, so every input rule still fires first; the rule
table itself is a pure function under test. Auto-URL and auto-list were
already TipTap's and now answer to the same checkboxes.
LibreOffice's style pool, probed: Heading 6 is 12pt bold italic and
Heading 4 italic too. MAX_HEADING_LEVEL drives the extension registry,
the shortcut plugin, the TOC depth and both importers' yardstick, so
only the constant table and the CSS selector lists had to grow.
Bookmarks/cross-references move to Implemented — they already are.
Per cell the attr beside the computed value, cell and row height, the gap above
and below the content, and each block's margins — plus the style sheet and the
spacing model, which is what decides whether the content fills the box.
The marker moves from a CSS ::before to a widget decoration: generated
content is no text node, so it reached neither the PDF nor a measurement.
The note also carries the paragraph style its file names, so font, size and
colour are the document's; only the hanging indent stays the editor's,
because the style model has no first-line half to pair with the margin.
Adds a unit test for the numbering and the settings, plus the architecture
doc and the two open parity findings.
Numbering format, start value, restart, position, prefix/suffix and the two
styles per class, plus the separator's length, thickness, spacing, alignment
and colour. The separator rides :root custom properties, so editor.css draws
the line and pageBreaks.ts reserves its band from the same values. A settings
change is no document change, so it carries a resync meta — without it the
numbering kept the format it was typed under.
Anchors become an FNT sentinel and each note is hoisted to a top-level
paragraph, so its text rides every existing pass — marks, character styles,
links, bookmarks and formulas included; applyNotes cuts the region back out
into <text:note> at the anchor. styles.xml gains LibreOffice's own Footnote/
Endnote styles, the notes-configuration per class and the separator, and the
resolver reads all of it back. The importer builds the section instead of
warning that notes were dropped.
The placement loop moves into placeLeaves(), which takes a per-page note
reservation off each contentEnd; a bounded fixed point re-runs it until the
reservation and the pages its own anchors land on agree. Each break now
carries the leaf offset it applies from, so an anchor below a line split
follows its half of the paragraph. Endnotes stay in flow and open their own
page, as LibreOffice collects them.
The anchor is an inline atom, the note text a block in the one noteSection
at the document end; a sync plugin keeps notes in anchor order, renumbers
them per class and drops the ones whose anchor is gone. Numbering, indent,
size and separator defaults are LibreOffice's, read out of a document it
saved. Endnotes still render in flow at the document end — lifting a
footnote to the foot of its page is the next step.
w:tblInd means the distance to the cell's text up to Word 2010, so a table
indented 0 hangs its left cell margin into the page margin and its first column
lines up with the body. We drew every such table 2.2mm right of LibreOffice.
Import subtracts it, marginLeft may now be negative, and both exports carry the
edge as it stands.
Word 2013 (compatibilityMode 15) redefined it as the edge — probed both ways
against soffice on one file, 29.6mm against 31.75mm. Corpus 795 -> 761.
ODF style:page-usage="mirrored" and Word's w:mirrorMargins make the
declared left/right the inner/outer pair, which an even page swaps.
Read, rendered and written back in both formats; the body rides the
per-block inset a section's own margins already use, the header and
footer band moves with it. Measured on a mirrored two-column file:
its page 2 went from 12.7-184.1mm to 31.8-203.2, which is LibreOffice's
to 0.1mm, and its running head with it.
A pagination pass measures the DOM the last one changed, so a fresh
document searched for its layout in public — 12 of them over 550ms on a
two-column file, a phantom third page among them. Two real causes: the
flow's overflow test had no headroom, so it read its own last answer
(880px fits undecorated, 913px overflows once the decoration pins the
column height), and a split dropped the height decorations without
clearing their cache key. The rest is inherent to measuring rendered
DOM, so the page stays hidden until its layout repeats: one visible
layout instead of 12, and 21-139ms where there are no columns.
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.
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.
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.
Clipboard, Font, Paragraph, a style gallery of live tiles, and Find/Replace —
which had no button in either chrome before. So did change case, underline and
strikethrough line styles: the engine has carried them all along.
Two things the fixed-height band forced. Its own overflow clips a dropdown, so
a menu leaves the flow and pins itself under its trigger. And with one shared
open-menu id, each wrapper must name the menu it owns, or every sibling closes
it on mousedown and the click on a row never lands.
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.
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.
The dot advance was probed once, on the first row, and reused for every
level below it — so a level with a smaller style ran ~9% short, 11mm of
bare gap before the page number. Probed per font now (a handful of
measurements, not one per row). A fixture's contents: 122/84/72 dots per row,
exactly what LibreOffice draws.
Collapsed borders paint a shared edge once, so the spacer <tr> bridging
the gap took the row separator with it and left one fragment open —
visible in a fixture as tables missing their frame on a page. The spacer now
draws the line LibreOffice draws there: the separator the break falls on,
or the table's own box where the rows carry none. Absolutely positioned,
since a collapsed border on the spacer shifts every row below by half.
A book's chapter heading is "Chapter 1" and its title on two lines, and both
LibreOffice and Word copy that break into the index. We joined it into one line,
so a fixture's contents came out 18 rows short. The leader and page
number ride the last of an entry's lines (align-items: last baseline).
.tiptap's padding draws one pair of side margins for the whole document, so a
title page declaring 3.18cm left sat 3.85mm off. The difference now rides a
--sec-inset-left/-right node decoration on the section's top-level blocks, and
everything that writes a block margin adds it in — an indent, a named style's
and a table's alignment all compose with it instead of losing to it.
Page geometry was document-wide, so a title page declaring 1cm started 1cm too
low. w:pgMar / the section's ODF page layout now ride its HfSet and feed the
per-page content area; an ODF master handing over (style:next-style-name) gives
its own layout to the section's first page and the successor's to the rest.
A spacer can only push down, so a smaller margin than the document's needs the
opposite: the section's first block gets a lifting spacer where nothing was
pushed onto its page ahead of it.
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.
`wrapDist` carries Word's distL/distR and ODF's fo:margin-left/-right onto
the frame; a file that declares none wraps flush, which is what LibreOffice
draws (probed: text runs to the frame edge, and 0.5cm of declared margin
stops it exactly 0.5cm short). The hardcoded 14px was 2px more than Word's
own 0.32cm and broke a line one word early.
Whole pixels cost a page-wide picture 0.3mm of height, and the column cap
then scaled that loss into the width too. Sizes stay fractional (snapped
back to a whole pixel inside the export's own 0.001cm quantization), and a
sub-pixel overhang trims the width without touching the height — which is
what LibreOffice draws.
A block spaced above single is drawn half a leading above where it flows,
and offsetTop reports the drawn position — so every block a break pushed
to a page top landed that half low. Take the shift back off where the
document is measured.
The no-descent rule matched a block holding nothing but frames, so the
image-break-caption idiom kept a text descent under its picture — 0.9mm
per figure, and LibreOffice's own guides stack dozens of them in tables.
Test the run between two hard breaks instead and mark the frame itself.
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.
Probed: an as-char frame alone in a paragraph makes a line exactly as tall as
itself whatever style:vertical-pos/-rel says — only from-top's offset still
lengthens it. The node view cannot see its siblings, so the block's class
overrides the alignment it wrote.
LibreOffice sizes such a line max(frame, line-height) — nothing of the text
sits under the frame (probed: an image-only paragraph is exactly as tall as
its image, down to a small one, where the text line wins). Our baseline
alignment left the strut's descent below it, ~1.1mm per formula. Whitespace
beside the frame counts as nothing, which CSS cannot ask, so image.ts marks
the block.
fo:min-height is a floor, not the band: LibreOffice grows it for the zone's
own lines and, in a footer, for the first paragraph's space above (probed —
a header's space below only hangs into the body). The zone paragraph now
carries those margins, so the reach counts them at its real font size and a
page with no zone of its own keeps the floor.
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.
LibreOffice drops it at the page bottom, as it drops space above at a page
top (probed: a paragraph 8mm clear of the bottom keeps its 2cm below and
stays). Counting it pushed the last block of a page down a page too early.
A paragraph with fewer lines than orphan and widow control demand together
has no legal break point, so it moves to the next page whole — and the
heading kept with it has to follow. Measuring only its first two lines left
the heading stranded at the page foot.
The 6px we put over and under every wrapped frame is a default only this editor
has: LibreOffice and Word wrap flush unless the file declares a distance. It
made a fixture's figure boxes 3mm too tall each.
One fixture: 120 → 100 parity issues, the DOCX fixtures unchanged.
The atomic leaf a text box becomes carried neither its space above nor its space
below, so the fit test ignored both and a page top never dropped the space above
the way it does for a paragraph. The box's space rides --space-top, which the
page-top decoration zeroes — the rotor centres itself in the padding box and has
to follow it.
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.
A heading only moved down when its successor's *first* line no longer fit. But
widow-orphan control carries a lone first line over anyway, so the heading was
left stranded at the page foot — LibreOffice moves the pair. The fit test now
asks for MIN_KEPT_LINES of the successor.
Two ways a table's height was off. Its space above was a margin, so it
collapsed against the paragraph above instead of adding to it — now it rides
--space-before like a paragraph's. And every cell border paints a whole pixel
however thin the file declares it, where LibreOffice reserves the declared
width; the excess now comes off the cell's padding-bottom, 0.25mm a row.
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.
ODF puts fo:margin-top/-bottom on the table style and LibreOffice honours
them like a paragraph's; we dropped both, so every callout table in a fixture sat
half a line too tight and the document ran a page short. With them the Math
Guide paginates to LibreOffice's 63 pages exactly.
Only the ordinary-block push site added the space the page-top rule had
dropped above it; the index, text box, columns and table-row sites subtracted
the spacers alone, so each of those measured too high by whatever the pages
above had swallowed and got an equally oversized spacer — a fixture's figure
frames landed 16mm down their page. One naturalTopOf() now serves all six.
Two measured deviations, both worth ~1mm a row on a fixture: a cell's first block
got the space above that LibreOffice adds nowhere in a cell, and a repeating
header row rendered centred and bold because it is a <th> and the UA says so.
Pagination's page-top-drop detection now skips cell leaves, or the space that
is never there would read as a decoration to add back.