Files
smartxml/ts/smartxml.xmlbuilder.ts

181 lines
4.5 KiB
TypeScript
Raw Normal View History

import * as plugins from './smartxml.plugins.js';
/**
* A lightweight chainable XML builder that provides a fluent API
* for programmatically constructing XML documents.
*/
export class XmlBuilder {
private stack: any[] = [];
private current: any = null;
private rootElement: any = null;
/**
* Creates a new XML builder instance
* @param input Optional: object to convert to XML, or XML string to parse
*/
constructor(input?: any) {
if (input) {
if (typeof input === 'string') {
// Parse XML string using fast-xml-parser
const parser = new plugins.fastXmlParser.XMLParser({
preserveOrder: true,
ignoreAttributes: false,
});
this.rootElement = parser.parse(input);
this.current = this.rootElement;
} else if (typeof input === 'object') {
// Accept object input
this.rootElement = input;
this.current = input;
}
}
}
/**
* Static factory method for creating instances
*/
static create(input?: any): XmlBuilder {
return new XmlBuilder(input);
}
/**
* Add an element
* @param name Element name
* @param attributes Optional attributes object
*/
public ele(name: string, attributes?: Record<string, any>): this {
if (!this.current) {
// First element becomes root
this.rootElement = {};
this.current = this.rootElement;
}
const newElement: any = {};
// Add attributes with @_ prefix (fast-xml-parser format)
if (attributes) {
for (const [key, value] of Object.entries(attributes)) {
newElement[`@_${key}`] = value;
}
}
// Check if this property already exists
if (this.current[name]) {
// Convert to array if not already
if (!Array.isArray(this.current[name])) {
this.current[name] = [this.current[name]];
}
this.current[name].push(newElement);
this.stack.push(this.current);
this.current = newElement;
} else {
this.current[name] = newElement;
this.stack.push(this.current);
this.current = newElement;
}
return this;
}
/**
* Add text content to current element
* @param content Text content
*/
public txt(content: string | number): this {
if (this.current) {
// Check if current element already has properties
const hasProperties = Object.keys(this.current).length > 0;
if (hasProperties) {
// Use #text format for mixed content
this.current['#text'] = String(content);
} else {
// For simple text-only elements, we can use direct assignment
// But to maintain consistency, we'll use #text
this.current['#text'] = String(content);
}
}
return this;
}
/**
* Add attribute to current element
* @param name Attribute name
* @param value Attribute value
*/
public att(name: string, value: any): this {
if (this.current) {
this.current[`@_${name}`] = value;
}
return this;
}
/**
* Move up to parent element
*/
public up(): this {
if (this.stack.length > 0) {
this.current = this.stack.pop();
}
return this;
}
/**
* Get the root element (for navigation)
*/
public root(): this {
if (this.stack.length > 0) {
this.current = this.stack[0];
this.stack = [];
}
return this;
}
/**
* Add a comment
* @param content Comment text
*/
public com(content: string): this {
// Comments in fast-xml-parser format
if (this.current) {
if (!this.current['#comment']) {
this.current['#comment'] = [];
}
if (!Array.isArray(this.current['#comment'])) {
this.current['#comment'] = [this.current['#comment']];
}
this.current['#comment'].push(content);
}
return this;
}
/**
* Serialize the XML document
* @param options Serialization options
*/
public end(options?: { prettyPrint?: boolean; format?: string }): string | any {
const opts = options || {};
if (opts.format === 'object') {
return this.rootElement;
}
// Use fast-xml-parser to build XML
const builder = new plugins.fastXmlParser.XMLBuilder({
ignoreAttributes: false,
attributeNamePrefix: '@_',
format: opts.prettyPrint !== false,
indentBy: ' ',
});
const xml = builder.build(this.rootElement || {});
return '<?xml version="1.0" encoding="UTF-8"?>\n' + xml;
}
/**
* Convert to string (alias for end)
*/
public toString(): string {
return this.end() as string;
}
}