Skip to content

Bookmark: every bookmark is emitted with w:id="1" (per-instance id generator), so multiple bookmarks collide #3478

Description

@alexvcasillas

Summary

When a document contains more than one Bookmark, every w:bookmarkStart/w:bookmarkEnd is written with the same w:id="1". The numeric bookmark ids are meant to be unique per document, so multiple bookmarks collide, which makes start/end pairing ambiguous and produces an invalid document (Word cannot reliably resolve PageReference / internal hyperlinks to the intended target).

Reproduction

import { Document, Packer, Paragraph, TextRun, Bookmark } from "docx";

const doc = new Document({
  sections: [{
    children: [
      new Paragraph({ children: [new Bookmark({ id: "AnchorA", children: [new TextRun("A")] })] }),
      new Paragraph({ children: [new Bookmark({ id: "AnchorB", children: [new TextRun("B")] })] }),
    ],
  }],
});

const buf = await Packer.toBuffer(doc);
// unzip word/document.xml

Resulting word/document.xml (abridged):

<w:bookmarkStart w:name="AnchorA" w:id="1"/> ... <w:bookmarkEnd w:id="1"/>
<w:bookmarkStart w:name="AnchorB" w:id="1"/> ... <w:bookmarkEnd w:id="1"/>

Both bookmarks receive w:id="1".

Root cause

In src/file/paragraph/links/bookmark.ts, the id generator is created per Bookmark instance:

private readonly bookmarkUniqueNumericId = bookmarkUniqueNumericIdGen();
// ...
const linkId = this.bookmarkUniqueNumericId();

bookmarkUniqueNumericIdGen() returns a fresh counter starting at 1, and each instance calls its own counter exactly once, so every bookmark gets 1. (Container types such as Numbering share one generator across all their children, which is why their ids increment correctly; Bookmark has no shared container, so each instance is isolated.)

Expected

Each bookmark in a document should receive a distinct numeric w:id, e.g. 1, 2, 3, with the matching bookmarkStart/bookmarkEnd sharing that id.

Suggested fix

Use a single module-scoped generator shared by all Bookmark instances (uniqueness is only required within a document, and a monotonic module-level counter satisfies that), for example:

const bookmarkNumericId = bookmarkUniqueNumericIdGen();

export class Bookmark {
  public constructor(options: IBookmarkOptions) {
    const linkId = bookmarkNumericId();
    this.start = new BookmarkStart(options.id, linkId);
    this.children = options.children;
    this.end = new BookmarkEnd(linkId);
  }
}

Version

Reproduced on docx@9.7.1 and on current master.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions