Repository: FontoXML/fontoxpath
Pull request #641 (2024-05-31 00:36:01)

Implement the fn:generate-id function
https://www.w3.org/TR/xpath-functions-31/#func-generate-id

Simple implementation, but it seems to work.


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

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

Status | Path | Size | Limits
:------------: | ------------ | :------------: | :------------:
:white_check_mark: | fontoxpath.js<br/> | 78.43KB (+101B +0.13%) | -
:white_check_mark: | fontoxpath.esm.js<br/> | 78.54KB (+99B +0.12%) | -
</details>

  
  


Total files change +200B +0.12%

Final result: :white_check_mark:

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

---
<p align="center"><a href="https://app.bundlemon.dev/projects/6437c8f89a7cb205c434db8d/reports?branch=implement-generate-id&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/67805582/badge)](https://coveralls.io/builds/67805582)

coverage: 91.425% (-0.1%) from 91.556%
when pulling **c485d7f53313bab6609fab808885af549c1b7e22 on egh:implement-generate-id**
into **35d0da50bcbd81f905382c5731d28ff2522968c7 on FontoXML:master**.

egh: @DrRataplan Thank you for looking at this! I wasn't sure if those xpath features were implemented (my xpath knowledge mostly stops at 2.0 :grinning: ). It turns out the counter wasn't properly being passed to some children. I believe I have corrected the issue.
egh: Looks like there are some type errors I need to fix...
egh: I just realized that this won't really work for my use case (an xslt processor) unless I allow the caller of an `evaluateXpath` function to pass in a counter. I'll give that a shot...
DrRataplan: Is that because you need the result to be deterministic over multiple
calls? A weakmap on the module level should fit that, right? Otherwise,
something on DomFacade would not be too dirty...

On Wed, 5 Jun 2024, 22:07 Erik Hetzner, ***@***.***> wrote:

> I just realized that this won't really work for my use case (an xslt
> processor) unless I allow the caller of an evaluateXpath function to pass
> in a counter. I'll give that a shot...
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/FontoXML/fontoxpath/pull/641#issuecomment-2150865092>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ABGKEJEEUL2BVD3BJWHOE2TZF5VWLAVCNFSM6AAAAABIR3SHFWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCNJQHA3DKMBZGI>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>

egh: > Is that because you need the result to be deterministic over multiple calls? A weakmap on the module level should fit that, right? Otherwise, something on DomFacade would not be too dirty...

Yes, that's right... I think a DomFacade seems like the way to go. Thanks for the tip.
egh: I'm pretty happy with what I have here - what do you think? 
DrRataplan: I think it looks great! I'll have to take a proper gander next week, if
that is OK, after Midsommar. Thanks again!

On Wed, 19 Jun 2024, 19:55 Erik Hetzner, ***@***.***> wrote:

> I'm pretty happy with what I have here - what do you think?
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/FontoXML/fontoxpath/pull/641#issuecomment-2179247424>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ABGKEJFH4UGSUNTAPTB6QNDZIHAYZAVCNFSM6AAAAABIR3SHFWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCNZZGI2DONBSGQ>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>

egh: No problem! Thank you. I just wanted to let you know it was ready for your review. Thanks for everything.
DrRataplan: Finally got around to those linting errors. Thanks again for the help!

Patch:
diff --git a/src/expressions/functions/builtInFunctions_node.ts b/src/expressions/functions/builtInFunctions_node.ts
index b564426d..42006831 100644
--- a/src/expressions/functions/builtInFunctions_node.ts
+++ b/src/expressions/functions/builtInFunctions_node.ts
@@ -30,7 +30,7 @@ import { errXPDY0002 } from '../XPathErrors';
 import { BuiltinDeclarationType } from './builtInFunctions';
 import builtinStringFunctions from './builtInFunctions_string';
 import FunctionDefinitionType from './FunctionDefinitionType';
