Repository: FontoXML/fontoxpath
Pull request #603 (2023-08-02 11:42:31)

Fix text() not selecting CDATA sections
While the expression version of the text() node test did check for the CDataSection node type, the codegen-emitted code did not. Additionally, the bucket generated for a CDataSection node was type-4, while the text() selector always uses type-3. This fixes both issues and adds a few tests to confirm that fix.

bundlemon[bot]: <!-- bundlemon -->
## BundleMon

  
  
  
  
  
<details open>
<summary>Files updated (2)</summary>

Status | Path | Size | Limits
:------------: | ------------ | :------------: | :------------:
:white_check_mark: | fontoxpath.js<br/> | 76.23KB (+36B +0.05%) | -
:white_check_mark: | fontoxpath.esm.js<br/> | 76.35KB (+32B +0.04%) | -
</details>

  
  


Total files change +68B +0.04%

Final result: :white_check_mark:

[View report in BundleMon website ➡️](https://app.bundlemon.dev/projects/6437c8f89a7cb205c434db8d/reports/64ca4187b326e02079cb395d)

---
<p align="center"><a href="https://app.bundlemon.dev/projects/6437c8f89a7cb205c434db8d/reports?branch=fix-cdata-test&resolution=all" target="_blank" rel="noreferrer noopener">Current branch size history</a> | <a href="https://app.bundlemon.dev/projects/6437c8f89a7cb205c434db8d/reports?branch=master&resolution=days" target="_blank" rel="noreferrer noopener">Target branch size history</a></p>
coveralls: 
[![Coverage Status](https://coveralls.io/builds/61732934/badge)](https://coveralls.io/builds/61732934)

coverage: 91.499% (+0.002%) from 91.497% when pulling **b9f58ae399491ff733c948493fb5464476f460d9 on fix-cdata-test** into **ddc694a3c478dfa3c9308a250a49e180de540180 on master**.

DrRataplan: Looks great! Thanks! The parser for the QT3 tests just consider CDataSections as textnodes, so that's why this never surfaced. I guess with an upgrade to Slimdom#latest this will kick in tho

Patch:
diff --git a/.github/workflows/bundlesize.yml b/.github/workflows/bundlesize.yml
index d0f078db..2fa2792f 100644
--- a/.github/workflows/bundlesize.yml
+++ b/.github/workflows/bundlesize.yml
@@ -13,7 +13,7 @@ jobs:
             - uses: actions/checkout@v2
             - uses: actions/setup-node@v1
               with:
-                  node-version: '12'
+                  node-version: '20'
             - name: Install dependencies
               run: npm ci
             - name: override CI_COMMIT_SHA
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 0900c1ec..f13e8f57 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -12,9 +12,9 @@ jobs:
             fail-fast: false
             matrix:
                 node-version:
+                    - 20
                     - 18
                     - 16
-                    - 14
         steps:
             - uses: actions/checkout@v2
               with:
diff --git a/package-lock.json b/package-lock.json
index d2062562..bbe6886a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4773,9 +4773,9 @@
 			]
 		},
 		"node_modules/google-closure-compiler-osx": {
-			"version": "20221004.0.0",
-			"resolved": "https://registry.npmjs.org/google-closure-compiler-osx/-/google-closure-compiler-osx-20221004.0.0.tgz",
-			"integrity": "sha512-z5V7BvaMauPga8DMTt9u6RGcjBdLAuv4gL2Ebw5NIQRTAHVkEVzCd3kiMX7CVCGhmWdS/1r3jZcCg4BswGia6w==",
+			"version": "20230206.0.0",
+			"resolved": "https://registry.npmjs.org/google-closure-compiler-osx/-/google-closure-compiler-osx-20230206.0.0.tgz",
+			"integrity": "sha512-lJ/Y4HTk+KdL6PhLmmalP/3DdzGK0mS0+htuFP6y4t9+QXiUKnpHWx/VDQ3Fwm2fWEzqDxfhX3R+wC9lBvFiAg==",
 			"cpu": [
 				"x32",
 				"x64",
@@ -10645,7 +10645,7 @@
 				"fs-extra": "^10.0.1",
 				"google-closure-compiler-java": "^20221004.0.0",
 				"google-closure-compiler-linux": "^20221004.0.0",
-				"google-closure-compiler-osx": "^20221004.0.0",
+				"google-closure-compiler-osx": "^20230206.0.0",
 				"google-closure-compiler-windows": "^20221004.0.0",
 				"ora": "^5.4.1",
 				"resolve-from": "^5.0.0",
@@ -13330,9 +13330,9 @@
 			"optional": true
 		},
 		"google-closure-compiler-osx": {
-			"version": "20221004.0.0",
-			"resolved": "https://registry.npmjs.org/google-closure-compiler-osx/-/google-closure-compiler-osx-20221004.0.0.tgz",
-			"integrity": "sha512-z5V7BvaMauPga8DMTt9u6RGcjBdLAuv4gL2Ebw5NIQRTAHVkEVzCd3kiMX7CVCGhmWdS/1r3jZcCg4BswGia6w==",
+			"version": "20230206.0.0",
+			"resolved": "https://registry.npmjs.org/google-closure-compiler-osx/-/google-closure-compiler-osx-20230206.0.0.tgz",
+			"integrity": "sha512-lJ/Y4HTk+KdL6PhLmmalP/3DdzGK0mS0+htuFP6y4t9+QXiUKnpHWx/VDQ3Fwm2fWEzqDxfhX3R+wC9lBvFiAg==",
 			"dev": true,
 			"optional": true
 		},
diff --git a/package.json b/package.json
index 72a6f153..c70c1afa 100644
--- a/package.json
+++ b/package.json
@@ -115,5 +115,8 @@
 	"dependencies": {
 		"prsc": "4.0.0",
 		"xspattern": "^3.1.0"
+	},
+	"overrides": {
+		"google-closure-compiler-osx": "^20230206.0.0"
 	}
 }
diff --git a/src/expressions/Expression.ts b/src/expressions/Expression.ts
index 47b8cefd..7c11ffef 100644
--- a/src/expressions/Expression.ts
+++ b/src/expressions/Expression.ts
@@ -4,6 +4,7 @@ import DynamicContext from './DynamicContext';
 import ExecutionParameters from './ExecutionParameters';
 import Specificity from './Specificity';
 import StaticContext from './StaticContext';
+import UnfocusableDynamicContext from './UnfocusableDynamicContext';
 import { Bucket } from './util/Bucket';
 import createDoublyIterableSequence from './util/createDoublyIterableSequence';
 import { errXUST0001 } from './xquery-update/XQueryUpdateFacilityErrors';
@@ -101,12 +102,17 @@ abstract class Expression {
 	}
 
 	protected evaluateWithoutFocus(
-		_contextlessDynamicContext: DynamicContext | null,
+		_contextlessDynamicContext: DynamicContext,
 		executionParameters: ExecutionParameters
 	): ISequence {
 		if (this._eagerlyEvaluatedValue === null) {
 			this._eagerlyEvaluatedValue = createDoublyIterableSequence(
-				this.evaluate(null, executionParameters).expandSequence()
+				this.evaluate(
+					new UnfocusableDynamicContext({
+						variableBindings: {},
+					}),
+					executionParameters
+				).expandSequence()
 			);
 		}
 		return this._eagerlyEvaluatedValue();
diff --git a/src/expressions/functions/builtInFunctions_arrays.ts b/src/expressions/functions/builtInFunctions_arrays.ts
index fd122ebc..f4f772d8 100644
--- a/src/expressions/functions/builtInFunctions_arrays.ts
+++ b/src/expressions/functions/builtInFunctions_arrays.ts
@@ -460,17 +460,16 @@ const arraySort: FunctionDefinitionType = (
 	});
 };
 
