@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
|
@@ -82,17 +82,20 @@ export default class FxSetvalue extends AbstractAction {
|
|
|
82
82
|
*/
|
|
83
83
|
dispatchExecute() {}
|
|
84
84
|
|
|
85
|
+
// Adjustment in setValue logic to ensure we work with JSONNode, not just raw values
|
|
85
86
|
setValue(modelItem, newVal) {
|
|
86
87
|
console.log('setValue', modelItem, newVal);
|
|
87
88
|
const item = modelItem;
|
|
88
89
|
if (!item) return;
|
|
89
90
|
|
|
90
|
-
if
|
|
91
|
-
|
|
92
|
-
const path = Fore.getDomNodeIndexString(modelItem.node);
|
|
91
|
+
// Check if current node is a JSONNode
|
|
92
|
+
const node = Array.isArray(item.node) ? item.node[0] : item.node;
|
|
93
93
|
|
|
94
|
+
if (item.value !== newVal) {
|
|
95
|
+
const path = Fore.getDomNodeIndexString(node);
|
|
94
96
|
const ev = this.event;
|
|
95
97
|
const targetElem = this;
|
|
98
|
+
|
|
96
99
|
this.dispatchEvent(
|
|
97
100
|
new CustomEvent('execute-action', {
|
|
98
101
|
composed: true,
|
|
@@ -107,20 +110,19 @@ export default class FxSetvalue extends AbstractAction {
|
|
|
107
110
|
}),
|
|
108
111
|
);
|
|
109
112
|
|
|
113
|
+
// Use ModelItem's value setter which handles both DOM nodes and JSON lenses
|
|
110
114
|
if (newVal?.nodeType) {
|
|
111
115
|
if (newVal.nodeType === Node.ELEMENT_NODE) {
|
|
112
116
|
item.value = newVal;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
if (newVal.nodeType === Node.TEXT_NODE) {
|
|
117
|
+
} else if (newVal.nodeType === Node.ATTRIBUTE_NODE) {
|
|
118
|
+
item.value = newVal.nodeValue;
|
|
119
|
+
} else if (newVal.nodeType === Node.TEXT_NODE) {
|
|
118
120
|
item.value = newVal.textContent;
|
|
119
121
|
}
|
|
120
122
|
} else {
|
|
121
123
|
item.value = newVal;
|
|
122
|
-
item.node.textContent = newVal;
|
|
123
124
|
}
|
|
125
|
+
|
|
124
126
|
this.getModel().changed.push(modelItem);
|
|
125
127
|
this.needsUpdate = true;
|
|
126
128
|
modelItem.notify();
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authoring integrity checks for Fore forms.
|
|
3
|
+
*
|
|
4
|
+
* Runs by default at startup. Add the `no-check` attribute to `<fx-fore>` to disable
|
|
5
|
+
* (e.g. in production). The module is dynamically imported, so it is never loaded
|
|
6
|
+
* when checks are disabled.
|
|
7
|
+
*
|
|
8
|
+
* Adding a new check: add a function `_check<Name>(fore, errors)` and call it in
|
|
9
|
+
* `checkAuthoring()` below.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const INSTANCE_RE = /instance\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
13
|
+
const INDEX_RE = /index\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
14
|
+
|
|
15
|
+
// Attributes that may carry XPath expressions
|
|
16
|
+
const XPATH_ATTRS = [
|
|
17
|
+
'ref',
|
|
18
|
+
'value',
|
|
19
|
+
'calculate',
|
|
20
|
+
'constraint',
|
|
21
|
+
'required',
|
|
22
|
+
'readonly',
|
|
23
|
+
'relevant',
|
|
24
|
+
'bind',
|
|
25
|
+
'context',
|
|
26
|
+
'if',
|
|
27
|
+
'while',
|
|
28
|
+
'origin',
|
|
29
|
+
'iterate',
|
|
30
|
+
'at',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function _isDynamic(val) {
|
|
34
|
+
return !val || val.includes('{');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function _byId(fore, id) {
|
|
38
|
+
return (
|
|
39
|
+
fore.ownerDocument.getElementById(id) ||
|
|
40
|
+
fore.getRootNode().getElementById?.(id) ||
|
|
41
|
+
fore.querySelector(`#${id}`)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function _checkSendSubmissions(fore, errors) {
|
|
46
|
+
fore.querySelectorAll('fx-send[submission]').forEach(el => {
|
|
47
|
+
const id = el.getAttribute('submission');
|
|
48
|
+
if (_isDynamic(id)) return;
|
|
49
|
+
const localFore = el.closest('fx-fore');
|
|
50
|
+
const { model } = localFore;
|
|
51
|
+
const target = model
|
|
52
|
+
? model.querySelector(`fx-submission#${id}`)
|
|
53
|
+
: fore.querySelector(`fx-submission#${id}`);
|
|
54
|
+
if (!target) {
|
|
55
|
+
errors.push({
|
|
56
|
+
element: el,
|
|
57
|
+
message: `<fx-send submission="${id}">: no <fx-submission id="${id}"> found`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function _checkDispatchTargets(fore, errors) {
|
|
64
|
+
fore.querySelectorAll('fx-dispatch[targetid]').forEach(el => {
|
|
65
|
+
const id = el.getAttribute('targetid');
|
|
66
|
+
if (_isDynamic(id)) return;
|
|
67
|
+
if (!_byId(fore, id)) {
|
|
68
|
+
errors.push({
|
|
69
|
+
element: el,
|
|
70
|
+
message: `<fx-dispatch targetid="${id}">: no element with id="${id}" found`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _checkXPathInstanceRefs(fore, errors) {
|
|
77
|
+
const allEls = Array.from(fore.querySelectorAll('*'));
|
|
78
|
+
for (const el of allEls) {
|
|
79
|
+
const localFore = el.closest('fx-fore');
|
|
80
|
+
for (const attr of XPATH_ATTRS) {
|
|
81
|
+
const val = el.getAttribute(attr);
|
|
82
|
+
if (!val) continue;
|
|
83
|
+
|
|
84
|
+
INSTANCE_RE.lastIndex = 0;
|
|
85
|
+
let m;
|
|
86
|
+
while ((m = INSTANCE_RE.exec(val)) !== null) {
|
|
87
|
+
const id = m[1];
|
|
88
|
+
const localInstance = localFore.querySelector(`fx-instance#${id}`);
|
|
89
|
+
const sharedInstance =
|
|
90
|
+
!localInstance && localFore.ownerDocument.querySelector(`fx-instance[shared]#${id}`);
|
|
91
|
+
if (!localInstance && !sharedInstance) {
|
|
92
|
+
errors.push({
|
|
93
|
+
element: el,
|
|
94
|
+
message: `[${attr}="${val}"]: instance('${id}') — no <fx-instance id="${id}"> found`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
INDEX_RE.lastIndex = 0;
|
|
100
|
+
while ((m = INDEX_RE.exec(val)) !== null) {
|
|
101
|
+
const id = m[1];
|
|
102
|
+
if (!localFore.querySelector(`fx-repeat#${id}`)) {
|
|
103
|
+
errors.push({
|
|
104
|
+
element: el,
|
|
105
|
+
message: `[${attr}="${val}"]: index('${id}') — no <fx-repeat id="${id}"> found`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _checkCallActions(fore, errors) {
|
|
114
|
+
fore.querySelectorAll('fx-call[action]').forEach(el => {
|
|
115
|
+
const id = el.getAttribute('action');
|
|
116
|
+
if (_isDynamic(id)) return;
|
|
117
|
+
if (!_byId(fore, id)) {
|
|
118
|
+
errors.push({
|
|
119
|
+
element: el,
|
|
120
|
+
message: `<fx-call action="${id}">: no element with id="${id}" found`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function _checkShowHideDialogs(fore, errors) {
|
|
127
|
+
fore.querySelectorAll('fx-show[dialog], fx-hide[dialog]').forEach(el => {
|
|
128
|
+
const id = el.getAttribute('dialog');
|
|
129
|
+
if (_isDynamic(id)) return;
|
|
130
|
+
if (!_byId(fore, id)) {
|
|
131
|
+
errors.push({
|
|
132
|
+
element: el,
|
|
133
|
+
message: `<${el.localName} dialog="${id}">: no element with id="${id}" found`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function _checkLoadAttachTo(fore, errors) {
|
|
140
|
+
fore.querySelectorAll('fx-load[attach-to]').forEach(el => {
|
|
141
|
+
const val = el.getAttribute('attach-to');
|
|
142
|
+
if (_isDynamic(val)) return;
|
|
143
|
+
if (!val.startsWith('#')) return; // _blank, _self etc. are valid non-id targets
|
|
144
|
+
const id = val.substring(1);
|
|
145
|
+
if (!_byId(fore, id)) {
|
|
146
|
+
errors.push({
|
|
147
|
+
element: el,
|
|
148
|
+
message: `<fx-load attach-to="${val}">: no element with id="${id}" found`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function _checkRefreshControl(fore, errors) {
|
|
155
|
+
fore.querySelectorAll('fx-refresh[control]').forEach(el => {
|
|
156
|
+
const id = el.getAttribute('control');
|
|
157
|
+
if (_isDynamic(id)) return;
|
|
158
|
+
if (!_byId(fore, id)) {
|
|
159
|
+
errors.push({
|
|
160
|
+
element: el,
|
|
161
|
+
message: `<fx-refresh control="${id}">: no element with id="${id}" found`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function _checkResetInstance(fore, errors) {
|
|
168
|
+
const model = fore.querySelector(':scope > fx-model');
|
|
169
|
+
fore.querySelectorAll('fx-reset[instance]').forEach(el => {
|
|
170
|
+
const id = el.getAttribute('instance');
|
|
171
|
+
if (_isDynamic(id)) return;
|
|
172
|
+
const target = model
|
|
173
|
+
? model.querySelector(`fx-instance#${id}`)
|
|
174
|
+
: fore.querySelector(`fx-instance#${id}`);
|
|
175
|
+
const sharedTarget = !target && fore.ownerDocument.querySelector(`fx-instance[shared]#${id}`);
|
|
176
|
+
if (!target && !sharedTarget) {
|
|
177
|
+
errors.push({
|
|
178
|
+
element: el,
|
|
179
|
+
message: `<fx-reset instance="${id}">: no <fx-instance id="${id}"> found`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function _checkSetfocusControl(fore, errors) {
|
|
186
|
+
fore.querySelectorAll('fx-setfocus[control]').forEach(el => {
|
|
187
|
+
const id = el.getAttribute('control');
|
|
188
|
+
if (_isDynamic(id)) return;
|
|
189
|
+
if (!_byId(fore, id)) {
|
|
190
|
+
errors.push({
|
|
191
|
+
element: el,
|
|
192
|
+
message: `<fx-setfocus control="${id}">: no element with id="${id}" found`,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function _checkToggleCase(fore, errors) {
|
|
199
|
+
fore.querySelectorAll('fx-toggle[case]').forEach(el => {
|
|
200
|
+
const id = el.getAttribute('case');
|
|
201
|
+
if (_isDynamic(id)) return;
|
|
202
|
+
if (!fore.querySelector(`fx-case#${id}`)) {
|
|
203
|
+
errors.push({
|
|
204
|
+
element: el,
|
|
205
|
+
message: `<fx-toggle case="${id}">: no <fx-case id="${id}"> found`,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Run all authoring checks on a given `<fx-fore>` element.
|
|
213
|
+
* Returns an array of `{ element, message }` error objects.
|
|
214
|
+
*
|
|
215
|
+
* @param {HTMLElement} fore
|
|
216
|
+
* @returns {{ element: HTMLElement, message: string }[]}
|
|
217
|
+
*/
|
|
218
|
+
export function checkAuthoring(fore) {
|
|
219
|
+
const errors = [];
|
|
220
|
+
_checkSendSubmissions(fore, errors);
|
|
221
|
+
_checkDispatchTargets(fore, errors);
|
|
222
|
+
_checkXPathInstanceRefs(fore, errors);
|
|
223
|
+
_checkCallActions(fore, errors);
|
|
224
|
+
_checkShowHideDialogs(fore, errors);
|
|
225
|
+
_checkLoadAttachTo(fore, errors);
|
|
226
|
+
_checkRefreshControl(fore, errors);
|
|
227
|
+
_checkResetInstance(fore, errors);
|
|
228
|
+
_checkSetfocusControl(fore, errors);
|
|
229
|
+
_checkToggleCase(fore, errors);
|
|
230
|
+
return errors;
|
|
231
|
+
}
|
package/src/fore.js
CHANGED
|
@@ -17,7 +17,7 @@ export class Fore {
|
|
|
17
17
|
|
|
18
18
|
static REQUIRED_DEFAULT = false;
|
|
19
19
|
|
|
20
|
-
static RELEVANT_DEFAULT = true
|
|
20
|
+
static RELEVANT_DEFAULT = true;
|
|
21
21
|
|
|
22
22
|
static CONSTRAINT_DEFAULT = true;
|
|
23
23
|
|
|
@@ -267,7 +267,6 @@ export class Fore {
|
|
|
267
267
|
return ['FX-BIND', 'FX-FUNCTION', 'FX-MODEL', 'FX-INSTANCE', 'FX-SUBMISSION'];
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
|
|
271
270
|
static async initUI(startElement) {
|
|
272
271
|
const inited = new Promise(resolve => {
|
|
273
272
|
const { children } = startElement;
|
|
@@ -293,82 +292,41 @@ export class Fore {
|
|
|
293
292
|
* @returns {Promise<void>}
|
|
294
293
|
*/
|
|
295
294
|
static async refreshChildren(startElement, force) {
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const { children } = startElement;
|
|
314
|
-
if (children) {
|
|
315
|
-
for (const element of Array.from(children)) {
|
|
316
|
-
if (element.nodeName.toUpperCase() === 'FX-FORE') {
|
|
317
|
-
break;
|
|
318
|
-
}
|
|
319
|
-
if (Fore.isUiElement(element.nodeName) && typeof element.refresh === 'function') {
|
|
320
|
-
/**
|
|
321
|
-
* @type {import('./ForeElementMixin.js').default}
|
|
322
|
-
*/
|
|
323
|
-
const bound = element;
|
|
324
|
-
if (!force) {
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
/*
|
|
328
|
-
if(element.nodeName === 'FX-CASE') {
|
|
329
|
-
console.log('hey - got a case', element);
|
|
330
|
-
}
|
|
331
|
-
*/
|
|
332
|
-
if (force === true) {
|
|
333
|
-
// console.log('🔄 refreshing ', element);
|
|
334
|
-
// Unconditional force refresh
|
|
335
|
-
bound.refresh(force);
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
if (typeof force !== 'object') {
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
/*
|
|
342
|
-
if (
|
|
343
|
-
force.reason === 'index-function' &&
|
|
344
|
-
bound.dependencies.isInvalidatedByIndexFunction()
|
|
345
|
-
) {
|
|
346
|
-
console.log('🔄 refreshing ', element);
|
|
347
|
-
|
|
348
|
-
bound.refresh(force);
|
|
349
|
-
continue;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
bound.dependencies.isInvalidatedByChildlistChanges(force.elementLocalnamesWithChanges)
|
|
354
|
-
) {
|
|
355
|
-
console.log('🔄 refreshing ', element);
|
|
356
|
-
|
|
357
|
-
bound.refresh(force);
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
*/
|
|
295
|
+
const children = startElement?.children ? Array.from(startElement.children) : [];
|
|
296
|
+
for (const element of children) {
|
|
297
|
+
// Do not cross into nested fore roots
|
|
298
|
+
if (element.nodeName.toUpperCase() === 'FX-FORE') {
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (Fore.isUiElement(element.nodeName) && typeof element.refresh === 'function') {
|
|
303
|
+
/** @type {import('./ui/UIElement.js').UIElement} */
|
|
304
|
+
const bound = element;
|
|
305
|
+
|
|
306
|
+
// Keep old behavior: only refresh UI elements during full/forced refresh
|
|
307
|
+
// Any #refresh call does its own recursion.
|
|
308
|
+
if (force === true) {
|
|
309
|
+
const maybePromise = bound.refresh(force);
|
|
310
|
+
if (maybePromise && typeof maybePromise.then === 'function') {
|
|
311
|
+
await maybePromise;
|
|
361
312
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (typeof force === 'object') {
|
|
316
|
+
// future selective refresh logic can live here if you re-enable it
|
|
317
|
+
const maybePromise = bound.refresh(force);
|
|
318
|
+
if (maybePromise && typeof maybePromise.then === 'function') {
|
|
319
|
+
await maybePromise;
|
|
365
320
|
}
|
|
321
|
+
continue;
|
|
366
322
|
}
|
|
367
323
|
}
|
|
368
|
-
resolve('done');
|
|
369
|
-
});
|
|
370
324
|
|
|
371
|
-
|
|
325
|
+
// Traverse DOM unless inert
|
|
326
|
+
if (!(element.inert === true)) {
|
|
327
|
+
await Fore.refreshChildren(element, force);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
372
330
|
}
|
|
373
331
|
|
|
374
332
|
static copyDom(inputElement) {
|
|
@@ -564,10 +522,7 @@ export class Fore {
|
|
|
564
522
|
const reg = /(>)(<)(\/*)/g;
|
|
565
523
|
const wsexp = / *(.*) +\n/g;
|
|
566
524
|
const contexp = /(<.+>)(.+\n)/g;
|
|
567
|
-
xml = xml
|
|
568
|
-
.replace(reg, '$1\n$2$3')
|
|
569
|
-
.replace(wsexp, '$1\n')
|
|
570
|
-
.replace(contexp, '$1\n$2');
|
|
525
|
+
xml = xml.replace(reg, '$1\n$2$3').replace(wsexp, '$1\n').replace(contexp, '$1\n$2');
|
|
571
526
|
let formatted = '';
|
|
572
527
|
const lines = xml.split('\n');
|
|
573
528
|
let indent = 0;
|
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import { createTypedValueFactory, registerCustomXPathFunction } from 'fontoxpath';
|
|
2
2
|
import { evaluateXPath, globallyDeclaredFunctionLocalNames } from '../xpath-evaluation.js';
|
|
3
3
|
|
|
4
|
+
// FontoXPath custom functions are registered globally. If multiple <fx-functionlib> (or multiple
|
|
5
|
+
// <fx-fore> instances) register the same function name+arity, later registrations would overwrite
|
|
6
|
+
// earlier ones across the whole page. We enforce "first wins".
|
|
7
|
+
const _registeredFunctionKeys = new Set();
|
|
8
|
+
|
|
9
|
+
function _makeFunctionKey(functionIdentifier, arity) {
|
|
10
|
+
if (typeof functionIdentifier === 'string') {
|
|
11
|
+
return `str:${functionIdentifier}#${arity}`;
|
|
12
|
+
}
|
|
13
|
+
return `{${functionIdentifier.namespaceURI}}${functionIdentifier.localName}#${arity}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function _ensureGlobalUnprefixedName(localName) {
|
|
17
|
+
// Keep this list unique to avoid unbounded growth across tests
|
|
18
|
+
if (!globallyDeclaredFunctionLocalNames.includes(localName)) {
|
|
19
|
+
globallyDeclaredFunctionLocalNames.push(localName);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
4
23
|
/**
|
|
5
|
-
* @param functionObject {{signature: string, type: string|null, functionBody: string}}
|
|
24
|
+
* @param functionObject {{signature: string, type: string|null, functionBody: string, implementation?: Function}}
|
|
6
25
|
* @param formElement {HTMLElement} The form element connected to this function. Used to determine inscope context
|
|
7
26
|
* @returns {undefined}
|
|
8
27
|
*/
|
|
@@ -31,29 +50,51 @@ export default function registerFunction(functionObject, formElement) {
|
|
|
31
50
|
? { namespaceURI: 'http://www.w3.org/2005/xquery-local-functions', localName }
|
|
32
51
|
: `${prefix}:${localName}`;
|
|
33
52
|
|
|
34
|
-
// Make the function available globally w/o a prefix. See the functionNameResolver for for how
|
|
35
|
-
// functionObject is picked up
|
|
36
|
-
if (!prefix) {
|
|
37
|
-
globallyDeclaredFunctionLocalNames.push(localName);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
53
|
const paramParts = params
|
|
41
|
-
? params
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
? params
|
|
55
|
+
.split(',')
|
|
56
|
+
.map(param => param.trim())
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.map(param => {
|
|
59
|
+
const match = param.match(/(?<variableName>\$[^\s]+)(?:\sas\s(?<varType>[^\s]+))/);
|
|
60
|
+
if (!match) {
|
|
61
|
+
throw new Error(`Param ${param} could not be parsed`);
|
|
62
|
+
}
|
|
63
|
+
const { variableName, varType } = match.groups;
|
|
64
|
+
return {
|
|
65
|
+
variableName,
|
|
66
|
+
variableType: varType || 'item()*',
|
|
67
|
+
};
|
|
68
|
+
})
|
|
52
69
|
: [];
|
|
53
70
|
|
|
71
|
+
const arity = paramParts.length;
|
|
72
|
+
|
|
73
|
+
// -------------------------------------------------
|
|
74
|
+
// FIRST-WINS GUARD (name + arity) WITH NAME EXPORT
|
|
75
|
+
// -------------------------------------------------
|
|
76
|
+
const key = _makeFunctionKey(functionIdentifier, arity);
|
|
77
|
+
|
|
78
|
+
if (_registeredFunctionKeys.has(key)) {
|
|
79
|
+
// If this registration is unprefixed, we must still make it callable without prefix.
|
|
80
|
+
// This fixes the unit test case where local:hello-world() was registered earlier and
|
|
81
|
+
// hello-world() should resolve to that implementation.
|
|
82
|
+
if (!prefix) {
|
|
83
|
+
_ensureGlobalUnprefixedName(localName);
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_registeredFunctionKeys.add(key);
|
|
89
|
+
|
|
90
|
+
// Make the function available globally w/o a prefix.
|
|
91
|
+
if (!prefix) {
|
|
92
|
+
_ensureGlobalUnprefixedName(localName);
|
|
93
|
+
}
|
|
94
|
+
|
|
54
95
|
switch (type) {
|
|
55
96
|
case 'text/javascript': {
|
|
56
|
-
//
|
|
97
|
+
// If a real JS function is provided (module libs), register it directly.
|
|
57
98
|
if (typeof functionObject.implementation === 'function') {
|
|
58
99
|
const impl = functionObject.implementation;
|
|
59
100
|
registerCustomXPathFunction(
|
|
@@ -74,6 +115,7 @@ export default function registerFunction(functionObject, formElement) {
|
|
|
74
115
|
'form',
|
|
75
116
|
functionObject.functionBody,
|
|
76
117
|
);
|
|
118
|
+
|
|
77
119
|
registerCustomXPathFunction(
|
|
78
120
|
functionIdentifier,
|
|
79
121
|
paramParts.map(paramPart => paramPart.variableType),
|
|
@@ -83,25 +125,27 @@ export default function registerFunction(functionObject, formElement) {
|
|
|
83
125
|
);
|
|
84
126
|
break;
|
|
85
127
|
}
|
|
128
|
+
|
|
86
129
|
case 'text/xquf':
|
|
87
130
|
case 'text/xquery':
|
|
88
131
|
case 'text/xpath': {
|
|
89
132
|
const typedValueFactories = paramParts.map(param =>
|
|
90
133
|
createTypedValueFactory(param.variableType),
|
|
91
134
|
);
|
|
135
|
+
|
|
92
136
|
const language =
|
|
93
137
|
type === 'text/xpath'
|
|
94
138
|
? 'XPath3.1'
|
|
95
139
|
: type === 'text/xquery'
|
|
96
140
|
? 'XQuery3.1'
|
|
97
141
|
: 'XQueryUpdate3.1';
|
|
142
|
+
|
|
98
143
|
const fun = (domFacade, ...args) =>
|
|
99
144
|
evaluateXPath(
|
|
100
145
|
functionObject.functionBody,
|
|
101
146
|
formElement.getInScopeContext(),
|
|
102
147
|
formElement.getOwnerForm(),
|
|
103
148
|
paramParts.reduce((variablesByName, paramPart, i) => {
|
|
104
|
-
// Because we know the XPath type here (from the function declaration) we do not have to depend on the implicit typings
|
|
105
149
|
variablesByName[paramPart.variableName.replace('$', '')] = typedValueFactories[i](
|
|
106
150
|
args[i],
|
|
107
151
|
domFacade,
|
|
@@ -110,6 +154,7 @@ export default function registerFunction(functionObject, formElement) {
|
|
|
110
154
|
}, {}),
|
|
111
155
|
{ language },
|
|
112
156
|
);
|
|
157
|
+
|
|
113
158
|
registerCustomXPathFunction(
|
|
114
159
|
functionIdentifier,
|
|
115
160
|
paramParts.map(paramPart => paramPart.variableType),
|