@openxmldev/linq-to-xml
Version:
LINQ to XML for TypeScript
392 lines • 13.1 kB
JavaScript
/**
* @author Thomas Barnekow
* @license MIT
*/
import { ArgumentException, DomFactory, DomParser, DomReader, getAncestors, getDescendants, InvalidOperationException, LinqAttributes, LinqElements, StringBuilder, XAttribute, XContainer, XDocument, XName, XNamespace, XNode, } from './internal.js';
/**
* Represents an XML element.
*/
export class XElement extends XContainer {
constructor(nameOrOther, ...contentArray) {
super();
this._lastAttr = null;
if (nameOrOther instanceof XName || typeof nameOrOther === 'string') {
this._name =
nameOrOther instanceof XName ? nameOrOther : XName.get(nameOrOther);
for (const contentArrayItem of contentArray) {
this.addContentSkipNotify(contentArrayItem);
}
}
else {
this._name = nameOrOther._name;
this.copyNodes(nameOrOther);
this.copyAttributes(nameOrOther);
}
}
/**
* Gets the first attribute of this element.
*/
get firstAttribute() {
return this._lastAttr ? this._lastAttr._next : null;
}
/**
* Gets a value indicating whether the element has at least one attribute.
*/
get hasAttributes() {
return this._lastAttr !== null;
}
/**
* Gets a value indicating whether the element has at least one child element.
*/
get hasElements() {
if (this._content instanceof XNode) {
let node = this._content;
do {
if (node instanceof XElement)
return true;
node = node._next;
} while (node !== this._content);
}
return false;
}
/**
* Gets a value indicating whether the element contains no content.
*/
get isEmpty() {
return this._content === null;
}
/**
* Gets the last attribute of this element.
*/
get lastAttribute() {
return this._lastAttr;
}
/**
* Gets the name of this element.
*/
get name() {
return this._name;
}
/**
* Sets the name of this element.
*/
set name(value) {
this._name = value;
}
/**
* Gets the text contents of this element.
*
* If there is text content interspersed with nodes (mixed content) then the
* text content will be concatenated and returned.
*/
get value() {
if (this._content === null)
return '';
if (typeof this._content === 'string')
return this._content;
const sb = new StringBuilder();
this.appendText(sb);
return sb.toString();
}
/**
* Sets the text content of this element.
*/
set value(content) {
this.removeNodes();
this.add(content);
}
/**
* Returns this `XElement` and all of it's ancestors up to the root node.
* Optionally, an `XName` can be passed in to target specific ancestors.
*
* @param name The optional `XName` of the target ancestor.
* @returns An iterable containing this `XElement` and its ancestors (with
* a matching `XName` if `name` was provided).
*/
ancestorsAndSelf(name) {
return name === null
? new LinqElements(XElement.emptySequence)
: new LinqElements(getAncestors(this, name !== null && name !== void 0 ? name : null, true));
}
/** @internal */
addAttributeSkipNotify(a) {
if (this.attribute(a.name)) {
throw new Error('Invalid operation: duplicate attribute.');
}
if (a._parent) {
a = new XAttribute(a.name, a.value);
}
this.appendAttributeSkipNotify(a);
}
/** @internal */
appendAttributeSkipNotify(a) {
a._parent = this;
if (!this._lastAttr) {
a._next = a;
}
else {
a._next = this._lastAttr._next;
this._lastAttr._next = a;
}
this._lastAttr = a;
}
/**
* Returns the `XAttribute` associated with this `XElement` that has the
* given name.
*
* @param name The `XName` of the `XAttribute` to get.
* @returns The `XAttribute` having the given `XName` or `null`.
*/
attribute(name) {
if (this._lastAttr === null)
return null;
let attr = this._lastAttr;
do {
attr = attr._next;
if (attr._name === name) {
return attr;
}
} while (attr !== this._lastAttr);
return null;
}
/**
* Gets all attributes associated with this element or the attribute having
* the given name.
*
* @param name The name of the attribute to return.
* @returns All attributes associated with this element or the attribute
* having the given name.
*/
attributes(name) {
return name === null
? new LinqAttributes(XAttribute.emptySequence)
: new LinqAttributes(getAttributes(this, name !== null && name !== void 0 ? name : null));
}
/** @internal */
cloneNode() {
return new XElement(this);
}
/** @internal */
copyAttributes(other) {
if (!other._lastAttr) {
return;
}
let attr = other._lastAttr;
do {
attr = attr._next;
this.appendAttributeSkipNotify(new XAttribute(attr._name, attr._value));
} while (attr !== other._lastAttr);
}
/**
* Gets this `XElement` and the descendant `XElement`s of this `XElement`.
*
* @param name The optional name of the descendants to return.
* @returns This `XElement` and the descendant `XElement`s of this `XElement`.
*/
descendantsAndSelf(name) {
return name === null
? new LinqElements(XElement.emptySequence)
: new LinqElements(getDescendants(this, name !== null && name !== void 0 ? name : null, true));
}
/**
* Returns the default `XNamespace` of this `XElement`.
*
* @returns The default `XNamespace` of this `XElement`.
*/
getDefaultNamespace() {
const namespaceName = this.getNamespaceOfPrefixInScope('xmlns', null);
return namespaceName !== null
? XNamespace.get(namespaceName)
: XNamespace.none;
}
/**
* Get the namespace associated with a particular prefix for this `XElement`
* in its document context.
*
* @param prefix The namespace prefix to look up.
* @returns The `XNamespace` for the namespace bound to the prefix.
* @throws If the prefix is falsy, e.g., the empty string.
*/
getNamespaceOfPrefix(prefix) {
if (!prefix) {
throw new ArgumentException('prefix', 'The prefix is invalid.');
}
if (prefix === 'xmlns')
return XNamespace.xmlns;
const namespaceName = this.getNamespaceOfPrefixInScope(prefix, null);
if (namespaceName !== null)
return XNamespace.get(namespaceName);
if (prefix === 'xml')
return XNamespace.xml;
return null;
}
/**
* Get the prefix associated with a namespace for an element in its context.
*
* @param ns The `XNamespace` for which to get the prefix.
* @returns The namespace prefix `string`.
*/
getPrefixOfNamespace(ns) {
const namespaceName = ns.namespaceName;
// eslint-disable-next-line @typescript-eslint/no-this-alias
let e = this;
do {
let a = e._lastAttr;
if (a !== null) {
do {
a = a._next;
if (a.isNamespaceDeclaration &&
a.name.namespaceName.length !== 0 &&
a.value === namespaceName) {
// The current attribute is a namespace declaration in the form
// xmlns:prefix="namespaceName".
return a.name.localName;
}
} while (a !== e._lastAttr);
}
e = e._parent;
} while (e !== null);
if (namespaceName === XNamespace.xmlPrefixNamespaceName) {
return 'xml';
}
else if (namespaceName === XNamespace.xmlnsPrefixNamespaceName) {
return 'xmlns';
}
return null;
}
/** @internal */
getNamespaceOfPrefixInScope(prefix, outOfScope) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let e = this;
while (e !== outOfScope) {
let a = e._lastAttr;
if (a !== null) {
do {
a = a._next;
if (a.isNamespaceDeclaration && a.name.localName === prefix) {
// The current attribute declares the namespace for the given prefix.
return a.value;
}
} while (a !== e._lastAttr);
}
e = e._parent;
}
// There is no namespace declaration for the given prefix within the scope.
return null;
}
/**
* Creates a new `XElement` instance from the given DOM `Element`.
*
* @param element The DOM `Element`.
* @returns A new `XElement` instance.
*/
static load(element) {
return DomReader.loadXElement(element);
}
/**
* Creates a new `XElement` instance from the given XML string.
*
* @param text The XML string.
* @returns A new `XElement` instance.
* @throws An `ArgumentException` if the XML is malformed or does not contain
* a root element.
*/
static parse(text) {
const element = DomParser.parseElement(text);
return XElement.load(element);
}
/**
* Removes all nodes and attributes from this `XElement`.
*/
removeAll() {
this.removeAttributes();
this.removeNodes();
}
/**
* Removes all attributes from this `XElement`.
*/
removeAttributes() {
this.removeAttributesSkipNotify();
}
/** @internal */
removeAttribute(attr) {
if (attr._parent !== this) {
throw new InvalidOperationException('This operation was corrupted by external code.');
}
// Get the previous attribute.
let previousAttr = this._lastAttr;
while (previousAttr._next !== attr)
previousAttr = previousAttr._next;
if (previousAttr === attr) {
// The attribute is the only attribute, so we just remove the link.
this._lastAttr = null;
}
else {
// The attribute is not the only attribute, so we reroute the links.
if (this._lastAttr === attr)
this._lastAttr = previousAttr;
previousAttr._next = attr._next;
}
attr._parent = null;
attr._next = null;
}
/** @internal */
removeAttributesSkipNotify() {
if (this._lastAttr !== null) {
let attr = this._lastAttr;
do {
const next = attr._next;
attr._parent = null;
attr._next = null;
attr = next;
} while (attr !== this._lastAttr);
this._lastAttr = null;
}
}
setAttributeValue(name, value) {
const attr = this.attribute(name);
if (value === null || value === undefined) {
if (attr !== null)
this.removeAttribute(attr);
}
else {
if (attr !== null) {
attr.value = XContainer.getStringValue(value);
}
else {
this.appendAttributeSkipNotify(new XAttribute(name, value));
}
}
}
toString() {
return new XMLSerializer().serializeToString(DomFactory.createElement(this));
}
/** @internal */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
validateNode(node, _previous) {
if (node instanceof XDocument) {
throw new ArgumentException('node', 'Invalid node type: XDocument');
}
}
}
/**
* Gets an empty collection of elements.
*/
XElement.emptySequence = {
*[Symbol.iterator]() {
// Do not return anything.
},
};
/** @internal */
export function* getAttributes(element, name) {
if (element._lastAttr === null)
return;
let attr = element._lastAttr;
do {
attr = attr._next;
if (name === null || attr._name === name) {
yield attr;
}
} while (attr._parent === element && attr !== element._lastAttr);
}
//# sourceMappingURL=XElement.js.map