-
-
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
base: master
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
| }); | ||
| }); |
| 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; | ||
|
Comment on lines
+52
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,41 +1,90 @@ | ||
| 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, BookmarkEnd, 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 on the start element", () => { | ||
| const tree = new Formatter().format(new BookmarkStart("named", 7), context); | ||
|
|
||
| expect(tree["w:bookmarkStart"]._attr["w:id"]).to.equal(7); | ||
| }); | ||
|
|
||
| it("should keep an explicitly supplied id on the end element", () => { | ||
| const tree = new Formatter().format(new BookmarkEnd(7), context); | ||
|
|
||
| expect(tree["w:bookmarkEnd"]._attr["w:id"]).to.equal(7); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.