@jinntec/fore 0.25.0 → 1.0.0-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +75 -22
  2. package/dist/fore-all.js +11 -11
  3. package/dist/fore.js +1 -1
  4. package/index.js +5 -1
  5. package/package.json +17 -6
  6. package/resources/fore.css +121 -4
  7. package/resources/toastify.css +3 -1
  8. package/src/DependencyNotifyingDomFacade.js +9 -1
  9. package/src/ForeElementMixin.js +83 -12
  10. package/src/actions/abstract-action.js +101 -27
  11. package/src/actions/fx-action.js +4 -2
  12. package/src/actions/fx-append.js +21 -18
  13. package/src/actions/fx-confirm.js +22 -0
  14. package/src/actions/fx-dispatch.js +10 -2
  15. package/src/actions/fx-insert.js +35 -30
  16. package/src/actions/fx-message.js +7 -1
  17. package/src/actions/fx-send.js +1 -1
  18. package/src/actions/fx-setvalue.js +2 -9
  19. package/src/dep_graph.js +9 -0
  20. package/src/drawdown.js +172 -0
  21. package/src/fore.js +126 -18
  22. package/src/functions/fx-function.js +2 -2
  23. package/src/fx-bind.js +11 -7
  24. package/src/fx-fore.js +283 -67
  25. package/src/fx-header.js +20 -0
  26. package/src/fx-instance.js +54 -10
  27. package/src/fx-model.js +175 -38
  28. package/src/fx-submission.js +235 -53
  29. package/src/getInScopeContext.js +2 -3
  30. package/src/ui/abstract-control.js +23 -44
  31. package/src/ui/fx-alert.js +20 -19
  32. package/src/ui/fx-container.js +9 -4
  33. package/src/ui/fx-control.js +92 -37
  34. package/src/ui/fx-inspector.js +44 -0
  35. package/src/ui/fx-items.js +104 -20
  36. package/src/ui/fx-output.js +92 -3
  37. package/src/ui/fx-repeat.js +87 -80
  38. package/src/ui/fx-repeatitem.js +38 -48
  39. package/src/ui/fx-trigger.js +49 -27
  40. package/src/xpath-evaluation.js +533 -235
  41. package/src/xpath-util.js +50 -12
@@ -1,17 +1,512 @@
1
1
  import {
2
2
  evaluateXPath as fxEvaluateXPath,
3
+ evaluateXPathToBoolean as fxEvaluateXPathToBoolean,
3
4
  evaluateXPathToFirstNode as fxEvaluateXPathToFirstNode,
4
5
  evaluateXPathToNodes as fxEvaluateXPathToNodes,
5
- evaluateXPathToBoolean as fxEvaluateXPathToBoolean,
6
- evaluateXPathToString as fxEvaluateXPathToString,
7
6
  evaluateXPathToNumber as fxEvaluateXPathToNumber,
7
+ evaluateXPathToString as fxEvaluateXPathToString,
8
+ evaluateXPathToStrings as fxEvaluateXPathToStrings,
9
+ parseScript,
8
10
  registerCustomXPathFunction,
9
11
  registerXQueryModule,
10
12
  } from 'fontoxpath';
11
- import getInScopeContext from './getInScopeContext.js';
13
+ import { XPathUtil } from './xpath-util.js';
12
14
 
13
15
  const XFORMS_NAMESPACE_URI = 'http://www.w3.org/2002/xforms';
14
16
 
