File
Blob: src/worker/xml/tree.ts
| 1 | import type { ParsedXmlElement, ParsedXmlNode } from "@/worker/xml/parser"; |
| 2 | import type { QName } from "@/worker/xml/namespaces"; |
| 3 | |
| 4 | export function splitXmlName(name: string): { prefix: string; localName: string } { |
| 5 | const index = name.indexOf(":"); |
| 6 | if (index === -1) return { prefix: "", localName: name }; |
| 7 | return { prefix: name.slice(0, index), localName: name.slice(index + 1) }; |
| 8 | } |
| 9 | |
| 10 | function elementChildren(element: ParsedXmlElement): ParsedXmlElement[] { |
| 11 | return element.children.filter((child): child is ParsedXmlElement => typeof child !== "string"); |
| 12 | } |
| 13 | |
| 14 | export function namespaceMap(element: ParsedXmlElement, parent: Record<string, string> = {}): Record<string, string> { |
| 15 | const namespaces = { ...parent }; |
| 16 | for (const [name, value] of Object.entries(element.attributes)) { |
| 17 | if (name === "xmlns") namespaces[""] = value; |
| 18 | if (name.startsWith("xmlns:")) namespaces[name.slice("xmlns:".length)] = value; |
| 19 | } |
| 20 | return namespaces; |
| 21 | } |
| 22 | |
| 23 | export function qnameForElement(element: ParsedXmlElement, namespaces = namespaceMap(element)): QName { |
| 24 | const { prefix, localName } = splitXmlName(element.name); |
| 25 | return { nsUri: namespaces[prefix] ?? "", localName }; |
| 26 | } |
| 27 | |
| 28 | export interface NamedXmlElement { |
| 29 | element: ParsedXmlElement; |
| 30 | qname: QName; |
| 31 | namespaces: Record<string, string>; |
| 32 | } |
| 33 | |
| 34 | export function namedElement( |
| 35 | element: ParsedXmlElement, |
| 36 | parentNamespaces: Record<string, string> = {}, |
| 37 | ): NamedXmlElement { |
| 38 | const namespaces = namespaceMap(element, parentNamespaces); |
| 39 | return { element, qname: qnameForElement(element, namespaces), namespaces }; |
| 40 | } |
| 41 | |
| 42 | export function namedElementChildren( |
| 43 | element: ParsedXmlElement, |
| 44 | parentNamespaces: Record<string, string>, |
| 45 | ): NamedXmlElement[] { |
| 46 | const namespaces = namespaceMap(element, parentNamespaces); |
| 47 | return elementChildren(element).map((child) => namedElement(child, namespaces)); |
| 48 | } |
| 49 | |
| 50 | export function elementsByQName( |
| 51 | element: ParsedXmlElement, |
| 52 | qname: QName, |
| 53 | parentNamespaces: Record<string, string> = {}, |
| 54 | ): NamedXmlElement[] { |
| 55 | const matches: NamedXmlElement[] = []; |
| 56 | for (const child of namedElementChildren(element, parentNamespaces)) { |
| 57 | if (child.qname.nsUri === qname.nsUri && child.qname.localName === qname.localName) matches.push(child); |
| 58 | matches.push(...elementsByQName(child.element, qname, child.namespaces)); |
| 59 | } |
| 60 | return matches; |
| 61 | } |
| 62 | |
| 63 | export function textContent(node: ParsedXmlNode): string { |
| 64 | if (typeof node === "string") return node; |
| 65 | return node.children.map(textContent).join(""); |
| 66 | } |
| 67 | |
| 68 | export function firstTextByQName( |
| 69 | element: ParsedXmlElement, |
| 70 | qname: QName, |
| 71 | parentNamespaces: Record<string, string> = {}, |
| 72 | ): string | null { |
| 73 | return elementsByQName(element, qname, parentNamespaces).at(0)?.element.children.map(textContent).join("") ?? null; |
| 74 | } |