-
+import generateId from './generateId';
 const fnString = builtinStringFunctions.functions.string;
 
 function contextItemAsFirstArgument(
@@ -123,6 +123,23 @@ const fnData: FunctionDefinitionType = (
 	return atomize(sequence, executionParameters);
 };
 
+const fnGenerateId: FunctionDefinitionType = (
+	dynamicContext,
+	executionParameters,
+	_staticContext,
+	nodeValue,
+) => {
+	if (nodeValue.isEmpty()) {
+		return sequenceFactory.singleton(createAtomicValue('', ValueType.XSSTRING));
+	}
+	if (!isSubtypeOf(nodeValue.first().type, ValueType.NODE)) {
+		throw new Error('XPTY0004: The context item must be a node.');
+	}
+	const node = nodeValue.first().value as NodePointer;
+
+	return sequenceFactory.singleton(createAtomicValue(generateId(node), ValueType.XSSTRING));
+};
+
 const fnHasChildren: FunctionDefinitionType = (
 	_dynamicContext,
 	executionParameters,
@@ -635,6 +652,20 @@ const declarations: BuiltinDeclarationType[] = [
 		namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
 		returnType: { type: ValueType.XSBOOLEAN, mult: SequenceMultiplicity.EXACTLY_ONE },
 	},
+	{
+		argumentTypes: [],
+		callFunction: contextItemAsFirstArgument.bind(null, 'generate-id', fnGenerateId),
+		localName: 'generate-id',
+		namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
+		returnType: { type: ValueType.XSSTRING, mult: SequenceMultiplicity.EXACTLY_ONE },
+	},
+	{
+		argumentTypes: [{ type: ValueType.NODE, mult: SequenceMultiplicity.ZERO_OR_ONE }],
+		callFunction: fnGenerateId,
+		localName: 'generate-id',
+		namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
+		returnType: { type: ValueType.XSSTRING, mult: SequenceMultiplicity.EXACTLY_ONE },
+	},
 ];
 
 export default {
diff --git a/src/expressions/functions/generateId.ts b/src/expressions/functions/generateId.ts
new file mode 100644
index 00000000..c92b2344
--- /dev/null
+++ b/src/expressions/functions/generateId.ts
@@ -0,0 +1,15 @@
+import { ConcreteNode } from '../../domFacade/ConcreteNode';
+import { NodePointer, TinyNode } from '../../domClone/Pointer';
+
+const generateIdMap: WeakMap<TinyNode | ConcreteNode, string> = new WeakMap();
+let generateIdCounter = 0;
+
+function generateId(ptr: NodePointer): string {
+	const node = ptr.node;
+	if (!generateIdMap.has(node)) {
+		generateIdMap.set(node, `id${++generateIdCounter}`);
+	}
+	return generateIdMap.get(node);
+}
+
+export default generateId;


Test patch:
diff --git a/test/assets/unrunnableTestCases.csv b/test/assets/unrunnableTestCases.csv
index ee69fcab..79a5f487 100644
--- a/test/assets/unrunnableTestCases.csv
+++ b/test/assets/unrunnableTestCases.csv
@@ -617,10 +617,6 @@ fn-function-lookup-353,AssertionError: Expected XPath exists(function-lookup(fn:
 fn-function-lookup-354,Error: XPTY0004: Expected base expression of a function call to evaluate to a sequence of single function item
 fn-function-lookup-355,AssertionError: Expected XPath exists(function-lookup(fn:QName('http://www.w3.org/2005/xpath-functions', 'element-with-id'), 2)) to resolve to true: expected false to be true
 fn-function-lookup-356,Error: XPTY0004: Expected base expression of a function call to evaluate to a sequence of single function item
-fn-function-lookup-361,AssertionError: Expected XPath exists(function-lookup(fn:QName('http://www.w3.org/2005/xpath-functions', 'generate-id'), 0)) to resolve to true: expected false to be true
-fn-function-lookup-362,Error: XPTY0004: Expected base expression of a function call to evaluate to a sequence of single function item
-fn-function-lookup-363,AssertionError: Expected XPath exists(function-lookup(fn:QName('http://www.w3.org/2005/xpath-functions', 'generate-id'), 1)) to resolve to true: expected false to be true
-fn-function-lookup-364,Error: XPTY0004: Expected base expression of a function call to evaluate to a sequence of single function item
 fn-function-lookup-365,AssertionError: Expected XPath exists(function-lookup(fn:QName('http://www.w3.org/2005/xpath-functions', 'doc'), 1)) to resolve to true: expected false to be true
 fn-function-lookup-366,Error: XPTY0004: Expected base expression of a function call to evaluate to a sequence of single function item
 fn-function-lookup-367,AssertionError: Expected XPath exists(function-lookup(fn:QName('http://www.w3.org/2005/xpath-functions', 'doc-available'), 1)) to resolve to true: expected false to be true
@@ -723,33 +719,13 @@ fn-function-name-014,AssertionError: Expected executing the XPath "fn:function-n
 fn-function-name-018,AssertionError: Expected executing the XPath "fn:function-name( concat#340282366920938463463374607431768211456 )" to resolve to one of the expected results, but got AssertionError: expected [Function] to throw error matching /FOAR0002/ but got 'Invalid array length', RangeError: Invalid array length.
 fn-function-name-023,Error: 1: ( 2: 	declare function local:add($arg1, $arg2, $arg3)    	        ^ 3:         { 4:            $arg1 + $arg2 + $arg3  Error: XPST0003: Failed to parse script. Expected -,+,validate,(#,(,#,parent::,ancestor::,preceding-sibling::,preceding::,ancestor-or-self::,..,child::,descendant::,attribute::,self::,descendant-or-self::,following-sibling::,following::,document-node(,element,attribute,schema-element,schema-attribute(,processing-instruction(,processing-instruction(),comment(),text(),namespace-node(),node(),:*,Q,[A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD],[\uD800-\uDB7F],/,//   at <>:2:10 - 2:11
 fn-function-name-024,Error: No selector counterpart for: typedFunctionTest.
-generate-id-000,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}generate-id with arity of 1 not registered. No similar functions found.
-generate-id-001,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}generate-id with arity of 1 not registered. No similar functions found.
-generate-id-002,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}generate-id with arity of 1 not registered. No similar functions found.
-generate-id-003,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}generate-id with arity of 1 not registered. No similar functions found.
-generate-id-004,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}generate-id with arity of 1 not registered. No similar functions found.
-generate-id-005,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}generate-id with arity of 1 not registered. No similar functions found.
-generate-id-006,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}generate-id with arity of 1 not registered. No similar functions found.
-generate-id-008,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}generate-id with arity of 0 not registered. No similar functions found.
-generate-id-009,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}generate-id with arity of 0 not registered. No similar functions found.
-generate-id-010,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}generate-id with arity of 0 not registered. No similar functions found.
 generate-id-012,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}collection with arity of 0 not registered. No similar functions found.
 generate-id-013,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}collection with arity of 0 not registered. No similar functions found.
 generate-id-014,Error: No selector counterpart for: computedDocumentConstructor.
 generate-id-015,Error: No selector counterpart for: computedDocumentConstructor.
 generate-id-016,Error: No selector counterpart for: computedDocumentConstructor.
 generate-id-017,Error: No selector counterpart for: computedDocumentConstructor.
