ltx
Version:
<xml for="JavaScript">
360 lines (317 loc) • 7.54 kB
JavaScript
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;