feat: Added NumberedItemReference (#3042)

* feat: Added NumberedItemReference

* Fix linting

* ts-ignore until TypeScript upgrade

---------

Co-authored-by: Alexander Nortung <alexander.nortung@oakdigital.dk>
This commit is contained in:
Dolan
2025-04-16 08:54:50 +01:00
committed by GitHub
parent b19025881b
commit 7f26badbc9
3 changed files with 218 additions and 10 deletions

View File

@ -16,12 +16,10 @@ const chapter1 = new Paragraph({
children: [ children: [
new Bookmark({ new Bookmark({
id: "anchorForChapter1", id: "anchorForChapter1",
children: [ children: [new TextRun("Chapter 1")],
new TextRun("Chapter 1"),
],
}), }),
], ],
}) });
``` ```
Then you can create an hyperlink pointing to that bookmark with an `InternalHyperLink`: Then you can create an hyperlink pointing to that bookmark with an `InternalHyperLink`:
@ -35,20 +33,32 @@ const link = new InternalHyperlink({
}), }),
], ],
anchor: "anchorForChapter1", anchor: "anchorForChapter1",
}) });
``` ```
### Page reference
You can also get the page number of the bookmark by creating a page reference to it: You can also get the page number of the bookmark by creating a page reference to it:
```ts ```ts
const paragraph = new Paragraph({ const paragraph = new Paragraph({
children: [ children: [new TextRun("Chapter 1 can be seen on page "), new PageReference("anchorForChapter1")],
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 ## 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: 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 ## 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. 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.

View File

@ -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",
],
},
],
},
],
});
});
});
});

View File

@ -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, Switch | undefined> = {
[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);
}
}