diff --git a/package.json b/package.json index 330ad72bac..84a941106b 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "fast-xml-parser": "^3.3.6", "image-size": "^0.6.2", "jszip": "^3.1.5", + "lodash": "^4.17.11", "xml": "^1.0.1" }, "author": "Dolan Miu", diff --git a/src/export/packer/next-compiler.ts b/src/export/packer/next-compiler.ts index f06c09e196..e89786c78f 100644 --- a/src/export/packer/next-compiler.ts +++ b/src/export/packer/next-compiler.ts @@ -33,6 +33,8 @@ export class Compiler { } public async compile(file: File): Promise { + file.generateTablesOfContents(); + const zip = new JSZip(); const xmlifiedFileMapping = this.xmlifyFile(file); diff --git a/src/file/document/body/body.ts b/src/file/document/body/body.ts index e274fa359f..941a913bf9 100644 --- a/src/file/document/body/body.ts +++ b/src/file/document/body/body.ts @@ -1,5 +1,5 @@ import { IXmlableObject, XmlComponent } from "file/xml-components"; -import { Paragraph, ParagraphProperties } from "../.."; +import { Paragraph, ParagraphProperties, TableOfContents } from "../.."; import { SectionProperties, SectionPropertiesOptions } from "./section-properties/section-properties"; export class Body extends XmlComponent { @@ -53,6 +53,14 @@ export class Body extends XmlComponent { return this.defaultSection; } + public getTablesOfContents(): TableOfContents[] { + return this.root.filter((child) => child instanceof TableOfContents) as TableOfContents[]; + } + + public getParagraphs(): Paragraph[] { + return this.root.filter((child) => child instanceof Paragraph) as Paragraph[]; + } + private createSectionParagraph(section: SectionProperties): Paragraph { const paragraph = new Paragraph(); const properties = new ParagraphProperties(); diff --git a/src/file/document/document.ts b/src/file/document/document.ts index bae187ca2d..d9dae1020f 100644 --- a/src/file/document/document.ts +++ b/src/file/document/document.ts @@ -66,4 +66,12 @@ export class Document extends XmlComponent { public get Body(): Body { return this.body; } + + public getTablesOfContents(): TableOfContents[] { + return this.body.getTablesOfContents(); + } + + public getParagraphs(): Paragraph[] { + return this.body.getParagraphs(); + } } diff --git a/src/file/file.ts b/src/file/file.ts index 69a2f53a2c..429ec5d9be 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -1,3 +1,4 @@ +import { cloneDeep } from "lodash"; import { AppProperties } from "./app-properties/app-properties"; import { ContentTypes } from "./content-types/content-types"; import { CoreProperties, IPropertiesOptions } from "./core-properties"; @@ -8,13 +9,15 @@ import { FootNotes } from "./footnotes"; import { HeaderWrapper } from "./header-wrapper"; import { Image, Media } from "./media"; import { Numbering } from "./numbering"; -import { Bookmark, Hyperlink, Paragraph } from "./paragraph"; +import { Bookmark, Hyperlink, Paragraph, Run } from "./paragraph"; +import { Begin, End, Separate } from "./paragraph/run/field"; +import { Tab } from "./paragraph/run/tab"; import { Relationships } from "./relationships"; import { Styles } from "./styles"; import { ExternalStylesFactory } from "./styles/external-styles-factory"; import { DefaultStylesFactory } from "./styles/factory"; import { Table } from "./table"; -import { TableOfContents } from "./table-of-contents"; +import { PageReferenceInstruction, TableOfContents } from "./table-of-contents"; export class File { private readonly document: Document; @@ -32,6 +35,7 @@ export class File { private readonly appProperties: AppProperties; private currentRelationshipId: number = 1; + private currentTocBookmarkId: number = 1; constructor(options?: IPropertiesOptions, sectionPropertiesOptions?: SectionPropertiesOptions) { if (!options) { @@ -284,6 +288,65 @@ export class File { } public generateTablesOfContents(): void { - + this.document.getTablesOfContents().forEach((child) => this.generateContent(child)); + } + + private generateContent(toc: TableOfContents): void { + // console.log("TOC", JSON.stringify(toc)); + if (toc.getHeaderRange()) { + this.generateContentForHeaderRange(toc); + } + } + + private generateContentForHeaderRange(toc: TableOfContents): void { + const headerRange = toc.getHeaderRange(); + const hyphenIndex = headerRange.indexOf("-"); + if (hyphenIndex !== -1) { + const rangeBegin = parseInt(headerRange.substring(0, hyphenIndex), 2); + const rangeEnd = parseInt(headerRange.substring(hyphenIndex + 1), 2); + const styles = new Array(); + for (let i = rangeBegin; i <= rangeEnd; i++) { + styles.push(`Heading${i}`); + } + // console.log("Find Headers for range ", rangeBegin, " - ", rangeEnd, styles.join(",")); + this.document + .getParagraphs() + .filter((paragraph) => this.paragraphContainAnyStyle(paragraph, styles)) + .forEach((paragraph) => this.generateContentForParagraph(paragraph, toc)); + } else { + throw new Error(`Invalid headerRange: '${headerRange}'`); + } + } + + private paragraphContainAnyStyle(paragraph: Paragraph, styles: string[]): boolean { + return paragraph.getStyles().some((style) => styles.indexOf(style.styleId) !== -1); + } + + private generateContentForParagraph(paragraph: Paragraph, toc: TableOfContents): void { + const bookmarkId = `_TOC_${this.currentTocBookmarkId}`; + // console.log("Generating content for paragraph: ", bookmarkId); + + const generatedParagraph = cloneDeep(paragraph); + generatedParagraph.rightTabStop(9016, "dot"); + + const tabRun = new Run(); + tabRun.addChildElement(new Tab()); + generatedParagraph.addChildElement(tabRun); + + const beginRun = new Run(); + beginRun.addChildElement(new Begin()); + beginRun.addChildElement(new PageReferenceInstruction(bookmarkId)); + beginRun.addChildElement(new Separate()); + generatedParagraph.addRun(beginRun); + + const endRun = new Run(); + endRun.addChildElement(new End()); + generatedParagraph.addRun(endRun); + + toc.addGeneratedContent(generatedParagraph); + + paragraph.addBookmark(this.createBookmark(bookmarkId, "")); + // console.log("Paragraph after content generation", JSON.stringify(paragraph, null, 2)); + this.currentTocBookmarkId++; } } diff --git a/src/file/paragraph/formatting/style.ts b/src/file/paragraph/formatting/style.ts index 8a4ed4f9ad..30d57eb997 100644 --- a/src/file/paragraph/formatting/style.ts +++ b/src/file/paragraph/formatting/style.ts @@ -1,11 +1,14 @@ import { Attributes, XmlComponent } from "file/xml-components"; export class Style extends XmlComponent { - constructor(type: string) { + public readonly styleId: string; + + constructor(styleId: string) { super("w:pStyle"); + this.styleId = styleId; this.root.push( new Attributes({ - val: type, + val: styleId, }), ); } diff --git a/src/file/paragraph/formatting/tab-stop.ts b/src/file/paragraph/formatting/tab-stop.ts index 86f61fb747..3159a12177 100644 --- a/src/file/paragraph/formatting/tab-stop.ts +++ b/src/file/paragraph/formatting/tab-stop.ts @@ -2,25 +2,27 @@ import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; export class TabStop extends XmlComponent { - constructor(tab: Tab) { + constructor(tab: TabStopItem) { super("w:tabs"); this.root.push(tab); } } export type TabValue = "left" | "right" | "center" | "bar" | "clear" | "decimal" | "end" | "num" | "start"; +export type LeaderType = "dot" | "hyphen" | "middleDot" | "none" | "underscore"; -export class TabAttributes extends XmlAttributeComponent<{ val: TabValue; pos: string | number }> { - protected xmlKeys = { val: "w:val", pos: "w:pos" }; +export class TabAttributes extends XmlAttributeComponent<{ val: TabValue; pos: string | number; leader: LeaderType }> { + protected xmlKeys = { val: "w:val", pos: "w:pos", leader: "w:leader" }; } -export class Tab extends XmlComponent { - constructor(value: TabValue, position: string | number) { +export class TabStopItem extends XmlComponent { + constructor(value: TabValue, position: string | number, leader?: LeaderType) { super("w:tab"); this.root.push( new TabAttributes({ val: value, pos: position, + leader: leader || "none", }), ); } @@ -28,24 +30,24 @@ export class Tab extends XmlComponent { export class MaxRightTabStop extends TabStop { constructor() { - super(new Tab("right", 9026)); + super(new TabStopItem("right", 9026)); } } export class LeftTabStop extends TabStop { - constructor(position: number) { - super(new Tab("left", position)); + constructor(position: number, leader?: LeaderType) { + super(new TabStopItem("left", position, leader)); } } export class RightTabStop extends TabStop { - constructor(position: number) { - super(new Tab("right", position)); + constructor(position: number, leader?: LeaderType) { + super(new TabStopItem("right", position, leader)); } } export class CenterTabStop extends TabStop { - constructor(position: number) { - super(new Tab("center", position)); + constructor(position: number, leader?: LeaderType) { + super(new TabStopItem("center", position, leader)); } } diff --git a/src/file/paragraph/paragraph.ts b/src/file/paragraph/paragraph.ts index 42b34e0c0d..c08d168fe8 100644 --- a/src/file/paragraph/paragraph.ts +++ b/src/file/paragraph/paragraph.ts @@ -12,7 +12,7 @@ import { KeepLines, KeepNext } from "./formatting/keep"; import { PageBreak, PageBreakBefore } from "./formatting/page-break"; import { ISpacingProperties, Spacing } from "./formatting/spacing"; import { Style } from "./formatting/style"; -import { CenterTabStop, LeftTabStop, MaxRightTabStop, RightTabStop } from "./formatting/tab-stop"; +import { CenterTabStop, LeaderType, LeftTabStop, MaxRightTabStop, RightTabStop } from "./formatting/tab-stop"; import { NumberProperties } from "./formatting/unordered-list"; import { Bookmark, Hyperlink } from "./links"; import { ParagraphProperties } from "./properties"; @@ -160,18 +160,18 @@ export class Paragraph extends XmlComponent { return this; } - public leftTabStop(position: number): Paragraph { - this.properties.push(new LeftTabStop(position)); + public leftTabStop(position: number, leader?: LeaderType): Paragraph { + this.properties.push(new LeftTabStop(position, leader)); return this; } - public rightTabStop(position: number): Paragraph { - this.properties.push(new RightTabStop(position)); + public rightTabStop(position: number, leader?: LeaderType): Paragraph { + this.properties.push(new RightTabStop(position, leader)); return this; } - public centerTabStop(position: number): Paragraph { - this.properties.push(new CenterTabStop(position)); + public centerTabStop(position: number, leader?: LeaderType): Paragraph { + this.properties.push(new CenterTabStop(position, leader)); return this; } @@ -232,7 +232,12 @@ export class Paragraph extends XmlComponent { return this; } - public get Properties(): ParagraphProperties { - return this.properties; + public getStyles(): Style[] { + return this.properties.getStyles(); + } + + public addTabStop(run: Run): Paragraph { + this.root.splice(1, 0, run); + return this; } } diff --git a/src/file/paragraph/properties.ts b/src/file/paragraph/properties.ts index 5f0c651246..fa0dd971ff 100644 --- a/src/file/paragraph/properties.ts +++ b/src/file/paragraph/properties.ts @@ -1,6 +1,7 @@ // http://officeopenxml.com/WPparagraphProperties.php import { XmlComponent } from "file/xml-components"; import { Border } from "./formatting/border"; +import { Style } from "./formatting/style"; export class ParagraphProperties extends XmlComponent { public paragraphBorder: Border; @@ -17,4 +18,8 @@ export class ParagraphProperties extends XmlComponent { public push(item: XmlComponent): void { this.root.push(item); } + + public getStyles(): Style[] { + return this.root.filter((child) => child instanceof Style) as Style[]; + } } diff --git a/src/file/table-of-contents/index.ts b/src/file/table-of-contents/index.ts index 13700e9bfb..0f593a9652 100644 --- a/src/file/table-of-contents/index.ts +++ b/src/file/table-of-contents/index.ts @@ -1 +1,2 @@ export * from "./table-of-contents"; +export * from "./page-reference-instruction"; diff --git a/src/file/table-of-contents/page-reference-instruction.ts b/src/file/table-of-contents/page-reference-instruction.ts new file mode 100644 index 0000000000..e630e164b4 --- /dev/null +++ b/src/file/table-of-contents/page-reference-instruction.ts @@ -0,0 +1,13 @@ +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; + +class TextAttributes extends XmlAttributeComponent<{ space: "default" | "preserve" }> { + protected xmlKeys = { space: "xml:space" }; +} + +export class PageReferenceInstruction extends XmlComponent { + constructor(bookmarkId: string) { + super("w:instrText"); + this.root.push(new TextAttributes({ space: "preserve" })); + this.root.push(`PAGEREF ${bookmarkId} \h`); + } +} diff --git a/src/file/table-of-contents/instruction.ts b/src/file/table-of-contents/table-of-contents-instruction.ts similarity index 95% rename from src/file/table-of-contents/instruction.ts rename to src/file/table-of-contents/table-of-contents-instruction.ts index 1ea9481141..aaf0df948d 100644 --- a/src/file/table-of-contents/instruction.ts +++ b/src/file/table-of-contents/table-of-contents-instruction.ts @@ -54,4 +54,8 @@ export class TableOfContentsInstruction extends XmlComponent { } this.root.push(instruction); } + + public getHeaderRange(): string { + return this.properties.headerRange; + } } diff --git a/src/file/table-of-contents/table-of-contents.ts b/src/file/table-of-contents/table-of-contents.ts index d6e17e8cf6..e9984f073e 100644 --- a/src/file/table-of-contents/table-of-contents.ts +++ b/src/file/table-of-contents/table-of-contents.ts @@ -1,27 +1,42 @@ // import { TableOfContentsProperties } from "./properties"; -import { ParagraphProperties } from "file/paragraph"; +import { Paragraph, ParagraphProperties } from "file/paragraph"; import { Run } from "file/paragraph/run"; import { Begin, End, Separate } from "file/paragraph/run/field"; import { XmlComponent } from "file/xml-components"; -import { TableOfContentsInstruction } from "./instruction"; +import { TableOfContentsInstruction } from "./table-of-contents-instruction"; export class TableOfContents extends XmlComponent { // private readonly tocProperties: TableOfContentsProperties; private readonly properties: ParagraphProperties; + private readonly instruction: TableOfContentsInstruction; + constructor(/*tocProperties?: TableOfContentsProperties*/) { - super("w:p"); + super("w:sdt"); this.properties = new ParagraphProperties(); + this.instruction = new TableOfContentsInstruction(); this.root.push(this.properties); // this.tocProperties = tocProperties || new TableOfContentsProperties(); - const firstRun = new Run(); - firstRun.addChildElement(new Begin()); - firstRun.addChildElement(new TableOfContentsInstruction()); - firstRun.addChildElement(new Separate()); - this.root.push(firstRun); + const beginParagraph = new Paragraph(); + const beginRun = new Run(); + beginRun.addChildElement(new Begin()); + beginRun.addChildElement(this.instruction); + beginRun.addChildElement(new Separate()); + beginParagraph.addRun(beginRun); + this.root.push(beginParagraph); - const secondRun = new Run(); - secondRun.addChildElement(new End()); - this.root.push(secondRun); + const endParagraph = new Paragraph(); + const endRun = new Run(); + endRun.addChildElement(new End()); + endParagraph.addRun(endRun); + this.root.push(endParagraph); + } + + public getHeaderRange(): string { + return this.instruction.getHeaderRange(); + } + + public addGeneratedContent(paragraph: Paragraph): void { + this.root.splice(this.root.length - 1, 0, paragraph); } }