Optimize XML output by properly constructing objects to send to the xml library so that it can produce proper empty elements.
Rework the way attributes are stored in ImportedXmlComponent to match elsewhere (required allowing for a null xmlKeys in the XmlAttributeComponent interface). Rework the way paragraphs get added to the end of table cells if needed. The goal in both reworks is to not mess around with the objects output from `prepForXml` if we can avoid it. Made the output of RunProperties, ParagraphProperties, TableCellProperties, TableRowProperties, and TableProperties all optional based on whether they contain any attributes or children. Changed code in PageBorders, TableCellMargin, and TableCellBorders that implemented this same thing by overriding `prepForXml` so that it uses the new XmlComponent subclass instead. Removed commented out code that attempted to fix-up XML output and make proper empty elements. Fixed all affected tests. Turn off `no-null-keyword` in the linter as we need to use null to signal to the `xml` library to create an empty element with no attributes (`undefined` will not work in its place). Fixes #306
This commit is contained in:
@ -6,7 +6,7 @@ export type AttributeMap<T> = { [P in keyof T]: string };
|
||||
export abstract class XmlAttributeComponent<T> extends BaseXmlComponent {
|
||||
// tslint:disable-next-line:readonly-keyword
|
||||
protected root: T;
|
||||
protected readonly xmlKeys: AttributeMap<T>;
|
||||
protected readonly xmlKeys?: AttributeMap<T>;
|
||||
|
||||
constructor(properties: T) {
|
||||
super("_attr");
|
||||
@ -18,7 +18,7 @@ export abstract class XmlAttributeComponent<T> extends BaseXmlComponent {
|
||||
Object.keys(this.root).forEach((key) => {
|
||||
const value = this.root[key];
|
||||
if (value !== undefined) {
|
||||
const newKey = this.xmlKeys[key];
|
||||
const newKey = (this.xmlKeys && this.xmlKeys[key]) || key;
|
||||
attrs[newKey] = value;
|
||||
}
|
||||
});
|
||||
|
@ -25,11 +25,25 @@ const convertedXmlElement = {
|
||||
deleted: false,
|
||||
rootKey: "w:p",
|
||||
root: [
|
||||
{ deleted: false, rootKey: "_attr", root: { "w:one": "value 1", "w:two": "value 2" } },
|
||||
{ deleted: false, rootKey: "w:rPr", root: [{ deleted: false, rootKey: "w:noProof", root: ["some value"] }] },
|
||||
{ deleted: false, rootKey: "w:r", root: [{ deleted: false, rootKey: "w:t", root: ["Text 1"] }], _attr: { active: "true" } },
|
||||
{ deleted: false, rootKey: "w:r", root: [{ deleted: false, rootKey: "w:t", root: ["Text 2"] }], _attr: { active: "true" } },
|
||||
{
|
||||
deleted: false,
|
||||
rootKey: "w:r",
|
||||
root: [
|
||||
{ deleted: false, rootKey: "_attr", root: { active: "true" } },
|
||||
{ deleted: false, rootKey: "w:t", root: ["Text 1"] },
|
||||
],
|
||||
},
|
||||
{
|
||||
deleted: false,
|
||||
rootKey: "w:r",
|
||||
root: [
|
||||
{ deleted: false, rootKey: "_attr", root: { active: "true" } },
|
||||
{ deleted: false, rootKey: "w:t", root: ["Text 2"] },
|
||||
],
|
||||
},
|
||||
],
|
||||
_attr: { "w:one": "value 1", "w:two": "value 2" },
|
||||
},
|
||||
],
|
||||
rootKey: undefined,
|
||||
@ -59,7 +73,7 @@ describe("ImportedXmlComponent", () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
"w:child": [],
|
||||
"w:child": null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
// tslint:disable:no-any
|
||||
import { Element as XmlElement, xml2js } from "xml-js";
|
||||
import { IXmlableObject, XmlComponent } from ".";
|
||||
import { IXmlableObject, XmlAttributeComponent, XmlComponent } from ".";
|
||||
|
||||
/**
|
||||
* Converts the given xml element (in json format) into XmlComponent.
|
||||
@ -27,6 +27,10 @@ export function convertToXmlComponent(element: XmlElement): ImportedXmlComponent
|
||||
}
|
||||
}
|
||||
|
||||
class ImportedXmlComponentAttributes extends XmlAttributeComponent<any> {
|
||||
// noop
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents imported xml component from xml file.
|
||||
*/
|
||||
@ -46,58 +50,14 @@ export class ImportedXmlComponent extends XmlComponent {
|
||||
* @param importedContent xml content of the imported component
|
||||
*/
|
||||
|
||||
// tslint:disable-next-line:variable-name
|
||||
private readonly _attr: any;
|
||||
|
||||
// tslint:disable-next-line:variable-name
|
||||
constructor(rootKey: string, _attr?: any) {
|
||||
super(rootKey);
|
||||
if (_attr) {
|
||||
this._attr = _attr;
|
||||
this.root.push(new ImportedXmlComponentAttributes(_attr));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the object so it can be converted to xml. Example:
|
||||
* <w:someKey someAttr="1" otherAttr="11">
|
||||
* <w:child childAttr="2">
|
||||
* </w:child>
|
||||
* </w:someKey>
|
||||
* {
|
||||
* 'w:someKey': [
|
||||
* {
|
||||
* _attr: {
|
||||
* someAttr: "1",
|
||||
* otherAttr: "11"
|
||||
* }
|
||||
* },
|
||||
* {
|
||||
* 'w:child': [
|
||||
* {
|
||||
* _attr: {
|
||||
* childAttr: "2"
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
public prepForXml(): IXmlableObject | undefined {
|
||||
const result = super.prepForXml();
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!!this._attr) {
|
||||
if (!Array.isArray(result[this.rootKey])) {
|
||||
result[this.rootKey] = [result[this.rootKey]];
|
||||
}
|
||||
result[this.rootKey].unshift({ _attr: this._attr });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public push(xmlComponent: XmlComponent | string): void {
|
||||
this.root.push(xmlComponent);
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ describe("XmlComponent", () => {
|
||||
return;
|
||||
}
|
||||
|
||||
assert.equal(xml["w:test"].length, 0);
|
||||
assert.isNull(xml["w:test"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -17,7 +17,7 @@ export abstract class XmlComponent extends BaseXmlComponent {
|
||||
if (c instanceof BaseXmlComponent) {
|
||||
return !c.IsDeleted;
|
||||
}
|
||||
return true;
|
||||
return c !== undefined;
|
||||
})
|
||||
.map((comp) => {
|
||||
if (comp instanceof BaseXmlComponent) {
|
||||
@ -26,8 +26,15 @@ export abstract class XmlComponent extends BaseXmlComponent {
|
||||
return comp;
|
||||
})
|
||||
.filter((comp) => comp !== undefined); // Exclude undefined
|
||||
// If we only have a single IXmlableObject in our children array and it
|
||||
// represents our attributes, use the object itself as our children to
|
||||
// avoid an unneeded XML close element. (Note: We have to use this
|
||||
// function to get typescript to allow our check.)
|
||||
// Additionally, if the array is empty, use null as our children to
|
||||
// get an empty XML element generated.
|
||||
const onlyAttrs = (c) => typeof c === "object" && c._attr;
|
||||
return {
|
||||
[this.rootKey]: children,
|
||||
[this.rootKey]: children.length ? (children.length === 1 && onlyAttrs(children[0]) ? children[0] : children) : null,
|
||||
};
|
||||
}
|
||||
|
||||
@ -41,3 +48,12 @@ export abstract class XmlComponent extends BaseXmlComponent {
|
||||
this.deleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class IgnoreIfEmptyXmlComponent extends XmlComponent {
|
||||
public prepForXml(): IXmlableObject | undefined {
|
||||
const result = super.prepForXml();
|
||||
if (result && result[this.rootKey]) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user