@jinntec/fore 2.9.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +0 -2
  2. package/dist/fore-dev.js +9505 -6057
  3. package/dist/fore.js +9218 -5996
  4. package/index.js +1 -0
  5. package/package.json +4 -2
  6. package/src/DependentXPathQueries.js +37 -2
  7. package/src/ForeElementMixin.js +64 -38
  8. package/src/actions/fx-delete.js +424 -73
  9. package/src/actions/fx-insert.js +472 -73
  10. package/src/actions/fx-setattribute.js +5 -5
  11. package/src/actions/fx-setvalue.js +11 -9
  12. package/src/authoring-check.js +231 -0
  13. package/src/fore.js +32 -77
  14. package/src/functions/registerFunction.js +65 -20
  15. package/src/fx-bind.js +128 -73
  16. package/src/fx-fore.js +653 -219
  17. package/src/fx-instance.js +145 -143
  18. package/src/fx-model.js +292 -102
  19. package/src/fx-speech.js +268 -0
  20. package/src/fx-submission.js +246 -135
  21. package/src/fx-var.js +28 -4
  22. package/src/json/JSONDomFacade.js +84 -0
  23. package/src/json/JSONLens.js +67 -0
  24. package/src/json/JSONNode.js +248 -0
  25. package/src/json/lensFromNode.js +5 -0
  26. package/src/modelitem.js +16 -2
  27. package/src/tools/fx-action-log.js +1 -1
  28. package/src/ui/UIElement.js +16 -2
  29. package/src/ui/abstract-control.js +14 -7
  30. package/src/ui/fx-case.js +3 -1
  31. package/src/ui/fx-control.js +4 -2
  32. package/src/ui/fx-group.js +1 -1
  33. package/src/ui/fx-items.js +26 -32
  34. package/src/ui/fx-output.js +8 -38
  35. package/src/ui/fx-repeat-attributes.js +1 -1
  36. package/src/ui/fx-repeat.js +683 -247
  37. package/src/ui/fx-repeatitem.js +16 -1
  38. package/src/ui/repeat-base.js +9 -5
  39. package/src/withDraggability.js +0 -1
  40. package/src/xpath-evaluation.js +1763 -740
  41. package/src/xpath-path.js +274 -24
  42. package/src/xpath-util.js +92 -46
