@jinntec/fore 1.0.0-0 → 1.0.0-3

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.
@@ -1,15 +1,586 @@
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';
13
+ import { XPathUtil } from './xpath-util.js';
14
+
15
+ const XFORMS_NAMESPACE_URI = 'http://www.w3.org/2002/xforms';
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
+ * Get the variables in scope of the form element. These are the values of the variables that
222
+ * logically precede the formElement that declares the XPath
223
+ *
224
+ * @param {Node} formElement The element that declares the XPath
225
+ *
226
+ * @return {Object} A key-value mapping of the variables
227
+ */
228
+ function getVariablesInScope(formElement) {
229
+ let closestActualFormElement = formElement;
230
+ while (closestActualFormElement && !('inScopeVariables' in closestActualFormElement)) {
231
+ closestActualFormElement = closestActualFormElement.parentNode;
232
+ }
233
+
234
+ if (!closestActualFormElement) {
235
+ return {};
236
+ }
237
+
238
+ const variables = {};
239
+ for (const key of closestActualFormElement.inScopeVariables.keys()) {
240
+ const varElement = closestActualFormElement.inScopeVariables.get(key);
241
+ variables[key] = varElement.value;
242
+ }
243
+ return variables;
244
+ }
245
+
246
+ /**
247
+ * Evaluate an XPath to _any_ type. When possible, prefer to use any other function to ensure the
248
+ * type of the output is more predictable.
249
+ *
250
+ * @param {string} xpath The XPath to run
251
+ * @param {Node} contextNode The start of the XPath
252
+ * @param {{parentNode}|ForeElementMixin} formElement The form element associated to the XPath
253
+ */
254
+ export function evaluateXPath(xpath, contextNode, formElement, variables = {}) {
255
+ const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
256
+ const variablesInScope = getVariablesInScope(formElement);
257
+
258
+ return fxEvaluateXPath(
259
+ xpath,
260
+ contextNode,
261
+ null,
262
+ { ...variablesInScope, ...variables },
263
+ 'xs:anyType',
264
+ {
265
+ currentContext: { formElement, variables },
266
+ moduleImports: {
267
+ xf: XFORMS_NAMESPACE_URI,
268
+ },
269
+ functionNameResolver,
270
+ namespaceResolver,
271
+ },
272
+ );
273
+ }
274
+
275
+ /**
276
+ * Evaluate an XPath to the first Node
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
+ * @return {Node} The first node found by the XPath
282
+ */
283
+ export function evaluateXPathToFirstNode(xpath, contextNode, formElement) {
284
+ const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
285
+ const variablesInScope = getVariablesInScope(formElement);
286
+ return fxEvaluateXPathToFirstNode(xpath, contextNode, null, variablesInScope, {
287
+ defaultFunctionNamespaceURI: XFORMS_NAMESPACE_URI,
288
+ moduleImports: {
289
+ xf: XFORMS_NAMESPACE_URI,
290
+ },
291
+ currentContext: { formElement },
292
+ namespaceResolver,
293
+ });
294
+ }
295
+
296
+ /**
297
+ * Evaluate an XPath to all nodes
298
+ *
299
+ * @param {string} xpath The XPath to run
300
+ * @param {Node} contextNode The start of the XPath
301
+ * @param {Node} formElement The form element associated to the XPath
302
+ * @return {Node[]} All nodes
303
+ */
304
+ export function evaluateXPathToNodes(xpath, contextNode, formElement) {
305
+ const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
306
+ const variablesInScope = getVariablesInScope(formElement);
307
+
308
+ return fxEvaluateXPathToNodes(xpath, contextNode, null, variablesInScope, {
309
+ currentContext: { formElement },
310
+ functionNameResolver,
311
+ moduleImports: {
312
+ xf: XFORMS_NAMESPACE_URI,
313
+ },
314
+ namespaceResolver,
315
+ });
316
+ }
317
+
318
+ /**
319
+ * Evaluate an XPath to a boolean
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 {boolean}
325
+ */
326
+ export function evaluateXPathToBoolean(xpath, contextNode, formElement) {
327
+ const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
328
+ const variablesInScope = getVariablesInScope(formElement);
329
+
330
+ return fxEvaluateXPathToBoolean(xpath, contextNode, null, variablesInScope, {
331
+ currentContext: { formElement },
332
+ functionNameResolver,
333
+ moduleImports: {
334
+ xf: XFORMS_NAMESPACE_URI,
335
+ },
336
+ namespaceResolver,
337
+ });
338
+ }
339
+
340
+ /**
341
+ * Evaluate an XPath to a string
342
+ *
343
+ * @param {string} xpath The XPath to run
344
+ * @param {Node} contextNode The start of the XPath
345
+ * @param {Node} formElement The form element associated to the XPath
346
+ * @param {DomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
347
+ * access. This is used to determine dependencies between bind elements.
348
+ * @param {Node} formElement The element where the XPath is defined: used for namespace resolving
349
+ * @return {string}
350
+ */
351
+ export function evaluateXPathToString(
352
+ xpath,
353
+ contextNode,
354
+ formElement,
355
+ domFacade = null,
356
+ namespaceReferenceNode = formElement,
357
+ ) {
358
+ const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
359
+ const variablesInScope = getVariablesInScope(formElement);
360
+
361
+ return fxEvaluateXPathToString(xpath, contextNode, domFacade, variablesInScope, {
362
+ currentContext: { formElement },
363
+ functionNameResolver,
364
+ moduleImports: {
365
+ xf: XFORMS_NAMESPACE_URI,
366
+ },
367
+ namespaceResolver,
368
+ });
369
+ }
370
+
371
+ /**
372
+ * Evaluate an XPath to a set of strings
373
+ *
374
+ * @param {string} xpath The XPath to run
375
+ * @param {Node} contextNode The start of the XPath
376
+ * @param {Node} formElement The form element associated to the XPath
377
+ * @param {DomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
378
+ * access. This is used to determine dependencies between bind elements.
379
+ * @param {Node} formElement The element where the XPath is defined: used for namespace resolving
380
+ * @return {string}
381
+ */
382
+ export function evaluateXPathToStrings(
383
+ xpath,
384
+ contextNode,
385
+ formElement,
386
+ domFacade = null,
387
+ namespaceReferenceNode = formElement,
388
+ ) {
389
+ const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
390
+ return fxEvaluateXPathToStrings(
391
+ xpath,
392
+ contextNode,
393
+ domFacade,
394
+ {},
395
+
396
+ {
397
+ currentContext: { formElement },
398
+ functionNameResolver,
399
+ moduleImports: {
400
+ xf: XFORMS_NAMESPACE_URI,
401
+ },
402
+ namespaceResolver,
403
+ },
404
+ );
405
+ }
406
+
407
+ /**
408
+ * Evaluate an XPath to a number
409
+ *
410
+ * @param {string} xpath The XPath to run
411
+ * @param {Node} contextNode The start of the XPath
412
+ * @param {Node} formElement The form element associated to the XPath
413
+ * @param {DomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
414
+ * @param {Node} formElement The element where the XPath is defined: used for namespace resolving
415
+ * access. This is used to determine dependencies between bind elements.
416
+ * @return {Number}
417
+ */
418
+ export function evaluateXPathToNumber(
419
+ xpath,
420
+ contextNode,
421
+ formElement,
422
+ domFacade = null,
423
+ namespaceReferenceNode = formElement,
424
+ ) {
425
+ const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
426
+ const variablesInScope = getVariablesInScope(formElement);
427
+
428
+ return fxEvaluateXPathToNumber(xpath, contextNode, domFacade, variablesInScope, {
429
+ currentContext: { formElement },
430
+ functionNameResolver,
431
+ moduleImports: {
432
+ xf: XFORMS_NAMESPACE_URI,
433
+ },
434
+ namespaceResolver,
435
+ });
436
+ }
437
+
438
+ /**
439
+ * Resolve an id in scope. Behaves like the algorithm defined on https://www.w3.org/community/xformsusers/wiki/XForms_2.0#idref-resolve
440
+ */
441
+ export function resolveId(id, sourceObject, nodeName = null) {
442
+ const allMatchingTargetObjects = fxEvaluateXPathToNodes(
443
+ 'outermost(ancestor-or-self::fx-fore[1]/(descendant::xf-fore|descendant::*[@id = $id]))[not(self::fx-fore)]',
444
+ sourceObject,
445
+ null,
446
+ { id },
447
+ { namespaceResolver: xhtmlNamespaceResolver },
448
+ );
449
+
450
+ if (allMatchingTargetObjects.length === 0) {
451
+ return null;
452
+ }
453
+
454
+ if (
455
+ allMatchingTargetObjects.length === 1 &&
456
+ fxEvaluateXPathToBoolean(
457
+ '(ancestor::fx-fore | ancestor::fx-repeat)[last()]/self::fx-fore',
458
+ allMatchingTargetObjects[0],
459
+ null,
460
+ null,
461
+ { namespaceResolver: xhtmlNamespaceResolver },
462
+ )
463
+ ) {
464
+ // If the target element is not repeated, then the search for the target object is trivial since
465
+ // there is only one associated with the target element that bears the matching ID. This is true
466
+ // regardless of whether or not the source object is repeated. However, if the target element is
467
+ // repeated, then additional information must be used to help select a target object from among
468
+ // those associated with the identified target element.
469
+ const targetObject = allMatchingTargetObjects[0];
470
+ if (nodeName && targetObject.localName !== nodeName) {
471
+ return null;
472
+ }
473
+ return targetObject;
474
+ }
11
475
 
12
- const XFORMS_NAMESPACE_URI = 'http://www.w3.org/2002/xforms';
476
+ // SPEC:
477
+
478
+ // 12.2.1 References to Elements within a repeat Element
479
+
480
+ // When the target element that is identified by the IDREF of a source object has one or more
481
+ // repeat elements as ancestors, then the set of ancestor repeats are partitioned into two
482
+ // subsets, those in common with the source element and those that are not in common. Any ancestor
483
+ // repeat elements of the target element not in common with the source element are descendants of
484
+ // the repeat elements that the source and target element have in common, if any.
485
+
486
+ // For the repeat elements that are in common, the desired target object exists in the same set of
487
+ // run-time objects that contains the source object. Then, for each ancestor repeat of the target
488
+ // element that is not in common with the source element, the current index of the repeat
489
+ // determines the set of run-time objects that contains the desired target object.
490
+ for (const ancestorRepeatItem of fxEvaluateXPathToNodes(
491
+ 'ancestor::fx-repeatitem => reverse()',
492
+ sourceObject,
493
+ null,
494
+ null,
495
+ { namespaceResolver: xhtmlNamespaceResolver },
496
+ )) {
497
+ const foundTargetObjects = allMatchingTargetObjects.filter(to =>
498
+ ancestorRepeatItem.contains(to),
499
+ );
500
+ switch (foundTargetObjects.length) {
501
+ case 0:
502
+ // Nothing found: ignore
503
+ break;
504
+ case 1: {
505
+ // A single one is found: the target object is directly in a common repeat
506
+ const targetObject = foundTargetObjects[0];
507
+ if (nodeName && targetObject.localName !== nodeName) {
508
+ return null;
509
+ }
510
+ return targetObject;
511
+ }
512
+ default: {
513
+ // Multiple target objects are found: they are in a repeat that is not common with the source object
514
+ // 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
515
+ const targetObject = foundTargetObjects.find(to =>
516
+ fxEvaluateXPathToNodes(
517
+ 'every $ancestor of ancestor::fx-repeatitem satisfies $ancestor is $ancestor/../child::fx-repeatitem[../@repeat-index]',
518
+ to,
519
+ null,
520
+ {},
521
+ ),
522
+ );
523
+ if (!targetObject) {
524
+ // Nothing valid found for whatever reason. This might be something dynamic?
525
+ return null;
526
+ }
527
+ if (nodeName && targetObject.localName !== nodeName) {
528
+ return null;
529
+ }
530
+ return targetObject;
531
+ }
532
+ }
533
+ }
534
+ // We found no target objects in common repeats. The id is unresolvable
535
+ return null;
536
+ }
537
+
538
+ const contextFunction = (dynamicContext, string) => {
539
+ const caller = dynamicContext.currentContext.formElement;
540
+ if (string) {
541
+ const instance = resolveId(string, caller);
542
+ if (instance) {
543
+ if (instance.nodeName === 'FX-REPEAT') {
544
+ const { nodeset } = instance;
545
+ for (let parent = caller; parent; parent = parent.parentNode) {
546
+ if (parent.parentNode === instance) {
547
+ const offset = Array.from(parent.parentNode.children).indexOf(parent);
548
+ return nodeset[offset];
549
+ }
550
+ }
551
+ }
552
+ return instance.nodeset;
553
+ }
554
+ }
555
+ const parent = XPathUtil.getParentBindingElement(caller);
556
+ const p = caller.nodeName;
557
+ // const p = dynamicContext.domFacade.getParentElement();
558
+
559
+ if (parent) return parent;
560
+ return caller.getInScopeContext();
561
+ };
562
+
563
+ /**
564
+ * @param id as string
565
+ * @return instance data for given id serialized to string.
566
+ */
567
+ registerCustomXPathFunction(
568
+ { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'context' },
569
+ [],
570
+ 'item()?',
571
+ contextFunction,
572
+ );
573
+
574
+ /**
575
+ * @param id as string
576
+ * @return instance data for given id serialized to string.
577
+ */
578
+ registerCustomXPathFunction(
579
+ { namespaceURI: XFORMS_NAMESPACE_URI, localName: 'context' },
580
+ ['xs:string'],
581
+ 'item()?',
582
+ contextFunction,
583
+ );
13
584
 
