UNPKG

ltx

Version:

<xml for="JavaScript">

360 lines (317 loc) 7.54 kB
import { escapeXML, escapeXMLText } from "./escape.js"; /** * Element * * Attributes are in the element.attrs object. Children is a list of * either other Elements or Strings for text content. **/ class Element { constructor(name, attrs) { this.name = name; this.parent = null; this.children = []; this.attrs = {}; this.setAttrs(attrs); } /* Accessors */ /** * if (element.is('message', 'jabber:client')) ... **/ is(name, xmlns) { return this.getName() === name && (!xmlns || this.getNS() === xmlns); } /* without prefix */ getName() { const idx = this.name.indexOf(":"); return idx >= 0 ? this.name.slice(idx + 1) : this.name; } /** * retrieves the namespace of the current element, upwards recursively **/ getNS() { const idx = this.name.indexOf(":"); if (idx >= 0) { const prefix = this.name.slice(0, idx); return this.findNS(prefix); } return this.findNS(); } /** * find the namespace to the given prefix, upwards recursively **/ findNS(prefix) { if (!prefix) { /* default namespace */ if (this.attrs.xmlns) { return this.attrs.xmlns; } else if (this.parent) { return this.parent.findNS(); } } else { /* prefixed namespace */ const attr = "xmlns:" + prefix; if (this.attrs[attr]) { return this.attrs[attr]; } else if (this.parent) { return this.parent.findNS(prefix); } } } /** * Recursiverly gets all xmlns defined, in the form of {url:prefix} **/ getXmlns() { let namespaces = {}; if (this.parent) { namespaces = this.parent.getXmlns(); } for (const attr in this.attrs) { const m = attr.match("xmlns:?(.*)"); // eslint-disable-next-line no-prototype-builtins if (this.attrs.hasOwnProperty(attr) && m) { namespaces[this.attrs[attr]] = m[1]; } } return namespaces; } setAttrs(attrs) { if (typeof attrs === "string") { this.attrs.xmlns = attrs; } else if (attrs) { Object.assign(this.attrs, attrs); } } /** * xmlns can be null, returns the matching attribute. **/ getAttr(name, xmlns) { if (!xmlns) { return this.attrs[name]; } const namespaces = this.getXmlns(); if (!namespaces[xmlns]) { return null; } return this.attrs[[namespaces[xmlns], name].join(":")]; } /** * xmlns can be null **/ getChild(name, xmlns) { return this.getChildren(name, xmlns)[0]; } /** * xmlns can be null **/ getChildren(name, xmlns) { const result = []; for (const child of this.children) { if ( child.getName && child.getName() === name && (!xmlns || child.getNS() === xmlns) ) { result.push(child); } } return result; } /** * xmlns and recursive can be null **/ getChildByAttr(attr, val, xmlns, recursive) { return this.getChildrenByAttr(attr, val, xmlns, recursive)[0]; } /** * xmlns and recursive can be null **/ getChildrenByAttr(attr, val, xmlns, recursive) { let result = []; for (const child of this.children) { if ( child.attrs && child.attrs[attr] === val && (!xmlns || child.getNS() === xmlns) ) { result.push(child); } if (recursive && child.getChildrenByAttr) { result.push(child.getChildrenByAttr(attr, val, xmlns, true)); } } if (recursive) { result = result.flat(); } return result; } getChildrenByFilter(filter, recursive) { let result = []; for (const child of this.children) { if (filter(child)) { result.push(child); } if (recursive && child.getChildrenByFilter) { result.push(child.getChildrenByFilter(filter, true)); } } if (recursive) { result = result.flat(); } return result; } getText() { let text = ""; for (const child of this.children) { if (typeof child === "string" || typeof child === "number") { text += child; } } return text; } getChildText(name, xmlns) { const child = this.getChild(name, xmlns); return child ? child.getText() : null; } /** * Return all direct descendents that are Elements. * This differs from `getChildren` in that it will exclude text nodes, * processing instructions, etc. */ getChildElements() { return this.getChildrenByFilter((child) => { return child instanceof Element; }); } /* Builder */ /** returns uppermost parent */ root() { if (this.parent) { return this.parent.root(); } return this; } /** just parent or itself */ up() { if (this.parent) { return this.parent; } return this; } /** create child node and return it */ c(name, attrs) { return this.cnode(new Element(name, attrs)); } cnode(child) { this.children.push(child); if (typeof child === "object") { child.parent = this; } return child; } append(...nodes) { for (const node of nodes) { this.children.push(node); if (typeof node === "object") { node.parent = this; } } } prepend(...nodes) { for (const node of nodes) { this.children.unshift(node); if (typeof node === "object") { node.parent = this; } } } /** add text node and return element */ t(text) { this.children.push(text); return this; } /* Manipulation */ /** * Either: * el.remove(childEl) * el.remove('author', 'urn:...') */ remove(el, xmlns) { const filter = typeof el === "string" ? (child) => { /* 1st parameter is tag name */ return !(child.is && child.is(el, xmlns)); } : (child) => { /* 1st parameter is element */ return child !== el; }; this.children = this.children.filter(filter); return this; } text(val) { if (val && this.children.length === 1) { this.children[0] = val; return this; } return this.getText(); } attr(attr, val) { if (typeof val !== "undefined" || val === null) { if (!this.attrs) { this.attrs = {}; } this.attrs[attr] = val; return this; } return this.attrs[attr]; } /* Serialization */ toString() { let s = ""; this.write((c) => { s += c; }); return s; } _addChildren(writer) { writer(">"); for (const child of this.children) { /* Skip null/undefined */ if (child != null) { if (child.write) { child.write(writer); } else if (typeof child === "string") { writer(escapeXMLText(child)); } else if (child.toString) { writer(escapeXMLText(child.toString(10))); } } } writer("</"); writer(this.name); writer(">"); } write(writer) { writer("<"); writer(this.name); for (const k in this.attrs) { const v = this.attrs[k]; // === null || undefined if (v != null) { writer(" "); writer(k); writer('="'); writer(escapeXML(typeof v === "string" ? v : v.toString(10))); writer('"'); } } if (this.children.length === 0) { writer("/>"); } else { this._addChildren(writer); } } } Element.prototype.tree = Element.prototype.root; export default Element;