@@ -1,3 +1,6 @@
1
+ // src/xpath-evaluation.js
2
+ // NOTE: This file is intentionally written as plain JS (no JSX) and must parse under Vite.
3
+
1
4
  import {
2
5
  evaluateXPath as fxEvaluateXPath,
3
6
  evaluateXPathToBoolean as fxEvaluateXPathToBoolean,
@@ -9,85 +12,147 @@ import {
9
12
  parseScript,
10
13
  registerCustomXPathFunction,
11
14
  registerXQueryModule,
15
+ Language,
12
16
  } from 'fontoxpath';
13
17
 
18
+ import * as fx from 'fontoxpath';
14
19
  import { XPathUtil } from './xpath-util.js';
15
20
  import { prettifyXml } from './functions/common-function.js';
21
+ import { JSONDomFacade } from './json/JSONDomFacade.js';
16
22
 
17
23
  const XFORMS_NAMESPACE_URI = 'http://www.w3.org/2002/xforms';
18
-
19
24
  const createdNamespaceResolversByXPathQueryAndNode = new Map();
20
25
 
21
- // A global registry of function names that are declared in Fore by a developer using the
22
- // `fx-function` element. These should be available without providing a prefix as well
23
- export const globallyDeclaredFunctionLocalNames = [];
26
+ const __jsonDomFacade = new JSONDomFacade();
24
27
 
25
- function getCachedNamespaceResolver(xpath, node) {
26
- if (!createdNamespaceResolversByXPathQueryAndNode.has(xpath)) {
27
- return null;
28
+ // ------------------------------------------------------------
29
+ // Helpers: Fore/model/instance
30
+ // ------------------------------------------------------------
31
+
32
+ function _getOwningFore(node) {
33
+ let n = node;
34
+ if (!n) return null;
35
+
36
+ // Prefer ForeElementMixin API when available (handles shadow/slot traversal correctly)
37
+ if (typeof n.getOwnerForm === 'function') {
38
+ try {
39
+ const fore = n.getOwnerForm();
40
+ if (fore) return fore;
41
+ } catch (_e) {
42
+ // ignore
43
+ }
28
44
  }
29
- return createdNamespaceResolversByXPathQueryAndNode.get(xpath).get(node) || null;
45
+
46
+ if (n.nodeType === Node.ATTRIBUTE_NODE) n = n.ownerElement;
47
+ if (n.nodeType === Node.TEXT_NODE) n = n.parentNode;
48
+
49
+ // cross shadow
50
+ if (n?.parentNode?.nodeType === Node.DOCUMENT_FRAGMENT_NODE) n = n.parentNode.host;
51
+
52
+ // Element.closest works across light DOM; for shadow, we normalized to host above
53
+ return n?.closest ? n.closest('fx-fore') : null;
30
54
  }
31
55
 
32
- function setCachedNamespaceResolver(xpath, node, resolver) {
33
- if (!createdNamespaceResolversByXPathQueryAndNode.has(xpath)) {
34
- return createdNamespaceResolversByXPathQueryAndNode.set(xpath, new Map());
56
+ function _getModelFromFormElement(formElement) {
57
+ if (!formElement) return null;
58
+
59
+ if (typeof formElement.getModel === 'function') {
60
+ try {
61
+ return formElement.getModel();
62
+ } catch (_e) {}
35
63
  }
36
- return createdNamespaceResolversByXPathQueryAndNode.get(xpath).set(node, resolver);
64
+
65
+ const fore = _getOwningFore(formElement);
66
+ if (fore && typeof fore.getModel === 'function') {
67
+ try {
68
+ return fore.getModel();
69
+ } catch (_e) {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ return null;
37
75
  }
38
76
 
39
- const xhtmlNamespaceResolver = prefix => {
40
- if (!prefix) {
41
- return 'http://www.w3.org/1999/xhtml';
77
+ function _getInstanceFromFormElement(formElement, instanceId) {
78
+ const model = _getModelFromFormElement(formElement);
79
+ if (!model || typeof model.getInstance !== 'function') return null;
80
+ try {
81
+ return model.getInstance(instanceId);
82
+ } catch (_e) {
83
+ return null;
42
84
  }
43
- return undefined;
44
- };
45
- export function isInShadow(node) {
46
- return node.getRootNode() instanceof ShadowRoot;
85
+ }
86
+
87
+ // IMPORTANT: source of truth is instance.type / @type
88
+ function _isJsonInstance(instance) {
89
+ if (!instance) return false;
90
+ const t =
91
+ (typeof instance.getAttribute === 'function' && instance.getAttribute('type')) ||
92
+ instance.type ||
93
+ '';
94
+ return t === 'json';
95
+ }
96
+
97
+ function _isJsonNode(n) {
98
+ return !!n && typeof n === 'object' && n.__jsonlens__ === true;
99
+ }
100
+
101
+ function _getJsonRootNode(instance) {
102
+ return instance?.nodeset && _isJsonNode(instance.nodeset) ? instance.nodeset : null;
47
103
  }
48
104
 
49
105
  /**
50
- * Resolve an id in scope. Behaves like the algorithm defined on https://www.w3.org/community/xformsusers/wiki/XForms_2.0#idref-resolve
51
- *
52
- * @param {string} id
53
- * @param {Node} sourceObject
54
- * @param {string} nodeName
55
- *
56
- * @returns {HTMLElement} The element with that ID, resolved with respect to repeats
106
+ * Avoid calling any instance getters here.
107
+ * Some FxInstance implementations rebuild lenses / trigger evaluation in getters,
108
+ * which can recurse into XPath evaluation and overflow the stack.
57
109
  */
110
+ function _getRawJsonRootValue(instance) {
111
+ if (!instance) return null;
112
+
113
+ // Canonical backing field in FxInstance
114
+ if (instance._instanceData !== undefined) return instance._instanceData;
115
+
116
+ // Alternate field name
117
+ if (instance.jsonData !== undefined) return instance.jsonData;
118
+
119
+ // Last fallback: unwrap a JSONNode root
120
+ if (instance.nodeset && instance.nodeset.__jsonlens__ === true) return instance.nodeset.value;
121
+
122
+ return null;
123
+ }
124
+
125
+ // ------------------------------------------------------------
126
+ // Index('repeat') without XPath evaluation (prevents recursion)
127
+ // ------------------------------------------------------------
128
+
129
+ function _matchIndexExpr(expr) {
130
+ const s = String(expr ?? '').trim();
131
+ const m = s.match(/^index\s*\(\s*(['"])(.*?)\1\s*\)\s*$/);
132
+ return m ? m[2] : null;
133
+ }
134
+
58
135
  export function resolveId(id, sourceObject, nodeName = null) {
59
136
  const query =
60
137
  'outermost(ancestor-or-self::fx-fore[1]/(descendant::fx-fore|descendant::*[@id = $id]))[not(self::fx-fore)]';
61
- /*
62
- if (nodeName === 'fx-instance') {
63
- // Instance elements can only be in the `model` element
64
- // query = 'ancestor-or-self::fx-fore[1]/fx-model/fx-instance[@id = $id]';
65
-
66
- const fore = Fore.getFore(sourceObject);
67
- const instances = fore.getModel().instances;
68
- const targetInstance = instances.find(i => i.id === id);
69
- return targetInstance;
70
- return document.getElementById(id);
71
- }
72
- */
138
+
73
139
  if (sourceObject.nodeType === Node.TEXT_NODE) {
74
140
  sourceObject = sourceObject.parentNode;
75
141
  }
76
142
  if (sourceObject.nodeType === Node.ATTRIBUTE_NODE) {
77
143
  sourceObject = sourceObject.ownerElement;
78
144
  }
79
- if (sourceObject.parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
145
+ if (sourceObject.parentNode?.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
80
146
  sourceObject = sourceObject.parentNode.host;
81
147
  }
148
+
82
149
  const ownerForm =
83
150
  sourceObject.localName === 'fx-fore' ? sourceObject : sourceObject.closest('fx-fore');
151
+
84
152
  const elementsWithId = ownerForm.querySelectorAll(`[id='${id}']`);
85
153
  if (elementsWithId.length === 1) {
86
- // A single one is found. Assume no ID reuse.
87
154
  const targetObject = elementsWithId[0];
88
- if (nodeName && targetObject.localName !== nodeName) {
89
- return null;
90
- }
155
+ if (nodeName && targetObject.localName !== nodeName) return null;
91
156
  return targetObject;
92
157
  }
93
158
 
@@ -98,10 +163,7 @@ export function resolveId(id, sourceObject, nodeName = null) {
98
163
  { id },
99
164
  { namespaceResolver: xhtmlNamespaceResolver },
100
165
  );
101
-
102
- if (allMatchingTargetObjects.length === 0) {
103
- return null;
104
- }
166
+ if (allMatchingTargetObjects.length === 0) return null;
105
167
 
106
168
  if (
107
169
  allMatchingTargetObjects.length === 1 &&
@@ -113,32 +175,11 @@ export function resolveId(id, sourceObject, nodeName = null) {
113
175
  { namespaceResolver: xhtmlNamespaceResolver },
114
176
  )
115
177
  ) {
116
- // If the target element is not repeated, then the search for the target object is trivial since
117
- // there is only one associated with the target element that bears the matching ID. This is true
118
- // regardless of whether or not the source object is repeated. However, if the target element is
119
- // repeated, then additional information must be used to help select a target object from among
120
- // those associated with the identified target element.
121
178
  const targetObject = allMatchingTargetObjects[0];
122
- if (nodeName && targetObject.localName !== nodeName) {
123
- return null;
124
- }
179
+ if (nodeName && targetObject.localName !== nodeName) return null;
125
180
  return targetObject;
126
181
  }
127
182
 
128
- // SPEC:
129
-
130
- // 12.2.1 References to Elements within a repeat Element
131
-
132
- // When the target element that is identified by the IDREF of a source object has one or more
133
- // repeat elements as ancestors, then the set of ancestor repeats are partitioned into two
134
- // subsets, those in common with the source element and those that are not in common. Any ancestor
135
- // repeat elements of the target element not in common with the source element are descendants of
136
- // the repeat elements that the source and target element have in common, if any.
137
-
138
- // For the repeat elements that are in common, the desired target object exists in the same set of
139
- // run-time objects that contains the source object. Then, for each ancestor repeat of the target
140
- // element that is not in common with the source element, the current index of the repeat
141
- // determines the set of run-time objects that contains the desired target object.
142
183
  for (const ancestorRepeatItem of fxEvaluateXPathToNodes(
143
184
  'ancestor::fx-repeatitem => reverse()',
144
185
  sourceObject,
@@ -151,20 +192,13 @@ export function resolveId(id, sourceObject, nodeName = null) {
151
192
  );
152
193
  switch (foundTargetObjects.length) {
153
194
  case 0:
154
- // Nothing found: ignore
155
195
  break;
156
196
  case 1: {
157
- // A single one is found: the target object is directly in a common repeat
158
197
  const targetObject = foundTargetObjects[0];
159
- if (nodeName && targetObject.localName !== nodeName) {
160
- return null;
161
- }
198
+ if (nodeName && targetObject.localName !== nodeName) return null;
162
199
  return targetObject;
163
200
  }
164
201
  default: {
165
- // Multiple target objects are found: they are in a repeat that is not common with the
166
- // source object We found a target object in a common repeat! We now need to find the one
167
- // that is in the repeatitem identified at the current index
168
202
  const targetObject = foundTargetObjects.find(to =>
169
203
  fxEvaluateXPathToNodes(
170
204
  'every $ancestor of ancestor::fx-repeatitem satisfies $ancestor is $ancestor/../child::fx-repeatitem[../@repeat-index]',
@@ -173,34 +207,520 @@ export function resolveId(id, sourceObject, nodeName = null) {
173
207
  {},
174
208
  ),
175
209
  );
176
- if (!targetObject) {
177
- // Nothing valid found for whatever reason. This might be something dynamic?
178
- return null;
179
- }
180
- if (nodeName && targetObject.localName !== nodeName) {
181
- return null;
182
- }
210
+ if (!targetObject) return null;
211
+ if (nodeName && targetObject.localName !== nodeName) return null;
183
212
  return targetObject;
184
213
  }
185
214
  }
186
215
  }
187
- // We found no target objects in common repeats. The id is unresolvable
216
+
188
217
  return null;
189
218
  }
190
219
 
191
- // Make namespace resolving use the `instance` element that is related to here
192
- const xmlDocument = new DOMParser().parseFromString('<xml />', 'text/xml');
220
+ /**
221
+ * Resolve index('repeatId') without evaluating XPath (prevents recursion).
222
+ * Returns:
223
+ * - null => not an index() expr
224
+ * - number => resolved index (defaults to 1)
225
+ */
226
+ function tryResolveIndexExpr(expr, formElementOrNode) {
227
+ try {
228
+ const repeatId = _matchIndexExpr(expr);
229
+ if (!repeatId) return null;
193
230
 
194
- const instanceReferencesByQuery = new Map();
231
+ const source = formElementOrNode?.nodeType
232
+ ? formElementOrNode
233
+ : _getOwningFore(formElementOrNode);
195
234
 
196
- function findInstanceReferences(xpathQuery) {
197
- if (!xpathQuery.includes('instance')) {
198
- // No call to the instance function anyway: short-circuit and prevent AST processing
199
- return [];
235
+ const repeat =
236
+ (source && resolveId(repeatId, source, 'fx-repeat')) ||
237
+ _getOwningFore(source)?.querySelector?.(`#${CSS.escape(repeatId)}`);
238
+
239
+ if (!repeat) return 1;
240
+
241
+ const attr = repeat.getAttribute('index') ?? repeat.getAttribute('repeat-index');
242
+ let idx = Number(attr);
243
+
244
+ if (!Number.isFinite(idx) || idx < 1) {
245
+ if (typeof repeat.getIndex === 'function') idx = Number(repeat.getIndex());
246
+ else idx = Number(repeat.index);
247
+ }
248
+
249
+ return Number.isFinite(idx) && idx >= 1 ? idx : 1;
250
+ } catch (_e) {
251
+ return null;
200
252
  }
201
- if (instanceReferencesByQuery.has(xpathQuery)) {
202
- return instanceReferencesByQuery.get(xpathQuery);
253
+ }
254
+
255
+ // ------------------------------------------------------------
256
+ // JSON lookup handling
257
+ // ------------------------------------------------------------
258
+
259
+ function _toXQueryMapItem(value) {
260
+ if (value == null) return value;
261
+
262
+ if (Array.isArray(value)) {
263
+ return value.map(v => _toXQueryMapItem(v));
203
264
  }
265
+
266
+ if (value instanceof Map) {
267
+ const m = new Map();
268
+ for (const [k, v] of value.entries()) m.set(k, _toXQueryMapItem(v));
269
+ return m;
270
+ }
271
+
272
+ // plain object -> Map
273
+ if (typeof value === 'object' && !value.nodeType && value.__jsonlens__ !== true) {
274
+ const m = new Map();
275
+ for (const [k, v] of Object.entries(value)) m.set(k, _toXQueryMapItem(v));
276
+ return m;
277
+ }
278
+
279
+ return value;
280
+ }
281
+
282
+ function _resolveLookupOnMapItem(base, rest) {
283
+ // Resolve XQuery 3.1 lookup steps like "?ui?query" against Map/Array/plain objects.
284
+ // Returns the resolved JS value (primitive/Map/Array/object) or null.
285
+ if (base == null) return null;
286
+
287
+ let s = String(rest ?? '').trim();
288
+ if (!s) return base;
289
+
290
+ // Allow prefixes like '.?ui?query'
291
+ if (s.startsWith('.')) s = s.slice(1);
292
+ if (!s.startsWith('?')) return base;
293
+
294
+ const steps = s
295
+ .split('?')
296
+ .filter(Boolean)
297
+ .map(p => String(p).trim())
298
+ .filter(Boolean);
299
+
300
+ let cur = base;
301
+
302
+ const getProp = (obj, key) => {
303
+ if (obj == null) return null;
304
+ if (obj instanceof Map) return obj.get(key);
305
+ if (Array.isArray(obj)) {
306
+ // numeric key for arrays
307
+ const n = Number(key);
308
+ if (Number.isFinite(n)) return obj[n - 1];
309
+ return null;
310
+ }
311
+ if (typeof obj === 'object') return obj[key];
312
+ return null;
313
+ };
314
+
315
+ for (const raw of steps) {
316
+ if (cur == null) return null;
317
+
318
+ if (raw === '*') {
319
+ // Star lookup returns the current collection as-is
320
+ continue;
321
+ }
322
+
323
+ // Support bracket index: prop[3]
324
+ const bm = raw.match(/^(.*?)\[(.+)\]$/);
325
+ if (bm) {
326
+ const prop = bm[1].trim();
327
+ const idxExpr = bm[2].trim();
328
+ const container = prop ? getProp(cur, prop) : cur;
329
+ if (container == null) return null;
330
+
331
+ const idx1 = _resolveBracketIndex1(idxExpr, null) ?? Number(idxExpr);
332
+ if (!Number.isFinite(idx1) || idx1 < 1) return null;
333
+
334
+ if (Array.isArray(container)) {
335
+ cur = container[idx1 - 1];
336
+ continue;
337
+ }
338
+ if (container instanceof Map) {
339
+ // Map with numeric keys is rare; treat as array-like if values are array
340
+ const v = container.get(idx1);
341
+ cur = v !== undefined ? v : null;
342
+ continue;
343
+ }
344
+ return null;
345
+ }
346
+
347
+ cur = getProp(cur, raw);
348
+ }
349
+
350
+ return cur;
351
+ }
352
+
353
+ function _looksLikeLookupExpr(expr) {
354
+ const s = String(expr ?? '').trim();
355
+ return s.includes('?');
356
+ }
357
+
358
+ /**
359
+ * Split "…?*[(predicate)]" => { base: "…?*", predicate: "(predicate)" }
360
+ * Works for:
361
+ * instance('data')?movies?*[true()]
362
+ * ?movies?*[instance('data')?ui?query = 'Ma']
363
+ */
364
+ function _splitStarPredicate(expr) {
365
+ const s = String(expr ?? '').trim();
366
+ const m = s.match(/^(.*\?\*)\s*\[\s*([\s\S]+?)\s*\]\s*$/);
367
+ if (!m) return null;
368
+ return { base: m[1].trim(), predicate: m[2].trim() };
369
+ }
370
+
371
+ /**
372
+ * Determine whether expression is a "simple navigation" lens path that can be resolved
373
+ * by JSONNode.get chain:
374
+ * - no "*[predicate]" (handled separately)
375
+ * - no operators
376
+ * - no function calls beyond instance()/index()
377
+ * - predicates allowed ONLY in form "prop[NUMBER]" or "prop[index('repeat')]"
378
+ */
379
+ function _isSimpleLookupExpr(expr) {
380
+ const s = String(expr ?? '').trim();
381
+
382
+ // star predicate is not simple (handled by _splitStarPredicate path)
383
+ if (/\?\*\s*\[/.test(s)) return false;
384
+
385
+ // operators => not simple
386
+ if (/[=<>!]=|[=<>]/.test(s)) return false;
387
+
388
+ // function calls other than instance()/index() => not simple
389
+ const parens = s.match(/[a-zA-Z_][\w.-]*\s*\(/g) || [];
390
+ const otherCalls = parens.filter(m => !/^instance\s*\(/.test(m) && !/^index\s*\(/.test(m));
391
+ if (otherCalls.length) return false;
392
+
393
+ // bracket predicates allowed only as array access
394
+ if (/\[[\s\S]*\]/.test(s)) {
395
+ const steps = s.split('?').filter(Boolean);
396
+ for (const step of steps) {
397
+ const bm = step.match(/^(.*?)\[(.+)\]$/);
398
+ if (!bm) continue;
399
+ const inside = bm[2].trim();
400
+ if (/^\d+$/.test(inside)) continue;
401
+ if (_matchIndexExpr(inside)) continue;
402
+ // allow index("x") too
403
+ if (/^index\s*\(\s*(['"])(.*?)\1\s*\)\s*$/.test(inside)) continue;
404
+ return false;
405
+ }
406
+ }
407
+
408
+ return true;
409
+ }
410
+
411
+ function _parseSimpleLookupPath(expr) {
412
+ const s = String(expr ?? '').trim();
413
+
414
+ let instanceId = null;
415
+ let rest = s;
416
+
417
+ const instExplicit = s.match(/^instance\s*\(\s*(['"])(.*?)\1\s*\)\s*(\?.*)$/);
418
+ if (instExplicit) {
419
+ instanceId = instExplicit[2];
420
+ rest = instExplicit[3];
421
+ } else {
422
+ const instDefault = s.match(/^instance\s*\(\s*\)\s*(\?.*)$/);
423
+ if (instDefault) {
424
+ instanceId = 'default';
425
+ rest = instDefault[1];
426
+ } else if (s.startsWith('.?')) rest = s.slice(1);
427
+ else if (!s.startsWith('?')) return null;
428
+ }
429
+
430
+ const steps = rest
431
+ .split('?')
432
+ .filter(Boolean)
433
+ .map(part => part.trim())
434
+ .filter(Boolean);
435
+
436
+ return { instanceId, steps, hasExplicitInstance: !!instExplicit };
437
+ }
438
+
439
+ function _getInstanceIdForLookupExpr(expr0, formElement) {
440
+ const parsed = _parseSimpleLookupPath(expr0);
441
+ if (parsed && parsed.instanceId) return parsed.instanceId;
442
+ return XPathUtil.getInstanceId(expr0, formElement);
443
+ }
444
+
445
+ function _isRelativeJsonLookup(expr0, contextNode) {
446
+ const s = String(expr0 ?? '').trim();
447
+ // relative lookup: starts with ? or .?
448
+ if (!(s.startsWith('?') || s.startsWith('.?'))) return false;
449
+ return _isJsonNode(contextNode);
450
+ }
451
+
452
+ function _resolveBracketIndex1(idxExpr, formElement) {
453
+ const t = String(idxExpr ?? '').trim();
454
+ if (/^\d+$/.test(t)) return Number(t);
455
+
456
+ // index('movies')
457
+ const rid = _matchIndexExpr(t);
458
+ if (rid) return tryResolveIndexExpr(`index('${rid}')`, formElement) ?? 1;
459
+
460
+ // index("movies")
461
+ const m = t.match(/^index\s*\(\s*(['"])(.*?)\1\s*\)\s*$/);
462
+ if (m) return tryResolveIndexExpr(`index('${m[2]}')`, formElement) ?? 1;
463
+
464
+ const n = Number(t);
465
+ return Number.isFinite(n) ? n : null;
466
+ }
467
+
468
+ function _resolveSimpleLookupToJsonNode(expr, contextNode, formElement) {
469
+ const parsed = _parseSimpleLookupPath(expr);
470
+ if (!parsed) return null;
471
+
472
+ const trimmed = String(expr ?? '').trim();
473
+ const isExplicitInstance = trimmed.startsWith('instance(');
474
+
475
+ // IMPORTANT: always return a NEW array for children (copy),
476
+ // otherwise fx-repeat may keep a cached reference and miss inserts/deletes.
477
+ const getChildren = n => {
478
+ if (!n) return [];
479
+ const kids =
480
+ typeof n.getChildren === 'function'
481
+ ? n.getChildren() || []
482
+ : Array.isArray(n.children)
483
+ ? n.children
484
+ : [];
485
+ return Array.from(kids);
486
+ };
487
+
488
+ let node = null;
489
+
490
+ if (parsed.instanceId) {
491
+ const instance = _getInstanceFromFormElement(formElement, parsed.instanceId);
492
+ if (!_isJsonInstance(instance)) return null;
493
+ node = _getJsonRootNode(instance);
494
+ if (!node) return null;
495
+ } else if (!isExplicitInstance && _isJsonNode(contextNode)) {
496
+ node = contextNode;
497
+ } else {
498
+ const fallbackId = XPathUtil.getInstanceId(expr, formElement) || 'default';
499
+ const instance = _getInstanceFromFormElement(formElement, fallbackId);
500
+ if (!_isJsonInstance(instance)) return null;
501
+ node = _getJsonRootNode(instance);
502
+ if (!node) return null;
503
+ }
504
+
505
+ for (const rawStep of parsed.steps) {
506
+ if (!node) return null;
507
+
508
+ const step = String(rawStep);
509
+
510
+ if (step === '*') {
511
+ return getChildren(node);
512
+ }
513
+
514
+ if (/^\d+$/.test(step)) {
515
+ const idx0 = Number(step) - 1;
516
+ node = node.get?.(idx0) || null;
517
+ continue;
518
+ }
519
+
520
+ const bm = step.match(/^(.*?)\[(.+)\]$/);
521
+ if (bm) {
522
+ const prop = bm[1].trim();
523
+ const idxExpr = bm[2].trim();
524
+
525
+ const container = prop ? (typeof node.get === 'function' ? node.get(prop) : null) : node;
526
+ if (!container) return null;
527
+
528
+ const arrVal = container.value;
529
+ if (!Array.isArray(arrVal)) return null;
530
+
531
+ const idx1 = _resolveBracketIndex1(idxExpr, formElement);
532
+ if (!Number.isFinite(idx1) || idx1 < 1) return null;
533
+
534
+ const idx0 = idx1 - 1;
535
+ node = container.get?.(idx0) || null;
536
+ continue;
537
+ }
538
+
539
+ node = node.get?.(step) || null;
540
+ }
541
+
542
+ if (!node) return null;
543
+
544
+ if (Array.isArray(node.value)) return getChildren(node);
545
+
546
+ return node;
547
+ }
548
+ // ------------------------------------------------------------
549
+ // RAW JSON evaluation helpers (FontoXPath over JS values)
550
+ // ------------------------------------------------------------
551
+
552
+ function getVariablesInScope(formElement) {
553
+ let closestActualFormElement = formElement;
554
+ while (closestActualFormElement && !('inScopeVariables' in closestActualFormElement)) {
555
+ closestActualFormElement =
556
+ closestActualFormElement.nodeType === Node.ATTRIBUTE_NODE
557
+ ? closestActualFormElement.ownerElement
558
+ : closestActualFormElement.parentNode;
559
+ }
560
+
561
+ const scopeNode = closestActualFormElement || formElement;
562
+ const fore =
563
+ (scopeNode && typeof scopeNode.getOwnerForm === 'function' && scopeNode.getOwnerForm()) ||
564
+ _getOwningFore(scopeNode);
565
+
566
+ const variables = {};
567
+
568
+ // Helper: get :scope > fx-instance list from a fore's model without triggering instance getters
569
+ const getLocalInstances = aFore => {
570
+ if (!aFore) return [];
571
+ const model =
572
+ (typeof aFore.getModel === 'function' && aFore.getModel()) ||
573
+ aFore.shadowRoot?.querySelector?.('fx-model') ||
574
+ aFore.querySelector?.('fx-model') ||
575
+ null;
576
+ if (!model) return [];
577
+ return Array.from(model.querySelectorAll(':scope > fx-instance'));
578
+ };
579
+
580
+ const buildBindingsFromInstances = instEls => {
581
+ if (!instEls || !instEls.length) return null;
582
+ const b = Object.create(null);
583
+
584
+ // default = first instance in doc order
585
+ const first = instEls[0];
586
+ const firstIsJson = _isJsonInstance(first);
587
+ b.default = firstIsJson
588
+ ? _getRawJsonRootValue(first)
589
+ : _getInstanceDefaultContextNoSideEffects(first);
590
+
591
+ // $<id> for explicitly id'ed instances
592
+ for (const inst of instEls) {
593
+ const id = (inst.getAttribute && inst.getAttribute('id')) || '';
594
+ if (!id) continue;
595
+ if (id === 'default') continue;
596
+
597
+ b[id] = _isJsonInstance(inst)
598
+ ? _getRawJsonRootValue(inst)
599
+ : _getInstanceDefaultContextNoSideEffects(inst);
600
+ }
601
+ return b;
602
+ };
603
+
604
+ // 1) Implicit instance vars: $default and $<id>
605
+ let instanceBindings = fore && fore._instanceVarBindings;
606
+
607
+ // Ensure we have at least a `default` binding; if not, build it.
608
+ if (fore && (!instanceBindings || !('default' in instanceBindings))) {
609
+ try {
610
+ const localInst = getLocalInstances(fore);
611
+ const built = buildBindingsFromInstances(localInst);
612
+
613
+ if (built && built.default !== undefined && built.default !== null) {
614
+ fore._instanceVarBindings = built;
615
+ instanceBindings = built;
616
+ } else {
617
+ // NEW: fallback to nearest ancestor fore that has a SHARED instance as default
618
+ let p =
619
+ fore.parentNode?.nodeType === Node.DOCUMENT_FRAGMENT_NODE
620
+ ? fore.parentNode.host
621
+ : fore.parentNode;
622
+ while (p) {
623
+ const parentFore = p.closest ? p.closest('fx-fore') : null;
624
+ if (!parentFore) break;
625
+
626
+ const parentInst = getLocalInstances(parentFore).filter(
627
+ i => i.hasAttribute && i.hasAttribute('shared'),
628
+ );
629
+ const parentBuilt = buildBindingsFromInstances(parentInst);
630
+
631
+ if (parentBuilt && parentBuilt.default !== undefined && parentBuilt.default !== null) {
632
+ // Do NOT cache this onto the child fore; it’s a fallback view, not ownership.
633
+ instanceBindings = parentBuilt;
634
+ break;
635
+ }
636
+
637
+ p =
638
+ parentFore.parentNode?.nodeType === Node.DOCUMENT_FRAGMENT_NODE
639
+ ? parentFore.parentNode.host
640
+ : parentFore.parentNode;
641
+ }
642
+ }
643
+ } catch (_e) {
644
+ // ignore
645
+ }
646
+ }
647
+
648
+ if (instanceBindings) {
649
+ for (const [k, v] of Object.entries(instanceBindings)) {
650
+ variables[k] = _toXQueryMapItem(v);
651
+ }
652
+ }
653
+
654
+ // 2) Explicit in-scope variables (fx-var or other injectors) override implicit ones
655
+ if (closestActualFormElement && closestActualFormElement.inScopeVariables) {
656
+ for (const key of closestActualFormElement.inScopeVariables.keys()) {
657
+ const varElementOrValue = closestActualFormElement.inScopeVariables.get(key);
658
+ if (!varElementOrValue) continue;
659
+
660
+ if (varElementOrValue.nodeType) {
661
+ const el = varElementOrValue;
662
+
663
+ // Preserve implicit binding for $default when fx-var simply re-declares default instance
664
+ if (
665
+ el.nodeName === 'FX-VAR' &&
666
+ fore &&
667
+ fore._instanceVarBindings &&
668
+ key in fore._instanceVarBindings
669
+ ) {
670
+ const vexpr = String(el.getAttribute('value') || '').trim();
671
+ const isDefaultInstanceExpr =
672
+ /^instance\s*\(\s*\)\s*$/.test(vexpr) ||
673
+ /^instance\s*\(\s*(['"])default\1\s*\)\s*$/.test(vexpr);
674
+ if (isDefaultInstanceExpr) continue;
675
+ }
676
+
677
+ if (el.nodeName === 'FX-VAR') {
678
+ if (el._isRefreshing) continue;
679
+ if ('_value' in el) {
680
+ variables[key] = el._value;
681
+ continue;
682
+ }
683
+ if ('_computedValue' in el) {
684
+ variables[key] = el._computedValue;
685
+ continue;
686
+ }
687
+ }
688
+
689
+ variables[key] = el.value;
690
+ } else {
691
+ variables[key] = varElementOrValue;
692
+ }
693
+ }
694
+ }
695
+
696
+ // Prevent self-recursive fx-var evaluation
697
+ if (formElement && formElement.nodeName === 'FX-VAR') {
698
+ const selfName = (formElement.getAttribute('name') || '').trim();
699
+ if (selfName) delete variables[selfName];
700
+ }
701
+
702
+ return variables;
703
+ }
704
+ // ------------------------------------------------------------
705
+ // Namespace resolver infra (XML only)
706
+ // ------------------------------------------------------------
707
+
708
+ const xhtmlNamespaceResolver = prefix => {
709
+ if (!prefix) return 'http://www.w3.org/1999/xhtml';
710
+ return undefined;
711
+ };
712
+
713
+ export function isInShadow(node) {
714
+ return node.getRootNode() instanceof ShadowRoot;
715
+ }
716
+
717
+ const xmlDocument = new DOMParser().parseFromString('<xml />', 'text/xml');
718
+ const instanceReferencesByQuery = new Map();
719
+
720
+ function findInstanceReferences(xpathQuery) {
721
+ if (!xpathQuery.includes('instance')) return [];
722
+ if (instanceReferencesByQuery.has(xpathQuery)) return instanceReferencesByQuery.get(xpathQuery);
723
+
204
724
  const xpathAST = parseScript(xpathQuery, {}, xmlDocument);
205
725
  const instanceReferences = fxEvaluateXPathToStrings(
206
726
  `descendant::xqx:functionCallExpr
@@ -218,60 +738,61 @@ function findInstanceReferences(xpathQuery) {
218
738
  );
219
739
 
220
740
  instanceReferencesByQuery.set(xpathQuery, instanceReferences);
221
-
222
741
  return instanceReferences;
223
742
  }
224
- /**
225
- * @typedef {function(string):string} NamespaceResolver
226
- */
227
743
 
228
- /**
229
- * @function
230
- * Resolve a namespace. Needs a namespace prefix and the element that is most closely related to the
231
- * XPath in which the namespace is being resolved. The prefix will be resolved by using the
232
- * ancestry of said element.
233
- *
234
- * It has two ways of doing so:
235
- *
236
- * - If the prefix is defined in an `xmlns:XXX="YYY"` namespace declaration, it will return 'YYY'.
237
- * - If the prefix is the empty prefix and there is an `xpath-default-namespace="YYY"` attribute in
238
- * - the * ancestry, that attribute will be used and 'YYY' will be returned
239
- *
240
- * @param {string} xpathQuery
241
- * @param {HTMLElement} formElement
242
- * @returns {NamespaceResolver} The namespace resolver for this context
243
- */
744
+ function getCachedNamespaceResolver(xpath, node) {
745
+ if (!createdNamespaceResolversByXPathQueryAndNode.has(xpath)) return null;
746
+ return createdNamespaceResolversByXPathQueryAndNode.get(xpath).get(node) || null;
747
+ }
748
+
749
+ function setCachedNamespaceResolver(xpath, node, resolver) {
750
+ if (!createdNamespaceResolversByXPathQueryAndNode.has(xpath)) {
751
+ createdNamespaceResolversByXPathQueryAndNode.set(xpath, new Map());
752
+ }
753
+ createdNamespaceResolversByXPathQueryAndNode.get(xpath).set(node, resolver);
754
+ }
755
+
244
756
  export function createNamespaceResolver(xpathQuery, formElement) {
245
757
  const cachedResolver = getCachedNamespaceResolver(xpathQuery, formElement);
246
- if (cachedResolver) {
247
- return cachedResolver;
248
- }
758
+ if (cachedResolver) return cachedResolver;
759
+
760
+ const provisionalResolver = prefix => (prefix ? undefined : '');
761
+ setCachedNamespaceResolver(xpathQuery, formElement, provisionalResolver);
762
+
249
763
  let instanceReferences = findInstanceReferences(xpathQuery);
764
+
765
+ const closestRefExcludingSelf = el => {
766
+ if (!el) return null;
767
+
768
+ let n = el;
769
+ if (n.nodeType === Node.ATTRIBUTE_NODE) n = n.ownerElement;
770
+ if (n?.parentNode?.nodeType === Node.DOCUMENT_FRAGMENT_NODE) n = n.parentNode.host;
771
+
772
+ let start = n?.parentNode;
773
+ if (start?.nodeType === Node.DOCUMENT_FRAGMENT_NODE) start = start.host;
774
+
775
+ return start?.closest ? start.closest('[ref]') : null;
776
+ };
777
+
250
778
  if (instanceReferences.length === 0) {
251
- // No instance functions. Look up further in the hierarchy to see if we can deduce the intended context from there
252
- const ancestorComponent =
253
- formElement.parentNode &&
254
- formElement.parentNode.nodeType === formElement.ELEMENT_NODE &&
255
- formElement.parentNode.closest('[ref]');
256
- if (ancestorComponent) {
257
- const resolver = createNamespaceResolver(
258
- ancestorComponent.getAttribute('ref'),
259
- ancestorComponent,
260
- );
261
- setCachedNamespaceResolver(xpathQuery, formElement, resolver);
262
- return resolver;
779
+ const ancestorComponent = closestRefExcludingSelf(formElement);
780
+
781
+ if (ancestorComponent && ancestorComponent !== formElement) {
782
+ const ancestorRef = ancestorComponent.getAttribute('ref');
783
+ if (ancestorRef && ancestorRef !== xpathQuery) {
784
+ const resolver = createNamespaceResolver(ancestorRef, ancestorComponent);
785
+ setCachedNamespaceResolver(xpathQuery, formElement, resolver);
786
+ return resolver;
787
+ }
263
788
  }
264
- // Nothing found: let's just assume we're supposed to use the `default` instance
789
+
265
790
  instanceReferences = ['default'];
266
791
  }
267
792
 
268
793
  if (instanceReferences.length === 1) {
269
- // console.log(`resolving ${xpathQuery} with ${instanceReferences[0]}`);
270
794
  let instance;
271
795
  if (instanceReferences[0] === 'default') {
272
- /**
273
- * @type {HTMLElement}
274
- */
275
796
  const actualForeElement = fxEvaluateXPathToFirstNode(
276
797
  'ancestor-or-self::fx-fore[1]',
277
798
  formElement,
@@ -279,54 +800,25 @@ export function createNamespaceResolver(xpathQuery, formElement) {
279
800
  null,
280
801
  { namespaceResolver: xhtmlNamespaceResolver },
281
802
  );
282
-
283
803
  instance = actualForeElement && actualForeElement.querySelector('fx-instance');
284
804
  } else {
285
805
  instance = resolveId(instanceReferences[0], formElement, 'fx-instance');
286
806
  }
807
+
287
808
  if (instance && instance.hasAttribute('xpath-default-namespace')) {
288
809
  const xpathDefaultNamespace = instance.getAttribute('xpath-default-namespace');
289
- /*
290
- console.log(
291
- `Resolving the xpath ${xpathQuery} with the default namespace set to ${xpathDefaultNamespace}`,
292
- );
293
- */
294
- /**
295
- * @type {NamespaceResolver}
296
- */
297
- const resolveNamespacePrefix = prefix => {
298
- if (!prefix) {
299
- return xpathDefaultNamespace;
300
- }
301
- return undefined;
302
- };
810
+ const resolveNamespacePrefix = prefix => (!prefix ? xpathDefaultNamespace : undefined);
303
811
  setCachedNamespaceResolver(xpathQuery, formElement, resolveNamespacePrefix);
304
812
  return resolveNamespacePrefix;
305
813
  }
306
814
  }
307
- /*
308
- if (instanceReferences.length > 1) {
309
- console.warn(
310
- `More than one instance is used in the query "${xpathQuery}". The default namespace resolving will be used`,
311
- );
312
- }
313
- */
314
815
 
315
816
  const xpathDefaultNamespace =
316
817
  fxEvaluateXPathToString('ancestor-or-self::*/@xpath-default-namespace[last()]', formElement) ||
317
818
  '';
318
819
 
319
- /**
320
- * @type {NamespaceResolver}
321
- */
322
820
  const resolveNamespacePrefix = function resolveNamespacePrefix(prefix) {
323
- if (prefix === '') {
324
- return xpathDefaultNamespace;
325
- }
326
-
327
- // Note: ideally we should use Node#lookupNamespaceURI. However, the nodes we are passed are
328
- // XML. The best we can do is emulate the `xmlns:xxx` namespace declarations by regarding them as
329
- // attributes. Which they technically ARE NOT!
821
+ if (prefix === '') return xpathDefaultNamespace;
330
822
 
331
823
  return fxEvaluateXPathToString(
332
824
  'ancestor-or-self::*/@*[name() = "xmlns:" || $prefix][last()]',
@@ -342,20 +834,19 @@ export function createNamespaceResolver(xpathQuery, formElement) {
342
834
 
343
835
  function createNamespaceResolverForNode(query, contextNode, formElement) {
344
836
  if (((contextNode && contextNode.ownerDocument) || contextNode) === window.document) {
345
- // Running a query on the HTML DOM. Don't bother resolving namespaces in any other way
346
837
  return xhtmlNamespaceResolver;
347
838
  }
348
839
  return createNamespaceResolver(query, formElement);
349
840
  }
350
841
 
351
- /**
352
- * Implementation of the functionNameResolver passed to FontoXPath to
353
- * redirect function resolving for unprefixed functions to either the fn or the xf namespace
354
- */
355
- // eslint-disable-next-line no-unused-vars
842
+ // ------------------------------------------------------------
843
+ // Function resolver
844
+ // ------------------------------------------------------------
845
+
846
+ export const globallyDeclaredFunctionLocalNames = [];
847
+
356
848
  function functionNameResolver({ prefix, localName }, _arity) {
357
849
  switch (localName) {
358
- // TODO: put the full XForms library functions set here
359
850
  case 'context':
360
851
  case 'base64encode':
361
852
  case 'boolean-from-string':
@@ -385,8 +876,6 @@ function functionNameResolver({ prefix, localName }, _arity) {
385
876
  return { namespaceURI: XFORMS_NAMESPACE_URI, localName };
386
877
  default:
387
878
  if (prefix === '' && globallyDeclaredFunctionLocalNames.includes(localName)) {
388
- // The function has been declared without a prefix and is called here without a prefix.
389
- // Just make this work. It is the developer-friendly way
390
879
  return { namespaceURI: 'http://www.w3.org/2005/xquery-local-functions', localName };
391
880
  }
392
881
  if (prefix === 'fn' || prefix === '') {
@@ -399,110 +888,552 @@ function functionNameResolver({ prefix, localName }, _arity) {
399
888
  }
400
889
  }
401
890
 
402
- /**
403
- * Get the variables in scope of the form element. These are the values of the variables that
404
- * logically precede the formElement that declares the XPath
405
- *
406
- * @param {Node} formElement The element that declares the XPath
407
- *
408
- * @returns {Object} A key-value mapping of the variables
409
- */
410
- function getVariablesInScope(formElement) {
411
- let closestActualFormElement = formElement;
412
- while (closestActualFormElement && !('inScopeVariables' in closestActualFormElement)) {
413
- closestActualFormElement =
414
- closestActualFormElement.nodeType === Node.ATTRIBUTE_NODE
415
- ? closestActualFormElement.ownerElement
416
- : closestActualFormElement.parentNode;
891
+ // ------------------------------------------------------------
892
+ // JSON star-predicate filtering using FontoXPath per item
893
+ // ------------------------------------------------------------
894
+
895
+ function _jsonAtomicFromResolved(resolved) {
896
+ if (resolved === null || resolved === undefined) return '';
897
+
898
+ // Arrays: join atomic values with spaces (XPath-ish).
899
+ if (Array.isArray(resolved)) {
900
+ return resolved
901
+ .map(r => _jsonAtomicFromResolved(r))
902
+ .filter(s => s !== null && s !== undefined && s !== '')
903
+ .join(' ');
417
904
  }
418
905
 
419
- if (!closestActualFormElement) {
420
- return {};
906
+ // JSONNode: prefer getValue() if present.
907
+ if (_isJsonNode(resolved)) {
908
+ const v = typeof resolved.getValue === 'function' ? resolved.getValue() : resolved.value;
909
+
910
+ if (v === null || v === undefined) return '';
911
+
912
+ const t = typeof v;
913
+ if (t === 'string') return v;
914
+ if (t === 'number' || t === 'boolean' || t === 'bigint') return String(v);
915
+
916
+ try {
917
+ return JSON.stringify(v);
918
+ } catch (_e) {
919
+ return '';
920
+ }
421
921
  }
422
922
 
423
- const variables = {};
424
- if (closestActualFormElement.inScopeVariables) {
425
- for (const key of closestActualFormElement.inScopeVariables.keys()) {
426
- const varElementOrValue = closestActualFormElement.inScopeVariables.get(key);
427
- if (!varElementOrValue) {
923
+ // Primitives
924
+ const t = typeof resolved;
925
+ if (t === 'string') return resolved;
926
+ if (t === 'number' || t === 'boolean' || t === 'bigint') return String(resolved);
927
+
928
+ // Other objects
929
+ try {
930
+ return JSON.stringify(resolved);
931
+ } catch (_e) {
932
+ return String(resolved);
933
+ }
934
+ }
935
+ // Paste this over the existing function in src/xpath-evaluation.js
936
+ function _materializeInstanceLookupsInPredicate(predicateExpr, currentJsonNode, formElement) {
937
+ const src = String(predicateExpr ?? '');
938
+ let out = '';
939
+ const extraVars = {};
940
+ let varCount = 0;
941
+
942
+ const inScope = getVariablesInScope(formElement);
943
+
944
+ let inSingle = false;
945
+ let inDouble = false;
946
+
947
+ const isBoundary = ch =>
948
+ ch === undefined ||
949
+ ch === null ||
950
+ /\s/.test(ch) ||
951
+ ch === ',' ||
952
+ ch === ')' ||
953
+ ch === ']' ||
954
+ ch === '+' ||
955
+ ch === '-' ||
956
+ ch === '*' ||
957
+ ch === '=' ||
958
+ ch === '>' ||
959
+ ch === '<' ||
960
+ ch === '!' ||
961
+ ch === '|' ||
962
+ ch === '&';
963
+
964
+ function readLookupTail(start) {
965
+ let k = start;
966
+
967
+ if (src[k] === '.') {
968
+ if (src[k + 1] !== '?') return null;
969
+ k += 1;
970
+ }
971
+ if (src[k] !== '?') return null;
972
+
973
+ let bracketDepth = 0;
974
+ let inS = false;
975
+ let inD = false;
976
+
977
+ while (k < src.length) {
978
+ const ch = src[k];
979
+
980
+ if (ch === "'" && !inD) {
981
+ inS = !inS;
982
+ k += 1;
428
983
  continue;
429
984
  }
430
- if (varElementOrValue.nodeType) {
431
- // We are a var element, set the value to the value computed there
432
- variables[key] = varElementOrValue.value;
433
- // variables[key] = varElementOrValue.inScopeVariables.get(key);
434
- } else {
435
- // We are a direct value. This is used to leak in event variables
436
- variables[key] = varElementOrValue;
985
+ if (ch === '"' && !inS) {
986
+ inD = !inD;
987
+ k += 1;
988
+ continue;
437
989
  }
990
+ if (inS || inD) {
991
+ k += 1;
992
+ continue;
993
+ }
994
+
995
+ if (ch === '[') bracketDepth += 1;
996
+ else if (ch === ']') {
997
+ if (bracketDepth > 0) bracketDepth -= 1;
998
+ else break;
999
+ }
1000
+
1001
+ if (bracketDepth === 0 && k !== start && isBoundary(ch)) break;
1002
+ k += 1;
438
1003
  }
1004
+
1005
+ return { raw: src.slice(start, k), end: k };
439
1006
  }
440
- return variables;
1007
+
1008
+ function readInstanceLensAt(start) {
1009
+ if (!src.slice(start).match(/^instance\s*\(/)) return null;
1010
+
1011
+ let j = start;
1012
+ let inS = false;
1013
+ let inD = false;
1014
+ let depth = 0;
1015
+
1016
+ while (j < src.length) {
1017
+ const ch = src[j];
1018
+
1019
+ if (ch === "'" && !inD) {
1020
+ inS = !inS;
1021
+ j += 1;
1022
+ continue;
1023
+ }
1024
+ if (ch === '"' && !inS) {
1025
+ inD = !inD;
1026
+ j += 1;
1027
+ continue;
1028
+ }
1029
+ if (inS || inD) {
1030
+ j += 1;
1031
+ continue;
1032
+ }
1033
+
1034
+ if (ch === '(') depth += 1;
1035
+ else if (ch === ')') {
1036
+ depth -= 1;
1037
+ if (depth === 0) break;
1038
+ }
1039
+ j += 1;
1040
+ }
1041
+
1042
+ if (j >= src.length) return null;
1043
+
1044
+ let k = j + 1;
1045
+ while (k < src.length && /\s/.test(src[k])) k += 1;
1046
+ const tail = readLookupTail(k);
1047
+ if (!tail) return null;
1048
+
1049
+ return { raw: src.slice(start, tail.end), end: tail.end };
1050
+ }
1051
+
1052
+ function readVariableLensAt(start) {
1053
+ if (src[start] !== '$') return null;
1054
+
1055
+ let j = start + 1;
1056
+ if (!/[A-Za-z_]/.test(src[j] || '')) return null;
1057
+ j += 1;
1058
+ while (j < src.length && /[\w.-]/.test(src[j])) j += 1;
1059
+
1060
+ let k = j;
1061
+ while (k < src.length && /\s/.test(src[k])) k += 1;
1062
+
1063
+ const tail = readLookupTail(k);
1064
+ if (!tail) {
1065
+ return { raw: src.slice(start, j), end: j, name: src.slice(start + 1, j), tail: '' };
1066
+ }
1067
+
1068
+ return {
1069
+ raw: src.slice(start, tail.end),
1070
+ end: tail.end,
1071
+ name: src.slice(start + 1, j),
1072
+ tail: src.slice(k, tail.end),
1073
+ };
1074
+ }
1075
+
1076
+ function resolveVariableLens(rawVarExpr) {
1077
+ const m = String(rawVarExpr).match(/^\$([A-Za-z_][\w.-]*)([\s\S]*)$/);
1078
+ if (!m) return null;
1079
+
1080
+ const varName = m[1];
1081
+ const rest = (m[2] || '').trim();
1082
+
1083
+ const base = Object.prototype.hasOwnProperty.call(inScope, varName) ? inScope[varName] : null;
1084
+
1085
+ if (rest && (rest.startsWith('?') || rest.startsWith('.?'))) {
1086
+ if (base && _isJsonNode(base)) {
1087
+ const resolved = _resolveSimpleLookupToJsonNode(rest, base, formElement);
1088
+ return _jsonAtomicFromResolved(resolved);
1089
+ }
1090
+ const resolved = _resolveLookupOnMapItem(base, rest);
1091
+ return _jsonAtomicFromResolved(resolved);
1092
+ }
1093
+
1094
+ return _jsonAtomicFromResolved(base);
1095
+ }
1096
+
1097
+ function resolveRelativeLookup(rawLookupExpr) {
1098
+ const resolved = _resolveSimpleLookupToJsonNode(rawLookupExpr, currentJsonNode, formElement);
1099
+ return _jsonAtomicFromResolved(resolved);
1100
+ }
1101
+
1102
+ for (let i = 0; i < src.length; i += 1) {
1103
+ const ch = src[i];
1104
+
1105
+ if (ch === "'" && !inDouble) {
1106
+ inSingle = !inSingle;
1107
+ out += ch;
1108
+ continue;
1109
+ }
1110
+ if (ch === '"' && !inSingle) {
1111
+ inDouble = !inDouble;
1112
+ out += ch;
1113
+ continue;
1114
+ }
1115
+
1116
+ if (!inSingle && !inDouble) {
1117
+ // 1) instance('x')?foo?bar
1118
+ const instLens = readInstanceLensAt(i);
1119
+ if (instLens && _looksLikeLookupExpr(instLens.raw) && _isSimpleLookupExpr(instLens.raw)) {
1120
+ const vname = `__fxp${varCount++}`;
1121
+ const resolved = _resolveSimpleLookupToJsonNode(instLens.raw, currentJsonNode, formElement);
1122
+ extraVars[vname] = _jsonAtomicFromResolved(resolved);
1123
+ out += `$${vname}`;
1124
+ i = instLens.end - 1;
1125
+ continue;
1126
+ }
1127
+
1128
+ // 2) $default?ui?query (or plain $var)
1129
+ const varLens = readVariableLensAt(i);
1130
+ if (varLens) {
1131
+ const vname = `__fxp${varCount++}`;
1132
+ extraVars[vname] = resolveVariableLens(varLens.raw);
1133
+ out += `$${vname}`;
1134
+ i = varLens.end - 1;
1135
+ continue;
1136
+ }
1137
+
1138
+ // 3) relative lookup like ?title / .?title
1139
+ const relLens = readLookupTail(i);
1140
+ if (relLens && _looksLikeLookupExpr(relLens.raw) && _isSimpleLookupExpr(relLens.raw)) {
1141
+ const vname = `__fxp${varCount++}`;
1142
+ extraVars[vname] = resolveRelativeLookup(relLens.raw);
1143
+ out += `$${vname}`;
1144
+ i = relLens.end - 1;
1145
+ continue;
1146
+ }
1147
+ }
1148
+
1149
+ out += ch;
1150
+ }
1151
+
1152
+ return { expr: out.trim(), extraVars };
441
1153
  }
1154
+ function _filterJsonNodesByPredicate(nodes, predicateExpr, formElement, variables = {}) {
1155
+ const pred = String(predicateExpr ?? '').trim();
1156
+
1157
+ if (pred === 'true()' || pred === 'true') return (nodes || []).filter(_isJsonNode);
1158
+ if (pred === 'false()' || pred === 'false') return [];
1159
+
1160
+ // Keep the existing special fast-paths (they’re fine as an optimization)
1161
+ const containsDot = pred.match(/^contains\s*\(\s*\.\s*,\s*([\s\S]+)\s*\)\s*$/);
1162
+ if (containsDot) {
1163
+ const rhs = containsDot[1].trim();
1164
+ const inScope = getVariablesInScope(formElement);
1165
+
1166
+ const toPlain = v => {
1167
+ if (v == null) return v;
1168
+ if (_isJsonNode(v)) return toPlain(v.value);
1169
+ if (Array.isArray(v)) return v.map(toPlain);
1170
+ if (v instanceof Map) {
1171
+ const o = Object.create(null);
1172
+ for (const [k, vv] of v.entries()) o[String(k)] = toPlain(vv);
1173
+ return o;
1174
+ }
1175
+ if (typeof v === 'object' && !v.nodeType) {
1176
+ const o = Object.create(null);
1177
+ for (const [k, vv] of Object.entries(v)) o[k] = toPlain(vv);
1178
+ return o;
1179
+ }
1180
+ return v;
1181
+ };
442
1182
 
443
- /**
444
- * Evaluate an XPath to _any_ type. When possible, prefer to use any other function to ensure the
445
- * type of the output is more predictable.
446
- *
447
- * @param {string} xpath The XPath to run
448
- * @param {Node} contextNode The start of the XPath
449
- * @param {import('./ForeElementMixin.js').default} formElement The form element associated to the XPath
450
- * @param {Object} variables Any variables to pass to the XPath
451
- * @param {Object} options Any options to pass to the XPath
452
- *
453
- * @returns {any[]}
454
- */
455
- /*
456
- export function evaluateXPath(xpath, contextNode, formElement, variables = {}, options={}, domFacade = null) {
457
- const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
458
- const variablesInScope = getVariablesInScope(formElement);
1183
+ const toStringSafe = v => {
1184
+ const vv = toPlain(v);
1185
+ if (vv === null || vv === undefined) return '';
1186
+ if (typeof vv === 'string' || typeof vv === 'number' || typeof vv === 'boolean')
1187
+ return String(vv);
1188
+ try {
1189
+ return JSON.stringify(vv);
1190
+ } catch (_e) {
1191
+ return String(vv);
1192
+ }
1193
+ };
459
1194
 
460
- return fxEvaluateXPath(
461
- xpath,
1195
+ const resolveExprToString = (rawExpr, itemNode) => {
1196
+ const expr = String(rawExpr ?? '').trim();
1197
+
1198
+ const quoted = expr.match(/^(['"])([\s\S]*)\1$/);
1199
+ if (quoted) return String(quoted[2] ?? '');
1200
+
1201
+ const varLens = expr.match(/^\$([A-Za-z_][\w.-]*)([\s\S]*)$/);
1202
+ if (varLens) {
1203
+ const varName = varLens[1];
1204
+ const rest = (varLens[2] || '').trim();
1205
+
1206
+ const base =
1207
+ (variables && varName in variables ? variables[varName] : null) ??
1208
+ (inScope && varName in inScope ? inScope[varName] : null);
1209
+
1210
+ if (rest && (rest.startsWith('?') || rest.startsWith('.?'))) {
1211
+ if (base && _isJsonNode(base)) {
1212
+ const resolved = _resolveSimpleLookupToJsonNode(rest, base, formElement);
1213
+ return toStringSafe(_jsonAtomicFromResolved(resolved));
1214
+ }
1215
+ const resolved = _resolveLookupOnMapItem(base, rest);
1216
+ return toStringSafe(_jsonAtomicFromResolved(resolved));
1217
+ }
1218
+
1219
+ return toStringSafe(_jsonAtomicFromResolved(base));
1220
+ }
1221
+
1222
+ if (expr === '.') {
1223
+ const v = typeof itemNode.getValue === 'function' ? itemNode.getValue() : itemNode.value;
1224
+ return toStringSafe(v);
1225
+ }
1226
+
1227
+ if (_looksLikeLookupExpr(expr)) {
1228
+ const resolved = _resolveSimpleLookupToJsonNode(expr, itemNode, formElement);
1229
+ return toStringSafe(_jsonAtomicFromResolved(resolved));
1230
+ }
1231
+
1232
+ if (expr.startsWith('$')) {
1233
+ const key = expr.slice(1);
1234
+ const v =
1235
+ (variables && key in variables ? variables[key] : null) ??
1236
+ (inScope && key in inScope ? inScope[key] : null);
1237
+ return toStringSafe(_jsonAtomicFromResolved(v));
1238
+ }
1239
+
1240
+ return expr;
1241
+ };
1242
+
1243
+ return (nodes || []).filter(n => {
1244
+ if (!_isJsonNode(n)) return false;
1245
+ const needle = resolveExprToString(rhs, n);
1246
+ if (needle === '') return true;
1247
+
1248
+ const raw = typeof n.getValue === 'function' ? n.getValue() : n.value;
1249
+ const haystack = toStringSafe(raw);
1250
+ return String(haystack || '').includes(String(needle));
1251
+ });
1252
+ }
1253
+
1254
+ // --- GENERAL CASE (the important fix) ---
1255
+ const inScope = getVariablesInScope(formElement);
1256
+
1257
+ const out = [];
1258
+ for (const n of nodes || []) {
1259
+ if (!_isJsonNode(n)) continue;
1260
+
1261
+ try {
1262
+ const { expr: predExpr, extraVars } = _materializeInstanceLookupsInPredicate(
1263
+ pred,
1264
+ n,
1265
+ formElement,
1266
+ );
1267
+ const mergedVars = { ...inScope, ...variables, ...extraVars };
1268
+
1269
+ // Evaluate predicate in RAW context (no JSONDomFacade).
1270
+ // This avoids JSONDomFacade quirks and matches the behavior of the working instance() variant.
1271
+ const rawContext = typeof n.getValue === 'function' ? n.getValue() : n.value;
1272
+
1273
+ const ok = fxEvaluateXPathToBoolean(predExpr, rawContext, null, mergedVars, {
1274
+ currentContext: { formElement, jsonMode: 'raw' },
1275
+ moduleImports: { xf: XFORMS_NAMESPACE_URI },
1276
+ functionNameResolver,
1277
+ namespaceResolver: null,
1278
+ language: Language.XPATH_3_1_LANGUAGE,
1279
+ xmlSerializer: new XMLSerializer(),
1280
+ });
1281
+
1282
+ if (ok) out.push(n);
1283
+ } catch (e) {
1284
+ // IMPORTANT: don't swallow silently — but keep it non-fatal.
1285
+ // This will immediately show you if variable resolution or predicate rewriting is still wrong.
1286
+ console.warn('[Fore] JSON predicate failed:', pred, '=>', e);
1287
+ }
1288
+ }
1289
+
1290
+ return out;
1291
+ }
1292
+ // ------------------------------------------------------------
1293
+
1294
+ // Exported evaluation helpers
1295
+ // ------------------------------------------------------------
1296
+
1297
+ export function evaluateXPath(
1298
+ xpath,
1299
+ contextNode,
1300
+ formElement,
1301
+ variables = {},
1302
+ options = {},
1303
+ domFacade = null,
1304
+ ) {
1305
+ const expr0 = String(xpath ?? '').trim();
1306
+
1307
+ try {
1308
+ const idx = tryResolveIndexExpr(expr0, formElement);
1309
+ if (idx !== null) return [idx];
1310
+
1311
+ if (_isJsonNode(contextNode) && expr0 === '.') {
1312
+ return [contextNode];
1313
+ }
1314
+
1315
+ if (_isJsonNode(contextNode) && !_looksLikeLookupExpr(expr0)) {
1316
+ const variablesInScope = getVariablesInScope(formElement);
1317
+ return fxEvaluateXPath(
1318
+ expr0,
462
1319
  contextNode,
463
- domFacade,
464
- {...variablesInScope, ...variables},
1320
+ __jsonDomFacade,
1321
+ { ...variablesInScope, ...variables },
465
1322
  fxEvaluateXPath.ALL_RESULTS_TYPE,
466
1323
  {
467
- debug: true,
468
- currentContext: {formElement, variables},
469
- moduleImports: {
470
- xf: XFORMS_NAMESPACE_URI,
471
- },
472
- functionNameResolver,
473
- namespaceResolver,
474
- language: options.language || evaluateXPath.XPATH_3_1
1324
+ debug: true,
1325
+ currentContext: { formElement, variables },
1326
+ moduleImports: { xf: XFORMS_NAMESPACE_URI },
1327
+ functionNameResolver,
1328
+ namespaceResolver: null,
1329
+ language: Language.XPATH_3_1_LANGUAGE,
1330
+ xmlSerializer: new XMLSerializer(),
1331
+ ...options,
475
1332
  },
476
- );
477
- }
478
- */
479
- export function evaluateXPath(xpath, contextNode, formElement, variables = {}, options = {}) {
480
- try {
481
- const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
1333
+ );
1334
+ }
1335
+
1336
+ if (_looksLikeLookupExpr(expr0)) {
1337
+ // --- NEW: support variable lookups like $default?ui?query ---
1338
+ const varLens = expr0.match(/^\$([A-Za-z_][\w.-]*)(.*)$/);
1339
+ if (varLens) {
1340
+ const varName = varLens[1];
1341
+ const rest = varLens[2] || '';
1342
+
1343
+ const inScope = getVariablesInScope(formElement);
1344
+ const base =
1345
+ (variables && Object.prototype.hasOwnProperty.call(variables, varName)
1346
+ ? variables[varName]
1347
+ : null) ??
1348
+ (inScope && Object.prototype.hasOwnProperty.call(inScope, varName)
1349
+ ? inScope[varName]
1350
+ : null);
1351
+
1352
+ if (!rest) return base == null ? [] : [base];
1353
+
1354
+ if (rest.startsWith('?') || rest.startsWith('.?')) {
1355
+ if (base && _isJsonNode(base)) {
1356
+ const resolved = _resolveSimpleLookupToJsonNode(rest, base, formElement);
1357
+ if (resolved === null) return [];
1358
+ if (Array.isArray(resolved)) return resolved;
1359
+ return [resolved];
1360
+ }
1361
+ if (typeof _resolveLookupOnMapItem === 'function') {
1362
+ const resolved = _resolveLookupOnMapItem(base, rest);
1363
+ if (resolved == null) return [];
1364
+ return Array.isArray(resolved) ? resolved : [resolved];
1365
+ }
1366
+ }
1367
+
1368
+ return base == null ? [] : [base];
1369
+ }
1370
+ // --- END NEW ---
1371
+
1372
+ const relativeJson = _isRelativeJsonLookup(expr0, contextNode);
1373
+
1374
+ const instanceId = relativeJson ? null : _getInstanceIdForLookupExpr(expr0, formElement);
1375
+ const instance = relativeJson ? null : _getInstanceFromFormElement(formElement, instanceId);
1376
+
1377
+ if (!relativeJson && !instance) {
1378
+ formElement?.dispatchEvent?.(
1379
+ new CustomEvent('error', {
1380
+ composed: false,
1381
+ bubbles: true,
1382
+ detail: {
1383
+ origin: formElement,
1384
+ message: `Instance with id '${instanceId}' not found for expression '${expr0}'`,
1385
+ expr: expr0,
1386
+ level: 'Error',
1387
+ },
1388
+ }),
1389
+ );
1390
+ }
1391
+
1392
+ if (relativeJson || _isJsonInstance(instance)) {
1393
+ const sp = _splitStarPredicate(expr0);
1394
+ if (sp) {
1395
+ const baseResolved = _resolveSimpleLookupToJsonNode(sp.base, contextNode, formElement);
1396
+ const baseNodes = Array.isArray(baseResolved)
1397
+ ? baseResolved
1398
+ : baseResolved
1399
+ ? [baseResolved]
1400
+ : [];
1401
+ return _filterJsonNodesByPredicate(baseNodes, sp.predicate, formElement, variables);
1402
+ }
1403
+
1404
+ if (_isSimpleLookupExpr(expr0)) {
1405
+ const resolved = _resolveSimpleLookupToJsonNode(expr0, contextNode, formElement);
1406
+ if (resolved === null) return [];
1407
+ if (Array.isArray(resolved)) return resolved;
1408
+ return [resolved];
1409
+ }
1410
+
1411
+ return [];
1412
+ }
1413
+ }
1414
+
1415
+ const namespaceResolver = createNamespaceResolverForNode(expr0, contextNode, formElement);
482
1416
  const variablesInScope = getVariablesInScope(formElement);
483
1417
 
484
- const result = fxEvaluateXPath(
485
- xpath,
1418
+ return fxEvaluateXPath(
1419
+ expr0,
486
1420
  contextNode,
487
- null,
1421
+ domFacade,
488
1422
  { ...variablesInScope, ...variables },
489
1423
  fxEvaluateXPath.ALL_RESULTS_TYPE,
490
1424
  {
491
- xmlSerializer: new XMLSerializer(),
492
1425
  debug: true,
493
1426
  currentContext: { formElement, variables },
494
- moduleImports: {
495
- xf: XFORMS_NAMESPACE_URI,
496
- },
1427
+ moduleImports: { xf: XFORMS_NAMESPACE_URI },
497
1428
  functionNameResolver,
498
1429
  namespaceResolver,
499
- language: options.language || fxEvaluateXPath.XPATH_3_1_LANGUAGE,
1430
+ language: Language.XPATH_3_1_LANGUAGE,
1431
+ xmlSerializer: new XMLSerializer(),
1432
+ ...options,
500
1433
  },
501
1434
  );
502
- // console.log('evaluateXPath',xpath, result);
503
- return result;
504
1435
  } catch (e) {
505
- formElement.dispatchEvent(
1436
+ formElement?.dispatchEvent?.(
506
1437
  new CustomEvent('error', {
507
1438
  composed: false,
508
1439
  bubbles: true,
@@ -514,51 +1445,70 @@ export function evaluateXPath(xpath, contextNode, formElement, variables = {}, o
514
1445
  },
515
1446
  }),
516
1447
  );
517
-
518
- /*
519
- formElement.dispatchEvent(
520
- new CustomEvent('error', {
521
- composed: false,
522
- bubbles: true,
523
- cancelable:true,
524
- detail: {
525
- origin: formElement,
526
- message: `Expression '${xpath}' failed`,
527
- expr:xpath,
528
- level:'Error'},
529
- }),
530
- );
531
- */
532
- // Return 'nothing' in hope the rest of the page can forgive this
533
1448
  return [];
534
1449
  }
535
1450
  }
536
- /**
537
- * Evaluate an XPath to the first Node
538
- *
539
- * @param {string} xpath The XPath to run
540
- * @param {Node} contextNode The start of the XPath
541
- * @param {import('./ForeElementMixin.js').default} formElement The form element associated to the XPath
542
- * @returns {Node} The first node found in the XPath
543
- */
544
1451
  export function evaluateXPathToFirstNode(xpath, contextNode, formElement) {
1452
+ const expr0 = String(xpath ?? '').trim();
1453
+
545
1454
  try {
546
- const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
1455
+ if (_isJsonNode(contextNode) && expr0 === '.') {
1456
+ return contextNode;
1457
+ }
1458
+ if (_isJsonNode(contextNode) && !_looksLikeLookupExpr(expr0)) {
1459
+ const variablesInScope = getVariablesInScope(formElement);
1460
+ return fxEvaluateXPathToFirstNode(expr0, contextNode, __jsonDomFacade, variablesInScope, {
1461
+ currentContext: { formElement },
1462
+ functionNameResolver,
1463
+ moduleImports: { xf: XFORMS_NAMESPACE_URI },
1464
+ namespaceResolver: null,
1465
+ language: Language.XPATH_3_1_LANGUAGE,
1466
+ xmlSerializer: new XMLSerializer(),
1467
+ });
1468
+ }
1469
+
1470
+ if (_looksLikeLookupExpr(expr0)) {
1471
+ const relativeJson = _isRelativeJsonLookup(expr0, contextNode);
1472
+ const instanceId = relativeJson ? null : _getInstanceIdForLookupExpr(expr0, formElement);
1473
+ const instance = relativeJson ? null : _getInstanceFromFormElement(formElement, instanceId);
1474
+
1475
+ if (relativeJson || _isJsonInstance(instance)) {
1476
+ const sp = _splitStarPredicate(expr0);
1477
+ if (sp) {
1478
+ const baseResolved = _resolveSimpleLookupToJsonNode(sp.base, contextNode, formElement);
1479
+ const baseNodes = Array.isArray(baseResolved)
1480
+ ? baseResolved
1481
+ : baseResolved
1482
+ ? [baseResolved]
1483
+ : [];
1484
+ const filtered = _filterJsonNodesByPredicate(baseNodes, sp.predicate, formElement);
1485
+ return filtered[0] || null;
1486
+ }
1487
+
1488
+ if (_isSimpleLookupExpr(expr0)) {
1489
+ const resolved = _resolveSimpleLookupToJsonNode(expr0, contextNode, formElement);
1490
+ if (!resolved) return null;
1491
+ if (Array.isArray(resolved)) return resolved[0] || null;
1492
+ return resolved;
1493
+ }
1494
+
1495
+ return null;
1496
+ }
1497
+ }
1498
+
1499
+ const namespaceResolver = createNamespaceResolverForNode(expr0, contextNode, formElement);
547
1500
  const variablesInScope = getVariablesInScope(formElement);
548
- const result = fxEvaluateXPathToFirstNode(xpath, contextNode, null, variablesInScope, {
549
- defaultFunctionNamespaceURI: XFORMS_NAMESPACE_URI,
550
- moduleImports: {
551
- xf: XFORMS_NAMESPACE_URI,
552
- },
1501
+
1502
+ return fxEvaluateXPathToFirstNode(expr0, contextNode, null, variablesInScope, {
553
1503
  currentContext: { formElement },
554
1504
  functionNameResolver,
1505
+ moduleImports: { xf: XFORMS_NAMESPACE_URI },
555
1506
  namespaceResolver,
1507
+ language: Language.XPATH_3_1_LANGUAGE,
556
1508
  xmlSerializer: new XMLSerializer(),
557
1509
  });
558
- // console.log('evaluateXPathToFirstNode',xpath, result);
559
- return result;
560
1510
  } catch (e) {
561
- formElement.dispatchEvent(
1511
+ formElement?.dispatchEvent?.(
562
1512
  new CustomEvent('error', {
563
1513
  composed: false,
564
1514
  bubbles: true,
@@ -570,36 +1520,70 @@ export function evaluateXPathToFirstNode(xpath, contextNode, formElement) {
570
1520
  },
571
1521
  }),
572
1522
  );
1523
+ return null;
573
1524
  }
574
- return null;
575
1525
  }
576
1526
 
577
- /**
578
- * Evaluate an XPath to all nodes
579
- *
580
- * @param {string} xpath The XPath to run
581
- * @param {Node} contextNode The start of the XPath
582
- * @param {import('./ForeElementMixin.js').default} formElement The form element associated to the XPath
583
- * @return {Node[]} All nodes
584
- */
585
1527
  export function evaluateXPathToNodes(xpath, contextNode, formElement) {
1528
+ const expr0 = String(xpath ?? '').trim();
1529
+
586
1530
  try {
587
- const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
1531
+ if (_isJsonNode(contextNode) && expr0 === '.') {
1532
+ return [contextNode];
1533
+ }
1534
+ if (_isJsonNode(contextNode) && !_looksLikeLookupExpr(expr0)) {
1535
+ const variablesInScope = getVariablesInScope(formElement);
1536
+ return fxEvaluateXPathToNodes(expr0, contextNode, __jsonDomFacade, variablesInScope, {
1537
+ currentContext: { formElement },
1538
+ functionNameResolver,
1539
+ moduleImports: { xf: XFORMS_NAMESPACE_URI },
1540
+ namespaceResolver: null,
1541
+ language: Language.XPATH_3_1_LANGUAGE,
1542
+ xmlSerializer: new XMLSerializer(),
1543
+ });
1544
+ }
1545
+
1546
+ if (_looksLikeLookupExpr(expr0)) {
1547
+ const relativeJson = _isRelativeJsonLookup(expr0, contextNode);
1548
+ const instanceId = relativeJson ? null : _getInstanceIdForLookupExpr(expr0, formElement);
1549
+ const instance = relativeJson ? null : _getInstanceFromFormElement(formElement, instanceId);
1550
+
1551
+ if (relativeJson || _isJsonInstance(instance)) {
1552
+ const sp = _splitStarPredicate(expr0);
1553
+ if (sp) {
1554
+ const baseResolved = _resolveSimpleLookupToJsonNode(sp.base, contextNode, formElement);
1555
+ const baseNodes = Array.isArray(baseResolved)
1556
+ ? baseResolved
1557
+ : baseResolved
1558
+ ? [baseResolved]
1559
+ : [];
1560
+ return _filterJsonNodesByPredicate(baseNodes, sp.predicate, formElement);
1561
+ }
1562
+
1563
+ if (_isSimpleLookupExpr(expr0)) {
1564
+ const resolved = _resolveSimpleLookupToJsonNode(expr0, contextNode, formElement);
1565
+ if (!resolved) return [];
1566
+ if (Array.isArray(resolved)) return resolved;
1567
+ return [resolved];
1568
+ }
1569
+
1570
+ return [];
1571
+ }
1572
+ }
1573
+
1574
+ const namespaceResolver = createNamespaceResolverForNode(expr0, contextNode, formElement);
588
1575
  const variablesInScope = getVariablesInScope(formElement);
589
1576
 
590
- const result = fxEvaluateXPathToNodes(xpath, contextNode, null, variablesInScope, {
1577
+ return fxEvaluateXPathToNodes(expr0, contextNode, null, variablesInScope, {
591
1578
  currentContext: { formElement },
592
1579
  functionNameResolver,
593
- moduleImports: {
594
- xf: XFORMS_NAMESPACE_URI,
595
- },
1580
+ moduleImports: { xf: XFORMS_NAMESPACE_URI },
596
1581
  namespaceResolver,
1582
+ language: Language.XPATH_3_1_LANGUAGE,
597
1583
  xmlSerializer: new XMLSerializer(),
598
1584
  });
599
- // console.log('evaluateXPathToNodes',xpath, result);
600
- return result;
601
1585
  } catch (e) {
602
- formElement.dispatchEvent(
1586
+ formElement?.dispatchEvent?.(
603
1587
  new CustomEvent('error', {
604
1588
  composed: false,
605
1589
  bubbles: true,
@@ -611,33 +1595,96 @@ export function evaluateXPathToNodes(xpath, contextNode, formElement) {
611
1595
  },
612
1596
  }),
613
1597
  );
1598
+ return [];
614
1599
  }
615
1600
  }
616
1601
 
617
- /**
618
- * Evaluate an XPath to a boolean
619
- *
620
- * @param {string} xpath The XPath to run
621
- * @param {Node} contextNode The start of the XPath
622
- * @param {import('./ForeElementMixin.js').default} formElement The form element associated to the XPath
623
- * @return {boolean}
624
- */
625
1602
  export function evaluateXPathToBoolean(xpath, contextNode, formElement) {
1603
+ const expr0 = String(xpath ?? '').trim();
1604
+
626
1605
  try {
627
- const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
1606
+ const idx = tryResolveIndexExpr(expr0, formElement);
1607
+ if (idx !== null) return Boolean(idx);
1608
+
1609
+ if (_isJsonNode(contextNode) && expr0 === '.') {
1610
+ return true;
1611
+ }
1612
+
1613
+ // ------------------------------------------------------------
1614
+ // JSON CONTEXT
1615
+ // ------------------------------------------------------------
1616
+ if (_isJsonNode(contextNode)) {
1617
+ // 1) Star predicate (repeat filtering style): ?movies?*[ ... ]
1618
+ if (_looksLikeLookupExpr(expr0)) {
1619
+ const sp = _splitStarPredicate(expr0);
1620
+ if (sp) {
1621
+ const baseResolved = _resolveSimpleLookupToJsonNode(sp.base, contextNode, formElement);
1622
+ const baseNodes = Array.isArray(baseResolved)
1623
+ ? baseResolved
1624
+ : baseResolved
1625
+ ? [baseResolved]
1626
+ : [];
1627
+ return _filterJsonNodesByPredicate(baseNodes, sp.predicate, formElement).length > 0;
1628
+ }
1629
+
1630
+ // 2) Simple navigation lookup: ?title, instance('data')?ui?query, etc.
1631
+ if (_isSimpleLookupExpr(expr0)) {
1632
+ const resolved = _resolveSimpleLookupToJsonNode(expr0, contextNode, formElement);
1633
+ if (!resolved) return false;
1634
+ const node = Array.isArray(resolved) ? resolved[0] : resolved;
1635
+ return Boolean(_isJsonNode(node) ? node.value : node);
1636
+ }
1637
+
1638
+ // 3) ✅ Complex expressions containing lookup operator, e.g.
1639
+ // contains(?title, instance('data')?ui?query)
1640
+ //
1641
+ // Important: XPath 3.1 lookup operator works on map(*) / array(*) items.
1642
+ // A JSON lens node is not a map(*) to the XPath engine.
1643
+ // So evaluate against the RAW JS value of the current JSON node.
1644
+ // Also enable jsonMode:'raw' so instance('data') returns raw JS root without recursion.
1645
+ const rawContext =
1646
+ typeof contextNode.getValue === 'function' ? contextNode.getValue() : contextNode.value;
1647
+
1648
+ const variablesInScope = getVariablesInScope(formElement);
1649
+
1650
+ return fxEvaluateXPathToBoolean(expr0, rawContext, null, variablesInScope, {
1651
+ currentContext: { formElement, jsonMode: 'raw' },
1652
+ functionNameResolver,
1653
+ moduleImports: { xf: XFORMS_NAMESPACE_URI },
1654
+ namespaceResolver: null,
1655
+ language: Language.XPATH_3_1_LANGUAGE,
1656
+ xmlSerializer: new XMLSerializer(),
1657
+ });
1658
+ }
1659
+
1660
+ // 4) Non-lookup XPath in JSON context: evaluate via JSONDomFacade
1661
+ const variablesInScope = getVariablesInScope(formElement);
1662
+ return fxEvaluateXPathToBoolean(expr0, contextNode, __jsonDomFacade, variablesInScope, {
1663
+ currentContext: { formElement },
1664
+ functionNameResolver,
1665
+ moduleImports: { xf: XFORMS_NAMESPACE_URI },
1666
+ namespaceResolver: null,
1667
+ language: Language.XPATH_3_1_LANGUAGE,
1668
+ xmlSerializer: new XMLSerializer(),
1669
+ });
1670
+ }
1671
+
1672
+ // ------------------------------------------------------------
1673
+ // XML / normal evaluation
1674
+ // ------------------------------------------------------------
1675
+ const namespaceResolver = createNamespaceResolverForNode(expr0, contextNode, formElement);
628
1676
  const variablesInScope = getVariablesInScope(formElement);
629
1677
 
630
- return fxEvaluateXPathToBoolean(xpath, contextNode, null, variablesInScope, {
1678
+ return fxEvaluateXPathToBoolean(expr0, contextNode, null, variablesInScope, {
631
1679
  currentContext: { formElement },
632
1680
  functionNameResolver,
633
- moduleImports: {
634
- xf: XFORMS_NAMESPACE_URI,
635
- },
1681
+ moduleImports: { xf: XFORMS_NAMESPACE_URI },
636
1682
  namespaceResolver,
1683
+ language: Language.XPATH_3_1_LANGUAGE,
637
1684
  xmlSerializer: new XMLSerializer(),
638
1685
  });
639
1686
  } catch (e) {
640
- formElement.dispatchEvent(
1687
+ formElement?.dispatchEvent?.(
641
1688
  new CustomEvent('error', {
642
1689
  composed: false,
643
1690
  bubbles: true,
@@ -649,36 +1696,123 @@ export function evaluateXPathToBoolean(xpath, contextNode, formElement) {
649
1696
  },
650
1697
  }),
651
1698
  );
1699
+ return false;
652
1700
  }
653
1701
  }
654
-
655
- /**
656
- * Evaluate an XPath to a string
657
- *
658
- * @param {string} xpath The XPath to run
659
- * @param {Node} contextNode The start of the XPath
660
- * @param {Node} formElement The form element associated to the XPath
661
- * @param {Node} formElement The element where the XPath is defined: used for namespace resolving
662
- * @param {import('fontoxpath').IDomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
663
- * access. This is used to determine dependencies between bind elements.
664
- * @return {string}
665
- */
666
1702
  export function evaluateXPathToString(xpath, contextNode, formElement, domFacade = null) {
1703
+ const expr0 = String(xpath ?? '').trim();
1704
+
1705
+ const stringify = v => {
1706
+ if (v === null || v === undefined) return '';
1707
+ if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') return String(v);
1708
+ if (v?.nodeType) {
1709
+ if (v.nodeType === Node.ATTRIBUTE_NODE) return String(v.nodeValue ?? '');
1710
+ return String(v.textContent ?? '');
1711
+ }
1712
+ if (_isJsonNode(v)) {
1713
+ const vv = v.value;
1714
+ if (vv === null || vv === undefined) return '';
1715
+ if (typeof vv === 'string' || typeof vv === 'number' || typeof vv === 'boolean')
1716
+ return String(vv);
1717
+ try {
1718
+ return JSON.stringify(vv);
1719
+ } catch (_e) {
1720
+ return '';
1721
+ }
1722
+ }
1723
+ try {
1724
+ return JSON.stringify(v);
1725
+ } catch (_e) {
1726
+ return '';
1727
+ }
1728
+ };
1729
+
667
1730
  try {
668
- const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
1731
+ const idx = tryResolveIndexExpr(expr0, formElement);
1732
+ if (idx !== null) return String(idx);
1733
+
1734
+ if (_isJsonNode(contextNode) && expr0 === '.') {
1735
+ return stringify(contextNode);
1736
+ }
1737
+
1738
+ // JSON context, non-lookup => evaluate via JSON facade
1739
+ if (_isJsonNode(contextNode) && !_looksLikeLookupExpr(expr0)) {
1740
+ const variablesInScope = getVariablesInScope(formElement);
1741
+ const res = fxEvaluateXPathToString(expr0, contextNode, __jsonDomFacade, variablesInScope, {
1742
+ currentContext: { formElement },
1743
+ functionNameResolver,
1744
+ moduleImports: { xf: XFORMS_NAMESPACE_URI },
1745
+ namespaceResolver: null,
1746
+ language: Language.XPATH_3_1_LANGUAGE,
1747
+ xmlSerializer: new XMLSerializer(),
1748
+ });
1749
+ return stringify(res);
1750
+ }
1751
+
1752
+ // ✅ Lookup expressions (JSON lens / XQuery map lookups)
1753
+ if (_looksLikeLookupExpr(expr0)) {
1754
+ // --- NEW: support variable lookups like $default?ui?query ---
1755
+ const varLens = expr0.match(/^\$([A-Za-z_][\w.-]*)(.*)$/);
1756
+ if (varLens) {
1757
+ const varName = varLens[1];
1758
+ const rest = varLens[2] || '';
1759
+
1760
+ const inScope = getVariablesInScope(formElement);
1761
+ const base =
1762
+ inScope && Object.prototype.hasOwnProperty.call(inScope, varName)
1763
+ ? inScope[varName]
1764
+ : null;
1765
+
1766
+ if (!rest) return stringify(base);
1767
+
1768
+ if (rest.startsWith('?') || rest.startsWith('.?')) {
1769
+ if (base && _isJsonNode(base)) {
1770
+ const resolved = _resolveSimpleLookupToJsonNode(rest, base, formElement);
1771
+ return stringify(resolved);
1772
+ }
1773
+ // Map/array/plain object lookup
1774
+ if (typeof _resolveLookupOnMapItem === 'function') {
1775
+ const resolved = _resolveLookupOnMapItem(base, rest);
1776
+ return stringify(resolved);
1777
+ }
1778
+ }
1779
+
1780
+ return stringify(base);
1781
+ }
1782
+ // --- END NEW ---
1783
+
1784
+ const relativeJson = _isRelativeJsonLookup(expr0, contextNode);
1785
+ const instanceId = relativeJson ? null : _getInstanceIdForLookupExpr(expr0, formElement);
1786
+ const instance = relativeJson ? null : _getInstanceFromFormElement(formElement, instanceId);
1787
+
1788
+ if (relativeJson || _isJsonInstance(instance)) {
1789
+ if (_isSimpleLookupExpr(expr0)) {
1790
+ const resolved = _resolveSimpleLookupToJsonNode(expr0, contextNode, formElement);
1791
+ if (!resolved) return '';
1792
+ const node = Array.isArray(resolved) ? resolved[0] : resolved;
1793
+ return stringify(node);
1794
+ }
1795
+ // for now, keep string conversions conservative
1796
+ return '';
1797
+ }
1798
+ }
1799
+
1800
+ // Normal XPath path
1801
+ const namespaceResolver = createNamespaceResolverForNode(expr0, contextNode, formElement);
669
1802
  const variablesInScope = getVariablesInScope(formElement);
670
1803
 
671
- return fxEvaluateXPathToString(xpath, contextNode, domFacade, variablesInScope, {
1804
+ const res = fxEvaluateXPathToString(expr0, contextNode, domFacade, variablesInScope, {
672
1805
  currentContext: { formElement },
673
1806
  functionNameResolver,
674
- moduleImports: {
675
- xf: XFORMS_NAMESPACE_URI,
676
- },
1807
+ moduleImports: { xf: XFORMS_NAMESPACE_URI },
677
1808
  namespaceResolver,
1809
+ language: Language.XPATH_3_1_LANGUAGE,
678
1810
  xmlSerializer: new XMLSerializer(),
679
1811
  });
1812
+
1813
+ return stringify(res);
680
1814
  } catch (e) {
681
- formElement.dispatchEvent(
1815
+ formElement?.dispatchEvent?.(
682
1816
  new CustomEvent('error', {
683
1817
  composed: false,
684
1818
  bubbles: true,
@@ -690,40 +1824,95 @@ export function evaluateXPathToString(xpath, contextNode, formElement, domFacade
690
1824
  },
691
1825
  }),
692
1826
  );
1827
+ return '';
693
1828
  }
694
1829
  }
695
-
696
- /**
697
- * Evaluate an XPath to a set of strings
698
- *
699
- * @param {string} xpath The XPath to run
700
- * @param {Node} contextNode The start of the XPath
701
- * @param {Node} formElement The form element associated to the XPath
702
- * @param {Node} formElement The element where the XPath is defined: used for namespace resolving
703
- * @param {import('fontoxpath').IDomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
704
- * access. This is used to determine dependencies between bind elements.
705
- * @return {string[]}
706
- */
707
1830
  export function evaluateXPathToStrings(xpath, contextNode, formElement, domFacade = null) {
1831
+ const expr0 = String(xpath ?? '').trim();
1832
+
1833
+ const stringify = v => {
1834
+ if (v === null || v === undefined) return '';
1835
+ if (
1836
+ typeof v === 'string' ||
1837
+ typeof v === 'number' ||
1838
+ typeof v === 'boolean' ||
1839
+ typeof v === 'bigint'
1840
+ )
1841
+ return String(v);
1842
+ if (v?.nodeType) {
1843
+ if (v.nodeType === Node.ATTRIBUTE_NODE) return String(v.nodeValue ?? '');
1844
+ return String(v.textContent ?? '');
1845
+ }
1846
+ if (_isJsonNode(v)) return stringify(v.value);
1847
+ try {
1848
+ return JSON.stringify(v);
1849
+ } catch (_e) {
1850
+ return '';
1851
+ }
1852
+ };
1853
+
708
1854
  try {
709
- const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
710
- return fxEvaluateXPathToStrings(
711
- xpath,
1855
+ const idx = tryResolveIndexExpr(expr0, formElement);
1856
+ if (idx !== null) return [String(idx)];
1857
+
1858
+ if (_isJsonNode(contextNode) && expr0 === '.') {
1859
+ return [stringify(contextNode)];
1860
+ }
1861
+
1862
+ if (_isJsonNode(contextNode) && !_looksLikeLookupExpr(expr0)) {
1863
+ const res = fxEvaluateXPathToStrings(
1864
+ expr0,
1865
+ contextNode,
1866
+ __jsonDomFacade,
1867
+ getVariablesInScope(formElement),
1868
+ {
1869
+ currentContext: { formElement },
1870
+ functionNameResolver,
1871
+ moduleImports: { xf: XFORMS_NAMESPACE_URI },
1872
+ namespaceResolver: null,
1873
+ language: Language.XPATH_3_1_LANGUAGE,
1874
+ xmlSerializer: new XMLSerializer(),
1875
+ },
1876
+ );
1877
+ return Array.isArray(res) ? res.map(stringify) : [stringify(res)];
1878
+ }
1879
+
1880
+ if (_looksLikeLookupExpr(expr0)) {
1881
+ const relativeJson = _isRelativeJsonLookup(expr0, contextNode);
1882
+ const instanceId = relativeJson ? null : _getInstanceIdForLookupExpr(expr0, formElement);
1883
+ const instance = relativeJson ? null : _getInstanceFromFormElement(formElement, instanceId);
1884
+
1885
+ if (relativeJson || _isJsonInstance(instance)) {
1886
+ if (_isSimpleLookupExpr(expr0)) {
1887
+ const resolved = _resolveSimpleLookupToJsonNode(expr0, contextNode, formElement);
1888
+ if (!resolved) return [];
1889
+ const arr = Array.isArray(resolved) ? resolved : [resolved];
1890
+ return arr.map(stringify);
1891
+ }
1892
+ return [];
1893
+ }
1894
+ }
1895
+
1896
+ const namespaceResolver = createNamespaceResolverForNode(expr0, contextNode, formElement);
1897
+
1898
+ const res = fxEvaluateXPathToStrings(
1899
+ expr0,
712
1900
  contextNode,
713
1901
  domFacade,
714
- {},
1902
+ getVariablesInScope(formElement),
715
1903
  {
716
1904
  currentContext: { formElement },
717
1905
  functionNameResolver,
718
- moduleImports: {
719
- xf: XFORMS_NAMESPACE_URI,
720
- },
1906
+ moduleImports: { xf: XFORMS_NAMESPACE_URI },
721
1907
  namespaceResolver,
1908
+ language: Language.XPATH_3_1_LANGUAGE,
722
1909
  xmlSerializer: new XMLSerializer(),
723
1910
  },
724
1911
  );
1912
+
1913
+ return Array.isArray(res) ? res.map(stringify) : [stringify(res)];
725
1914
  } catch (e) {
726
- formElement.dispatchEvent(
1915
+ formElement?.dispatchEvent?.(
727
1916
  new CustomEvent('error', {
728
1917
  composed: false,
729
1918
  bubbles: true,
@@ -735,36 +1924,59 @@ export function evaluateXPathToStrings(xpath, contextNode, formElement, domFacad
735
1924
  },
736
1925
  }),
737
1926
  );
1927
+ return [];
738
1928
  }
739
1929
  }
740
1930
 
741
- /**
742
- * Evaluate an XPath to a number
743
- *
744
- * @param {string} xpath The XPath to run
745
- * @param {Node} contextNode The start of the XPath
746
- * @param {Node} formElement The form element associated to the XPath
747
- * @param {Node} formElement The element where the XPath is defined: used for namespace resolving
748
- * @param {import('fontoxpath').IDomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
749
- * access. This is used to determine dependencies between bind elements.
750
- * @return {number}
751
- */
752
1931
  export function evaluateXPathToNumber(xpath, contextNode, formElement, domFacade = null) {
1932
+ const expr0 = String(xpath ?? '').trim();
1933
+
753
1934
  try {
754
- const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
1935
+ const idx = tryResolveIndexExpr(expr0, formElement);
1936
+ if (idx !== null) return idx;
1937
+
1938
+ if (_isJsonNode(contextNode) && expr0 === '.') {
1939
+ const s = String(contextNode?.value ?? '');
1940
+ const n = Number(s);
1941
+ return Number.isFinite(n) ? n : NaN;
1942
+ }
1943
+
1944
+ if (_isJsonNode(contextNode) && !_looksLikeLookupExpr(expr0)) {
1945
+ const variablesInScope = getVariablesInScope(formElement);
1946
+ return fxEvaluateXPathToNumber(expr0, contextNode, __jsonDomFacade, variablesInScope, {
1947
+ currentContext: { formElement },
1948
+ functionNameResolver,
1949
+ moduleImports: { xf: XFORMS_NAMESPACE_URI },
1950
+ namespaceResolver: null,
1951
+ language: Language.XPATH_3_1_LANGUAGE,
1952
+ xmlSerializer: new XMLSerializer(),
1953
+ });
1954
+ }
1955
+
1956
+ if (_looksLikeLookupExpr(expr0)) {
1957
+ const relativeJson = _isRelativeJsonLookup(expr0, contextNode);
1958
+ const instanceId = relativeJson ? null : _getInstanceIdForLookupExpr(expr0, formElement);
1959
+ const instance = relativeJson ? null : _getInstanceFromFormElement(formElement, instanceId);
1960
+
1961
+ if (relativeJson || _isJsonInstance(instance)) {
1962
+ // numbers for JSON are not a priority for now; keep conservative
1963
+ return NaN;
1964
+ }
1965
+ }
1966
+
1967
+ const namespaceResolver = createNamespaceResolverForNode(expr0, contextNode, formElement);
755
1968
  const variablesInScope = getVariablesInScope(formElement);
756
1969
 
757
- return fxEvaluateXPathToNumber(xpath, contextNode, domFacade, variablesInScope, {
1970
+ return fxEvaluateXPathToNumber(expr0, contextNode, domFacade, variablesInScope, {
758
1971
  currentContext: { formElement },
759
1972
  functionNameResolver,
760
- moduleImports: {
761
- xf: XFORMS_NAMESPACE_URI,
762
- },
1973
+ moduleImports: { xf: XFORMS_NAMESPACE_URI },
763
1974
  namespaceResolver,
1975
+ language: Language.XPATH_3_1_LANGUAGE,
764
1976
  xmlSerializer: new XMLSerializer(),
765
1977
  });
766
1978
  } catch (e) {
767
- formElement.dispatchEvent(
1979
+ formElement?.dispatchEvent?.(
768
1980
  new CustomEvent('error', {
769
1981
  composed: false,
770
1982
  bubbles: true,
@@ -776,74 +1988,60 @@ export function evaluateXPathToNumber(xpath, contextNode, formElement, domFacade
776
1988
  },
777
1989
  }),
778
1990
  );
1991
+ return NaN;
779
1992
  }
780
1993
  }
781
1994
 
1995
+ // ------------------------------------------------------------
1996
+ // Custom functions (XForms/Fore)
1997
+ // ------------------------------------------------------------
1998
+
1999
+ // context()
782
2000
  const contextFunction = (dynamicContext, string) => {
783
2001
  const caller = dynamicContext.currentContext.formElement;
784
- let instance = null;
785
- if (string) {
786
- instance = resolveId(string, caller);
787
- } else {
788
- instance = XPathUtil.getParentBindingElement(caller);
789
- }
790
- if (instance) {
791
- if (instance.nodeName === 'FX-REPEAT') {
792
- const { nodeset } = instance;
2002
+ let instanceEl = null;
2003
+ if (string) instanceEl = resolveId(string, caller);
2004
+ else instanceEl = XPathUtil.getParentBindingElement(caller);
2005
+
2006
+ if (instanceEl) {
2007
+ if (instanceEl.nodeName === 'FX-REPEAT') {
2008
+ const { nodeset } = instanceEl;
793
2009
  for (let parent = caller; parent; parent = parent.parentNode) {
794
- if (parent.parentNode === instance) {
2010
+ if (parent.parentNode === instanceEl) {
795
2011
  const offset = Array.from(parent.parentNode.children).indexOf(parent);
796
2012
  return nodeset[offset];
797
2013
  }
798
2014
  }
799
2015
  }
800
- return instance.nodeset;
2016
+ return instanceEl.nodeset;
801
2017
  }
802
2018
 
803
2019
  return caller.getInScopeContext();
804
2020
  };
805
2021
 
806
- // todo: implement
807
- const currentFunction = (dynamicContext, string) => {
808
- const caller = dynamicContext.currentContext.formElement;
809
- return null;
810
- };
2022
+ // current() - todo
2023
+ const currentFunction = (_dynamicContext, _string) => null;
811
2024
 
812
- const elementFunction = (dynamicContext, string) => {
813
- const caller = dynamicContext.currentContext.formElement;
814
- const newElement = document.createElement(string);
815
- return newElement;
816
- };
2025
+ const elementFunction = (_dynamicContext, string) => document.createElement(string);
817
2026
 
818
- /**
819
- * @param id as string
820
- * @return instance data for given id serialized to string.
821
- */
822
2027
  registerCustomXPathFunction(
823
2028
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'context' },
824
2029
  [],
825
2030
  'item()?',
826
2031
  contextFunction,
827
2032
  );
828
-
829
- /**
830
- * @param id as string
831
- * @return instance data for given id serialized to string.
832
- */
833
2033
  registerCustomXPathFunction(
834
2034
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'context' },
835
2035
  ['xs:string'],
836
2036
  'item()?',
837
2037
  contextFunction,
838
2038
  );
839
-
840
2039
  registerCustomXPathFunction(
841
2040
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'current' },
842
2041
  ['xs:string'],
843
2042
  'item()?',
844
2043
  currentFunction,
845
2044
  );
846
-
847
2045
  registerCustomXPathFunction(
848
2046
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'element' },
849
2047
  ['xs:string'],
@@ -851,29 +2049,25 @@ registerCustomXPathFunction(
851
2049
  elementFunction,
852
2050
  );
853
2051
 
854
- /**
855
- * @param id as string
856
- * @return instance data for given id serialized to string.
857
- */
858
2052
  registerCustomXPathFunction(
859
2053
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'log' },
860
2054
  ['xs:string?'],
861
2055
  'xs:string?',
862
2056
  (dynamicContext, string) => {
863
2057
  const { formElement } = dynamicContext.currentContext;
864
- const instance = resolveId(string, formElement, 'fx-instance');
865
- if (instance) {
866
- if (instance.getAttribute('type') === 'json') {
2058
+ const instanceEl = resolveId(string, formElement, 'fx-instance');
2059
+ if (instanceEl) {
2060
+ if (instanceEl.getAttribute('type') === 'json') {
867
2061
  console.warn('log() does not work for JSON yet');
868
- // return JSON.stringify(instance.getDefaultContext());
869
- } else {
870
- const def = new XMLSerializer().serializeToString(instance.getDefaultContext());
871
- return prettifyXml(def);
2062
+ return JSON.stringify(instanceEl.getDefaultContext());
872
2063
  }
2064
+ const def = new XMLSerializer().serializeToString(instanceEl.getDefaultContext());
2065
+ return prettifyXml(def);
873
2066
  }
874
2067
  return null;
875
2068
  },
876
2069
  );
2070
+
877
2071
  registerCustomXPathFunction(
878
2072
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'fore-attr' },
879
2073
  ['xs:string?'],
@@ -882,13 +2076,10 @@ registerCustomXPathFunction(
882
2076
  const { formElement } = dynamicContext.currentContext;
883
2077
 
884
2078
  let parent = formElement;
885
- if (formElement.nodeType === Node.TEXT_NODE) {
886
- parent = formElement.parentNode;
887
- }
2079
+ if (formElement.nodeType === Node.TEXT_NODE) parent = formElement.parentNode;
2080
+
888
2081
  const foreElement = parent.closest('fx-fore');
889
- if (foreElement.hasAttribute(string)) {
890
- return foreElement.getAttribute(string);
891
- }
2082
+ if (foreElement.hasAttribute(string)) return foreElement.getAttribute(string);
892
2083
  return null;
893
2084
  },
894
2085
  );
@@ -900,79 +2091,45 @@ registerCustomXPathFunction(
900
2091
  (_dynamicContext, string) => {
901
2092
  const parser = new DOMParser();
902
2093
  const out = parser.parseFromString(string, 'application/xml');
903
- console.log('parse', out);
904
-
905
- /*
906
- const {formElement} = dynamicContext.currentContext;
907
- const instance = resolveId(string, formElement, 'fx-instance');
908
- if (instance) {
909
- if (instance.getAttribute('type') === 'json') {
910
- console.warn('log() does not work for JSON yet');
911
- // return JSON.stringify(instance.getDefaultContext());
912
- } else {
913
- const def = new XMLSerializer().serializeToString(instance.getDefaultContext());
914
- return Fore.prettifyXml(def);
915
- }
916
- }
917
- */
918
2094
  return out.firstElementChild;
919
2095
  },
920
2096
  );
921
2097
 
922
2098
  function buildTree(tree, data) {
923
2099
  if (!data) return;
924
- if (data.nodeType === Node.ELEMENT_NODE) {
925
- if (data.children) {
926
- const details = document.createElement('details');
927
- details.setAttribute('data-path', data.nodeName);
928
- const summary = document.createElement('summary');
929
-
930
- let display = ` <${data.nodeName}`;
931
- Array.from(data.attributes).forEach(attr => {
932
- display += ` ${attr.nodeName}="${attr.nodeValue}"`;
933
- });
2100
+ if (data.nodeType !== Node.ELEMENT_NODE) return;
934
2101
 
935
- let contents;
936
- if (
937
- data.firstChild &&
938
- data.firstChild.nodeType === Node.TEXT_NODE &&
939
- data.firstChild.data.trim() !== ''
940
- ) {
941
- // console.log('whoooooooooopp');
942
- contents = data.firstChild.nodeValue;
943
- display += `>${contents}</${data.nodeName}>`;
944
- } else {
945
- display += '>';
946
- }
947
- summary.textContent = display;
2102
+ const details = document.createElement('details');
2103
+ details.setAttribute('data-path', data.nodeName);
2104
+ const summary = document.createElement('summary');
948
2105
 
949
- details.appendChild(summary);
950
- if (data.childElementCount !== 0) {
951
- details.setAttribute('open', 'open');
952
- } else {
953
- summary.setAttribute('style', 'list-style:none;');
954
- }
955
- tree.appendChild(details);
2106
+ let display = ` <${data.nodeName}`;
2107
+ Array.from(data.attributes).forEach(attr => {
2108
+ display += ` ${attr.nodeName}="${attr.nodeValue}"`;
2109
+ });
956
2110
 
957
- Array.from(data.children).forEach(child => {
958
- // if(child.nodeType === Node.ELEMENT_NODE){
959
- // child.parentNode.appendChild(buildTree(child));
960
- buildTree(details, child);
961
- // }
962
- });
963
- }
964
- } /* else if(data.nodeType === Node.ATTRIBUTE_NODE){
965
- //create span for now
966
- // const span = document.createElement('span');
967
- // span.style.background = 'grey';
968
- // span.textContent = data.value;
969
- // tree.appendChild(span);
970
- tree.setAttribute(data.nodeName,data.value);
971
- }else {
972
- tree.textContent = data;
973
- } */
974
-
975
- // return tree;
2111
+ if (
2112
+ data.firstChild &&
2113
+ data.firstChild.nodeType === Node.TEXT_NODE &&
2114
+ data.firstChild.data.trim() !== ''
2115
+ ) {
2116
+ const contents = data.firstChild.nodeValue;
2117
+ display += `>${contents}</${data.nodeName}>`;
2118
+ } else {
2119
+ display += '>';
2120
+ }
2121
+
2122
+ summary.textContent = display;
2123
+ details.appendChild(summary);
2124
+
2125
+ if (data.childElementCount !== 0) {
2126
+ details.setAttribute('open', 'open');
2127
+ Array.from(data.children).forEach(child => buildTree(details, child));
2128
+ } else {
2129
+ summary.setAttribute('style', 'list-style:none;');
2130
+ }
2131
+
2132
+ tree.appendChild(details);
976
2133
  }
977
2134
 
978
2135
  registerCustomXPathFunction(
@@ -981,71 +2138,103 @@ registerCustomXPathFunction(
981
2138
  'element()?',
982
2139
  (dynamicContext, string) => {
983
2140
  const { formElement } = dynamicContext.currentContext;
984
- const instance = resolveId(string, formElement, 'fx-instance');
985
-
986
- if (instance) {
987
- // const def = new XMLSerializer().serializeToString(instance.getDefaultContext());
988
- // const def = JSON.stringify(instance.getDefaultContext());
989
-
990
- const treeDiv = document.createElement('div');
991
- treeDiv.setAttribute('class', 'logtree');
992
- // const datatree = buildTree(tree,instance.getDefaultContext());
993
- // return tree.appendChild(datatree);
994
- // return buildTree(root,instance.getDefaultContext());;
995
- const form = dynamicContext.currentContext.formElement;
996
- const logtree = form.querySelector('.logtree');
997
- if (logtree) {
998
- logtree.parentNode.removeChild(logtree);
999
- }
1000
- const tree = buildTree(treeDiv, instance.getDefaultContext());
1001
- if (tree) {
1002
- form.appendChild(tree);
1003
- }
1004
- }
2141
+ const instanceEl = resolveId(string, formElement, 'fx-instance');
2142
+ if (!instanceEl) return null;
2143
+
2144
+ const treeDiv = document.createElement('div');
2145
+ treeDiv.setAttribute('class', 'logtree');
2146
+
2147
+ const form = dynamicContext.currentContext.formElement;
2148
+ const logtree = form.querySelector('.logtree');
2149
+ if (logtree) logtree.parentNode.removeChild(logtree);
2150
+
2151
+ buildTree(treeDiv, instanceEl.getDefaultContext());
2152
+ form.appendChild(treeDiv);
2153
+
1005
2154
  return null;
1006
2155
  },
1007
2156
  );
1008
2157
 
1009
- const instance = (dynamicContext, string) => {
1010
- // Spec: https://www.w3.org/TR/xforms-xpath/#The_XForms_Function_Library#The_instance.28.29_Function
1011
- // TODO: handle no string passed (null will be passed instead)
1012
-
1013
- /**
1014
- * @type {import('./fx-fore.js').FxFore}
1015
- */
1016
- const formElement = fxEvaluateXPathToFirstNode(
1017
- 'ancestor-or-self::fx-fore[1]',
1018
- dynamicContext.currentContext.formElement,
1019
- null,
1020
- null,
1021
- { namespaceResolver: xhtmlNamespaceResolver },
1022
- );
2158
+ function _getInstanceDefaultContextNoSideEffects(instEl) {
2159
+ if (!instEl) return null;
1023
2160
 
1024
- let lookup = null;
1025
- if (string === null || string === 'default') {
1026
- lookup = formElement.getModel().getDefaultInstance();
1027
- } else {
1028
- lookup = formElement.getModel().getInstance(string);
1029
- if (!lookup) {
1030
- document.querySelector('fx-fore').dispatchEvent(
1031
- new CustomEvent('error', {
1032
- composed: true,
1033
- bubbles: true,
1034
- detail: {
1035
- origin: 'functions',
1036
- message: `Instance not found '${string}'`,
1037
- level: 'Error',
1038
- },
1039
- }),
1040
- );
2161
+ const type =
2162
+ (typeof instEl.getAttribute === 'function' && instEl.getAttribute('type')) || instEl.type || '';
2163
+ const isJson = type === 'json';
2164
+
2165
+ if (isJson) {
2166
+ // Prefer already-built lens root
2167
+ if (instEl.nodeset && instEl.nodeset.__jsonlens__ === true) return instEl.nodeset;
2168
+ // As a fallback, try to wrap raw if present (should be rare here)
2169
+ return instEl.nodeset || null;
2170
+ }
2171
+
2172
+ // XML/HTML: prefer backing document without calling getters
2173
+ const doc = instEl._instanceData || instEl.nodeset || null;
2174
+ if (doc && doc.nodeType === Node.DOCUMENT_NODE) return doc.firstElementChild;
2175
+ if (doc && doc.nodeType === Node.ELEMENT_NODE) return doc;
2176
+
2177
+ return null;
2178
+ }
2179
+
2180
+ // instance() — supports RAW JSON mode for predicates/filters
2181
+ const instance = (dynamicContext, string) => {
2182
+ let caller = dynamicContext?.currentContext?.formElement || null;
2183
+ if (caller && caller.nodeType === Node.TEXT_NODE) caller = caller.parentNode;
2184
+
2185
+ const fore =
2186
+ (caller && typeof caller.getOwnerForm === 'function' && caller.getOwnerForm()) ||
2187
+ _getOwningFore(caller) ||
2188
+ null;
2189
+
2190
+ if (!fore) return null;
2191
+
2192
+ const modelEl =
2193
+ (typeof fore.getModel === 'function' && fore.getModel()) ||
2194
+ fore.shadowRoot?.querySelector?.('fx-model') ||
2195
+ fore.querySelector?.('fx-model') ||
2196
+ null;
2197
+
2198
+ if (!modelEl) return null;
2199
+
2200
+ const id =
2201
+ string === null || string === undefined || String(string).trim() === ''
2202
+ ? 'default'
2203
+ : String(string);
2204
+
2205
+ let instEl = typeof modelEl.getInstance === 'function' ? modelEl.getInstance(id) : null;
2206
+
2207
+ if (!instEl) {
2208
+ if (id === 'default') {
2209
+ instEl =
2210
+ modelEl.querySelector?.('fx-instance:not([id])') ||
2211
+ modelEl.querySelector?.("fx-instance[id='default']") ||
2212
+ modelEl.querySelector?.('fx-instance') ||
2213
+ null;
2214
+ } else {
2215
+ instEl =
2216
+ resolveId(id, caller, 'fx-instance') ||
2217
+ modelEl.querySelector?.(`#${CSS.escape(id)}`) ||
2218
+ modelEl.querySelector?.(`fx-instance[id="${id}"]`) ||
2219
+ null;
1041
2220
  }
1042
2221
  }
1043
2222
 
1044
- const context = lookup.getDefaultContext();
1045
- if (!context) {
1046
- return null;
2223
+ if (!instEl) return null;
2224
+
2225
+ const type =
2226
+ (typeof instEl.getAttribute === 'function' && instEl.getAttribute('type')) || instEl.type || '';
2227
+ const isJson = type === 'json';
2228
+
2229
+ // RAW JSON mode: return raw JS root (maps/arrays)
2230
+ if (dynamicContext?.currentContext?.jsonMode === 'raw' && isJson) {
2231
+ return _getRawJsonRootValue(instEl);
1047
2232
  }
1048
- return context;
2233
+
2234
+ // Normal mode (side-effect free): do NOT call instEl.getDefaultContext() here.
2235
+ // FxInstance getters may rebuild lenses / dispatch events that re-enter evaluation.
2236
+ const ctx = _getInstanceDefaultContextNoSideEffects(instEl);
2237
+ return ctx;
1049
2238
  };
1050
2239
 
1051
2240
  registerCustomXPathFunction(
@@ -1054,28 +2243,28 @@ registerCustomXPathFunction(
1054
2243
  'xs:integer?',
1055
2244
  (dynamicContext, string) => {
1056
2245
  const { formElement } = dynamicContext.currentContext;
1057
- if (string === null) {
1058
- return 1;
1059
- }
2246
+
2247
+ if (string === null) return 1;
1060
2248
  const repeat = resolveId(string, formElement, 'fx-repeat');
2249
+ if (!repeat) return 1;
1061
2250
 
1062
- // const def = instance.getInstanceData();
1063
- if (repeat) {
1064
- return repeat.getAttribute('index');
1065
- }
1066
- return Number(1);
2251
+ const attr = repeat.getAttribute('index');
2252
+ const attrNum = Number(attr);
2253
+ if (Number.isFinite(attrNum) && attrNum > 0) return attrNum;
2254
+
2255
+ const propNum = Number(repeat.index);
2256
+ if (Number.isFinite(propNum) && propNum > 0) return propNum;
2257
+
2258
+ return 1;
1067
2259
  },
1068
2260
  );
1069
2261
 
1070
- // Note that this is not to spec. The spec enforces elements to be returned from the
1071
- // instance. However, we allow instances to actually be JSON!
1072
2262
  registerCustomXPathFunction(
1073
2263
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'instance' },
1074
2264
  [],
1075
2265
  'item()?',
1076
- domFacade => instance(domFacade, null),
2266
+ dynamicContext => instance(dynamicContext, null),
1077
2267
  );
1078
-
1079
2268
  registerCustomXPathFunction(
1080
2269
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'instance' },
1081
2270
  ['xs:string?'],
@@ -1083,124 +2272,11 @@ registerCustomXPathFunction(
1083
2272
  instance,
1084
2273
  );
1085
2274
 
1086
- const jsonToXml = (_dynamicContext, json) => {
1087
- const escapeXml = str =>
1088
- str.replace(
1089
- /[^\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD]/g,
1090
- char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`,
1091
- );
1092
-
1093
- const convert = (obj, parent) => {
1094
- const type = typeof obj;
1095
- if (type === 'number') {
1096
- parent.setAttribute('type', 'number');
1097
- parent.textContent = obj.toString();
1098
- } else if (type === 'boolean') {
1099
- parent.setAttribute('type', 'boolean');
1100
- parent.textContent = obj.toString();
1101
- } else if (obj === null) {
1102
- const node = document.createElement('_');
1103
- node.setAttribute('type', 'null');
1104
- parent.appendChild(node);
1105
- } else if (type === 'string') {
1106
- parent.textContent = escapeXml(obj);
1107
- } else if (Array.isArray(obj)) {
1108
- parent.setAttribute('type', 'array');
1109
- obj.forEach(item => {
1110
- const node = document.createElement('_');
1111
- convert(item, node);
1112
- node.textContent = item;
1113
- parent.appendChild(node);
1114
- });
1115
- } else if (type === 'object') {
1116
- parent.setAttribute('type', 'object');
1117
- Object.entries(obj).forEach(([key, value]) => {
1118
- if (value) {
1119
- const childNode = document.createElement(key.replace(/[^a-zA-Z0-9_]/g, '_'));
1120
- convert(value, childNode);
1121
- parent.appendChild(childNode);
1122
- }
1123
- });
1124
- }
1125
- };
1126
-
1127
- const root = document.createElement('json');
1128
- if (Array.isArray(json)) {
1129
- root.setAttribute('type', 'array');
1130
- } else {
1131
- root.setAttribute('type', 'object');
1132
- }
1133
- convert(json, root);
1134
- // return root.outerHTML;
1135
- console.log('xml', root);
1136
- return root;
1137
- };
1138
-
1139
- registerCustomXPathFunction(
1140
- { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'json2xml' },
1141
- ['item()?'],
1142
- 'item()?',
1143
- jsonToXml,
1144
- );
1145
- const xmlToJson = (_dynamicContext, xml) => {
1146
- const isElementNode = node => node.nodeType === Node.ELEMENT_NODE;
1147
-
1148
- const isTextNode = node => node.nodeType === Node.TEXT_NODE;
1149
-
1150
- const parseNode = node => {
1151
- if (isElementNode(node)) {
1152
- const obj = {};
1153
- if (node.hasAttributes()) {
1154
- obj.type = node.getAttribute('type');
1155
- }
1156
- if (node.childNodes.length === 1 && isTextNode(node.firstChild)) {
1157
- return node.textContent;
1158
- }
1159
- for (const child of node.childNodes) {
1160
- const childName = child.nodeName;
1161
- const childValue = parseNode(child);
1162
- if (obj[childName]) {
1163
- if (!Array.isArray(obj[childName])) {
1164
- obj[childName] = [obj[childName]];
1165
- }
1166
- obj[childName].push(childValue);
1167
- } else {
1168
- obj[childName] = childValue;
1169
- }
1170
- }
1171
- return obj;
1172
- }
1173
- if (isTextNode(node)) {
1174
- return node.textContent;
1175
- }
1176
- return undefined;
1177
- };
1178
-
1179
- const parser = new DOMParser();
1180
- const xmlDoc = parser.parseFromString(xml, 'application/xml');
1181
- const root = xmlDoc.documentElement;
1182
- return parseNode(root);
1183
- };
1184
- registerCustomXPathFunction(
1185
- { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'xmltoJson' },
1186
- ['item()?'],
1187
- 'item()?',
1188
- xmlToJson,
1189
- );
1190
-
1191
- /*
1192
- // Example usage:
1193
- const xml = '<json type="object"><given>Mark</given><family>Smith</family></json>';
1194
- console.log(xmlToJson(xml));
1195
- */
1196
-
1197
2275
  registerCustomXPathFunction(
1198
2276
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'depends' },
1199
2277
  ['node()*'],
1200
2278
  'item()?',
1201
- (_dynamicContext, nodes) =>
1202
- // console.log('depends on : ', nodes[0]);
1203
- nodes[0],
2279
+ (_dynamicContext, nodes) => nodes[0],
1204
2280
  );
1205
2281
 
1206
2282
  registerCustomXPathFunction(
@@ -1215,13 +2291,8 @@ registerCustomXPathFunction(
1215
2291
  ancestor;
1216
2292
  ancestor = ancestor.parentNode
1217
2293
  ) {
1218
- if (!ancestor.currentEvent) {
1219
- continue;
1220
- }
2294
+ if (!ancestor.currentEvent) continue;
1221
2295
 
1222
- // We have a current event. read the property either from detail, or from the event
1223
- // itself.
1224
- // Check detail for custom events! This is how that is passed along
1225
2296
  if (
1226
2297
  ancestor.currentEvent.detail &&
1227
2298
  typeof ancestor.currentEvent.detail === 'object' &&
@@ -1230,108 +2301,76 @@ registerCustomXPathFunction(
1230
2301
  return ancestor.currentEvent.detail[arg];
1231
2302
  }
1232
2303
 
1233
- // arg might be `code`, so currentEvent.code should work
1234
- if (arg.includes('.')) {
1235
- return _propertyLookup(ancestor.currentEvent, arg);
1236
- }
2304
+ if (arg.includes('.')) return _propertyLookup(ancestor.currentEvent, arg);
2305
+
1237
2306
  return ancestor.currentEvent[arg] || null;
1238
2307
  }
2308
+
1239
2309
  return null;
1240
2310
  },
1241
2311
  );
1242
2312
 
1243
2313
  function _propertyLookup(obj, path) {
1244
2314
  const parts = path.split('.');
1245
- if (parts.length == 1) {
1246
- return obj[parts[0]];
1247
- }
2315
+ if (parts.length === 1) return obj[parts[0]];
1248
2316
  return _propertyLookup(obj[parts[0]], parts.slice(1).join('.'));
1249
2317
  }
1250
2318
 
1251
- // Implement the XForms standard functions here.
1252
2319
  registerXQueryModule(`
1253
- module namespace xf="${XFORMS_NAMESPACE_URI}";
2320
+ module namespace xf="${XFORMS_NAMESPACE_URI}";
1254
2321
 
1255
- declare %public function xf:boolean-from-string($str as xs:string) as xs:boolean {
1256
- lower-case($str) = "true" or $str = "1"
1257
- };
2322
+ declare %public function xf:boolean-from-string($str as xs:string) as xs:boolean {
2323
+ lower-case($str) = "true" or $str = "1"
2324
+ };
1258
2325
  `);
1259
2326
 
1260
- // How to run XQUERY:
1261
- /**
1262
- registerXQueryModule(`
1263
- module namespace my-custom-namespace = "my-custom-uri";
1264
- (:~
1265
- Insert attribute somewhere
1266
- ~:)
1267
- declare %public %updating function my-custom-namespace:do-something ($ele as element()) as xs:boolean {
1268
- if ($ele/@done) then false() else
1269
- (insert node
1270
- attribute done {"true"}
1271
- into $ele, true())
1272
- };
1273
- `)
1274
- // At some point:
1275
- const contextNode = null;
1276
- const pendingUpdatesAndXdmValue = evaluateUpdatingExpressionSync('ns:do-something(.)', contextNode, null, null, {moduleImports: {'ns': 'my-custom-uri'}})
1277
-
1278
- console.log(pendingUpdatesAndXdmValue.xdmValue); // this is true or false, see function
1279
-
1280
- executePendingUpdateList(pendingUpdatesAndXdmValue.pendingUpdateList, null, null, null);
1281
- */
1282
-
1283
- /**
1284
- * @param input as string
1285
- * @return {string}
1286
- */
1287
2327
  registerCustomXPathFunction(
1288
2328
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'base64encode' },
1289
2329
  ['xs:string?'],
1290
2330
  'xs:string?',
1291
2331
  (_dynamicContext, string) => btoa(string),
1292
2332
  );
1293
-
1294
2333
  registerCustomXPathFunction(
1295
2334
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'local-date' },
1296
2335
  [],
1297
2336
  'xs:string?',
1298
- (_dynamicContext, _string) => new Date().toLocaleDateString(),
2337
+ () => new Date().toLocaleDateString(),
1299
2338
  );
1300
2339
  registerCustomXPathFunction(
1301
2340
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'local-dateTime' },
1302
2341
  [],
1303
2342
  'xs:string?',
1304
- (_dynamicContext, _string) => new Date().toLocaleString(),
2343
+ () => new Date().toLocaleString(),
1305
2344
  );
1306
2345
  registerCustomXPathFunction(
1307
2346
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'uri' },
1308
2347
  [],
1309
2348
  'xs:string?',
1310
- (_dynamicContext, _string) => window.location.href,
2349
+ () => window.location.href,
1311
2350
  );
1312
2351
  registerCustomXPathFunction(
1313
2352
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'uri-fragment' },
1314
2353
  [],
1315
2354
  'xs:string?',
1316
- (_dynamicContext, _arg) => window.location.hash,
2355
+ () => window.location.hash,
1317
2356
  );
1318
2357
  registerCustomXPathFunction(
1319
2358
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'uri-host' },
1320
2359
  [],
1321
2360
  'xs:string?',
1322
- (_dynamicContext, _arg) => window.location.host,
2361
+ () => window.location.host,
1323
2362
  );
1324
2363
  registerCustomXPathFunction(
1325
2364
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'uri-query' },
1326
2365
  [],
1327
2366
  'xs:string?',
1328
- (_dynamicContext, _arg) => window.location.search,
2367
+ () => window.location.search,
1329
2368
  );
1330
2369
  registerCustomXPathFunction(
1331
2370
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'uri-relpath' },
1332
2371
  [],
1333
2372
  'xs:string?',
1334
- (_dynamicContext, _arg) => {
2373
+ () => {
1335
2374
  const path = new URL(window.location.href).pathname;
1336
2375
  return path.substring(0, path.lastIndexOf('/') + 1);
1337
2376
  },
@@ -1340,13 +2379,13 @@ registerCustomXPathFunction(
1340
2379
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'uri-path' },
1341
2380
  [],
1342
2381
  'xs:string?',
1343
- (_dynamicContext, _arg) => new URL(window.location.href).pathname,
2382
+ () => new URL(window.location.href).pathname,
1344
2383
  );
1345
2384
  registerCustomXPathFunction(
1346
2385
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'uri-port' },
1347
2386
  [],
1348
2387
  'xs:string?',
1349
- (_dynamicContext, _arg) => window.location.port,
2388
+ () => window.location.port,
1350
2389
  );
1351
2390
  registerCustomXPathFunction(
1352
2391
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'uri-param' },
@@ -1354,38 +2393,22 @@ registerCustomXPathFunction(
1354
2393
  'xs:string?',
1355
2394
  (_dynamicContext, arg) => {
1356
2395
  if (!arg) return null;
1357
-
1358
- const { search } = window.location;
1359
- const urlparams = new URLSearchParams(search);
1360
- const param = urlparams.get(arg);
1361
- return param || '';
2396
+ const urlparams = new URLSearchParams(window.location.search);
2397
+ return urlparams.get(arg) || '';
1362
2398
  },
1363
2399
  );
1364
2400
  registerCustomXPathFunction(
1365
2401
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'uri-scheme' },
1366
2402
  [],
1367
2403
  'xs:string?',
1368
- (_dynamicContext, _arg) => new URL(window.location.href).protocol,
2404
+ () => new URL(window.location.href).protocol,
1369
2405
  );
1370
2406
  registerCustomXPathFunction(
1371
2407
  { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'uri-scheme-specific-part' },
1372
2408
  [],
1373
2409
  'xs:string?',
1374
- (_dynamicContext, _arg) => {
2410
+ () => {
1375
2411
  const uri = window.location.href;
1376
- return uri.substring(uri.indexOf(':') + 1, uri.length);
2412
+ return uri.substring(uri.indexOf(':') + 1);
1377
2413
  },
1378
2414
  );
1379
-
1380
- /**
1381
- * @param {Node} node
1382
- * @returns string
1383
- */
1384
- /*
1385
- export static getDocPath(node) {
1386
- const path = fx.evaluateXPathToString('path()', node);
1387
- // Path is like `$default/x[1]/y[1]`
1388
- const shortened = XPathUtil.shortenPath(path);
1389
- return shortened.startsWith('/') ? `${shortened}` : `/${shortened}`;
1390
- }
1391
- */