#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
// 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
View File

@ -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",

View File

@ -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",

View File

@ -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;
});
});
});

View File

@ -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();
};

View File

@ -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;

View File

@ -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("");

View File

@ -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,

View File

@ -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",

View File

@ -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", () => {
const concreteNumbering = new ConcreteNumbering({
numId: 0,
abstractNumId: 1,
reference: "1",
instance: 0,
overrideLevel: {
num: 3,
},
});
it("sets a new override level for the given level number", () => {
concreteNumbering.overrideLevel(3);
const tree = new Formatter().format(concreteNumbering);
expect(tree["w:num"]).to.include({
"w:lvlOverride": [
{ _attr: { "w:ilvl": 3 } },
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,
},
},
},
],
});

View File

@ -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;
constructor(numId: number, abstractNumId: number, public readonly reference?: string) {
super("w:num");
this.root.push(
new NumAttributes({
numId: numId,
}),
);
this.root.push(new AbstractNumId(abstractNumId));
this.id = numId;
export interface IConcreteNumberingOptions {
readonly numId: number;
readonly abstractNumId: number;
readonly reference: string;
readonly instance: number;
readonly overrideLevel?: {
readonly num: number;
readonly start?: number;
};
}
public overrideLevel(num: number, start?: number): LevelOverride {
const olvl = new LevelOverride(num, start);
this.root.push(olvl);
return olvl;
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: options.numId,
}),
);
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;
}
}

View File

@ -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);
});
});
});
});

View File

@ -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);
if (!abstractNumbering) {
return;
}
private createAbstractNumbering(options: ILevelsOptions[]): AbstractNumbering {
const num = new AbstractNumbering(this.nextId++, options);
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[] {
return this.concreteNumbering;
return Array.from(this.concreteNumberingMap.values());
}
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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({

View File

@ -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",

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
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
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;
});
});
});