diff --git a/.gitignore b/.gitignore index 4f8777b466..e6143b4db7 100644 --- a/.gitignore +++ b/.gitignore @@ -48,8 +48,15 @@ docs/.nojekyll !.vscode/extensions.json .history +# IntelliJ +.idea + # Lock files package-lock.json +yarn.lock # Documents My Document.docx + +# Temporary folder +tmp \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..469d080845 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v8 \ No newline at end of file diff --git a/README.md b/README.md index 7e25666ee0..020cb78f96 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- clippy the assistant + clippy the assistant

@@ -17,11 +17,20 @@ [![PRs Welcome][pr-image]][pr-url]

- drawing + drawing

# Demo +## Browser + +Here are examples of `docx` being used with basic `HTML/JS` in a browser environment. + +* https://codepen.io/anon/pen/dqoVgQ +* https://jsfiddle.net/3xhezb5w/2 + +## Node + Press `endpoint` on the `RunKit` website: ![RunKit Instructions](https://user-images.githubusercontent.com/2917613/38582539-f84311b6-3d07-11e8-90db-5885ae02c3c4.png) @@ -33,9 +42,11 @@ Press `endpoint` on the `RunKit` website: * https://runkit.com/dolanmiu/docx-demo5 - Images * https://runkit.com/dolanmiu/docx-demo6 - Margins * https://runkit.com/dolanmiu/docx-demo7 - Landscape -* https://runkit.com/dolanmiu/docx-demo8/1.0.1 - Header and Footer +* https://runkit.com/dolanmiu/docx-demo8 - Header and Footer * https://runkit.com/dolanmiu/docx-demo10 - **My CV generated with docx** +More [here](https://docx.js.org/#/examples) and [here](https://github.com/dolanmiu/docx/tree/master/demo) + # How to use & Documentation Please refer to the [documentation at https://docx.js.org/](https://docx.js.org/) for details on how to use this library, examples and much more! diff --git a/demo/demo27.ts b/demo/demo27.ts new file mode 100644 index 0000000000..e04fc07e02 --- /dev/null +++ b/demo/demo27.ts @@ -0,0 +1,34 @@ +import * as fs from "fs"; +import { Document, Packer } from "../build"; + +const doc = new Document(); +const myStyles = doc.Styles; + +// The first argument is an ID you use to apply the style to paragraphs +// The second argument is a human-friendly name to show in the UI +myStyles.createParagraphStyle("myWonkyStyle", "My Wonky Style") + .basedOn("Normal") + .next("Normal") + .color("990000") + .italics() + .indent({left: 720}) // 720 TWIP === 720 / 20 pt === .5 in + .spacing({line: 276}); // 276 / 240 = 1.15x line spacing + +myStyles.createParagraphStyle("Heading2", "Heading 2") + .basedOn("Normal") + .next("Normal") + .quickFormat() + .size(26) // 26 half-points === 13pt font + .bold() + .underline("double", "FF0000") + .spacing({before: 240, after: 120}); // TWIP for both + +doc.createParagraph("Hello").style("myWonkyStyle"); +doc.createParagraph("World").heading2(); // Uses the Heading2 style + +const packer = new Packer(); + +packer.toBuffer(doc).then((buffer) => { + fs.writeFileSync("My Document.docx", buffer); + console.log("Document created successfully at project root!"); +}); diff --git a/demo/demo28.ts b/demo/demo28.ts new file mode 100644 index 0000000000..d13acf5b46 --- /dev/null +++ b/demo/demo28.ts @@ -0,0 +1,43 @@ +// Creates two paragraphs, one with a border and one without +// Import from 'docx' rather than '../build' if you install from npm +import * as fs from "fs"; +import { File, Packer, Paragraph, StyleLevel, TableOfContents } from "../build"; + +const doc = new File(); + +// The first argument is an ID you use to apply the style to paragraphs +// The second argument is a human-friendly name to show in the UI +doc.Styles.createParagraphStyle("MySpectacularStyle", "My Spectacular Style") + .basedOn("Heading1") + .next("Heading1") + .color("990000") + .italics(); + +// WordprocessingML docs for TableOfContents can be found here: +// http://officeopenxml.com/WPtableOfContents.php + +// Let's define the properties for generate a TOC for heading 1-5 and MySpectacularStyle, +// making the entries be hyperlinks for the paragraph +const toc = new TableOfContents("Summary", { + hyperlink: true, + headingStyleRange: "1-5", + stylesWithLevels: [new StyleLevel("MySpectacularStyle", 1)], +}); + +doc.addTableOfContents(toc); + +doc.addParagraph(new Paragraph("Header #1").heading1().pageBreakBefore()); +doc.addParagraph(new Paragraph("I'm a little text very nicely written.'")); + +doc.addParagraph(new Paragraph("Header #2").heading1().pageBreakBefore()); +doc.addParagraph(new Paragraph("I'm a other text very nicely written.'")); +doc.addParagraph(new Paragraph("Header #2.1").heading2()); +doc.addParagraph(new Paragraph("I'm a another text very nicely written.'")); + +doc.addParagraph(new Paragraph("My Spectacular Style #1").style("MySpectacularStyle").pageBreakBefore()); + +const packer = new Packer(); + +packer.toBuffer(doc).then((buffer) => { + fs.writeFileSync("My Document.docx", buffer); +}); diff --git a/docs/README.md b/docs/README.md index f3134b8fa3..4fc5757477 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,5 @@

- clippy the assistant + clippy the assistant

