@openxmldev/linq-to-xml
Version:
LINQ to XML for TypeScript
397 lines • 13.6 kB
JavaScript
/**
* @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