diff --git a/docs/usage/hyperlinks.md b/docs/usage/hyperlinks.md index 1d7c1355d5..a5faa3d7f4 100644 --- a/docs/usage/hyperlinks.md +++ b/docs/usage/hyperlinks.md @@ -16,12 +16,10 @@ const chapter1 = new Paragraph({ children: [ new Bookmark({ id: "anchorForChapter1", - children: [ - new TextRun("Chapter 1"), - ], + children: [new TextRun("Chapter 1")], }), ], -}) +}); ``` Then you can create an hyperlink pointing to that bookmark with an `InternalHyperLink`: @@ -35,20 +33,32 @@ const link = new InternalHyperlink({ }), ], anchor: "anchorForChapter1", -}) +}); ``` +### Page reference + You can also get the page number of the bookmark by creating a page reference to it: ```ts const paragraph = new Paragraph({ - children: [ - new TextRun("Chapter 1 can be seen on page "), - new PageReference("anchorForChapter1"), - ], + children: [new TextRun("Chapter 1 can be seen on page "), new PageReference("anchorForChapter1")], }); ``` +### Numbered item reference + +You can also create cross references for numbered items with `NumberedItemReference`. + +```ts +const paragraph = new Paragraph({ + children: [new TextRun("See Paragraph "), new NumberedItemReference("anchorForParagraph1", "1.1")], +}); +``` + +> [!NOTE] +> The `NumberedItemReference` currently needs a cached value (in this case `1.1`) + ## External To create an external hyperlink you just need to specify the url and the text of the link, then add it to a paragraph: @@ -69,7 +79,6 @@ const paragraph = new Paragraph({ }); ``` - ## Styling hyperlinks It is possible to set the style of the text of both internal and external hyperlinks. This can be done applying run formatting on any of the `TextRun` children of the hyperlink. Use the `style: "Hyperlink"` property to show the default link styles, which can be combined with any other style. diff --git a/src/file/paragraph/links/numbered-item-ref.spec.ts b/src/file/paragraph/links/numbered-item-ref.spec.ts new file mode 100644 index 0000000000..ad0af14d64 --- /dev/null +++ b/src/file/paragraph/links/numbered-item-ref.spec.ts @@ -0,0 +1,133 @@ +import { describe, expect, it } from "vitest"; + +import { Formatter } from "@export/formatter"; + +import { NumberedItemReference, NumberedItemReferenceFormat } from "./numbered-item-ref"; + +describe("NumberedItemReference", () => { + describe("#constructor()", () => { + it("should create a numbered item ref without options", () => { + const ref = new NumberedItemReference("some_bookmark"); + const tree = new Formatter().format(ref); + expect(tree).to.deep.equal({ + "w:fldSimple": { + _attr: { + "w:instr": "REF some_bookmark \\h \\w", + }, + }, + }); + }); + + it("should create a numbered item ref with hyperlink option disabled", () => { + const ref = new NumberedItemReference("some_bookmark", "1", { hyperlink: false }); + const tree = new Formatter().format(ref); + expect(tree).to.deep.equal({ + "w:fldSimple": [ + { + _attr: { + "w:instr": "REF some_bookmark \\w", + }, + }, + { + "w:r": [ + { + "w:t": [ + { + _attr: { + "xml:space": "preserve", + }, + }, + "1", + ], + }, + ], + }, + ], + }); + }); + + it("should create a numbered item ref with referenceFormat option", () => { + const ref = new NumberedItemReference("some_bookmark", "1", { referenceFormat: NumberedItemReferenceFormat.RELATIVE }); + const tree = new Formatter().format(ref); + expect(tree).to.deep.equal({ + "w:fldSimple": [ + { + _attr: { + "w:instr": "REF some_bookmark \\h \\r", + }, + }, + { + "w:r": [ + { + "w:t": [ + { + _attr: { + "xml:space": "preserve", + }, + }, + "1", + ], + }, + ], + }, + ], + }); + }); + + it("should be possible to use the none referenceFormat option", () => { + const ref = new NumberedItemReference("some_bookmark", "1", { referenceFormat: NumberedItemReferenceFormat.NONE }); + const tree = new Formatter().format(ref); + expect(tree).to.deep.equal({ + "w:fldSimple": [ + { + _attr: { + "w:instr": "REF some_bookmark \\h", + }, + }, + { + "w:r": [ + { + "w:t": [ + { + _attr: { + "xml:space": "preserve", + }, + }, + "1", + ], + }, + ], + }, + ], + }); + }); + + it("should be possible to use the NO_CONTEXT referenceFormat option", () => { + const ref = new NumberedItemReference("some_bookmark", "1", { referenceFormat: NumberedItemReferenceFormat.NO_CONTEXT }); + const tree = new Formatter().format(ref); + expect(tree).to.deep.equal({ + "w:fldSimple": [ + { + _attr: { + "w:instr": "REF some_bookmark \\h \\n", + }, + }, + { + "w:r": [ + { + "w:t": [ + { + _attr: { + "xml:space": "preserve", + }, + }, + "1", + ], + }, + ], + }, + ], + }); + }); + }); +}); diff --git a/src/file/paragraph/links/numbered-item-ref.ts b/src/file/paragraph/links/numbered-item-ref.ts new file mode 100644 index 0000000000..30e2b4e56a --- /dev/null +++ b/src/file/paragraph/links/numbered-item-ref.ts @@ -0,0 +1,66 @@ +import { SimpleField } from "../run"; + +// https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oi29500/7088a8ce-e784-49d4-94b8-cba6ef8fce78 +export enum NumberedItemReferenceFormat { + NONE = "none", + /** + * \r option - inserts the paragraph number of the bookmarked paragraph in relative context, or relative to its position in the numbering scheme + */ + RELATIVE = "relative", + /** + * \n option - causes the field result to be the paragraph number without trailing periods. No information about prior numbered levels is displayed unless it is included as part of the current level. + */ + NO_CONTEXT = "no_context", + /** + * \w option - causes the field result to be the entire paragraph number without trailing periods, regardless of the location of the REF field. + */ + FULL_CONTEXT = "full_context", +} + +export type INumberedItemReferenceOptions = { + /** + * \h option - Creates a hyperlink to the bookmarked paragraph. + * @default true + */ + readonly hyperlink?: boolean; + /** + * which switch to use for the reference format + * @default NumberedItemReferenceFormat.FULL_CONTEXT + */ + readonly referenceFormat?: NumberedItemReferenceFormat; +}; + +type Switch = "\\h" | "\\r" | "\\n" | "\\w"; + +const SWITCH_MAP: Record = { + [NumberedItemReferenceFormat.RELATIVE]: "\\r", + [NumberedItemReferenceFormat.NO_CONTEXT]: "\\n", + [NumberedItemReferenceFormat.FULL_CONTEXT]: "\\w", + [NumberedItemReferenceFormat.NONE]: undefined, +}; + +/** + * Creates a field/cross reference to a numbered item in the document. + */ +export class NumberedItemReference extends SimpleField { + public constructor( + bookmarkId: string, + // TODO: It would be nice if the cached value could be automatically generated + /** + * The cached value of the field. This is used to display the field result in the document. + */ + cachedValue?: string, + options: INumberedItemReferenceOptions = {}, + ) { + const { hyperlink = true, referenceFormat = NumberedItemReferenceFormat.FULL_CONTEXT } = options; + const baseInstruction = `REF ${bookmarkId}`; + + // TODO: Requires TypeScript 5.5 update for it to understand `filter` + // @ts-expect-error TS2322 + const switches: readonly Switch[] = [...(hyperlink ? (["\\h"] as const) : []), ...[SWITCH_MAP[referenceFormat]].filter((a) => !!a)]; + + const instruction = `${baseInstruction} ${switches.join(" ")}`; + + super(instruction, cachedValue); + } +}