UNPKG

@openxmldev/linq-to-xml

Version:
392 lines 13.1 kB
/** * @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