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.
This commit is contained in:
1 parent
fe55aec95f
commit
64cc010f01
5 files changed
+20
-153
No files matched your search
@@ -12,6 +12,10 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
# CLAUDE.md is read at every session start; detail belongs in the nested files.
|
||||
- run: test "$(wc -l < CLAUDE.md)" -le 120 || { echo "CLAUDE.md over its 120-line budget"; exit 1; }
|
||||
# The corpus is gitignored, so a test reading it would name a document and fail here.
|
||||
- run: |
|
||||
! git grep -l "render-parity/fixtures" -- src tests ':!tests/render-parity' \
|
||||
|| { echo "a test reads the gitignored parity corpus"; exit 1; }
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
@@ -4,4 +4,5 @@ dist
|
||||
debug
|
||||
.DS_Store
|
||||
tests/render-parity/fixtures
|
||||
tests/render-parity/FINDINGS.md
|
||||
docs/others
|
||||
@@ -139,6 +139,8 @@ The gap against Word/LibreOffice, most valuable first. Reviewed 2026-08-08.
|
||||
- Password-protected ODT/DOCX; digital signatures
|
||||
- Navigator / outline view, split view
|
||||
- PWA / offline support
|
||||
- A committed corpus of documents we author ourselves, so CI can test against real files
|
||||
(`.gitignore` excludes all of `tests/render-parity/fixtures/` today)
|
||||
|
||||
**Out of scope for now**
|
||||
- Mail merge, data sources, form fields
|
||||
|
||||
@@ -37,6 +37,10 @@ Tests live in `tests/` (outside `src/`, so `svelte-check` ignores them), jsdom v
|
||||
lines**, however large the change. Probed behaviour, measurements and rationale lists belong
|
||||
in `docs/architecture/` or the nearest `CLAUDE.md`, not repeated in the history.
|
||||
|
||||
**IMPORTANT: no real-world document's name anywhere in the repo, and a commit message
|
||||
names no document at all** — not even "the thesis": describe the fix and its measurement,
|
||||
never the file it helped. Detail in `tests/render-parity/README.md`.
|
||||
|
||||
**IMPORTANT: never introduce a default only this editor has.** Both importers suppress values equal
|
||||
to the defaults, so an editor-only default is indistinguishable from a failed style resolution and
|
||||
silently lands in every imported document as direct formatting. The defaults follow **LibreOffice**
|
||||
|
||||
@@ -55,17 +55,16 @@ against itself. Real-world `.docx`/`.odt` files dropped into `fixtures/` are pic
|
||||
up too. The directory is gitignored: run the generator once, and keep whatever
|
||||
real-world files you test against local.
|
||||
|
||||
## Differences that are not editor bugs
|
||||
## Names stay local
|
||||
|
||||
- **Justified text.** LibreOffice *compresses* inter-word spaces (measured: 0.864mm
|
||||
against a natural 1.058mm) to pull one more word onto a line. CSS justification
|
||||
only stretches, and Chromium exposes no minimum-word-spacing control, so a
|
||||
justified line may end one word short. `02-blocks` reports this permanently.
|
||||
- **Omitted properties of built-in styles.** For a style it recognizes by name,
|
||||
LibreOffice fills what the file leaves out from its own defaults (a Heading with
|
||||
no spacing gets 0.42cm before). Word uses only what the file declares, and so do
|
||||
we — so fixtures must declare heading spacing and font explicitly, or the
|
||||
reference disagrees with Word rather than with us.
|
||||
A real-world document's name goes in no file, and a commit message names no document at
|
||||
all — write the rule that changed and what it measured. `FINDINGS.md` is gitignored for
|
||||
that reason.
|
||||
|
||||
Only this harness reads `fixtures/`. The directory is gitignored, so a `tests/unit/` test
|
||||
reading it fails with `ENOENT` on a fresh clone; it zips its own document instead
|
||||
(`docx-onoff.test.ts` — both importers take a `Uint8Array`, not a path). CI greps for the
|
||||
read.
|
||||
|
||||
## Prerequisites, part two: Calibri Light
|
||||
|
||||
@@ -82,146 +81,3 @@ editor (`global.css`), so give LibreOffice the same answer:
|
||||
</match>
|
||||
```
|
||||
|
||||
## Open findings
|
||||
|
||||
What the real-world fixtures — the contract (an IHK contract
|
||||
form), the summary (a student summary: bullets, wrapped
|
||||
pictures, one table), the thesis (a 48-page thesis with charts, metafile figures and
|
||||
a German TOC) the Math Guide (LibreOffice's own 63-page Math Guide: a title page,
|
||||
a three-page index, per-section masters, ~35 figure frames and 400 formulas) and
|
||||
the Writer Guide (its 458-page Writer Guide) — still report. The two guides and
|
||||
the thesis run off LibreOffice on page count (thesis 48 against 49, Math Guide 64 against
|
||||
63, Writer Guide 477 against 458), in each case downstream of a line the rules below cost
|
||||
or gained:
|
||||
|
||||
- **A page-anchored frame's coordinates are its page's, but a frame in a *header* keeps
|
||||
the ones its zone gives it.** Both are drawn out of flow; the body one (`anchorPage`,
|
||||
`image.ts`) sits behind the text at `--page-cycle` × (page − 1) + its `svg:y`.
|
||||
- **An index entry keeps its heading's hard break.** A book's chapter heading is its
|
||||
number and its title on two lines, and both producers copy that break into the entry —
|
||||
the Writer Guide's contents is 18 rows shorter without it. The leader and the page
|
||||
number ride the entry's last line (`align-items: last baseline`).
|
||||
- **The 458-page Writer Guide is a frontier of its own**: 19 pages over LibreOffice, with
|
||||
the drift accumulating (its index is one page short at "Autoformatting", two long by
|
||||
"Index"). Nothing in it has been diagnosed past the contents.
|
||||
- **The index is its own pagination.** A generated TOC is a block atom, so pagination has
|
||||
no inner position to put a spacer at; the node view breaks it between entries itself
|
||||
(`tableOfContents.ts`) and asks for one recompute when that changes its height.
|
||||
- **A hoisted text box loses the vertical offset it was anchored by.** A frame's own
|
||||
offsets are drawn now (`docs/architecture/frames.md`), and a lifted box takes its anchor
|
||||
paragraph's spacing (`docs/architecture/frames.md`), but a text box anchored inside a
|
||||
paragraph is a block node here, so the importer lifts it out and it simply follows that
|
||||
paragraph. On the thesis' figure page that leaves its caption 4.7mm high — LibreOffice
|
||||
places the box by its own 19.52cm, we place it after the picture.
|
||||
Attempted and **reverted**: a top margin of the offset minus the previous block's
|
||||
measured height overshot by 4.5mm (231.2 against 227.7, from 223.0) and improved
|
||||
nothing in the corpus. The anchor paragraph's box is not the right basis — the frame
|
||||
it holds is a float, so its height is the text's alone.
|
||||
- **A page's last line is where the footer band ends.** The body margin a file's header or
|
||||
footer reconstructs is the page margin plus the zone's declared height; the zone's own gap
|
||||
to the body sits *inside* that height (probed — `styleResolver.ts`). The height is the
|
||||
zone's *own* font: it is one paragraph here, so its mark struts every line of the band
|
||||
(`blockFontSize.ts` is in the zone schema, `hfReachPx` measures the same value). Getting
|
||||
that wrong costs a line on every page, which is worth more than any single block's
|
||||
spacing.
|
||||
- **Chromium floors every painted border to one pixel**, so a table's rows come out
|
||||
~0.25mm too tall each unless the excess is taken off the cell padding
|
||||
(`docs/architecture/tables.md`).
|
||||
- **A bulleted line is 0.12mm shorter here than in LibreOffice.** The numbering font joins
|
||||
the line it labels there; our marker is out of flow and CSS gives an outside `::marker`
|
||||
no metric influence either (probed: its `font-family` changes nothing, its `font-size`
|
||||
grows the line above the baseline where LibreOffice grows it below). The summary
|
||||
fixture's bullet pages drift ~1mm by their last line — that is all its `position`
|
||||
reports are.
|
||||
- **A formula page cannot be compared.** LibreOffice draws each formula from its own OLE
|
||||
frame; we typeset MathML, so `pdftotext` and `getClientRects` measure two different
|
||||
typesettings and the harness splits a row whose glyph boxes disagree. Honouring the
|
||||
frame's `style:vertical-pos` on a formula was tried and **reverted** — every inline
|
||||
formula sank ~1mm and the Math Guide went from 100 to 125 issues (CHANGELOG).
|
||||
- **Page geometry is document-wide.** Headers and footers are per section (each is
|
||||
editable in place), but margins, orientation and format are not: they come from the
|
||||
master page governing the most body blocks (`import/odt.ts`), so a title page with its
|
||||
own margins is laid out with the body's. The Math Guide's title sits 9.8mm low and
|
||||
3.3mm left for exactly that reason.
|
||||
- **Line height follows the paragraph, not the line.** The block's CSS strut applies to
|
||||
every line; a word processor takes each line's own runs and the paragraph mark only on
|
||||
the last. The mark's font rides the block (`blockFontSize.ts`), and a paragraph whose
|
||||
runs all agree takes theirs — which covers everything the corpus has. A paragraph of
|
||||
*mixed* sizes still struts at the block's.
|
||||
Attempted and **reverted**: `line-height: 0` on the block with the content's line-height
|
||||
restored by an inline decoration does drop the strut, and measured right on a purpose-built
|
||||
fixture (a 24pt run over 10pt text came to 62.1pt against LibreOffice's 61.8, from 69.0).
|
||||
It cost the contract 22 issues all the same. Two things the strut was quietly holding up:
|
||||
a line of no runs at all — two breaks in a row — collapses to nothing, and the reserved
|
||||
paragraph mark is an `::after` on the block, so the same rule takes the *last* line's
|
||||
height away with it. An empty widget span at the block's end gets that line back but not
|
||||
to the same value. Whatever replaces the strut has to carry both.
|
||||
- **Metafiles are placeholders, and a re-export keeps the placeholder.** EMF/WMF/SVM have
|
||||
no JS decoder, so such a frame keeps its box and its label but not its picture
|
||||
(`imageFormats.ts`) — and what is exported is that placeholder. **Charts** are drawn
|
||||
now (`import/chart.ts`), but as a *picture*: a re-export carries the drawing, not a
|
||||
chart object, since the editor has none.
|
||||
- **A list level's own hanging indent is not read**, only the 0.635cm both exports
|
||||
write (`LIST_HANGING_CM`). A **left**-set label wider than that overflows into the text,
|
||||
where Word moves the text to the next list tab — reading `w:lvl/w:pPr/w:ind w:hanging`
|
||||
would settle it. Deliberate for now: in flow, a wide marker pushed the text and cost
|
||||
the thesis fixture a page. (The level's *left* indent is read at every depth, and a
|
||||
**right**-set label — `markerAlign`, which is what the built-in Roman numberings use —
|
||||
grows into the margin, so the case that actually collided is gone.)
|
||||
What a probe of the ODF side turned up, before anything is built on it: LibreOffice
|
||||
honours a nested level's `fo:text-indent` linearly (−0.300 → label at 2.243cm, −1.000
|
||||
→ 1.543, −1.200 → 1.344, against `fo:margin-left="2.540cm"`) — **except** at −1.270,
|
||||
the value odf-kit writes for level 2, where it draws its own flat 0.635cm hanging
|
||||
(1.909cm) instead. So reading the attribute back naively moves the markers of our own
|
||||
exports. Whatever rule that is has to be understood before the value is trusted.
|
||||
- Lines take one word more or fewer than LibreOffice, which is most of the thesis'
|
||||
remaining `lineBreak` reports. Two causes, both measured on that fixture:
|
||||
- **LibreOffice compresses inter-word spaces to fit a line**, CSS justification only
|
||||
expands. Of 349 full-width justified lines, **91** are narrower in LibreOffice than
|
||||
their natural width here — up to **0.83px per space** at 12pt (p99 0.68px).
|
||||
Attempted and **reverted**: `word-spacing: -0.05em` on justified blocks. It does fix
|
||||
the break decision (the thesis' page 6 paragraph goes 7 lines → LibreOffice's 6), but
|
||||
the corpus got *worse* — `lineBreak` 45 → 43 while `lineEnd` went 2 → 16, because the
|
||||
tighter spacing also shortens every paragraph's unjustified last line by 1–3mm.
|
||||
- Sub-0.1mm engine rounding, which **accumulates**: matching word widths (11.86mm for
|
||||
`haben,` on both sides) had drifted 0.45mm apart by the 150th mm of the line.
|
||||
LibreOffice quantizes each glyph advance where Chromium keeps it fractional, and
|
||||
nothing in CSS exposes that.
|
||||
|
||||
## Findings that did not survive measurement
|
||||
|
||||
Kept here so they are not re-derived. Each was checked against `soffice` on a purpose-built
|
||||
one-page fixture.
|
||||
|
||||
- **A trailing space on a right-aligned line.** LibreOffice counts it exactly as we do
|
||||
(its last glyph lands at 189.0mm, ours at 188.9mm). Hanging it — which is what Word
|
||||
does — moved us 1.0mm *away* from the reference, so it was reverted.
|
||||
- **Breaking after a slash, and inside a run of dots.** Neither engine does either:
|
||||
`Sensor/Messinstrument` and a 50-dot leader stay one word in both. What LibreOffice
|
||||
really does that we did not is break a word *wider than its line*; that is fixed
|
||||
(`overflow-wrap: break-word`).
|
||||
- **A block ending in a hard break.** Real — the next paragraph sat 4.7mm too low — but
|
||||
the cause was not the trailing-break `<br>`: under `pre-wrap` that adds no line. It was
|
||||
the always-reserved paragraph mark, which is inline content *past* the break and so
|
||||
opened a line of its own. Fixed in `editor.css`.
|
||||
|
||||
## Known ceilings
|
||||
|
||||
- **A stop's leader is drawn, but the harness can't see it.** The fill is CSS generated
|
||||
content, so `Range.getClientRects()` reads the gap as empty while LibreOffice's PDF has
|
||||
the dots in its text. The contract's `Zwischen ......` line reports a `lineBreak` for
|
||||
that reason alone — compare it by eye, not by this harness. An **index** leader is real
|
||||
text on both sides, so it is compared: as one token, since how many dots fill the gap is
|
||||
the fill's business (`norm`). A TOC whose file declares *no* leader still gets ours.
|
||||
- Lines are grouped by vertical band, so **multi-column** text on one page merges
|
||||
columns into single lines. Columns fixtures need per-block grouping first.
|
||||
- **Only where text sits is compared** — not what it looks like. A dropped colour,
|
||||
highlight, fill, border or image changes nothing this harness can see, so a green
|
||||
run does not mean the page matches. Word's highlighter pen went unread through
|
||||
several rounds of green reports. Check that by eye: `pdftoppm -r 96 -png` the
|
||||
LibreOffice PDF beside a Playwright screenshot of the same page. Two traps there —
|
||||
the app's toolbar and ruler cover the top ~85px of a page in the viewport, and
|
||||
LibreOffice caches font matching per user profile, so a reused profile keeps
|
||||
substituting the font it resolved before `30-calibri-light.conf` existed.
|
||||
- LibreOffice is the reference, not Word. They agree on the layout rules exercised
|
||||
here, but not on everything (e.g. Word's own line-height rounding).
|
||||
Reference in new issue
Block a user