diff --git a/docs/numbering.md b/docs/numbering.md new file mode 100644 index 0000000000..12cce9480f --- /dev/null +++ b/docs/numbering.md @@ -0,0 +1,103 @@ +# Bullets and numbering + +DOCX is quite flexible in its bullets and numbering system, allowing +the user great freedom in how bullets and numbers are to be styled and +displayed. E.g., numbers can be shown using Arabic numerals, roman +numerals, or even ordinal words ("one", "two", "three", ...). The +format also supports re-using bullets/numbering styles throughout the +document, so that different lists using the same style need not +redefine them. + +Because of this flexibility, bullets and numbering in DOCX involves a +couple of moving pieces: + +1. Document-level bullets/numbering definitions (abstract) +2. Document-level bullets/numbering definitions (concrete) +3. Paragraph-level bullets/numbering selection + + +## Document-level bullets/numbering definitions (abstract) + +Every document contains a set of abstract bullets/numbering +definitions which define the formatting and layout of paragraphs using +those bullets/numbering. An abstract numbering system defines how +bullets/numbers are to be shown for lists, including any sublists that +may be used. Thus each abstract definition includes a series of +*levels* which form a sequence starting at 0 indicating the top-level +list look and increasing from there to descibe the sublists, then +sub-sublists, etc. Each level includes the following properties: + +- **level**: This its 0-based index in the defintion stack +- **numberFormat**: This indicates how the bullet or number should be + generated. Options include `bullet` (meaning don't count), `decimal` + (arabic numerals), `upperRoman`, `lowerRoman`, `hex`, and many + more. +- **levelText**: This is a format string using the output of the + `numberFormat` function and generating a string to insert before + every item in the list. You may use `%1`, `%2`, ... to reference the + numbers from each numbering level before this one. Thus a level + text of `%d)` with a number format of `lowerLetter` would result in + the sequence "a)", "b)", ... +- and a few others, which you can see in the OXML spec section 17.9.6 + + +## Document-level bullets/numbering defintions (concrete) + +Concrete definitions are sort of like concrete subclasses of the +abstract defintions. They indicate their parent and are allowed to +override certain level definitions. Thus two lists that differ only in +how sub-sub-lists are to be displayed can share the same abstract +numbering definition and have slightly different concrete definitions. + + +## Paragraph-level bullets/numbering selection + +In order to use a bullets/numbering definition (which must be +concrete), paragraphs need to select it, similar to applying a CSS +class to an element, using both the concrete numbering definition ID +and the level number that the paragraph should be at. Additionally, MS +Word and LibreOffice typically apply a "ListParagraph" style to +paragraphs that are being numbered. + + +## Using bullets/numbering in `docx` + +`docx` includes a pre-defined bullet style which you can add to your +paragraphs using `para.bullets()`. If you require different bullet +styles or numbering of any kind, you'll have to use the +`docx.Numbering` class. + +First you need to create a new numbering container class and use it to +create your abstract numbering style, define your levels, and creat +your concreate numbering style: + +```js +const numbering = new Numbering(); + +const abstractNum = numbering.createAbstractNumbering(); +abstractNum.createLevel(0, "upperRoman", "%1", "start") + .addParagraphProperty(new Indent(720, 260)); +abstractNum.createLevel(1, "decimal", "%2.", "start") + .addParagraphProperty(new Indent(1440, 980)); +abstractNum.createLevel(2, "lowerLetter", "%3)", "start") + .addParagraphProperty(new Indent(2160, 1700)); + +const concrete = numbering.createConcreteNumbering(numberedAbstract); +``` + +You can then apply your concrete style to paragraphs using their +`#setNumbering` method: + +```js +topLevelP.setNumbering(concrete, 0); +subP.setNumbering(concrete, 1); +subSubP.setNumbering(concrete, 2); +``` + +Finally, you need to let your exporter know about your numbering +styles when you're ready to render the document: + +```js +const packer = new Packer(doc, undefined, undefined, numbering); +packer.pack(myOutput); +``` diff --git a/ts/docx/paragraph/index.ts b/ts/docx/paragraph/index.ts index 064226f0e6..9b1c0cc08d 100644 --- a/ts/docx/paragraph/index.ts +++ b/ts/docx/paragraph/index.ts @@ -6,6 +6,7 @@ import {ParagraphProperties} from "./properties"; import {MaxRightTabStop, LeftTabStop} from "./tab-stop"; import {Style} from "./style"; import {NumberProperties} from "./unordered-list"; +import { Num } from "../../numbering/num"; class Alignment extends XmlComponent { @@ -106,7 +107,13 @@ export class Paragraph extends XmlComponent { bullet(): Paragraph { this.properties.push(new Style("ListParagraph")); - this.properties.push(new NumberProperties()); + this.properties.push(new NumberProperties(1, 0)); return this; } -} \ No newline at end of file + + public setNumbering(numbering: Num, indentLevel: number): Paragraph { + this.properties.push(new Style("ListParagraph")); + this.properties.push(new NumberProperties(numbering.id, indentLevel)); + return this; + } +} diff --git a/ts/docx/paragraph/unordered-list.ts b/ts/docx/paragraph/unordered-list.ts index ec270cd899..9a6611b273 100644 --- a/ts/docx/paragraph/unordered-list.ts +++ b/ts/docx/paragraph/unordered-list.ts @@ -3,10 +3,10 @@ import {Style} from "./style"; export class NumberProperties extends XmlComponent { - constructor() { + constructor(numberId: number, indentLevel: number) { super("w:numPr"); - this.root.push(new IndentLevel(0)); - this.root.push(new NumberId(1)); + this.root.push(new IndentLevel(indentLevel)); + this.root.push(new NumberId(numberId)); } } diff --git a/ts/index.ts b/ts/index.ts index b7537fd38f..800814ea0b 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -1,2 +1,3 @@ export * from "./docx"; -export * from "./export"; \ No newline at end of file +export * from "./export"; +export { Numbering } from './numbering'; diff --git a/ts/numbering/abstract-numbering.ts b/ts/numbering/abstract-numbering.ts index 1839c25389..6e344df114 100644 --- a/ts/numbering/abstract-numbering.ts +++ b/ts/numbering/abstract-numbering.ts @@ -1,42 +1,50 @@ -import {XmlComponent} from "../docx/xml-components"; -import {XmlAttributeComponent} from "../docx/xml-components"; -import {Level} from "./level"; -import {MultiLevelType} from "./multi-level-type"; import * as _ from "lodash"; +import { XmlAttributeComponent, XmlComponent } from "../docx/xml-components"; +import { Level } from "./level"; +import { MultiLevelType } from "./multi-level-type"; -interface AbstractNumberingAttributesProperties { +interface IAbstractNumberingAttributesProperties { abstractNumId?: number; restartNumberingAfterBreak?: number; } class AbstractNumberingAttributes extends XmlAttributeComponent { - constructor(properties: AbstractNumberingAttributesProperties) { + constructor(properties: IAbstractNumberingAttributesProperties) { super({ abstractNumId: "w:abstractNumId", - restartNumberingAfterBreak: "w15:restartNumberingAfterBreak" + restartNumberingAfterBreak: "w15:restartNumberingAfterBreak", }, properties); } } export class AbstractNumbering extends XmlComponent { + public id: number; constructor(id: number) { super("w:abstractNum"); this.root.push(new AbstractNumberingAttributes({ abstractNumId: id, - restartNumberingAfterBreak: 0 + restartNumberingAfterBreak: 0, })); this.root.push(new MultiLevelType("hybridMultilevel")); + this.id = id; } - addLevel(level: Level): void { + public addLevel(level: Level): void { this.root.push(level); } - clearVariables() { - _.forEach(this.root, element => { + public createLevel(num: number, format: string, text: string, align: string="start") { + const level = new Level(num, format, text, align); + this.addLevel(level); + return level; + } + + public clearVariables(): void { + _.forEach(this.root, (element) => { element.clearVariables(); }); + delete this.id; } -} \ No newline at end of file +} diff --git a/ts/numbering/indent.ts b/ts/numbering/indent.ts index 05477f5d20..38163fe812 100644 --- a/ts/numbering/indent.ts +++ b/ts/numbering/indent.ts @@ -1,4 +1,4 @@ -import {XmlComponent, XmlAttributeComponent} from "../docx/xml-components"; +import {XmlAttributeComponent, XmlComponent} from "../docx/xml-components"; interface IndentAttributesProperties { left: number; @@ -10,7 +10,7 @@ class IndentAttributes extends XmlAttributeComponent { constructor(properties: IndentAttributesProperties) { super({ left: "w:left", - hanging: "w:hanging" + hanging: "w:hanging", }, properties); } } @@ -21,7 +21,7 @@ export class Indent extends XmlComponent { super("w:ind"); this.root.push(new IndentAttributes({ left: left, - hanging: hanging + hanging: hanging, })); } -} \ No newline at end of file +} diff --git a/ts/numbering/index.ts b/ts/numbering/index.ts index d32f8767e1..b813c665cf 100644 --- a/ts/numbering/index.ts +++ b/ts/numbering/index.ts @@ -1,13 +1,14 @@ -import {MultiPropertyXmlComponent} from "../docx/xml-components"; -import {DocumentAttributes} from "../docx/document/document-attributes"; -import {AbstractNumbering} from "./abstract-numbering"; -import {Level} from "./level"; -import {Indent} from "./indent"; -import {RunFonts} from "./run-fonts"; -import {Num} from "./num"; import * as _ from "lodash"; +import { DocumentAttributes } from "../docx/document/document-attributes"; +import { MultiPropertyXmlComponent } from "../docx/xml-components"; +import { AbstractNumbering } from "./abstract-numbering"; +import { Indent } from "./indent"; +import { Level } from "./level"; +import { Num } from "./num"; +import { RunFonts } from "./run-fonts"; export class Numbering extends MultiPropertyXmlComponent { + private nextId: number; constructor() { super("w:numbering"); @@ -28,65 +29,69 @@ export class Numbering extends MultiPropertyXmlComponent { 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" + Ignorable: "w14 w15 wp14", })); - let abstractNumbering = new AbstractNumbering(0); + this.nextId = 0; - let level0 = new Level(0, "bullet", "•", "left"); - level0.addParagraphProperty(new Indent(720, 360)); - level0.addRunProperty(new RunFonts("Symbol", "default")); - abstractNumbering.addLevel(level0); + const abstractNumbering = this.createAbstractNumbering(); - let level1 = new Level(1, "bullet", "o", "left"); - level1.addParagraphProperty(new Indent(1440, 360)); - level1.addRunProperty(new RunFonts("Courier New", "default")); - abstractNumbering.addLevel(level1); + abstractNumbering.createLevel(0, "bullet", "•", "left") + .addParagraphProperty(new Indent(720, 360)) + .addRunProperty(new RunFonts("Symbol", "default")); - let level2 = new Level(2, "bullet", "•", "left"); - level2.addParagraphProperty(new Indent(2160, 360)); - level2.addRunProperty(new RunFonts("Wingdings", "default")); - abstractNumbering.addLevel(level2); + abstractNumbering.createLevel(1, "bullet", "o", "left") + .addParagraphProperty(new Indent(1440, 360)) + .addRunProperty(new RunFonts("Courier New", "default")); - let level3 = new Level(3, "bullet", "•", "left"); - level3.addParagraphProperty(new Indent(2880, 360)); - level3.addRunProperty(new RunFonts("Symbol", "default")); - abstractNumbering.addLevel(level3); + abstractNumbering.createLevel(2, "bullet", "•", "left") + .addParagraphProperty(new Indent(2160, 360)) + .addRunProperty(new RunFonts("Wingdings", "default")); - let level4 = new Level(4, "bullet", "o", "left"); - level4.addParagraphProperty(new Indent(3600, 360)); - level4.addRunProperty(new RunFonts("Courier New", "default")); - abstractNumbering.addLevel(level4); + abstractNumbering.createLevel(3, "bullet", "•", "left") + .addParagraphProperty(new Indent(2880, 360)) + .addRunProperty(new RunFonts("Symbol", "default")); - let level5 = new Level(5, "bullet", "•", "left"); - level5.addParagraphProperty(new Indent(4320, 360)); - level5.addRunProperty(new RunFonts("Wingdings", "default")); - abstractNumbering.addLevel(level5); + abstractNumbering.createLevel(4, "bullet", "o", "left") + .addParagraphProperty(new Indent(3600, 360)) + .addRunProperty(new RunFonts("Courier New", "default")); - let level6 = new Level(6, "bullet", "•", "left"); - level6.addParagraphProperty(new Indent(5040, 360)); - level6.addRunProperty(new RunFonts("Symbol", "default")); - abstractNumbering.addLevel(level6); + abstractNumbering.createLevel(5, "bullet", "•", "left") + .addParagraphProperty(new Indent(4320, 360)) + .addRunProperty(new RunFonts("Wingdings", "default")); - let level7 = new Level(4, "bullet", "o", "left"); - level7.addParagraphProperty(new Indent(5760, 360)); - level7.addRunProperty(new RunFonts("Courier New", "default")); - abstractNumbering.addLevel(level7); + abstractNumbering.createLevel(6, "bullet", "•", "left") + .addParagraphProperty(new Indent(5040, 360)) + .addRunProperty(new RunFonts("Symbol", "default")); - let level8 = new Level(5, "bullet", "•", "left"); - level8.addParagraphProperty(new Indent(6480, 360)); - level8.addRunProperty(new RunFonts("Wingdings", "default")); - abstractNumbering.addLevel(level8); + abstractNumbering.createLevel(7, "bullet", "o", "left") + .addParagraphProperty(new Indent(5760, 360)) + .addRunProperty(new RunFonts("Courier New", "default")); - this.root.push(abstractNumbering); - this.root.push(new Num(1, 0)); + abstractNumbering.createLevel(8, "bullet", "•", "left") + .addParagraphProperty(new Indent(6480, 360)) + .addRunProperty(new RunFonts("Wingdings", "default")); + + this.createConcreteNumbering(abstractNumbering); } - clearVariables() { + public createAbstractNumbering(): AbstractNumbering { + const num = new AbstractNumbering(this.nextId++); + this.root.push(num); + return num; + } + + public createConcreteNumbering(abstractNumbering: AbstractNumbering): Num { + const num = new Num(this.nextId++, abstractNumbering.id); + this.root.push(num); + return num; + } + + public clearVariables(): void { super.clearVariables(); - _.forEach(this.root, element => { - console.log(element); + _.forEach(this.root, (element) => { element.clearVariables(); }); + delete this.nextId; } -} \ No newline at end of file +} diff --git a/ts/numbering/level.ts b/ts/numbering/level.ts index 32c2625b02..54937cbf63 100644 --- a/ts/numbering/level.ts +++ b/ts/numbering/level.ts @@ -1,19 +1,18 @@ -import {XmlComponent, Attributes, MultiPropertyXmlComponent} from "../docx/xml-components"; -import {XmlAttributeComponent} from "../docx/xml-components"; -import {RunProperties} from "../docx/run/properties"; -import {ParagraphProperties} from "../docx/paragraph/properties"; +import { ParagraphProperties } from "../docx/paragraph/properties"; +import { RunProperties } from "../docx/run/properties"; +import { Attributes, MultiPropertyXmlComponent, XmlAttributeComponent, XmlComponent } from "../docx/xml-components"; -interface LevelAttributesProperties { +interface ILevelAttributesProperties { ilvl?: number; tentative?: number; } class LevelAttributes extends XmlAttributeComponent { - constructor(properties: LevelAttributesProperties) { + constructor(properties: ILevelAttributesProperties) { super({ ilvl: "w:ilvl", - tentative: "w15:tentative" + tentative: "w15:tentative", }, properties); } } @@ -23,7 +22,7 @@ class Start extends XmlComponent { constructor(value: number) { super("w:start"); this.root.push(new Attributes({ - val: value + val: value, })); } } @@ -33,7 +32,7 @@ class NumberFormat extends XmlComponent { constructor(value: string) { super("w:numFmt"); this.root.push(new Attributes({ - val: value + val: value, })); } } @@ -43,7 +42,7 @@ class LevelText extends XmlComponent { constructor(value: string) { super("w:lvlText"); this.root.push(new Attributes({ - val: value + val: value, })); } } @@ -53,7 +52,7 @@ class LevelJc extends XmlComponent { constructor(value: string) { super("w:lvlJc"); this.root.push(new Attributes({ - val: value + val: value, })); } } @@ -66,7 +65,7 @@ export class Level extends XmlComponent { super("w:lvl"); this.root.push(new LevelAttributes({ ilvl: level, - tentative: 1 + tentative: 1, })); this.root.push(new Start(1)); @@ -81,7 +80,7 @@ export class Level extends XmlComponent { this.root.push(this.runProperties); } - clearVariables(): void { + public clearVariables(): void { this.paragraphProperties.clearVariables(); this.runProperties.clearVariables(); @@ -89,11 +88,13 @@ export class Level extends XmlComponent { delete this.runProperties; } - addParagraphProperty(property: XmlComponent): void { + public addParagraphProperty(property: XmlComponent): Level { this.paragraphProperties.push(property); + return this; } - addRunProperty(property: XmlComponent): void { + public addRunProperty(property: XmlComponent): Level { this.runProperties.push(property); + return this; } -} \ No newline at end of file +} diff --git a/ts/numbering/multi-level-type.ts b/ts/numbering/multi-level-type.ts index 359b988e9c..d987a9c652 100644 --- a/ts/numbering/multi-level-type.ts +++ b/ts/numbering/multi-level-type.ts @@ -1,11 +1,11 @@ -import {XmlComponent, Attributes} from "../docx/xml-components"; +import {Attributes, XmlComponent} from "../docx/xml-components"; export class MultiLevelType extends XmlComponent { constructor(value: string) { super("w:multiLevelType"); this.root.push(new Attributes({ - val: value + val: value, })); } -} \ No newline at end of file +} diff --git a/ts/numbering/num.ts b/ts/numbering/num.ts index 2889b456ef..f127817654 100644 --- a/ts/numbering/num.ts +++ b/ts/numbering/num.ts @@ -1,35 +1,42 @@ -import {XmlComponent, Attributes, XmlAttributeComponent} from "../docx/xml-components"; +import { Attributes, XmlAttributeComponent, XmlComponent } from "../docx/xml-components"; class AbstractNumId extends XmlComponent { constructor(value: number) { super("w:abstractNumId"); this.root.push(new Attributes({ - val: value + val: value, })); } } -interface NumAttributesProperties { +interface INumAttributesProperties { numId: number; } class NumAttributes extends XmlAttributeComponent { - constructor(properties: NumAttributesProperties) { + constructor(properties: INumAttributesProperties) { super({ - numId: "w:numId" + numId: "w:numId", }, properties); } } export class Num extends XmlComponent { + public id: number; constructor(numId: number, abstractNumId: number) { super("w:num"); this.root.push(new NumAttributes({ - numId: numId + numId: numId, })); this.root.push(new AbstractNumId(abstractNumId)); + this.id = numId; } -} \ No newline at end of file + + public clearVariables(): void { + super.clearVariables(); + delete this.id; + } +} diff --git a/ts/numbering/run-fonts.ts b/ts/numbering/run-fonts.ts index cc5c640702..dc4a8cd77b 100644 --- a/ts/numbering/run-fonts.ts +++ b/ts/numbering/run-fonts.ts @@ -1,6 +1,6 @@ -import {XmlComponent, XmlAttributeComponent} from "../docx/xml-components"; +import {XmlAttributeComponent, XmlComponent} from "../docx/xml-components"; -interface RunFontAttributesProperties { +interface IRunFontAttributesProperties { ascii: string; hAnsi: string; hint: string; @@ -8,10 +8,11 @@ interface RunFontAttributesProperties { class RunFontAttributes extends XmlAttributeComponent { - constructor(properties: RunFontAttributesProperties) { + constructor(properties: IRunFontAttributesProperties) { super({ - left: "w:left", - hanging: "w:hanging" + ascii: "w:ascii", + hAnsi: "w:hAnsi", + hint: "w:hint", }, properties); } } @@ -19,11 +20,11 @@ class RunFontAttributes extends XmlAttributeComponent { export class RunFonts extends XmlComponent { constructor(ascii: string, hint: string) { - super("w:ind"); + super("w:rFonts"); this.root.push(new RunFontAttributes({ ascii: ascii, hAnsi: ascii, - hint: hint + hint: hint, })); } -} \ No newline at end of file +} diff --git a/ts/tests/docx/paragraph/paragraphTests.ts b/ts/tests/docx/paragraph/paragraphTests.ts index cda3632e33..81fb18564d 100644 --- a/ts/tests/docx/paragraph/paragraphTests.ts +++ b/ts/tests/docx/paragraph/paragraphTests.ts @@ -1,5 +1,7 @@ import * as docx from "../../../docx"; -import { assert } from "chai"; +import { Formatter } from "../../../export/formatter"; +import { Numbering } from "../../../numbering"; +import { assert, expect } from "chai"; function jsonify(obj: Object) { let stringifiedJson = JSON.stringify(obj); @@ -113,4 +115,44 @@ describe("Paragraph", () => { assert.isDefined(newJson.root[0].root[2]); }); }); -}); \ No newline at end of file + + 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); + + paragraph.setNumbering(letterNumbering, 0); + let newJson = jsonify(paragraph); + assert.equal(newJson.root[0].root[1].root[0].root.val, "ListParagraph"); + }); + + 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); + + paragraph.setNumbering(letterNumbering, 0); + const tree = new Formatter().format(paragraph); + console.log(JSON.stringify(tree, null, 2)); + expect(tree).to.deep.equal({ + "w:p": [ + { + "w:pPr": [ + {"_attr": {}}, + {"w:pStyle": [{"_attr": {"w:val": "ListParagraph"}}]}, + { + "w:numPr": [ + {"w:ilvl": [{"_attr": {"w:val": 0}}]}, + {"w:numId": [{"_attr": {"w:val": letterNumbering.id}}]} + ] + }, + ], + }, + ] + }) + }); + }); +}); diff --git a/ts/tests/docx/paragraph/unorderedListTests.ts b/ts/tests/docx/paragraph/unorderedListTests.ts index 6a94ee4d6c..ab1b14f474 100644 --- a/ts/tests/docx/paragraph/unorderedListTests.ts +++ b/ts/tests/docx/paragraph/unorderedListTests.ts @@ -10,7 +10,7 @@ describe("NumberProperties", () => { let numberProperties: NumberProperties; beforeEach(() => { - numberProperties = new NumberProperties(); + numberProperties = new NumberProperties(5, 10); }); describe("#constructor()", () => { @@ -22,11 +22,13 @@ describe("NumberProperties", () => { it("should create a Page Break with a Indent Level inside", () => { let newJson = jsonify(numberProperties); assert.equal(newJson.root[0].rootKey, "w:ilvl"); + assert.equal(newJson.root[0].root[0].root.val, 10); }); it("should create a Page Break with a Number Id inside", () => { let newJson = jsonify(numberProperties); assert.equal(newJson.root[1].rootKey, "w:numId"); + assert.equal(newJson.root[1].root[0].root.val, 5); }); }); }); \ No newline at end of file diff --git a/ts/tests/numberingTest.ts b/ts/tests/numberingTest.ts index 404460133a..7b562567a8 100644 --- a/ts/tests/numberingTest.ts +++ b/ts/tests/numberingTest.ts @@ -1,21 +1,109 @@ -import { assert } from "chai"; +import { expect } from "chai"; +import { Formatter } from "../export/formatter"; import { Numbering } from "../numbering"; +import { AbstractNumbering } from "../numbering/abstract-numbering"; +import { Num } from "../numbering/num"; -function jsonify(obj: Object) { - let stringifiedJson = JSON.stringify(obj); - return JSON.parse(stringifiedJson); +function jsonify(obj: object) { + return JSON.parse(JSON.stringify(obj)); } -describe("", () => { +describe("Numbering", () => { - let numbering = new Numbering; + let numbering: Numbering; beforeEach(() => { numbering = new Numbering(); }); - describe("#methodName()", () => { - it("should ", () => { + describe("#constructor", () => { + it("creates a default numbering with one abstract and one concrete instance", () => { + 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"]); + expect(abstractNums).to.have.lengthOf(1); + expect(abstractNums[0]["w:abstractNum"]).to.deep.include.members([ + {_attr: {"w:abstractNumId": 0, "w15:restartNumberingAfterBreak": 0}}, + {"w:multiLevelType": [{_attr: {"w:val": "hybridMultilevel"}}]}, + ]); + abstractNums.filter((el) => el["w:lvl"]).forEach((el, ix) => { + expect(Object.keys(el)).to.have.lengthOf(1); + expect(Object.keys(el["w:lvl"]).sort()).to.deep.equal([ + "_attr", "w:start", "w:lvlJc", "w:numFmt", "w:pPr", "w:rPr", + ]); + expect(el["w:lvl"]).to.have.deep.members([ + {_attr: {"w:ilvl": ix, "w15:tentative": 1}}, + {"w:start": [{_attr: {"w:val": 1}}]}, + {"w:lvlJc": [{_attr: {"w:val": "left"}}]}, + {"w:numFmt": [{_attr: {"w:val": "bullet"}}]}, + ]); + // Once chai 4.0.0 lands and #644 is resolved, we can add the following to the test: + // {"w:lvlText": [{"_attr": {"w:val": "•"}}]}, + // {"w:rPr": [{"w:rFonts": [{"_attr": {"w:ascii": "Symbol", "w:hAnsi": "Symbol", "w:hint": "default"}}]}]}, + // {"w:pPr": [{"_attr": {}}, + // {"w:ind": [{"_attr": {"w:left": 720, "w:hanging": 360}}]}]}, + }); }); }); -}); \ No newline at end of file + + 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); + expect(n.id).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)"}}]}) + }); + }); +});