import { createLogger } from '@sim/logger' const logger = createLogger('PptxXmlParser') /** * Safe XML parser using browser DOMParser. * All operations are null-safe — accessing missing elements never crashes. */ export class SafeXmlNode { private readonly el: Element | null constructor(el: Element | null) { this.el = el } /** Get a string attribute value, or undefined if missing. */ attr(name: string): string | undefined { if (!this.el) return undefined return this.el.hasAttribute(name) ? this.el.getAttribute(name)! : undefined } /** Get a numeric attribute value, or undefined if missing or not a number. */ numAttr(name: string): number | undefined { const raw = this.attr(name) if (raw === undefined) return undefined const n = Number(raw) return Number.isNaN(n) ? undefined : n } /** * Find the first child element matching the given localName (namespace-agnostic). * Returns an empty SafeXmlNode if not found, so chaining never crashes. */ child(localName: string): SafeXmlNode { if (!this.el) return new SafeXmlNode(null) const children = this.el.children for (let i = 0; i < children.length; i++) { if (children[i].localName === localName) { return new SafeXmlNode(children[i]) } } return new SafeXmlNode(null) } /** * Get child elements, optionally filtered by localName (namespace-agnostic). * If no localName is given, returns all direct child elements. */ children(localName?: string): SafeXmlNode[] { if (!this.el) return [] const result: SafeXmlNode[] = [] const children = this.el.children for (let i = 0; i < children.length; i++) { if (localName === undefined || children[i].localName === localName) { result.push(new SafeXmlNode(children[i])) } } return result } /** Get the text content, or empty string if the element is missing. */ text(): string { if (!this.el) return '' return this.el.textContent ?? '' } /** Whether the underlying element actually exists. */ exists(): boolean { return this.el !== null } /** All direct child elements as SafeXmlNode[]. */ allChildren(): SafeXmlNode[] { return this.children() } /** The localName of the underlying element, or empty string. */ get localName(): string { return this.el?.localName ?? '' } /** Raw access to the underlying Element (may be null). */ get element(): Element | null { return this.el } } /** * Parse an XML string into a SafeXmlNode wrapping the document element. * Uses the browser's built-in DOMParser. */ export function parseXml(xmlString: string): SafeXmlNode { const parser = new DOMParser() const doc = parser.parseFromString(xmlString, 'application/xml') // Check for parser errors — DOMParser returns a parsererror document on failure const errorNode = doc.querySelector('parsererror') if (errorNode) { logger.warn('XML parse error', { error: errorNode.textContent ?? '' }) return new SafeXmlNode(null) } return new SafeXmlNode(doc.documentElement) }