-generate-id-018,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}generate-id with arity of 1 not registered. No similar functions found.
-generate-id-019,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}generate-id with arity of 0 not registered. No similar functions found.
-generate-id-020,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}generate-id with arity of 1 not registered. No similar functions found.
-generate-id-021,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}generate-id with arity of 1 not registered. No similar functions found.
-generate-id-022,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}generate-id with arity of 1 not registered. No similar functions found.
-generate-id-023,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}generate-id with arity of 1 not registered. No similar functions found.
-generate-id-901,AssertionError: expected [Function] to throw error matching /XPDY0002/ but got 'XPST0017: Function Q{http://www.w3.or…'
-generate-id-902,AssertionError: expected [Function] to throw error matching /XPTY0004/ but got 'XPST0017: Function Q{http://www.w3.or…'
-generate-id-903,AssertionError: expected [Function] to throw error matching /XPTY0004/ but got 'XPST0017: Function Q{http://www.w3.or…'
-generate-id-904,AssertionError: expected [Function] to throw error matching /XPTY0004/ but got 'XPST0017: Function Q{http://www.w3.or…'
-generate-id-905,AssertionError: expected [Function] to throw error matching /XPTY0004/ but got 'XPST0017: Function Q{http://www.w3.or…'
+generate-id-018,Error: XPTY0004: Multiplicity of function argument of type node()? for generate-id is incorrect. Expected "?", but got "+".
 fn-has-children-007,AssertionError: Expected executing the XPath "(1)[fn:has-children()]" to resolve to one of the expected results, but got AssertionError: expected [Function] to throw error matching /XPTY0004/ but got 'Cannot read properties of undefined (…'.
 fn-has-children-008,AssertionError: Expected executing the XPath "(fn:concat#2)[fn:has-children()]" to resolve to one of the expected results, but got AssertionError: expected [Function] to throw error matching /XPTY0004/ but got 'Cannot read properties of undefined (…'.
 fn-has-children-012,AssertionError: expected [Function] to throw an error
