Skip to main content
Glama
web-xml-document-builder.ts9.54 kB
/** * Web-based XML Document Builder with fluent API * Compatible with Cloudflare Workers environment */ import { WebXmlService } from './web-xml-service.js'; /** * Builds XML documents with a fluent API * Designed to be compatible with Cloudflare Workers environment */ export class WebXmlDocumentBuilder { private service: WebXmlService; private rootElementName: string; private namespaces: Record<string, string>; private currentElement: ElementNode; private rootElement: ElementNode; private isDisposed: boolean; /** * Creates a new XML document builder * * @param service WebXmlService instance * @param rootElementName Name of the root element * @param namespaces Optional map of namespace prefixes to URIs */ constructor(service: WebXmlService, rootElementName: string, namespaces?: Record<string, string>) { this.service = service; this.rootElementName = rootElementName; this.namespaces = namespaces || {}; this.rootElement = new ElementNode(rootElementName); this.currentElement = this.rootElement; this.isDisposed = false; // Add namespace attributes to root element if (namespaces) { for (const [prefix, uri] of Object.entries(namespaces)) { this.rootElement.setAttribute(prefix === 'xmlns' ? prefix : `xmlns:${prefix}`, uri); } } } /** * Adds an element to the current context * * @param name Element name * @param content Optional element content * @returns This builder instance (for chaining) */ addElement(name: string, content?: string): WebXmlDocumentBuilder { if (this.isDisposed) { throw new Error('Cannot use WebXmlDocumentBuilder after it has been disposed'); } const element = new ElementNode(name); if (content !== undefined) { element.setContent(content); } this.currentElement.addChild(element); return this; } /** * Adds an attribute to the current element * * @param name Attribute name * @param value Attribute value * @returns This builder instance (for chaining) */ addAttribute(name: string, value: string): WebXmlDocumentBuilder { this.currentElement.setAttribute(name, value); return this; } /** * Starts a new element and makes it the current element * * @param name Element name * @returns This builder instance (for chaining) */ startElement(name: string): WebXmlDocumentBuilder { const element = new ElementNode(name); this.currentElement.addChild(element); this.currentElement = element; return this; } /** * Sets the content of the current element * * @param content Text content * @returns This builder instance (for chaining) */ setContent(content: string): WebXmlDocumentBuilder { this.currentElement.setContent(content); return this; } /** * Ends the current element and moves back to its parent * * @returns This builder instance (for chaining) */ endElement(): WebXmlDocumentBuilder { if (this.currentElement.parent) { this.currentElement = this.currentElement.parent; } return this; } /** * Adds an empty element (self-closing tag) * * @param name Element name * @returns This builder instance (for chaining) */ addEmptyElement(name: string): WebXmlDocumentBuilder { const element = new ElementNode(name, true); this.currentElement.addChild(element); return this; } /** * Converts the XML document to string representation * * @param includeDeclaration Whether to include XML declaration * @param version XML version for declaration * @param encoding XML encoding for declaration * @returns String representation of the XML document */ toString( includeDeclaration: boolean = false, version: string = '1.0', encoding: string = 'UTF-8', ): string { if (this.isDisposed) { throw new Error('Cannot use WebXmlDocumentBuilder after it has been disposed'); } const xmlContent = this.rootElement.toString(this.service); if (includeDeclaration) { return this.service.createXmlDocument(xmlContent, version, encoding); } return xmlContent; } /** * Converts the XML document to a JavaScript object * This is useful when you want to serialize using the WebXmlService * * @returns JavaScript object representation of the XML document */ toObject(): Record<string, unknown> { if (this.isDisposed) { throw new Error('Cannot use WebXmlDocumentBuilder after it has been disposed'); } return this.rootElement.toObject(); } /** * Disposes the document builder by breaking circular references * Call this method when you're done with the document builder to prevent memory leaks * The builder cannot be used after calling dispose() * * @returns void */ dispose(): void { if (this.isDisposed) { return; } // Break circular references by traversing the tree and nullifying parent references this.disposeElementNode(this.rootElement); // Reset references this.rootElement = null as unknown as ElementNode; this.currentElement = null as unknown as ElementNode; this.isDisposed = true; } /** * Helper method to recursively clean up circular references in element nodes * * @param node The element node to dispose */ private disposeElementNode(node: ElementNode): void { // Clean up children first if (node.children && node.children.length > 0) { for (const child of node.children) { // Break parent reference child.parent = undefined; // Recursively dispose child node this.disposeElementNode(child); } // Clear children array node.children.length = 0; } } } /** * Internal representation of an XML element node */ class ElementNode { name: string; attributes: Record<string, string>; content?: string; children: ElementNode[]; parent?: ElementNode; isEmpty: boolean; constructor(name: string, isEmpty: boolean = false) { this.name = name; this.attributes = {}; this.children = []; this.isEmpty = isEmpty; } /** * Sets an attribute on this element * * @param name Attribute name * @param value Attribute value */ setAttribute(name: string, value: string): void { this.attributes[name] = value; } /** * Sets the text content of this element * * @param content Text content */ setContent(content: string): void { this.content = content; } /** * Adds a child element to this element * * @param child Child element */ addChild(child: ElementNode): void { child.parent = this; this.children.push(child); } /** * Converts this element and its children to a JavaScript object * * @returns Object representation of this element */ toObject(): Record<string, unknown> { const result: Record<string, unknown> = {}; // Add name as the key result[this.name] = {}; const elementContent = result[this.name] as Record<string, unknown>; // Add attributes if present if (Object.keys(this.attributes).length > 0) { elementContent['$'] = {}; for (const [key, value] of Object.entries(this.attributes)) { (elementContent['$'] as Record<string, string>)[key] = value; } } // Add content if present if (this.content !== undefined) { elementContent['#text'] = this.content; } // Add children if present if (this.children.length > 0) { for (const child of this.children) { const childObject = child.toObject(); for (const [key, value] of Object.entries(childObject)) { if (elementContent[key]) { // If the key already exists, convert to array if (!Array.isArray(elementContent[key])) { elementContent[key] = [elementContent[key]]; } (elementContent[key] as Array<unknown>).push(value); } else { // Otherwise just add the key elementContent[key] = value; } } } } return result; } /** * Converts this element and its children to string representation * * @param service XML service for content escaping * @returns String representation of this element */ toString(service: WebXmlService): string { // Handle empty elements (self-closing tags) if (this.isEmpty) { const attributeString = this.getAttributeString(service); return `<${this.name}${attributeString} />`; } // Build opening tag with attributes const attributeString = this.getAttributeString(service); let result = `<${this.name}${attributeString}>`; // Add text content or child elements if (this.content !== undefined) { result += service.escapeXml(this.content); } else if (this.children.length > 0) { for (const child of this.children) { result += child.toString(service); } } // Add closing tag result += `</${this.name}>`; return result; } /** * Gets attribute string for element tag * * @param service XML service for attribute value escaping * @returns Formatted attribute string */ private getAttributeString(service: WebXmlService): string { let result = ''; for (const [name, value] of Object.entries(this.attributes)) { result += ` ${name}="${service.escapeXml(value)}"`; } return result; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Cheffromspace/mcp-nextcloud-calendar'

If you have feedback or need assistance with the MCP directory API, please join our Discord server