diff --git a/lib/components/document/src/BookmarkRangeEnd.ts b/lib/components/document/src/BookmarkRangeEnd.ts index a852a1d..f7a66f0 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 } + | { bookmark: Bookmark; id?: never; displaced?: 'next' | 'prev' | null } // Deprecate this way: - | { bookmark?: never; id: number }; + | { 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, @@ -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($displaced)) then attribute ${QNS.w}displacedByCustomXml { $displaced } else () }`, - this.props.bookmark || { - id: this.props.id, + { + id: this.props.bookmark + ? this.props.bookmark.id + : this.props.id, + displaced: this.props.displaced || null, } ); } @@ -58,14 +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() - }`, - 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 14aaae7..b222c80 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' | null; } // Deprecate this way: | { bookmark?: never; id: number; name: string; + displaced?: 'next' | 'prev' | null; }; /** @@ -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($displaced)) then attribute ${QNS.w}displacedByCustomXml { $displaced } 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, + displaced: this.props.displaced || null, } ); } @@ -75,11 +83,16 @@ 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 ); + if (!props.displaced) { + props.displaced = undefined; + } + context.bookmarks?.registerIdentifier(props.id!, props.name); return new BookmarkRangeStart(props); } diff --git a/lib/components/document/test/BookmarkRangeEnd.test.ts b/lib/components/document/test/BookmarkRangeEnd.test.ts new file mode 100644 index 0000000..1ffd263 --- /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 b21a364..b6be601 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'); + }); });