easy-template-x 7.2.3 → 7.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -543,7 +543,13 @@ class Parser {
543
543
  return '';
544
544
  });
545
545
  }
546
- serializeNode(node) {
546
+ serializeNode(node, options) {
547
+ return this._serializeNode(node, 0, options);
548
+ }
549
+ serializeFile(xmlNode) {
550
+ return Parser.xmlFileHeader + xml.parser.serializeNode(xmlNode);
551
+ }
552
+ _serializeNode(node, depth, options) {
547
553
  if (!node) return '';
548
554
  if (xml.query.isTextNode(node)) return xml.parser.encodeValue(node.textContent || '');
549
555
  if (xml.query.isCommentNode(node)) {
@@ -563,21 +569,27 @@ class Parser {
563
569
  const hasChildren = (node.childNodes || []).length > 0;
564
570
  const suffix = hasChildren ? '' : '/';
565
571
  const openTag = `<${node.nodeName}${attributes}${suffix}>`;
566
- let xmlString;
567
- if (hasChildren) {
568
- // child nodes
569
- const childrenXml = node.childNodes.map(child => xml.parser.serializeNode(child)).join('');
570
-
571
- // close tag
572
- const closeTag = `</${node.nodeName}>`;
573
- xmlString = openTag + childrenXml + closeTag;
574
- } else {
575
- xmlString = openTag;
572
+
573
+ // No children
574
+ if (!hasChildren) {
575
+ return openTag;
576
576
  }
577
- return xmlString;
578
- }
579
- serializeFile(xmlNode) {
580
- return Parser.xmlFileHeader + xml.parser.serializeNode(xmlNode);
577
+
578
+ // Close tag
579
+ const closeTag = `</${node.nodeName}>`;
580
+
581
+ // Children without indentation
582
+ const indentSize = options?.indent ?? 0;
583
+ if (!indentSize || node.childNodes.every(xml.query.isTextNode)) {
584
+ const childrenXml = node.childNodes.map(child => this._serializeNode(child, depth + 1, options)).join('');
585
+ return openTag + childrenXml + closeTag;
586
+ }
587
+
588
+ // Children with indentation
589
+ const childIndent = "\n" + " ".repeat((depth + 1) * indentSize);
590
+ const childrenXml = node.childNodes.map(child => `${childIndent}${this._serializeNode(child, depth + 1, options)}`).join("");
591
+ const closeIndent = "\n" + " ".repeat(depth * indentSize);
592
+ return openTag + childrenXml + closeIndent + closeTag;
581
593
  }
582
594
  }
583
595
  class Create {
@@ -908,19 +920,19 @@ let Modify$1 = class Modify {
908
920
  removeChild(parent, childOrIndex) {
909
921
  if (!parent) throw new InternalArgumentMissingError("parent");
910
922
  if (childOrIndex === null || childOrIndex === undefined) throw new InternalArgumentMissingError("childOrIndex");
911
- if (!parent.childNodes || !parent.childNodes.length) throw new Error('Parent node has node children');
923
+ if (!parent.childNodes || !parent.childNodes.length) throw new Error('Parent node has no children');
912
924
 
913
- // get child index
925
+ // Get child index
914
926
  let childIndex;
915
927
  if (typeof childOrIndex === 'number') {
916
928
  childIndex = childOrIndex;
917
929
  } else {
918
930
  childIndex = parent.childNodes.indexOf(childOrIndex);
919
- if (childIndex === -1) throw new Error('Specified child node is not a child of the specified parent');
931
+ if (childIndex === -1) throw new Error('Selected child node is not a child of the specified parent');
920
932
  }
921
933
  if (childIndex >= parent.childNodes.length) throw new RangeError(`Child index ${childIndex} is out of range. Parent has only ${parent.childNodes.length} child nodes.`);
922
934
 
923
- // update references
935
+ // Update references
924
936
  const child = parent.childNodes[childIndex];
925
937
  if (childIndex > 0) {
926
938
  const beforeChild = parent.childNodes[childIndex - 1];
@@ -929,7 +941,7 @@ let Modify$1 = class Modify {
929
941
  child.parentNode = null;
930
942
  child.nextSibling = null;
931
943
 
932
- // remove and return
944
+ // Remove and return
933
945
  return parent.childNodes.splice(childIndex, 1)[0];
934
946
  }
935
947
  removeChildren(parent, predicate) {
@@ -5312,7 +5324,7 @@ class TemplateHandler {
5312
5324
  /**
5313
5325
  * Version number of the `easy-template-x` library.
5314
5326
  */
5315
- version = "7.2.3" ;
5327
+ version = "7.2.4" ;
5316
5328
  constructor(options) {
5317
5329
  this.options = new TemplateHandlerOptions(options);
5318
5330
  const delimiters = this.options.delimiters;
@@ -541,7 +541,13 @@ class Parser {
541
541
  return '';
542
542
  });
543
543
  }
544
- serializeNode(node) {
544
+ serializeNode(node, options) {
545
+ return this._serializeNode(node, 0, options);
546
+ }
547
+ serializeFile(xmlNode) {
548
+ return Parser.xmlFileHeader + xml.parser.serializeNode(xmlNode);
549
+ }
550
+ _serializeNode(node, depth, options) {
545
551
  if (!node) return '';
546
552
  if (xml.query.isTextNode(node)) return xml.parser.encodeValue(node.textContent || '');
547
553
  if (xml.query.isCommentNode(node)) {
@@ -561,21 +567,27 @@ class Parser {
561
567
  const hasChildren = (node.childNodes || []).length > 0;
562
568
  const suffix = hasChildren ? '' : '/';
563
569
  const openTag = `<${node.nodeName}${attributes}${suffix}>`;
564
- let xmlString;
565
- if (hasChildren) {
566
- // child nodes
567
- const childrenXml = node.childNodes.map(child => xml.parser.serializeNode(child)).join('');
568
-
569
- // close tag
570
- const closeTag = `</${node.nodeName}>`;
571
- xmlString = openTag + childrenXml + closeTag;
572
- } else {
573
- xmlString = openTag;
570
+
571
+ // No children
572
+ if (!hasChildren) {
573
+ return openTag;
574
574
  }
575
- return xmlString;
576
- }
577
- serializeFile(xmlNode) {
578
- return Parser.xmlFileHeader + xml.parser.serializeNode(xmlNode);
575
+
576
+ // Close tag
577
+ const closeTag = `</${node.nodeName}>`;
578
+
579
+ // Children without indentation
580
+ const indentSize = options?.indent ?? 0;
581
+ if (!indentSize || node.childNodes.every(xml.query.isTextNode)) {
582
+ const childrenXml = node.childNodes.map(child => this._serializeNode(child, depth + 1, options)).join('');
583
+ return openTag + childrenXml + closeTag;
584
+ }
585
+
586
+ // Children with indentation
587
+ const childIndent = "\n" + " ".repeat((depth + 1) * indentSize);
588
+ const childrenXml = node.childNodes.map(child => `${childIndent}${this._serializeNode(child, depth + 1, options)}`).join("");
589
+ const closeIndent = "\n" + " ".repeat(depth * indentSize);
590
+ return openTag + childrenXml + closeIndent + closeTag;
579
591
  }
580
592
  }
581
593
  class Create {
@@ -906,19 +918,19 @@ let Modify$1 = class Modify {
906
918
  removeChild(parent, childOrIndex) {
907
919
  if (!parent) throw new InternalArgumentMissingError("parent");
908
920
  if (childOrIndex === null || childOrIndex === undefined) throw new InternalArgumentMissingError("childOrIndex");
909
- if (!parent.childNodes || !parent.childNodes.length) throw new Error('Parent node has node children');
921
+ if (!parent.childNodes || !parent.childNodes.length) throw new Error('Parent node has no children');
910
922
 
911
- // get child index
923
+ // Get child index
912
924
  let childIndex;
913
925
  if (typeof childOrIndex === 'number') {
914
926
  childIndex = childOrIndex;
915
927
  } else {
916
928
  childIndex = parent.childNodes.indexOf(childOrIndex);
917
- if (childIndex === -1) throw new Error('Specified child node is not a child of the specified parent');
929
+ if (childIndex === -1) throw new Error('Selected child node is not a child of the specified parent');
918
930
  }
919
931
  if (childIndex >= parent.childNodes.length) throw new RangeError(`Child index ${childIndex} is out of range. Parent has only ${parent.childNodes.length} child nodes.`);
920
932
 
921
- // update references
933
+ // Update references
922
934
  const child = parent.childNodes[childIndex];
923
935
  if (childIndex > 0) {
924
936
  const beforeChild = parent.childNodes[childIndex - 1];
@@ -927,7 +939,7 @@ let Modify$1 = class Modify {
927
939
  child.parentNode = null;
928
940
  child.nextSibling = null;
929
941
 
930
- // remove and return
942
+ // Remove and return
931
943
  return parent.childNodes.splice(childIndex, 1)[0];
932
944
  }
933
945
  removeChildren(parent, predicate) {
@@ -5310,7 +5322,7 @@ class TemplateHandler {
5310
5322
  /**
5311
5323
  * Version number of the `easy-template-x` library.
5312
5324
  */
5313
- version = "7.2.3" ;
5325
+ version = "7.2.4" ;
5314
5326
  constructor(options) {
5315
5327
  this.options = new TemplateHandlerOptions(options);
5316
5328
  const delimiters = this.options.delimiters;
@@ -1,4 +1,3 @@
1
- import type { IMap } from "src/types";
2
1
  import type { XmlGeneralNode, XmlTextNode } from "src/xml";
3
2
  export declare const TagDisposition: Readonly<{
4
3
  readonly Open: "Open";
@@ -14,7 +13,7 @@ export type TagPlacement = typeof TagPlacement[keyof typeof TagPlacement];
14
13
  export type Tag = TextNodeTag | AttributeTag;
15
14
  export interface BaseTag {
16
15
  name: string;
17
- options?: IMap<any>;
16
+ options?: Record<string, any>;
18
17
  rawText: string;
19
18
  disposition: TagDisposition;
20
19
  }
@@ -1,4 +1,3 @@
1
- export type IMap<T> = Record<string, T>;
2
1
  export interface Constructor<T> {
3
2
  new (...args: any[]): T;
4
3
  }
@@ -1,6 +1,5 @@
1
- import { IMap } from '../types';
2
1
  export type ItemMapper<TIn, TOut = string> = (item: TIn, index: number) => TOut;
3
2
  export declare function pushMany<T>(destArray: T[], items: T[]): void;
4
3
  export declare function first<T>(array: T[]): T;
5
4
  export declare function last<T>(array: T[]): T;
6
- export declare function toDictionary<TIn, TOut = TIn>(array: TIn[], keySelector: ItemMapper<TIn>, valueSelector?: ItemMapper<TIn, TOut>): IMap<TOut>;
5
+ export declare function toDictionary<TIn, TOut = TIn>(array: TIn[], keySelector: ItemMapper<TIn>, valueSelector?: ItemMapper<TIn, TOut>): Record<string, TOut>;
@@ -1,7 +1,6 @@
1
1
  import { XmlGeneralNode, XmlNode, XmlNodeType } from "./xmlNode";
2
2
  import { XmlCommentNode } from "./xmlNode";
3
3
  import { XmlTextNode } from "./xmlNode";
4
- import type { IMap } from "src/types";
5
4
  export type NodeTypeToNode<T extends XmlNodeType> = T extends typeof XmlNodeType.Text ? XmlTextNode : T extends typeof XmlNodeType.Comment ? XmlCommentNode : T extends typeof XmlNodeType.General ? XmlGeneralNode : XmlNode;
6
5
  export type XmlNodePredicate = (node: XmlNode) => boolean;
7
6
  export declare class XmlUtils {
@@ -10,17 +9,21 @@ export declare class XmlUtils {
10
9
  readonly query: Query;
11
10
  readonly modify: Modify;
12
11
  }
12
+ export interface XmlSerializationOptions {
13
+ indent?: number;
14
+ }
13
15
  declare class Parser {
14
16
  private static xmlFileHeader;
15
17
  private static readonly parser;
16
18
  parse(str: string): XmlNode;
17
19
  domParse(str: string): Document;
18
20
  encodeValue(str: string): string;
19
- serializeNode(node: XmlNode): string;
21
+ serializeNode(node: XmlNode, options?: XmlSerializationOptions): string;
20
22
  serializeFile(xmlNode: XmlNode): string;
23
+ private _serializeNode;
21
24
  }
22
25
  interface XmlGeneralNodeInit {
23
- attributes?: IMap<string>;
26
+ attributes?: Record<string, string>;
24
27
  childNodes?: XmlNode[];
25
28
  }
26
29
  declare class Create {
@@ -1,4 +1,3 @@
1
- import { IMap } from "src/types";
2
1
  export declare const XmlNodeType: Readonly<{
3
2
  readonly Text: "Text";
4
3
  readonly General: "General";
@@ -27,5 +26,5 @@ export interface XmlCommentNode extends XmlNodeBase {
27
26
  }
28
27
  export interface XmlGeneralNode extends XmlNodeBase {
29
28
  nodeType: typeof XmlNodeType.General;
30
- attributes?: IMap<string>;
29
+ attributes?: Record<string, string>;
31
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easy-template-x",
3
- "version": "7.2.3",
3
+ "version": "7.2.4",
4
4
  "description": "Generate docx documents from templates, in Node or in the browser.",
5
5
  "keywords": [
6
6
  "docx",
@@ -49,6 +49,7 @@
49
49
  "build-src": "rollup -c",
50
50
  "build-types": "tsc -p tsconfig.types.json --emitDeclarationOnly",
51
51
  "build": "yarn build-types && yarn build-src",
52
+ "push-both": "git push && git checkout master && git merge develop && git push && git checkout develop",
52
53
  "release": "yarn clean && yarn quality && yarn build && yarn dist-verify",
53
54
  "dist-verify": "yarn dist-verify-cjs && yarn dist-verify-es",
54
55
  "dist-verify-cjs": "cd dist-verify/cjs && npm install && node main.js",
@@ -59,7 +60,7 @@
59
60
  },
60
61
  "packageManager": "yarn@4.3.1",
61
62
  "dependencies": {
62
- "@xmldom/xmldom": "0.8.12",
63
+ "@xmldom/xmldom": "0.8.13",
63
64
  "json5": "2.2.3",
64
65
  "jszip": "3.10.1",
65
66
  "lodash.get": "4.4.2"
@@ -1,4 +1,3 @@
1
- import type { IMap } from "src/types";
2
1
  import type { XmlGeneralNode, XmlTextNode } from "src/xml";
3
2
 
4
3
  export const TagDisposition = Object.freeze({
@@ -20,7 +19,7 @@ export type Tag = TextNodeTag | AttributeTag;
20
19
 
21
20
  export interface BaseTag {
22
21
  name: string;
23
- options?: IMap<any>;
22
+ options?: Record<string, any>;
24
23
  /**
25
24
  * The full tag text, for instance: "{#my-tag}".
26
25
  */
@@ -1,6 +1,5 @@
1
1
  import { UnclosedTagError, UnknownContentTypeError, UnopenedTagError } from '../errors';
2
2
  import { PluginContent, TemplatePlugin } from '../plugins';
3
- import { IMap } from '../types';
4
3
  import { isPromiseLike, stringValue, toDictionary } from '../utils';
5
4
  import { XmlNode } from '../xml';
6
5
  import { DelimiterSearcher } from './delimiters';
@@ -27,7 +26,7 @@ export interface TemplateCompilerOptions {
27
26
  */
28
27
  export class TemplateCompiler {
29
28
 
30
- private readonly pluginsLookup: IMap<TemplatePlugin>;
29
+ private readonly pluginsLookup: Record<string, TemplatePlugin>;
31
30
  private readonly delimiterSearcher: DelimiterSearcher;
32
31
  private readonly tagParser: TagParser;
33
32
  private readonly options: TemplateCompilerOptions;
@@ -1,5 +1,4 @@
1
1
  import { MimeType, MimeTypeHelper } from '../mimeType';
2
- import { IMap } from '../types';
3
2
  import { Binary, Path, sha1 } from '../utils';
4
3
  import { Zip } from '../zip';
5
4
 
@@ -10,7 +9,7 @@ export class MediaFiles {
10
9
 
11
10
  private static readonly mediaDir = 'word/media';
12
11
 
13
- private hashes: IMap<string>;
12
+ private hashes: Record<string, string>;
14
13
  private readonly files = new Map<Binary, string>();
15
14
  private nextFileId = 0;
16
15
 
@@ -1,4 +1,4 @@
1
- import { Constructor, IMap } from "src/types";
1
+ import { Constructor } from "src/types";
2
2
  import { Binary } from "src/utils";
3
3
  import { xml, XmlNode } from "src/xml";
4
4
  import { Zip } from "src/zip";
@@ -18,7 +18,7 @@ export class OpenXmlPart {
18
18
  public readonly path: string;
19
19
 
20
20
  private root: XmlNode;
21
- private readonly openedParts: IMap<OpenXmlPart> = {};
21
+ private readonly openedParts: Record<string, OpenXmlPart> = {};
22
22
  private readonly zip: Zip;
23
23
 
24
24
  constructor(path: string, zip: Zip) {
@@ -1,4 +1,3 @@
1
- import { IMap } from "src/types";
2
1
  import { Path } from "src/utils";
3
2
  import { xml, XmlGeneralNode, XmlNode } from "src/xml";
4
3
  import { Zip } from "src/zip";
@@ -11,8 +10,8 @@ import { Relationship, RelTargetMode } from "./relationship";
11
10
  */
12
11
  export class RelsFile {
13
12
 
14
- private rels: IMap<Relationship>;
15
- private relTargets: IMap<string>;
13
+ private rels: Record<string, Relationship>;
14
+ private relTargets: Record<string, string>;
16
15
  private nextRelId = 0;
17
16
 
18
17
  private readonly partDir: string;
@@ -1,5 +1,5 @@
1
1
  import { MalformedFileError } from "src/errors";
2
- import { Constructor, IMap } from "src/types";
2
+ import { Constructor } from "src/types";
3
3
  import { Binary } from "src/utils";
4
4
  import { Zip } from "src/zip";
5
5
  import { OpenXmlPart } from "./openXmlPart";
@@ -53,7 +53,7 @@ export class Xlsx {
53
53
 
54
54
  public readonly mainDocument: OpenXmlPart;
55
55
 
56
- private readonly _parts: IMap<OpenXmlPart> = {};
56
+ private readonly _parts: Record<string, OpenXmlPart> = {};
57
57
 
58
58
  private readonly zip: Zip;
59
59
 
@@ -1,6 +1,5 @@
1
1
  import { MalformedFileError, TemplateSyntaxError } from "src/errors";
2
2
  import { OmlAttribute, OpenXmlPart, RelType, Xlsx } from "src/office";
3
- import { IMap } from "src/types";
4
3
  import { xml, XmlGeneralNode, XmlNode } from "src/xml";
5
4
  import { ChartColors } from "./chartColors";
6
5
  import {
@@ -580,7 +579,7 @@ async function updateEmbeddedExcelFile(existingChart: ExistingChart, chartData:
580
579
  await xlsxPart.save(newXlsxBinary);
581
580
  }
582
581
 
583
- async function updateSharedStringsPart(workbookPart: OpenXmlPart, chartData: ChartData): Promise<IMap<number>> {
582
+ async function updateSharedStringsPart(workbookPart: OpenXmlPart, chartData: ChartData): Promise<Record<string, number>> {
584
583
 
585
584
  // Get the shared strings part
586
585
  const sharedStringsPart = await workbookPart.getFirstPartByType(RelType.SharedStrings);
@@ -595,7 +594,7 @@ async function updateSharedStringsPart(workbookPart: OpenXmlPart, chartData: Cha
595
594
  root.childNodes = [];
596
595
 
597
596
  let count = 0;
598
- const sharedStrings: IMap<number> = {};
597
+ const sharedStrings: Record<string, number> = {};
599
598
 
600
599
  function addString(str: string) {
601
600
  xml.modify.appendChild(root, xml.create.generalNode("si", {
@@ -643,7 +642,7 @@ async function updateSharedStringsPart(workbookPart: OpenXmlPart, chartData: Cha
643
642
  return sharedStrings;
644
643
  }
645
644
 
646
- async function updateSheetPart(workbookPart: OpenXmlPart, sheetName: string, sharedStrings: IMap<number>, chartData: ChartData): Promise<OpenXmlPart> {
645
+ async function updateSheetPart(workbookPart: OpenXmlPart, sheetName: string, sharedStrings: Record<string, number>, chartData: ChartData): Promise<OpenXmlPart> {
647
646
 
648
647
  // Get the sheet rel ID
649
648
  const root = await workbookPart.xmlRoot();
@@ -679,7 +678,7 @@ async function updateSheetPart(workbookPart: OpenXmlPart, sheetName: string, sha
679
678
  return sheetPart;
680
679
  }
681
680
 
682
- async function updateSheetRootStandard(workbookPart: OpenXmlPart, sheetRoot: XmlNode, chartData: StandardChartData, sharedStrings: IMap<number>): Promise<XmlNode[]> {
681
+ async function updateSheetRootStandard(workbookPart: OpenXmlPart, sheetRoot: XmlNode, chartData: StandardChartData, sharedStrings: Record<string, number>): Promise<XmlNode[]> {
683
682
 
684
683
  // Create first row
685
684
  const firstRow = `
@@ -728,7 +727,7 @@ async function updateSheetRootStandard(workbookPart: OpenXmlPart, sheetRoot: Xml
728
727
  ];
729
728
  }
730
729
 
731
- async function updateSheetRootScatter(workbookPart: OpenXmlPart, sheetRoot: XmlNode, chartData: ScatterChartData, sharedStrings: IMap<number>): Promise<XmlNode[]> {
730
+ async function updateSheetRootScatter(workbookPart: OpenXmlPart, sheetRoot: XmlNode, chartData: ScatterChartData, sharedStrings: Record<string, number>): Promise<XmlNode[]> {
732
731
 
733
732
  const isBubbleChart = isBubbleChartData(chartData);
734
733
 
package/src/types.ts CHANGED
@@ -1,6 +1,4 @@
1
1
 
2
- export type IMap<T> = Record<string, T>;
3
-
4
2
  export interface Constructor<T> {
5
3
  new(...args: any[]): T;
6
4
  }
@@ -1,4 +1,3 @@
1
- import { IMap } from '../types';
2
1
 
3
2
  export type ItemMapper<TIn, TOut = string> = (item: TIn, index: number) => TOut;
4
3
 
@@ -18,11 +17,11 @@ export function last<T>(array: T[]): T {
18
17
  return array[array.length - 1];
19
18
  }
20
19
 
21
- export function toDictionary<TIn, TOut = TIn>(array: TIn[], keySelector: ItemMapper<TIn>, valueSelector?: ItemMapper<TIn, TOut>): IMap<TOut> {
20
+ export function toDictionary<TIn, TOut = TIn>(array: TIn[], keySelector: ItemMapper<TIn>, valueSelector?: ItemMapper<TIn, TOut>): Record<string, TOut> {
22
21
  if (!array.length)
23
22
  return {};
24
23
 
25
- const res: IMap<any> = {};
24
+ const res: Record<string, any> = {};
26
25
  array.forEach((item, index) => {
27
26
  const key = keySelector(item, index);
28
27
  const value = (valueSelector ? valueSelector(item, index) : item);
@@ -39,6 +39,12 @@ describe(XmlUtils, () => {
39
39
  const str = xml.parser.serializeNode(node);
40
40
  expect(str).toEqual('<!-- comment -->');
41
41
  });
42
+
43
+ it('serializes a node with indentation', () => {
44
+ const node = parseXml('<node><child>hello</child></node>', true);
45
+ const str = xml.parser.serializeNode(node, { indent: 2 });
46
+ expect(str).toEqual('<node>\n <child>hello</child>\n</node>');
47
+ });
42
48
  });
43
49
 
44
50
  describe(xml.create.fromDomNode, () => {
package/src/xml/xml.ts CHANGED
@@ -5,7 +5,6 @@ import { COMMENT_NODE_NAME, XmlGeneralNode, XmlNode, XmlNodeType } from "./xmlNo
5
5
  import { TEXT_NODE_NAME, XmlCommentNode } from "./xmlNode";
6
6
  import { XmlTextNode } from "./xmlNode";
7
7
  import { XmlTreeIterator } from "./xmlTreeIterator";
8
- import type { IMap } from "src/types";
9
8
 
10
9
  export type NodeTypeToNode<T extends XmlNodeType> =
11
10
  T extends typeof XmlNodeType.Text ? XmlTextNode :
@@ -23,6 +22,15 @@ export class XmlUtils {
23
22
  public readonly modify = new Modify();
24
23
  }
25
24
 
25
+ export interface XmlSerializationOptions {
26
+ /**
27
+ * If specified, the XML will be serialized pretty-printed with the
28
+ * specified number of spaces for each level of indentation. Otherwise, the
29
+ * XML will be serialized in a compact format (single line).
30
+ */
31
+ indent?: number;
32
+ }
33
+
26
34
  class Parser {
27
35
 
28
36
  private static xmlFileHeader = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
@@ -75,7 +83,15 @@ class Parser {
75
83
  });
76
84
  }
77
85
 
78
- public serializeNode(node: XmlNode): string {
86
+ public serializeNode(node: XmlNode, options?: XmlSerializationOptions): string {
87
+ return this._serializeNode(node, 0, options);
88
+ }
89
+
90
+ public serializeFile(xmlNode: XmlNode): string {
91
+ return Parser.xmlFileHeader + xml.parser.serializeNode(xmlNode);
92
+ }
93
+
94
+ private _serializeNode(node: XmlNode, depth: number, options?: XmlSerializationOptions): string {
79
95
  if (!node)
80
96
  return '';
81
97
 
@@ -102,33 +118,38 @@ class Parser {
102
118
  const suffix = hasChildren ? '' : '/';
103
119
  const openTag = `<${node.nodeName}${attributes}${suffix}>`;
104
120
 
105
- let xmlString: string;
121
+ // No children
122
+ if (!hasChildren) {
123
+ return openTag;
124
+ }
125
+
126
+ // Close tag
127
+ const closeTag = `</${node.nodeName}>`;
106
128
 
107
- if (hasChildren) {
129
+ // Children without indentation
130
+ const indentSize = options?.indent ?? 0;
131
+ if (!indentSize || node.childNodes.every(xml.query.isTextNode)) {
108
132
 
109
- // child nodes
110
133
  const childrenXml = node.childNodes
111
- .map(child => xml.parser.serializeNode(child))
134
+ .map(child => this._serializeNode(child, depth + 1, options))
112
135
  .join('');
113
136
 
114
- // close tag
115
- const closeTag = `</${node.nodeName}>`;
116
-
117
- xmlString = openTag + childrenXml + closeTag;
118
- } else {
119
- xmlString = openTag;
137
+ return openTag + childrenXml + closeTag;
120
138
  }
121
139
 
122
- return xmlString;
123
- }
140
+ // Children with indentation
141
+ const childIndent = "\n" + " ".repeat((depth + 1) * indentSize);
142
+ const childrenXml = node.childNodes
143
+ .map(child => `${childIndent}${this._serializeNode(child, depth + 1, options)}`)
144
+ .join("");
124
145
 
125
- public serializeFile(xmlNode: XmlNode): string {
126
- return Parser.xmlFileHeader + xml.parser.serializeNode(xmlNode);
146
+ const closeIndent = "\n" + " ".repeat(depth * indentSize);
147
+ return openTag + childrenXml + closeIndent + closeTag;
127
148
  }
128
149
  }
129
150
 
130
151
  interface XmlGeneralNodeInit {
131
- attributes?: IMap<string>;
152
+ attributes?: Record<string, string>;
132
153
  childNodes?: XmlNode[];
133
154
  }
134
155
 
@@ -358,7 +379,7 @@ class Query {
358
379
  return null;
359
380
  }
360
381
  }
361
-
382
+
362
383
  if (curNode.nodeType !== nodeType) {
363
384
  return null;
364
385
  }
@@ -533,22 +554,22 @@ class Modify {
533
554
  throw new InternalArgumentMissingError("childOrIndex");
534
555
 
535
556
  if (!parent.childNodes || !parent.childNodes.length)
536
- throw new Error('Parent node has node children');
557
+ throw new Error('Parent node has no children');
537
558
 
538
- // get child index
559
+ // Get child index
539
560
  let childIndex: number;
540
561
  if (typeof childOrIndex === 'number') {
541
562
  childIndex = childOrIndex;
542
563
  } else {
543
564
  childIndex = parent.childNodes.indexOf(childOrIndex);
544
565
  if (childIndex === -1)
545
- throw new Error('Specified child node is not a child of the specified parent');
566
+ throw new Error('Selected child node is not a child of the specified parent');
546
567
  }
547
568
 
548
569
  if (childIndex >= parent.childNodes.length)
549
570
  throw new RangeError(`Child index ${childIndex} is out of range. Parent has only ${parent.childNodes.length} child nodes.`);
550
571
 
551
- // update references
572
+ // Update references
552
573
  const child = parent.childNodes[childIndex];
553
574
  if (childIndex > 0) {
554
575
  const beforeChild = parent.childNodes[childIndex - 1];
@@ -557,7 +578,7 @@ class Modify {
557
578
  child.parentNode = null;
558
579
  child.nextSibling = null;
559
580
 
560
- // remove and return
581
+ // Remove and return
561
582
  return parent.childNodes.splice(childIndex, 1)[0];
562
583
  }
563
584
 
@@ -1,4 +1,3 @@
1
- import { IMap } from "src/types";
2
1
 
3
2
  export const XmlNodeType = Object.freeze({
4
3
  Text: "Text",
@@ -35,5 +34,5 @@ export interface XmlCommentNode extends XmlNodeBase {
35
34
 
36
35
  export interface XmlGeneralNode extends XmlNodeBase {
37
36
  nodeType: typeof XmlNodeType.General;
38
- attributes?: IMap<string>;
37
+ attributes?: Record<string, string>;
39
38
  }