Skip to content

fix(docx): name a block that writes no paragraph for its anchor's bookmark - #936

Merged
DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-anchor-without-bookmark
Oct 21, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-anchor-without-bookmark

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Why

The DOCX export marks an anchored block with a bookmark round the paragraphs it writes
(bookmarkAround). A section or a container of drawings alone writes no paragraph: its drawings
stand in the paragraph beside them. It got no bookmark, while a link to its anchor was still
written as w:hyperlink w:anchor pointing at it.

Measured in Word 16 and LibreOffice on Windows, with a paragraph linking to such a section:

  • the GraphCompose PDF links to the section's place on page 1;
  • both editors' PDFs carry no link at all;
  • the export report said nothing.

A drawn shape, a stack's column and a page zone's paragraph or picture already name this loss in
their own notes, and a page reference to such an anchor already named itself.

What changed

  • bookmarkAround returns whether it marked anything. As before, it asks for a bookmark name
    only where it marks something; a test now pins that. A name is kept once given, without case: one
    given for nothing would push another anchor that cleans to it onto a suffix.
  • writePlacedNode records a block that marked nothing (UnmarkedBlock), with the note count
    before and after it was written.
  • nameTheBlocksNoBookmarkMarks runs once every bookmark is written. It names a recorded block
    only where no other node's bookmark carries the same anchor: where one does, a link goes there,
    and keepTheLastBookmarkOfEachAnchor names the anchor two nodes declare.
    • The loss goes into the block's own note where it has one, found by subject and path among the
      notes added while the block was written (DocxExportReport.Builder.addTo). A path is its
      section's own: a later section's node, or a later place the same node is written, may have the
      same one, and is not looked at. Otherwise the block
      gets a note of its own: "writes no paragraph, so its anchor "…" has no bookmark in the Word
      file: a link to it points at none, and a page reference to it is a fixed number rather than a
      field".
    • That note is added at the end, not put in among the others. settleThePageReferences puts its
      notes in by the places it counted while writing, and a note put in earlier would move them.
    • A block composed in a table's cell has no path to tell it from the nodes inside it, so it always
      gets a note of its own.
  • DocxNodeFieldLedgerTest: SectionNode.anchor and ContainerNode.anchor now say the anchor
    is reported where the block writes no paragraph.

The DOCX bytes do not change; only the report does.

Other block kinds that go through bookmarkAround always write something:

  • nodes turn a blank anchor into none;
  • the layout rejects a table with no rows;
  • an image with no bytes cannot be built;
  • the checks that draw a picture where the page puts it leave an anchored one in its paragraph;
    tried beside its label, it was written inline and bookmarked.

Verification

  • Probe, in Word and LibreOffice:
    • a link to a section of one shape, plain and inline, links nowhere in either editor;
    • a link to a paragraph anchor works in both;
    • a page reference to the section was already named.
    • With the change, every probe case's DOCX is byte-identical to the base. The section cases now
      carry the note, and the drawn shape keeps its one note.
  • Corpus: all 62 DOCX byte-identical to 2.5-dev, and the 1246 notes unchanged. No corpus
    document has such an anchor.
  • Tests: DocxAnchorWithoutABookmarkTest (new, 12 tests):
    • a section, and a container, of drawings alone are named, the link still written and no bookmark
      in the file;
    • a section with an outline entry names both losses in one note;
    • a section in a panel's cell is named;
    • in a table's composed cell, the anchored section has its own note, and the section inside keeps
      its own;
    • an anchor another node marks is not named on the block;
    • a section that writes a paragraph is bookmarked and not named;
    • an anchor nothing links to is named all the same;
    • a block with nothing to mark takes no bookmark name;
    • in a document of two sections, a later section's node at the same path keeps its own note, and
      two blocks at one path have a note each;
    • a page reference after the block keeps its note in its place.
  • 8 sabotages, all caught:
    • no block named;
    • a second note beside the node's own;
    • the node's notes looked for after they were written;
    • a node with no path told by it;
    • a name given for nothing;
    • named though another node marks it;
    • looked for past the block's own notes;
    • put in among the other notes.
  • Gate: ./mvnw -B -ntp clean verify on the reactor: BUILD SUCCESS (render-docx 1955, qa 1828),
    plus the JDK 17 knowledge checks and core Javadoc.

Not changed here

Writing a bookmark for such a block is left for later: a collapsed one in the paragraph that holds
its first drawing, so a link lands where the page sends it. The report now says where it is missing.
It names the block and its anchor, not the linking paragraph.

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

…kmark

A bookmark goes round the paragraphs a block writes. A section or a
container of drawings alone writes none, its drawings standing in the
paragraph beside them, and a link to its anchor is written to a bookmark
the file lacks. Measured in Word and LibreOffice, the page's link went
to the section's place, and both editors' PDFs carried no link at all.
The report said nothing.

Such a block is now named, with its anchor, once every bookmark is
written and only where no other node's bookmark carries that anchor: a
link to it points at none, and a page reference to it is a fixed number.
A block with a note of its own has the loss added there. A bookmark name
is asked for only where a block marks something, so one given for
nothing cannot push another anchor onto a suffix. The file itself is
unchanged.
@DemchaAV
DemchaAV marked this pull request as draft October 21, 2026 13:37
A block that writes no paragraph for its anchor's bookmark has the loss
added to its own note, found by subject and path from the note count
before it was written. The search ran on to the report's end, and a path
is its section's own. In a document of two sections, the second's first
block, with a note for its outline entry, had the first's anchor added
to that note, and the first went unnamed; two such blocks at one path
shared one note. The search now stops at the note count after the block
was written.
@DemchaAV
DemchaAV marked this pull request as ready for review October 21, 2026 13:57
@DemchaAV
DemchaAV merged commit e17f7bf into 2.5-dev Oct 21, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-anchor-without-bookmark branch October 21, 2026 14:14
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