Skip to content

test(docx): hold every page reference to a bookmark the file holds - #969

Merged
DemchaAV merged 2 commits into
2.5-devfrom
test/docx-page-reference-bookmark-guard
Oct 26, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
test/docx-page-reference-bookmark-guard

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

Why

Word reads a PAGEREF field that names a bookmark the file does not hold as "Error! Bookmark not defined." once it updates its fields. The export writes a field only where it wrote the anchor a bookmark, and otherwise the number the layout read, with a report note; #967 and #968 gave a layer stack's column and a block of drawings alone their bookmarks. That rule was held shape by shape: each test read the fields and bookmarks a paragraph holds directly, so a header's field, a text box's, a simple field, or a bookmark outside a paragraph went unread, as did any shape no test had built. The template corpus declares no anchor, so the fidelity gate never saw a page reference at all.

What changed

  • DocxPageReferenceFields (qa, test scope) reads every PAGEREF field, bookmark and paragraph from each XML part of the exported package — the body, headers, footers, nested tables, text boxes — with StAX, rather than from the paragraphs a model lists. It reads complex fields whose instruction runs over several runs, nested fields, simple ones, and the keyword in any case. problems() names a field that names a bookmark the body does not hold, or a name Word does not give a bookmark (a letter, then letters, digits and underscores, 40 at most) — the two Word turns into its error — and one naming more than one bookmark, one in another case or one never closed, since the export names exactly the bookmark it wrote; and field marks out of order or of no type, an instruction where no field's code is open, a simple field closed over an open one, and a bookmark id opened twice.
  • PageReferenceDocxDocuments — four documents that refer to pages, each listing every reference it holds and what it must be written as and read; the two whose shapes the export's report names (the split tables, the page nothing is written on) list those notes too, and the rest are held by the bookmarks their fields name:
    • anchor_kinds: an anchor on each of the nine node kinds that declare one — paragraph, section, container, table, picture, barcode, rule, shape, ellipse — blocks of drawings alone, and anchors named by sentences past Word's 40 characters;
    • reference_places: references standing in a table's composed cell, a layer stack's column, a panel, a row's column and the body, and anchors in each of those;
    • unmarked: the anchors the export writes no bookmark for — a section that draws nothing, one whose first drawing fills a page nothing is written on, one composed in a table's cell, an anchor nothing declares — beside names that clean or fold to another's (a. and a, Intro and intro);
    • margin_sections: a block whose shape a row of a table split for page 2's margins carries, a block over a table split under a copy of its header row, an anchor in a header row the page repeats.
      A reference written as its number stands between paragraphs of its own, so the guard finds it, reads its number, and tells one reading nothing from one not written.
  • DocxPageReferenceGuardTest:
    • over the template corpus: every field names one closed bookmark of the body, and the corpus's page references equal its fields plus its references written as numbers, named;
    • over the four documents: no problem; a document's report holds the notes of the shapes it lists; the fields are exactly the expected ones, each on its anchor's bookmark, reading the page the layout resolved; every other reference is its number — read off its paragraph, the paragraph after it read too — with the report note, never a field; and the documents refer to an anchor of each kind the node classes declare one on, read off the core's node packages;
    • each document must be exported from its layout: a document the layout cannot lay out is exported without it (the report's measured geometry note), and its references read no page;
    • the reader on files written by hand: a field in a header, a nested table and a text box is found; an instruction split over runs, a nested field, a lowercase keyword and a simple field are read; a missing bookmark, one only in a header, one held twice, in another case or never closed, a name past 40 characters, a field left open, a stray mark, a mark of no type, an instruction outside a field's code, a simple field closed over an open one and a bookmark id opened twice are each a problem.
  • DocxFidelityCorpusTest.composed opens a corpus document on its page, so the guard exports it as the fidelity gate does; exportTo uses it.
  • Docs: the DOCX recipe and the capability matrix name the three cases measured below, the matrix's DOCX cell for page references going from ✅ to ⚠️, its legend's mark for a documented deviation; the CHANGELOG's Tests entry.

Measured

The four documents in Word 16 (every field updated, as F9 on each story) and LibreOffice 24.2, then the same files with a page put in at the start:

Word 16 LibreOffice 24.2
27 fields each reads its bookmark's page, then the next 25 read their page, then the next
8 references written as text do not move do not move
field in a table's composed cell stores nothing; reads 2, then 3, once updated reads 2, then 3
reference as text in a table's composed cell, to a drawn shape empty (the PDF prints 2) empty
anchor named table reads 2, then 3 reads nothing
anchored panel right under a layer stack's columns reads 2, then 3 "Error: Reference source not found"
anchor in a repeated header row stores 8 (the last repeat, where the page sends a link), reads 7 (where the file holds the row) reads 7
  • A reference in a table's composed cell stores no number: the layout keeps no text for what such a cell holds. As a field, the file opens without the number the PDF prints until Word updates its fields; written as text, where its anchor has no bookmark, it is empty for good, though its note says the number is written.
  • LibreOffice reads a PAGEREF as empty where its bookmark is named like one of its numbering sequences: Table (table, TABLE too), Figure, Illustration, Drawing and Text read nothing; Equation, Listing and Caption read their page.
  • A block that opens with a table right after another table is bookmarked from the paragraph Word keeps between the two tables (separateFromTheTableAbove), which stays with the table below it, to the block's last line. LibreOffice drops a bookmark opened on that paragraph: opened on the panel's first line instead, the same field reads 2. A bookmark opened on an ordinary paragraph above a table, or closed on one below it, is kept: a section opening with a paragraph and ending with a table, and one the other way round, read their page in both editors, and 3 with a page put in.
  • The repeated header row reads as the recipe already says.

No template declares an anchor, so none of the three reaches the corpus; they are named here and in the recipe, not changed. The guard pins the composed cell's empty number as it stands, so a fix flips that expectation.

Notes

  • The corpus check exports the 67 documents to DOCX in every qa run, about 30 s: the corpus has no page reference today, and the export is what tells, wherever a template later puts one.

Verification

  • New DocxPageReferenceGuardTest (11 tests) — above.
  • Sabotages: 25 sabotages each undo one rule once, and every one fails a test; the files are byte-identical afterwards. In the export: a block's, a block of drawings' and a paragraph's bookmark start dropped, a block's bookmark never closed, a field written on a bookmark the file lacks, the note on a reference written as its number dropped, that number dropped, a reference reading nothing taken out with its paragraph. In the guard: one bookmark taken out of the exported file; the reader reading the body part alone, matching names with their case, taking an unclosed bookmark, any name or a bookmark of any part, reading an instruction's first run alone, not naming a stray mark, a mark of no type, an instruction outside a field's code, a simple field closed over an open one or a bookmark id opened twice; a referring document, or the corpus, composed where the layout cannot lay it out; the corpus check reading nothing; a split a document is built to hold not made; no anchor of one kind referred to.
  • Template corpus (67 DOCX) against 2.5-dev: all 67 byte-identical; report notes 1491 → 1491.
  • clean verify of the reactor as CI runs it → BUILD SUCCESS (render-docx 2293, qa 1846); the four knowledge checks and core javadoc (0 warnings) pass; examples 93.

Lane: test (qa guard over the semantic DOCX backend's page references), no public API change.

Word reads a PAGEREF field naming a bookmark the file lacks as "Error!
Bookmark not defined." once it updates its fields. The export writes a
field only where it wrote the anchor a bookmark, and the number with a
report note elsewhere; that was held shape by shape, by tests reading
the fields and bookmarks a paragraph holds directly.

DocxPageReferenceGuardTest reads every PAGEREF field, bookmark and
paragraph from each XML part of the exported file and fails on a field
naming a bookmark the body does not hold or a name Word does not take,
or naming more than one bookmark, one in another case or one never
closed. It runs over the template corpus and over four documents
referring to an anchor on each kind of node that declares one, from
each place a reference stands; the anchors written no bookmark must come
out as their number, read off its paragraph, with a report note.

The DOCX recipe and the capability matrix name what Word 16 and
LibreOffice 24.2 read differently: a reference in a table's composed
cell stores no number, and LibreOffice reads neither a bookmark named
like one of its numbering sequences nor one of a block that opens with
a table right after another table.
A reference whose anchor has no bookmark and no page reads nothing, as
the page prints it; read off the paragraph after its label, a missing
reference read the same. Each such reference now stands between
paragraphs of its own, and the guard reads the one after it too.

The reader names an instruction where no field's code is open, a
simple field closed over an open one and a bookmark id opened twice,
each now held by a test, and reads an anchor's kind only off a String
component.

Measured in Word 16 and LibreOffice 24.2, a bookmark running from a
body paragraph into a table's cell, or out of one, is kept: LibreOffice
drops only one opened on the paragraph written between two tables. The
recipe says so, points to the repeated header row, and names the cases
of `Table` measured; the capability matrix marks the DOCX page
reference ⚠️, a documented deviation, as its legend has it.
@DemchaAV
DemchaAV marked this pull request as ready for review October 26, 2026 15:55
@DemchaAV
DemchaAV merged commit 00c2ede into 2.5-dev Oct 26, 2026
11 checks passed
@DemchaAV
DemchaAV deleted the test/docx-page-reference-bookmark-guard branch October 26, 2026 16:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant