@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.
- package/README.md +0 -2
- package/dist/fore-dev.js +9505 -6057
- package/dist/fore.js +9218 -5996
- package/index.js +1 -0
- package/package.json +4 -2
- package/src/DependentXPathQueries.js +37 -2
- package/src/ForeElementMixin.js +64 -38
- package/src/actions/fx-delete.js +424 -73
- package/src/actions/fx-insert.js +472 -73
- package/src/actions/fx-setattribute.js +5 -5
- package/src/actions/fx-setvalue.js +11 -9
- package/src/authoring-check.js +231 -0
- package/src/fore.js +32 -77
- package/src/functions/registerFunction.js +65 -20
- package/src/fx-bind.js +128 -73
- package/src/fx-fore.js +653 -219
- package/src/fx-instance.js +145 -143
- package/src/fx-model.js +292 -102
- package/src/fx-speech.js +268 -0
- package/src/fx-submission.js +246 -135
- package/src/fx-var.js +28 -4
- package/src/json/JSONDomFacade.js +84 -0
- package/src/json/JSONLens.js +67 -0
- package/src/json/JSONNode.js +248 -0
- package/src/json/lensFromNode.js +5 -0
- package/src/modelitem.js +16 -2
- package/src/tools/fx-action-log.js +1 -1
- package/src/ui/UIElement.js +16 -2
- package/src/ui/abstract-control.js +14 -7
- package/src/ui/fx-case.js +3 -1
- package/src/ui/fx-control.js +4 -2
- package/src/ui/fx-group.js +1 -1
- package/src/ui/fx-items.js +26 -32
- package/src/ui/fx-output.js +8 -38
- package/src/ui/fx-repeat-attributes.js +1 -1
- package/src/ui/fx-repeat.js +683 -247
- package/src/ui/fx-repeatitem.js +16 -1
- package/src/ui/repeat-base.js +9 -5
- package/src/withDraggability.js +0 -1
- package/src/xpath-evaluation.js +1763 -740
- package/src/xpath-path.js +274 -24
- package/src/xpath-util.js +92 -46
package/src/xpath-evaluation.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
33
|
-
if (!
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
216
|
+
|
|
188
217
|
return null;
|
|
189
218
|
}
|
|
190
219
|
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
231
|
+
const source = formElementOrNode?.nodeType
|
|
232
|
+
? formElementOrNode
|
|
233
|
+
: _getOwningFore(formElementOrNode);
|
|
195
234
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
420
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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 (
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
461
|
-
|
|
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
|
-
|
|
464
|
-
{...variablesInScope, ...variables},
|
|
1320
|
+
__jsonDomFacade,
|
|
1321
|
+
{ ...variablesInScope, ...variables },
|
|
465
1322
|
fxEvaluateXPath.ALL_RESULTS_TYPE,
|
|
466
1323
|
{
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
485
|
-
|
|
1418
|
+
return fxEvaluateXPath(
|
|
1419
|
+
expr0,
|
|
486
1420
|
contextNode,
|
|
487
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
549
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
710
|
-
return
|
|
711
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
785
|
-
if (string)
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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 ===
|
|
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
|
|
2016
|
+
return instanceEl.nodeset;
|
|
801
2017
|
}
|
|
802
2018
|
|
|
803
2019
|
return caller.getInScopeContext();
|
|
804
2020
|
};
|
|
805
2021
|
|
|
806
|
-
// todo
|
|
807
|
-
const currentFunction = (
|
|
808
|
-
const caller = dynamicContext.currentContext.formElement;
|
|
809
|
-
return null;
|
|
810
|
-
};
|
|
2022
|
+
// current() - todo
|
|
2023
|
+
const currentFunction = (_dynamicContext, _string) => null;
|
|
811
2024
|
|
|
812
|
-
const elementFunction = (
|
|
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
|
|
865
|
-
if (
|
|
866
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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
|
-
|
|
1010
|
-
|
|
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
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1058
|
-
|
|
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
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1234
|
-
|
|
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
|
|
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
|
-
|
|
2320
|
+
module namespace xf="${XFORMS_NAMESPACE_URI}";
|
|
1254
2321
|
|
|
1255
|
-
|
|
1256
|
-
|
|
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
|
-
(
|
|
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
|
-
(
|
|
2343
|
+
() => new Date().toLocaleString(),
|
|
1305
2344
|
);
|
|
1306
2345
|
registerCustomXPathFunction(
|
|
1307
2346
|
{ namespaceURI: XFORMS_NAMESPACE_URI, localName: 'uri' },
|
|
1308
2347
|
[],
|
|
1309
2348
|
'xs:string?',
|
|
1310
|
-
(
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
-
(
|
|
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
|
-
(
|
|
2410
|
+
() => {
|
|
1375
2411
|
const uri = window.location.href;
|
|
1376
|
-
return uri.substring(uri.indexOf(':') + 1
|
|
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
|
-
*/
|