#548 #508 Restart numbered lists

This commit is contained in:
Dolan
2021-03-12 03:58:05 +00:00
parent 9864cdea16
commit 0b88cb0ca5
20 changed files with 430 additions and 163 deletions

View File

@ -1,7 +1,8 @@
// Numbered lists // Numbered lists
// The lists can also be restarted by specifying the instance number
// Import from 'docx' rather than '../build' if you install from npm // Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs"; import * as fs from "fs";
import { AlignmentType, convertInchesToTwip, Document, LevelFormat, Packer, Paragraph } from "../build"; import { AlignmentType, convertInchesToTwip, Document, HeadingLevel, LevelFormat, Packer, Paragraph } from "../build";
const doc = new Document({ const doc = new Document({
numbering: { numbering: {
@ -125,11 +126,16 @@ doc.addSection({
level: 0, level: 0,
}, },
}), }),
new Paragraph({
text: "Next",
heading: HeadingLevel.HEADING_2,
}),
new Paragraph({ new Paragraph({
text: "test", text: "test",
numbering: { numbering: {
reference: "padded-numbering-reference", reference: "padded-numbering-reference",
level: 0, level: 0,
instance: 2,
}, },
}), }),
new Paragraph({ new Paragraph({
@ -137,6 +143,19 @@ doc.addSection({
numbering: { numbering: {
reference: "padded-numbering-reference", reference: "padded-numbering-reference",
level: 0, level: 0,
instance: 2,
},
}),
new Paragraph({
text: "Next",
heading: HeadingLevel.HEADING_2,
}),
new Paragraph({
text: "test",
numbering: {
reference: "padded-numbering-reference",
level: 0,
instance: 3,
}, },
}), }),
new Paragraph({ new Paragraph({
@ -144,6 +163,7 @@ doc.addSection({
numbering: { numbering: {
reference: "padded-numbering-reference", reference: "padded-numbering-reference",
level: 0, level: 0,
instance: 3,
}, },
}), }),
new Paragraph({ new Paragraph({
@ -151,14 +171,12 @@ doc.addSection({
numbering: { numbering: {
reference: "padded-numbering-reference", reference: "padded-numbering-reference",
level: 0, level: 0,
instance: 3,
}, },
}), }),
new Paragraph({ new Paragraph({
text: "test", text: "Next",
numbering: { heading: HeadingLevel.HEADING_2,
reference: "padded-numbering-reference",
level: 0,
},
}), }),
new Paragraph({ new Paragraph({
text: "test", text: "test",

22
package-lock.json generated
View File

@ -605,12 +605,6 @@
"@types/request": "*" "@types/request": "*"
} }
}, },
"@types/shortid": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/shortid/-/shortid-0.0.29.tgz",
"integrity": "sha1-gJPuBBam4r8qpjOBCRFLP7/6Dps=",
"dev": true
},
"@types/sinon": { "@types/sinon": {
"version": "9.0.11", "version": "9.0.11",
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-9.0.11.tgz", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-9.0.11.tgz",
@ -4875,7 +4869,7 @@
}, },
"jsesc": { "jsesc": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz",
"integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=",
"dev": true "dev": true
}, },
@ -5592,9 +5586,9 @@
"optional": true "optional": true
}, },
"nanoid": { "nanoid": {
"version": "2.1.11", "version": "3.1.20",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz",
"integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==" "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw=="
}, },
"nanomatch": { "nanomatch": {
"version": "1.2.13", "version": "1.2.13",
@ -7360,14 +7354,6 @@
"vscode-textmate": "^5.2.0" "vscode-textmate": "^5.2.0"
} }
}, },
"shortid": {
"version": "2.2.16",
"resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz",
"integrity": "sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==",
"requires": {
"nanoid": "^2.1.0"
}
},
"signal-exit": { "signal-exit": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",

View File

@ -52,7 +52,7 @@
"@types/jszip": "^3.1.4", "@types/jszip": "^3.1.4",
"@types/node": "^14.0.5", "@types/node": "^14.0.5",
"jszip": "^3.1.5", "jszip": "^3.1.5",
"shortid": "^2.2.15", "nanoid": "^3.1.20",
"xml": "^1.0.1", "xml": "^1.0.1",
"xml-js": "^1.6.8" "xml-js": "^1.6.8"
}, },
@ -66,7 +66,6 @@
"@types/chai": "^4.2.15", "@types/chai": "^4.2.15",
"@types/mocha": "^8.0.0", "@types/mocha": "^8.0.0",
"@types/request-promise": "^4.1.42", "@types/request-promise": "^4.1.42",
"@types/shortid": "0.0.29",
"@types/sinon": "^9.0.4", "@types/sinon": "^9.0.4",
"@types/webpack": "^4.4.24", "@types/webpack": "^4.4.24",
"awesome-typescript-loader": "^3.4.1", "awesome-typescript-loader": "^3.4.1",

View File

@ -1,5 +1,6 @@
import { expect } from "chai"; import { expect } from "chai";
import { convertInchesToTwip, convertMillimetersToTwip } from "./convenience-functions";
import { convertInchesToTwip, convertMillimetersToTwip, uniqueId, uniqueNumericId } from "./convenience-functions";
describe("Utility", () => { describe("Utility", () => {
describe("#convertMillimetersToTwip", () => { describe("#convertMillimetersToTwip", () => {
@ -15,4 +16,18 @@ describe("Utility", () => {
expect(convertInchesToTwip(0.25)).to.equal(360); expect(convertInchesToTwip(0.25)).to.equal(360);
}); });
}); });
describe("#uniqueNumericId", () => {
it("should generate a unique ID", () => {
// tslint:disable-next-line: no-unused-expression
expect(uniqueNumericId()).to.not.be.empty;
});
});
describe("#uniqueId", () => {
it("should call the underlying header's addChildElement", () => {
// tslint:disable-next-line: no-unused-expression
expect(uniqueId()).to.not.be.empty;
});
});
}); });