-function flattenItem(flatteneditems: ISequence, item: Value): ISequence {
+function flattenItem(item: Value): ISequence {
 	if (isSubtypeOf(item.type, ValueType.ARRAY)) {
-		return (item as ArrayValue).members.reduce(
-			(flatteneditemsOfMember, member) =>
-				member().mapAll((allValues) =>
-					allValues.reduce(flattenItem, flatteneditemsOfMember)
-				),
-			flatteneditems
+		const arrayItem = item as ArrayValue;
+		return concatSequences(
+			arrayItem.members.map((member) =>
+				member().mapAll((subItems) => concatSequences(subItems.map(flattenItem)))
+			)
 		);
 	}
-	return concatSequences([flatteneditems, sequenceFactory.singleton(item)]);
+	return sequenceFactory.singleton(item);
 }
 const arrayFlatten: FunctionDefinitionType = (
 	_dynamicContext,
@@ -478,7 +477,7 @@ const arrayFlatten: FunctionDefinitionType = (
 	_staticContext,
 	itemSequence
 ) => {
-	return itemSequence.mapAll((items) => items.reduce(flattenItem, sequenceFactory.empty()));
+	return itemSequence.mapAll((items) => concatSequences(items.map(flattenItem)));
 };
 
 const declarations: BuiltinDeclarationType[] = [
diff --git a/src/getBuckets.ts b/src/getBuckets.ts
index e4d56cfc..90cce754 100644
--- a/src/getBuckets.ts
+++ b/src/getBuckets.ts
@@ -42,7 +42,11 @@ export function getBucketsForPointer(pointer: NodePointer, domFacade: DomFacade)
  * @param node - The node which buckets should be retrieved
  */
 export function getBucketsForNode(node: Node): Bucket[] {
-	const nodeType = node.nodeType;
+	let nodeType = node.nodeType;
+	// Put CDataSection nodes in the Text bucket
+	if (nodeType === NODE_TYPES.CDATA_SECTION_NODE) {
+		nodeType = NODE_TYPES.TEXT_NODE;
+	}
 	let localName;
 
 	if (nodeType === NODE_TYPES.ATTRIBUTE_NODE || nodeType === NODE_TYPES.ELEMENT_NODE) {


Test patch:
diff --git a/src/jsCodegen/emitTest.ts b/src/jsCodegen/emitTest.ts
index 3c46a8b7..38510102 100644
--- a/src/jsCodegen/emitTest.ts
+++ b/src/jsCodegen/emitTest.ts
@@ -36,7 +36,8 @@ function emitTextTest(
 	return [
 		mapPartialCompilationResult(contextItemExpr, (contextItemExpr) =>
 			acceptAst(
-				`${contextItemExpr.code}.nodeType === /*TEXT_NODE*/ ${NODE_TYPES.TEXT_NODE}`,
+				`(${contextItemExpr.code}.nodeType === /*TEXT_NODE*/ ${NODE_TYPES.TEXT_NODE} ||
+				${contextItemExpr.code}.nodeType === /* CDATA_SECTION_NODE */ ${NODE_TYPES.CDATA_SECTION_NODE})`,
 				{
 					type: GeneratedCodeBaseType.Value,
 				},
diff --git a/test/specs/parsing/arrays/arrayFunctions.tests.ts b/test/specs/parsing/arrays/arrayFunctions.tests.ts
index d9878430..cfef7b75 100644
--- a/test/specs/parsing/arrays/arrayFunctions.tests.ts
+++ b/test/specs/parsing/arrays/arrayFunctions.tests.ts
@@ -3,6 +3,7 @@ import {
 	evaluateXPath,
 	evaluateXPathToArray,
 	evaluateXPathToBoolean,
+	evaluateXPathToNumber,
 	evaluateXPathToNumbers,
 	evaluateXPathToString,
 	evaluateXPathToStrings,
@@ -433,6 +434,21 @@ describe('functions over arrays', () => {
 				evaluateXPathToStrings('array:flatten(["a", ["b", "c"], "d"])', documentNode),
 				['a', 'b', 'c', 'd']
 			));
+
+		it('can flatten long arrays', () =>
+			chai.assert.equal(
+				evaluateXPathToNumber('array:flatten(array{1 to 20000}) => count()', documentNode),
+				20000
+			));
+
+		it('can flatten deep arrays', () =>
+			chai.assert.equal(
+				evaluateXPathToNumber(
+					'(1 to 200) => fold-left([1], function ($accum, $item) {[$accum, $item]}) => array:flatten() => count()',
+					documentNode
+				),
+				201
+			));
 	});
 
 	describe('atomizing arrays', () => {
diff --git a/test/specs/parsing/getBucketsForNode.tests.ts b/test/specs/parsing/getBucketsForNode.tests.ts
index fbe522d8..5dc91ebd 100644
--- a/test/specs/parsing/getBucketsForNode.tests.ts
+++ b/test/specs/parsing/getBucketsForNode.tests.ts
@@ -12,5 +12,8 @@ describe('getBucketsForNode', () => {
 	});
 	it('returns the correct buckets for text nodes', () => {
 		chai.assert.deepEqual(getBucketsForNode(doc.createTextNode('A piece of text')), ['type-3']);
+		chai.assert.deepEqual(getBucketsForNode(doc.createCDATASection('A piece of cdata')), [
+			'type-3',
+		]);
 	});
 });
diff --git a/test/specs/parsing/jsCodegen/nodeTests.tests.ts b/test/specs/parsing/jsCodegen/nodeTests.tests.ts
index 427da86c..a25f93ff 100644
--- a/test/specs/parsing/jsCodegen/nodeTests.tests.ts
+++ b/test/specs/parsing/jsCodegen/nodeTests.tests.ts
@@ -26,6 +26,13 @@ describe('node tests', () => {
 		chai.assert.isTrue(
 			evaluateXPathWithJsCodegen('/xml/text()', documentNode, null, ReturnType.BOOLEAN)
 		);
+		// text() should also match CDATA sections
+		const cdata = documentNode.createCDATASection('hello');
+		documentNode.documentElement.replaceChildren(cdata);
+		chai.assert.equal(
+			evaluateXPathWithJsCodegen('/xml/text()', documentNode, null, ReturnType.FIRST_NODE),
+			cdata
+		);
 	});
 
 	it('does not select non-text nodes', () => {
diff --git a/test/specs/parsing/operators/SimpleMapOperator.tests.ts b/test/specs/parsing/operators/SimpleMapOperator.tests.ts
index df7efa96..2c30210f 100644
--- a/test/specs/parsing/operators/SimpleMapOperator.tests.ts
+++ b/test/specs/parsing/operators/SimpleMapOperator.tests.ts
@@ -2,6 +2,7 @@ import * as chai from 'chai';
 import {
 	evaluateXPath,
 	evaluateXPathToNodes,
+	evaluateXPathToNumber,
 	evaluateXPathToString,
 	evaluateXPathToStrings,
 } from 'fontoxpath';
@@ -95,4 +96,6 @@ describe('Simple map operator', () => {
 		));
 	it('throws the correct error when mapping numbers to a ancestor path expresion', () =>
 		chai.assert.throws(() => evaluateXPathToNodes('0!ancestor::a', documentNode), 'XPTY0020'));
+	it('does not crash when mapping numbers to numbers', () =>
+		chai.assert.equal(evaluateXPathToNumber('0!1', documentNode), 1));
 });