Skip to content

fix(docx): bookmark a block of drawings alone where its first drawing is anchored - #968

Merged
DemchaAV merged 3 commits into
2.5-devfrom
fix/docx-bookmark-a-block-without-a-paragraph
Oct 26, 2026
Merged

DemchaAV merged 3 commits into
2.5-devfrom
fix/docx-bookmark-a-block-without-a-paragraph

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

Why

A section, a container or a layer stack's column that holds nothing but drawings writes no paragraph: its drawings are anchored in the paragraphs beside them, or in one written on their page, when the section ends. bookmarkAround had nothing to go round, so its anchor got no bookmark:

  • a link to it (linkTo, a table of contents' entry) named a bookmark the file did not hold;
  • a page reference to it was written as the number the layout read, with a report note — a number no edit updates.

Measured in Word 16 (fields updated, as F9 on the whole text) and LibreOffice 24.2, on such a block on page 2 and on the same file with a page put in at its start, so a live field must move to 3: both editors read 2 in both files and carried no link, for every shape of block tried.

What changed

  • DocxDrawingAnchors keeps the paragraph each shape is anchored in, by the shape's number (Ordered#drawn), whichever way it is anchored: beside a paragraph's text, in a paragraph on its page, or in a table cell. It also keeps the shapes a section's end finds nothing to carry. drawnSoFar() reads how many shapes the document has numbered; carrierOfTheFirst(from, to) returns the paragraph of the first of a range — the shape on the earliest page, highest on it — or null where none of them is anchored, or the first stands on a page nothing carries: a later page's paragraph is not where the block starts.
  • writePlacedNode and the column writer note how many shapes were numbered before a block was written. A block that wrote no paragraph is kept with that range (UnmarkedBlock), as it was kept to be named.
  • markTheBlocksOfDrawingsAlone runs at each section's end, once its drawings are anchored and before its pages are given their margins. It puts each such block's bookmark at the start of the paragraph holding its first drawing, opened and closed there: a place, not a range, the paragraph's text not being the block's. Word keeps a drawing on the page of the paragraph it is anchored in and moves it with that paragraph, so a page reference reads the page the block starts on and goes on reading it after edits above; a link lands beside the drawing. Before the margins: a table split for its pages' margins (DocxTableSplit.split) copies the table and takes the moved rows out of the first part, and a row's paragraph read before that is out of the file.
  • nameTheBlocksNoBookmarkMarks runs at the document's end, as before, on the blocks left: one that draws nothing, one whose first drawing stands on a page nothing is written on, and one composed in a table's cell, whose drawings its table draws as its own. Their note now says "writes no paragraph, and no paragraph carries the first drawing of its own". It runs after every section's blocks of drawings are marked, so a block that draws nothing is not named where one of drawings marks the same anchor.
  • settleThePageReferences and the links need nothing new: a field is named after the bookmark its anchor was written under, and a link already names the bookmark by its anchor.
  • startBookmark opens a bookmark and records it (name, bookmarksWritten, bookmarksOfAnchors): bookmarkAround and openAnchor each wrote those lines, and the new bookmarkAtTheStartOf uses it too.
  • DocxTableSplit: a body table's split and a panel's cut copy the header rows without their bookmarks, as a panel's cut already copied them without their drawings. A split is refused where a header row holds a bookmark when it is decided (rowStarting), but a block's bookmark is written after that, before the split is made: copied with its name and id, a table's split would have both copies go when the export keeps the last bookmark of each name, leaving the field on none, and a panel's cut made after that would leave two bookmarks of one name and id. The first part keeps it, on the table's first page.
  • A drawn shape's own anchor — a shape, an ellipse, a line not written as a rule — still gets no bookmark, as before; only a block holding drawings is marked here.
  • Docs: the recipe's links and anchors row, its page reference paragraph and its drawing limits; the capability matrix's internal links row; the CHANGELOG, whose entry for the column bookmark said a column of drawings alone had none.

Measured

Each case on a 400×500pt page (the last, 400×300), then the same DOCX with a page put in at its start (the stored field still reads the old number). Each cell: what the reference reads in the file as written / in the shifted one, and where the link goes.

Case Word 16, before Word 16, after LibreOffice 24.2, before LibreOffice 24.2, after
section of drawings on page 2, under three lines 2 / 2, no link 2 / 3, link to the page 2 / 2, no link 2 / 3, link to the page
container of drawings, the same 2 / 2, no link 2 / 3, link 2 / 2, no link 2 / 3, link
block opening page 2 2 / 2, no link 2 / 3, link 2 / 2, no link 2 / 3, link
block alone on page 2 (the closing paragraph holds it) 2 / 2, no link 2 / 3, link 2 / 2, no link 2 / 3, link
block at the foot of page 1 1 / 1, no link 1 / 2, link 1 / 1, no link 1 / 2, link
block starting at the foot of page 1, its circle on page 2 1 / 1, no link 1 / 2, link 1 / 1, no link 1 / 2, link
block in a panel on page 2 2 / 2, no link 2 / 3, link 2 / 2, no link 2 / 3, link
layer stack's column of a circle on page 2 2 / 2, no link 2 / 3, link 2 / 2, no link 2 / 3, link
anchor "Terms and conditions of the agreement between both parties" on drawings 2 / 2, no link 2 / 3, link 2 / 2, no link 2 / 3, link
block at page 2's foot after a table split for page 2's other margins, a row holding its shape not run 2 / 3, link not run 2 / 3, link
section of drawings composed in a table's cell 2 / 2, no link unchanged, named 2 / 2, no link unchanged, named
anchored section with nothing in it empty, no link unchanged, named empty, no link unchanged, named
  • The anchor named with spaces cleans to Terms_and_conditions_of_the_agreement_be, Word's 40 characters; the bookmark, the link and PAGEREF name it alike. On a section of text the same reference read 2 / 3 before this change already: the name is made once per anchor (DocxBookmarkNames), so it is the same when the bookmark is written and when the field is settled.
  • The anchored empty section resolves to nothing on the page either: the PDF prints no number for the reference and carries no link.
  • An empty table cannot reach the export: the layout refuses a table without rows.
  • No document of the template corpus, and no template, declares an anchor; the corpus holds no bookmark, page reference or internal link.

Verification

  • New DocxDrawnBlockBookmarkTest (14 tests):
    • a section of drawings alone is marked at the start of the paragraph its higher drawing is anchored in, and the paragraph holding its lower one holds no mark; a container likewise; each reference is PAGEREF <anchor> \h reading 2, the link names it, and no note mentions a bookmark;
    • a block starting at a page's foot is marked on the page it starts on, reading 1;
    • a block opening its page is marked in the paragraph under it; one alone on its page in the paragraph closing the section; one in a panel in the panel's cell;
    • a block whose shape a row holds, in a table split for page 2's other margins, is marked in that row, in the table's second part;
    • a block opening a page above a table with a repeated header row, split for page 3's other margins, is marked once, in the paragraph holding its room above the table, and neither part's header row holds a bookmark;
    • an anchor named with spaces past 40 characters is one name for its bookmark, its link and its reference, on drawings and on text;
    • the block's name is given after the body named its anchors: a paragraph anchored Intro keeps Intro, the block anchored intro takes intro_2;
    • two blocks whose drawings one paragraph carries keep a mark each;
    • a paragraph declaring the anchor after the block keeps the bookmark, and the anchor is named;
    • an empty block is not named where a later block of drawings marks its anchor, in its own section or in a later one.
  • DocxDrawingAnchorsTest (+4): the carrier of a range is the shape's on the earliest page, highest on it; a shape anchored in a cell is carried by the cell's paragraph; shapes whose first nothing carries have none, though a later one is carried; carriers are kept across sections and forgotten by reset.
  • DocxTableSplitTest (+2): a body table's split and a panel's cut leave a header row's bookmark, start and end, in the first part and out of the copy.
  • Changed: DocxAnchorWithoutABookmarkTest holds the blocks that still have no bookmark — one that draws nothing, one whose first drawing fills a page nothing is written on (+1), one composed in a table's cell — with the new note; DocxLayerColumnBookmarkTest (a column of drawings is marked; an empty column is named); DocxPanelOverPagesTest refers to a drawn shape's own anchor to get a reference settled as its number; the node and value ledgers.
  • Sabotages: 19 sabotages each undo one part of the change — no holder looked up, the latest page or the lower drawing taken for the first, a shape beside a paragraph, on its page, in a cell or carried by nothing left unrecorded, the mark made a range, a block or a column counting its drawings from after it is written, a block named where another node marks its anchor or before every holder is marked, holders marked after the margin split, carriers kept across exports, a moved bookmark read after the move, a holder left out of the page references, a header row's bookmark copied by a table's split or a panel's cut, blocks named at each section's end — and every one fails a test; the files are byte-identical afterwards.
  • 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 1835); the four knowledge checks and core javadoc (0 warnings) pass; examples 93.

