-
-
Notifications
You must be signed in to change notification settings - Fork 612
fix: allocate bookmark numeric ids per document #3502
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
alexvcasillas
wants to merge
3
commits into
dolanmiu:master
Choose a base branch
from
alexvcasillas:fix/shared-bookmark-numeric-id
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+276
−43
Open
Changes from 1 commit
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import { BookmarkIds } from "./bookmark-ids"; | ||
|
|
||
| describe("BookmarkIds", () => { | ||
| it("should number bookmarks from one, in the order they are asked for", () => { | ||
| const ids = new BookmarkIds(); | ||
|
|
||
| expect(ids.getId("first")).to.equal(1); | ||
| expect(ids.getId("second")).to.equal(2); | ||
| expect(ids.getId("third")).to.equal(3); | ||
| }); | ||
|
|
||
| it("should return the same id for the same name, so start and end markers pair", () => { | ||
| const ids = new BookmarkIds(); | ||
|
|
||
| const first = ids.getId("intro"); | ||
| ids.getId("other"); | ||
|
|
||
| expect(ids.getId("intro")).to.equal(first); | ||
| }); | ||
|
|
||
| it("should start again at one for a new instance, so ids are per document", () => { | ||
| const first = new BookmarkIds(); | ||
| first.getId("a"); | ||
| first.getId("b"); | ||
|
|
||
| const second = new BookmarkIds(); | ||
|
|
||
| expect(second.getId("a")).to.equal(1); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| /** | ||
| * Per-document numeric id allocation for bookmarks. | ||
| * | ||
| * @module | ||
| */ | ||
|
|
||
| /** | ||
| * Allocates the numeric ids written to `w:bookmarkStart` and `w:bookmarkEnd`. | ||
| * | ||
| * Ids must be unique within a document, and a bookmark's start and end must | ||
| * share one. Both markers look up their bookmark name here, so whichever is | ||
| * serialized first allocates and the other reuses. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const ids = new BookmarkIds(); | ||
| * ids.getId("intro"); // 1 | ||
| * ids.getId("summary"); // 2 | ||
| * ids.getId("intro"); // 1 | ||
| * ``` | ||
| */ | ||
| export class BookmarkIds { | ||
| // eslint-disable-next-line functional/prefer-readonly-type | ||
| private readonly ids: Map<string, number>; | ||
|
|
||
| public constructor() { | ||
| this.ids = new Map<string, number>(); | ||
| } | ||
|
|
||
| /** | ||
| * Returns the id for a bookmark name, allocating one on first use. | ||
| * | ||
| * @returns The id shared by that bookmark's start and end markers | ||
| */ | ||
| public getId(name: string): number { | ||
| const existing = this.ids.get(name); | ||
|
|
||
| if (existing !== undefined) { | ||
| return existing; | ||
| } | ||
|
|
||
| const id = this.ids.size + 1; | ||
| // eslint-disable-next-line functional/immutable-data | ||
| this.ids.set(name, id); | ||
|
|
||
| return id; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,41 +1,84 @@ | ||
| import { assert, beforeEach, describe, expect, it } from "vitest"; | ||
| import { beforeEach, describe, expect, it } from "vitest"; | ||
|
|
||
| import { Utility } from "tests/utility"; | ||
| import { Formatter } from "@export/formatter"; | ||
| import type { IContext } from "@file/xml-components"; | ||
|
|
||
| import type { IViewWrapper } from "../../document-wrapper"; | ||
| import type { File } from "../../file"; | ||
| import { TextRun } from "../run"; | ||
| import { Bookmark } from "./bookmark"; | ||
| import { Bookmark, BookmarkStart } from "./bookmark"; | ||
| import { BookmarkIds } from "./bookmark-ids"; | ||
|
|
||
| const documentContext = (): IContext => ({ | ||
| file: { BookmarkIds: new BookmarkIds() } as unknown as File, | ||
| viewWrapper: {} as unknown as IViewWrapper, | ||
| stack: [], | ||
| }); | ||
|
|
||
| describe("Bookmark", () => { | ||
| let context: IContext; | ||
| let textRun: TextRun; | ||
| let bookmark: Bookmark; | ||
|
|
||
| beforeEach(() => { | ||
| bookmark = new Bookmark({ | ||
| id: "anchor", | ||
| children: [new TextRun("Internal Link")], | ||
| }); | ||
| context = documentContext(); | ||
| textRun = new TextRun("Internal Link"); | ||
| bookmark = new Bookmark({ id: "anchor", children: [textRun] }); | ||
| }); | ||
|
|
||
| it("should create a bookmark with three root elements", () => { | ||
| const newJson = Utility.jsonify(bookmark); | ||
| assert.equal(newJson.rootKey, undefined); | ||
| assert.equal(newJson.start.rootKey, "w:bookmarkStart"); | ||
| assert.equal(newJson.children[0].rootKey, "w:r"); | ||
| assert.equal(newJson.end.rootKey, "w:bookmarkEnd"); | ||
| expect(new Formatter().format(bookmark.start, context)).to.have.property("w:bookmarkStart"); | ||
| expect(new Formatter().format(textRun, context)).to.have.property("w:r"); | ||
| expect(new Formatter().format(bookmark.end, context)).to.have.property("w:bookmarkEnd"); | ||
| }); | ||
|
|
||
| it("should create a bookmark with the correct attributes on the bookmark start element", () => { | ||
| const newJson = Utility.jsonify(bookmark); | ||
| const tree = new Formatter().format(bookmark.start, context); | ||
|
|
||
| assert.equal(newJson.start.root[0].root.name, "anchor"); | ||
| expect(tree["w:bookmarkStart"]._attr["w:name"]).to.equal("anchor"); | ||
| }); | ||
|
|
||
| it("should create a bookmark with the correct attributes on the text element", () => { | ||
| const newJson = Utility.jsonify(bookmark); | ||
| assert.equal(JSON.stringify(newJson.children[0].root[1].root[1]), JSON.stringify("Internal Link")); | ||
| it("should keep the bookmark's children", () => { | ||
| expect(bookmark.children).to.deep.equal([textRun]); | ||
| expect(JSON.stringify(new Formatter().format(textRun, context))).to.contain("Internal Link"); | ||
| }); | ||
|
|
||
| it("should create a bookmark with the correct attributes on the bookmark end element", () => { | ||
| const newJson = Utility.jsonify(bookmark); | ||
| expect(newJson.end.root[0].root.id).to.be.a("number"); | ||
| const tree = new Formatter().format(bookmark.end, context); | ||
|
|
||
| expect(tree["w:bookmarkEnd"]._attr["w:id"]).to.be.a("number"); | ||
| }); | ||
|
|
||
| it("should pair the start and end elements with the same id", () => { | ||
| const start = new Formatter().format(bookmark.start, context); | ||
| const end = new Formatter().format(bookmark.end, context); | ||
|
|
||
| expect(start["w:bookmarkStart"]._attr["w:id"]).to.equal(end["w:bookmarkEnd"]._attr["w:id"]); | ||
| }); | ||
|
|
||
| // Regression: a per-instance generator gave every bookmark `w:id="1"`, so | ||
| // start and end pairing was ambiguous and Word could not resolve references. | ||
| it("should give each bookmark in a document a distinct id", () => { | ||
| const bookmarks = ["first", "second", "third"].map((id) => new Bookmark({ id, children: [new TextRun(id)] })); | ||
|
|
||
| const ids = bookmarks.map((item) => new Formatter().format(item.start, context)["w:bookmarkStart"]._attr["w:id"]); | ||
|
|
||
| expect(ids).to.deep.equal([1, 2, 3]); | ||
| }); | ||
|
|
||
| it("should number bookmarks from one in every document", () => { | ||
| const first = new Bookmark({ id: "a", children: [new TextRun("a")] }); | ||
| new Formatter().format(first.start, context); | ||
|
|
||
| const otherDocument = documentContext(); | ||
| const second = new Bookmark({ id: "b", children: [new TextRun("b")] }); | ||
|
|
||
| expect(new Formatter().format(second.start, otherDocument)["w:bookmarkStart"]._attr["w:id"]).to.equal(1); | ||
| }); | ||
|
|
||
| it("should keep an explicitly supplied id", () => { | ||
| const tree = new Formatter().format(new BookmarkStart("named", 7), context); | ||
|
|
||
| expect(tree["w:bookmarkStart"]._attr["w:id"]).to.equal(7); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Reserve explicit IDs in the document registry.
Line 42 allocates from
ids.sizeonly. IfBookmarkStart("fixed", 1)is formatted,BookmarkStart.prepForXmlinsrc/file/paragraph/links/bookmark.tsat Line 125 emits1without updating this registry. A later implicit bookmark then also receives1.Track used numeric IDs and reserve the name-to-ID mapping for explicit start IDs. Reject conflicting explicit IDs. Add tests for explicit-then-implicit and implicit-then-conflicting-explicit combinations.
🤖 Prompt for AI Agents