View File

@ -1,3 +1,7 @@
import { customAlphabet, nanoid } from "nanoid/non-secure";
const numericNanoId = customAlphabet("0123456789", 15);
// Twip - twentieths of a point // Twip - twentieths of a point
export const convertMillimetersToTwip = (millimeters: number): number => { export const convertMillimetersToTwip = (millimeters: number): number => {
return Math.floor((millimeters / 25.4) * 72 * 20); return Math.floor((millimeters / 25.4) * 72 * 20);
@ -6,3 +10,11 @@ export const convertMillimetersToTwip = (millimeters: number): number => {
export const convertInchesToTwip = (inches: number): number => { export const convertInchesToTwip = (inches: number): number => {
return Math.floor(inches * 72 * 20); return Math.floor(inches * 72 * 20);
}; };
export const uniqueNumericId = (): number => {
return parseFloat(numericNanoId());
};
export const uniqueId = (): string => {
return nanoid().toLowerCase();
};

View File

@ -5,11 +5,10 @@ export class NumberingReplacer {
let currentXmlData = xmlData; let currentXmlData = xmlData;
for (const concreteNumbering of concreteNumberings) { for (const concreteNumbering of concreteNumberings) {
if (!concreteNumbering.reference) { currentXmlData = currentXmlData.replace(
continue; new RegExp(`{${concreteNumbering.reference}-${concreteNumbering.instance}}`, "g"),
} concreteNumbering.numId.toString(),
);
currentXmlData = currentXmlData.replace(new RegExp(`{${concreteNumbering.reference}}`, "g"), concreteNumbering.id.toString());
} }
return currentXmlData; return currentXmlData;

View File

@ -1,7 +1,8 @@
// tslint:disable:object-literal-key-quotes // tslint:disable:object-literal-key-quotes
import { expect } from "chai"; import { expect } from "chai";
import { stub } from "sinon"; import { SinonStub, stub } from "sinon";
import * as convenienceFunctions from "convenience-functions";
import { Formatter } from "export/formatter"; import { Formatter } from "export/formatter";
import { File } from "../file"; import { File } from "../file";
@ -9,6 +10,14 @@ import { Paragraph } from "../paragraph";
import { Media } from "./media"; import { Media } from "./media";
describe("Media", () => { describe("Media", () => {
before(() => {
stub(convenienceFunctions, "uniqueId").callsFake(() => "test");
});
after(() => {
(convenienceFunctions.uniqueId as SinonStub).restore();
});
describe("#addImage", () => { describe("#addImage", () => {
it("should add image", () => { it("should add image", () => {
const file = new File(); const file = new File();
@ -23,7 +32,6 @@ describe("Media", () => {
it("should ensure the correct relationship id is used when adding image", () => { it("should ensure the correct relationship id is used when adding image", () => {
// tslint:disable-next-line:no-any // tslint:disable-next-line:no-any
stub(Media as any, "generateId").callsFake(() => "testId");
const file = new File(); const file = new File();
const image1 = Media.addImage(file, "test"); const image1 = Media.addImage(file, "test");
@ -33,7 +41,7 @@ describe("Media", () => {
expect(graphicData["a:graphic"][1]["a:graphicData"][1]["pic:pic"][2]["pic:blipFill"][0]["a:blip"]).to.deep.equal({ expect(graphicData["a:graphic"][1]["a:graphicData"][1]["pic:pic"][2]["pic:blipFill"][0]["a:blip"]).to.deep.equal({
_attr: { _attr: {
"r:embed": `rId{testId.png}`, "r:embed": `rId{test.png}`,
cstate: "none", cstate: "none",
}, },
}); });
@ -45,7 +53,7 @@ describe("Media", () => {
expect(graphicData2["a:graphic"][1]["a:graphicData"][1]["pic:pic"][2]["pic:blipFill"][0]["a:blip"]).to.deep.equal({ expect(graphicData2["a:graphic"][1]["a:graphicData"][1]["pic:pic"][2]["pic:blipFill"][0]["a:blip"]).to.deep.equal({
_attr: { _attr: {
"r:embed": `rId{testId.png}`, "r:embed": `rId{test.png}`,
cstate: "none", cstate: "none",
}, },
}); });
@ -54,9 +62,6 @@ describe("Media", () => {
describe("#addMedia", () => { describe("#addMedia", () => {
it("should add media", () => { it("should add media", () => {
// tslint:disable-next-line:no-any
(Media as any).generateId = () => "test";
const image = new Media().addMedia(""); const image = new Media().addMedia("");
expect(image.fileName).to.equal("test.png"); expect(image.fileName).to.equal("test.png");
expect(image.dimensions).to.deep.equal({ expect(image.dimensions).to.deep.equal({
@ -74,8 +79,6 @@ describe("Media", () => {
it("should return UInt8Array if atob is present", () => { it("should return UInt8Array if atob is present", () => {
// tslint:disable-next-line // tslint:disable-next-line
((process as any).atob as any) = () => "atob result"; ((process as any).atob as any) = () => "atob result";
// tslint:disable-next-line:no-any
(Media as any).generateId = () => "test";
const image = new Media().addMedia(""); const image = new Media().addMedia("");
expect(image.stream).to.be.an.instanceof(Uint8Array); expect(image.stream).to.be.an.instanceof(Uint8Array);
@ -84,8 +87,6 @@ describe("Media", () => {
it("should use data as is if its not a string", () => { it("should use data as is if its not a string", () => {
// tslint:disable-next-line // tslint:disable-next-line
((process as any).atob as any) = () => "atob result"; ((process as any).atob as any) = () => "atob result";
// tslint:disable-next-line:no-any
(Media as any).generateId = () => "test";
const image = new Media().addMedia(new Buffer("")); const image = new Media().addMedia(new Buffer(""));
expect(image.stream).to.be.an.instanceof(Uint8Array); expect(image.stream).to.be.an.instanceof(Uint8Array);
@ -94,9 +95,6 @@ describe("Media", () => {
describe("#getMedia", () => { describe("#getMedia", () => {
it("should get media", () => { it("should get media", () => {
// tslint:disable-next-line:no-any
(Media as any).generateId = () => "test";
const media = new Media(); const media = new Media();
media.addMedia(""); media.addMedia("");
@ -124,9 +122,6 @@ describe("Media", () => {
describe("#Array", () => { describe("#Array", () => {
it("Get images as array", () => { it("Get images as array", () => {
// tslint:disable-next-line:no-any
(Media as any).generateId = () => "test";
const media = new Media(); const media = new Media();
media.addMedia(""); media.addMedia("");

View File

@ -1,3 +1,5 @@
import { uniqueId } from "convenience-functions";
import { IDrawingOptions } from "../drawing"; import { IDrawingOptions } from "../drawing";
import { File } from "../file"; import { File } from "../file";
import { PictureRun } from "../paragraph"; import { PictureRun } from "../paragraph";
@ -17,11 +19,6 @@ export class Media {
return new PictureRun(mediaData, drawingOptions); return new PictureRun(mediaData, drawingOptions);
} }
private static generateId(): string {
// https://gist.github.com/6174/6062387
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
}
private readonly map: Map<string, IMediaData>; private readonly map: Map<string, IMediaData>;
constructor() { constructor() {
@ -39,7 +36,7 @@ export class Media {
} }
public addMedia(buffer: Buffer | string | Uint8Array | ArrayBuffer, width: number = 100, height: number = 100): IMediaData { public addMedia(buffer: Buffer | string | Uint8Array | ArrayBuffer, width: number = 100, height: number = 100): IMediaData {
const key = `${Media.generateId()}.png`; const key = `${uniqueId()}.png`;
return this.createMedia( return this.createMedia(
key, key,

View File

@ -3,12 +3,10 @@ import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
import { ILevelsOptions, Level } from "./level"; import { ILevelsOptions, Level } from "./level";
import { MultiLevelType } from "./multi-level-type"; import { MultiLevelType } from "./multi-level-type";
interface IAbstractNumberingAttributesProperties { class AbstractNumberingAttributes extends XmlAttributeComponent<{
readonly abstractNumId?: number; readonly abstractNumId: number;
readonly restartNumberingAfterBreak?: number; readonly restartNumberingAfterBreak: number;
} }> {
class AbstractNumberingAttributes extends XmlAttributeComponent<IAbstractNumberingAttributesProperties> {
protected readonly xmlKeys = { protected readonly xmlKeys = {
abstractNumId: "w:abstractNumId", abstractNumId: "w:abstractNumId",
restartNumberingAfterBreak: "w15:restartNumberingAfterBreak", restartNumberingAfterBreak: "w15:restartNumberingAfterBreak",

View File

@ -2,35 +2,59 @@ import { expect } from "chai";
import { Formatter } from "export/formatter"; import { Formatter } from "export/formatter";
import { LevelForOverride } from "./level";
import { ConcreteNumbering } from "./num"; import { ConcreteNumbering } from "./num";
describe("ConcreteNumbering", () => { describe("ConcreteNumbering", () => {
describe("#overrideLevel", () => { describe("#overrideLevel", () => {
let concreteNumbering: ConcreteNumbering;
beforeEach(() => {
concreteNumbering = new ConcreteNumbering(0, 1);
});
it("sets a new override level for the given level number", () => { it("sets a new override level for the given level number", () => {
concreteNumbering.overrideLevel(3); const concreteNumbering = new ConcreteNumbering({
numId: 0,
abstractNumId: 1,
reference: "1",
instance: 0,
overrideLevel: {
num: 3,
},
});
const tree = new Formatter().format(concreteNumbering); const tree = new Formatter().format(concreteNumbering);
expect(tree["w:num"]).to.include({
"w:lvlOverride": [ expect(tree).to.deep.equal({
{ _attr: { "w:ilvl": 3 } }, "w:num": [
{ {
"w:lvl": [ _attr: {
{ _attr: { "w:ilvl": 3, "w15:tentative": 1 } }, "w:numId": 0,
{ "w:start": { _attr: { "w:val": 1 } } }, },
{ "w:lvlJc": { _attr: { "w:val": "start" } } }, },
], {
"w:abstractNumId": {
_attr: {
"w:val": 1,
},
},
},
{
"w:lvlOverride": {
_attr: {
"w:ilvl": 3,
},
},
}, },
], ],
}); });
}); });
it("sets the startOverride element if start is given", () => { it("sets the startOverride element if start is given", () => {
concreteNumbering.overrideLevel(1, 9); const concreteNumbering = new ConcreteNumbering({
numId: 0,
abstractNumId: 1,
reference: "1",
instance: 0,
overrideLevel: {
num: 1,
start: 9,
},
});
const tree = new Formatter().format(concreteNumbering); const tree = new Formatter().format(concreteNumbering);
expect(tree["w:num"]).to.include({ expect(tree["w:num"]).to.include({
"w:lvlOverride": [ "w:lvlOverride": [
@ -46,31 +70,41 @@ describe("ConcreteNumbering", () => {
}, },
}, },
}, },
{
"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", () => { it("sets the lvl element if overrideLevel.Level is accessed", () => {
const ol = concreteNumbering.overrideLevel(1); const concreteNumbering = new ConcreteNumbering({
expect(ol.Level).to.be.instanceof(LevelForOverride); numId: 0,
abstractNumId: 1,
reference: "1",
instance: 0,
overrideLevel: {
num: 1,
},
});
const tree = new Formatter().format(concreteNumbering); const tree = new Formatter().format(concreteNumbering);
expect(tree).to.deep.equal({
expect(tree["w:num"]).to.include({ "w:num": [
"w:lvlOverride": [
{ _attr: { "w:ilvl": 1 } },
{ {
"w:lvl": [ _attr: {
{ _attr: { "w:ilvl": 1, "w15:tentative": 1 } }, "w:numId": 0,
{ "w:start": { _attr: { "w:val": 1 } } }, },
{ "w:lvlJc": { _attr: { "w:val": "start" } } }, },
], {
"w:abstractNumId": {
_attr: {
"w:val": 1,
},
},
},
{
"w:lvlOverride": {
_attr: {
"w:ilvl": 1,
},
},
}, },
], ],
}); });

View File

@ -1,5 +1,4 @@
import { Attributes, XmlAttributeComponent, XmlComponent } from "file/xml-components"; import { Attributes, XmlAttributeComponent, XmlComponent } from "file/xml-components";
import { LevelForOverride } from "./level";
class AbstractNumId extends XmlComponent { class AbstractNumId extends XmlComponent {
constructor(value: number) { constructor(value: number) {
@ -12,32 +11,46 @@ class AbstractNumId extends XmlComponent {
} }
} }
interface INumAttributesProperties { class NumAttributes extends XmlAttributeComponent<{
readonly numId: number; readonly numId: number;
} }> {
class NumAttributes extends XmlAttributeComponent<INumAttributesProperties> {
protected readonly xmlKeys = { numId: "w:numId" }; protected readonly xmlKeys = { numId: "w:numId" };
} }
export class ConcreteNumbering extends XmlComponent { export interface IConcreteNumberingOptions {
public readonly id: number; readonly numId: number;
readonly abstractNumId: number;
readonly reference: string;
readonly instance: number;
readonly overrideLevel?: {
readonly num: number;
readonly start?: number;
};
}
constructor(numId: number, abstractNumId: number, public readonly reference?: string) { export class ConcreteNumbering extends XmlComponent {
public readonly numId: number;
public readonly reference: string;
public readonly instance: number;
constructor(options: IConcreteNumberingOptions) {
super("w:num"); super("w:num");
this.numId = options.numId;
this.reference = options.reference;
this.instance = options.instance;
this.root.push( this.root.push(
new NumAttributes({ new NumAttributes({
numId: numId, numId: options.numId,
}), }),
); );
this.root.push(new AbstractNumId(abstractNumId));
this.id = numId;
}
public overrideLevel(num: number, start?: number): LevelOverride { this.root.push(new AbstractNumId(options.abstractNumId));
const olvl = new LevelOverride(num, start);
this.root.push(olvl); if (options.overrideLevel) {
return olvl; this.root.push(new LevelOverride(options.overrideLevel.num, options.overrideLevel.start));
}
} }
} }
@ -46,23 +59,12 @@ class LevelOverrideAttributes extends XmlAttributeComponent<{ readonly ilvl: num
} }
export class LevelOverride extends XmlComponent { export class LevelOverride extends XmlComponent {
private readonly lvl: LevelForOverride; constructor(levelNum: number, start?: number) {
constructor(private readonly levelNum: number, start?: number) {
super("w:lvlOverride"); super("w:lvlOverride");
this.root.push(new LevelOverrideAttributes({ ilvl: levelNum })); this.root.push(new LevelOverrideAttributes({ ilvl: levelNum }));
if (start !== undefined) { if (start !== undefined) {
this.root.push(new StartOverride(start)); this.root.push(new StartOverride(start));
} }
this.lvl = new LevelForOverride({
level: this.levelNum,
});
this.root.push(this.lvl);
}
public get Level(): LevelForOverride {
return this.lvl;
} }
} }

View File

@ -1,10 +1,20 @@
import { expect } from "chai"; import { expect } from "chai";
import { SinonStub, stub } from "sinon";
import * as convenienceFunctions from "convenience-functions";
import { Formatter } from "export/formatter"; import { Formatter } from "export/formatter";
import { Numbering } from "./numbering"; import { Numbering } from "./numbering";
describe("Numbering", () => { describe("Numbering", () => {
before(() => {
stub(convenienceFunctions, "uniqueNumericId").callsFake(() => 0);
});
after(() => {
(convenienceFunctions.uniqueNumericId as SinonStub).restore();
});
describe("#constructor", () => { describe("#constructor", () => {
it("creates a default numbering with one abstract and one concrete instance", () => { it("creates a default numbering with one abstract and one concrete instance", () => {
const numbering = new Numbering({ const numbering = new Numbering({
@ -38,5 +48,69 @@ describe("Numbering", () => {
// {"w:ind": [{"_attr": {"w:left": 720, "w:hanging": 360}}]}]}, // {"w:ind": [{"_attr": {"w:left": 720, "w:hanging": 360}}]}]},
}); });
}); });
describe("#createConcreteNumberingInstance", () => {
it("should create a concrete numbering instance", () => {
const numbering = new Numbering({
config: [
{
reference: "test-reference",
levels: [
{
level: 0,
},
],
},
],
});
expect(numbering.ConcreteNumbering).to.have.length(1);
numbering.createConcreteNumberingInstance("test-reference", 0);
expect(numbering.ConcreteNumbering).to.have.length(2);
});
it("should not create a concrete numbering instance if reference is invalid", () => {
const numbering = new Numbering({
config: [
{
reference: "test-reference",
levels: [
{
level: 0,
},
],
},
],
});
expect(numbering.ConcreteNumbering).to.have.length(1);
numbering.createConcreteNumberingInstance("invalid-reference", 0);
expect(numbering.ConcreteNumbering).to.have.length(1);
});
it("should not create a concrete numbering instance if one already exists", () => {
const numbering = new Numbering({
config: [
{
reference: "test-reference",
levels: [
{
level: 0,
},
],
},
],
});
expect(numbering.ConcreteNumbering).to.have.length(1);
numbering.createConcreteNumberingInstance("test-reference", 0);
numbering.createConcreteNumberingInstance("test-reference", 0);
expect(numbering.ConcreteNumbering).to.have.length(2);
});
});
}); });
}); });

View File

@ -1,5 +1,6 @@
// http://officeopenxml.com/WPnumbering.php // http://officeopenxml.com/WPnumbering.php
import { convertInchesToTwip } from "convenience-functions"; // https://stackoverflow.com/questions/58622437/purpose-of-abstractnum-and-numberinginstance
import { convertInchesToTwip, uniqueNumericId } from "convenience-functions";
import { AlignmentType } from "file/paragraph"; import { AlignmentType } from "file/paragraph";
import { IContext, IXmlableObject, XmlComponent } from "file/xml-components"; import { IContext, IXmlableObject, XmlComponent } from "file/xml-components";
@ -16,11 +17,8 @@ export interface INumberingOptions {
} }
export class Numbering extends XmlComponent { export class Numbering extends XmlComponent {
// tslint:disable-next-line:readonly-keyword private readonly abstractNumberingMap = new Map<string, AbstractNumbering>();
private nextId: number; private readonly concreteNumberingMap = new Map<string, ConcreteNumbering>();
private readonly abstractNumbering: AbstractNumbering[] = [];
private readonly concreteNumbering: ConcreteNumbering[] = [];
constructor(options: INumberingOptions) { constructor(options: INumberingOptions) {
super("w:numbering"); super("w:numbering");
@ -46,9 +44,7 @@ export class Numbering extends XmlComponent {
}), }),
); );
this.nextId = 0; const abstractNumbering = new AbstractNumbering(uniqueNumericId(), [
const abstractNumbering = this.createAbstractNumbering([
{ {
level: 0, level: 0,
format: LevelFormat.BULLET, format: LevelFormat.BULLET,
@ -150,33 +146,67 @@ export class Numbering extends XmlComponent {
}, },
]); ]);
this.createConcreteNumbering(abstractNumbering); this.concreteNumberingMap.set(
"default-bullet-numbering",
new ConcreteNumbering({
numId: 0,
abstractNumId: abstractNumbering.id,
reference: "default-bullet-numbering",
instance: 0,
overrideLevel: {
num: 0,
start: 1,
},
}),
);
this.abstractNumberingMap.set("default-bullet-numbering", abstractNumbering);
for (const con of options.config) { for (const con of options.config) {
const currentAbstractNumbering = this.createAbstractNumbering(con.levels); this.abstractNumberingMap.set(con.reference, new AbstractNumbering(uniqueNumericId(), con.levels));
this.createConcreteNumbering(currentAbstractNumbering, con.reference);
} }
} }
public prepForXml(context: IContext): IXmlableObject | undefined { public prepForXml(context: IContext): IXmlableObject | undefined {
this.abstractNumbering.forEach((x) => this.root.push(x)); for (const numbering of this.abstractNumberingMap.values()) {
this.concreteNumbering.forEach((x) => this.root.push(x)); this.root.push(numbering);
}
for (const numbering of this.concreteNumberingMap.values()) {
this.root.push(numbering);
}
return super.prepForXml(context); return super.prepForXml(context);
} }
private createConcreteNumbering(abstractNumbering: AbstractNumbering, reference?: string): ConcreteNumbering { public createConcreteNumberingInstance(reference: string, instance: number): void {
const num = new ConcreteNumbering(this.nextId++, abstractNumbering.id, reference); const abstractNumbering = this.abstractNumberingMap.get(reference);
this.concreteNumbering.push(num);
return num;
}
private createAbstractNumbering(options: ILevelsOptions[]): AbstractNumbering { if (!abstractNumbering) {
const num = new AbstractNumbering(this.nextId++, options); return;
this.abstractNumbering.push(num); }
return num;
const fullReference = `${reference}-${instance}`;
if (this.concreteNumberingMap.has(fullReference)) {
return;
}
this.concreteNumberingMap.set(
fullReference,
new ConcreteNumbering({
numId: uniqueNumericId(),
abstractNumId: abstractNumbering.id,
reference,
instance,
overrideLevel: {
num: 0,
start: 1,
},
}),
);
} }
public get ConcreteNumbering(): ConcreteNumbering[] { public get ConcreteNumbering(): ConcreteNumbering[] {
return this.concreteNumbering; return Array.from(this.concreteNumberingMap.values());
} }
} }

View File

@ -1,6 +1,7 @@
// http://officeopenxml.com/WPbookmark.php // http://officeopenxml.com/WPbookmark.php
import { uniqueId } from "convenience-functions";
import { XmlComponent } from "file/xml-components"; import { XmlComponent } from "file/xml-components";
import * as shortid from "shortid";
import { TextRun } from "../run"; import { TextRun } from "../run";
import { BookmarkEndAttributes, BookmarkStartAttributes } from "./bookmark-attributes"; import { BookmarkEndAttributes, BookmarkStartAttributes } from "./bookmark-attributes";
@ -10,7 +11,7 @@ export class Bookmark {
public readonly end: BookmarkEnd; public readonly end: BookmarkEnd;
constructor(options: { readonly id: string; readonly children: TextRun[] }) { constructor(options: { readonly id: string; readonly children: TextRun[] }) {
const linkId = shortid.generate().toLowerCase(); const linkId = uniqueId();
this.start = new BookmarkStart(options.id, linkId); this.start = new BookmarkStart(options.id, linkId);
this.children = options.children; this.children = options.children;

View File

@ -1,6 +1,5 @@
// http://officeopenxml.com/WPhyperlink.php // http://officeopenxml.com/WPhyperlink.php
import * as shortid from "shortid"; import { uniqueId } from "convenience-functions";
import { XmlComponent } from "file/xml-components"; import { XmlComponent } from "file/xml-components";
import { ParagraphChild } from "../paragraph"; import { ParagraphChild } from "../paragraph";
@ -33,7 +32,7 @@ export class ConcreteHyperlink extends XmlComponent {
export class InternalHyperlink extends ConcreteHyperlink { export class InternalHyperlink extends ConcreteHyperlink {
constructor(options: { readonly child: ParagraphChild; readonly anchor: string }) { constructor(options: { readonly child: ParagraphChild; readonly anchor: string }) {
super(options.child, shortid.generate().toLowerCase(), options.anchor); super(options.child, uniqueId(), options.anchor);
} }
} }

View File

@ -1,7 +1,7 @@
import { assert, expect } from "chai"; import { assert, expect } from "chai";
import * as shortid from "shortid"; import { SinonStub, stub } from "sinon";
import { stub } from "sinon";
import * as convenienceFunctions from "convenience-functions";
import { Formatter } from "export/formatter"; import { Formatter } from "export/formatter";
import { EMPTY_OBJECT } from "file/xml-components"; import { EMPTY_OBJECT } from "file/xml-components";
@ -14,6 +14,16 @@ import { Paragraph } from "./paragraph";
import { TextRun } from "./run"; import { TextRun } from "./run";
describe("Paragraph", () => { describe("Paragraph", () => {
before(() => {
stub(convenienceFunctions, "uniqueId").callsFake(() => {
return "test-unique-id";
});
});
after(() => {
(convenienceFunctions.uniqueId as SinonStub).restore();
});
describe("#constructor()", () => { describe("#constructor()", () => {
it("should create valid JSON", () => { it("should create valid JSON", () => {
const paragraph = new Paragraph(""); const paragraph = new Paragraph("");
@ -603,6 +613,7 @@ describe("Paragraph", () => {
numbering: { numbering: {
reference: "test id", reference: "test id",
level: 0, level: 0,
instance: 4,
}, },
}); });
const tree = new Formatter().format(paragraph); const tree = new Formatter().format(paragraph);
@ -612,7 +623,7 @@ describe("Paragraph", () => {
"w:pPr": [ "w:pPr": [
{ "w:pStyle": { _attr: { "w:val": "ListParagraph" } } }, { "w:pStyle": { _attr: { "w:val": "ListParagraph" } } },
{ {
"w:numPr": [{ "w:ilvl": { _attr: { "w:val": 0 } } }, { "w:numId": { _attr: { "w:val": "{test id}" } } }], "w:numPr": [{ "w:ilvl": { _attr: { "w:val": 0 } } }, { "w:numId": { _attr: { "w:val": "{test id-4}" } } }],
}, },
], ],
}, },
@ -634,7 +645,7 @@ describe("Paragraph", () => {
{ {
"w:pPr": [ "w:pPr": [
{ {
"w:numPr": [{ "w:ilvl": { _attr: { "w:val": 0 } } }, { "w:numId": { _attr: { "w:val": "{test id}" } } }], "w:numPr": [{ "w:ilvl": { _attr: { "w:val": 0 } } }, { "w:numId": { _attr: { "w:val": "{test id-0}" } } }],
}, },
], ],
}, },
@ -644,9 +655,6 @@ describe("Paragraph", () => {
}); });
it("it should add bookmark", () => { it("it should add bookmark", () => {
stub(shortid, "generate").callsFake(() => {
return "test-unique-id";
});
const paragraph = new Paragraph({ const paragraph = new Paragraph({
children: [ children: [
new Bookmark({ new Bookmark({

View File

@ -1,6 +1,6 @@
// http://officeopenxml.com/WPparagraph.php // http://officeopenxml.com/WPparagraph.php
import * as shortid from "shortid";
import { uniqueId } from "convenience-functions";
import { FootnoteReferenceRun } from "file/footnotes/footnote/run/reference-run"; import { FootnoteReferenceRun } from "file/footnotes/footnote/run/reference-run";
import { IContext, IXmlableObject, XmlComponent } from "file/xml-components"; import { IContext, IXmlableObject, XmlComponent } from "file/xml-components";
@ -79,7 +79,7 @@ export class Paragraph extends XmlComponent {
for (const element of this.root) { for (const element of this.root) {
if (element instanceof ExternalHyperlink) { if (element instanceof ExternalHyperlink) {
const index = this.root.indexOf(element); const index = this.root.indexOf(element);
const concreteHyperlink = new ConcreteHyperlink(element.options.child, shortid.generate().toLowerCase()); const concreteHyperlink = new ConcreteHyperlink(element.options.child, uniqueId());
context.viewWrapper.Relationships.createRelationship( context.viewWrapper.Relationships.createRelationship(
concreteHyperlink.linkId, concreteHyperlink.linkId,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",

View File

@ -0,0 +1,69 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { DocumentWrapper } from "../document-wrapper";
import { File } from "../file";
import { ParagraphProperties } from "./properties";
describe("ParagraphProperties", () => {
describe("#constructor()", () => {
it("creates an initially empty property object", () => {
const properties = new ParagraphProperties();
expect(() => new Formatter().format(properties)).to.throw("XMLComponent did not format correctly");
});
it("should create", () => {
const properties = new ParagraphProperties({
numbering: {
reference: "test-reference",
level: 0,
instance: 0,
},
});
const tree = new Formatter().format(properties, {
// tslint:disable-next-line: no-object-literal-type-assertion
file: {
Numbering: {
createConcreteNumberingInstance: (_: string, __: number) => {
return;
},
},
} as File,
// tslint:disable-next-line: no-object-literal-type-assertion
viewWrapper: new DocumentWrapper({ background: {} }),
});
expect(tree).to.deep.equal({
"w:pPr": [
{
"w:pStyle": {
_attr: {
"w:val": "ListParagraph",
},
},
},
{
"w:numPr": [
{
"w:ilvl": {
_attr: {
"w:val": 0,
},
},
},
{
"w:numId": {
_attr: {
"w:val": "{test-reference-0}",
},
},
},
],
},
],
});
});
});
});

View File

@ -1,5 +1,6 @@
// http://officeopenxml.com/WPparagraphProperties.php // http://officeopenxml.com/WPparagraphProperties.php
import { IgnoreIfEmptyXmlComponent, XmlComponent } from "file/xml-components"; import { IContext, IgnoreIfEmptyXmlComponent, IXmlableObject, XmlComponent } from "file/xml-components";
import { DocumentWrapper } from "../document-wrapper";
import { ShadingType } from "../table/shading"; import { ShadingType } from "../table/shading";
import { Alignment, AlignmentType } from "./formatting/alignment"; import { Alignment, AlignmentType } from "./formatting/alignment";
import { Bidirectional } from "./formatting/bidirectional"; import { Bidirectional } from "./formatting/bidirectional";
@ -44,6 +45,7 @@ export interface IParagraphPropertiesOptions extends IParagraphStylePropertiesOp
readonly numbering?: { readonly numbering?: {
readonly reference: string; readonly reference: string;
readonly level: number; readonly level: number;
readonly instance?: number;
readonly custom?: boolean; readonly custom?: boolean;
}; };
readonly shading?: { readonly shading?: {
@ -54,6 +56,8 @@ export interface IParagraphPropertiesOptions extends IParagraphStylePropertiesOp
} }
export class ParagraphProperties extends IgnoreIfEmptyXmlComponent { export class ParagraphProperties extends IgnoreIfEmptyXmlComponent {
private readonly numberingReferences: { readonly reference: string; readonly instance: number }[] = [];
constructor(options?: IParagraphPropertiesOptions) { constructor(options?: IParagraphPropertiesOptions) {
super("w:pPr"); super("w:pPr");
@ -128,7 +132,12 @@ export class ParagraphProperties extends IgnoreIfEmptyXmlComponent {
if (!options.numbering.custom) { if (!options.numbering.custom) {
this.push(new Style("ListParagraph")); this.push(new Style("ListParagraph"));
} }
this.push(new NumberProperties(options.numbering.reference, options.numbering.level)); this.numberingReferences.push({
reference: options.numbering.reference,
instance: options.numbering.instance ?? 0,
});
this.push(new NumberProperties(`${options.numbering.reference}-${options.numbering.instance ?? 0}`, options.numbering.level));
} }
if (options.rightTabStop) { if (options.rightTabStop) {
@ -147,4 +156,14 @@ export class ParagraphProperties extends IgnoreIfEmptyXmlComponent {
public push(item: XmlComponent): void { public push(item: XmlComponent): void {
this.root.push(item); this.root.push(item);
} }
public prepForXml(context: IContext): IXmlableObject | undefined {
if (context.viewWrapper instanceof DocumentWrapper) {
for (const reference of this.numberingReferences) {
context.file.Numbering.createConcreteNumberingInstance(reference.reference, reference.instance);
}
}
return super.prepForXml(context);
}
} }

12
src/index.spec.ts Normal file
View File

@ -0,0 +1,12 @@
import { expect } from "chai";
import { Document } from "./index";
describe("Index", () => {
describe("Document", () => {
it("should instantiate the Document", () => {
// tslint:disable-next-line: no-unused-expression
expect(new Document()).to.be.ok;
});
});
});