Lane: shared-engine (render-docx semantic DOCX backend), no public API change.

… is anchored

A section, a container or a layer stack's column holding nothing but
drawings writes no paragraph for its anchor's bookmark to go round, so a
link to the anchor named a bookmark the file did not hold and a page
reference to it was written as a fixed number.

DocxDrawingAnchors now keeps the paragraph each shape is anchored in, by
the shape's number, and the shapes nothing carries. At each section's end,
once its drawings are anchored and before a table is split for its pages'
margins, the export bookmarks such a block at the start of the paragraph
holding its first drawing - the one on the earliest page, highest on it.
Word keeps a drawing on its paragraph's page and moves it with that
paragraph, so the reference reads the block's page and follows it through
edits. A block that draws nothing, whose first drawing nothing carries, or
that is composed in a table's cell keeps its report note.

Measured in Word 16 (fields updated) and LibreOffice 24.2 on such a block on
page 2 and on the same file with a page put in ahead of it: before, both
read 2 in each and carried no link; now both read 2 and then 3, and the
link goes to the block's page. The template corpus declares no anchors and
is unchanged.
…t makes

A table is split for its pages' margins, or a panel cut at a page, under a
copy of its header rows. The split is refused where a header row holds a
bookmark when it is decided, but a block of drawings alone is now marked
after that, in the paragraph holding its first drawing, before the split is
made. A bookmark copied with its name and id would leave two of one id, and
keeping the last bookmark of each name would take both out, leaving a page
reference on a bookmark the file lacks.

DocxTableSplit.split and splitInItsPanel now copy the header rows without
their bookmark starts and ends, as a panel's cut already copied them
without their drawings; the first part keeps the bookmark, on the table's
first page.

Also: an end-to-end test of a block whose first drawing fills a page
nothing is written on, which is named rather than marked on a later page,
and of a block above a table split for its pages' margins, marked once.
…e marked

A block that writes no paragraph and draws nothing is named only where no
other node marks its anchor, which is known once every section's blocks of
drawings alone are marked. The test pinning it held one section; a second
holds the empty block in the first section and the block of drawings in the
second.

The Javadoc, the test class's and the recipe now say which block is named:
one whose first drawing no paragraph carries as its own. A block whose first
drawing stands on a page nothing is written on may have a later drawing
carried, and is named all the same.
@DemchaAV
DemchaAV marked this pull request as ready for review October 26, 2026 12:57
@DemchaAV
DemchaAV merged commit efef567 into 2.5-dev Oct 26, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-bookmark-a-block-without-a-paragraph branch October 26, 2026 13:08
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