Skip to content

Commit 2a3ae23

Browse files
committed
Implement otherwise and combinating tests from XQ4
1 parent 67b87f8 commit 2a3ae23

18 files changed

+399
-68
lines changed

src/evaluateUpdatingExpression.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { Language, Logger, XMLSerializer } from './types/Options';
3030
* nodesFactory - Reference to a nodes factory object.
3131
* returnType - The type that the evaluation function will return.
3232
* xmlSerializer - An XML serializer that can serialize nodes. Used when the `fn:serialize` function is called with a node.
33+
* language - Whether to allow experimental XQuery 4 features in the script
3334
*/
3435
export type UpdatingOptions = {
3536
debug?: boolean;
@@ -41,6 +42,7 @@ export type UpdatingOptions = {
4142
nodesFactory?: INodesFactory;
4243
returnType?: ReturnType;
4344
xmlSerializer?: XMLSerializer;
45+
language?: Language.XQUERY_UPDATE_3_1_LANGUAGE | Language.XQUERY_UPDATE_4_0_LANGUAGE;
4446
};
4547

4648
/**
@@ -82,6 +84,7 @@ export default async function evaluateUpdatingExpression(
8284
allowXQuery: true,
8385
debug: !!options['debug'],
8486
disableCache: !!options['disableCache'],
87+
version: options.language === Language.XQUERY_UPDATE_4_0_LANGUAGE ? 4 : 3.1,
8588
},
8689
);
8790
dynamicContext = context.dynamicContext;

src/evaluateUpdatingExpressionSync.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import UpdatingExpressionResult from './expressions/UpdatingExpressionResult';
1212
import { IterationHint, IterationResult } from './expressions/util/iterators';
1313
import { IReturnTypes, ReturnType } from './parsing/convertXDMReturnValue';
1414
import { performStaticCompilationOnModules } from './parsing/globalModuleCache';
15+
import { Language } from './types/Options';
1516
import { Node } from './types/Types';
1617

1718
/**
@@ -57,6 +58,7 @@ export default function evaluateUpdatingExpressionSync<
5758
allowXQuery: true,
5859
debug: !!options['debug'],
5960
disableCache: !!options['disableCache'],
61+
version: options.language === Language.XQUERY_UPDATE_4_0_LANGUAGE ? 4 : 3.1,
6062
},
6163
);
6264
dynamicContext = context.dynamicContext;

src/evaluateXPath.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,12 @@ const evaluateXPath = <TNode extends Node, TReturnType extends keyof IReturnType
180180
options['language'] === Language.XQUERY_UPDATE_3_1_LANGUAGE,
181181
debug: !!options['debug'],
182182
disableCache: !!options['disableCache'],
183+
version:
184+
options['language'] === Language.XPATH_4_0_LANGUAGE ||
185+
options['language'] === Language.XQUERY_4_0_LANGUAGE ||
186+
options['language'] === Language.XQUERY_UPDATE_4_0_LANGUAGE
187+
? 4
188+
: 3.1,
183189
},
184190
);
185191
dynamicContext = context.dynamicContext;

src/evaluationUtils/buildEvaluationContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export default function buildEvaluationContext(
7171
allowXQuery: boolean;
7272
debug: boolean;
7373
disableCache: boolean;
74+
version: 3.1 | 4;
7475
},
7576
): {
7677
dynamicContext: DynamicContext;

src/expressions/functions/builtInFunctions_fontoxpath.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ function buildResultIterator(
6161
allowXQuery: true,
6262
debug: executionParameters.debug,
6363
disableCache: executionParameters.disableCache,
64+
version: 3.1,
6465
},
6566
(prefix) => staticContext.resolveNamespace(prefix),
6667
// Set up temporary bindings for the given variables
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import Value from '../dataTypes/Value';
2+
import DynamicContext from '../DynamicContext';
3+
import ExecutionParameters from '../ExecutionParameters';
4+
import {} from '../Expression';
5+
import Specificity from '../Specificity';
6+
import StaticContext from '../StaticContext';
7+
import { Bucket, unionBucket } from '../util/Bucket';
8+
import TestAbstractExpression from './TestAbstractExpression';
9+
10+
/**
11+
* Combinating test, used to process `descendant::(a|b|c)` selectors
12+
*/
13+
class CombinatingTest extends TestAbstractExpression {
14+
private _subTests: TestAbstractExpression[];
15+
private _bucket: Bucket;
16+
constructor(subTests: TestAbstractExpression[]) {
17+
const maxSpecificity = subTests.reduce<Specificity>((currentMaxSpecificity, selector) => {
18+
if (currentMaxSpecificity.compareTo(selector.specificity) > 0) {
19+
return currentMaxSpecificity;
20+
}
21+
return selector.specificity;
22+
}, new Specificity({}));
23+
24+
super(maxSpecificity);
25+
26+
// If all subExpressions define the same bucket: use that one, else, use no bucket.
27+
let bucket: Bucket | null;
28+
for (let i = 0; i < subTests.length; ++i) {
29+
const subTestBucket = subTests[i].getBucket();
30+
if (bucket === undefined) {
31+
bucket = subTestBucket;
32+
}
33+
if (bucket === null) {
34+
// Not applicable buckets
35+
break;
36+
}
37+
38+
bucket = unionBucket(bucket, subTestBucket);
39+
}
40+
41+
this._bucket = bucket;
42+
this._subTests = subTests;
43+
}
44+
45+
public evaluateToBoolean(
46+
dynamicContext: DynamicContext,
47+
value: Value,
48+
executionParameters: ExecutionParameters,
49+
) {
50+
return this._subTests.some((test) => {
51+
const result = test.evaluateToBoolean(dynamicContext, value, executionParameters);
52+
return result;
53+
});
54+
}
55+
56+
public override getBucket(): Bucket {
57+
return this._bucket;
58+
}
59+
60+
public performStaticEvaluation(staticContext: StaticContext) {
61+
this._subTests.forEach((test) => test.performStaticEvaluation(staticContext));
62+
}
63+
}
64+
65+
export default CombinatingTest;

src/expressions/util/Bucket.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ const subBucketsByBucket: Map<Bucket, Bucket[]> = new Map([
3535
['type-2', ['name']],
3636
]);
3737

38+
// Some buckets exclude others. For the purppose of determining their unions, this lists supertypes pet bucket.
39+
const superBucketsByBucket: Map<Bucket, Bucket> = new Map([
40+
['name', 'type-1-or-type-2'],
41+
['type-1', 'type-1-or-type-2'],
42+
['type-2', 'type-1-or-type-2'],
43+
]);
44+
3845
/**
3946
* Determine the intersection between the two passed buckets. The intersection is the 'strongest'
4047
* bucket of the two. This may return `empty` if there is no such intersection.
@@ -78,3 +85,54 @@ export function intersectBuckets(bucket1: Bucket | null, bucket2: Bucket | null)
7885
// Expression will never match any nodes
7986
return 'empty';
8087
}
88+
89+
/**
90+
* Determine the union between the two passed buckets. The union is the common denominator between buckets
91+
*
92+
* Example: `null` ∪ `name-div` = `null`
93+
* Example: `name-p` ∪ `name-div` = `type-1-or-type-2`
94+
* Example: `type-1` ∪ `name-p` = `type-1`
95+
* Example: `type-1` ∪ `empty` = `type-1`
96+
*
97+
* @param bucket1 - The first bucket to check
98+
* @param bucket2 - The second bucket to check
99+
*
100+
* @returns The union of the two buckets.
101+
*/
102+
export function unionBucket(bucket1: Bucket | null, bucket2: Bucket | null): Bucket | null {
103+
// null bucket applies to everything
104+
if (bucket1 === null || bucket2 === null) {
105+
return null;
106+
}
107+
if (bucket1 === 'empty') {
108+
return bucket2;
109+
}
110+
if (bucket2 === 'empty') {
111+
return bucket1;
112+
}
113+
// Same bucket is same
114+
if (bucket1 === bucket2) {
115+
return bucket1;
116+
}
117+
// Find the more specific one, given that the buckets are not equal
118+
const type1 = bucket1.startsWith('name-') ? 'name' : bucket1;
119+
const type2 = bucket2.startsWith('name-') ? 'name' : bucket2;
120+
121+
const supertypes1 = superBucketsByBucket.get(type1);
122+
if (supertypes1 !== undefined && supertypes1 === type2) {
123+
// bucket 2 includes bucket 1
124+
return supertypes1;
125+
}
126+
const supertypes2 = superBucketsByBucket.get(type2);
127+
if (supertypes2 !== undefined && supertypes2 === type1) {
128+
// bucket 1 includes bucket 2
129+
return supertypes2;
130+
}
131+
132+
if (supertypes1 === supertypes2) {
133+
return supertypes1;
134+
}
135+
136+
// Expression will never match any nodes
137+
return 'empty';
138+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import ISequence from '../dataTypes/ISequence';
2+
import sequenceFactory from '../dataTypes/sequenceFactory';
3+
import { SequenceType, ValueType } from '../dataTypes/Value';
4+
import DynamicContext from '../DynamicContext';
5+
import ExecutionParameters from '../ExecutionParameters';
6+
import Expression, { RESULT_ORDERINGS } from '../Expression';
7+
import PossiblyUpdatingExpression, { SequenceCallbacks } from '../PossiblyUpdatingExpression';
8+
import Specificity from '../Specificity';
9+
10+
/**
11+
* The 'otherwise' expression: returns the first operand that's not empty
12+
*/
13+
class Otherwise extends PossiblyUpdatingExpression {
14+
constructor(expressions: Expression[], type: SequenceType) {
15+
const maxSpecificity = expressions.reduce((maxSpecificitySoFar, expression) => {
16+
if (maxSpecificitySoFar.compareTo(expression.specificity) > 0) {
17+
return maxSpecificitySoFar;
18+
}
19+
return expression.specificity;
20+
}, new Specificity({}));
21+
super(
22+
maxSpecificity,
23+
expressions,
24+
{
25+
canBeStaticallyEvaluated: expressions.every(
26+
(expression) => expression.canBeStaticallyEvaluated,
27+
),
28+
},
29+
type,
30+
);
31+
}
32+
33+
public override performFunctionalEvaluation(
34+
dynamicContext: DynamicContext,
35+
_executionParameters: ExecutionParameters,
36+
sequenceCallbacks: SequenceCallbacks,
37+
): ISequence {
38+
for (const cb of sequenceCallbacks) {
39+
const sequence = cb(dynamicContext);
40+
if (!sequence.isEmpty()) {
41+
return sequence;
42+
}
43+
}
44+
return sequenceFactory.empty();
45+
}
46+
}
47+
export default Otherwise;

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ function parseXPath(xpathExpression: EvaluableExpression): Expression {
8484

8585
const ast =
8686
typeof xpathExpression === 'string'
87-
? parseExpression(xpathExpression, { allowXQuery: false })
87+
? parseExpression(xpathExpression, { allowXQuery: false, version: 4 })
8888
: // AST is an element: convert to jsonml
8989
convertXmlToAst(xpathExpression);
9090

src/jsCodegen/compileXPathToJavaScript.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { version } from 'chai';
12
import { EvaluableExpression } from '../evaluateXPath';
23
import {
34
createDefaultFunctionNameResolver,
@@ -11,7 +12,7 @@ import astHelper from '../parsing/astHelper';
1112
import { ReturnType } from '../parsing/convertXDMReturnValue';
1213
import convertXmlToAst from '../parsing/convertXmlToAst';
1314
import normalizeEndOfLines from '../parsing/normalizeEndOfLines';
14-
import parseExpression from '../parsing/parseExpression';
15+
import parseExpression, { CompilationOptions } from '../parsing/parseExpression';
1516
import annotateAst from '../typeInference/annotateAST';
1617
import { AnnotationContext } from '../typeInference/AnnotationContext';
1718
import { Language, Options } from '../types/Options';
@@ -42,13 +43,14 @@ function compileXPathToJavaScript(
4243
let ast;
4344
if (typeof selector === 'string') {
4445
const expressionString = normalizeEndOfLines(selector);
45-
const parserOptions = {
46+
const parserOptions: CompilationOptions = {
4647
allowXQuery:
4748
options['language'] === Language.XQUERY_3_1_LANGUAGE ||
4849
options['language'] === Language.XQUERY_UPDATE_3_1_LANGUAGE,
4950
// Debugging inserts xs:stackTrace in the AST, but this is not supported
5051
// yet by the js-codegen backend.
5152
debug: false,
53+
version: 3.1,
5254
};
5355
try {
5456
ast = parseExpression(expressionString, parserOptions);

0 commit comments

Comments
 (0)