From 28d71be4bce96f40d8527bc9353c1488664a81a2 Mon Sep 17 00:00:00 2001 From: Luis Gallego Date: Wed, 19 Aug 2026 13:40:40 +0200 Subject: [PATCH 1/2] DEV-26382 Added displacedByCustomXml prop to bookmarks --- .../document/src/BookmarkRangeEnd.ts | 17 ++++-- .../document/src/BookmarkRangeStart.ts | 19 +++++-- .../document/test/BookmarkRangeEnd.test.ts | 55 +++++++++++++++++++ .../document/test/BookmarkRangeStart.test.ts | 33 +++++++++++ 4 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 lib/components/document/test/BookmarkRangeEnd.test.ts diff --git a/lib/components/document/src/BookmarkRangeEnd.ts b/lib/components/document/src/BookmarkRangeEnd.ts index a852a1da..36cd0c0f 100644 --- a/lib/components/document/src/BookmarkRangeEnd.ts +++ b/lib/components/document/src/BookmarkRangeEnd.ts @@ -17,9 +17,9 @@ export type BookmarkRangeEndChild = never; * A type describing the props accepted by {@link BookmarkRangeEnd}. */ export type BookmarkRangeEndProps = - | { bookmark: Bookmark; id?: never } + | { bookmark: Bookmark; id?: never; displaced?: 'next' | 'prev' } // Deprecate this way: - | { bookmark?: never; id: number }; + | { bookmark?: never; id: number; displaced?: 'next' | 'prev' }; /** * The end of a range associated with a comment. @@ -39,10 +39,14 @@ export class BookmarkRangeEnd extends Component< public override toNode(_ancestry: ComponentAncestor[]): Node { return create( `element ${QNS.w}bookmarkEnd { - attribute ${QNS.w}id { $id } + attribute ${QNS.w}id { $id }, + if (exists($displacedByCustomXml)) then attribute ${QNS.w}displacedByCustomXml { $displacedByCustomXml } else () }`, - this.props.bookmark || { - id: this.props.id, + { + id: this.props.bookmark + ? this.props.bookmark.id + : this.props.id, + displacedByCustomXml: this.props.displaced ?? null, } ); } @@ -61,7 +65,8 @@ export class BookmarkRangeEnd extends Component< return new BookmarkRangeEnd( evaluateXPathToMap( `map { - "id": ./@${QNS.w}id/number() + "id": ./@${QNS.w}id/number(), + "displaced": ./@${QNS.w}displacedByCustomXml/string() }`, node ) diff --git a/lib/components/document/src/BookmarkRangeStart.ts b/lib/components/document/src/BookmarkRangeStart.ts index 14aaae77..d1c1af5c 100644 --- a/lib/components/document/src/BookmarkRangeStart.ts +++ b/lib/components/document/src/BookmarkRangeStart.ts @@ -22,12 +22,14 @@ export type BookmarkRangeStartProps = bookmark: Bookmark; id?: never; name?: never; + displaced?: 'next' | 'prev'; } // Deprecate this way: | { bookmark?: never; id: number; name: string; + displaced?: 'next' | 'prev'; }; /** @@ -49,11 +51,17 @@ export class BookmarkRangeStart extends Component< return create( `element ${QNS.w}bookmarkStart { attribute ${QNS.w}id { $id }, - attribute ${QNS.w}name { $name } + attribute ${QNS.w}name { $name }, + if (exists($displacedByCustomXml)) then attribute ${QNS.w}displacedByCustomXml { $displacedByCustomXml } else () }`, - this.props.bookmark || { - id: this.props.id, - name: this.props.name, + { + id: this.props.bookmark + ? this.props.bookmark.id + : this.props.id, + name: this.props.bookmark + ? this.props.bookmark.name + : this.props.name, + displacedByCustomXml: this.props.displaced ?? null, } ); } @@ -75,7 +83,8 @@ export class BookmarkRangeStart extends Component< const props = evaluateXPathToMap( `map { "id": ./@${QNS.w}id/number(), - "name": ./@${QNS.w}name/string() + "name": ./@${QNS.w}name/string(), + "displaced": ./@${QNS.w}displacedByCustomXml/string() }`, node ); diff --git a/lib/components/document/test/BookmarkRangeEnd.test.ts b/lib/components/document/test/BookmarkRangeEnd.test.ts new file mode 100644 index 00000000..1ffd263e --- /dev/null +++ b/lib/components/document/test/BookmarkRangeEnd.test.ts @@ -0,0 +1,55 @@ +import { expect } from 'std/expect'; +import { describe, it } from 'std/testing/bdd'; + +import { create, serialize } from '../../../utilities/src/dom.ts'; +import { NamespaceUri } from '../../../utilities/src/namespaces.ts'; +import { BookmarkRangeEnd } from '../src/BookmarkRangeEnd.ts'; + +describe('BookmarkRangeEnd', () => { + it('parses id from XML', () => { + const node = create(` + + `); + const component = BookmarkRangeEnd.fromNode(node); + expect(component.props.id).toBe(3); + }); + + it('serializes back to XML correctly', () => { + const node = create(` + + `); + const component = BookmarkRangeEnd.fromNode(node); + const output = serialize(component.toNode([])); + expect(output).toContain('id="1"'); + }); + + it('parses the displaced prop from XML', () => { + const node = create(` + + `); + const component = BookmarkRangeEnd.fromNode(node); + expect(component.props.displaced).toBe('prev'); + }); + + it('serializes the displaced prop to XML', () => { + const component = new BookmarkRangeEnd({ + id: 2, + displaced: 'next', + }); + const output = serialize(component.toNode([])); + expect(output).toContain('displacedByCustomXml="next"'); + }); + + it('omits the displaced attribute when not set', () => { + const component = new BookmarkRangeEnd({ id: 3 }); + const output = serialize(component.toNode([])); + expect(output).not.toContain('displacedByCustomXml'); + }); + + it('matches a w:bookmarkEnd node', () => { + const node = create( + `` + ); + expect(BookmarkRangeEnd.matchesNode(node)).toBe(true); + }); +}); diff --git a/lib/components/document/test/BookmarkRangeStart.test.ts b/lib/components/document/test/BookmarkRangeStart.test.ts index b21a364a..b6be601e 100644 --- a/lib/components/document/test/BookmarkRangeStart.test.ts +++ b/lib/components/document/test/BookmarkRangeStart.test.ts @@ -99,4 +99,37 @@ describe('BookmarkRangeStart', () => { expect(output).toContain('id="1"'); expect(output).toContain('name="test_bm"'); }); + + it('parses the displaced prop from XML', () => { + const bookmarks = new Bookmarks(); + const context: ComponentContext = { + archive: new Archive(), + relationships: null, + bookmarks, + }; + const node = create(` + + `); + const component = BookmarkRangeStart.fromNode(node, context); + expect(component.props.displaced).toBe('next'); + }); + + it('serializes the displaced prop to XML', () => { + const component = new BookmarkRangeStart({ + id: 2, + name: 'displaced_bm', + displaced: 'prev', + }); + const output = serialize(component.toNode([])); + expect(output).toContain('displacedByCustomXml="prev"'); + }); + + it('omits the displaced attribute when not set', () => { + const component = new BookmarkRangeStart({ + id: 3, + name: 'plain_bm', + }); + const output = serialize(component.toNode([])); + expect(output).not.toContain('displacedByCustomXml'); + }); }); From d534db0ba6f700b29545d5febe11a75fab320ef7 Mon Sep 17 00:00:00 2001 From: Luis Gallego Date: Thu, 20 Aug 2026 08:36:49 +0200 Subject: [PATCH 2/2] fix --- .../document/src/BookmarkRangeEnd.ts | 32 +++++++++++-------- .../document/src/BookmarkRangeStart.ts | 14 +++++--- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/lib/components/document/src/BookmarkRangeEnd.ts b/lib/components/document/src/BookmarkRangeEnd.ts index 36cd0c0f..f7a66f07 100644 --- a/lib/components/document/src/BookmarkRangeEnd.ts +++ b/lib/components/document/src/BookmarkRangeEnd.ts @@ -1,7 +1,7 @@ import type { Bookmark } from '../../../classes/src/Bookmarks.ts'; import { - type ComponentAncestor, Component, + type ComponentAncestor, } from '../../../classes/src/Component.ts'; import { registerComponent } from '../../../utilities/src/components.ts'; import { create } from '../../../utilities/src/dom.ts'; @@ -17,12 +17,12 @@ export type BookmarkRangeEndChild = never; * A type describing the props accepted by {@link BookmarkRangeEnd}. */ export type BookmarkRangeEndProps = - | { bookmark: Bookmark; id?: never; displaced?: 'next' | 'prev' } + | { bookmark: Bookmark; id?: never; displaced?: 'next' | 'prev' | null } // Deprecate this way: - | { bookmark?: never; id: number; displaced?: 'next' | 'prev' }; + | { bookmark?: never; id: number; displaced?: 'next' | 'prev' | null }; /** - * The end of a range associated with a comment. + * The end of a range associated with a bookmark. */ export class BookmarkRangeEnd extends Component< BookmarkRangeEndProps, @@ -40,13 +40,13 @@ export class BookmarkRangeEnd extends Component< return create( `element ${QNS.w}bookmarkEnd { attribute ${QNS.w}id { $id }, - if (exists($displacedByCustomXml)) then attribute ${QNS.w}displacedByCustomXml { $displacedByCustomXml } else () + if (exists($displaced)) then attribute ${QNS.w}displacedByCustomXml { $displaced } else () }`, { id: this.props.bookmark ? this.props.bookmark.id : this.props.id, - displacedByCustomXml: this.props.displaced ?? null, + displaced: this.props.displaced || null, } ); } @@ -62,15 +62,19 @@ export class BookmarkRangeEnd extends Component< * Instantiate this component from the XML in an existing DOCX file. */ static override fromNode(node: Node): BookmarkRangeEnd { - return new BookmarkRangeEnd( - evaluateXPathToMap( - `map { - "id": ./@${QNS.w}id/number(), - "displaced": ./@${QNS.w}displacedByCustomXml/string() - }`, - node - ) + const props = evaluateXPathToMap( + `map { + "id": ./@${QNS.w}id/number(), + "displaced": ./@${QNS.w}displacedByCustomXml/string() + }`, + node ); + + if (!props.displaced) { + props.displaced = undefined; + } + + return new BookmarkRangeEnd(props); } } diff --git a/lib/components/document/src/BookmarkRangeStart.ts b/lib/components/document/src/BookmarkRangeStart.ts index d1c1af5c..b222c806 100644 --- a/lib/components/document/src/BookmarkRangeStart.ts +++ b/lib/components/document/src/BookmarkRangeStart.ts @@ -22,14 +22,14 @@ export type BookmarkRangeStartProps = bookmark: Bookmark; id?: never; name?: never; - displaced?: 'next' | 'prev'; + displaced?: 'next' | 'prev' | null; } // Deprecate this way: | { bookmark?: never; id: number; name: string; - displaced?: 'next' | 'prev'; + displaced?: 'next' | 'prev' | null; }; /** @@ -52,7 +52,7 @@ export class BookmarkRangeStart extends Component< `element ${QNS.w}bookmarkStart { attribute ${QNS.w}id { $id }, attribute ${QNS.w}name { $name }, - if (exists($displacedByCustomXml)) then attribute ${QNS.w}displacedByCustomXml { $displacedByCustomXml } else () + if (exists($displaced)) then attribute ${QNS.w}displacedByCustomXml { $displaced } else () }`, { id: this.props.bookmark @@ -61,7 +61,7 @@ export class BookmarkRangeStart extends Component< name: this.props.bookmark ? this.props.bookmark.name : this.props.name, - displacedByCustomXml: this.props.displaced ?? null, + displaced: this.props.displaced || null, } ); } @@ -85,10 +85,14 @@ export class BookmarkRangeStart extends Component< "id": ./@${QNS.w}id/number(), "name": ./@${QNS.w}name/string(), "displaced": ./@${QNS.w}displacedByCustomXml/string() - }`, + }`, node ); + if (!props.displaced) { + props.displaced = undefined; + } + context.bookmarks?.registerIdentifier(props.id!, props.name); return new BookmarkRangeStart(props); }