diff --git a/src/dom/functions.ts b/src/dom/functions.ts index 27ef309..837278f 100644 --- a/src/dom/functions.ts +++ b/src/dom/functions.ts @@ -7,7 +7,7 @@ import { XDocument } from "./xdocument"; import { XNode } from './xnode'; // Wrapper around DOM methods so we can condense their invocations. -export function domGetAttributeValue(node: any, name: string) { +export function domGetAttributeValue(node: XNode, name: string) { return node.getAttributeValue(name); } diff --git a/src/dom/xdocument.ts b/src/dom/xdocument.ts index af2a7c6..4c70359 100644 --- a/src/dom/xdocument.ts +++ b/src/dom/xdocument.ts @@ -20,10 +20,11 @@ export class XDocument extends XNode { this.documentElement = null; } - clear() { + // TODO: Do we still need this? + /* clear() { XNode.recycle(this.documentElement); this.documentElement = null; - } + } */ appendChild(node: any) { super.appendChild(node); diff --git a/src/dom/xml-functions.ts b/src/dom/xml-functions.ts index 6c42cb5..9352055 100644 --- a/src/dom/xml-functions.ts +++ b/src/dom/xml-functions.ts @@ -25,7 +25,7 @@ import { XmlOutputOptions } from './xml-output-options'; * @param disallowBrowserSpecificOptimization A boolean, to avoid browser optimization. * @returns The XML value as a string. */ -export function xmlValue(node: any, disallowBrowserSpecificOptimization: boolean = false): string { +export function xmlValue(node: XNode | any, disallowBrowserSpecificOptimization: boolean = false): string { if (!node) { return ''; } @@ -42,12 +42,13 @@ export function xmlValue(node: any, disallowBrowserSpecificOptimization: boolean case DOM_DOCUMENT_NODE: case DOM_DOCUMENT_FRAGMENT_NODE: if (!disallowBrowserSpecificOptimization) { - // IE, Safari, Opera, and friends + // Only returns something if node has either `innerText` or `textContent` (not an XNode). + // IE, Safari, Opera, and friends (`innerText`) const innerText = node.innerText; if (innerText != undefined) { return innerText; } - // Firefox + // Firefox (`textContent`) const textContent = node.textContent; if (textContent != undefined) { return textContent; @@ -55,12 +56,14 @@ export function xmlValue(node: any, disallowBrowserSpecificOptimization: boolean } if (node.transformedChildNodes.length > 0) { - for (let i = 0; i < node.transformedChildNodes.length; ++i) { - ret += xmlValue(node.transformedChildNodes[i]); + const transformedTextNodes = node.transformedChildNodes.filter((n: XNode) => n.nodeType !== DOM_ATTRIBUTE_NODE); + for (let i = 0; i < transformedTextNodes.length; ++i) { + ret += xmlValue(transformedTextNodes[i]); } } else { - for (let i = 0; i < node.childNodes.length; ++i) { - ret += xmlValue(node.childNodes[i]); + const textNodes = node.childNodes.filter((n: XNode) => n.nodeType !== DOM_ATTRIBUTE_NODE); + for (let i = 0; i < textNodes.length; ++i) { + ret += xmlValue(textNodes[i]); } } @@ -96,7 +99,7 @@ export function xmlValue2(node: any, disallowBrowserSpecificOptimization: boolea return textContent; } } - // pobrecito! + const len = node.transformedChildNodes.length; for (let i = 0; i < len; ++i) { ret += xmlValue(node.transformedChildNodes[i]); @@ -137,10 +140,15 @@ function xmlTextRecursive(node: XNode, buffer: string[], options: XmlOutputOptio buffer.push(``); } else if (node.nodeType == DOM_ELEMENT_NODE) { buffer.push(`<${xmlFullNodeName(node)}`); - for (let i = 0; i < node.attributes.length; ++i) { - const a = node.attributes[i]; - if (a && a.nodeName && a.nodeValue) { - buffer.push(` ${xmlFullNodeName(a)}="${xmlEscapeAttr(a.nodeValue)}"`); + + for (let i = 0; i < node.childNodes.length; ++i) { + const childNode = node.childNodes[i]; + if (!childNode || childNode.nodeType !== DOM_ATTRIBUTE_NODE) { + continue; + } + + if (childNode.nodeName && childNode.nodeValue) { + buffer.push(` ${xmlFullNodeName(childNode)}="${xmlEscapeAttr(childNode.nodeValue)}"`); } } @@ -233,7 +241,11 @@ function xmlTransformedTextRecursive(node: XNode, buffer: any[], options: XmlOut function xmlElementLogicTrivial(node: XNode, buffer: string[], options: XmlOutputOptions) { buffer.push(`<${xmlFullNodeName(node)}`); - const attributes = node.transformedAttributes || node.attributes; + let attributes = node.transformedChildNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); + if (attributes.length === 0) { + attributes = node.childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); + } + for (let i = 0; i < attributes.length; ++i) { const attribute = attributes[i]; if (!attribute) { @@ -245,7 +257,11 @@ function xmlElementLogicTrivial(node: XNode, buffer: string[], options: XmlOutpu } } - let childNodes = node.transformedChildNodes.length > 0 ? node.transformedChildNodes : node.childNodes; + let childNodes = node.transformedChildNodes.filter(n => n.nodeType !== DOM_ATTRIBUTE_NODE); + if (childNodes.length === 0) { + childNodes = node.childNodes.filter(n => n.nodeType !== DOM_ATTRIBUTE_NODE); + } + childNodes = childNodes.sort((a, b) => a.siblingPosition - b.siblingPosition); if (childNodes.length === 0) { if (options.outputMethod === 'html' && ['hr', 'link', 'meta'].includes(node.nodeName)) { diff --git a/src/dom/xml-parser.ts b/src/dom/xml-parser.ts index c85edf7..9436af4 100644 --- a/src/dom/xml-parser.ts +++ b/src/dom/xml-parser.ts @@ -19,6 +19,7 @@ import { XML11_VERSION_INFO } from './xmltoken'; import { XNode } from './xnode'; +import { DOM_ATTRIBUTE_NODE } from '../constants'; /** * Original author: Steffen Meschkat (the `xmlParse` function, @@ -67,12 +68,17 @@ export class XmlParser { }; let n = node; while (n !== null) { - for (let i = 0; i < n.attributes.length; i++) { - if (n.attributes[i].nodeName.startsWith('xmlns:')) { - const prefix = n.attributes[i].nodeName.split(':')[1]; - if (!(prefix in map)) map[prefix] = n.attributes[i].nodeValue; - } else if (n.attributes[i].nodeName == 'xmlns') { - if (!('' in map)) map[''] = n.attributes[i].nodeValue || null; + for (let i = 0; i < n.childNodes.length; i++) { + const childNode = n.childNodes[i]; + if (childNode.nodeType !== DOM_ATTRIBUTE_NODE) { + continue; + } + + if (childNode.nodeName.startsWith('xmlns:')) { + const prefix = childNode.nodeName.split(':')[1]; + if (!(prefix in map)) map[prefix] = childNode.nodeValue; + } else if (childNode.nodeName == 'xmlns') { + if (!('' in map)) map[''] = childNode.nodeValue || null; } } n = n.parentNode; @@ -258,11 +264,15 @@ export class XmlParser { } else { if ('' in namespaceMap) node.namespaceUri = namespaceMap['']; } - for (let i = 0; i < node.attributes.length; ++i) { - if (node.attributes[i].prefix !== null) { - if (node.attributes[i].prefix in namespaceMap) { - node.attributes[i].namespaceUri = namespaceMap[node.attributes[i].prefix]; - } + + for (let i = 0; i < node.childNodes.length; ++i) { + const childNode = node.childNodes[i]; + if (childNode.nodeType !== DOM_ATTRIBUTE_NODE) { + continue; + } + + if (childNode.prefix !== null && childNode.prefix in namespaceMap) { + childNode.namespaceUri = namespaceMap[childNode.prefix]; // else, prefix undefined. } // elements with no prefix always have no namespace, so do nothing here. diff --git a/src/dom/xnode.ts b/src/dom/xnode.ts index 9ec6358..9b45233 100644 --- a/src/dom/xnode.ts +++ b/src/dom/xnode.ts @@ -1,16 +1,16 @@ -// Our W3C DOM Node implementation. Note we call it XNode because we -// can't define the identifier Node. We do this mostly for Opera, -// where we can't reuse the HTML DOM for parsing our own XML, and for -// Safari, where it is too expensive to have the template processor - import { DOM_ATTRIBUTE_NODE, DOM_ELEMENT_NODE } from '../constants'; // operate on native DOM nodes. +/** + * Our W3C DOM Node implementation. Note we call it XNode because we + * can't define the identifier Node. We do this mostly for Opera, + * where we can't reuse the HTML DOM for parsing our own XML, and for + * Safari, where it is too expensive to have the template processor. + */ export class XNode { id: number; - attributes: XNode[]; childNodes: XNode[]; - nodeType: any; + nodeType: number; nodeName: string; nodeValue: any; firstChild: XNode; @@ -27,7 +27,6 @@ export class XNode { parentNode: XNode; outputNode: XNode; - transformedAttributes: XNode[]; transformedChildNodes: XNode[]; transformedNodeType: any; transformedNodeName: string; @@ -46,11 +45,9 @@ export class XNode { static _unusedXNodes: any[] = []; - constructor(type: any, name: string, opt_value: any, opt_owner: any, opt_namespace?: any) { + constructor(type: number, name: string, opt_value: any, opt_owner: any, opt_namespace?: any) { this.id = Math.random() * (Number.MAX_SAFE_INTEGER - 1) + 1; - this.attributes = []; this.childNodes = []; - this.transformedAttributes = []; this.transformedChildNodes = []; this.visited = false; this.escape = true; @@ -67,7 +64,7 @@ export class XNode { * @param owner The node owner. * @param namespaceUri The node namespace. */ - init(type: any, name: string, value: string, owner: any, namespaceUri: any) { + init(type: number, name: string, value: string, owner: any, namespaceUri: any) { this.nodeType = type - 0; this.nodeName = `${name}`; this.nodeValue = `${value}`; @@ -97,7 +94,7 @@ export class XNode { // traversed. Traversal will not be continued if a callback function // returns boolean false. NOTE(mesch): copied from // . - protected domTraverseElements(node: any, opt_pre: any, opt_post: any) { + protected domTraverseElements(node: XNode, opt_pre: Function, opt_post: any) { let ret; if (opt_pre) { ret = opt_pre.call(null, node); @@ -123,6 +120,7 @@ export class XNode { } } + // TODO: Do we still need this? static recycle(node: any) { if (!node) { return; @@ -138,15 +136,15 @@ export class XNode { } this._unusedXNodes.push(node); - for (let a = 0; a < node.attributes.length; ++a) { + /* for (let a = 0; a < node.attributes.length; ++a) { this.recycle(node.attributes[a]); - } + } */ for (let c = 0; c < node.childNodes.length; ++c) { this.recycle(node.childNodes[c]); } - node.attributes.length = 0; + // node.attributes.length = 0; node.childNodes.length = 0; node.init.call(0, '', '', null); } @@ -168,16 +166,16 @@ export class XNode { newNode.appendChild(XNode.clone(child, newNode)); } - for (let attribute of node.attributes) { + /* for (let attribute of node.attributes) { newNode.setAttribute(attribute.nodeName, attribute.nodeValue); - } + } */ return newNode; } appendChild(node: XNode) { // firstChild - if (this.childNodes.length == 0) { + if (this.childNodes.length === 0) { this.firstChild = node; } @@ -202,7 +200,7 @@ export class XNode { appendTransformedChild(node: XNode) { // firstChild - if (this.transformedChildNodes.length == 0) { + if (this.transformedChildNodes.length === 0) { this.transformedFirstChild = node; } @@ -304,7 +302,7 @@ export class XNode { this.childNodes = newChildren; } - removeChild(node: any) { + removeChild(node: XNode) { const newChildren = []; for (const c of this.childNodes) { @@ -330,27 +328,31 @@ export class XNode { } hasAttributes() { - return this.attributes.length > 0; + const attributes = this.childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); + return attributes.length > 0; } setAttribute(name: string, value: any) { - for (let i = 0; i < this.attributes.length; ++i) { - if (this.attributes[i].nodeName == name) { - this.attributes[i].nodeValue = `${value}`; + const attributes = this.childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); + for (let i = 0; i < attributes.length; ++i) { + if (attributes[i].nodeName == name) { + attributes[i].nodeValue = `${value}`; return; } } const newAttribute = XNode.create(DOM_ATTRIBUTE_NODE, name, value, this); newAttribute.parentNode = this; - this.attributes.push(newAttribute); + this.appendChild(newAttribute); } setTransformedAttribute(name: string, value: any) { - for (let i = 0; i < this.transformedAttributes.length; ++i) { - if (this.transformedAttributes[i].nodeName === name) { - this.transformedAttributes[i].transformedNodeName = name; - this.transformedAttributes[i].transformedNodeValue = `${value}`; + const transformedAttributes = this.transformedChildNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); + for (let i = 0; i < transformedAttributes.length; ++i) { + const transformedAttribute = transformedAttributes[i]; + if (transformedAttribute.nodeName === name) { + transformedAttribute.transformedNodeName = name; + transformedAttribute.transformedNodeValue = `${value}`; return; } } @@ -359,29 +361,34 @@ export class XNode { newAttribute.transformedNodeName = name; newAttribute.transformedNodeValue = value; newAttribute.parentNode = this; - this.transformedAttributes.push(newAttribute); + this.appendTransformedChild(newAttribute); } setAttributeNS(namespace: any, name: any, value: any) { - for (let i = 0; i < this.attributes.length; ++i) { + const attributes = this.childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); + for (let i = 0; i < attributes.length; ++i) { + const attribute = attributes[i]; if ( - this.attributes[i].namespaceUri == namespace && - this.attributes[i].localName == this.qualifiedNameToParts(`${name}`)[1] + attribute.namespaceUri == namespace && + attribute.localName == this.qualifiedNameToParts(`${name}`)[1] ) { - this.attributes[i].nodeValue = `${value}`; - this.attributes[i].nodeName = `${name}`; - this.attributes[i].prefix = this.qualifiedNameToParts(`${name}`)[0]; + attribute.nodeValue = `${value}`; + attribute.nodeName = `${name}`; + attribute.prefix = this.qualifiedNameToParts(`${name}`)[0]; return; } } - this.attributes.push(XNode.create(DOM_ATTRIBUTE_NODE, name, value, this, namespace)); + const newAttribute = XNode.create(DOM_ATTRIBUTE_NODE, name, value, this, namespace); + newAttribute.parentNode = this; + this.appendChild(newAttribute); } - getAttributeValue(name: any): any { - for (let i = 0; i < this.attributes.length; ++i) { - if (this.attributes[i].nodeName == name) { - return this.attributes[i].nodeValue; + getAttributeValue(name: string): any { + const attributes = this.childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); + for (let i = 0; i < attributes.length; ++i) { + if (attributes[i].nodeName === name) { + return attributes[i].nodeValue; } } @@ -389,18 +396,21 @@ export class XNode { } getAttributeNS(namespace: any, localName: any) { - for (let i = 0; i < this.attributes.length; ++i) { - if (this.attributes[i].namespaceUri == namespace && this.attributes[i].localName == localName) { - return this.attributes[i].nodeValue; + const attributes = this.childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); + for (let i = 0; i < attributes.length; ++i) { + const attribute = attributes[i]; + if (attribute.namespaceUri === namespace && attribute.localName === localName) { + return attribute.nodeValue; } } return null; } - hasAttribute(name: any) { - for (let i = 0; i < this.attributes.length; ++i) { - if (this.attributes[i].nodeName == name) { + hasAttribute(name: string) { + const attributes = this.childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); + for (let i = 0; i < attributes.length; ++i) { + if (attributes[i].nodeName === name) { return true; } } @@ -408,42 +418,58 @@ export class XNode { return false; } - hasAttributeNS(namespace: any, localName: any) { - for (let i = 0; i < this.attributes.length; ++i) { - if (this.attributes[i].namespaceUri == namespace && this.attributes[i].localName == localName) { + hasAttributeNS(namespace: string, localName: string) { + const attributes = this.childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); + for (let i = 0; i < attributes.length; ++i) { + const attribute = attributes[i]; + if (attribute.namespaceUri === namespace && attribute.localName === localName) { return true; } } return false; } - removeAttribute(name: any) { - const a = []; - for (let i = 0; i < this.attributes.length; ++i) { - if (this.attributes[i].nodeName != name) { - a.push(this.attributes[i]); + removeAttribute(name: string) { + const newChildNodes: XNode[] = []; + for (let i = 0; i < this.childNodes.length; ++i) { + const childNode = this.childNodes[i]; + if (childNode.nodeType !== DOM_ATTRIBUTE_NODE) { + newChildNodes.push(childNode); + continue; + } + + if (childNode.nodeName !== name) { + newChildNodes.push(childNode); } } - this.attributes = a; + + this.childNodes = newChildNodes; } - removeAttributeNS(namespace: any, localName: any) { - const a = []; - for (let i = 0; i < this.attributes.length; ++i) { - if (this.attributes[i].localName != localName || this.attributes[i].namespaceUri != namespace) { - a.push(this.attributes[i]); + removeAttributeNS(namespace: string, localName: string) { + const newChildNodes: XNode[] = []; + for (let i = 0; i < this.childNodes.length; ++i) { + const childNode = this.childNodes[i]; + if (childNode.nodeType !== DOM_ATTRIBUTE_NODE) { + newChildNodes.push(childNode); + continue; + } + + if (childNode.localName !== localName || childNode.namespaceUri !== namespace) { + newChildNodes.push(childNode); } } - this.attributes = a; + + this.childNodes = newChildNodes; } - getElementsByTagName(name: any) { + getElementsByTagName(name: string) { const ret = []; const self = this; if ('*' == name) { this.domTraverseElements( this, - (node: any) => { + (node: XNode) => { if (self == node) return; ret.push(node); }, @@ -452,7 +478,7 @@ export class XNode { } else { this.domTraverseElements( this, - (node: any) => { + (node: XNode) => { if (self == node) return; if (node.nodeName == name) { ret.push(node); @@ -464,7 +490,7 @@ export class XNode { return ret; } - getElementsByTagNameNS(namespace: any, localName: any) { + getElementsByTagNameNS(namespace: string, localName: string) { const ret = []; const self = this; if ('*' == namespace && '*' == localName) { diff --git a/src/xpath/common-function.ts b/src/xpath/common-function.ts index 0f78d93..e8a12cb 100644 --- a/src/xpath/common-function.ts +++ b/src/xpath/common-function.ts @@ -14,7 +14,7 @@ export function copyArray(dst: any[], src: any[]) { * significant extra processing when evaluating attribute steps. With this * function, we ignore any such attributes that has an empty string value. */ -export function copyArrayIgnoringAttributesWithoutValue(dst, src) { +export function copyArrayIgnoringAttributesWithoutValue(dst: any[], src: any[]) { if (!src) return; for (let i = src.length - 1; i >= 0; --i) { // this test will pass so long as the attribute has a non-empty string diff --git a/src/xpath/expr-context.ts b/src/xpath/expr-context.ts index d075a53..d083cb0 100644 --- a/src/xpath/expr-context.ts +++ b/src/xpath/expr-context.ts @@ -6,6 +6,7 @@ import { StringValue } from './values/string-value'; import { TOK_NUMBER } from './tokens'; import { XNode } from '../dom'; import { XsltDecimalFormatSettings } from '../xslt/xslt-decimal-format-settings'; +import { NodeValue } from './values'; /** * XPath expression evaluation context. An XPath context consists of a @@ -49,7 +50,7 @@ export class ExprContext { outputDepth: number; xsltVersion: '1.0' | '2.0' | '3.0'; - variables: { [name: string]: any }; + variables: { [name: string]: NodeValue }; knownNamespaces: { [alias: string]: string }; caseInsensitive: any; @@ -217,7 +218,7 @@ export class ExprContext { } } - getVariable(name: string) { + getVariable(name: string): NodeValue { if (typeof this.variables[name] != 'undefined') { return this.variables[name]; } diff --git a/src/xpath/expressions/predicate-expr.ts b/src/xpath/expressions/predicate-expr.ts index 05895fe..aaad584 100644 --- a/src/xpath/expressions/predicate-expr.ts +++ b/src/xpath/expressions/predicate-expr.ts @@ -3,22 +3,22 @@ import { BooleanValue } from "../values/boolean-value"; import { Expression } from "./expression"; export class PredicateExpr extends Expression { - expr: Expression; + expression: Expression; - constructor(expr: Expression) { + constructor(expression: Expression) { super(); - this.expr = expr; + this.expression = expression; } evaluate(context: ExprContext) { - const v = this.expr.evaluate(context); - if (v.type == 'number') { + const value = this.expression.evaluate(context); + if (value.type == 'number') { // NOTE(mesch): Internally, position is represented starting with // 0, however in XPath position starts with 1. See functions // position() and last(). - return new BooleanValue(context.position == v.numberValue() - 1); + return new BooleanValue(context.position == value.numberValue() - 1); } - return new BooleanValue(v.booleanValue()); + return new BooleanValue(value.booleanValue()); } } diff --git a/src/xpath/expressions/step-expr.ts b/src/xpath/expressions/step-expr.ts index 74155bb..b77cf8d 100644 --- a/src/xpath/expressions/step-expr.ts +++ b/src/xpath/expressions/step-expr.ts @@ -11,6 +11,7 @@ import { FunctionCallExpr } from './function-call-expr'; import { NumberExpr } from './number-expr'; import { UnaryMinusExpr } from './unary-minus-expr'; import { copyArray, copyArrayIgnoringAttributesWithoutValue } from '../common-function'; +import { PredicateExpr } from './predicate-expr'; export class StepExpr extends Expression { axis: any; @@ -43,7 +44,7 @@ export class StepExpr extends Expression { * selector may be based on the result of evaluating predicates that precede * it. */ - private predicateExprHasPositionalSelector(expr: any, isRecursiveCall?: any) { + private predicateExprHasPositionalSelector(expr: Expression, isRecursiveCall?: any) { if (!expr) { return false; } @@ -102,15 +103,15 @@ export class StepExpr extends Expression { return false; } - appendPredicate(p) { - this.predicate.push(p); + appendPredicate(predicateExpression: PredicateExpr) { + this.predicate.push(predicateExpression); if (!this.hasPositionalPredicate) { - this.hasPositionalPredicate = this.predicateExprHasPositionalSelector(p.expr); + this.hasPositionalPredicate = this.predicateExprHasPositionalSelector(predicateExpression.expression); } } evaluate(context: ExprContext) { - const input = context.nodeList[context.position]; + const node = context.nodeList[context.position]; let nodeList = []; let skipNodeTest = false; @@ -120,65 +121,74 @@ export class StepExpr extends Expression { switch (this.axis) { case xPathAxis.ANCESTOR_OR_SELF: - nodeList.push(input); - for (let n = input.parentNode; n; n = n.parentNode) { - nodeList.push(n); + nodeList.push(node); + for (let n = node.parentNode; n; n = n.parentNode) { + if (n.nodeType !== DOM_ATTRIBUTE_NODE) { + nodeList.push(n); + } } break; case xPathAxis.ANCESTOR: - for (let n = input.parentNode; n; n = n.parentNode) { - nodeList.push(n); + for (let n = node.parentNode; n; n = n.parentNode) { + if (n.nodeType !== DOM_ATTRIBUTE_NODE) { + nodeList.push(n); + } } break; case xPathAxis.ATTRIBUTE: - if (this.nodeTest.name != undefined) { + const attributes = node.childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); + if (this.nodeTest.name !== undefined) { // single-attribute step - if (input.attributes) { - if (input.attributes instanceof Array) { + if (attributes) { + if (attributes instanceof Array) { // probably evaluating on document created by xmlParse() - copyArray(nodeList, input.attributes); + copyArray(nodeList, attributes); } else { + // TODO: I think this `else` does't make any sense now. + // Before unifying attributes with child nodes, `node.attributes` was always an array. if (this.nodeTest.name == 'style') { - const value = input.getAttributeValue('style'); + const value = node.getAttributeValue('style'); if (value && typeof value != 'string') { // this is the case where indexing into the attributes array // doesn't give us the attribute node in IE - we create our own // node instead nodeList.push(XNode.create(DOM_ATTRIBUTE_NODE, 'style', value.cssText, document)); } else { - nodeList.push(input.attributes[this.nodeTest.name]); + nodeList.push(attributes[this.nodeTest.name]); } } else { - nodeList.push(input.attributes[this.nodeTest.name]); + nodeList.push(attributes[this.nodeTest.name]); } } } } else { // all-attributes step if (context.ignoreAttributesWithoutValue) { - copyArrayIgnoringAttributesWithoutValue(nodeList, input.attributes); + copyArrayIgnoringAttributesWithoutValue(nodeList, attributes); } else { - copyArray(nodeList, input.attributes); + copyArray(nodeList, attributes); } } break; case xPathAxis.CHILD: - copyArray(nodeList, input.childNodes); + copyArray(nodeList, node.childNodes.filter(n => n.nodeType !== DOM_ATTRIBUTE_NODE)); break; case xPathAxis.DESCENDANT_OR_SELF: { if (this.nodeTest.evaluate(context).booleanValue()) { - nodeList.push(input); + nodeList.push(node); } + let tagName = this.xPath.xPathExtractTagNameFromNodeTest( this.nodeTest, context.ignoreNonElementNodesForNTA ); - this.xPath.xPathCollectDescendants(nodeList, input, tagName); + + this.xPath.xPathCollectDescendants(nodeList, node, tagName); if (tagName) skipNodeTest = true; break; @@ -189,16 +199,19 @@ export class StepExpr extends Expression { this.nodeTest, context.ignoreNonElementNodesForNTA ); - this.xPath.xPathCollectDescendants(nodeList, input, tagName); + this.xPath.xPathCollectDescendants(nodeList, node, tagName); if (tagName) skipNodeTest = true; break; } case xPathAxis.FOLLOWING: - for (let n = input; n; n = n.parentNode) { + for (let n = node; n; n = n.parentNode) { for (let nn = n.nextSibling; nn; nn = nn.nextSibling) { - nodeList.push(nn); + if (nn.nodeType !== DOM_ATTRIBUTE_NODE) { + nodeList.push(nn); + } + this.xPath.xPathCollectDescendants(nodeList, nn); } } @@ -206,8 +219,14 @@ export class StepExpr extends Expression { break; case xPathAxis.FOLLOWING_SIBLING: - for (let n = input.nextSibling; n; n = n.nextSibling) { - nodeList.push(n); + if (node.nodeType === DOM_ATTRIBUTE_NODE) { + break; + } + + for (let n = node.nextSibling; n; n = n.nextSibling) { + if (n.nodeType !== DOM_ATTRIBUTE_NODE) { + nodeList.push(n); + } } break; @@ -216,16 +235,19 @@ export class StepExpr extends Expression { throw new Error('not implemented: axis namespace'); case xPathAxis.PARENT: - if (input.parentNode) { - nodeList.push(input.parentNode); + if (node.parentNode) { + nodeList.push(node.parentNode); } break; case xPathAxis.PRECEDING: - for (let n = input; n; n = n.parentNode) { + for (let n = node; n; n = n.parentNode) { for (let nn = n.previousSibling; nn; nn = nn.previousSibling) { - nodeList.push(nn); + if (nn.nodeType !== DOM_ATTRIBUTE_NODE) { + nodeList.push(nn); + } + this.xPath.xPathCollectDescendantsReverse(nodeList, nn); } } @@ -233,19 +255,23 @@ export class StepExpr extends Expression { break; case xPathAxis.PRECEDING_SIBLING: - for (let n = input.previousSibling; n; n = n.previousSibling) { - nodeList.push(n); + for (let n = node.previousSibling; n; n = n.previousSibling) { + if (n.nodeType !== DOM_ATTRIBUTE_NODE) { + nodeList.push(n); + } } break; case xPathAxis.SELF: - nodeList.push(input); + nodeList.push(node); break; case xPathAxis.SELF_AND_SIBLINGS: for (const node of context.nodeList) { - nodeList.push(node); + if (node.nodeType !== DOM_ATTRIBUTE_NODE) { + nodeList.push(node); + } } break; diff --git a/src/xpath/expressions/variable-expr.ts b/src/xpath/expressions/variable-expr.ts index 0b150cb..618a6f9 100644 --- a/src/xpath/expressions/variable-expr.ts +++ b/src/xpath/expressions/variable-expr.ts @@ -9,7 +9,7 @@ export class VariableExpr extends Expression { this.name = name; } - evaluate(ctx: ExprContext) { - return ctx.getVariable(this.name); + evaluate(context: ExprContext) { + return context.getVariable(this.name); } } diff --git a/src/xpath/values/boolean-value.ts b/src/xpath/values/boolean-value.ts index 0f75f48..001d3a6 100644 --- a/src/xpath/values/boolean-value.ts +++ b/src/xpath/values/boolean-value.ts @@ -1,3 +1,4 @@ +import { XNode } from "../../dom"; import { NodeValue } from "./node-value"; export class BooleanValue implements NodeValue { @@ -21,7 +22,7 @@ export class BooleanValue implements NodeValue { return this.value ? 1 : 0; } - nodeSetValue() { + nodeSetValue(): XNode[] { throw this; } } diff --git a/src/xpath/values/node-set-value.ts b/src/xpath/values/node-set-value.ts index 2b6b631..3161a32 100644 --- a/src/xpath/values/node-set-value.ts +++ b/src/xpath/values/node-set-value.ts @@ -26,7 +26,7 @@ export class NodeSetValue implements NodeValue { return parseInt(this.stringValue()) - 0; } - nodeSetValue() { + nodeSetValue(): XNode[] { return this.value; } } diff --git a/src/xpath/values/node-value.ts b/src/xpath/values/node-value.ts index 4cf73fd..dccef9f 100644 --- a/src/xpath/values/node-value.ts +++ b/src/xpath/values/node-value.ts @@ -4,5 +4,5 @@ export interface NodeValue { stringValue(): string; booleanValue(): boolean; numberValue(): number; - nodeSetValue(): XNode[] | void; + nodeSetValue(): XNode[]; } diff --git a/src/xpath/values/number-value.ts b/src/xpath/values/number-value.ts index 37ea372..a4424d6 100644 --- a/src/xpath/values/number-value.ts +++ b/src/xpath/values/number-value.ts @@ -1,3 +1,4 @@ +import { XNode } from "../../dom"; import { NodeValue } from "./node-value"; export class NumberValue implements NodeValue { @@ -21,7 +22,7 @@ export class NumberValue implements NodeValue { return this.value - 0; } - nodeSetValue() { + nodeSetValue(): XNode[] { throw this; } } diff --git a/src/xpath/values/string-value.ts b/src/xpath/values/string-value.ts index b1f56df..b626312 100644 --- a/src/xpath/values/string-value.ts +++ b/src/xpath/values/string-value.ts @@ -1,3 +1,4 @@ +import { XNode } from "../../dom"; import { NodeValue } from "./node-value"; export class StringValue implements NodeValue { @@ -21,7 +22,7 @@ export class StringValue implements NodeValue { return this.value - 0; } - nodeSetValue() { + nodeSetValue(): XNode[] { throw this; } } diff --git a/src/xpath/xpath.ts b/src/xpath/xpath.ts index f1c0bea..8d96ed8 100644 --- a/src/xpath/xpath.ts +++ b/src/xpath/xpath.ts @@ -120,7 +120,9 @@ import { import { GrammarRuleCandidate } from './grammar-rule-candidate'; import { XPathTokenRule } from './xpath-token-rule'; import { XNode } from '../dom'; -import { NodeTestAny, NodeTestElementOrAttribute, NodeTestNC, NodeTestName, NodeTestText, NodeTestComment, NodeTestPI } from './node-tests'; +import { NodeTestAny, NodeTestElementOrAttribute, NodeTestNC, NodeTestName, NodeTestText, NodeTestComment, NodeTestPI, NodeTest } from './node-tests'; +import { DOM_ATTRIBUTE_NODE } from '../constants'; +import { NodeValue } from './values'; export class XPath { xPathParseCache: any; @@ -508,14 +510,17 @@ export class XPath { return this.xPathParseCache[expr]; } - xPathCollectDescendants(nodeList: any, node: any, opt_tagName?: any) { + xPathCollectDescendants(nodeList: XNode[], node: XNode, opt_tagName?: string) { if (opt_tagName && node.getElementsByTagName) { copyArray(nodeList, node.getElementsByTagName(opt_tagName)); return; } for (let n = node.firstChild; n; n = n.nextSibling) { - nodeList.push(n); + if (n.nodeType !== DOM_ATTRIBUTE_NODE) { + nodeList.push(n); + } + this.xPathCollectDescendants(nodeList, n); } } @@ -527,12 +532,17 @@ export class XPath { } } - // Parses and then evaluates the given XPath expression in the given - // input context. Notice that parsed xpath expressions are cached. - xPathEval(select: string, context: ExprContext) { - const expr = this.xPathParse(select); - const ret = expr.evaluate(context); - return ret; + /** + * Parses and then evaluates the given XPath expression in the given + * input context. + * @param select The xPath string. + * @param context The Expression Context. + * @returns A Node Value. + */ + xPathEval(select: string, context: ExprContext): NodeValue { + const expression = this.xPathParse(select); + const response = expression.evaluate(context); + return response; } /** @@ -544,7 +554,7 @@ export class XPath { * non-element nodes. This can boost * performance. This is false by default. */ - xPathExtractTagNameFromNodeTest(nodeTest: any, ignoreNonElementNodesForNTA: any) { + xPathExtractTagNameFromNodeTest(nodeTest: NodeTest, ignoreNonElementNodesForNTA: any): string { if (nodeTest instanceof NodeTestName) { return nodeTest.name; } diff --git a/src/xslt/xslt.ts b/src/xslt/xslt.ts index 33a16c0..a4b18e1 100644 --- a/src/xslt/xslt.ts +++ b/src/xslt/xslt.ts @@ -442,8 +442,8 @@ export class Xslt { case 'text': text = xmlValue(template); node = domCreateTransformedTextNode(this.outputDocument, text); - const disableOutputEscaping = template.attributes.filter( - (a) => a.nodeName === 'disable-output-escaping' + const disableOutputEscaping = template.childNodes.filter( + (a) => a.nodeType === DOM_ATTRIBUTE_NODE && a.nodeName === 'disable-output-escaping' ); if (disableOutputEscaping.length > 0 && disableOutputEscaping[0].nodeValue === 'yes') { node.escape = false; @@ -553,9 +553,9 @@ export class Xslt { if (node) { // This was an element node -- recurse to attributes and // children. - for (let i = 0; i < source.attributes.length; ++i) { + /* for (let i = 0; i < source.attributes.length; ++i) { this.xsltCopyOf(node, source.attributes[i]); - } + } */ for (let i = 0; i < source.childNodes.length; ++i) { this.xsltCopyOf(node, source.childNodes[i]); @@ -636,7 +636,7 @@ export class Xslt { * @param output The output XML. */ protected xsltTransformOrStylesheet(template: XNode, context: ExprContext, output: XNode): void { - for (let stylesheetAttribute of template.attributes) { + for (let stylesheetAttribute of template.childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE)) { switch (stylesheetAttribute.nodeName) { case 'version': this.version = stylesheetAttribute.nodeValue; @@ -675,14 +675,13 @@ export class Xslt { let value: any; - if (template.childNodes.length > 0) { + const nonAttributeChildren = template.childNodes.filter(n => n.nodeType !== DOM_ATTRIBUTE_NODE); + if (nonAttributeChildren.length > 0) { const root = domCreateDocumentFragment(template.ownerDocument); this.xsltChildNodes(context, template, root); value = new NodeSetValue([root]); } else if (select) { value = this.xPath.xPathEval(select, context); - } else if (name in context.variables) { - value = context.variables[name]; } else { let parameterValue = ''; const filteredParameter = this.options.parameters.filter((p) => p.name === name); @@ -775,13 +774,14 @@ export class Xslt { newNode.transformedLocalName = template.localName; // The node can have transformed attributes from previous transformations. - for (const previouslyTransformedAttribute of node.transformedAttributes) { + const transformedAttributes = node.transformedChildNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); + for (const previouslyTransformedAttribute of transformedAttributes) { const name = previouslyTransformedAttribute.transformedNodeName; const value = previouslyTransformedAttribute.transformedNodeValue; domSetTransformedAttribute(newNode, name, value); } - const templateAttributes = template.attributes.filter((a: any) => a); + const templateAttributes = template.childNodes.filter((a: XNode) => a?.nodeType === DOM_ATTRIBUTE_NODE); for (const attribute of templateAttributes) { const name = attribute.nodeName; const value = this.xsltAttributeValue(attribute.nodeValue, elementContext); @@ -841,7 +841,7 @@ export class Xslt { } protected xsltAttribute(attributeName: string, context: ExprContext): XNode { - return context.nodeList[context.position].attributes.find(a => a.nodeName === attributeName); + return context.nodeList[context.position].childNodes.find((a: XNode) => a.nodeType === DOM_ATTRIBUTE_NODE && a.nodeName === attributeName); } /** diff --git a/tests/dom.test.tsx b/tests/dom.test.tsx index 7f30dfc..40bb994 100644 --- a/tests/dom.test.tsx +++ b/tests/dom.test.tsx @@ -13,6 +13,7 @@ import { XmlParser, xmlText } from '../src/dom'; import assert from 'assert'; import { dom } from 'isomorphic-jsx'; +import { DOM_ATTRIBUTE_NODE } from '../src/constants'; // Just touching the `dom`, otherwise Babel prunes the import. console.log(dom); @@ -35,12 +36,14 @@ describe('dom parsing', () => { const dom2 = xmlParser.xmlParse(`${xml}`); doTestXmlParse(dom1, dom2); + const dom1Attributes = dom1.firstChild.childNodes[1].childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); + const dom2Attributes = dom2.firstChild.childNodes[1].childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); + assert.equal( - dom1.firstChild.childNodes[1].attributes.length, - dom2.firstChild.childNodes[1].attributes.length, + dom1Attributes.length, + dom2Attributes.length, 'location.attributes.length' ); - // assert.equal(dom1.firstChild.childNodes[1].attributes.length, 2, 'location.attributes.length'); const tag = 'q'; const byTag = dom1.getElementsByTagName(tag); @@ -67,12 +70,15 @@ describe('dom parsing', () => { const dom2 = xmlParser.xmlParse(`${xml}`); doTestXmlParse(dom1, dom2); + const dom1Attributes = dom1.firstChild.childNodes[1].childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); + const dom2Attributes = dom2.firstChild.childNodes[1].childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); + assert.equal( - dom1.firstChild.childNodes[1].attributes.length, - dom2.firstChild.childNodes[1].attributes.length, + dom1Attributes.length, + dom2Attributes.length, 'location.attributes.length' ); - assert.equal(dom1.firstChild.childNodes[1].attributes.length, 2, 'location.attributes.length'); + assert.equal(dom1Attributes.length, 2, 'location.attributes.length'); }); it('can parse Japanese xml', () => { @@ -90,12 +96,15 @@ describe('dom parsing', () => { const dom2 = xmlParser.xmlParse(`${xml}`); doTestXmlParse(dom1, dom2); + const dom1Attributes = dom1.firstChild.childNodes[1].childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); + const dom2Attributes = dom2.firstChild.childNodes[1].childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); + assert.equal( - dom1.firstChild.childNodes[1].attributes.length, - dom2.firstChild.childNodes[1].attributes.length, + dom1Attributes.length, + dom2Attributes.length, 'location.attributes.length' ); - assert.equal(dom1.firstChild.childNodes[1].attributes.length, 2, 'location.attributes.length'); + assert.equal(dom1Attributes.length, 2, 'location.attributes.length'); }); it('can resolve entities', () => { diff --git a/tests/xpath/xpath.test.tsx b/tests/xpath/xpath.test.tsx index ac81ee2..ea84ebf 100644 --- a/tests/xpath/xpath.test.tsx +++ b/tests/xpath/xpath.test.tsx @@ -615,16 +615,16 @@ describe('xpath', () => { ' ', '' ].join(''); - const ctx = new ExprContext([xmlParser.xmlParse(xml)], []); - - for (const e of axisTests) { - const result = xPath.xPathParse(e[0] as any).evaluate(ctx); - if (typeof e[1] == 'number') { - assert.equal(e[1], result.numberValue(), e[0] as any); - } else if (typeof e[1] == 'string') { - assert.equal(e[1], result.stringValue(), e[0] as any); - } else if (typeof e[1] == 'boolean') { - assert.equal(e[1], result.booleanValue(), e[0] as any); + const context = new ExprContext([xmlParser.xmlParse(xml)], []); + + for (const axisTest of axisTests) { + const result = xPath.xPathParse(axisTest[0] as any).evaluate(context); + if (typeof axisTest[1] === 'number') { + assert.equal(result.numberValue(),axisTest[1], axisTest[0] as string); + } else if (typeof axisTest[1] === 'string') { + assert.equal(result.stringValue(), axisTest[1], axisTest[0] as string); + } else if (typeof axisTest[1] === 'boolean') { + assert.equal(result.booleanValue(), axisTest[1], axisTest[0] as string); } } }); @@ -705,7 +705,8 @@ describe('xpath', () => { ]; for (const test of tests) { - assert.equal(xPath.xPathParse(test[0] as any).steps[1].hasPositionalPredicate, test[1], test[0] as any); + const xPathParseResult = xPath.xPathParse(test[0] as string); + assert.equal(test[1], xPathParseResult.steps[1].hasPositionalPredicate, test[0] as string); } });