xpath-ts2
Version:
DOM 3 and 4 XPath 1.0 implementation for browser and Node.js environment with support for typescript 5.
371 lines (329 loc) • 10.8 kB
text/typescript
import { XML_NAMESPACE_URI, XMLNS_NAMESPACE_URI } from './consts';
import { LocationPath } from './location-path';
import { Step } from './step';
import { isAttribute, isDocument, isElement } from './utils/types';
import { XPathNamespace } from './xpath-namespace';
import { Expression, XNodeSet, XNumber, XPathContext, XString } from './xpath-types';
export class PathExpr extends Expression {
static predicateMatches(pred: Expression, c: XPathContext) {
const res = pred.evaluate(c);
return res instanceof XNumber ? c.contextPosition === res.numberValue : res.booleanValue;
}
static applyLocationPath(locationPath: LocationPath | undefined, xpc: XPathContext, nodes: Node[]) {
if (!locationPath) {
return nodes;
}
const startNodes = locationPath.absolute ? [PathExpr.getRoot(xpc, nodes)] : nodes;
return PathExpr.applySteps(locationPath.steps, xpc, startNodes);
}
static applyStep(step: Step, xpc: XPathContext, node: Node): Node[] {
const newNodes = [];
xpc.contextNode = node;
switch (step.axis) {
case Step.ANCESTOR: {
// look at all the ancestor nodes
if (xpc.contextNode === xpc.virtualRoot) {
break;
}
let m: Node | null;
if (isAttribute(xpc.contextNode)) {
m = PathExpr.getOwnerElement(xpc.contextNode);
} else {
m = xpc.contextNode.parentNode;
}
while (m != null) {
if (step.nodeTest.matches(m, xpc)) {
newNodes.push(m);
}
if (m === xpc.virtualRoot) {
break;
}
m = m.parentNode;
}
break;
}
case Step.ANCESTORORSELF: {
// look at all the ancestor nodes and the current node
for (
let m: Node | null = xpc.contextNode;
m != null;
m = isAttribute(m) ? PathExpr.getOwnerElement(m) : m.parentNode
) {
if (step.nodeTest.matches(m, xpc)) {
newNodes.push(m);
}
if (m === xpc.virtualRoot) {
break;
}
}
break;
}
case Step.ATTRIBUTE: {
// look at the attributes
const nnm = (xpc.contextNode as Element).attributes;
if (nnm != null) {
for (let k = 0; k < nnm.length; k++) {
const m = nnm.item(k)!;
if (step.nodeTest.matches(m, xpc)) {
newNodes.push(m);
}
}
}
break;
}
case Step.CHILD: {
// look at all child elements
for (let m: Node | null = xpc.contextNode.firstChild; m != null; m = m.nextSibling) {
if (step.nodeTest.matches(m, xpc)) {
newNodes.push(m);
}
}
break;
}
case Step.DESCENDANT: {
// look at all descendant nodes
const st: Array<Node | null> = [xpc.contextNode.firstChild];
while (st.length > 0) {
for (let m = st.pop(); m != null; ) {
if (step.nodeTest.matches(m, xpc)) {
newNodes.push(m);
}
if (m.firstChild != null) {
st.push(m.nextSibling);
m = m.firstChild;
} else {
m = m.nextSibling;
}
}
}
break;
}
case Step.DESCENDANTORSELF: {
// look at self
if (step.nodeTest.matches(xpc.contextNode, xpc)) {
newNodes.push(xpc.contextNode);
}
// look at all descendant nodes
const st: Array<Node | null> = [xpc.contextNode.firstChild];
while (st.length > 0) {
for (let m: Node | null = st.pop()!; m != null; ) {
if (step.nodeTest.matches(m, xpc)) {
newNodes.push(m);
}
if (m.firstChild != null) {
st.push(m.nextSibling);
m = m.firstChild;
} else {
m = m.nextSibling;
}
}
}
break;
}
case Step.FOLLOWING: {
if (xpc.contextNode === xpc.virtualRoot) {
break;
}
for (let n: Node | null = xpc.contextNode; n != null; n = n.parentNode) {
for (let m: Node | null = n.nextSibling; m != null; m = m.nextSibling) {
if (step.nodeTest.matches(m, xpc)) {
newNodes.push(m);
}
}
}
break;
}
case Step.FOLLOWINGSIBLING: {
if (xpc.contextNode === xpc.virtualRoot) {
break;
}
for (let m = xpc.contextNode.nextSibling; m != null; m = m.nextSibling) {
if (step.nodeTest.matches(m, xpc)) {
newNodes.push(m);
}
}
break;
}
case Step.NAMESPACE: {
const n: { [name: string]: string } = {};
if (isElement(xpc.contextNode)) {
n.xml = XML_NAMESPACE_URI;
n.xmlns = XMLNS_NAMESPACE_URI;
for (let m: Node | null = xpc.contextNode; m != null && isElement(m); m = m.parentNode) {
for (let k = 0; k < m.attributes.length; k++) {
const attr = m.attributes.item(k)!;
const nm = String(attr.name);
if (nm === 'xmlns') {
if (n[''] === undefined) {
n[''] = attr.value;
}
} else if (nm.length > 6 && nm.substring(0, 6) === 'xmlns:') {
const pre = nm.substring(6, nm.length);
if (n[pre] === undefined) {
n[pre] = attr.value;
}
}
}
}
// tslint:disable-next-line:forin
for (const pre in n) {
const nsn = new XPathNamespace(pre, n[pre], xpc.contextNode);
if (step.nodeTest.matches(nsn, xpc)) {
newNodes.push(nsn);
}
}
}
break;
}
case Step.PARENT: {
let m: Node | null = null;
if (xpc.contextNode !== xpc.virtualRoot) {
if (isAttribute(xpc.contextNode)) {
m = PathExpr.getOwnerElement(xpc.contextNode);
} else {
m = xpc.contextNode.parentNode;
}
}
if (m != null && step.nodeTest.matches(m, xpc)) {
newNodes.push(m);
}
break;
}
case Step.PRECEDING: {
if (xpc.contextNode === xpc.virtualRoot) {
break;
}
for (let n: Node | null = xpc.contextNode; n != null; n = n.parentNode) {
for (let m: Node | null = n.previousSibling; m != null; m = m.previousSibling) {
if (step.nodeTest.matches(m, xpc)) {
newNodes.push(m);
}
}
}
break;
}
case Step.PRECEDINGSIBLING: {
if (xpc.contextNode === xpc.virtualRoot) {
break;
}
for (let m = xpc.contextNode.previousSibling; m != null; m = m.previousSibling) {
if (step.nodeTest.matches(m, xpc)) {
newNodes.push(m);
}
}
break;
}
case Step.SELF: {
if (step.nodeTest.matches(xpc.contextNode, xpc)) {
newNodes.push(xpc.contextNode);
}
break;
}
default:
}
return newNodes;
}
static getRoot(xpc: XPathContext, nodes: Node[]) {
const firstNode = nodes[0];
if (isDocument(firstNode)) {
return firstNode;
}
if (xpc.virtualRoot) {
return xpc.virtualRoot;
}
const ownerDoc = firstNode.ownerDocument;
if (ownerDoc) {
return ownerDoc;
}
// IE 5.5 doesn't have ownerDocument?
let n = firstNode;
while (n.parentNode != null) {
n = n.parentNode;
}
return n;
}
static applySteps(steps: Step[], xpc: XPathContext, nodes: Node[]) {
return steps.reduce((inNodes: Node[], step: Step) => {
return inNodes
.map((node) => {
return PathExpr.applyPredicates(step.predicates, xpc, PathExpr.applyStep(step, xpc, node));
})
.flat();
}, nodes);
}
static getOwnerElement(n: Attr) {
if (n.ownerElement) {
return n.ownerElement;
}
return null;
}
static applyPredicates(predicates: Expression[], c: XPathContext, nodes: Node[]) {
return predicates.reduce((inNodes, pred) => {
const ctx = c.extend({ contextSize: inNodes.length });
return inNodes.filter((node: Node, i: number) => {
return PathExpr.predicateMatches(pred, ctx.extend({ contextNode: node, contextPosition: i + 1 }));
});
}, nodes);
}
static predicateString = (e: Expression) => `[${e.toString()}]`;
static predicatesString = (es: Expression[]) => es.map(PathExpr.predicateString).join('');
filter?: Expression;
filterPredicates?: Expression[];
locationPath?: LocationPath;
constructor(
filter: Expression | undefined,
filterPreds: Expression[] | undefined,
locpath: LocationPath | undefined
) {
super();
this.filter = filter;
this.filterPredicates = filterPreds;
this.locationPath = locpath;
}
applyFilter(c: XPathContext, xpc: XPathContext) {
if (!this.filter) {
return { nodes: [c.contextNode!] };
}
const ns = this.filter.evaluate(c);
if (!(ns instanceof XNodeSet)) {
if ((this.filterPredicates != null && this.filterPredicates.length > 0) || this.locationPath != null) {
throw new Error('Path expression filter must evaluate to a nodeset if predicates or location path are used');
}
return { nonNodes: ns };
}
return {
nodes: PathExpr.applyPredicates(this.filterPredicates || [], xpc, ns.toUnsortedArray())
};
}
evaluate(c: XPathContext) {
const xpc = c.clone();
const filterResult = this.applyFilter(c, xpc);
if (filterResult.nonNodes !== undefined) {
return filterResult.nonNodes;
}
// filterResult.nodes is defined because nonNodes is not
const ns = new XNodeSet();
ns.addArray(PathExpr.applyLocationPath(this.locationPath, xpc, filterResult.nodes!));
return ns;
}
toString() {
if (this.filter !== undefined) {
const filterStr = this.filter.toString();
if (this.filter instanceof XString) {
return `'${filterStr}'`;
}
if (this.filterPredicates !== undefined && this.filterPredicates.length) {
return `(${filterStr})` + PathExpr.predicatesString(this.filterPredicates);
}
if (this.locationPath !== undefined) {
return filterStr + (this.locationPath.absolute ? '' : '/') + this.locationPath.toString();
}
return filterStr;
}
if (this.locationPath !== undefined) {
return this.locationPath.toString();
} else {
return '<Empty PathExpr>';
}
}
}