17
+ const createdNamespaceResolversByXPathQueryAndNode = new Map();
18
+
19
+ function prettifyXml(source) {
20
+ const xmlDoc = new DOMParser().parseFromString(source, 'application/xml');
21
+ const xsltDoc = new DOMParser().parseFromString(
22
+ [
23
+ // describes how we want to modify the XML - indent everything
24
+ '<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform">',
25
+ ' <xsl:strip-space elements="*"/>',
26
+ ' <xsl:template match="para[content-style][not(text())]">', // change to just text() to strip space in text nodes
27
+ ' <xsl:value-of select="normalize-space(.)"/>',
28
+ ' </xsl:template>',
29
+ ' <xsl:template match="node()|@*">',
30
+ ' <xsl:copy><xsl:apply-templates select="node()|@*"/></xsl:copy>',
31
+ ' </xsl:template>',
32
+ ' <xsl:output indent="yes"/>',
33
+ '</xsl:stylesheet>',
34
+ ].join('\n'),
35
+ 'application/xml',
36
+ );
37
+
38
+ const xsltProcessor = new XSLTProcessor();
39
+ xsltProcessor.importStylesheet(xsltDoc);
40
+ const resultDoc = xsltProcessor.transformToDocument(xmlDoc);
41
+ const resultXml = new XMLSerializer().serializeToString(resultDoc);
42
+ return resultXml;
43
+ }
44
+
45
+ function getCachedNamespaceResolver(xpath, node) {
46
+ if (!createdNamespaceResolversByXPathQueryAndNode.has(xpath)) {
47
+ return null;
48
+ }
49
+ return createdNamespaceResolversByXPathQueryAndNode.get(xpath).get(node) || null;
50
+ }
51
+ function setCachedNamespaceResolver(xpath, node, resolver) {
52
+ if (!createdNamespaceResolversByXPathQueryAndNode.has(xpath)) {
53
+ return createdNamespaceResolversByXPathQueryAndNode.set(xpath, new Map());
54
+ }
55
+ return createdNamespaceResolversByXPathQueryAndNode.get(xpath).set(node, resolver);
56
+ }
57
+
58
+ const xhtmlNamespaceResolver = prefix => {
59
+ if (!prefix) {
60
+ return 'http://www.w3.org/1999/xhtml';
61
+ }
62
+ return undefined;
63
+ };
64
+
65
+ /**
66
+ * Resolve a namespace. Needs a namespace prefix and the element that is most closely related to the
67
+ * XPath in which the namespace is being resolved. The prefix will be resolved by using the
68
+ * ancestry of said element.
69
+ *
70
+ * It has two ways of doing so:
71
+ *
72
+ * - If the prefix is defined in an `xmlns:XXX="YYY"` namespace declaration, it will return 'YYY'.
73
+ * - If the prefix is the empty prefix and there is an `xpath-default-namespace="YYY"` attribute in the
74
+ * - ancestry, that attribute will be used and 'YYY' will be returned
75
+ *
76
+ * @param {Node} contextElement The element that is most closely related with the XPath in which this prefix is resolved.
77
+ * @param {string} prefix The prefix to resolve
78
+ */
79
+ function createNamespaceResolver(xpathQuery, formElement) {
80
+ const cachedResolver = getCachedNamespaceResolver(xpathQuery, formElement);
81
+ if (cachedResolver) {
82
+ return cachedResolver;
83
+ }
84
+
85
+ // Make namespace resolving use the `instance` element that is related to here
86
+ const xmlDocument = new DOMParser().parseFromString('<xml />', 'text/xml');
87
+
88
+ const xpathAST = parseScript(xpathQuery, {}, xmlDocument);
89
+ let instanceReferences = fxEvaluateXPathToStrings(
90
+ `descendant::xqx:functionCallExpr
91
+ [xqx:functionName = "instance"]
92
+ /xqx:arguments
93
+ /xqx:stringConstantExpr
94
+ /xqx:value`,
95
+ xpathAST,
96
+ null,
97
+ {},
98
+ {
99
+ namespaceResolver: prefix =>
100
+ prefix === 'xqx' ? 'http://www.w3.org/2005/XQueryX' : undefined,
101
+ },
102
+ );
103
+ if (instanceReferences.length === 0) {
104
+ // No instance functions. Look up further in the hierarchy to see if we can deduce the intended context from there
105
+ const ancestorComponent = fxEvaluateXPathToFirstNode('ancestor::*[@ref][1]', formElement);
106
+ if (ancestorComponent) {
107
+ const resolver = createNamespaceResolver(
108
+ ancestorComponent.getAttribute('ref'),
109
+ ancestorComponent,
110
+ );
111
+ setCachedNamespaceResolver(xpathQuery, formElement, resolver);
112
+ return resolver;
113
+ }
114
+ // Nothing found: let's just assume we're supposed to use the `default` instance
115
+ instanceReferences = ['default'];
116
+ }
117
+
118
+ if (instanceReferences.length === 1) {
119
+ // console.log(`resolving ${xpathQuery} with ${instanceReferences[0]}`);
120
+ let instance;
121
+ if (instanceReferences[0] === 'default') {
122
+ const actualForeElement = fxEvaluateXPathToFirstNode(
123
+ 'ancestor-or-self::fx-fore',
124
+ formElement,
125
+ null,
126
+ null,
127
+ { namespaceResolver: xhtmlNamespaceResolver },
128
+ );
129
+
130
+ instance = actualForeElement && actualForeElement.querySelector('fx-instance');
131
+ } else {
132
+ instance = resolveId(instanceReferences[0], formElement, 'fx-instance');
133
+ }
134
+ if (instance && instance.hasAttribute('xpath-default-namespace')) {
135
+ const xpathDefaultNamespace = instance.getAttribute('xpath-default-namespace');
136
+ /*
137
+ console.log(
138
+ `Resolving the xpath ${xpathQuery} with the default namespace set to ${xpathDefaultNamespace}`,
139
+ );
140
+ */
141
+ const resolveNamespacePrefix = prefix => {
142
+ if (!prefix) {
143
+ return xpathDefaultNamespace;
144
+ }
145
+ return undefined;
146
+ };
147
+ setCachedNamespaceResolver(xpathQuery, formElement, resolveNamespacePrefix);
148
+ return resolveNamespacePrefix;
149
+ }
150
+ }
151
+ if (instanceReferences.length > 1) {
152
+ console.warn(
153
+ `More than one instance is used in the query "${xpathQuery}". The default namespace resolving will be used`,
154
+ );
155
+ }
156
+
157
+ const xpathDefaultNamespace =
158
+ fxEvaluateXPathToString('ancestor-or-self::*/@xpath-default-namespace[last()]', formElement) ||
159
+ '';
160
+
161
+ const resolveNamespacePrefix = function resolveNamespacePrefix(prefix) {
162
+ if (prefix === '') {
163
+ return xpathDefaultNamespace;
164
+ }
165
+
166
+ // Note: ideally we should use Node#lookupNamespaceURI. However, the nodes we are passed are
167
+ // XML. The best we can do is emulate the `xmlns:xxx` namespace declarations by regarding them as
168
+ // attributes. Which they technically ARE NOT!
169
+
170
+ return fxEvaluateXPathToString(
171
+ 'ancestor-or-self::*/@*[name() = "xmlns:" || $prefix][last()]',
172
+ formElement,
173
+ null,
174
+ { prefix },
175
+ );
176
+ };
177
+
178
+ setCachedNamespaceResolver(xpathQuery, formElement, resolveNamespacePrefix);
179
+ return resolveNamespacePrefix;
180
+ }
181
+
182
+ function createNamespaceResolverForNode(query, contextNode, formElement) {
183
+ if (((contextNode && contextNode.ownerDocument) || contextNode) === window.document) {
184
+ // Running a query on the HTML DOM. Don't bother resolving namespaces in any other way
185
+ return xhtmlNamespaceResolver;
186
+ }
187
+ return createNamespaceResolver(query, formElement);
188
+ }
189
+
190
+ /**
191
+ * Implementation of the functionNameResolver passed to FontoXPath to
192
+ * redirect function resolving for unprefixed functions to either the fn or the xf namespace
193
+ */
194
+ // eslint-disable-next-line no-unused-vars
195
+ function functionNameResolver({ prefix, localName }, _arity) {
196
+ switch (localName) {
197
+ // TODO: put the full XForms library functions set here
198
+ case 'context':
199
+ case 'base64encode':
200
+ case 'boolean-from-string':
201
+ case 'current':
202
+ case 'depends':
203
+ case 'event':
204
+ case 'index':
205
+ case 'instance':
206
+ case 'log':
207
+ case 'logtree':
208
+ return { namespaceURI: XFORMS_NAMESPACE_URI, localName };
209
+ default:
210
+ if (prefix === '' || prefix === 'fn') {
211
+ return { namespaceURI: 'http://www.w3.org/2005/xpath-functions', localName };
212
+ }
213
+ if (prefix === 'local') {
214
+ return { namespaceURI: 'http://www.w3.org/2005/xquery-local-functions', localName };
215
+ }
216
+ return null;
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Evaluate an XPath to _any_ type. When possible, prefer to use any other function to ensure the
222
+ * type of the output is more predictable.
223
+ *
224
+ * @param {string} xpath The XPath to run
225
+ * @param {Node} contextNode The start of the XPath
226
+ * @param {{parentNode}|ForeElementMixin} formElement The form element associated to the XPath
227
+ */
228
+ export function evaluateXPath(xpath, contextNode, formElement, variables = {}) {
229
+ const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
230
+
231
+ return fxEvaluateXPath(xpath, contextNode, null, variables, 'xs:anyType', {
232
+ currentContext: { formElement, variables },
233
+ moduleImports: {
234
+ xf: XFORMS_NAMESPACE_URI,
235
+ },
236
+ functionNameResolver,
237
+ namespaceResolver,
238
+ });
239
+ }
240
+
241
+ /**
242
+ * Evaluate an XPath to the first Node
243
+ *
244
+ * @param {string} xpath The XPath to run
245
+ * @param {Node} contextNode The start of the XPath
246
+ * @param {Node} formElement The form element associated to the XPath
247
+ * @return {Node} The first node found by the XPath
248
+ */
249
+ export function evaluateXPathToFirstNode(xpath, contextNode, formElement) {
250
+ const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
251
+ return fxEvaluateXPathToFirstNode(
252
+ xpath,
253
+ contextNode,
254
+ null,
255
+ {},
256
+ {
257
+ defaultFunctionNamespaceURI: XFORMS_NAMESPACE_URI,
258
+ moduleImports: {
259
+ xf: XFORMS_NAMESPACE_URI,
260
+ },
261
+ currentContext: { formElement },
262
+ namespaceResolver,
263
+ },
264
+ );
265
+ }
266
+
267
+ /**
268
+ * Evaluate an XPath to all nodes
269
+ *
270
+ * @param {string} xpath The XPath to run
271
+ * @param {Node} contextNode The start of the XPath
272
+ * @param {Node} formElement The form element associated to the XPath
273
+ * @return {Node[]} All nodes
274
+ */
275
+ export function evaluateXPathToNodes(xpath, contextNode, formElement) {
276
+ const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
277
+ return fxEvaluateXPathToNodes(
278
+ xpath,
279
+ contextNode,
280
+ null,
281
+ {},
282
+ {
283
+ currentContext: { formElement },
284
+ functionNameResolver,
285
+ moduleImports: {
286
+ xf: XFORMS_NAMESPACE_URI,
287
+ },
288
+ namespaceResolver,
289
+ },
290
+ );
291
+ }
292
+
293
+ /**
294
+ * Evaluate an XPath to a boolean
295
+ *
296
+ * @param {string} xpath The XPath to run
297
+ * @param {Node} contextNode The start of the XPath
298
+ * @param {Node} formElement The form element associated to the XPath
299
+ * @return {boolean}
300
+ */
301
+ export function evaluateXPathToBoolean(xpath, contextNode, formElement) {
302
+ const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
303
+ return fxEvaluateXPathToBoolean(
304
+ xpath,
305
+ contextNode,
306
+ null,
307
+ {},
308
+ {
309
+ currentContext: { formElement },
310
+ functionNameResolver,
311
+ moduleImports: {
312
+ xf: XFORMS_NAMESPACE_URI,
313
+ },
314
+ namespaceResolver,
315
+ },
316
+ );
317
+ }
318
+
319
+ /**
320
+ * Evaluate an XPath to a string
321
+ *
322
+ * @param {string} xpath The XPath to run
323
+ * @param {Node} contextNode The start of the XPath
324
+ * @param {Node} formElement The form element associated to the XPath
325
+ * @param {DomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
326
+ * access. This is used to determine dependencies between bind elements.
327
+ * @param {Node} formElement The element where the XPath is defined: used for namespace resolving
328
+ * @return {string}
329
+ */
330
+ export function evaluateXPathToString(
331
+ xpath,
332
+ contextNode,
333
+ formElement,
334
+ domFacade = null,
335
+ namespaceReferenceNode = formElement,
336
+ ) {
337
+ const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
338
+ return fxEvaluateXPathToString(
339
+ xpath,
340
+ contextNode,
341
+ domFacade,
342
+ {},
343
+
344
+ {
345
+ currentContext: { formElement },
346
+ functionNameResolver,
347
+ moduleImports: {
348
+ xf: XFORMS_NAMESPACE_URI,
349
+ },
350
+ namespaceResolver,
351
+ },
352
+ );
353
+ }
354
+
355
+ /**
356
+ * Evaluate an XPath to a number
357
+ *
358
+ * @param {string} xpath The XPath to run
359
+ * @param {Node} contextNode The start of the XPath
360
+ * @param {Node} formElement The form element associated to the XPath
361
+ * @param {DomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
362
+ * @param {Node} formElement The element where the XPath is defined: used for namespace resolving
363
+ * access. This is used to determine dependencies between bind elements.
364
+ * @return {Number}
365
+ */
366
+ export function evaluateXPathToNumber(
367
+ xpath,
368
+ contextNode,
369
+ formElement,
370
+ domFacade = null,
371
+ namespaceReferenceNode = formElement,
372
+ ) {
373
+ const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
374
+ return fxEvaluateXPathToNumber(
375
+ xpath,
376
+ contextNode,
377
+ domFacade,
378
+ {},
379
+ {
380
+ currentContext: { formElement },
381
+ functionNameResolver,
382
+ moduleImports: {
383
+ xf: XFORMS_NAMESPACE_URI,
384
+ },
385
+ namespaceResolver,
386
+ },
387
+ );
388
+ }
389
+
390
+ /**
391
+ * Resolve an id in scope. Behaves like the algorithm defined on https://www.w3.org/community/xformsusers/wiki/XForms_2.0#idref-resolve
392
+ */
393
+ export function resolveId(id, sourceObject, nodeName = null) {
394
+ const allMatchingTargetObjects = fxEvaluateXPathToNodes(
395
+ 'outermost(ancestor-or-self::fx-fore[1]/(descendant::xf-fore|descendant::*[@id = $id]))[not(self::fx-fore)]',
396
+ sourceObject,
397
+ null,
398
+ { id },
399
+ { namespaceResolver: xhtmlNamespaceResolver },
400
+ );
401
+
402
+ if (allMatchingTargetObjects.length === 0) {
403
+ return null;
404
+ }
405
+
406
+ if (
407
+ allMatchingTargetObjects.length === 1 &&
408
+ fxEvaluateXPathToBoolean(
409
+ '(ancestor::fx-fore | ancestor::fx-repeat)[last()]/self::fx-fore',
410
+ allMatchingTargetObjects[0],
411
+ null,
412
+ null,
413
+ { namespaceResolver: xhtmlNamespaceResolver },
414
+ )
415
+ ) {
416
+ // If the target element is not repeated, then the search for the target object is trivial since
417
+ // there is only one associated with the target element that bears the matching ID. This is true
418
+ // regardless of whether or not the source object is repeated. However, if the target element is
419
+ // repeated, then additional information must be used to help select a target object from among
420
+ // those associated with the identified target element.
421
+ const targetObject = allMatchingTargetObjects[0];
422
+ if (nodeName && targetObject.localName !== nodeName) {
423
+ return null;
424
+ }
425
+ return targetObject;
426
+ }
427
+
428
+ // SPEC:
429
+
430
+ // 12.2.1 References to Elements within a repeat Element
431
+
432
+ // When the target element that is identified by the IDREF of a source object has one or more
433
+ // repeat elements as ancestors, then the set of ancestor repeats are partitioned into two
434
+ // subsets, those in common with the source element and those that are not in common. Any ancestor
435
+ // repeat elements of the target element not in common with the source element are descendants of
436
+ // the repeat elements that the source and target element have in common, if any.
437
+
438
+ // For the repeat elements that are in common, the desired target object exists in the same set of
439
+ // run-time objects that contains the source object. Then, for each ancestor repeat of the target
440
+ // element that is not in common with the source element, the current index of the repeat
441
+ // determines the set of run-time objects that contains the desired target object.
442
+ for (const ancestorRepeatItem of fxEvaluateXPathToNodes(
443
+ 'ancestor::fx-repeatitem => reverse()',
444
+ sourceObject,
445
+ null,
446
+ null,
447
+ { namespaceResolver: xhtmlNamespaceResolver },
448
+ )) {
449
+ const foundTargetObjects = allMatchingTargetObjects.filter(to =>
450
+ ancestorRepeatItem.contains(to),
451
+ );
452
+ switch (foundTargetObjects.length) {
453
+ case 0:
454
+ // Nothing found: ignore
455
+ break;
456
+ case 1: {
457
+ // A single one is found: the target object is directly in a common repeat
458
+ const targetObject = foundTargetObjects[0];
459
+ if (nodeName && targetObject.localName !== nodeName) {
460
+ return null;
461
+ }
462
+ return targetObject;
463
+ }
464
+ default: {
465
+ // Multiple target objects are found: they are in a repeat that is not common with the source object
466
+ // We found a target object in a common repeat! We now need to find the one that is in the repeatitem identified at the current index
467
+ const targetObject = foundTargetObjects.find(to =>
468
+ fxEvaluateXPathToNodes(
469
+ 'every $ancestor of ancestor::fx-repeatitem satisfies $ancestor is $ancestor/../child::fx-repeatitem[../@repeat-index]',
470
+ to,
471
+ null,
472
+ {},
473
+ ),
474
+ );
475
+ if (!targetObject) {
476
+ // Nothing valid found for whatever reason. This might be something dynamic?
477
+ return null;
478
+ }
479
+ if (nodeName && targetObject.localName !== nodeName) {
480
+ return null;
481
+ }
482
+ return targetObject;
483
+ }
484
+ }
485
+ }
486
+ // We found no target objects in common repeats. The id is unresolvable
487
+ return null;
488
+ }
489
+
490
+ /**
491
+ * @param id as string
492
+ * @return instance data for given id serialized to string.
493
+ */
494
+ registerCustomXPathFunction(
495
+ { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'context' },
496
+ [],
497
+ 'item()?',
498
+ (dynamicContext, string) => {
499
+ const caller = dynamicContext.currentContext.formElement;
500
+ const parent = XPathUtil.getParentBindingElement(caller);
501
+ // const instance = resolveId('default', caller, 'fx-instance');
502
+ const p = caller.nodeName;
503
+ // const p = dynamicContext.domFacade.getParentElement();
504
+
505
+ if (parent) return parent;
506
+ return caller.getInScopeContext();
507
+ },
508
+ );
509
+
15
510
  /**
16
511
  * @param id as string
17
512
  * @return instance data for given id serialized to string.
@@ -22,10 +517,15 @@ registerCustomXPathFunction(
22
517
  'xs:string?',
23
518
  (dynamicContext, string) => {
24
519
  const { formElement } = dynamicContext.currentContext;
25
- const instance = formElement.querySelector(`fx-instance[id=${string}]`);
520
+ const instance = resolveId(string, formElement, 'fx-instance');
26
521
  if (instance) {
27
- const def = new XMLSerializer().serializeToString(instance.getDefaultContext());
28
- return def;
522
+ if (instance.getAttribute('type') === 'json') {
523
+ console.warn('log() does not work for JSON yet');
524
+ // return JSON.stringify(instance.getDefaultContext());
525
+ } else {
526
+ const def = new XMLSerializer().serializeToString(instance.getDefaultContext());
527
+ return prettifyXml(def);
528
+ }
29
529
  }
30
530
  return null;
31
531
  },
@@ -93,13 +593,14 @@ registerCustomXPathFunction(
93
593
  'element()?',
94
594
  (dynamicContext, string) => {
95
595
  const { formElement } = dynamicContext.currentContext;
96
- const instance = formElement.querySelector(`fx-instance[id=${string}]`);
596
+ const instance = resolveId(string, formElement, 'fx-instance');
597
+
97
598
  if (instance) {
98
599
  // const def = new XMLSerializer().serializeToString(instance.getDefaultContext());
99
600
  // const def = JSON.stringify(instance.getDefaultContext());
100
601
 
101
- const tree = document.createElement('div');
102
- tree.setAttribute('class', 'logtree');
602
+ const treeDiv = document.createElement('div');
603
+ treeDiv.setAttribute('class', 'logtree');
103
604
  // const datatree = buildTree(tree,instance.getDefaultContext());
104
605
  // return tree.appendChild(datatree);
105
606
  // return buildTree(root,instance.getDefaultContext());;
@@ -108,7 +609,10 @@ registerCustomXPathFunction(
108
609
  if (logtree) {
109
610
  logtree.parentNode.removeChild(logtree);
110
611
  }
111
- form.appendChild(buildTree(tree, instance.getDefaultContext()));
612
+ const tree = buildTree(treeDiv, instance.getDefaultContext());
613
+ if (tree) {
614
+ form.appendChild(tree);
615
+ }
112
616
  }
113
617
  return null;
114
618
  },
@@ -118,13 +622,19 @@ const instance = (dynamicContext, string) => {
118
622
  // Spec: https://www.w3.org/TR/xforms-xpath/#The_XForms_Function_Library#The_instance.28.29_Function
119
623
  // TODO: handle no string passed (null will be passed instead)
120
624
 
121
- const { formElement } = dynamicContext.currentContext;
625
+ const formElement = fxEvaluateXPathToFirstNode(
626
+ 'ancestor-or-self::fx-fore',
627
+ dynamicContext.currentContext.formElement,
628
+ null,
629
+ null,
630
+ { namespaceResolver: xhtmlNamespaceResolver },
631
+ );
122
632
 
123
633
  // console.log('fnInstance dynamicContext: ', dynamicContext);
124
634
  // console.log('fnInstance string: ', string);
125
635
 
126
636
  const inst = string
127
- ? formElement.querySelector(`fx-instance[id=${string}]`)
637
+ ? resolveId(string, formElement, 'fx-instance')
128
638
  : formElement.querySelector(`fx-instance`);
129
639
 
130
640
  // const def = instance.getInstanceData();
@@ -143,7 +653,10 @@ registerCustomXPathFunction(
143
653
  'xs:integer?',
144
654
  (dynamicContext, string) => {
145
655
  const { formElement } = dynamicContext.currentContext;
146
- const repeat = string ? formElement.querySelector(`fx-repeat[id=${string}]`) : null;
656
+ if (string === null) {
657
+ return 1;
658
+ }
659
+ const repeat = resolveId(string, formElement, 'fx-repeat');
147
660
 
148
661
  // const def = instance.getInstanceData();
149
662
  if (repeat) {
@@ -225,227 +738,12 @@ executePendingUpdateList(pendingUpdatesAndXdmValue.pendingUpdateList, null, null
225
738
  */
226
739
 
227
740
  /**
228
- * Implementation of the functionNameResolver passed to FontoXPath to
229
- * redirect function resolving for unprefixed functions to either the fn or the xf namespace
230
- */
231
- // eslint-disable-next-line no-unused-vars
232
- function functionNameResolver({ prefix, localName }, _arity) {
233
- switch (localName) {
234
- // TODO: put the full XForms library functions set here
235
- case 'boolean-from-string':
236
- case 'depends':
237
- case 'event':
238
- case 'index':
239
- case 'instance':
240
- case 'log':
241
- case 'logtree':
242
- return { namespaceURI: XFORMS_NAMESPACE_URI, localName };
243
- default:
244
- if (prefix === '' || prefix === 'fn') {
245
- return { namespaceURI: 'http://www.w3.org/2005/xpath-functions', localName };
246
- }
247
- if (prefix === 'local') {
248
- return { namespaceURI: 'http://www.w3.org/2005/xquery-local-functions', localName };
249
- }
250
- return null;
251
- }
252
- }
253
-
254
- function namespaceResolver(prefix) {
255
- // TODO: Do proper namespace resolving. Look at the ancestry / namespacesInScope of the declaration
256
-
257
- /**
258
- * for (let ancestor = this; ancestor; ancestor = ancestor.parentNode) {
259
- * if (ancestor.getAttribute(`xmlns:${prefix}`)) {
260
- * // Return value
261
- * }
262
- * }
263
- */
264
-
265
- // console.log('namespaceResolver prefix', prefix);
266
- const ns = {
267
- xhtml: 'http://www.w3.org/1999/xhtml',
268
- tei: 'http://www.tei-c.org/ns/1.0',
269
- // '' : Fore.XFORMS_NAMESPACE_URI
270
- };
271
- return ns[prefix] || null;
272
- }
273
-
274
- /**
275
- * Evaluate an XPath to _any_ type. When possible, prefer to use any other function to ensure the
276
- * type of the output is more predictable.
277
- *
278
- * @param {string} xpath The XPath to run
279
- * @param {Node} contextNode The start of the XPath
280
- * @param {Node} formElement The form element associated to the XPath
281
- */
282
- export function evaluateXPath(xpath, contextNode, formElement, variables = {}) {
283
- return fxEvaluateXPath(xpath, contextNode, null, variables, 'xs:anyType', {
284
- currentContext: { formElement, variables },
285
- moduleImports: {
286
- xf: XFORMS_NAMESPACE_URI,
287
- },
288
- functionNameResolver,
289
- namespaceResolver,
290
- });
291
- }
292
-
293
- /**
294
- * Evaluate an XPath to the first Node
295
- *
296
- * @param {string} xpath The XPath to run
297
- * @param {Node} contextNode The start of the XPath
298
- * @param {Node} formElement The form element associated to the XPath
299
- * @return {Node} The first node found by the XPath
300
- */
301
- export function evaluateXPathToFirstNode(xpath, contextNode, formElement) {
302
- return fxEvaluateXPathToFirstNode(
303
- xpath,
304
- contextNode,
305
- null,
306
- {},
307
- {
308
- namespaceResolver,
309
- defaultFunctionNamespaceURI: XFORMS_NAMESPACE_URI,
310
- moduleImports: {
311
- xf: XFORMS_NAMESPACE_URI,
312
- },
313
- currentContext: { formElement },
314
- },
315
- );
316
- }
317
-
318
- /**
319
- * Evaluate an XPath to all nodes
320
- *
321
- * @param {string} xpath The XPath to run
322
- * @param {Node} contextNode The start of the XPath
323
- * @param {Node} formElement The form element associated to the XPath
324
- * @return {Node[]} All nodes
325
- */
326
- export function evaluateXPathToNodes(xpath, contextNode, formElement) {
327
- return fxEvaluateXPathToNodes(
328
- xpath,
329
- contextNode,
330
- null,
331
- {},
332
- {
333
- currentContext: { formElement },
334
- functionNameResolver,
335
- moduleImports: {
336
- xf: XFORMS_NAMESPACE_URI,
337
- },
338
- namespaceResolver,
339
- },
340
- );
341
- }
342
-
343
- /**
344
- * Evaluate an XPath to a boolean
345
- *
346
- * @param {string} xpath The XPath to run
347
- * @param {Node} contextNode The start of the XPath
348
- * @param {Node} formElement The form element associated to the XPath
349
- * @return {boolean}
350
- */
351
- export function evaluateXPathToBoolean(xpath, contextNode, formElement) {
352
- return fxEvaluateXPathToBoolean(
353
- xpath,
354
- contextNode,
355
- null,
356
- {},
357
- {
358
- currentContext: { formElement },
359
- functionNameResolver,
360
- moduleImports: {
361
- xf: XFORMS_NAMESPACE_URI,
362
- },
363
- namespaceResolver,
364
- },
365
- );
366
- }
367
-
368
- /**
369
- * Evaluate an XPath to a string
370
- *
371
- * @param {string} xpath The XPath to run
372
- * @param {Node} contextNode The start of the XPath
373
- * @param {Node} formElement The form element associated to the XPath
374
- * @param {DomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
375
- * access. This is used to determine dependencies between bind elements.
741
+ * @param input as string
376
742
  * @return {string}
377
743
  */
378
- export function evaluateXPathToString(xpath, contextNode, formElement, domFacade = null) {
379
- return fxEvaluateXPathToString(
380
- xpath,
381
- contextNode,
382
- domFacade,
383
- {},
384
-
385
- {
386
- currentContext: { formElement },
387
- functionNameResolver,
388
- moduleImports: {
389
- xf: XFORMS_NAMESPACE_URI,
390
- },
391
- namespaceResolver,
392
- },
393
- );
394
- }
395
- /**
396
- * Evaluate an XPath to a number
397
- *
398
- * @param {string} xpath The XPath to run
399
- * @param {Node} contextNode The start of the XPath
400
- * @param {Node} formElement The form element associated to the XPath
401
- * @param {DomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
402
- * access. This is used to determine dependencies between bind elements.
403
- * @return {string}
404
- */
405
- export function evaluateXPathToNumber(xpath, contextNode, formElement, domFacade = null) {
406
- return fxEvaluateXPathToNumber(
407
- xpath,
408
- contextNode,
409
- domFacade,
410
- {},
411
-
412
- {
413
- currentContext: { formElement },
414
- functionNameResolver,
415
- moduleImports: {
416
- xf: XFORMS_NAMESPACE_URI,
417
- },
418
- namespaceResolver,
419
- },
420
- );
421
- }
422
- /**
423
- * evaluate a template expression (some expression in {} brackets) on a node (either text- or attribute node.
424
- * @param expr the XPath to evaluate
425
- * @param node the node which will get updated with evaluation result
426
- * @param form the form element
427
- */
428
- export function evaluateTemplateExpression(expr, node, form) {
429
- const matches = expr.match(/{[^}]*}/g);
430
- if (matches) {
431
- matches.forEach(match => {
432
- console.log('match ', match);
433
- const naked = match.substring(1, match.length - 1);
434
- const inscope = getInScopeContext(node, naked);
435
- const result = evaluateXPathToString(naked, inscope, form);
436
-
437
- // console.log('result of eval ', result);
438
- const replaced = expr.replaceAll(match, result);
439
- console.log('result of replacing ', replaced);
440
-
441
- if (node.nodeType === Node.ATTRIBUTE_NODE) {
442
- const parent = node.ownerElement;
443
-
444
- // parent.setAttribute(name, replaced);
445
- parent.setAttribute(node.nodeName, replaced);
446
- } else if (node.nodeType === Node.TEXT_NODE) {
447
- node.textContent = replaced;
448
- }
449
- });
450
- }
451
- }
744
+ registerCustomXPathFunction(
745
+ { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'base64encode' },
746
+ ['xs:string?'],
747
+ 'xs:string?',
748
+ (dynamicContext, string) => btoa(string),
749
+ );