#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

@ -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", () => {
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,
},
},
},
],
});

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

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