@@ -2981,7 +2957,6 @@ K-FunctionCallExpr-5,AssertionError: expected [Function] to throw error matching
 K-FunctionCallExpr-15a,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}format-number with arity of 2 not registered. Did you mean "Q{http://www.w3.org/2005/xpath-functions}format-integer (xs:integer?, xs:string)"?
 K-FunctionCallExpr-16a,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}format-time with arity of 2 not registered. Did you mean "Q{http://www.w3.org/2005/xpath-functions}current-time ()" or "Q{http://www.w3.org/2005/xpath-functions}format-integer (xs:integer?, xs:string)"?
 K-FunctionCallExpr-17a,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}format-time with arity of 5 not registered. Did you mean "Q{http://www.w3.org/2005/xpath-functions}current-time ()" or "Q{http://www.w3.org/2005/xpath-functions}format-integer (xs:integer?, xs:string)"?
-K-FunctionCallExpr-25a,Error: XPST0017: Function generate-id with arity of 1 not registered. No similar functions found.
 function-call-reserved-function-names-034,AssertionError: expected [Function] to throw error matching /XPST0017/ but got '1: \n2: \tnamespace-node(1)\n   \t   …'
 function-declaration-017,Error: XPST0008, The variable v is not in scope.
 function-declaration-022,AssertionError: expected [Function] to throw error matching /XQST0034/ but got 'XQST0049: The function or variable "Q…'
@@ -3168,10 +3143,6 @@ function-literal-353,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-fu
 function-literal-354,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}element-with-id with arity of 1 not registered. No similar functions found.
 function-literal-355,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}element-with-id with arity of 2 not registered. No similar functions found.
 function-literal-356,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}element-with-id with arity of 2 not registered. No similar functions found.
-function-literal-361,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}generate-id with arity of 0 not registered. No similar functions found.
-function-literal-362,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}generate-id with arity of 0 not registered. No similar functions found.
-function-literal-363,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}generate-id with arity of 1 not registered. No similar functions found.
-function-literal-364,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}generate-id with arity of 1 not registered. No similar functions found.
 function-literal-365,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}doc with arity of 1 not registered. No similar functions found.
 function-literal-366,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}doc with arity of 1 not registered. No similar functions found.
 function-literal-367,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}doc-available with arity of 1 not registered. No similar functions found.
diff --git a/test/specs/parsing/functions/functions.node.tests.ts b/test/specs/parsing/functions/functions.node.tests.ts
index 52462689..520a8444 100644
--- a/test/specs/parsing/functions/functions.node.tests.ts
+++ b/test/specs/parsing/functions/functions.node.tests.ts
@@ -16,6 +16,33 @@ beforeEach(() => {
 });
 
 describe('functions over nodes', () => {
+	describe('generate-id()', () => {
+		it('returns the same value for the same node', () => {
+			jsonMlMapper.parse(['someElement'], documentNode);
+			chai.assert.isTrue(
+				evaluateXPathToBoolean('generate-id(/) = generate-id(/)', documentNode),
+			);
+		});
+
+		it('returns the empty string for the empty sequence', () => {
+			jsonMlMapper.parse(['someElement'], documentNode);
+			chai.assert.isTrue(evaluateXPathToBoolean('generate-id(()) = ""', documentNode));
+		});
+
+		it('throws the correct error when the context item is not a node', () => {
+			chai.assert.throws(
+				() => evaluateXPathToBoolean('"not a node" ! generate-id()'),
+				'XPTY0004',
+			);
+		});
+
+		it('returns the same value for subsequent calls', () => {
+			jsonMlMapper.parse(['someElement'], documentNode);
+			const id = evaluateXPathToString('generate-id(/)', documentNode);
+			chai.assert.equal(evaluateXPathToString('generate-id(/)', documentNode), id);
+		});
+	});
+
 	describe('lang()', () => {
 		it('returns true when the node has the right lang', () => {
 			jsonMlMapper.parse(['someElement', { 'xml:lang': 'en' }], documentNode);