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): 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 '\n' + xml; } /** * Convert to string (alias for end) */ public toString(): string { return this.end() as string; } }