diff --git a/demo/2-declaritive-styles.ts b/demo/2-declaritive-styles.ts index 34d7e19e70..472b916091 100644 --- a/demo/2-declaritive-styles.ts +++ b/demo/2-declaritive-styles.ts @@ -1,7 +1,7 @@ // Example on how to customise the look at feel using Styles // Import from 'docx' rather than '../build' if you install from npm import * as fs from "fs"; -import { Document, HeadingLevel, Packer, Paragraph, TextRun, UnderlineType } from "../build"; +import { AlignmentType, Document, HeadingLevel, Packer, Paragraph, TextRun, UnderlineType } from "../build"; const doc = new Document({ creator: "Clippy", @@ -83,15 +83,23 @@ const doc = new Document({ }, ], }, + numbering: { + config: [ + { + reference: "my-crazy-numbering", + levels: [ + { + level: 0, + format: "lowerLetter", + text: "%1)", + alignment: AlignmentType.LEFT, + }, + ], + }, + ], + }, }); -const numberedAbstract = doc.Numbering.createAbstractNumbering(); -numberedAbstract.createLevel(0, "lowerLetter", "%1)", "left"); - -const letterNumbering = doc.Numbering.createConcreteNumbering(numberedAbstract); -const letterNumbering5 = doc.Numbering.createConcreteNumbering(numberedAbstract); -letterNumbering5.overrideLevel(0, 5); - doc.addSection({ children: [ new Paragraph({ @@ -106,21 +114,21 @@ doc.addSection({ new Paragraph({ text: "Option1", numbering: { - num: letterNumbering, + reference: "my-crazy-numbering", level: 0, }, }), new Paragraph({ text: "Option5 -- override 2 to 5", numbering: { - num: letterNumbering, + reference: "my-crazy-numbering", level: 0, }, }), new Paragraph({ text: "Option3", numbering: { - num: letterNumbering, + reference: "my-crazy-numbering", level: 0, }, }), diff --git a/demo/29-numbered-lists.ts b/demo/29-numbered-lists.ts index 740320e5f8..18da5cc359 100644 --- a/demo/29-numbered-lists.ts +++ b/demo/29-numbered-lists.ts @@ -1,23 +1,37 @@ // Numbered lists // Import from 'docx' rather than '../build' if you install from npm import * as fs from "fs"; -import { Document, Numbering, Packer, Paragraph } from "../build"; +import { AlignmentType, Document, Packer, Paragraph } from "../build"; -const doc = new Document(); - -const numbering = new Numbering(); - -const abstractNum = numbering.createAbstractNumbering(); -abstractNum.createLevel(0, "upperRoman", "%1", "start").indent({ left: 720, hanging: 260 }); - -const concrete = numbering.createConcreteNumbering(abstractNum); +const doc = new Document({ + numbering: { + config: [ + { + levels: [ + { + level: 0, + format: "upperRoman", + text: "%1", + alignment: AlignmentType.START, + style: { + paragraph: { + indent: { left: 720, hanging: 260 }, + }, + }, + }, + ], + reference: "my-crazy-reference", + }, + ], + }, +}); doc.addSection({ children: [ new Paragraph({ text: "line with contextual spacing", numbering: { - num: concrete, + reference: "my-crazy-reference", level: 0, }, contextualSpacing: true, @@ -28,7 +42,7 @@ doc.addSection({ new Paragraph({ text: "line with contextual spacing", numbering: { - num: concrete, + reference: "my-crazy-reference", level: 0, }, contextualSpacing: true, @@ -39,7 +53,7 @@ doc.addSection({ new Paragraph({ text: "line without contextual spacing", numbering: { - num: concrete, + reference: "my-crazy-reference", level: 0, }, contextualSpacing: false, @@ -50,7 +64,7 @@ doc.addSection({ new Paragraph({ text: "line without contextual spacing", numbering: { - num: concrete, + reference: "my-crazy-reference", level: 0, }, contextualSpacing: false, diff --git a/demo/3-numbering-and-bullet-points.ts b/demo/3-numbering-and-bullet-points.ts index e87e6616d2..72b33d2e8e 100644 --- a/demo/3-numbering-and-bullet-points.ts +++ b/demo/3-numbering-and-bullet-points.ts @@ -1,46 +1,80 @@ // Numbering and bullet points example // Import from 'docx' rather than '../build' if you install from npm import * as fs from "fs"; -import { Document, Numbering, Packer, Paragraph } from "../build"; +import { AlignmentType, Document, Packer, Paragraph } from "../build"; -const doc = new Document(); - -const numbering = new Numbering(); - -const abstractNum = numbering.createAbstractNumbering(); -abstractNum.createLevel(0, "upperRoman", "%1", "start").indent({ left: 720, hanging: 260 }); -abstractNum.createLevel(1, "decimal", "%2.", "start").indent({ left: 1440, hanging: 980 }); -abstractNum.createLevel(2, "lowerLetter", "%3)", "start").indent({ left: 2160, hanging: 1700 }); - -const concrete = numbering.createConcreteNumbering(abstractNum); +const doc = new Document({ + numbering: { + config: [ + { + reference: "my-crazy-numbering", + levels: [ + { + level: 0, + format: "upperRoman", + text: "%1", + alignment: AlignmentType.START, + style: { + paragraph: { + indent: { left: 720, hanging: 260 }, + }, + }, + }, + { + level: 1, + format: "decimal", + text: "%2.", + alignment: AlignmentType.START, + style: { + paragraph: { + indent: { left: 1440, hanging: 980 }, + }, + }, + }, + { + level: 2, + format: "lowerLetter", + text: "%3)", + alignment: AlignmentType.START, + style: { + paragraph: { + indent: { left: 2160, hanging: 1700 }, + }, + }, + }, + ], + }, + ], + }, +}); doc.addSection({ children: [ new Paragraph({ text: "Hey you", numbering: { - num: concrete, + reference: "my-crazy-numbering", level: 0, }, }), new Paragraph({ text: "What's up fam", numbering: { - num: concrete, + reference: "my-crazy-numbering", level: 1, }, }), new Paragraph({ text: "Hello World 2", numbering: { - num: concrete, + reference: "my-crazy-numbering", level: 1, }, }), new Paragraph({ text: "Yeah boi", numbering: { - num: concrete, + reference: "my-crazy-numbering", level: 2, }, }), diff --git a/docs/usage/paragraph.md b/docs/usage/paragraph.md index f681a09148..f6da3ec963 100644 --- a/docs/usage/paragraph.md +++ b/docs/usage/paragraph.md @@ -20,11 +20,7 @@ This method is useful for adding different [text](text.md) with different styles ```ts const paragraph = new Paragraph({ - children: [ - new TextRun("Lorem Ipsum Foo Bar"), - new TextRun("Hello World"), - new SymbolRun("F071"), - ], + children: [new TextRun("Lorem Ipsum Foo Bar"), new TextRun("Hello World"), new SymbolRun("F071")], }); ``` @@ -60,27 +56,27 @@ doc.addSection({ This is the list of options for a paragraph. A detailed explanation is below: -| Property | Type | Mandatory? | Possible Values | -| ----------------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------- | -| [text](#text) | `string` | Optional | | -| [heading](#heading) | `HeadingLevel` | Optional | `HEADING_1`, `HEADING_2`, `HEADING_3`, `HEADING_4`, `HEADING_5`, `HEADING_6`, `TITLE` | -| [border](#border) | `IBorderOptions` | Optional | `top`, `bottom`, `left`, `right`. Each of these are of type IBorderPropertyOptions. Click here for Example | -| [spacing](#spacing) | `ISpacingProperties` | Optional | See below for ISpacingProperties | +| Property | Type | Mandatory? | Possible Values | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------- | +| [text](#text) | `string` | Optional | | +| [heading](#heading) | `HeadingLevel` | Optional | `HEADING_1`, `HEADING_2`, `HEADING_3`, `HEADING_4`, `HEADING_5`, `HEADING_6`, `TITLE` | +| [border](#border) | `IBorderOptions` | Optional | `top`, `bottom`, `left`, `right`. Each of these are of type IBorderPropertyOptions. Click here for Example | +| [spacing](#spacing) | `ISpacingProperties` | Optional | See below for ISpacingProperties | | [outlineLevel](#outline-level) | `number` | Optional | | -| alignment | `AlignmentType` | Optional | | -| heading | `HeadingLevel` | Optional | | -| bidirectional | `boolean` | Optional | | -| thematicBreak | `boolean` | Optional | | -| pageBreakBefore | `boolean` | Optional | | -| contextualSpacing | `boolean` | Optional | | -| indent | `IIndentAttributesProperties` | Optional | | -| keepLines | `boolean` | Optional | | -| keepNext | `boolean` | Optional | | -| children | `(TextRun or PictureRun or Hyperlink)[]` | Optional | | -| style | `string` | Optional | | -| tabStop | `{ left?: ITabStopOptions; right?: ITabStopOptions; maxRight?: { leader: LeaderType; }; center?: ITabStopOptions }` | Optional | | -| bullet | `{ level: number }` | Optional | | -| numbering | `{ num: Num; level: number; custom?: boolean }` | Optional | | +| alignment | `AlignmentType` | Optional | | +| heading | `HeadingLevel` | Optional | | +| bidirectional | `boolean` | Optional | | +| thematicBreak | `boolean` | Optional | | +| pageBreakBefore | `boolean` | Optional | | +| contextualSpacing | `boolean` | Optional | | +| indent | `IIndentAttributesProperties` | Optional | | +| keepLines | `boolean` | Optional | | +| keepNext | `boolean` | Optional | | +| children | `(TextRun or PictureRun or Hyperlink)[]` | Optional | | +| style | `string` | Optional | | +| tabStop | `{ left?: ITabStopOptions; right?: ITabStopOptions; maxRight?: { leader: LeaderType; }; center?: ITabStopOptions }` | Optional | | +| bullet | `{ level: number }` | Optional | | +| numbering | `{ num: ConcreteNumbering; level: number; custom?: boolean }` | Optional | | ## Text @@ -252,10 +248,7 @@ To move to a new page (insert a page break): ```ts const paragraph = new docx.Paragraph({ - children: [ - new TextRun("Amazing Heading"), - new PageBreak(), - ] + children: [new TextRun("Amazing Heading"), new PageBreak()], }); ``` diff --git a/src/export/packer/next-compiler.ts b/src/export/packer/next-compiler.ts index 6a3e752098..bff24a8ef7 100644 --- a/src/export/packer/next-compiler.ts +++ b/src/export/packer/next-compiler.ts @@ -4,6 +4,7 @@ import * as xml from "xml"; import { File } from "file"; import { Formatter } from "../formatter"; import { ImageReplacer } from "./image-replacer"; +import { NumberingReplacer } from "./numbering-replacer"; interface IXmlifyedFile { readonly data: string; @@ -30,10 +31,12 @@ interface IXmlifyedFileMapping { export class Compiler { private readonly formatter: Formatter; private readonly imageReplacer: ImageReplacer; + private readonly numberingReplacer: NumberingReplacer; constructor() { this.formatter = new Formatter(); this.imageReplacer = new ImageReplacer(); + this.numberingReplacer = new NumberingReplacer(); } public compile(file: File, prettifyXml?: boolean): JSZip { @@ -89,8 +92,9 @@ export class Compiler { Document: { data: (() => { const xmlData = this.imageReplacer.replace(documentXmlData, documentMediaDatas, documentRelationshipCount); + const referenedXmlData = this.numberingReplacer.replace(xmlData, file.Numbering.ConcreteNumbering); - return xmlData; + return referenedXmlData; })(), path: "word/document.xml", }, diff --git a/src/export/packer/numbering-replacer.ts b/src/export/packer/numbering-replacer.ts new file mode 100644 index 0000000000..4ac5dcbf1d --- /dev/null +++ b/src/export/packer/numbering-replacer.ts @@ -0,0 +1,17 @@ +import { ConcreteNumbering } from "file"; + +export class NumberingReplacer { + public replace(xmlData: string, concreteNumberings: ConcreteNumbering[]): string { + let currentXmlData = xmlData; + + for (const concreteNumbering of concreteNumberings) { + if (!concreteNumbering.reference) { + continue; + } + + currentXmlData = currentXmlData.replace(new RegExp(`{${concreteNumbering.reference}}`, "g"), concreteNumbering.id.toString()); + } + + return currentXmlData; + } +} diff --git a/src/file/core-properties/properties.ts b/src/file/core-properties/properties.ts index a99c74d98f..8ac2130310 100644 --- a/src/file/core-properties/properties.ts +++ b/src/file/core-properties/properties.ts @@ -1,6 +1,7 @@ import { XmlComponent } from "file/xml-components"; import { DocumentAttributes } from "../document/document-attributes"; +import { INumberingOptions } from "../numbering"; import { IStylesOptions } from "../styles"; import { Created, Creator, Description, Keywords, LastModifiedBy, Modified, Revision, Subject, Title } from "./components"; @@ -14,6 +15,7 @@ export interface IPropertiesOptions { readonly revision?: string; readonly externalStyles?: string; readonly styles?: IStylesOptions; + readonly numbering?: INumberingOptions; } export class CoreProperties extends XmlComponent { diff --git a/src/file/file.ts b/src/file/file.ts index ccf5fe374c..6d46fdb338 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -71,7 +71,13 @@ export class File { sections: ISectionOptions[] = [], ) { this.coreProperties = new CoreProperties(options); - this.numbering = new Numbering(); + this.numbering = new Numbering( + options.numbering + ? options.numbering + : { + config: [], + }, + ); this.docRelationships = new Relationships(); this.fileRelationships = new Relationships(); this.appProperties = new AppProperties(); diff --git a/src/file/numbering/abstract-numbering.spec.ts b/src/file/numbering/abstract-numbering.spec.ts new file mode 100644 index 0000000000..59655aa40e --- /dev/null +++ b/src/file/numbering/abstract-numbering.spec.ts @@ -0,0 +1,605 @@ +import { expect } from "chai"; + +import { Formatter } from "export/formatter"; +import { EMPTY_OBJECT } from "file/xml-components"; + +import { AlignmentType, TabStopPosition } from "../paragraph"; +import { UnderlineType } from "../paragraph/run/underline"; +import { ShadingType } from "../table"; +import { AbstractNumbering } from "./abstract-numbering"; + +describe("AbstractNumbering", () => { + it("stores its ID at its .id property", () => { + const abstractNumbering = new AbstractNumbering(5, []); + expect(abstractNumbering.id).to.equal(5); + }); + + describe("#createLevel", () => { + it("creates a level with the given characteristics", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 3, + format: "lowerLetter", + text: "%1)", + alignment: AlignmentType.END, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ _attr: { "w:ilvl": 3, "w15:tentative": 1 } }); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:start": { _attr: { "w:val": 1 } } }); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:lvlJc": { _attr: { "w:val": "end" } } }); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:numFmt": { _attr: { "w:val": "lowerLetter" } } }); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:lvlText": { _attr: { "w:val": "%1)" } } }); + }); + + it("uses 'start' as the default alignment", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 3, + format: "lowerLetter", + text: "%1)", + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ _attr: { "w:ilvl": 3, "w15:tentative": 1 } }); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:start": { _attr: { "w:val": 1 } } }); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:lvlJc": { _attr: { "w:val": "start" } } }); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:numFmt": { _attr: { "w:val": "lowerLetter" } } }); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ "w:lvlText": { _attr: { "w:val": "%1)" } } }); + }); + + describe("formatting methods: paragraph properties", () => { + it("#indent", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + paragraph: { + indent: { left: 720 }, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:pPr": [{ "w:ind": { _attr: { "w:left": 720 } } }], + }); + }); + + it("#spacing", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + paragraph: { + spacing: { before: 50, after: 150 }, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:pPr": [{ "w:spacing": { _attr: { "w:before": 50, "w:after": 150 } } }], + }); + }); + + it("#center", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + paragraph: { + alignment: AlignmentType.CENTER, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:pPr": [{ "w:jc": { _attr: { "w:val": "center" } } }], + }); + }); + + it("#left", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + paragraph: { + alignment: AlignmentType.LEFT, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:pPr": [{ "w:jc": { _attr: { "w:val": "left" } } }], + }); + }); + + it("#right", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + paragraph: { + alignment: AlignmentType.RIGHT, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:pPr": [{ "w:jc": { _attr: { "w:val": "right" } } }], + }); + }); + + it("#justified", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + paragraph: { + alignment: AlignmentType.JUSTIFIED, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:pPr": [{ "w:jc": { _attr: { "w:val": "both" } } }], + }); + }); + + it("#thematicBreak", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + paragraph: { + thematicBreak: true, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:pPr": [ + { + "w:pBdr": [ + { + "w:bottom": { + _attr: { + "w:color": "auto", + "w:space": 1, + "w:val": "single", + "w:sz": 6, + }, + }, + }, + ], + }, + ], + }); + }); + + it("#leftTabStop", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + paragraph: { + leftTabStop: 1200, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:pPr": [ + { + "w:tabs": [{ "w:tab": { _attr: { "w:val": "left", "w:pos": 1200 } } }], + }, + ], + }); + }); + + it("#maxRightTabStop", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + paragraph: { + rightTabStop: TabStopPosition.MAX, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:pPr": [ + { + "w:tabs": [{ "w:tab": { _attr: { "w:val": "right", "w:pos": 9026 } } }], + }, + ], + }); + }); + + it("#keepLines", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + paragraph: { + keepLines: true, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:pPr": [{ "w:keepLines": EMPTY_OBJECT }], + }); + }); + + it("#keepNext", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + paragraph: { + keepNext: true, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:pPr": [{ "w:keepNext": EMPTY_OBJECT }], + }); + }); + }); + + describe("formatting methods: run properties", () => { + it("#size", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + run: { + size: 24, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:rPr": [{ "w:sz": { _attr: { "w:val": 24 } } }], + }); + }); + + it("#smallCaps", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + run: { + smallCaps: true, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:rPr": [{ "w:smallCaps": { _attr: { "w:val": true } } }], + }); + }); + + it("#allCaps", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + run: { + allCaps: true, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:rPr": [{ "w:caps": { _attr: { "w:val": true } } }], + }); + }); + + it("#strike", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + run: { + strike: true, + }, + }, + }, + ]); + + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:rPr": [{ "w:strike": { _attr: { "w:val": true } } }], + }); + }); + + it("#doubleStrike", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + run: { + doubleStrike: true, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:rPr": [{ "w:dstrike": { _attr: { "w:val": true } } }], + }); + }); + + it("#subScript", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + run: { + subScript: true, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:rPr": [{ "w:vertAlign": { _attr: { "w:val": "subscript" } } }], + }); + }); + + it("#superScript", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + run: { + superScript: true, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:rPr": [{ "w:vertAlign": { _attr: { "w:val": "superscript" } } }], + }); + }); + + it("#font", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + run: { + font: "Times", + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:rPr": [ + { "w:rFonts": { _attr: { "w:ascii": "Times", "w:cs": "Times", "w:eastAsia": "Times", "w:hAnsi": "Times" } } }, + ], + }); + }); + + it("#bold", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + run: { + bold: true, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:rPr": [{ "w:b": { _attr: { "w:val": true } } }], + }); + }); + + it("#italics", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + run: { + italics: true, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:rPr": [{ "w:i": { _attr: { "w:val": true } } }], + }); + }); + + it("#highlight", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + run: { + highlight: "005599", + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:rPr": [{ "w:highlight": { _attr: { "w:val": "005599" } } }], + }); + }); + + it("#shadow", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + run: { + shadow: { + type: ShadingType.PERCENT_10, + fill: "00FFFF", + color: "FF0000", + }, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:rPr": [{ "w:shd": { _attr: { "w:val": "pct10", "w:fill": "00FFFF", "w:color": "FF0000" } } }], + }); + }); + + describe("#underline", () => { + it("should set underline to 'single' if no arguments are given", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + run: { + underline: {}, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:rPr": [{ "w:u": { _attr: { "w:val": "single" } } }], + }); + }); + + it("should set the style if given", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + run: { + underline: { + type: UnderlineType.DOUBLE, + }, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:rPr": [{ "w:u": { _attr: { "w:val": "double" } } }], + }); + }); + + it("should set the style and color if given", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + run: { + underline: { + type: UnderlineType.DOUBLE, + color: "005599", + }, + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:rPr": [{ "w:u": { _attr: { "w:val": "double", "w:color": "005599" } } }], + }); + }); + }); + + it("#color", () => { + const abstractNumbering = new AbstractNumbering(1, [ + { + level: 0, + format: "lowerRoman", + text: "%0.", + style: { + run: { + color: "123456", + }, + }, + }, + ]); + const tree = new Formatter().format(abstractNumbering); + expect(tree["w:abstractNum"][2]["w:lvl"]).to.include({ + "w:rPr": [{ "w:color": { _attr: { "w:val": "123456" } } }], + }); + }); + }); + }); +}); diff --git a/src/file/numbering/abstract-numbering.ts b/src/file/numbering/abstract-numbering.ts index e42e2e4fc8..2eed73fe39 100644 --- a/src/file/numbering/abstract-numbering.ts +++ b/src/file/numbering/abstract-numbering.ts @@ -1,5 +1,6 @@ import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; -import { Level } from "./level"; + +import { ILevelsOptions, Level } from "./level"; import { MultiLevelType } from "./multi-level-type"; interface IAbstractNumberingAttributesProperties { @@ -17,7 +18,7 @@ class AbstractNumberingAttributes extends XmlAttributeComponent { + describe("#overrideLevel", () => { + let concreteNumbering: ConcreteNumbering; + beforeEach(() => { + concreteNumbering = new ConcreteNumbering(0, 1); + }); + + it("sets a new override level for the given level number", () => { + concreteNumbering.overrideLevel(3); + const tree = new Formatter().format(concreteNumbering); + expect(tree["w:num"]).to.include({ + "w:lvlOverride": [ + { _attr: { "w:ilvl": 3 } }, + { + "w:lvl": [ + { _attr: { "w:ilvl": 3, "w15:tentative": 1 } }, + { "w:start": { _attr: { "w:val": 1 } } }, + { "w:lvlJc": { _attr: { "w:val": "start" } } }, + ], + }, + ], + }); + }); + + it("sets the startOverride element if start is given", () => { + concreteNumbering.overrideLevel(1, 9); + const tree = new Formatter().format(concreteNumbering); + expect(tree["w:num"]).to.include({ + "w:lvlOverride": [ + { + _attr: { + "w:ilvl": 1, + }, + }, + { + "w:startOverride": { + _attr: { + "w:val": 9, + }, + }, + }, + { + "w:lvl": [ + { _attr: { "w:ilvl": 1, "w15:tentative": 1 } }, + { "w:start": { _attr: { "w:val": 1 } } }, + { "w:lvlJc": { _attr: { "w:val": "start" } } }, + ], + }, + ], + }); + }); + + it("sets the lvl element if overrideLevel.Level is accessed", () => { + const ol = concreteNumbering.overrideLevel(1); + expect(ol.Level).to.be.instanceof(LevelForOverride); + const tree = new Formatter().format(concreteNumbering); + + expect(tree["w:num"]).to.include({ + "w:lvlOverride": [ + { _attr: { "w:ilvl": 1 } }, + { + "w:lvl": [ + { _attr: { "w:ilvl": 1, "w15:tentative": 1 } }, + { "w:start": { _attr: { "w:val": 1 } } }, + { "w:lvlJc": { _attr: { "w:val": "start" } } }, + ], + }, + ], + }); + }); + }); +}); diff --git a/src/file/numbering/level.ts b/src/file/numbering/level.ts index bdc65fbba4..cbef58dfc0 100644 --- a/src/file/numbering/level.ts +++ b/src/file/numbering/level.ts @@ -2,9 +2,7 @@ import { Attributes, XmlAttributeComponent, XmlComponent } from "file/xml-compon import { Alignment, AlignmentType, - IIndentAttributesProperties, Indent, - ISpacingProperties, KeepLines, KeepNext, Spacing, @@ -15,7 +13,7 @@ import { import { ParagraphProperties } from "../paragraph/properties"; import * as formatting from "../paragraph/run/formatting"; import { RunProperties } from "../paragraph/run/properties"; -import { UnderlineType } from "../paragraph/run/underline"; +import { IParagraphStyleOptions2, IRunStyleOptions } from "../styles/style-options"; interface ILevelAttributesProperties { readonly ilvl?: number; @@ -63,7 +61,7 @@ class LevelText extends XmlComponent { } class LevelJc extends XmlComponent { - constructor(value: string) { + constructor(value: AlignmentType) { super("w:lvlJc"); this.root.push( new Attributes({ @@ -79,6 +77,19 @@ export enum LevelSuffix { TAB = "tab", } +export interface ILevelsOptions { + readonly level: number; + readonly format?: string; + readonly text?: string; + readonly alignment?: AlignmentType; + readonly start?: number; + readonly suffix?: LevelSuffix; + readonly style?: { + readonly run?: IRunStyleOptions; + readonly paragraph?: IParagraphStyleOptions2; + }; +} + class Suffix extends XmlComponent { constructor(value: LevelSuffix) { super("w:suff"); @@ -94,7 +105,7 @@ export class LevelBase extends XmlComponent { private readonly paragraphProperties: ParagraphProperties; private readonly runProperties: RunProperties; - constructor(level: number, start?: number, numberFormat?: string, levelText?: string, lvlJc?: string) { + constructor({ level, format, text, alignment = AlignmentType.START, start = 1, style, suffix }: ILevelsOptions) { super("w:lvl"); this.root.push( new LevelAttributes({ @@ -103,17 +114,15 @@ export class LevelBase extends XmlComponent { }), ); - if (start !== undefined) { - this.root.push(new Start(start)); + this.root.push(new Start(start)); + this.root.push(new LevelJc(alignment)); + + if (format) { + this.root.push(new NumberFormat(format)); } - if (numberFormat !== undefined) { - this.root.push(new NumberFormat(numberFormat)); - } - if (levelText !== undefined) { - this.root.push(new LevelText(levelText)); - } - if (lvlJc !== undefined) { - this.root.push(new LevelJc(lvlJc)); + + if (text) { + this.root.push(new LevelText(text)); } this.paragraphProperties = new ParagraphProperties({}); @@ -121,156 +130,112 @@ export class LevelBase extends XmlComponent { this.root.push(this.paragraphProperties); this.root.push(this.runProperties); - } - public setSuffix(value: LevelSuffix): LevelBase { - this.root.push(new Suffix(value)); - return this; - } + if (suffix) { + this.root.push(new Suffix(suffix)); + } - public addParagraphProperty(property: XmlComponent): Level { - this.paragraphProperties.push(property); - return this; - } + if (style) { + if (style.run) { + if (style.run.size) { + this.runProperties.push(new formatting.Size(style.run.size)); + } - public addRunProperty(property: XmlComponent): Level { - this.runProperties.push(property); - return this; - } + if (style.run.bold) { + this.runProperties.push(new formatting.Bold()); + } - // ---------- Run formatting ---------------------- // + if (style.run.italics) { + this.runProperties.push(new formatting.Italics()); + } - public size(twips: number): Level { - this.addRunProperty(new formatting.Size(twips)); - return this; - } + if (style.run.smallCaps) { + this.runProperties.push(new formatting.SmallCaps()); + } - public bold(): Level { - this.addRunProperty(new formatting.Bold()); - return this; - } + if (style.run.allCaps) { + this.runProperties.push(new formatting.Caps()); + } - public italics(): Level { - this.addRunProperty(new formatting.Italics()); - return this; - } + if (style.run.strike) { + this.runProperties.push(new formatting.Strike()); + } - public smallCaps(): Level { - this.addRunProperty(new formatting.SmallCaps()); - return this; - } + if (style.run.doubleStrike) { + this.runProperties.push(new formatting.DoubleStrike()); + } - public allCaps(): Level { - this.addRunProperty(new formatting.Caps()); - return this; - } + if (style.run.subScript) { + this.runProperties.push(new formatting.SubScript()); + } - public strike(): Level { - this.addRunProperty(new formatting.Strike()); - return this; - } + if (style.run.superScript) { + this.runProperties.push(new formatting.SuperScript()); + } - public doubleStrike(): Level { - this.addRunProperty(new formatting.DoubleStrike()); - return this; - } + if (style.run.underline) { + this.runProperties.push(new formatting.Underline(style.run.underline.type, style.run.underline.color)); + } - public subScript(): Level { - this.addRunProperty(new formatting.SubScript()); - return this; - } + if (style.run.color) { + this.runProperties.push(new formatting.Color(style.run.color)); + } - public superScript(): Level { - this.addRunProperty(new formatting.SuperScript()); - return this; - } + if (style.run.font) { + this.runProperties.push(new formatting.RunFonts(style.run.font)); + } - public underline(underlineType?: UnderlineType, color?: string): Level { - this.addRunProperty(new formatting.Underline(underlineType, color)); - return this; - } + if (style.run.highlight) { + this.runProperties.push(new formatting.Highlight(style.run.highlight)); + } - public color(color: string): Level { - this.addRunProperty(new formatting.Color(color)); - return this; - } + if (style.run.shadow) { + this.runProperties.push(new formatting.Shading(style.run.shadow.type, style.run.shadow.fill, style.run.shadow.color)); + } + } - public font(fontName: string): Level { - this.addRunProperty(new formatting.RunFonts(fontName)); - return this; - } + if (style.paragraph) { + if (style.paragraph.alignment) { + this.paragraphProperties.push(new Alignment(style.paragraph.alignment)); + } - public highlight(color: string): Level { - this.addRunProperty(new formatting.Highlight(color)); - return this; - } + if (style.paragraph.thematicBreak) { + this.paragraphProperties.push(new ThematicBreak()); + } - public shadow(value: string, fill: string, color: string): Level { - this.addRunProperty(new formatting.Shading(value, fill, color)); - return this; - } - // --------------------- Paragraph formatting ------------------------ // + if (style.paragraph.rightTabStop) { + this.paragraphProperties.push(new TabStop(TabStopType.RIGHT, style.paragraph.rightTabStop)); + } - public center(): Level { - this.addParagraphProperty(new Alignment(AlignmentType.CENTER)); - return this; - } + if (style.paragraph.leftTabStop) { + this.paragraphProperties.push(new TabStop(TabStopType.LEFT, style.paragraph.leftTabStop)); + } - public left(): Level { - this.addParagraphProperty(new Alignment(AlignmentType.LEFT)); - return this; - } + if (style.paragraph.indent) { + this.paragraphProperties.push(new Indent(style.paragraph.indent)); + } - public right(): Level { - this.addParagraphProperty(new Alignment(AlignmentType.RIGHT)); - return this; - } + if (style.paragraph.spacing) { + this.paragraphProperties.push(new Spacing(style.paragraph.spacing)); + } - public justified(): Level { - this.addParagraphProperty(new Alignment(AlignmentType.BOTH)); - return this; - } + if (style.paragraph.keepNext) { + this.paragraphProperties.push(new KeepNext()); + } - public thematicBreak(): Level { - this.addParagraphProperty(new ThematicBreak()); - return this; - } - - public rightTabStop(position: number): Level { - return this.addParagraphProperty(new TabStop(TabStopType.RIGHT, position)); - } - - public leftTabStop(position: number): Level { - this.addParagraphProperty(new TabStop(TabStopType.LEFT, position)); - return this; - } - - public indent(attrs: IIndentAttributesProperties): Level { - this.addParagraphProperty(new Indent(attrs)); - return this; - } - - public spacing(params: ISpacingProperties): Level { - this.addParagraphProperty(new Spacing(params)); - return this; - } - - public keepNext(): Level { - this.addParagraphProperty(new KeepNext()); - return this; - } - - public keepLines(): Level { - this.addParagraphProperty(new KeepLines()); - return this; + if (style.paragraph.keepLines) { + this.paragraphProperties.push(new KeepLines()); + } + } + } } } export class Level extends LevelBase { // This is the level that sits under abstractNum. We make a // handful of properties required - constructor(level: number, numberFormat: string, levelText: string, lvlJc: string) { - super(level, 1, numberFormat, levelText, lvlJc); + constructor(options: ILevelsOptions) { + super(options); } } diff --git a/src/file/numbering/num.ts b/src/file/numbering/num.ts index bf552593aa..e459b03d11 100644 --- a/src/file/numbering/num.ts +++ b/src/file/numbering/num.ts @@ -20,10 +20,10 @@ class NumAttributes extends XmlAttributeComponent { protected readonly xmlKeys = { numId: "w:numId" }; } -export class Num extends XmlComponent { +export class ConcreteNumbering extends XmlComponent { public readonly id: number; - constructor(numId: number, abstractNumId: number) { + constructor(numId: number, abstractNumId: number, public readonly reference?: string) { super("w:num"); this.root.push( new NumAttributes({ @@ -55,7 +55,9 @@ export class LevelOverride extends XmlComponent { this.root.push(new StartOverride(start)); } - this.lvl = new LevelForOverride(this.levelNum); + this.lvl = new LevelForOverride({ + level: this.levelNum, + }); this.root.push(this.lvl); } diff --git a/src/file/numbering/numbering.spec.ts b/src/file/numbering/numbering.spec.ts index d5f2f9e168..79a824b67a 100644 --- a/src/file/numbering/numbering.spec.ts +++ b/src/file/numbering/numbering.spec.ts @@ -2,24 +2,15 @@ import { expect } from "chai"; import { Formatter } from "export/formatter"; -import { AbstractNumbering } from "./abstract-numbering"; -import { LevelForOverride } from "./level"; -import { Num } from "./num"; import { Numbering } from "./numbering"; -import { EMPTY_OBJECT } from "file/xml-components"; -import { TabStopPosition } from "../paragraph"; -import { UnderlineType } from "../paragraph/run/underline"; - describe("Numbering", () => { - let numbering: Numbering; - - beforeEach(() => { - numbering = new Numbering(); - }); - describe("#constructor", () => { it("creates a default numbering with one abstract and one concrete instance", () => { + const numbering = new Numbering({ + config: [], + }); + const tree = new Formatter().format(numbering); expect(Object.keys(tree)).to.deep.equal(["w:numbering"]); const abstractNums = tree["w:numbering"].filter((el) => el["w:abstractNum"]); @@ -48,418 +39,4 @@ describe("Numbering", () => { }); }); }); - - describe("#createAbstractNumbering", () => { - it("returns a new AbstractNumbering instance", () => { - const a2 = numbering.createAbstractNumbering(); - expect(a2).to.be.instanceof(AbstractNumbering); - }); - - it("assigns a unique ID to each abstract numbering it creates", () => { - const a2 = numbering.createAbstractNumbering(); - const a3 = numbering.createAbstractNumbering(); - expect(a2.id).not.to.equal(a3.id); - }); - }); - - describe("#createConcreteNumbering", () => { - it("returns a new Num instance with its abstract ID set to the AbstractNumbering's ID", () => { - const a2 = numbering.createAbstractNumbering(); - const n = numbering.createConcreteNumbering(a2); - expect(n).to.be.instanceof(Num); - const tree = new Formatter().format(numbering); - const serializedN = tree["w:numbering"].find((obj) => obj["w:num"] && obj["w:num"][0]._attr["w:numId"] === n.id); - expect(serializedN["w:num"][1]["w:abstractNumId"]._attr["w:val"]).to.equal(a2.id); - }); - - it("assigns a unique ID to each concrete numbering it creates", () => { - const a2 = numbering.createAbstractNumbering(); - const n = numbering.createConcreteNumbering(a2); - const n2 = numbering.createConcreteNumbering(a2); - expect(n.id).not.to.equal(n2.id); - }); - }); -}); - -describe("AbstractNumbering", () => { - it("stores its ID at its .id property", () => { - const abstractNumbering = new AbstractNumbering(5); - expect(abstractNumbering.id).to.equal(5); - }); - - describe("#createLevel", () => { - it("creates a level with the given characteristics", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(3, "lowerLetter", "%1)", "end"); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ _attr: { "w:ilvl": 3, "w15:tentative": 1 } }); - expect(tree["w:lvl"]).to.include({ "w:start": { _attr: { "w:val": 1 } } }); - expect(tree["w:lvl"]).to.include({ "w:lvlJc": { _attr: { "w:val": "end" } } }); - expect(tree["w:lvl"]).to.include({ "w:numFmt": { _attr: { "w:val": "lowerLetter" } } }); - expect(tree["w:lvl"]).to.include({ "w:lvlText": { _attr: { "w:val": "%1)" } } }); - }); - - it("uses 'start' as the default alignment", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(3, "lowerLetter", "%1)"); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ _attr: { "w:ilvl": 3, "w15:tentative": 1 } }); - expect(tree["w:lvl"]).to.include({ "w:start": { _attr: { "w:val": 1 } } }); - expect(tree["w:lvl"]).to.include({ "w:lvlJc": { _attr: { "w:val": "start" } } }); - expect(tree["w:lvl"]).to.include({ "w:numFmt": { _attr: { "w:val": "lowerLetter" } } }); - expect(tree["w:lvl"]).to.include({ "w:lvlText": { _attr: { "w:val": "%1)" } } }); - }); - - describe("formatting methods: paragraph properties", () => { - it("#indent", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerLetter", "%0.").indent({ left: 720 }); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:pPr": [{ "w:ind": { _attr: { "w:left": 720 } } }], - }); - }); - - it("#spacing", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerLetter", "%0.").spacing({ before: 50, after: 150 }); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:pPr": [{ "w:spacing": { _attr: { "w:before": 50, "w:after": 150 } } }], - }); - }); - - it("#center", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerLetter", "%0.").center(); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:pPr": [{ "w:jc": { _attr: { "w:val": "center" } } }], - }); - }); - - it("#left", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.", "left").left(); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:pPr": [{ "w:jc": { _attr: { "w:val": "left" } } }], - }); - }); - - it("#right", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").right(); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:pPr": [{ "w:jc": { _attr: { "w:val": "right" } } }], - }); - }); - - it("#justified", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").justified(); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:pPr": [{ "w:jc": { _attr: { "w:val": "both" } } }], - }); - }); - - it("#thematicBreak", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").thematicBreak(); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:pPr": [ - { - "w:pBdr": [ - { - "w:bottom": { - _attr: { - "w:color": "auto", - "w:space": 1, - "w:val": "single", - "w:sz": 6, - }, - }, - }, - ], - }, - ], - }); - }); - - it("#leftTabStop", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").leftTabStop(1200); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:pPr": [ - { - "w:tabs": [{ "w:tab": { _attr: { "w:val": "left", "w:pos": 1200 } } }], - }, - ], - }); - }); - - it("#maxRightTabStop", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").rightTabStop(TabStopPosition.MAX); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:pPr": [ - { - "w:tabs": [{ "w:tab": { _attr: { "w:val": "right", "w:pos": 9026 } } }], - }, - ], - }); - }); - - it("#keepLines", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").keepLines(); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:pPr": [{ "w:keepLines": EMPTY_OBJECT }], - }); - }); - - it("#keepNext", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").keepNext(); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:pPr": [{ "w:keepNext": EMPTY_OBJECT }], - }); - }); - }); - - describe("formatting methods: run properties", () => { - it("#size", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").size(24); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:rPr": [{ "w:sz": { _attr: { "w:val": 24 } } }], - }); - }); - - it("#smallCaps", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").smallCaps(); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:rPr": [{ "w:smallCaps": { _attr: { "w:val": true } } }], - }); - }); - - it("#allCaps", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").allCaps(); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:rPr": [{ "w:caps": { _attr: { "w:val": true } } }], - }); - }); - - it("#strike", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").strike(); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:rPr": [{ "w:strike": { _attr: { "w:val": true } } }], - }); - }); - - it("#doubleStrike", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").doubleStrike(); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:rPr": [{ "w:dstrike": { _attr: { "w:val": true } } }], - }); - }); - - it("#subScript", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").subScript(); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:rPr": [{ "w:vertAlign": { _attr: { "w:val": "subscript" } } }], - }); - }); - - it("#superScript", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").superScript(); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:rPr": [{ "w:vertAlign": { _attr: { "w:val": "superscript" } } }], - }); - }); - - it("#font", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").font("Times"); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:rPr": [ - { "w:rFonts": { _attr: { "w:ascii": "Times", "w:cs": "Times", "w:eastAsia": "Times", "w:hAnsi": "Times" } } }, - ], - }); - }); - - it("#bold", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").bold(); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:rPr": [{ "w:b": { _attr: { "w:val": true } } }], - }); - }); - - it("#italics", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").italics(); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:rPr": [{ "w:i": { _attr: { "w:val": true } } }], - }); - }); - - it("#highlight", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").highlight("005599"); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:rPr": [{ "w:highlight": { _attr: { "w:val": "005599" } } }], - }); - }); - - it("#shadow", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").shadow("pct10", "00FFFF", "FF0000"); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:rPr": [{ "w:shd": { _attr: { "w:val": "pct10", "w:fill": "00FFFF", "w:color": "FF0000" } } }], - }); - }); - - describe("#underline", () => { - it("should set underline to 'single' if no arguments are given", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").underline(); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:rPr": [{ "w:u": { _attr: { "w:val": "single" } } }], - }); - }); - - it("should set the style if given", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").underline(UnderlineType.DOUBLE); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:rPr": [{ "w:u": { _attr: { "w:val": "double" } } }], - }); - }); - - it("should set the style and color if given", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").underline(UnderlineType.DOUBLE, "005599"); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:rPr": [{ "w:u": { _attr: { "w:val": "double", "w:color": "005599" } } }], - }); - }); - }); - - it("#color", () => { - const abstractNumbering = new AbstractNumbering(1); - const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").color("123456"); - const tree = new Formatter().format(level); - expect(tree["w:lvl"]).to.include({ - "w:rPr": [{ "w:color": { _attr: { "w:val": "123456" } } }], - }); - }); - }); - }); -}); - -describe("concrete numbering", () => { - describe("#overrideLevel", () => { - let numbering; - let abstractNumbering; - let concreteNumbering; - beforeEach(() => { - numbering = new Numbering(); - abstractNumbering = numbering.createAbstractNumbering(); - concreteNumbering = numbering.createConcreteNumbering(abstractNumbering); - }); - - it("sets a new override level for the given level number", () => { - concreteNumbering.overrideLevel(3); - const tree = new Formatter().format(concreteNumbering); - expect(tree["w:num"]).to.include({ - "w:lvlOverride": [ - { - _attr: { - "w:ilvl": 3, - }, - }, - { - "w:lvl": { - _attr: { - "w:ilvl": 3, - "w15:tentative": 1, - }, - }, - }, - ], - }); - }); - - it("sets the startOverride element if start is given", () => { - concreteNumbering.overrideLevel(1, 9); - const tree = new Formatter().format(concreteNumbering); - expect(tree["w:num"]).to.include({ - "w:lvlOverride": [ - { - _attr: { - "w:ilvl": 1, - }, - }, - { - "w:startOverride": { - _attr: { - "w:val": 9, - }, - }, - }, - { - "w:lvl": { - _attr: { - "w:ilvl": 1, - "w15:tentative": 1, - }, - }, - }, - ], - }); - }); - - it("sets the lvl element if overrideLevel.Level is accessed", () => { - const ol = concreteNumbering.overrideLevel(1); - expect(ol.Level).to.be.instanceof(LevelForOverride); - const tree = new Formatter().format(concreteNumbering); - expect(tree["w:num"]).to.include({ - "w:lvlOverride": [ - { _attr: { "w:ilvl": 1 } }, - { - "w:lvl": { _attr: { "w15:tentative": 1, "w:ilvl": 1 } }, - }, - ], - }); - }); - }); }); diff --git a/src/file/numbering/numbering.ts b/src/file/numbering/numbering.ts index 0080b6b349..b8747991d0 100644 --- a/src/file/numbering/numbering.ts +++ b/src/file/numbering/numbering.ts @@ -1,17 +1,27 @@ -import { Indent } from "file/paragraph"; +// http://officeopenxml.com/WPnumbering.php +import { AlignmentType } from "file/paragraph"; import { IXmlableObject, XmlComponent } from "file/xml-components"; + import { DocumentAttributes } from "../document/document-attributes"; import { AbstractNumbering } from "./abstract-numbering"; -import { Num } from "./num"; +import { ILevelsOptions } from "./level"; +import { ConcreteNumbering } from "./num"; + +export interface INumberingOptions { + readonly config: Array<{ + readonly levels: ILevelsOptions[]; + readonly reference: string; + }>; +} export class Numbering extends XmlComponent { // tslint:disable-next-line:readonly-keyword private nextId: number; - private readonly abstractNumbering: XmlComponent[] = []; - private readonly concreteNumbering: XmlComponent[] = []; + private readonly abstractNumbering: AbstractNumbering[] = []; + private readonly concreteNumbering: ConcreteNumbering[] = []; - constructor() { + constructor(options: INumberingOptions) { super("w:numbering"); this.root.push( new DocumentAttributes({ @@ -37,39 +47,114 @@ export class Numbering extends XmlComponent { this.nextId = 0; - const abstractNumbering = this.createAbstractNumbering(); - - abstractNumbering.createLevel(0, "bullet", "\u25CF", "left").addParagraphProperty(new Indent({ left: 720, hanging: 360 })); - - abstractNumbering.createLevel(1, "bullet", "\u25CB", "left").addParagraphProperty(new Indent({ left: 1440, hanging: 360 })); - - abstractNumbering.createLevel(2, "bullet", "\u25A0", "left").addParagraphProperty(new Indent({ left: 2160, hanging: 360 })); - - abstractNumbering.createLevel(3, "bullet", "\u25CF", "left").addParagraphProperty(new Indent({ left: 2880, hanging: 360 })); - - abstractNumbering.createLevel(4, "bullet", "\u25CB", "left").addParagraphProperty(new Indent({ left: 3600, hanging: 360 })); - - abstractNumbering.createLevel(5, "bullet", "\u25A0", "left").addParagraphProperty(new Indent({ left: 4320, hanging: 360 })); - - abstractNumbering.createLevel(6, "bullet", "\u25CF", "left").addParagraphProperty(new Indent({ left: 5040, hanging: 360 })); - - abstractNumbering.createLevel(7, "bullet", "\u25CB", "left").addParagraphProperty(new Indent({ left: 5760, hanging: 360 })); - - abstractNumbering.createLevel(8, "bullet", "\u25A0", "left").addParagraphProperty(new Indent({ left: 6480, hanging: 360 })); + const abstractNumbering = this.createAbstractNumbering([ + { + level: 0, + format: "bullet", + text: "\u25CF", + alignment: AlignmentType.LEFT, + style: { + paragraph: { + indent: { left: 720, hanging: 360 }, + }, + }, + }, + { + level: 1, + format: "bullet", + text: "\u25CB", + alignment: AlignmentType.LEFT, + style: { + paragraph: { + indent: { left: 1440, hanging: 360 }, + }, + }, + }, + { + level: 2, + format: "bullet", + text: "\u25A0", + alignment: AlignmentType.LEFT, + style: { + paragraph: { + indent: { left: 2160, hanging: 360 }, + }, + }, + }, + { + level: 3, + format: "bullet", + text: "\u25CF", + alignment: AlignmentType.LEFT, + style: { + paragraph: { + indent: { left: 2880, hanging: 360 }, + }, + }, + }, + { + level: 4, + format: "bullet", + text: "\u25CB", + alignment: AlignmentType.LEFT, + style: { + paragraph: { + indent: { left: 3600, hanging: 360 }, + }, + }, + }, + { + level: 5, + format: "bullet", + text: "\u25A0", + alignment: AlignmentType.LEFT, + style: { + paragraph: { + indent: { left: 4320, hanging: 360 }, + }, + }, + }, + { + level: 6, + format: "bullet", + text: "\u25CF", + alignment: AlignmentType.LEFT, + style: { + paragraph: { + indent: { left: 5040, hanging: 360 }, + }, + }, + }, + { + level: 7, + format: "bullet", + text: "\u25CF", + alignment: AlignmentType.LEFT, + style: { + paragraph: { + indent: { left: 5760, hanging: 360 }, + }, + }, + }, + { + level: 8, + format: "bullet", + text: "\u25CF", + alignment: AlignmentType.LEFT, + style: { + paragraph: { + indent: { left: 6480, hanging: 360 }, + }, + }, + }, + ]); this.createConcreteNumbering(abstractNumbering); - } - public createAbstractNumbering(): AbstractNumbering { - const num = new AbstractNumbering(this.nextId++); - this.abstractNumbering.push(num); - return num; - } - - public createConcreteNumbering(abstractNumbering: AbstractNumbering): Num { - const num = new Num(this.nextId++, abstractNumbering.id); - this.concreteNumbering.push(num); - return num; + for (const con of options.config) { + const currentAbstractNumbering = this.createAbstractNumbering(con.levels); + this.createConcreteNumbering(currentAbstractNumbering, con.reference); + } } public prepForXml(): IXmlableObject | undefined { @@ -77,4 +162,20 @@ export class Numbering extends XmlComponent { this.concreteNumbering.forEach((x) => this.root.push(x)); return super.prepForXml(); } + + private createConcreteNumbering(abstractNumbering: AbstractNumbering, reference?: string): ConcreteNumbering { + const num = new ConcreteNumbering(this.nextId++, abstractNumbering.id, reference); + this.concreteNumbering.push(num); + return num; + } + + private createAbstractNumbering(options: ILevelsOptions[]): AbstractNumbering { + const num = new AbstractNumbering(this.nextId++, options); + this.abstractNumbering.push(num); + return num; + } + + public get ConcreteNumbering(): ConcreteNumbering[] { + return this.concreteNumbering; + } } diff --git a/src/file/paragraph/formatting/unordered-list.ts b/src/file/paragraph/formatting/unordered-list.ts index 0b6c4daae0..bd1bcb562a 100644 --- a/src/file/paragraph/formatting/unordered-list.ts +++ b/src/file/paragraph/formatting/unordered-list.ts @@ -1,7 +1,7 @@ import { Attributes, XmlComponent } from "file/xml-components"; export class NumberProperties extends XmlComponent { - constructor(numberId: number, indentLevel: number) { + constructor(numberId: number | string, indentLevel: number) { super("w:numPr"); this.root.push(new IndentLevel(indentLevel)); this.root.push(new NumberId(numberId)); @@ -20,11 +20,11 @@ class IndentLevel extends XmlComponent { } class NumberId extends XmlComponent { - constructor(id: number) { + constructor(id: number | string) { super("w:numId"); this.root.push( new Attributes({ - val: id, + val: typeof id === "string" ? `{${id}}` : id, }), ); } diff --git a/src/file/paragraph/paragraph.spec.ts b/src/file/paragraph/paragraph.spec.ts index e8e5f6c8c9..b4a797a8c2 100644 --- a/src/file/paragraph/paragraph.spec.ts +++ b/src/file/paragraph/paragraph.spec.ts @@ -3,7 +3,6 @@ import { assert, expect } from "chai"; import { Formatter } from "export/formatter"; import { EMPTY_OBJECT } from "file/xml-components"; -import { Numbering } from "../numbering"; import { AlignmentType, HeadingLevel, LeaderType, PageBreak, TabStopPosition, TabStopType } from "./formatting"; import { Paragraph } from "./paragraph"; @@ -596,14 +595,9 @@ describe("Paragraph", () => { describe("#setNumbering", () => { it("should add list paragraph style to JSON", () => { - const numbering = new Numbering(); - const numberedAbstract = numbering.createAbstractNumbering(); - numberedAbstract.createLevel(0, "lowerLetter", "%1)", "start"); - const letterNumbering = numbering.createConcreteNumbering(numberedAbstract); - const paragraph = new Paragraph({ numbering: { - num: letterNumbering, + reference: "test id", level: 0, }, }); @@ -622,14 +616,9 @@ describe("Paragraph", () => { }); it("it should add numbered properties", () => { - const numbering = new Numbering(); - const numberedAbstract = numbering.createAbstractNumbering(); - numberedAbstract.createLevel(0, "lowerLetter", "%1)", "start"); - const letterNumbering = numbering.createConcreteNumbering(numberedAbstract); - const paragraph = new Paragraph({ numbering: { - num: letterNumbering, + reference: "test id", level: 0, }, }); @@ -640,10 +629,7 @@ describe("Paragraph", () => { "w:pPr": [ { "w:pStyle": { _attr: { "w:val": "ListParagraph" } } }, { - "w:numPr": [ - { "w:ilvl": { _attr: { "w:val": 0 } } }, - { "w:numId": { _attr: { "w:val": letterNumbering.id } } }, - ], + "w:numPr": [{ "w:ilvl": { _attr: { "w:val": 0 } } }, { "w:numId": { _attr: { "w:val": "{test id}" } } }], }, ], }, diff --git a/src/file/paragraph/paragraph.ts b/src/file/paragraph/paragraph.ts index b11393ee37..1defd01779 100644 --- a/src/file/paragraph/paragraph.ts +++ b/src/file/paragraph/paragraph.ts @@ -1,6 +1,5 @@ // http://officeopenxml.com/WPparagraph.php import { FootnoteReferenceRun } from "file/footnotes/footnote/run/reference-run"; -import { Num } from "file/numbering/num"; import { XmlComponent } from "file/xml-components"; import { Alignment, AlignmentType } from "./formatting/alignment"; @@ -41,7 +40,7 @@ export interface IParagraphOptions { readonly level: number; }; readonly numbering?: { - readonly num: Num; + readonly reference: string; readonly level: number; readonly custom?: boolean; }; @@ -141,7 +140,7 @@ export class Paragraph extends XmlComponent { if (!options.numbering.custom) { this.properties.push(new Style("ListParagraph")); } - this.properties.push(new NumberProperties(options.numbering.num.id, options.numbering.level)); + this.properties.push(new NumberProperties(options.numbering.reference, options.numbering.level)); } if (options.children) { diff --git a/src/file/styles/style-options.ts b/src/file/styles/style-options.ts new file mode 100644 index 0000000000..dcedadbec5 --- /dev/null +++ b/src/file/styles/style-options.ts @@ -0,0 +1,39 @@ +import { AlignmentType, IIndentAttributesProperties, ISpacingProperties, UnderlineType } from "../paragraph"; +import { ShadingType } from "../table"; + +export interface IRunStyleOptions { + readonly size?: number; + readonly bold?: boolean; + readonly italics?: boolean; + readonly smallCaps?: boolean; + readonly allCaps?: boolean; + readonly strike?: boolean; + readonly doubleStrike?: boolean; + readonly subScript?: boolean; + readonly superScript?: boolean; + readonly underline?: { + readonly type?: UnderlineType; + readonly color?: string; + }; + readonly color?: string; + readonly font?: string; + readonly characterSpacing?: number; + readonly highlight?: string; + readonly shadow?: { + readonly type: ShadingType; + readonly fill: string; + readonly color: string; + }; +} + +export interface IParagraphStyleOptions2 { + readonly alignment?: AlignmentType; + readonly thematicBreak?: boolean; + readonly rightTabStop?: number; + readonly leftTabStop?: number; + readonly indent?: IIndentAttributesProperties; + readonly spacing?: ISpacingProperties; + readonly keepNext?: boolean; + readonly keepLines?: boolean; + readonly outlineLevel?: number; +} diff --git a/src/file/styles/style/paragraph-style.ts b/src/file/styles/style/paragraph-style.ts index ab592a9fc6..a47a33f492 100644 --- a/src/file/styles/style/paragraph-style.ts +++ b/src/file/styles/style/paragraph-style.ts @@ -1,21 +1,9 @@ -import { - Alignment, - AlignmentType, - Indent, - ISpacingProperties, - KeepLines, - KeepNext, - OutlineLevel, - ParagraphProperties, - Spacing, - ThematicBreak, -} from "file/paragraph"; -import { IIndentAttributesProperties, TabStop, TabStopType } from "file/paragraph/formatting"; +import { Alignment, Indent, KeepLines, KeepNext, OutlineLevel, ParagraphProperties, Spacing, ThematicBreak } from "file/paragraph"; +import { TabStop, TabStopType } from "file/paragraph/formatting"; import * as formatting from "file/paragraph/run/formatting"; import { RunProperties } from "file/paragraph/run/properties"; -import { UnderlineType } from "file/paragraph/run/underline"; -import { ShadingType } from "file/table"; +import { IParagraphStyleOptions2, IRunStyleOptions } from "../style-options"; import { BasedOn, Link, Next, QuickFormat, SemiHidden, UiPriority, UnhideWhenUsed } from "./components"; import { Style } from "./style"; @@ -27,41 +15,8 @@ export interface IBaseParagraphStyleOptions { readonly semiHidden?: boolean; readonly uiPriority?: number; readonly unhideWhenUsed?: boolean; - readonly run?: { - readonly size?: number; - readonly bold?: boolean; - readonly italics?: boolean; - readonly smallCaps?: boolean; - readonly allCaps?: boolean; - readonly strike?: boolean; - readonly doubleStrike?: boolean; - readonly subScript?: boolean; - readonly superScript?: boolean; - readonly underline?: { - readonly type?: UnderlineType; - readonly color?: string; - }; - readonly color?: string; - readonly font?: string; - readonly characterSpacing?: number; - readonly highlight?: string; - readonly shadow?: { - readonly type: ShadingType; - readonly fill: string; - readonly color: string; - }; - }; - readonly paragraph?: { - readonly alignment?: AlignmentType; - readonly thematicBreak?: boolean; - readonly rightTabStop?: number; - readonly leftTabStop?: number; - readonly indent?: IIndentAttributesProperties; - readonly spacing?: ISpacingProperties; - readonly keepNext?: boolean; - readonly keepLines?: boolean; - readonly outlineLevel?: number; - }; + readonly run?: IRunStyleOptions; + readonly paragraph?: IParagraphStyleOptions2; } export interface IParagraphStyleOptions extends IBaseParagraphStyleOptions {