14
585
  /**
15
586
  * @param id as string
@@ -21,10 +592,15 @@ registerCustomXPathFunction(
21
592
  'xs:string?',
22
593
  (dynamicContext, string) => {
23
594
  const { formElement } = dynamicContext.currentContext;
24
- const instance = formElement.querySelector(`fx-instance[id=${string}]`);
595
+ const instance = resolveId(string, formElement, 'fx-instance');
25
596
  if (instance) {
26
- const def = new XMLSerializer().serializeToString(instance.getDefaultContext());
27
- return def;
597
+ if (instance.getAttribute('type') === 'json') {
598
+ console.warn('log() does not work for JSON yet');
599
+ // return JSON.stringify(instance.getDefaultContext());
600
+ } else {
601
+ const def = new XMLSerializer().serializeToString(instance.getDefaultContext());
602
+ return prettifyXml(def);
603
+ }
28
604
  }
29
605
  return null;
30
606
  },
@@ -92,13 +668,14 @@ registerCustomXPathFunction(
92
668
  'element()?',
93
669
  (dynamicContext, string) => {
94
670
  const { formElement } = dynamicContext.currentContext;
95
- const instance = formElement.querySelector(`fx-instance[id=${string}]`);
671
+ const instance = resolveId(string, formElement, 'fx-instance');
672
+
96
673
  if (instance) {
97
674
  // const def = new XMLSerializer().serializeToString(instance.getDefaultContext());
98
675
  // const def = JSON.stringify(instance.getDefaultContext());
99
676
 
100
- const tree = document.createElement('div');
101
- tree.setAttribute('class', 'logtree');
677
+ const treeDiv = document.createElement('div');
678
+ treeDiv.setAttribute('class', 'logtree');
102
679
  // const datatree = buildTree(tree,instance.getDefaultContext());
103
680
  // return tree.appendChild(datatree);
104
681
  // return buildTree(root,instance.getDefaultContext());;
@@ -107,19 +684,15 @@ registerCustomXPathFunction(
107
684
  if (logtree) {
108
685
  logtree.parentNode.removeChild(logtree);
109
686
  }
110
- form.appendChild(buildTree(tree, instance.getDefaultContext()));
687
+ const tree = buildTree(treeDiv, instance.getDefaultContext());
688
+ if (tree) {
689
+ form.appendChild(tree);
690
+ }
111
691
  }
112
692
  return null;
113
693
  },
114
694
  );
115
695
 
116
- const xhtmlNamespaceResolver = prefix => {
117
- if (!prefix) {
118
- return 'http://www.w3.org/1999/xhtml';
119
- }
120
- return undefined;
121
- };
122
-
123
696
  const instance = (dynamicContext, string) => {
124
697
  // Spec: https://www.w3.org/TR/xforms-xpath/#The_XForms_Function_Library#The_instance.28.29_Function
125
698
  // TODO: handle no string passed (null will be passed instead)
@@ -136,7 +709,7 @@ const instance = (dynamicContext, string) => {
136
709
  // console.log('fnInstance string: ', string);
137
710
 
138
711
  const inst = string
139
- ? formElement.querySelector(`fx-instance[id=${string}]`)
712
+ ? resolveId(string, formElement, 'fx-instance')
140
713
  : formElement.querySelector(`fx-instance`);
141
714
 
142
715
  // const def = instance.getInstanceData();
@@ -155,13 +728,16 @@ registerCustomXPathFunction(
155
728
  'xs:integer?',
156
729
  (dynamicContext, string) => {
157
730
  const { formElement } = dynamicContext.currentContext;
158
- const repeat = string ? formElement.querySelector(`fx-repeat[id=${string}]`) : null;
731
+ if (string === null) {
732
+ return 1;
733
+ }
734
+ const repeat = resolveId(string, formElement, 'fx-repeat');
159
735
 
160
736
  // const def = instance.getInstanceData();
161
737
  if (repeat) {
162
738
  return repeat.getAttribute('index');
163
739
  }
164
- return 1;
740
+ return Number(1);
165
741
  },
166
742
  );
167
743
 
@@ -215,260 +791,26 @@ registerXQueryModule(`
215
791
 
216
792
  // How to run XQUERY:
217
793
  /**
218
- registerXQueryModule(`
219
- module namespace my-custom-namespace = "my-custom-uri";
220
- (:~
221
- Insert attribute somewhere
222
- ~:)
223
- declare %public %updating function my-custom-namespace:do-something ($ele as element()) as xs:boolean {
794
+ registerXQueryModule(`
795
+ module namespace my-custom-namespace = "my-custom-uri";
796
+ (:~
797
+ Insert attribute somewhere
798
+ ~:)
799
+ declare %public %updating function my-custom-namespace:do-something ($ele as element()) as xs:boolean {
224
800
  if ($ele/@done) then false() else
225
801
  (insert node
226
802
  attribute done {"true"}
227
803
  into $ele, true())
228
804
  };
229
- `)
230
- // At some point:
231
- const contextNode = null;
232
- const pendingUpdatesAndXdmValue = evaluateUpdatingExpressionSync('ns:do-something(.)', contextNode, null, null, {moduleImports: {'ns': 'my-custom-uri'}})
233
-
234
- console.log(pendingUpdatesAndXdmValue.xdmValue); // this is true or false, see function
235
-
236
- executePendingUpdateList(pendingUpdatesAndXdmValue.pendingUpdateList, null, null, null);
237
- */
238
-
239
- /**
240
- * Implementation of the functionNameResolver passed to FontoXPath to
241
- * redirect function resolving for unprefixed functions to either the fn or the xf namespace
242
- */
243
- // eslint-disable-next-line no-unused-vars
244
- function functionNameResolver({ prefix, localName }, _arity) {
245
- switch (localName) {
246
- // TODO: put the full XForms library functions set here
247
- case 'base64encode':
248
- case 'boolean-from-string':
249
- case 'depends':
250
- case 'event':
251
- case 'index':
252
- case 'instance':
253
- case 'log':
254
- case 'logtree':
255
- return { namespaceURI: XFORMS_NAMESPACE_URI, localName };
256
- default:
257
- if (prefix === '' || prefix === 'fn') {
258
- return { namespaceURI: 'http://www.w3.org/2005/xpath-functions', localName };
259
- }
260
- if (prefix === 'local') {
261
- return { namespaceURI: 'http://www.w3.org/2005/xquery-local-functions', localName };
262
- }
263
- return null;
264
- }
265
- }
266
-
267
- /**
268
- * Resolve a namespace. Needs a namespace prefix and the element that is most closely related to the
269
- * XPath in which the namespace is being resolved. The prefix will be resolved by using the
270
- * ancestry of said element.
271
- *
272
- * It has two ways of doing so:
273
- *
274
- * - If the prefix is defined in an `xmlns:XXX="YYY"` namespace declaration, it will return 'YYY'.
275
- * - If the prefix is the empty prefix and there is an `xpath-default-namespace="YYY"` attribute in the
276
- * - ancestry, that attribute will be used and 'YYY' will be returned
277
- *
278
- * @param {Node} contextElement The element that is most closely related with the XPath in which this prefix is resolved.
279
- * @param {string} prefix The prefix to resolve
280
- */
281
- function resolveNamespacePrefix(contextElement, prefix) {
282
- if (prefix === 'xhtml') {
283
- return 'http://www.w3.org/1999/xhtml';
284
- }
285
-
286
- if (prefix === '') {
287
- return (
288
- fxEvaluateXPathToString(
289
- 'ancestor-or-self::*/@xpath-default-namespace[last()]',
290
- contextElement,
291
- ) || null
292
- );
293
- }
294
-
295
- // Note: ideally we should use Node#lookupNamespaceURI. However, the nodes we are passed are
296
- // XML. The best we can do is emulate the `xmlns:xxx` namespace declarations by regarding them as
297
- // attributes. Which they technically ARE NOT!
298
-
299
- const result = fxEvaluateXPathToString(
300
- 'ancestor-or-self::*/@*[name() = "xmlns:" || $prefix][last()]',
301
- contextElement,
302
- null,
303
- { prefix },
304
- );
305
-
306
- console.log('result', result);
307
- return result;
308
- }
309
-
310
- /**
311
- * Evaluate an XPath to _any_ type. When possible, prefer to use any other function to ensure the
312
- * type of the output is more predictable.
313
- *
314
- * @param {string} xpath The XPath to run
315
- * @param {Node} contextNode The start of the XPath
316
- * @param {{parentNode}|ForeElementMixin} formElement The form element associated to the XPath
317
- */
318
- export function evaluateXPath(xpath, contextNode, formElement, variables = {}) {
319
- return fxEvaluateXPath(xpath, contextNode, null, variables, 'xs:anyType', {
320
- currentContext: { formElement, variables },
321
- moduleImports: {
322
- xf: XFORMS_NAMESPACE_URI,
323
- },
324
- functionNameResolver,
325
- namespaceResolver: prefix => resolveNamespacePrefix(formElement, prefix),
326
- });
327
- }
328
-
329
- /**
330
- * Evaluate an XPath to the first Node
331
- *
332
- * @param {string} xpath The XPath to run
333
- * @param {Node} contextNode The start of the XPath
334
- * @param {Node} formElement The form element associated to the XPath
335
- * @return {Node} The first node found by the XPath
336
- */
337
- export function evaluateXPathToFirstNode(xpath, contextNode, formElement) {
338
- return fxEvaluateXPathToFirstNode(
339
- xpath,
340
- contextNode,
341
- null,
342
- {},
343
- {
344
- namespaceResolver: prefix => resolveNamespacePrefix(formElement, prefix),
345
- defaultFunctionNamespaceURI: XFORMS_NAMESPACE_URI,
346
- moduleImports: {
347
- xf: XFORMS_NAMESPACE_URI,
348
- },
349
- currentContext: { formElement },
350
- },
351
- );
352
- }
353
-
354
- /**
355
- * Evaluate an XPath to all nodes
356
- *
357
- * @param {string} xpath The XPath to run
358
- * @param {Node} contextNode The start of the XPath
359
- * @param {Node} formElement The form element associated to the XPath
360
- * @return {Node[]} All nodes
361
- */
362
- export function evaluateXPathToNodes(xpath, contextNode, formElement) {
363
- return fxEvaluateXPathToNodes(
364
- xpath,
365
- contextNode,
366
- null,
367
- {},
368
- {
369
- currentContext: { formElement },
370
- functionNameResolver,
371
- moduleImports: {
372
- xf: XFORMS_NAMESPACE_URI,
373
- },
374
- namespaceResolver: prefix => resolveNamespacePrefix(formElement, prefix),
375
- },
376
- );
377
- }
378
-
379
- /**
380
- * Evaluate an XPath to a boolean
381
- *
382
- * @param {string} xpath The XPath to run
383
- * @param {Node} contextNode The start of the XPath
384
- * @param {Node} formElement The form element associated to the XPath
385
- * @return {boolean}
386
- */
387
- export function evaluateXPathToBoolean(xpath, contextNode, formElement) {
388
- return fxEvaluateXPathToBoolean(
389
- xpath,
390
- contextNode,
391
- null,
392
- {},
393
- {
394
- currentContext: { formElement },
395
- functionNameResolver,
396
- moduleImports: {
397
- xf: XFORMS_NAMESPACE_URI,
398
- },
399
- namespaceResolver: prefix => resolveNamespacePrefix(formElement, prefix),
400
- },
401
- );
402
- }
403
-
404
- /**
405
- * Evaluate an XPath to a string
406
- *
407
- * @param {string} xpath The XPath to run
408
- * @param {Node} contextNode The start of the XPath
409
- * @param {Node} formElement The form element associated to the XPath
410
- * @param {DomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
411
- * access. This is used to determine dependencies between bind elements.
412
- * @param {Node} formElement The element where the XPath is defined: used for namespace resolving
413
- * @return {string}
414
- */
415
- export function evaluateXPathToString(
416
- xpath,
417
- contextNode,
418
- formElement,
419
- domFacade = null,
420
- namespaceReferenceNode = formElement,
421
- ) {
422
- return fxEvaluateXPathToString(
423
- xpath,
424
- contextNode,
425
- domFacade,
426
- {},
805
+ `)
806
+ // At some point:
807
+ const contextNode = null;
808
+ const pendingUpdatesAndXdmValue = evaluateUpdatingExpressionSync('ns:do-something(.)', contextNode, null, null, {moduleImports: {'ns': 'my-custom-uri'}})
427
809
 
428
- {
429
- currentContext: { formElement },
430
- functionNameResolver,
431
- moduleImports: {
432
- xf: XFORMS_NAMESPACE_URI,
433
- },
434
- namespaceResolver: prefix => resolveNamespacePrefix(namespaceReferenceNode, prefix),
435
- },
436
- );
437
- }
810
+ console.log(pendingUpdatesAndXdmValue.xdmValue); // this is true or false, see function
438
811
 
439
- /**
440
- * Evaluate an XPath to a number
441
- *
442
- * @param {string} xpath The XPath to run
443
- * @param {Node} contextNode The start of the XPath
444
- * @param {Node} formElement The form element associated to the XPath
445
- * @param {DomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
446
- * @param {Node} formElement The element where the XPath is defined: used for namespace resolving
447
- * access. This is used to determine dependencies between bind elements.
448
- * @return {Number}
449
- */
450
- export function evaluateXPathToNumber(
451
- xpath,
452
- contextNode,
453
- formElement,
454
- domFacade = null,
455
- namespaceReferenceNode = formElement,
456
- ) {
457
- return fxEvaluateXPathToNumber(
458
- xpath,
459
- contextNode,
460
- domFacade,
461
- {},
462
- {
463
- currentContext: { formElement },
464
- functionNameResolver,
465
- moduleImports: {
466
- xf: XFORMS_NAMESPACE_URI,
467
- },
468
- namespaceResolver: prefix => resolveNamespacePrefix(namespaceReferenceNode, prefix),
469
- },
470
- );
471
- }
812
+ executePendingUpdateList(pendingUpdatesAndXdmValue.pendingUpdateList, null, null, null);
813
+ */
472
814
 
473
815
  /**
474
816
  * @param input as string