362 lines
7.4 KiB
JavaScript
362 lines
7.4 KiB
JavaScript
'use strict';
|
|
|
|
var escape = require('./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(escape.escapeXMLText(child));
|
|
} else if (child.toString) {
|
|
writer(escape.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(escape.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;
|
|
|
|
module.exports = Element;
|