@@ -54,6 +54,10 @@ exporter.pack("My First Document"); [@h4buli](https://github.com/h4buli) +

+ clippy the assistant +

+ --- Made with 💖 diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 0b159a95a8..4b9a626a03 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -16,6 +16,7 @@ * [Bullet Points](usage/bullet-points.md) * [Numbering](usage/numbering.md) * [Tab Stops](usage/tab-stops.md) + * [Table of Contents](usage/table-of-contents.md) * Styling * [Styling with JS](usage/styling-with-js.md) * [Styling with XML](usage/styling-with-xml.md) diff --git a/docs/usage/styling-with-js.md b/docs/usage/styling-with-js.md index 3e482bca14..4f362192af 100644 --- a/docs/usage/styling-with-js.md +++ b/docs/usage/styling-with-js.md @@ -20,6 +20,7 @@ const name = new TextRun("Name:") * `.size(halfPts)`: Set the font size, measured in half-points * `.font(name)`: Set the run's font * `.style(name)`: Apply a named run style + * `.characterSpacing(value)`: Set the character spacing adjustment (in TWIPs) * For paragraph formatting: * `.heading1()`, `.heading2()`, `.heading3()`, `.heading4()`, `.heading5()`, `.title()`: apply the appropriate style to the paragraph * `.left()`, `.center()`, `.right()`, `.justified()`: set the paragraph's alignment diff --git a/docs/usage/table-of-contents.md b/docs/usage/table-of-contents.md new file mode 100644 index 0000000000..7c226833cd --- /dev/null +++ b/docs/usage/table-of-contents.md @@ -0,0 +1,76 @@ +# Table of Contents + +You can generate table of contents with `docx`. More information can be found [here](http://officeopenxml.com/WPtableOfContents.php). + +>Tables of Contents are fields and, by design, it's content is only generated or updated by Word. We can't do it programatically. +>This is why, when you open a the file, Word you will prompt the message "This document contains fields that may refer to other files. Do you want to update the fields in this document?". +>You have say yes to Word generate the content of all table of contents. + +The complete documentation can be found [here](https://www.ecma-international.org/publications/standards/Ecma-376.htm) (at Part 1, Page 1251). + +## How to + +All you need to do is create a `TableOfContents` object and assign it to the document. + +```js +const toc = new TableOfContents("Summary", { + hyperlink: true, + headingStyleRange: "1-5", + stylesWithLevels: [new StyleLevel("MySpectacularStyle", 1)] +}); + +doc.addTableOfContents(toc); +``` + +## Table of Contents Options + +Here is the list of all options that you can use to generate your tables of contents: + +| Option | Type | TOC Field Switch | Description | +| --- | --- | --- | --- | +|captionLabel|string|`\a`|Includes captioned items, but omits caption labels and numbers. The identifier designated by `text` in this switch's field-argument corresponds to the caption label. Use ``\c`` to build a table of captions with labels and numbers.| +|entriesFromBookmark|string|`\b`|Includes entries only from the portion of the document marked by the bookmark named by `text` in this switch's field-argument.| +|captionLabelIncludingNumbers|string|`\c`|Includes figures, tables, charts, and other items that are numbered by a SEQ field (§17.16.5.56). The sequence identifier designated by `text` in this switch's field-argument, which corresponds to the caption label, shall match the identifier in the corresponding SEQ field.| +|sequenceAndPageNumbersSeparator|string|`\d`|When used with `\s`, the `text` in this switch's field-argument defines the separator between sequence and page numbers. The default separator is a hyphen (-).| +|tcFieldIdentifier|string|`\f`|Includes only those TC fields whose identifier exactly matches the `text` in this switch's field-argument (which is typically a letter).| +|hyperlink|boolean|`\h`|Makes the table of contents entries hyperlinks.| +|tcFieldLevelRange|string|`\l`|Includes TC fields that assign entries to one of the levels specified by `text` in this switch's field-argument as a range having the form startLevel-endLevel, where startLevel and endLevel are integers, and startLevel has a value equal-to or less-than endLevel. TC fields that assign entries to lower levels are skipped.| +|pageNumbersEntryLevelsRange|string|`\n`|Without field-argument, omits page numbers from the table of contents. Page numbers are omitted from all levels unless a range of entry levels is specified by `text` in this switch's field-argument. A range is specified as for `\l`.| +|headingStyleRange|string|`\o`|Uses paragraphs formatted with all or the specified range of builtin heading styles. Headings in a style range are specified by `text` in this switch's field-argument using the notation specified as for `\l`, where each integer corresponds to the style with a style ID of HeadingX (e.g. 1 corresponds to Heading1). If no heading range is specified, all heading levels used in the document are listed.| +|entryAndPageNumberSeparator|string|`\p`|`text` in this switch's field-argument specifies a sequence of characters that separate an entry and its page number. The default is a tab with leader dots.| +|seqFieldIdentifierForPrefix|string|`\s`|For entries numbered with a SEQ field (§17.16.5.56), adds a prefix to the page number. The prefix depends on the type of entry. `text` in this switch's field-argument shall match the identifier in the SEQ field.| +|stylesWithLevels|StyleLevel[]|`\t`| Uses paragraphs formatted with styles other than the built-in heading styles. `text` in this switch's field-argument specifies those styles as a set of comma-separated doublets, with each doublet being a comma-separated set of style name and table of content level. `\t` can be combined with `\o`.| +|useAppliedParagraphOutlineLevel|boolean|`\u`|Uses the applied paragraph outline level.| +|preserveTabInEntries|boolean|`\w`|Preserves tab entries within table entries.| +|preserveNewLineInEntries|boolean|`\x`|Preserves newline characters within table entries.| +|hideTabAndPageNumbersInWebView|boolean|`\z`|Hides tab leader and page numbers in web page view (§17.18.102).| + +## Examples + +```js +// Let's define the options for generate a TOC for heading 1-5 and MySpectacularStyle, +// making the entries be hyperlinks for the paragraph +const toc = new TableOfContents("Summary", { + hyperlink: true, + headingStyleRange: "1-5", + stylesWithLevels: [new StyleLevel("MySpectacularStyle", 1)] +}); + +doc.addTableOfContents(toc); + +doc.addParagraph(new Paragraph("Header #1").heading1().pageBreakBefore()); +doc.addParagraph(new Paragraph("I'm a little text, very nicely written.'")); + +doc.addParagraph(new Paragraph("Header #2").heading1().pageBreakBefore()); +doc.addParagraph(new Paragraph("I'm another text very nicely written.'")); +doc.addParagraph(new Paragraph("Header #2.1").heading2()); +doc.addParagraph(new Paragraph("I'm another text very nicely written.'")); + +doc.addParagraph(new Paragraph("My Spectacular Style #1").style("MySpectacularStyle").pageBreakBefore()); +``` + +### Complete example + +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/demo28.ts ":include") + +_Source: https://github.com/dolanmiu/docx/blob/master/demo/demo28.ts_ diff --git a/logo/logo-small.gif b/logo/logo-small.gif new file mode 100644 index 0000000000..4b4a567c02 Binary files /dev/null and b/logo/logo-small.gif differ diff --git a/logo/logo-small.png b/logo/logo-small.png new file mode 100644 index 0000000000..966fd4b1e7 Binary files /dev/null and b/logo/logo-small.png differ diff --git a/logo/logo-small.psd b/logo/logo-small.psd new file mode 100644 index 0000000000..4c6b32123c Binary files /dev/null and b/logo/logo-small.psd differ diff --git a/logo/logo.psd b/logo/logo.psd new file mode 100644 index 0000000000..9763168e2c Binary files /dev/null and b/logo/logo.psd differ diff --git a/package.json b/package.json index 330ad72bac..e63dd166bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "docx", - "version": "4.0.0", + "version": "4.1.0", "description": "Generate .docx documents with JavaScript (formerly Office-Clippy)", "main": "build/index.js", "scripts": { @@ -82,5 +82,8 @@ "typedoc": "^0.11.1", "typescript": "2.9.2", "webpack": "^3.10.0" + }, + "engines": { + "node": ">=8" } } diff --git a/src/export/packer/next-compiler.spec.ts b/src/export/packer/next-compiler.spec.ts index fc02d4a814..774b71c26f 100644 --- a/src/export/packer/next-compiler.spec.ts +++ b/src/export/packer/next-compiler.spec.ts @@ -19,7 +19,7 @@ describe("Compiler", () => { const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name); expect(fileNames).is.an.instanceof(Array); - expect(fileNames).has.length(17); + expect(fileNames).has.length(18); expect(fileNames).to.include("word/document.xml"); expect(fileNames).to.include("word/styles.xml"); expect(fileNames).to.include("docProps/core.xml"); @@ -29,6 +29,7 @@ describe("Compiler", () => { expect(fileNames).to.include("word/_rels/header1.xml.rels"); expect(fileNames).to.include("word/footer1.xml"); expect(fileNames).to.include("word/footnotes.xml"); + expect(fileNames).to.include("word/settings.xml"); expect(fileNames).to.include("word/_rels/footer1.xml.rels"); expect(fileNames).to.include("word/_rels/document.xml.rels"); expect(fileNames).to.include("[Content_Types].xml"); @@ -47,7 +48,7 @@ describe("Compiler", () => { const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name); expect(fileNames).is.an.instanceof(Array); - expect(fileNames).has.length(25); + expect(fileNames).has.length(26); expect(fileNames).to.include("word/header1.xml"); expect(fileNames).to.include("word/_rels/header1.xml.rels"); diff --git a/src/export/packer/next-compiler.ts b/src/export/packer/next-compiler.ts index f06c09e196..1ee02bf589 100644 --- a/src/export/packer/next-compiler.ts +++ b/src/export/packer/next-compiler.ts @@ -23,6 +23,7 @@ interface IXmlifyedFileMapping { ContentTypes: IXmlifyedFile; AppProperties: IXmlifyedFile; FootNotes: IXmlifyedFile; + Settings: IXmlifyedFile; } export class Compiler { @@ -62,6 +63,7 @@ export class Compiler { } private xmlifyFile(file: File): IXmlifyedFileMapping { + file.verifyUpdateFields(); return { Document: { data: xml(this.formatter.format(file.Document), true), @@ -120,6 +122,10 @@ export class Compiler { data: xml(this.formatter.format(file.FootNotes)), path: "word/footnotes.xml", }, + Settings: { + data: xml(this.formatter.format(file.Settings)), + path: "word/settings.xml", + }, }; } } 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/body/section-properties/page-margin/page-margin-attributes.ts b/src/file/document/body/section-properties/page-margin/page-margin-attributes.ts index a9d1ae46ee..e486ae5c72 100644 --- a/src/file/document/body/section-properties/page-margin/page-margin-attributes.ts +++ b/src/file/document/body/section-properties/page-margin/page-margin-attributes.ts @@ -8,6 +8,7 @@ export interface IPageMarginAttributes { header?: number; footer?: number; gutter?: number; + mirror?: boolean; } export class PageMarginAttributes extends XmlAttributeComponent { @@ -19,5 +20,6 @@ export class PageMarginAttributes extends XmlAttributeComponent { header: 708, footer: 708, gutter: 0, + mirror: false, space: 708, linePitch: 360, headerId: 100, @@ -40,6 +41,7 @@ describe("SectionProperties", () => { "w:left": 1440, "w:header": 708, "w:gutter": 0, + "w:mirrorMargins": false, }, }, ], @@ -69,6 +71,7 @@ describe("SectionProperties", () => { "w:left": 1440, "w:header": 708, "w:gutter": 0, + "w:mirrorMargins": false, }, }, ], @@ -99,6 +102,7 @@ describe("SectionProperties", () => { "w:left": 1440, "w:header": 708, "w:gutter": 0, + "w:mirrorMargins": false, }, }, ], @@ -124,6 +128,7 @@ describe("SectionProperties", () => { "w:left": 1440, "w:header": 708, "w:gutter": 0, + "w:mirrorMargins": false, }, }, ], @@ -150,6 +155,7 @@ describe("SectionProperties", () => { "w:left": 1440, "w:header": 708, "w:gutter": 0, + "w:mirrorMargins": false, }, }, ], diff --git a/src/file/document/body/section-properties/section-properties.ts b/src/file/document/body/section-properties/section-properties.ts index c23964cd14..5042c8a3ab 100644 --- a/src/file/document/body/section-properties/section-properties.ts +++ b/src/file/document/body/section-properties/section-properties.ts @@ -38,6 +38,7 @@ export class SectionProperties extends XmlComponent { header: 708, footer: 708, gutter: 0, + mirror: false, space: 708, linePitch: 360, orientation: PageOrientation.PORTRAIT, @@ -69,6 +70,7 @@ export class SectionProperties extends XmlComponent { mergedOptions.header, mergedOptions.footer, mergedOptions.gutter, + mergedOptions.mirror, ), ); this.root.push(new Columns(mergedOptions.space)); diff --git a/src/file/document/document.ts b/src/file/document/document.ts index 72e48852d7..d9dae1020f 100644 --- a/src/file/document/document.ts +++ b/src/file/document/document.ts @@ -2,6 +2,7 @@ import { XmlComponent } from "file/xml-components"; import { Paragraph } from "../paragraph"; import { Table } from "../table"; +import { TableOfContents } from "../table-of-contents"; import { Body } from "./body"; import { SectionPropertiesOptions } from "./body/section-properties"; import { DocumentAttributes } from "./document-attributes"; @@ -41,6 +42,11 @@ export class Document extends XmlComponent { return this; } + public addTableOfContents(toc: TableOfContents): Document { + this.body.push(toc); + return this; + } + public createParagraph(text?: string): Paragraph { const para = new Paragraph(text); this.addParagraph(para); @@ -60,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/document/index.ts b/src/file/document/index.ts index 6b128299f6..3430666623 100644 --- a/src/file/document/index.ts +++ b/src/file/document/index.ts @@ -1,2 +1,3 @@ export * from "./document"; +export * from "./document-attributes"; export * from "./body"; diff --git a/src/file/file.ts b/src/file/file.ts index 472baf28b5..afa268a5a7 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -10,14 +10,16 @@ import { Image, Media } from "./media"; import { Numbering } from "./numbering"; import { Bookmark, Hyperlink, Paragraph } from "./paragraph"; import { Relationships } from "./relationships"; +import { Settings } from "./settings"; 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"; export class File { private readonly document: Document; - private readonly styles: Styles; + private styles: Styles; private readonly coreProperties: CoreProperties; private readonly numbering: Numbering; private readonly media: Media; @@ -26,6 +28,7 @@ export class File { private readonly headerWrapper: HeaderWrapper[] = []; private readonly footerWrapper: FooterWrapper[] = []; private readonly footNotes: FootNotes; + private readonly settings: Settings; private readonly contentTypes: ContentTypes; private readonly appProperties: AppProperties; @@ -105,6 +108,11 @@ export class File { sectionPropertiesOptions.footerId = footer.Footer.ReferenceId; } this.document = new Document(sectionPropertiesOptions); + this.settings = new Settings(); + } + + public addTableOfContents(toc: TableOfContents): void { + this.document.addTableOfContents(toc); } public addParagraph(paragraph: Paragraph): void { @@ -214,6 +222,10 @@ export class File { return this.styles; } + public set Styles(styles: Styles) { + this.styles = styles; + } + public get CoreProperties(): CoreProperties { return this.coreProperties; } @@ -277,4 +289,14 @@ export class File { public get FootNotes(): FootNotes { return this.footNotes; } + + public get Settings(): Settings { + return this.settings; + } + + public verifyUpdateFields(): void { + if (this.document.getTablesOfContents().length) { + this.settings.addUpdateFields(); + } + } } diff --git a/src/file/index.ts b/src/file/index.ts index d1e04ffcf7..8705f40011 100644 --- a/src/file/index.ts +++ b/src/file/index.ts @@ -6,4 +6,5 @@ export * from "./media"; export * from "./drawing"; export * from "./document"; export * from "./styles"; +export * from "./table-of-contents"; export * from "./xml-components"; 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.spec.ts b/src/file/paragraph/formatting/tab-stop.spec.ts index bb9fa80d96..5b324623f5 100644 --- a/src/file/paragraph/formatting/tab-stop.spec.ts +++ b/src/file/paragraph/formatting/tab-stop.spec.ts @@ -1,7 +1,7 @@ import { assert } from "chai"; import { Utility } from "../../../tests/utility"; -import { LeftTabStop, MaxRightTabStop } from "./tab-stop"; +import { LeftTabStop, MaxRightTabStop, RightTabStop } from "./tab-stop"; describe("LeftTabStop", () => { let tabStop: LeftTabStop; @@ -28,7 +28,28 @@ describe("LeftTabStop", () => { }); describe("RightTabStop", () => { - // TODO + let tabStop: RightTabStop; + + beforeEach(() => { + tabStop = new RightTabStop(100, "dot"); + }); + + describe("#constructor()", () => { + it("should create a Tab Stop with correct attributes", () => { + const newJson = Utility.jsonify(tabStop); + const attributes = { + val: "right", + pos: 100, + leader: "dot", + }; + assert.equal(JSON.stringify(newJson.root[0].root[0].root), JSON.stringify(attributes)); + }); + + it("should create a Tab Stop with w:tab", () => { + const newJson = Utility.jsonify(tabStop); + assert.equal(newJson.root[0].rootKey, "w:tab"); + }); + }); }); describe("MaxRightTabStop", () => { diff --git a/src/file/paragraph/formatting/tab-stop.ts b/src/file/paragraph/formatting/tab-stop.ts index 86f61fb747..5dc4f68e4f 100644 --- a/src/file/paragraph/formatting/tab-stop.ts +++ b/src/file/paragraph/formatting/tab-stop.ts @@ -2,50 +2,52 @@ 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, }), ); } } export class MaxRightTabStop extends TabStop { - constructor() { - super(new Tab("right", 9026)); + constructor(leader?: LeaderType) { + super(new TabStopItem("right", 9026, leader)); } } 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..b7930970b5 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"; @@ -30,6 +30,10 @@ export class Paragraph extends XmlComponent { } } + public get paragraphProperties(): ParagraphProperties { + return this.properties; + } + public get Borders(): Border { return this.properties.paragraphBorder; } @@ -155,23 +159,23 @@ export class Paragraph extends XmlComponent { return this; } - public maxRightTabStop(): Paragraph { - this.properties.push(new MaxRightTabStop()); + public maxRightTabStop(leader?: LeaderType): Paragraph { + this.properties.push(new MaxRightTabStop(leader)); 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 +236,8 @@ export class Paragraph extends XmlComponent { return this; } - public get Properties(): ParagraphProperties { - return this.properties; + public addTabStop(run: Run): Paragraph { + this.root.splice(1, 0, run); + return this; } } diff --git a/src/file/paragraph/run/field.ts b/src/file/paragraph/run/field.ts new file mode 100644 index 0000000000..0e72c1d624 --- /dev/null +++ b/src/file/paragraph/run/field.ts @@ -0,0 +1,26 @@ +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; + +class FidCharAttrs extends XmlAttributeComponent<{ type: "begin" | "end" | "separate"; dirty?: boolean }> { + protected xmlKeys = { type: "w:fldCharType", dirty: "w:dirty" }; +} + +export class Begin extends XmlComponent { + constructor(dirty?: boolean) { + super("w:fldChar"); + this.root.push(new FidCharAttrs({ type: "begin", dirty })); + } +} + +export class Separate extends XmlComponent { + constructor(dirty?: boolean) { + super("w:fldChar"); + this.root.push(new FidCharAttrs({ type: "separate", dirty })); + } +} + +export class End extends XmlComponent { + constructor(dirty?: boolean) { + super("w:fldChar"); + this.root.push(new FidCharAttrs({ type: "end", dirty })); + } +} diff --git a/src/file/paragraph/run/formatting.ts b/src/file/paragraph/run/formatting.ts index 8b1be933ec..30d437e268 100644 --- a/src/file/paragraph/run/formatting.ts +++ b/src/file/paragraph/run/formatting.ts @@ -25,6 +25,17 @@ export class BoldComplexScript extends XmlComponent { } } +export class CharacterSpacing extends XmlComponent { + constructor(value: number) { + super("w:spacing"); + this.root.push( + new Attributes({ + val: value, + }), + ); + } +} + export class Italics extends XmlComponent { constructor() { super("w:i"); diff --git a/src/file/paragraph/run/page-number.ts b/src/file/paragraph/run/page-number.ts index 4048c4f79a..ab3592fd18 100644 --- a/src/file/paragraph/run/page-number.ts +++ b/src/file/paragraph/run/page-number.ts @@ -1,20 +1,9 @@ import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; -class FidCharAttrs extends XmlAttributeComponent<{ type: "begin" | "end" | "separate" }> { - protected xmlKeys = { type: "w:fldCharType" }; -} - class TextAttributes extends XmlAttributeComponent<{ space: "default" | "preserve" }> { protected xmlKeys = { space: "xml:space" }; } -export class Begin extends XmlComponent { - constructor() { - super("w:fldChar"); - this.root.push(new FidCharAttrs({ type: "begin" })); - } -} - export class Page extends XmlComponent { constructor() { super("w:instrText"); @@ -22,17 +11,3 @@ export class Page extends XmlComponent { this.root.push("PAGE"); } } - -export class Separate extends XmlComponent { - constructor() { - super("w:fldChar"); - this.root.push(new FidCharAttrs({ type: "separate" })); - } -} - -export class End extends XmlComponent { - constructor() { - super("w:fldChar"); - this.root.push(new FidCharAttrs({ type: "end" })); - } -} diff --git a/src/file/paragraph/run/run.ts b/src/file/paragraph/run/run.ts index 70b344842f..b7722e1e44 100644 --- a/src/file/paragraph/run/run.ts +++ b/src/file/paragraph/run/run.ts @@ -1,6 +1,7 @@ // http://officeopenxml.com/WPtext.php import { Break } from "./break"; import { Caps, SmallCaps } from "./caps"; +import { Begin, End, Separate } from "./field"; import { Bold, BoldComplexScript, @@ -13,7 +14,7 @@ import { SizeComplexScript, Strike, } from "./formatting"; -import { Begin, End, Page, Separate } from "./page-number"; +import { Page } from "./page-number"; import { RunProperties } from "./properties"; import { RunFonts } from "./run-fonts"; import { SubScript, SuperScript } from "./script"; diff --git a/src/file/settings/index.ts b/src/file/settings/index.ts new file mode 100644 index 0000000000..d750485a16 --- /dev/null +++ b/src/file/settings/index.ts @@ -0,0 +1,2 @@ +export * from "./settings"; +export * from "./update-fields"; diff --git a/src/file/settings/settings.spec.ts b/src/file/settings/settings.spec.ts new file mode 100644 index 0000000000..9ba0016319 --- /dev/null +++ b/src/file/settings/settings.spec.ts @@ -0,0 +1,60 @@ +import { expect } from "chai"; +import { Formatter } from "../../export/formatter"; +import { Settings } from "./"; +describe("Settings", () => { + describe("#constructor", () => { + it("should create a empty Settings with correct rootKey", () => { + const settings = new Settings(); + const tree = new Formatter().format(settings); + let keys = Object.keys(tree); + expect(keys).is.an.instanceof(Array); + expect(keys).has.length(1); + expect(keys[0]).to.be.equal("w:settings"); + expect(tree["w:settings"]).is.an.instanceof(Array); + expect(tree["w:settings"]).has.length(1); + keys = Object.keys(tree["w:settings"][0]); + expect(keys).is.an.instanceof(Array); + expect(keys).has.length(1); + expect(keys[0]).to.be.equal("_attr"); + }); + }); + describe("#addUpdateFields", () => { + const assertSettingsWithUpdateFields = (settings: Settings) => { + const tree = new Formatter().format(settings); + let keys = Object.keys(tree); + expect(keys).is.an.instanceof(Array); + expect(keys).has.length(1); + expect(keys[0]).to.be.equal("w:settings"); + const rootArray = tree["w:settings"]; + expect(rootArray).is.an.instanceof(Array); + expect(rootArray).has.length(2); + keys = Object.keys(rootArray[0]); + expect(keys).is.an.instanceof(Array); + expect(keys).has.length(1); + expect(keys[0]).to.be.equal("_attr"); + keys = Object.keys(rootArray[1]); + expect(keys).is.an.instanceof(Array); + expect(keys).has.length(1); + expect(keys[0]).to.be.equal("w:updateFields"); + const updateFieldsArray = rootArray[1]["w:updateFields"]; + keys = Object.keys(updateFieldsArray[0]); + expect(keys).is.an.instanceof(Array); + expect(keys).has.length(1); + expect(keys[0]).to.be.equal("_attr"); + const updateFieldsAttr = updateFieldsArray[0]._attr; + expect(updateFieldsAttr["w:val"]).to.be.equal(true); + }; + it("should add a UpdateFields with value true", () => { + const settings = new Settings(); + settings.addUpdateFields(); + assertSettingsWithUpdateFields(settings); + }); + it("should add a UpdateFields with value true only once", () => { + const settings = new Settings(); + settings.addUpdateFields(); + assertSettingsWithUpdateFields(settings); + settings.addUpdateFields(); + assertSettingsWithUpdateFields(settings); + }); + }); +}); diff --git a/src/file/settings/settings.ts b/src/file/settings/settings.ts new file mode 100644 index 0000000000..d955de0177 --- /dev/null +++ b/src/file/settings/settings.ts @@ -0,0 +1,73 @@ +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; +import { UpdateFields } from "./update-fields"; +export interface ISettingsAttributesProperties { + wpc?: string; + mc?: string; + o?: string; + r?: string; + m?: string; + v?: string; + wp14?: string; + wp?: string; + w10?: string; + w?: string; + w14?: string; + w15?: string; + wpg?: string; + wpi?: string; + wne?: string; + wps?: string; + Ignorable?: string; +} +export class SettingsAttributes extends XmlAttributeComponent { + protected 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", + }; +} +export class Settings extends XmlComponent { + constructor() { + super("w:settings"); + this.root.push( + new SettingsAttributes({ + 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", + }), + ); + } + public addUpdateFields(): void { + if (!this.root.find((child) => child instanceof UpdateFields)) { + this.addChildElement(new UpdateFields()); + } + } +} diff --git a/src/file/settings/update-fields.spec.ts b/src/file/settings/update-fields.spec.ts new file mode 100644 index 0000000000..70fad8685c --- /dev/null +++ b/src/file/settings/update-fields.spec.ts @@ -0,0 +1,40 @@ +import { expect } from "chai"; +import { Formatter } from "../../export/formatter"; +import { UpdateFields } from "./"; +const UF_TRUE = { + "w:updateFields": [ + { + _attr: { + "w:val": true, + }, + }, + ], +}; +const UF_FALSE = { + "w:updateFields": [ + { + _attr: { + "w:val": false, + }, + }, + ], +}; +describe("Update Fields", () => { + describe("#constructor", () => { + it("should construct a Update Fields with TRUE value by default", () => { + const uf = new UpdateFields(); + const tree = new Formatter().format(uf); + expect(tree).to.be.deep.equal(UF_TRUE); + }); + it("should construct a Update Fields with TRUE value", () => { + const uf = new UpdateFields(true); + const tree = new Formatter().format(uf); + expect(tree).to.be.deep.equal(UF_TRUE); + }); + it("should construct a Update Fields with FALSE value", () => { + const uf = new UpdateFields(false); + const tree = new Formatter().format(uf); + expect(tree).to.be.deep.equal(UF_FALSE); + }); + }); +}); diff --git a/src/file/settings/update-fields.ts b/src/file/settings/update-fields.ts new file mode 100644 index 0000000000..782aa13264 --- /dev/null +++ b/src/file/settings/update-fields.ts @@ -0,0 +1,19 @@ +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; +export interface IUpdateFieldsAttributesProperties { + enabled: boolean; +} +export class UpdateFieldsAttributes extends XmlAttributeComponent { + protected xmlKeys = { + enabled: "w:val", + }; +} +export class UpdateFields extends XmlComponent { + constructor(enabled: boolean = true) { + super("w:updateFields"); + this.root.push( + new UpdateFieldsAttributes({ + enabled, + }), + ); + } +} diff --git a/src/file/styles/style/index.ts b/src/file/styles/style/index.ts index 655cc28d2a..ee2d6d84aa 100644 --- a/src/file/styles/style/index.ts +++ b/src/file/styles/style/index.ts @@ -47,12 +47,14 @@ export class ParagraphStyle extends Style { this.root.push(this.runProperties); } - public addParagraphProperty(property: XmlComponent): void { + public addParagraphProperty(property: XmlComponent): ParagraphStyle { this.paragraphProperties.push(property); + return this; } - public addRunProperty(property: XmlComponent): void { + public addRunProperty(property: XmlComponent): ParagraphStyle { this.runProperties.push(property); + return this; } public basedOn(parentId: string): ParagraphStyle { @@ -73,121 +75,101 @@ export class ParagraphStyle extends Style { // ---------- Run formatting ---------------------- // public size(twips: number): ParagraphStyle { - this.addRunProperty(new formatting.Size(twips)); - this.addRunProperty(new formatting.SizeComplexScript(twips)); - return this; + return this.addRunProperty(new formatting.Size(twips)).addRunProperty(new formatting.SizeComplexScript(twips)); } public bold(): ParagraphStyle { - this.addRunProperty(new formatting.Bold()); - return this; + return this.addRunProperty(new formatting.Bold()); } public italics(): ParagraphStyle { - this.addRunProperty(new formatting.Italics()); - return this; + return this.addRunProperty(new formatting.Italics()); } public smallCaps(): ParagraphStyle { - this.addRunProperty(new formatting.SmallCaps()); - return this; + return this.addRunProperty(new formatting.SmallCaps()); } public allCaps(): ParagraphStyle { - this.addRunProperty(new formatting.Caps()); - return this; + return this.addRunProperty(new formatting.Caps()); } public strike(): ParagraphStyle { - this.addRunProperty(new formatting.Strike()); - return this; + return this.addRunProperty(new formatting.Strike()); } public doubleStrike(): ParagraphStyle { - this.addRunProperty(new formatting.DoubleStrike()); - return this; + return this.addRunProperty(new formatting.DoubleStrike()); } public subScript(): ParagraphStyle { - this.addRunProperty(new formatting.SubScript()); - return this; + return this.addRunProperty(new formatting.SubScript()); } public superScript(): ParagraphStyle { - this.addRunProperty(new formatting.SuperScript()); - return this; + return this.addRunProperty(new formatting.SuperScript()); } public underline(underlineType?: string, color?: string): ParagraphStyle { - this.addRunProperty(new formatting.Underline(underlineType, color)); - return this; + return this.addRunProperty(new formatting.Underline(underlineType, color)); } public color(color: string): ParagraphStyle { - this.addRunProperty(new formatting.Color(color)); - return this; + return this.addRunProperty(new formatting.Color(color)); } public font(fontName: string): ParagraphStyle { - this.addRunProperty(new formatting.RunFonts(fontName)); - return this; + return this.addRunProperty(new formatting.RunFonts(fontName)); + } + + public characterSpacing(value: number): ParagraphStyle { + return this.addRunProperty(new formatting.CharacterSpacing(value)); } // --------------------- Paragraph formatting ------------------------ // public center(): ParagraphStyle { - this.addParagraphProperty(new paragraph.Alignment("center")); - return this; + return this.addParagraphProperty(new paragraph.Alignment("center")); } public left(): ParagraphStyle { - this.addParagraphProperty(new paragraph.Alignment("left")); - return this; + return this.addParagraphProperty(new paragraph.Alignment("left")); } public right(): ParagraphStyle { - this.addParagraphProperty(new paragraph.Alignment("right")); - return this; + return this.addParagraphProperty(new paragraph.Alignment("right")); } public justified(): ParagraphStyle { - this.addParagraphProperty(new paragraph.Alignment("both")); - return this; + return this.addParagraphProperty(new paragraph.Alignment("both")); } public thematicBreak(): ParagraphStyle { - this.addParagraphProperty(new paragraph.ThematicBreak()); - return this; + return this.addParagraphProperty(new paragraph.ThematicBreak()); } public maxRightTabStop(): ParagraphStyle { - this.addParagraphProperty(new paragraph.MaxRightTabStop()); - return this; + return this.addParagraphProperty(new paragraph.MaxRightTabStop()); } public leftTabStop(position: number): ParagraphStyle { - this.addParagraphProperty(new paragraph.LeftTabStop(position)); - return this; + return this.addParagraphProperty(new paragraph.LeftTabStop(position)); } public indent(attrs: object): ParagraphStyle { - this.addParagraphProperty(new paragraph.Indent(attrs)); - return this; + return this.addParagraphProperty(new paragraph.Indent(attrs)); } public spacing(params: paragraph.ISpacingProperties): ParagraphStyle { - this.addParagraphProperty(new paragraph.Spacing(params)); - return this; + return this.addParagraphProperty(new paragraph.Spacing(params)); } public keepNext(): ParagraphStyle { - this.addParagraphProperty(new paragraph.KeepNext()); - return this; + return this.addParagraphProperty(new paragraph.KeepNext()); } public keepLines(): ParagraphStyle { - this.addParagraphProperty(new paragraph.KeepLines()); - return this; + return this.addParagraphProperty(new paragraph.KeepLines()); } } @@ -267,24 +249,21 @@ export class CharacterStyle extends Style { return this; } - public addRunProperty(property: XmlComponent): void { + public addRunProperty(property: XmlComponent): CharacterStyle { this.runProperties.push(property); + return this; } public color(color: string): CharacterStyle { - this.addRunProperty(new formatting.Color(color)); - return this; + return this.addRunProperty(new formatting.Color(color)); } public underline(underlineType?: string, color?: string): CharacterStyle { - this.addRunProperty(new formatting.Underline(underlineType, color)); - return this; + return this.addRunProperty(new formatting.Underline(underlineType, color)); } public size(twips: number): CharacterStyle { - this.addRunProperty(new formatting.Size(twips)); - this.addRunProperty(new formatting.SizeComplexScript(twips)); - return this; + return this.addRunProperty(new formatting.Size(twips)).addRunProperty(new formatting.SizeComplexScript(twips)); } } diff --git a/src/file/styles/styles.spec.ts b/src/file/styles/styles.spec.ts index 1c828337b2..6262b3cb9e 100644 --- a/src/file/styles/styles.spec.ts +++ b/src/file/styles/styles.spec.ts @@ -219,6 +219,20 @@ describe("ParagraphStyle", () => { }); }); + it("#character spacing", () => { + const style = new ParagraphStyle("myStyleId").characterSpacing(24); + const tree = new Formatter().format(style); + expect(tree).to.deep.equal({ + "w:style": [ + { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, + { "w:pPr": [] }, + { + "w:rPr": [{ "w:spacing": [{ _attr: { "w:val": 24 } }] }], + }, + ], + }); + }); + it("#left", () => { const style = new ParagraphStyle("myStyleId").left(); const tree = new Formatter().format(style); diff --git a/src/file/table-of-contents/alias.ts b/src/file/table-of-contents/alias.ts new file mode 100644 index 0000000000..d3a3235391 --- /dev/null +++ b/src/file/table-of-contents/alias.ts @@ -0,0 +1,12 @@ +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; + +class AliasAttributes extends XmlAttributeComponent<{ alias: string }> { + protected xmlKeys = { alias: "w:val" }; +} + +export class Alias extends XmlComponent { + constructor(alias: string) { + super("w:alias"); + this.root.push(new AliasAttributes({ alias })); + } +} diff --git a/src/file/table-of-contents/field-instruction.ts b/src/file/table-of-contents/field-instruction.ts new file mode 100644 index 0000000000..766e6fc2f2 --- /dev/null +++ b/src/file/table-of-contents/field-instruction.ts @@ -0,0 +1,76 @@ +// http://officeopenxml.com/WPfieldInstructions.php +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; +import { ITableOfContentsOptions } from "./table-of-contents-properties"; + +enum SpaceType { + DEFAULT = "default", + PRESERVE = "preserve", +} + +class TextAttributes extends XmlAttributeComponent<{ space: SpaceType }> { + protected xmlKeys = { space: "xml:space" }; +} + +export class FieldInstruction extends XmlComponent { + private readonly properties: ITableOfContentsOptions; + + constructor(properties: ITableOfContentsOptions = {}) { + super("w:instrText"); + + this.properties = properties; + + this.root.push(new TextAttributes({ space: SpaceType.PRESERVE })); + let instruction = "TOC"; + + if (this.properties.captionLabel) { + instruction = `${instruction} \\a "${this.properties.captionLabel}"`; + } + if (this.properties.entriesFromBookmark) { + instruction = `${instruction} \\b "${this.properties.entriesFromBookmark}"`; + } + if (this.properties.captionLabelIncludingNumbers) { + instruction = `${instruction} \\c "${this.properties.captionLabelIncludingNumbers}"`; + } + if (this.properties.sequenceAndPageNumbersSeparator) { + instruction = `${instruction} \\d "${this.properties.sequenceAndPageNumbersSeparator}"`; + } + if (this.properties.tcFieldIdentifier) { + instruction = `${instruction} \\f "${this.properties.tcFieldIdentifier}"`; + } + if (this.properties.hyperlink) { + instruction = `${instruction} \\h`; + } + if (this.properties.tcFieldLevelRange) { + instruction = `${instruction} \\l "${this.properties.tcFieldLevelRange}`; + } + if (this.properties.pageNumbersEntryLevelsRange) { + instruction = `${instruction} \\n "${this.properties.pageNumbersEntryLevelsRange}`; + } + if (this.properties.headingStyleRange) { + instruction = `${instruction} \\o "${this.properties.headingStyleRange}`; + } + if (this.properties.entryAndPageNumberSeparator) { + instruction = `${instruction} \\p "${this.properties.entryAndPageNumberSeparator}`; + } + if (this.properties.seqFieldIdentifierForPrefix) { + instruction = `${instruction} \\s "${this.properties.seqFieldIdentifierForPrefix}`; + } + if (this.properties.stylesWithLevels && this.properties.stylesWithLevels.length) { + const styles = this.properties.stylesWithLevels.map((sl) => `${sl.styleName},${sl.level}`).join(","); + instruction = `${instruction} \\t "${styles}"`; + } + if (this.properties.useAppliedParagraphOutlineLevel) { + instruction = `${instruction} \\u`; + } + if (this.properties.preserveTabInEntries) { + instruction = `${instruction} \\w`; + } + if (this.properties.preserveNewLineInEntries) { + instruction = `${instruction} \\x`; + } + if (this.properties.hideTabAndPageNumbersInWebView) { + instruction = `${instruction} \\z`; + } + this.root.push(instruction); + } +} diff --git a/src/file/table-of-contents/index.ts b/src/file/table-of-contents/index.ts new file mode 100644 index 0000000000..f36b16b738 --- /dev/null +++ b/src/file/table-of-contents/index.ts @@ -0,0 +1,2 @@ +export * from "./table-of-contents"; +export * from "./table-of-contents-properties"; diff --git a/src/file/table-of-contents/sdt-content.ts b/src/file/table-of-contents/sdt-content.ts new file mode 100644 index 0000000000..5228ff447f --- /dev/null +++ b/src/file/table-of-contents/sdt-content.ts @@ -0,0 +1,7 @@ +import { XmlComponent } from "file/xml-components"; + +export class StructuredDocumentTagContent extends XmlComponent { + constructor() { + super("w:sdtContent"); + } +} diff --git a/src/file/table-of-contents/sdt-properties.ts b/src/file/table-of-contents/sdt-properties.ts new file mode 100644 index 0000000000..3f7019f561 --- /dev/null +++ b/src/file/table-of-contents/sdt-properties.ts @@ -0,0 +1,10 @@ +// http://www.datypic.com/sc/ooxml/e-w_sdtPr-1.html +import { XmlComponent } from "file/xml-components"; +import { Alias } from "./alias"; + +export class StructuredDocumentTagProperties extends XmlComponent { + constructor(alias: string) { + super("w:sdtPr"); + this.root.push(new Alias(alias)); + } +} diff --git a/src/file/table-of-contents/table-of-contents-properties.ts b/src/file/table-of-contents/table-of-contents-properties.ts new file mode 100644 index 0000000000..bc312429af --- /dev/null +++ b/src/file/table-of-contents/table-of-contents-properties.ts @@ -0,0 +1,122 @@ +export class StyleLevel { + public styleName: string; + public level: number; + + constructor(styleName: string, level: number) { + this.styleName = styleName; + this.level = level; + } +} + +/** + * Options according to this docs: + * https://www.ecma-international.org/publications/standards/Ecma-376.htm + * Part 1 - Page 1251 + * + * Short Guide: + * http://officeopenxml.com/WPtableOfContents.php + */ +export interface ITableOfContentsOptions { + /** + * \a option - Includes captioned items, but omits caption labels and numbers. + * The identifier designated by text in this switch's field-argument corresponds to the caption label. + * Use captionLabelIncludingNumbers (\c) to build a table of captions with labels and numbers. + */ + captionLabel?: string; + + /** + * \b option - Includes entries only from the portion of the document marked by + * the bookmark named by text in this switch's field-argument. + */ + entriesFromBookmark?: string; + + /** + * \c option - Includes figures, tables, charts, and other items that are numbered + * by a SEQ field (§17.16.5.56). The sequence identifier designated by text in this switch's + * field-argument, which corresponds to the caption label, shall match the identifier in the + * corresponding SEQ field. + */ + captionLabelIncludingNumbers?: string; + + /** + * \d option - When used with \s, the text in this switch's field-argument defines + * the separator between sequence and page numbers. The default separator is a hyphen (-). + */ + sequenceAndPageNumbersSeparator?: string; + + /** + * \f option - Includes only those TC fields whose identifier exactly matches the + * text in this switch's field-argument (which is typically a letter). + */ + tcFieldIdentifier?: string; + + /** + * \h option - Makes the table of contents entries hyperlinks. + */ + hyperlink?: boolean; + + /** + * \l option - Includes TC fields that assign entries to one of the levels specified + * by text in this switch's field-argument as a range having the form startLevel-endLevel, + * where startLevel and endLevel are integers, and startLevel has a value equal-to or less-than endLevel. + * TC fields that assign entries to lower levels are skipped. + */ + tcFieldLevelRange?: string; + + /** + * \n option - Without field-argument, omits page numbers from the table of contents. + * Page numbers are omitted from all levels unless a range of entry levels is specified by + * text in this switch's field-argument. A range is specified as for \l. + */ + pageNumbersEntryLevelsRange?: string; + + /** + * \o option - Uses paragraphs formatted with all or the specified range of builtin + * heading styles. Headings in a style range are specified by text in this switch's + * field-argument using the notation specified as for \l, where each integer corresponds + * to the style with a style ID of HeadingX (e.g. 1 corresponds to Heading1). + * If no heading range is specified, all heading levels used in the document are listed. + */ + headingStyleRange?: string; + + /** + * \p option - Text in this switch's field-argument specifies a sequence of characters + * that separate an entry and its page number. The default is a tab with leader dots. + */ + entryAndPageNumberSeparator?: string; + + /** + * \s option - For entries numbered with a SEQ field (§17.16.5.56), adds a prefix to the page number. + * The prefix depends on the type of entry. text in this switch's field-argument shall match the + * identifier in the SEQ field. + */ + seqFieldIdentifierForPrefix?: string; + + /** + * \t field-argument Uses paragraphs formatted with styles other than the built-in heading styles. + * Text in this switch's field-argument specifies those styles as a set of comma-separated doublets, + * with each doublet being a comma-separated set of style name and table of content level. + * \t can be combined with \o. + */ + stylesWithLevels?: StyleLevel[]; + + /** + * \u Uses the applied paragraph outline level. + */ + useAppliedParagraphOutlineLevel?: boolean; + + /** + * \w Preserves tab entries within table entries. + */ + preserveTabInEntries?: boolean; + + /** + * \x Preserves newline characters within table entries. + */ + preserveNewLineInEntries?: boolean; + + /** + * \z Hides tab leader and page numbers in web page view (§17.18.102). + */ + hideTabAndPageNumbersInWebView?: boolean; +} diff --git a/src/file/table-of-contents/table-of-contents.spec.ts b/src/file/table-of-contents/table-of-contents.spec.ts new file mode 100644 index 0000000000..7d7d4a55b6 --- /dev/null +++ b/src/file/table-of-contents/table-of-contents.spec.ts @@ -0,0 +1,219 @@ +import { expect } from "chai"; + +import { Formatter } from "../../export/formatter"; +import { ITableOfContentsOptions, StyleLevel, TableOfContents } from "./"; + +describe("Table of Contents", () => { + describe("#constructor", () => { + it("should construct a TOC without options", () => { + const toc = new TableOfContents(); + const tree = new Formatter().format(toc); + expect(tree).to.be.deep.equal(DEFAULT_TOC); + }); + + it("should construct a TOC with all the options and alias", () => { + const props: ITableOfContentsOptions = {}; + + props.captionLabel = "A"; + props.entriesFromBookmark = "B"; + props.captionLabelIncludingNumbers = "C"; + props.sequenceAndPageNumbersSeparator = "D"; + props.tcFieldIdentifier = "F"; + props.hyperlink = true; + props.tcFieldLevelRange = "L"; + props.pageNumbersEntryLevelsRange = "N"; + props.headingStyleRange = "O"; + props.entryAndPageNumberSeparator = "P"; + props.seqFieldIdentifierForPrefix = "S"; + + const styles = new Array(); + styles.push(new StyleLevel("SL", 1)); + styles.push(new StyleLevel("SL", 2)); + props.stylesWithLevels = styles; + props.useAppliedParagraphOutlineLevel = true; + props.preserveTabInEntries = true; + props.preserveNewLineInEntries = true; + props.hideTabAndPageNumbersInWebView = true; + + const toc = new TableOfContents("Summary", props); + const tree = new Formatter().format(toc); + expect(tree).to.be.deep.equal(COMPLETE_TOC); + }); + }); +}); + +const DEFAULT_TOC = { + "w:sdt": [ + { + "w:sdtPr": [ + { + "w:alias": [ + { + _attr: { + "w:val": "Table of Contents", + }, + }, + ], + }, + ], + }, + { + "w:sdtContent": [ + { + "w:p": [ + { + "w:pPr": [], + }, + { + "w:r": [ + { + "w:rPr": [], + }, + { + "w:fldChar": [ + { + _attr: { + "w:fldCharType": "begin", + "w:dirty": true, + }, + }, + ], + }, + { + "w:instrText": [ + { + _attr: { + "xml:space": "preserve", + }, + }, + "TOC", + ], + }, + { + "w:fldChar": [ + { + _attr: { + "w:fldCharType": "separate", + }, + }, + ], + }, + ], + }, + ], + }, + { + "w:p": [ + { + "w:pPr": [], + }, + { + "w:r": [ + { + "w:rPr": [], + }, + { + "w:fldChar": [ + { + _attr: { + "w:fldCharType": "end", + }, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], +}; + +const COMPLETE_TOC = { + "w:sdt": [ + { + "w:sdtPr": [ + { + "w:alias": [ + { + _attr: { + "w:val": "Summary", + }, + }, + ], + }, + ], + }, + { + "w:sdtContent": [ + { + "w:p": [ + { + "w:pPr": [], + }, + { + "w:r": [ + { + "w:rPr": [], + }, + { + "w:fldChar": [ + { + _attr: { + "w:fldCharType": "begin", + "w:dirty": true, + }, + }, + ], + }, + { + "w:instrText": [ + { + _attr: { + "xml:space": "preserve", + }, + }, + 'TOC \\a "A" \\b "B" \\c "C" \\d "D" \\f "F" \\h \\l "L \\n "N \\o "O \\p "P \\s "S \\t "SL,1,SL,2" \\u \\w \\x \\z', + ], + }, + { + "w:fldChar": [ + { + _attr: { + "w:fldCharType": "separate", + }, + }, + ], + }, + ], + }, + ], + }, + { + "w:p": [ + { + "w:pPr": [], + }, + { + "w:r": [ + { + "w:rPr": [], + }, + { + "w:fldChar": [ + { + _attr: { + "w:fldCharType": "end", + }, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], +}; diff --git a/src/file/table-of-contents/table-of-contents.ts b/src/file/table-of-contents/table-of-contents.ts new file mode 100644 index 0000000000..7b1ff0f66d --- /dev/null +++ b/src/file/table-of-contents/table-of-contents.ts @@ -0,0 +1,35 @@ +// http://officeopenxml.com/WPtableOfContents.php +// http://www.datypic.com/sc/ooxml/e-w_sdt-1.html +import { Paragraph } 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 { FieldInstruction } from "./field-instruction"; +import { StructuredDocumentTagContent } from "./sdt-content"; +import { StructuredDocumentTagProperties } from "./sdt-properties"; +import { ITableOfContentsOptions } from "./table-of-contents-properties"; + +export class TableOfContents extends XmlComponent { + constructor(alias: string = "Table of Contents", properties?: ITableOfContentsOptions) { + super("w:sdt"); + this.root.push(new StructuredDocumentTagProperties(alias)); + + const content = new StructuredDocumentTagContent(); + + const beginParagraph = new Paragraph(); + const beginRun = new Run(); + beginRun.addChildElement(new Begin(true)); + beginRun.addChildElement(new FieldInstruction(properties)); + beginRun.addChildElement(new Separate()); + beginParagraph.addRun(beginRun); + content.addChildElement(beginParagraph); + + const endParagraph = new Paragraph(); + const endRun = new Run(); + endRun.addChildElement(new End()); + endParagraph.addRun(endRun); + content.addChildElement(endParagraph); + + this.root.push(content); + } +} diff --git a/src/file/table/properties.ts b/src/file/table/properties.ts index 7c5b0bad76..91ef393df3 100644 --- a/src/file/table/properties.ts +++ b/src/file/table/properties.ts @@ -3,13 +3,13 @@ import { WidthType } from "./table-cell"; import { TableCellMargin } from "./table-cell-margin"; export class TableProperties extends XmlComponent { - private readonly cellMargain: TableCellMargin; + private readonly cellMargin: TableCellMargin; constructor() { super("w:tblPr"); - this.cellMargain = new TableCellMargin(); - this.root.push(this.cellMargain); + this.cellMargin = new TableCellMargin(); + this.root.push(this.cellMargin); } public setWidth(type: WidthType, w: number | string): TableProperties { @@ -28,7 +28,7 @@ export class TableProperties extends XmlComponent { } public get CellMargin(): TableCellMargin { - return this.cellMargain; + return this.cellMargin; } } diff --git a/src/file/xml-components/xml-component.ts b/src/file/xml-components/xml-component.ts index 0ed604e9ee..fae843db29 100644 --- a/src/file/xml-components/xml-component.ts +++ b/src/file/xml-components/xml-component.ts @@ -30,7 +30,6 @@ export abstract class XmlComponent extends BaseXmlComponent { }; } - // TODO: Unused method public addChildElement(child: XmlComponent | string): XmlComponent { this.root.push(child);