diff --git a/demo/97-endnotes.ts b/demo/97-endnotes.ts new file mode 100644 index 00000000000..336990bf0a3 --- /dev/null +++ b/demo/97-endnotes.ts @@ -0,0 +1,85 @@ +// Endnotes + +import * as fs from "fs"; +import { Document, EndnoteReferenceRun, Packer, Paragraph, TextRun } from "docx"; + +const doc = new Document({ + endnotes: { + 1: { children: [new Paragraph("This is the first endnote with some detailed explanation.")] }, + 2: { children: [new Paragraph("Second endnote"), new Paragraph("With multiple paragraphs for more complex content.")] }, + 3: { children: [new Paragraph("Third endnote referencing important source material.")] }, + 4: { children: [new Paragraph("Fourth endnote from a different section.")] }, + }, + sections: [ + { + properties: { + page: { + margin: { + top: 1440, // 1 inch + right: 1440, + bottom: 1440, + left: 1440, + }, + }, + }, + children: [ + new Paragraph({ + children: [ + new TextRun({ + text: "Endnotes Demo Document", + bold: true, + size: 28, + }), + ], + spacing: { after: 400 }, + }), + new Paragraph({ + children: [ + new TextRun("This document demonstrates endnotes functionality. "), + new TextRun("Here is some text with an endnote reference"), + new EndnoteReferenceRun(1), + new TextRun(". This allows for detailed citations and references "), + new EndnoteReferenceRun(2), + new TextRun(" without cluttering the main text flow."), + ], + spacing: { after: 200 }, + }), + new Paragraph({ + children: [ + new TextRun("Endnotes appear at the end of the document, "), + new TextRun("unlike footnotes which appear at the bottom of each page"), + new EndnoteReferenceRun(3), + new TextRun(". This makes them ideal for academic papers and formal documents."), + ], + spacing: { after: 200 }, + }), + ], + }, + { + children: [ + new Paragraph({ + children: [ + new TextRun({ + text: "Second Section", + bold: true, + size: 24, + }), + ], + spacing: { after: 200 }, + }), + new Paragraph({ + children: [ + new TextRun("This is content from a different section "), + new TextRun("with its own endnote reference"), + new EndnoteReferenceRun(4), + new TextRun(". Endnotes from all sections appear together at the document end."), + ], + }), + ], + }, + ], +}); + +Packer.toBuffer(doc).then((buffer) => { + fs.writeFileSync("My Document.docx", buffer); +}); diff --git a/docs/usage/footnotes.md b/docs/usage/footnotes.md index c881a8cd3eb..69510269700 100644 --- a/docs/usage/footnotes.md +++ b/docs/usage/footnotes.md @@ -1,10 +1,10 @@ -# Footnotes +# Footnotes and Endnotes -!> Footnotes requires an understanding of [Sections](usage/sections.md). +!> Footnotes and endnotes require an understanding of [Sections](usage/sections.md). -Use footnotes and endnotes to explain, comment on, or provide references to something in a document. Usually, footnotes appear at the bottom of the page. +Use footnotes and endnotes to explain, comment on, or provide references to something in a document. Footnotes appear at the bottom of the page, while endnotes appear at the end of the document. -## Example +## Footnotes Example ```ts const doc = new Document({ @@ -33,10 +33,63 @@ const doc = new Document({ }); ``` +## Endnotes Example + +```ts +const doc = new Document({ + endnotes: { + 1: { children: [new Paragraph("This is the first endnote with some detailed explanation.")] }, + 2: { children: [new Paragraph("Second endnote"), new Paragraph("With multiple paragraphs for more complex content.")] }, + 3: { children: [new Paragraph("Third endnote referencing important source material.")] }, + }, + sections: [ + { + children: [ + new Paragraph({ + children: [ + new TextRun("This document demonstrates endnotes functionality. "), + new TextRun("Here is some text with an endnote reference"), + new EndnoteReferenceRun(1), + new TextRun(". This allows for detailed citations and references "), + new EndnoteReferenceRun(2), + new TextRun(" without cluttering the main text flow."), + ], + }), + new Paragraph({ + children: [ + new TextRun("Endnotes appear at the end of the document, "), + new TextRun("unlike footnotes which appear at the bottom of each page"), + new EndnoteReferenceRun(3), + new TextRun(". This makes them ideal for academic papers and formal documents."), + ], + }), + ], + }, + ], +}); +``` + ## Usage -Footnotes requires an entry into the `footnotes` array in the `Document` constructor, and a `FootnoteReferenceRun` in the `Paragraph` constructor. +### Footnotes + +Footnotes require an entry in the `footnotes` object in the `Document` constructor, and a `FootnoteReferenceRun` in the `Paragraph` constructor. `footnotes` is an object of number to `Footnote` objects. The number is the reference number, and the `Footnote` object is the content of the footnote. The `Footnote` object has a `children` property, which is an array of `Paragraph` objects. `FootnoteReferenceRun` is a `Run` object, which are added to `Paragraph`s. It takes a number as a parameter, which is the reference number of the footnote. + +### Endnotes + +Endnotes require an entry in the `endnotes` object in the `Document` constructor, and an `EndnoteReferenceRun` in the `Paragraph` constructor. + +`endnotes` is an object of number to `Endnote` objects. The number is the reference number, and the `Endnote` object is the content of the endnote. The `Endnote` object has a `children` property, which is an array of `Paragraph` objects. + +`EndnoteReferenceRun` is a `Run` object, which are added to `Paragraph`s. It takes a number as a parameter, which is the reference number of the endnote. + +### Key Differences + +- **Footnotes** appear at the bottom of each page where they are referenced +- **Endnotes** appear at the end of the entire document +- Both footnote and endnote references automatically appear as superscript in the document +- Endnotes are ideal for academic papers, research documents, and lengthy citations that would otherwise clutter the page diff --git a/src/export/packer/next-compiler.ts b/src/export/packer/next-compiler.ts index 9f2e41e0625..96ed7e2d73e 100644 --- a/src/export/packer/next-compiler.ts +++ b/src/export/packer/next-compiler.ts @@ -30,6 +30,8 @@ type IXmlifyedFileMapping = { readonly AppProperties: IXmlifyedFile; readonly FootNotes: IXmlifyedFile; readonly FootNotesRelationships: IXmlifyedFile; + readonly Endnotes: IXmlifyedFile; + readonly EndnotesRelationships: IXmlifyedFile; readonly Settings: IXmlifyedFile; readonly Comments?: IXmlifyedFile; readonly CommentsRelationships?: IXmlifyedFile; @@ -454,6 +456,38 @@ export class Compiler { ), path: "word/_rels/footnotes.xml.rels", }, + Endnotes: { + data: xml( + this.formatter.format(file.Endnotes.View, { + viewWrapper: file.Endnotes, + file, + stack: [], + }), + { + indent: prettify, + declaration: { + encoding: "UTF-8", + }, + }, + ), + path: "word/endnotes.xml", + }, + EndnotesRelationships: { + data: xml( + this.formatter.format(file.Endnotes.Relationships, { + viewWrapper: file.Endnotes, + file, + stack: [], + }), + { + indent: prettify, + declaration: { + encoding: "UTF-8", + }, + }, + ), + path: "word/_rels/endnotes.xml.rels", + }, Settings: { data: xml( this.formatter.format(file.Settings, { diff --git a/src/file/content-types/content-types.ts b/src/file/content-types/content-types.ts index 8107e2525c7..5838058fc93 100644 --- a/src/file/content-types/content-types.ts +++ b/src/file/content-types/content-types.ts @@ -34,6 +34,7 @@ export class ContentTypes extends XmlComponent { this.root.push(new Override("application/vnd.openxmlformats-officedocument.extended-properties+xml", "/docProps/app.xml")); this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml", "/word/numbering.xml")); this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml", "/word/footnotes.xml")); + this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml", "/word/endnotes.xml")); this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml", "/word/settings.xml")); this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml", "/word/comments.xml")); this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml", "/word/fontTable.xml")); diff --git a/src/file/core-properties/properties.ts b/src/file/core-properties/properties.ts index d1b78cf7e07..39eb7e60af7 100644 --- a/src/file/core-properties/properties.ts +++ b/src/file/core-properties/properties.ts @@ -34,6 +34,14 @@ export type IPropertiesOptions = { } > >; + readonly endnotes?: Readonly< + Record< + string, + { + readonly children: readonly Paragraph[]; + } + > + >; readonly background?: IDocumentBackgroundOptions; readonly features?: { readonly trackRevisions?: boolean; diff --git a/src/file/document-wrapper.ts b/src/file/document-wrapper.ts index 578ac2d048a..14c9199a759 100644 --- a/src/file/document-wrapper.ts +++ b/src/file/document-wrapper.ts @@ -1,4 +1,5 @@ import { Document, IDocumentOptions } from "./document"; +import { Endnotes } from "./endnotes"; import { Footer } from "./footer/footer"; import { FootNotes } from "./footnotes"; import { Header } from "./header/header"; @@ -6,7 +7,7 @@ import { Relationships } from "./relationships"; import { XmlComponent } from "./xml-components"; export type IViewWrapper = { - readonly View: Document | Footer | Header | FootNotes | XmlComponent; + readonly View: Document | Footer | Header | FootNotes | Endnotes | XmlComponent; readonly Relationships: Relationships; }; diff --git a/src/file/endnotes-wrapper.ts b/src/file/endnotes-wrapper.ts new file mode 100644 index 00000000000..00a2514de35 --- /dev/null +++ b/src/file/endnotes-wrapper.ts @@ -0,0 +1,21 @@ +import { IViewWrapper } from "./document-wrapper"; +import { Endnotes } from "./endnotes/endnotes"; +import { Relationships } from "./relationships"; + +export class EndnotesWrapper implements IViewWrapper { + private readonly endnotes: Endnotes; + private readonly relationships: Relationships; + + public constructor() { + this.endnotes = new Endnotes(); + this.relationships = new Relationships(); + } + + public get View(): Endnotes { + return this.endnotes; + } + + public get Relationships(): Relationships { + return this.relationships; + } +} diff --git a/src/file/endnotes/endnote/endnote-attributes.ts b/src/file/endnotes/endnote/endnote-attributes.ts new file mode 100644 index 00000000000..c008e50976c --- /dev/null +++ b/src/file/endnotes/endnote/endnote-attributes.ts @@ -0,0 +1,11 @@ +import { XmlAttributeComponent } from "@file/xml-components"; + +export class EndnoteAttributes extends XmlAttributeComponent<{ + readonly type?: string; + readonly id: number; +}> { + protected readonly xmlKeys = { + type: "w:type", + id: "w:id", + }; +} diff --git a/src/file/endnotes/endnote/endnote.spec.ts b/src/file/endnotes/endnote/endnote.spec.ts new file mode 100644 index 00000000000..cb7d74a4875 --- /dev/null +++ b/src/file/endnotes/endnote/endnote.spec.ts @@ -0,0 +1,162 @@ +import { describe, expect, it } from "vitest"; + +import { Formatter } from "@export/formatter"; +import { Paragraph, TextRun } from "@file/paragraph"; + +import { Endnote, EndnoteType } from "./endnote"; + +describe("Endnote", () => { + describe("#constructor", () => { + it("should create an endnote with an endnote type", () => { + const endnote = new Endnote({ + id: 1, + type: EndnoteType.SEPARATOR, + children: [], + }); + const tree = new Formatter().format(endnote); + + expect(Object.keys(tree)).to.deep.equal(["w:endnote"]); + expect(tree["w:endnote"]).to.deep.equal({ _attr: { "w:type": "separator", "w:id": 1 } }); + }); + + it("should create a endnote without a endnote type", () => { + const endnote = new Endnote({ + id: 1, + children: [], + }); + const tree = new Formatter().format(endnote); + + expect(Object.keys(tree)).to.deep.equal(["w:endnote"]); + expect(tree["w:endnote"]).to.deep.equal({ _attr: { "w:id": 1 } }); + }); + + it("should append endnote ref run on the first endnote paragraph", () => { + const endnote = new Endnote({ + id: 1, + children: [new Paragraph({ children: [new TextRun("test-endnote")] })], + }); + const tree = new Formatter().format(endnote); + + expect(tree).to.deep.equal({ + "w:endnote": [ + { + _attr: { + "w:id": 1, + }, + }, + { + "w:p": [ + { + "w:r": [ + { + "w:rPr": [ + { + "w:rStyle": { + _attr: { + "w:val": "EndnoteReference", + }, + }, + }, + ], + }, + { + "w:endnoteRef": {}, + }, + ], + }, + { + "w:r": [ + { + "w:t": [ + { + _attr: { + "xml:space": "preserve", + }, + }, + "test-endnote", + ], + }, + ], + }, + ], + }, + ], + }); + }); + + it("should add multiple paragraphs", () => { + const endnote = new Endnote({ + id: 1, + children: [ + new Paragraph({ children: [new TextRun("test-endnote")] }), + new Paragraph({ children: [new TextRun("test-endnote-2")] }), + ], + }); + const tree = new Formatter().format(endnote); + + expect(tree).to.deep.equal({ + "w:endnote": [ + { + _attr: { + "w:id": 1, + }, + }, + { + "w:p": [ + { + "w:r": [ + { + "w:rPr": [ + { + "w:rStyle": { + _attr: { + "w:val": "EndnoteReference", + }, + }, + }, + ], + }, + { + "w:endnoteRef": {}, + }, + ], + }, + { + "w:r": [ + { + "w:t": [ + { + _attr: { + "xml:space": "preserve", + }, + }, + "test-endnote", + ], + }, + ], + }, + ], + }, + { + "w:p": [ + { + "w:r": [ + { + "w:t": [ + { + _attr: { + "xml:space": "preserve", + }, + }, + "test-endnote-2", + ], + }, + ], + }, + ], + }, + ], + }); + }); + }); +}); diff --git a/src/file/endnotes/endnote/endnote.ts b/src/file/endnotes/endnote/endnote.ts new file mode 100644 index 00000000000..395134e128b --- /dev/null +++ b/src/file/endnotes/endnote/endnote.ts @@ -0,0 +1,39 @@ +import { Paragraph } from "@file/paragraph"; +import { XmlComponent } from "@file/xml-components"; + +import { EndnoteAttributes } from "./endnote-attributes"; +import { EndnoteRefRun } from "./run/endnote-ref-run"; + +export const EndnoteType = { + SEPARATOR: "separator", + + CONTINUATION_SEPARATOR: "continuationSeparator", +} as const; + +export type IEndnoteOptions = { + readonly id: number; + readonly type?: (typeof EndnoteType)[keyof typeof EndnoteType]; + readonly children: readonly Paragraph[]; +}; + +export class Endnote extends XmlComponent { + public constructor(options: IEndnoteOptions) { + super("w:endnote"); + this.root.push( + new EndnoteAttributes({ + type: options.type, + id: options.id, + }), + ); + + for (let i = 0; i < options.children.length; i++) { + const child = options.children[i]; + + if (i === 0) { + child.addRunToFront(new EndnoteRefRun()); + } + + this.root.push(child); + } + } +} diff --git a/src/file/endnotes/endnote/index.ts b/src/file/endnotes/endnote/index.ts new file mode 100644 index 00000000000..11d8ba33a9a --- /dev/null +++ b/src/file/endnotes/endnote/index.ts @@ -0,0 +1 @@ +export * from "./run"; diff --git a/src/file/endnotes/endnote/run/endnote-ref-run.ts b/src/file/endnotes/endnote/run/endnote-ref-run.ts new file mode 100644 index 00000000000..52c2322e174 --- /dev/null +++ b/src/file/endnotes/endnote/run/endnote-ref-run.ts @@ -0,0 +1,11 @@ +import { EndnoteReference, Run } from "@file/paragraph"; + +export class EndnoteRefRun extends Run { + public constructor() { + super({ + style: "EndnoteReference", + }); + + this.root.push(new EndnoteReference()); + } +} diff --git a/src/file/endnotes/endnote/run/index.ts b/src/file/endnotes/endnote/run/index.ts new file mode 100644 index 00000000000..548e717bbd1 --- /dev/null +++ b/src/file/endnotes/endnote/run/index.ts @@ -0,0 +1 @@ +export * from "./reference-run"; diff --git a/src/file/endnotes/endnote/run/reference-run.spec.ts b/src/file/endnotes/endnote/run/reference-run.spec.ts new file mode 100644 index 00000000000..9a07548faaa --- /dev/null +++ b/src/file/endnotes/endnote/run/reference-run.spec.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest"; + +import { Formatter } from "@export/formatter"; + +import { EndnoteIdReference, EndnoteReferenceRun } from "./reference-run"; + +describe("EndnoteReference", () => { + describe("#constructor()", () => { + it("should create an EndnoteReference with correct root key and id", () => { + const tree = new Formatter().format(new EndnoteIdReference(1)); + expect(tree).to.deep.equal({ + "w:endnoteReference": { + _attr: { + "w:id": 1, + }, + }, + }); + }); + }); +}); + +describe("EndnoteReferenceRun", () => { + describe("#constructor()", () => { + it("should create an EndnoteReferenceRun with correct style and reference", () => { + const run = new EndnoteReferenceRun(1); + const tree = new Formatter().format(run); + expect(tree).to.deep.equal({ + "w:r": [ + { + "w:rPr": [ + { + "w:rStyle": { + _attr: { + "w:val": "EndnoteReference", + }, + }, + }, + ], + }, + { + "w:endnoteReference": { + _attr: { + "w:id": 1, + }, + }, + }, + ], + }); + }); + }); +}); diff --git a/src/file/endnotes/endnote/run/reference-run.ts b/src/file/endnotes/endnote/run/reference-run.ts new file mode 100644 index 00000000000..46031eafa56 --- /dev/null +++ b/src/file/endnotes/endnote/run/reference-run.ts @@ -0,0 +1,30 @@ +import { Run } from "@file/paragraph/run"; +import { XmlAttributeComponent, XmlComponent } from "@file/xml-components"; + +export class EndnoteReferenceRunAttributes extends XmlAttributeComponent<{ + readonly id: number; +}> { + protected readonly xmlKeys = { + id: "w:id", + }; +} + +export class EndnoteIdReference extends XmlComponent { + public constructor(id: number) { + super("w:endnoteReference"); + + this.root.push( + new EndnoteReferenceRunAttributes({ + id: id, + }), + ); + } +} + +export class EndnoteReferenceRun extends Run { + public constructor(id: number) { + super({ style: "EndnoteReference" }); + + this.root.push(new EndnoteIdReference(id)); + } +} diff --git a/src/file/endnotes/endnotes-attributes.ts b/src/file/endnotes/endnotes-attributes.ts new file mode 100644 index 00000000000..2d0be9b762e --- /dev/null +++ b/src/file/endnotes/endnotes-attributes.ts @@ -0,0 +1,41 @@ +import { XmlAttributeComponent } from "@file/xml-components"; + +export class EndnotesAttributes extends XmlAttributeComponent<{ + readonly wpc?: string; + readonly mc?: string; + readonly o?: string; + readonly r?: string; + readonly m?: string; + readonly v?: string; + readonly wp14?: string; + readonly wp?: string; + readonly w10?: string; + readonly w?: string; + readonly w14?: string; + readonly w15?: string; + readonly wpg?: string; + readonly wpi?: string; + readonly wne?: string; + readonly wps?: string; + readonly Ignorable?: string; +}> { + protected readonly xmlKeys = { + wpc: "xmlns:wpc", + mc: "xmlns:mc", + o: "xmlns:o", + r: "xmlns:r", + m: "xmlns:m", + v: "xmlns:v", + wp14: "xmlns:wp14", + wp: "xmlns:wp", + w10: "xmlns:w10", + w: "xmlns:w", + w14: "xmlns:w14", + w15: "xmlns:w15", + wpg: "xmlns:wpg", + wpi: "xmlns:wpi", + wne: "xmlns:wne", + wps: "xmlns:wps", + Ignorable: "mc:Ignorable", + }; +} diff --git a/src/file/endnotes/endnotes.ts b/src/file/endnotes/endnotes.ts new file mode 100644 index 00000000000..25fc9d19f29 --- /dev/null +++ b/src/file/endnotes/endnotes.ts @@ -0,0 +1,78 @@ +import { XmlComponent } from "@file/xml-components"; + +import { EndnotesAttributes } from "./endnotes-attributes"; +import { LineRuleType, Paragraph } from "../paragraph"; +import { Endnote, EndnoteType } from "./endnote/endnote"; +import { ContinuationSeperatorRun } from "../footnotes/footnote/run/continuation-seperator-run"; +import { SeperatorRun } from "../footnotes/footnote/run/seperator-run"; + +export class Endnotes extends XmlComponent { + public constructor() { + super("w:endnotes"); + + this.root.push( + new EndnotesAttributes({ + wpc: "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas", + mc: "http://schemas.openxmlformats.org/markup-compatibility/2006", + o: "urn:schemas-microsoft-com:office:office", + r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + m: "http://schemas.openxmlformats.org/officeDocument/2006/math", + v: "urn:schemas-microsoft-com:vml", + wp14: "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing", + wp: "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", + w10: "urn:schemas-microsoft-com:office:word", + w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + w14: "http://schemas.microsoft.com/office/word/2010/wordml", + w15: "http://schemas.microsoft.com/office/word/2012/wordml", + wpg: "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup", + wpi: "http://schemas.microsoft.com/office/word/2010/wordprocessingInk", + wne: "http://schemas.microsoft.com/office/word/2006/wordml", + wps: "http://schemas.microsoft.com/office/word/2010/wordprocessingShape", + Ignorable: "w14 w15 wp14", + }), + ); + + const begin = new Endnote({ + id: -1, + type: EndnoteType.SEPARATOR, + children: [ + new Paragraph({ + spacing: { + after: 0, + line: 240, + lineRule: LineRuleType.AUTO, + }, + children: [new SeperatorRun()], + }), + ], + }); + + this.root.push(begin); + + const spacing = new Endnote({ + id: 0, + type: EndnoteType.CONTINUATION_SEPARATOR, + children: [ + new Paragraph({ + spacing: { + after: 0, + line: 240, + lineRule: LineRuleType.AUTO, + }, + children: [new ContinuationSeperatorRun()], + }), + ], + }); + + this.root.push(spacing); + } + + public createEndnote(id: number, paragraph: readonly Paragraph[]): void { + const endnote = new Endnote({ + id: id, + children: paragraph, + }); + + this.root.push(endnote); + } +} diff --git a/src/file/endnotes/index.ts b/src/file/endnotes/index.ts new file mode 100644 index 00000000000..cc3440e740e --- /dev/null +++ b/src/file/endnotes/index.ts @@ -0,0 +1,2 @@ +export * from "./endnotes"; +export * from "./endnote"; diff --git a/src/file/file.ts b/src/file/file.ts index 82229a2e757..fdc13d9ae3b 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -4,6 +4,7 @@ import { CoreProperties, IPropertiesOptions } from "./core-properties"; import { CustomProperties } from "./custom-properties"; import { HeaderFooterReferenceType, ISectionPropertiesOptions } from "./document/body/section-properties"; import { DocumentWrapper } from "./document-wrapper"; +import { EndnotesWrapper } from "./endnotes-wrapper"; import { FileChild } from "./file-child"; import { FontWrapper } from "./fonts/font-wrapper"; import { FooterWrapper, IDocumentFooter } from "./footer-wrapper"; @@ -48,6 +49,7 @@ export class File { private readonly media: Media; private readonly fileRelationships: Relationships; private readonly footnotesWrapper: FootnotesWrapper; + private readonly endnotesWrapper: EndnotesWrapper; private readonly settings: Settings; private readonly contentTypes: ContentTypes; private readonly customProperties: CustomProperties; @@ -71,6 +73,7 @@ export class File { this.customProperties = new CustomProperties(options.customProperties ?? []); this.appProperties = new AppProperties(); this.footnotesWrapper = new FootnotesWrapper(); + this.endnotesWrapper = new EndnotesWrapper(); this.contentTypes = new ContentTypes(); this.documentWrapper = new DocumentWrapper({ background: options.background }); this.settings = new Settings({ @@ -118,6 +121,13 @@ export class File { } } + if (options.endnotes) { + // eslint-disable-next-line guard-for-in + for (const key in options.endnotes) { + this.endnotesWrapper.View.createEndnote(parseFloat(key), options.endnotes[key].children); + } + } + this.fontWrapper = new FontWrapper(options.fonts ?? []); } @@ -233,6 +243,12 @@ export class File { "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes", "footnotes.xml", ); + this.documentWrapper.Relationships.createRelationship( + // eslint-disable-next-line functional/immutable-data + this.currentRelationshipId++, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes", + "endnotes.xml", + ); this.documentWrapper.Relationships.createRelationship( // eslint-disable-next-line functional/immutable-data this.currentRelationshipId++, @@ -295,6 +311,10 @@ export class File { return this.footnotesWrapper; } + public get Endnotes(): EndnotesWrapper { + return this.endnotesWrapper; + } + public get Settings(): Settings { return this.settings; } diff --git a/src/file/index.ts b/src/file/index.ts index 5f696be52e1..451e7ca0ddc 100644 --- a/src/file/index.ts +++ b/src/file/index.ts @@ -14,6 +14,7 @@ export * from "./header-wrapper"; export * from "./footer-wrapper"; export * from "./header"; export * from "./footnotes"; +export * from "./endnotes"; export * from "./track-revision"; export * from "./shared"; export * from "./border"; diff --git a/src/file/paragraph/run/empty-children.ts b/src/file/paragraph/run/empty-children.ts index b72ba255d63..9b5a7bc02be 100644 --- a/src/file/paragraph/run/empty-children.ts +++ b/src/file/paragraph/run/empty-children.ts @@ -84,6 +84,8 @@ export class FootnoteReferenceElement extends EmptyElement { } } +// TODO: There is a naming inconsistency between the Footnote elements and the Endnote elements. +// Not worrying about it for now to avoid breaking changes. export class EndnoteReference extends EmptyElement { public constructor() { super("w:endnoteRef"); diff --git a/src/file/relationships/relationship/relationship.ts b/src/file/relationships/relationship/relationship.ts index 8cfa46f1428..7bbe8e253d8 100644 --- a/src/file/relationships/relationship/relationship.ts +++ b/src/file/relationships/relationship/relationship.ts @@ -18,6 +18,7 @@ export type RelationshipType = | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes" + | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes" | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font"; diff --git a/src/file/styles/factory.ts b/src/file/styles/factory.ts index 49bac14ae2a..5ffc3089102 100644 --- a/src/file/styles/factory.ts +++ b/src/file/styles/factory.ts @@ -1,5 +1,8 @@ import { DocumentDefaults, IDocumentDefaultsOptions } from "./defaults"; import { + EndnoteReferenceStyle, + EndnoteText, + EndnoteTextChar, FootnoteReferenceStyle, FootnoteText, FootnoteTextChar, @@ -34,6 +37,9 @@ export type IDefaultStylesOptions = { readonly footnoteReference?: IBaseCharacterStyleOptions; readonly footnoteText?: IBaseParagraphStyleOptions; readonly footnoteTextChar?: IBaseCharacterStyleOptions; + readonly endnoteReference?: IBaseCharacterStyleOptions; + readonly endnoteText?: IBaseParagraphStyleOptions; + readonly endnoteTextChar?: IBaseCharacterStyleOptions; }; export class DefaultStylesFactory { @@ -100,6 +106,9 @@ export class DefaultStylesFactory { new FootnoteReferenceStyle(options.footnoteReference || {}), new FootnoteText(options.footnoteText || {}), new FootnoteTextChar(options.footnoteTextChar || {}), + new EndnoteReferenceStyle(options.endnoteReference || {}), + new EndnoteText(options.endnoteText || {}), + new EndnoteTextChar(options.endnoteTextChar || {}), ], }; } diff --git a/src/file/styles/style/default-styles.ts b/src/file/styles/style/default-styles.ts index 50a01f854ef..47d3e69e370 100644 --- a/src/file/styles/style/default-styles.ts +++ b/src/file/styles/style/default-styles.ts @@ -163,6 +163,62 @@ export class FootnoteTextChar extends StyleForCharacter { } } +export class EndnoteText extends StyleForParagraph { + public constructor(options: IBaseParagraphStyleOptions) { + super({ + id: "EndnoteText", + name: "endnote text", + link: "EndnoteTextChar", + basedOn: "Normal", + uiPriority: 99, + semiHidden: true, + unhideWhenUsed: true, + paragraph: { + spacing: { + after: 0, + line: 240, + lineRule: LineRuleType.AUTO, + }, + }, + run: { + size: 20, + }, + ...options, + }); + } +} + +export class EndnoteReferenceStyle extends StyleForCharacter { + public constructor(options: IBaseCharacterStyleOptions) { + super({ + id: "EndnoteReference", + name: "endnote reference", + basedOn: "DefaultParagraphFont", + semiHidden: true, + run: { + superScript: true, + }, + ...options, + }); + } +} + +export class EndnoteTextChar extends StyleForCharacter { + public constructor(options: IBaseCharacterStyleOptions) { + super({ + id: "EndnoteTextChar", + name: "Endnote Text Char", + basedOn: "DefaultParagraphFont", + link: "EndnoteText", + semiHidden: true, + run: { + size: 20, + }, + ...options, + }); + } +} + export class HyperlinkStyle extends StyleForCharacter { public constructor(options: IBaseCharacterStyleOptions) { super({