@ -1,7 +1,8 @@
|
||||
// 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 * 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({
|
||||
numbering: {
|
||||
@ -125,11 +126,16 @@ doc.addSection({
|
||||
level: 0,
|
||||
},
|
||||
}),
|
||||
new Paragraph({
|
||||
text: "Next",
|
||||
heading: HeadingLevel.HEADING_2,
|
||||
}),
|
||||
new Paragraph({
|
||||
text: "test",
|
||||
numbering: {
|
||||
reference: "padded-numbering-reference",
|
||||
level: 0,
|
||||
instance: 2,
|
||||
},
|
||||
}),
|
||||
new Paragraph({
|
||||
@ -137,6 +143,19 @@ doc.addSection({
|
||||
numbering: {
|
||||
reference: "padded-numbering-reference",
|
||||
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({
|
||||
@ -144,6 +163,7 @@ doc.addSection({
|
||||
numbering: {
|
||||
reference: "padded-numbering-reference",
|
||||
level: 0,
|
||||
instance: 3,
|
||||
},
|
||||
}),
|
||||
new Paragraph({
|
||||
@ -151,14 +171,12 @@ doc.addSection({
|
||||
numbering: {
|
||||
reference: "padded-numbering-reference",
|
||||
level: 0,
|
||||
instance: 3,
|
||||
},
|
||||
}),
|
||||
new Paragraph({
|
||||
text: "test",
|
||||
numbering: {
|
||||
reference: "padded-numbering-reference",
|
||||
level: 0,
|
||||
},
|
||||
text: "Next",
|
||||
heading: HeadingLevel.HEADING_2,
|
||||
}),
|
||||
new Paragraph({
|
||||
text: "test",
|
||||
|
22
package-lock.json
generated
22
package-lock.json
generated
@ -605,12 +605,6 @@
|
||||
"@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": {
|
||||
"version": "9.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-9.0.11.tgz",
|
||||
@ -4875,7 +4869,7 @@
|
||||
},
|
||||
"jsesc": {
|
||||
"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=",
|
||||
"dev": true
|
||||
},
|
||||
@ -5592,9 +5586,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "2.1.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz",
|
||||
"integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA=="
|
||||
"version": "3.1.20",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz",
|
||||
"integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw=="
|
||||
},
|
||||
"nanomatch": {
|
||||
"version": "1.2.13",
|
||||
@ -7360,14 +7354,6 @@
|
||||
"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": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
|
||||
|
@ -52,7 +52,7 @@
|
||||
"@types/jszip": "^3.1.4",
|
||||
"@types/node": "^14.0.5",
|
||||
"jszip": "^3.1.5",
|
||||
"shortid": "^2.2.15",
|
||||
"nanoid": "^3.1.20",
|
||||
"xml": "^1.0.1",
|
||||
"xml-js": "^1.6.8"
|
||||
},
|
||||
@ -66,7 +66,6 @@
|
||||
"@types/chai": "^4.2.15",
|
||||
"@types/mocha": "^8.0.0",
|
||||
"@types/request-promise": "^4.1.42",
|
||||
"@types/shortid": "0.0.29",
|
||||
"@types/sinon": "^9.0.4",
|
||||
"@types/webpack": "^4.4.24",
|
||||
"awesome-typescript-loader": "^3.4.1",
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { expect } from "chai";
|
||||
import { convertInchesToTwip, convertMillimetersToTwip } from "./convenience-functions";
|
||||
|
||||
import { convertInchesToTwip, convertMillimetersToTwip, uniqueId, uniqueNumericId } from "./convenience-functions";
|
||||
|
||||
describe("Utility", () => {
|
||||
describe("#convertMillimetersToTwip", () => {
|
||||
@ -15,4 +16,18 @@ describe("Utility", () => {
|
||||
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;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,3 +1,7 @@
|
||||
import { customAlphabet, nanoid } from "nanoid/non-secure";
|
||||
|
||||
const numericNanoId = customAlphabet("0123456789", 15);
|
||||
|
||||
// Twip - twentieths of a point
|
||||
export const convertMillimetersToTwip = (millimeters: number): number => {
|
||||
return Math.floor((millimeters / 25.4) * 72 * 20);
|
||||
@ -6,3 +10,11 @@ export const convertMillimetersToTwip = (millimeters: number): number => {
|
||||
export const convertInchesToTwip = (inches: number): number => {
|
||||
return Math.floor(inches * 72 * 20);
|
||||
};
|
||||
|
||||
export const uniqueNumericId = (): number => {
|
||||
return parseFloat(numericNanoId());
|
||||
};
|
||||
|
||||
export const uniqueId = (): string => {
|
||||
return nanoid().toLowerCase();
|
||||
};
|
||||
|
@ -5,11 +5,10 @@ export class NumberingReplacer {
|
||||
let currentXmlData = xmlData;
|
||||
|
||||
for (const concreteNumbering of concreteNumberings) {
|
||||
if (!concreteNumbering.reference) {
|
||||
continue;
|
||||
}
|
||||
|
||||
currentXmlData = currentXmlData.replace(new RegExp(`{${concreteNumbering.reference}}`, "g"), concreteNumbering.id.toString());
|
||||
currentXmlData = currentXmlData.replace(
|
||||
new RegExp(`{${concreteNumbering.reference}-${concreteNumbering.instance}}`, "g"),
|
||||
concreteNumbering.numId.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
return currentXmlData;
|
||||
|
@ -1,7 +1,8 @@
|
||||
// tslint:disable:object-literal-key-quotes
|
||||
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 { File } from "../file";
|
||||
@ -9,6 +10,14 @@ import { Paragraph } from "../paragraph";
|
||||
import { Media } from "./media";
|
||||
|
||||
describe("Media", () => {
|
||||
before(() => {
|
||||
stub(convenienceFunctions, "uniqueId").callsFake(() => "test");
|
||||
});
|
||||
|
||||
after(() => {
|
||||
(convenienceFunctions.uniqueId as SinonStub).restore();
|
||||
});
|
||||
|
||||
describe("#addImage", () => {
|
||||
it("should add image", () => {
|
||||
const file = new File();
|
||||
@ -23,7 +32,6 @@ describe("Media", () => {
|
||||
|
||||
it("should ensure the correct relationship id is used when adding image", () => {
|
||||
// tslint:disable-next-line:no-any
|
||||
stub(Media as any, "generateId").callsFake(() => "testId");
|
||||
|
||||
const file = new File();
|
||||
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({
|
||||
_attr: {
|
||||
"r:embed": `rId{testId.png}`,
|
||||
"r:embed": `rId{test.png}`,
|
||||
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({
|
||||
_attr: {
|
||||
"r:embed": `rId{testId.png}`,
|
||||
"r:embed": `rId{test.png}`,
|
||||
cstate: "none",
|
||||
},
|
||||
});
|
||||
@ -54,9 +62,6 @@ describe("Media", () => {
|
||||
|
||||
describe("#addMedia", () => {
|
||||
it("should add media", () => {
|
||||
// tslint:disable-next-line:no-any
|
||||
(Media as any).generateId = () => "test";
|
||||
|
||||
const image = new Media().addMedia("");
|
||||
expect(image.fileName).to.equal("test.png");
|
||||
expect(image.dimensions).to.deep.equal({
|
||||
@ -74,8 +79,6 @@ describe("Media", () => {
|
||||
it("should return UInt8Array if atob is present", () => {
|
||||
// tslint:disable-next-line
|
||||
((process as any).atob as any) = () => "atob result";
|
||||
// tslint:disable-next-line:no-any
|
||||
(Media as any).generateId = () => "test";
|
||||
|
||||
const image = new Media().addMedia("");
|
||||
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", () => {
|
||||
// tslint:disable-next-line
|
||||
((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(""));
|
||||
expect(image.stream).to.be.an.instanceof(Uint8Array);
|
||||
@ -94,9 +95,6 @@ describe("Media", () => {
|
||||
|
||||
describe("#getMedia", () => {
|
||||
it("should get media", () => {
|
||||
// tslint:disable-next-line:no-any
|
||||
(Media as any).generateId = () => "test";
|
||||
|
||||
const media = new Media();
|
||||
media.addMedia("");
|
||||
|
||||
@ -124,9 +122,6 @@ describe("Media", () => {
|
||||
|
||||
describe("#Array", () => {
|
||||
it("Get images as array", () => {
|
||||
// tslint:disable-next-line:no-any
|
||||
(Media as any).generateId = () => "test";
|
||||
|
||||
const media = new Media();
|
||||
media.addMedia("");
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { uniqueId } from "convenience-functions";
|
||||
|
||||
import { IDrawingOptions } from "../drawing";
|
||||
import { File } from "../file";
|
||||
import { PictureRun } from "../paragraph";
|
||||
@ -17,11 +19,6 @@ export class Media {
|
||||
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>;
|
||||
|
||||
constructor() {
|
||||
@ -39,7 +36,7 @@ export class Media {
|
||||
}
|
||||
|
||||
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(
|
||||
key,
|
||||
|
@ -3,12 +3,10 @@ import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
|
||||
import { ILevelsOptions, Level } from "./level";
|
||||
import { MultiLevelType } from "./multi-level-type";
|
||||
|
||||
interface IAbstractNumberingAttributesProperties {
|
||||
readonly abstractNumId?: number;
|
||||
readonly restartNumberingAfterBreak?: number;
|
||||
}
|
||||
|
||||
class AbstractNumberingAttributes extends XmlAttributeComponent<IAbstractNumberingAttributesProperties> {
|
||||
class AbstractNumberingAttributes extends XmlAttributeComponent<{
|
||||
readonly abstractNumId: number;
|
||||
readonly restartNumberingAfterBreak: number;
|
||||
}> {
|
||||
protected readonly xmlKeys = {
|
||||
abstractNumId: "w:abstractNumId",
|
||||
restartNumberingAfterBreak: "w15:restartNumberingAfterBreak",
|
||||
|
@ -2,35 +2,59 @@ import { expect } from "chai";
|
||||
|
||||
import { Formatter } from "export/formatter";
|
||||
|
||||
import { LevelForOverride } from "./level";
|
||||
import { ConcreteNumbering } from "./num";
|
||||
|
||||
describe("ConcreteNumbering", () => {
|
||||
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 concreteNumbering = new ConcreteNumbering({
|
||||
numId: 0,
|
||||
abstractNumId: 1,
|
||||
reference: "1",
|
||||
instance: 0,
|
||||
overrideLevel: {
|
||||
num: 3,
|
||||
},
|
||||
});
|
||||
|
||||
const tree = new Formatter().format(concreteNumbering);
|
||||
expect(tree["w:num"]).to.include({
|
||||
"w:lvlOverride": [
|
||||
{ _attr: { "w:ilvl": 3 } },
|
||||
|
||||
expect(tree).to.deep.equal({
|
||||
"w:num": [
|
||||
{
|
||||
"w:lvl": [
|
||||
{ _attr: { "w:ilvl": 3, "w15:tentative": 1 } },
|
||||
{ "w:start": { _attr: { "w:val": 1 } } },
|
||||
{ "w:lvlJc": { _attr: { "w:val": "start" } } },
|
||||
],
|
||||
_attr: {
|
||||
"w:numId": 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"w:abstractNumId": {
|
||||
_attr: {
|
||||
"w:val": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"w:lvlOverride": {
|
||||
_attr: {
|
||||
"w:ilvl": 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
expect(tree["w:num"]).to.include({
|
||||
"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", () => {
|
||||
const ol = concreteNumbering.overrideLevel(1);
|
||||
expect(ol.Level).to.be.instanceof(LevelForOverride);
|
||||
const concreteNumbering = new ConcreteNumbering({
|
||||
numId: 0,
|
||||
abstractNumId: 1,
|
||||
reference: "1",
|
||||
instance: 0,
|
||||
overrideLevel: {
|
||||
num: 1,
|
||||
},
|
||||
});
|
||||
const tree = new Formatter().format(concreteNumbering);
|
||||
|
||||
expect(tree["w:num"]).to.include({
|
||||
"w:lvlOverride": [
|
||||
{ _attr: { "w:ilvl": 1 } },
|
||||
expect(tree).to.deep.equal({
|
||||
"w:num": [
|
||||
{
|
||||
"w:lvl": [
|
||||
{ _attr: { "w:ilvl": 1, "w15:tentative": 1 } },
|
||||
{ "w:start": { _attr: { "w:val": 1 } } },
|
||||
{ "w:lvlJc": { _attr: { "w:val": "start" } } },
|
||||
],
|
||||
_attr: {
|
||||
"w:numId": 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"w:abstractNumId": {
|
||||
_attr: {
|
||||
"w:val": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"w:lvlOverride": {
|
||||
_attr: {
|
||||
"w:ilvl": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Attributes, XmlAttributeComponent, XmlComponent } from "file/xml-components";
|
||||
import { LevelForOverride } from "./level";
|
||||
|
||||
class AbstractNumId extends XmlComponent {
|
||||
constructor(value: number) {
|
||||
@ -12,32 +11,46 @@ class AbstractNumId extends XmlComponent {
|
||||
}
|
||||
}
|
||||
|
||||
interface INumAttributesProperties {
|
||||
class NumAttributes extends XmlAttributeComponent<{
|
||||
readonly numId: number;
|
||||
}
|
||||
|
||||
class NumAttributes extends XmlAttributeComponent<INumAttributesProperties> {
|
||||
}> {
|
||||
protected readonly xmlKeys = { numId: "w:numId" };
|
||||
}
|
||||
|
||||
export class ConcreteNumbering extends XmlComponent {
|
||||
public readonly id: number;
|
||||
export interface IConcreteNumberingOptions {
|
||||
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");
|
||||
|
||||
this.numId = options.numId;
|
||||
this.reference = options.reference;
|
||||
this.instance = options.instance;
|
||||
|
||||
this.root.push(
|
||||
new NumAttributes({
|
||||
numId: numId,
|
||||
numId: options.numId,
|
||||
}),
|
||||
);
|
||||
this.root.push(new AbstractNumId(abstractNumId));
|
||||
this.id = numId;
|
||||
}
|
||||
|
||||
public overrideLevel(num: number, start?: number): LevelOverride {
|
||||
const olvl = new LevelOverride(num, start);
|
||||
this.root.push(olvl);
|
||||
return olvl;
|
||||
this.root.push(new AbstractNumId(options.abstractNumId));
|
||||
|
||||
if (options.overrideLevel) {
|
||||
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 {
|
||||
private readonly lvl: LevelForOverride;
|
||||
|
||||
constructor(private readonly levelNum: number, start?: number) {
|
||||
constructor(levelNum: number, start?: number) {
|
||||
super("w:lvlOverride");
|
||||
this.root.push(new LevelOverrideAttributes({ ilvl: levelNum }));
|
||||
if (start !== undefined) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,20 @@
|
||||
import { expect } from "chai";
|
||||
import { SinonStub, stub } from "sinon";
|
||||
|
||||
import * as convenienceFunctions from "convenience-functions";
|
||||
import { Formatter } from "export/formatter";
|
||||
|
||||
import { Numbering } from "./numbering";
|
||||
|
||||
describe("Numbering", () => {
|
||||
before(() => {
|
||||
stub(convenienceFunctions, "uniqueNumericId").callsFake(() => 0);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
(convenienceFunctions.uniqueNumericId as SinonStub).restore();
|
||||
});
|
||||
|
||||
describe("#constructor", () => {
|
||||
it("creates a default numbering with one abstract and one concrete instance", () => {
|
||||
const numbering = new Numbering({
|
||||
@ -38,5 +48,69 @@ describe("Numbering", () => {
|
||||
// {"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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
// 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 { IContext, IXmlableObject, XmlComponent } from "file/xml-components";
|
||||
|
||||
@ -16,11 +17,8 @@ export interface INumberingOptions {
|
||||
}
|
||||
|
||||
export class Numbering extends XmlComponent {
|
||||
// tslint:disable-next-line:readonly-keyword
|
||||
private nextId: number;
|
||||
|
||||
private readonly abstractNumbering: AbstractNumbering[] = [];
|
||||
private readonly concreteNumbering: ConcreteNumbering[] = [];
|
||||
private readonly abstractNumberingMap = new Map<string, AbstractNumbering>();
|
||||
private readonly concreteNumberingMap = new Map<string, ConcreteNumbering>();
|
||||
|
||||
constructor(options: INumberingOptions) {
|
||||
super("w:numbering");
|
||||
@ -46,9 +44,7 @@ export class Numbering extends XmlComponent {
|
||||
}),
|
||||
);
|
||||
|
||||
this.nextId = 0;
|
||||
|
||||
const abstractNumbering = this.createAbstractNumbering([
|
||||
const abstractNumbering = new AbstractNumbering(uniqueNumericId(), [
|
||||
{
|
||||
level: 0,
|
||||
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) {
|
||||
const currentAbstractNumbering = this.createAbstractNumbering(con.levels);
|
||||
this.createConcreteNumbering(currentAbstractNumbering, con.reference);
|
||||
this.abstractNumberingMap.set(con.reference, new AbstractNumbering(uniqueNumericId(), con.levels));
|
||||
}
|
||||
}
|
||||
|
||||
public prepForXml(context: IContext): IXmlableObject | undefined {
|
||||
this.abstractNumbering.forEach((x) => this.root.push(x));
|
||||
this.concreteNumbering.forEach((x) => this.root.push(x));
|
||||
for (const numbering of this.abstractNumberingMap.values()) {
|
||||
this.root.push(numbering);
|
||||
}
|
||||
|
||||
for (const numbering of this.concreteNumberingMap.values()) {
|
||||
this.root.push(numbering);
|
||||
}
|
||||
return super.prepForXml(context);
|
||||
}
|
||||
|
||||
private createConcreteNumbering(abstractNumbering: AbstractNumbering, reference?: string): ConcreteNumbering {
|
||||
const num = new ConcreteNumbering(this.nextId++, abstractNumbering.id, reference);
|
||||
this.concreteNumbering.push(num);
|
||||
return num;
|
||||
}
|
||||
public createConcreteNumberingInstance(reference: string, instance: number): void {
|
||||
const abstractNumbering = this.abstractNumberingMap.get(reference);
|
||||
|
||||
private createAbstractNumbering(options: ILevelsOptions[]): AbstractNumbering {
|
||||
const num = new AbstractNumbering(this.nextId++, options);
|
||||
this.abstractNumbering.push(num);
|
||||
return num;
|
||||
if (!abstractNumbering) {
|
||||
return;
|
||||
}
|
||||
|
||||
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[] {
|
||||
return this.concreteNumbering;
|
||||
return Array.from(this.concreteNumberingMap.values());
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
// http://officeopenxml.com/WPbookmark.php
|
||||
import { uniqueId } from "convenience-functions";
|
||||
import { XmlComponent } from "file/xml-components";
|
||||
import * as shortid from "shortid";
|
||||
|
||||
import { TextRun } from "../run";
|
||||
import { BookmarkEndAttributes, BookmarkStartAttributes } from "./bookmark-attributes";
|
||||
|
||||
@ -10,7 +11,7 @@ export class Bookmark {
|
||||
public readonly end: BookmarkEnd;
|
||||
|
||||
constructor(options: { readonly id: string; readonly children: TextRun[] }) {
|
||||
const linkId = shortid.generate().toLowerCase();
|
||||
const linkId = uniqueId();
|
||||
|
||||
this.start = new BookmarkStart(options.id, linkId);
|
||||
this.children = options.children;
|
||||
|
@ -1,6 +1,5 @@
|
||||
// http://officeopenxml.com/WPhyperlink.php
|
||||
import * as shortid from "shortid";
|
||||
|
||||
import { uniqueId } from "convenience-functions";
|
||||
import { XmlComponent } from "file/xml-components";
|
||||
|
||||
import { ParagraphChild } from "../paragraph";
|
||||
@ -33,7 +32,7 @@ export class ConcreteHyperlink extends XmlComponent {
|
||||
|
||||
export class InternalHyperlink extends ConcreteHyperlink {
|
||||
constructor(options: { readonly child: ParagraphChild; readonly anchor: string }) {
|
||||
super(options.child, shortid.generate().toLowerCase(), options.anchor);
|
||||
super(options.child, uniqueId(), options.anchor);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { assert, expect } from "chai";
|
||||
import * as shortid from "shortid";
|
||||
import { stub } from "sinon";
|
||||
import { SinonStub, stub } from "sinon";
|
||||
|
||||
import * as convenienceFunctions from "convenience-functions";
|
||||
import { Formatter } from "export/formatter";
|
||||
import { EMPTY_OBJECT } from "file/xml-components";
|
||||
|
||||
@ -14,6 +14,16 @@ import { Paragraph } from "./paragraph";
|
||||
import { TextRun } from "./run";
|
||||
|
||||
describe("Paragraph", () => {
|
||||
before(() => {
|
||||
stub(convenienceFunctions, "uniqueId").callsFake(() => {
|
||||
return "test-unique-id";
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
(convenienceFunctions.uniqueId as SinonStub).restore();
|
||||
});
|
||||
|
||||
describe("#constructor()", () => {
|
||||
it("should create valid JSON", () => {
|
||||
const paragraph = new Paragraph("");
|
||||
@ -603,6 +613,7 @@ describe("Paragraph", () => {
|
||||
numbering: {
|
||||
reference: "test id",
|
||||
level: 0,
|
||||
instance: 4,
|
||||
},
|
||||
});
|
||||
const tree = new Formatter().format(paragraph);
|
||||
@ -612,7 +623,7 @@ describe("Paragraph", () => {
|
||||
"w:pPr": [
|
||||
{ "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: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", () => {
|
||||
stub(shortid, "generate").callsFake(() => {
|
||||
return "test-unique-id";
|
||||
});
|
||||
const paragraph = new Paragraph({
|
||||
children: [
|
||||
new Bookmark({
|
||||
|
@ -1,6 +1,6 @@
|
||||
// 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 { IContext, IXmlableObject, XmlComponent } from "file/xml-components";
|
||||
|
||||
@ -79,7 +79,7 @@ export class Paragraph extends XmlComponent {
|
||||
for (const element of this.root) {
|
||||
if (element instanceof ExternalHyperlink) {
|
||||
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(
|
||||
concreteHyperlink.linkId,
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
|
||||
|
69
src/file/paragraph/properties.spec.ts
Normal file
69
src/file/paragraph/properties.spec.ts
Normal 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}",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,5 +1,6 @@
|
||||
// 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 { Alignment, AlignmentType } from "./formatting/alignment";
|
||||
import { Bidirectional } from "./formatting/bidirectional";
|
||||
@ -44,6 +45,7 @@ export interface IParagraphPropertiesOptions extends IParagraphStylePropertiesOp
|
||||
readonly numbering?: {
|
||||
readonly reference: string;
|
||||
readonly level: number;
|
||||
readonly instance?: number;
|
||||
readonly custom?: boolean;
|
||||
};
|
||||
readonly shading?: {
|
||||
@ -54,6 +56,8 @@ export interface IParagraphPropertiesOptions extends IParagraphStylePropertiesOp
|
||||
}
|
||||
|
||||
export class ParagraphProperties extends IgnoreIfEmptyXmlComponent {
|
||||
private readonly numberingReferences: { readonly reference: string; readonly instance: number }[] = [];
|
||||
|
||||
constructor(options?: IParagraphPropertiesOptions) {
|
||||
super("w:pPr");
|
||||
|
||||
@ -128,7 +132,12 @@ export class ParagraphProperties extends IgnoreIfEmptyXmlComponent {
|
||||
if (!options.numbering.custom) {
|
||||
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) {
|
||||
@ -147,4 +156,14 @@ export class ParagraphProperties extends IgnoreIfEmptyXmlComponent {
|
||||
public push(item: XmlComponent): void {
|
||||
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
12
src/index.spec.ts
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user