UNPKG

@openxmldev/linq-to-xml

Version:
397 lines 13.6 kB
/** * @author Thomas Barnekow * @license MIT */ import { InvalidOperationException, LinqElements, LinqNodes, XAttribute, XElement, XNode, XText, } from './internal.js'; /** * Represents a node that can contain other nodes. * The two classes that derive from {@link XContainer} are {@link XDocument} and {@link XElement}. */ export class XContainer extends XNode { constructor() { super(); /** @internal */ this._content = null; } /** * Gets this container's first `XNode` or `null`, if this container does not * have any nodes. */ get firstNode() { const last = this.lastNode; return last ? last._next : null; } /** * Gets this container's last `XNode` or `null`, if this container does not * have any nodes. */ get lastNode() { if (!this._content) { // _content is falsy, i.e., it is null or the empty string. return null; } if (this._content instanceof XNode) { return this._content; } // At this point, _content is a non-empty string. const textNodeContent = new XText(this._content); textNodeContent._parent = this; textNodeContent._next = textNodeContent; this._content = textNodeContent; return this._content; } /** * Adds the specified content as a child (or children) of this `XContainer`. * * @param content A content object containing simple content or a collection * of content objects to be added. */ add(content) { this.addContentSkipNotify(content); } /** @internal */ // eslint-disable-next-line @typescript-eslint/no-unused-vars addAttributeSkipNotify(_a) { // No op } /** @internal */ addContentSkipNotify(content) { if (content === undefined || content === null || content === '') { // Content is undefined, null, or the empty string. return; } // At this point, content is neither null nor undefined nor empty. if (content instanceof XNode) { this.addNodeSkipNotify(content); } else if (typeof content === 'string') { this.addStringSkipNotify(content); } else if (content instanceof XAttribute) { this.addAttributeSkipNotify(content); } else if (typeof content[Symbol.iterator] === 'function') { // Content is iterable (and not a string, which we already considered above). for (const item of content) { this.addContentSkipNotify(item); } } else { // Add the string representation for any other type of object. this.addStringSkipNotify(XContainer.getStringValue(content)); } } /** @internal */ addNodeSkipNotify(node) { this.validateNode(node, this); // Clone the node if it is: // - associated with another parent or // - the root node (which does not have a parent). if (node._parent) { node = node.cloneNode(); } else { // eslint-disable-next-line @typescript-eslint/no-this-alias let parent = this; while (parent._parent) parent = parent._parent; if (node === parent) node = node.cloneNode(); } this.convertTextToNode(); this.appendNodeSkipNotify(node); } /** @internal */ addStringSkipNotify(s) { this.validateString(s); if (!this._content) { this._content = s; } else if (s.length > 0) { if (typeof this._content === 'string') { this._content += s; } else if (this._content instanceof XText) { // We don't have an XCData class, so we don't check for that here. this._content._text += s; } else { this.appendNodeSkipNotify(new XText(s)); } } } /** @internal */ appendNodeSkipNotify(node) { node._parent = this; if (this._content === null || typeof this._content === 'string') { node._next = node; } else { const lastNode = this._content; node._next = lastNode._next; lastNode._next = node; } this._content = node; } /** @internal */ appendText(sb) { if (typeof this._content === 'string') { sb.append(this._content); } else if (this._content instanceof XNode) { let node = this._content; do { node = node._next; node.appendText(sb); } while (node !== this._content); } } /** @internal */ convertTextToNode() { if (typeof this._content === 'string' && this._content.length > 0) { const textNode = new XText(this._content); textNode._parent = this; textNode._next = textNode; this._content = textNode; } } /** @internal */ copyNodes(other) { if (typeof other._content === 'string') { this._content = other._content; } else if (other._content instanceof XNode) { // other._content points to the last child XNode. let node = other._content; do { // In the first iteration, the following assignment sets node to the // first child XNode. node = node._next; this.appendNodeSkipNotify(node.cloneNode()); } while (node !== other._content); } } /** * Gets the descendant `XElement`s of this `XContainer`. * * @param name The optional name of the descendants to return. * @returns The descendant `XElement`s of this `XContainer`. */ descendants(name) { return name === null ? new LinqElements(XElement.emptySequence) : new LinqElements(getDescendants(this, name !== null && name !== void 0 ? name : null, false)); } element(name) { if (this._content instanceof XNode) { let node = this._content; do { node = node._next; if (node instanceof XElement && node._name == name) { return node; } } while (node !== this._content); } return null; } /** * Gets the child `XElement`s of this `XContainer`. * * @param name The optional name of the elements to return. * @returns The child `XElement`s of this `XContainer`. */ elements(name) { return name === null ? new LinqElements(XElement.emptySequence) : new LinqElements(getElements(this, name !== null && name !== void 0 ? name : null)); } /** @internal */ static getStringValue(value) { if (typeof value === 'string') { return value; } else if (typeof value === 'number') { return value.toString(10); } else if (typeof value === 'boolean') { return value ? 'true' : 'false'; } else if (value instanceof Date) { // Transform 2022-01-21T12:00:00.000Z into 2022-01-21T12:00:00Z return value.toISOString().slice(0, 19) + 'Z'; } else { return value.toString(); } } /** * Returns the content of this `XContainer`. * Note that the content does not include `XAttribute`s. * * @returns The content of this `XContainer` as an `IterableOfXNode`. */ nodes() { return new LinqNodes(getNodes(this)); } /** @internal */ removeNode(node) { if (node._parent !== this) { throw new InvalidOperationException('This operation was corrupted by external code.'); } // Get previous node. // Debug.Assert(content != null); let previousNode = this._content; while (previousNode._next !== node) previousNode = previousNode._next; if (previousNode === node) { // node is the only node. this._content = null; } else { if (this._content === node) { // n is the last node. this._content = previousNode; } previousNode._next = node._next; } node._parent = null; node._next = null; } /** * Removes the nodes from this `XContainer`. * Note that this method does not remove the attributes. */ removeNodes() { this.removeNodesSkipNotify(); } removeNodesSkipNotify() { if (this._content instanceof XNode) { let node = this._content; do { const next = node._next; node._parent = null; node._next = null; node = next; } while (node !== this._content); } this._content = null; } /** * Replaces the child nodes of this `XContainer` with the specified content. * The content can be simple content, a collection of content objects, a param * list of content objects, or null. * * @param content The new content. */ replaceNodes(...content) { let newContent = content.length === 1 ? content[0] : content; newContent = XContainer.getContentSnapshot(newContent); this.removeNodes(); this.add(newContent); } /** * Validate insertion of the given node. * previous === null means at beginning. * previous === this means at end. * * @param _node The node to be inserted. * @param _previous The node after which insertion will occur * * @internal */ // eslint-disable-next-line @typescript-eslint/no-unused-vars validateNode(_node, _previous) { // No op. } /** @internal */ // eslint-disable-next-line @typescript-eslint/no-unused-vars validateString(_s) { // No op. } /** @internal */ static getContentSnapshot(content) { if (!this.isIterable(content)) return content; const list = []; this.addContentToList(list, content); return list; } /** @internal */ static addContentToList(list, content) { if (!this.isIterable(content)) { list.push(content); } else { for (const item of content) { if (item != null) this.addContentToList(list, item); } } } /** @internal */ static isIterable(content) { return (typeof content !== 'string' && typeof content[Symbol.iterator] === 'function'); } } /** @internal */ export function* getDescendants(container, name, self) { if (self) { if (container instanceof XElement) { if (name === null || container.name === name) yield container; } } let n = container; let c = container; while (true) { if (c !== null && c._content instanceof XNode) { // The current container is the root container or the last-visited // element. Thus, move to the current container's first child node. n = c._content._next; } else { // Move back up the parent hierarchy as long as the current node is // not the root container but a last child node. while (n !== container && n === n._parent._content) n = n._parent; if (n === container) break; // The current node has been visited already and is not a last child // node. Thus, move to the current node's next sibling. n = n._next; } // Emit the current node, if it is an element. const e = n instanceof XElement ? n : null; if (e !== null && (name === null || e._name === name)) yield e; // The current element, if any, becomes the next container, the child // elements of which will be traversed next. c = e; } } /** @internal */ export function* getElements(container, name) { if (container._content instanceof XNode) { let node = container._content; do { node = node._next; if (node instanceof XElement && (name === null || node._name === name)) { yield node; } } while (node._parent === container && node !== container._content); } } /** @internal */ export function* getNodes(container) { // Getting container.lastNode will convert string content into XNode content // if required. let node = container.lastNode; if (node) { do { node = node._next; yield node; } while (node._parent === container && node !== container._content); } } //# sourceMappingURL=XContainer.js.map