@jinntec/fore 2.4.2 → 2.6.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/src/xpath-util.js CHANGED
@@ -1,187 +1,329 @@
1
1
  import * as fx from 'fontoxpath';
2
2
 
3
3
  export class XPathUtil {
4
- /**
5
- * Alternative to `contains` that respects shadowroots
6
- * @param {Node} ancestor
7
- * @param {Node} descendant
8
- * @returns {boolean}
9
- */
10
- static contains(ancestor, descendant) {
11
- while (descendant) {
12
- if (descendant === ancestor) {
13
- return true;
14
- }
15
-
16
- if (descendant.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
17
- // We are passing a shadow root boundary
18
- descendant = descendant.host;
19
- } else {
20
- descendant = descendant.parentNode;
21
- }
4
+ /**
5
+ * creates DOM Nodes from an XPath locationpath expression. Support namespaced and un-namespaced
6
+ * nodes.
7
+ * E.g. 'foo/bar' creates an element 'foo' with an child element 'bar'
8
+ * 'foo/@bar' creates a 'foo' element with an 'bar' attribute
9
+ *
10
+ * supports multiple steps
11
+ *
12
+ * @param xpath
13
+ * @param doc
14
+ * @param fore
15
+ * @return {*}
16
+ */
17
+ static createNodesFromXPath(xpath, doc, fore) {
18
+ if (!doc) {
19
+ doc = document.implementation.createDocument(null, null, null); // Create a new XML document if not provided
20
+ }
21
+
22
+ const parts = xpath.split('/');
23
+ let rootNode = null;
24
+ let currentNode = null;
25
+
26
+ for (const part of parts) {
27
+ if (!part) continue; // Skip empty parts (e.g., leading slashes)
28
+
29
+ // Handle attributes
30
+ if (part.startsWith('@')) {
31
+ const attrName = part.slice(1); // Strip '@'
32
+ if (!currentNode) {
33
+ throw new Error(
34
+ 'Cannot create an attribute without a parent element.',
35
+ );
36
+ }
37
+ currentNode.setAttribute(attrName, '');
38
+ } else {
39
+ // Handle namespaces if present
40
+ const [prefix, localName] = part.includes(':')
41
+ ? part.split(':')
42
+ : [null, part];
43
+ const namespace = prefix
44
+ ? XPathUtil.lookupNamespace(fore, prefix)
45
+ : null;
46
+
47
+ const newElement = namespace
48
+ ? doc.createElementNS(namespace, part)
49
+ : doc.createElement(localName);
50
+
51
+ if (!rootNode) {
52
+ rootNode = newElement; // Set as the root node
53
+ } else {
54
+ currentNode.appendChild(newElement);
55
+ }
56
+ currentNode = newElement;
57
+ }
58
+ }
59
+
60
+ if (!rootNode) {
61
+ throw new Error('Invalid XPath; no root element could be created.');
62
+ }
63
+
64
+ return rootNode;
22
65
  }
23
- return false;
24
- }
25
-
26
- /**
27
- * Alternative to `closest` that respects subcontrol boundaries
28
- *
29
- * @param {string} querySelector
30
- * @param {Node} start
31
- * @returns {HTMLElement}
32
- */
33
- static getClosest(querySelector, start) {
34
- while ((start && !start.matches) || !start.matches(querySelector)) {
35
- if (start.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
36
- // We are passing a shadow root boundary
37
- start = start.host;
38
- continue;
39
- }
40
- if (start.nodeType === Node.ATTRIBUTE_NODE) {
41
- // We are passing an attribute
42
- start = start.ownerElement;
43
- continue;
44
- }
45
- if (start.nodeType === Node.TEXT_NODE) {
46
- start = start.parentNode;
47
- }
48
- if (start.matches('fx-fore')) {
49
- // Subform reached. Bail out
50
- return null;
51
- }
52
- start = start.parentNode;
53
- if (!start) {
54
- return null;
55
- }
66
+
67
+ /**
68
+ * looks up namespace on ownerForm. Though not strictly in the sense of resolving namespaces in XML, the
69
+ * fx-fore element is a convenient place to put namespace declarations for 2 reasons:
70
+ * - this way namespaces are scoped to a Fore element
71
+ * - as fx-fore is a web component we can add our xmlns attributes as we got no restrictions to attributes
72
+ * though strictly speaking they are no xmlns declarations and just serve the purpose of namespace lookup.
73
+ *
74
+ * @param boundElement
75
+ * @param prefix
76
+ * @return {string}
77
+ */
78
+ static lookupNamespace(ownerForm, prefix) {
79
+ return ownerForm.getAttribute(`xmlns:${prefix}`);
80
+ }
81
+
82
+ static querySelectorAll(querySelector, start) {
83
+ const queue = [start];
84
+ const found = [];
85
+ while (queue.length) {
86
+ const item = queue.shift();
87
+ for (const child of Array.from(item.children).reverse()) {
88
+ queue.unshift(child);
89
+ }
90
+
91
+ if (item.matches && item.matches('template')) {
92
+ queue.unshift(item.content);
93
+ }
94
+
95
+ if (item.matches && item.matches(querySelector)) {
96
+ found.push(item);
97
+ }
98
+ }
99
+
100
+ return found;
101
+ }
102
+
103
+ /**
104
+ * Alternative to `contains` that respects shadowroots
105
+ * @param {Node} ancestor
106
+ * @param {Node} descendant
107
+ * @returns {boolean}
108
+ */
109
+ static contains(ancestor, descendant) {
110
+ while (descendant) {
111
+ if (descendant === ancestor) {
112
+ return true;
113
+ }
114
+
115
+ if (descendant.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
116
+ // We are passing a shadow root boundary
117
+ descendant = descendant.host;
118
+ } else {
119
+ descendant = descendant.parentNode;
120
+ }
121
+ }
122
+ return false;
123
+ }
124
+
125
+ /**
126
+ * Alternative to `closest` that respects subcontrol boundaries
127
+ *
128
+ * @param {string} querySelector
129
+ * @param {Node} start
130
+ * @returns {HTMLElement}
131
+ */
132
+ static getClosest(querySelector, start) {
133
+ while ((start && !start.matches) || !start.matches(querySelector)) {
134
+ if (start.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
135
+ // We are passing a shadow root boundary
136
+ start = start.host;
137
+ continue;
138
+ }
139
+ if (start.nodeType === Node.ATTRIBUTE_NODE) {
140
+ // We are passing an attribute
141
+ start = start.ownerElement;
142
+ continue;
143
+ }
144
+ if (start.nodeType === Node.TEXT_NODE) {
145
+ start = start.parentNode;
146
+ }
147
+ if (start.matches('fx-fore')) {
148
+ // Subform reached. Bail out
149
+ return null;
150
+ }
151
+ start = start.parentNode;
152
+ if (!start) {
153
+ return null;
154
+ }
155
+ }
156
+ return start;
56
157
  }
57
- return start;
58
- }
59
-
60
- /**
61
- * returns next bound element upwards in tree
62
- * @param {Node} start where to start the search
63
- * @returns {*|null}
64
- */
65
- static getParentBindingElement(start) {
66
- /* if (start.parentNode.host) {
158
+
159
+ /**
160
+ * returns next bound element upwards in tree
161
+ * @param {Node} start where to start the search
162
+ * @returns {*|null}
163
+ */
164
+ static getParentBindingElement(start) {
165
+ /* if (start.parentNode.host) {
67
166
  const { host } = start.parentNode;
68
167
  if (host.hasAttribute('ref')) {
69
168
  return host;
70
169
  }
71
170
  } else */
72
- if (
73
- start.parentNode &&
74
- (start.parentNode.nodeType !== Node.DOCUMENT_NODE ||
75
- start.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE)
76
- ) {
77
- return start.parentNode.closest('[ref]');
171
+ if (
172
+ start.parentNode &&
173
+ (start.parentNode.nodeType !== Node.DOCUMENT_NODE ||
174
+ start.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE)
175
+ ) {
176
+ return this.getClosest('[ref],fx-repeatitem', start.parentNode);
177
+ }
178
+ return null;
78
179
  }
79
- return null;
80
- }
81
-
82
- /**
83
- * Checks whether the specified path expression is an absolute path.
84
- *
85
- * @param {string} path the path expression.
86
- * @returns {boolean} <code>true</code> if specified path expression is an absolute
87
- * path, otherwise <code>false</code>.
88
- */
89
- static isAbsolutePath(path) {
90
- return path != null && (path.startsWith('/') || path.startsWith('instance('));
91
- }
92
-
93
- /**
94
- * @param {string} ref
95
- */
96
- static isSelfReference(ref) {
97
- return ref === '.' || ref === './text()' || ref === 'text()' || ref === '' || ref === null;
98
- }
99
-
100
- /**
101
- * returns the instance id from a complete XPath using `instance()` function.
102
- *
103
- * Will return 'default' in case no ref is given at all or the `instance()` function is called without arg.
104
- *
105
- * Otherwise instance id is extracted from function and returned. If all fails null is returned.
106
- * @param {string} ref
107
- * @returns {string}
108
- */
109
- static getInstanceId(ref) {
110
- if (!ref) {
111
- return 'default';
180
+
181
+ /**
182
+ * Checks whether the specified path expression is an absolute path.
183
+ *
184
+ * @param {string} path the path expression.
185
+ * @returns {boolean} <code>true</code> if specified path expression is an absolute
186
+ * path, otherwise <code>false</code>.
187
+ */
188
+ static isAbsolutePath(path) {
189
+ return (
190
+ path != null &&
191
+ (path.startsWith('/') ||
192
+ path.startsWith('instance(') ||
193
+ path.startsWith('$'))
194
+ );
112
195
  }
113
- if (ref.startsWith('instance()')) {
114
- return 'default';
196
+
197
+ /**
198
+ * @param {string} ref
199
+ */
200
+ static isSelfReference(ref) {
201
+ return (
202
+ ref === '.' ||
203
+ ref === './text()' ||
204
+ ref === 'text()' ||
205
+ ref === '' ||
206
+ ref === null
207
+ );
115
208
  }
116
- if (ref.startsWith('instance(')) {
117
- const result = ref.substring(ref.indexOf('(') + 1);
118
- return result.substring(1, result.indexOf(')') - 1);
209
+
210
+ /**
211
+ * returns the instance id from a complete XPath using `instance()` function.
212
+ *
213
+ * Will return 'default' in case no ref is given at all or the `instance()` function is called without arg.
214
+ *
215
+ * Otherwise instance id is extracted from function and returned. If all fails null is returned.
216
+ * @param {string} ref
217
+ * @param {HTMLElement} boundElement The element related to this ref. Used to resolve variables
218
+ * @returns {string}
219
+ */
220
+ static getInstanceId(ref, boundElement) {
221
+ if (!ref) {
222
+ return 'default';
223
+ }
224
+ if (ref.startsWith('instance()')) {
225
+ return 'default';
226
+ }
227
+ if (ref.startsWith('instance(')) {
228
+ const result = ref.substring(ref.indexOf('(') + 1);
229
+ return result.substring(1, result.indexOf(')') - 1);
230
+ }
231
+ if (ref.startsWith('$')) {
232
+ // this variable might actually point to an instance
233
+ const variableName = ref.match(
234
+ /\$(?<variableName>[a-zA-Z0-9\-\_]+).*/,
235
+ )?.groups?.variableName;
236
+ let closestActualFormElement = boundElement;
237
+ while (
238
+ closestActualFormElement &&
239
+ !('inScopeVariables' in closestActualFormElement)
240
+ ) {
241
+ closestActualFormElement =
242
+ closestActualFormElement.nodeType === Node.ATTRIBUTE_NODE
243
+ ? closestActualFormElement.ownerElement
244
+ : closestActualFormElement.parentNode;
245
+ }
246
+
247
+ const correspondingVariable =
248
+ closestActualFormElement?.inScopeVariables?.get(variableName);
249
+ if (!correspondingVariable) {
250
+ return null;
251
+ }
252
+ return this.getInstanceId(
253
+ correspondingVariable.valueQuery,
254
+ correspondingVariable,
255
+ );
256
+ }
257
+ return null;
258
+ }
259
+
260
+ /**
261
+ * @param {HTMLElement} boundElement
262
+ * @param {string} path
263
+ * @returns {string}
264
+ */
265
+ static resolveInstance(boundElement, path) {
266
+ let instanceId = XPathUtil.getInstanceId(path, boundElement);
267
+ if (!instanceId) {
268
+ instanceId = XPathUtil.getInstanceId(
269
+ boundElement.getAttribute('ref'),
270
+ boundElement,
271
+ );
272
+ }
273
+ if (instanceId !== null) {
274
+ return instanceId;
275
+ }
276
+
277
+ const parentBinding = XPathUtil.getParentBindingElement(boundElement);
278
+ if (parentBinding) {
279
+ return this.resolveInstance(parentBinding, path);
280
+ }
281
+ return 'default';
119
282
  }
120
- return null;
121
- }
122
-
123
- /**
124
- * @param {HTMLElement} boundElement
125
- * @param {string} path
126
- * @returns {string}
127
- */
128
- static resolveInstance(boundElement, path) {
129
- let instanceId = XPathUtil.getInstanceId(path);
130
- if (!instanceId) {
131
- instanceId = XPathUtil.getInstanceId(boundElement.getAttribute('ref'));
283
+
284
+ /**
285
+ * @param {Node} node
286
+ * @returns string
287
+ */
288
+ static getDocPath(node) {
289
+ const path = fx.evaluateXPathToString('path()', node);
290
+ // Path is like `$default/x[1]/y[1]`
291
+ const shortened = XPathUtil.shortenPath(path);
292
+ return shortened.startsWith('/') ? `${shortened}` : `/${shortened}`;
293
+ }
294
+
295
+ /**
296
+ * @param {Node} node
297
+ * @param {string} instanceId
298
+ * @returns string
299
+ */
300
+ static getPath(node, instanceId) {
301
+ const path = fx.evaluateXPathToString('path()', node);
302
+ // Path is like `$default/x[1]/y[1]`
303
+ const shortened = XPathUtil.shortenPath(path);
304
+ return shortened.startsWith('/')
305
+ ? `$${instanceId}${shortened}`
306
+ : `$${instanceId}/${shortened}`;
132
307
  }
133
- if (instanceId !== null) {
134
- return instanceId;
308
+
309
+ /**
310
+ * @param {string} path
311
+ * @returns string
312
+ */
313
+ static shortenPath(path) {
314
+ const tmp = path.replaceAll(/(Q{(.*?)\})/g, '');
315
+ // cut off leading slash
316
+ const tmp1 = tmp.substring(1, tmp.length);
317
+ // ### cut-off root node ref
318
+ return tmp1.substring(tmp1.indexOf('/'), tmp.length);
135
319
  }
136
320
 
137
- const parentBinding = XPathUtil.getParentBindingElement(boundElement);
138
- if (parentBinding) {
139
- return this.resolveInstance(parentBinding, path);
321
+ /**
322
+ * @param {string} dep
323
+ * @returns {string}
324
+ */
325
+ static getBasePath(dep) {
326
+ const split = dep.split(':');
327
+ return split[0];
140
328
  }
141
- return 'default';
142
- }
143
-
144
- /**
145
- * @param {Node} node
146
- * @returns string
147
- */
148
- static getDocPath(node) {
149
- const path = fx.evaluateXPathToString('path()', node);
150
- // Path is like `$default/x[1]/y[1]`
151
- const shortened = XPathUtil.shortenPath(path);
152
- return shortened.startsWith('/') ? `${shortened}` : `/${shortened}`;
153
- }
154
-
155
- /**
156
- * @param {Node} node
157
- * @param {string} instanceId
158
- * @returns string
159
- */
160
- static getPath(node, instanceId) {
161
- const path = fx.evaluateXPathToString('path()', node);
162
- // Path is like `$default/x[1]/y[1]`
163
- const shortened = XPathUtil.shortenPath(path);
164
- return shortened.startsWith('/') ? `$${instanceId}${shortened}` : `$${instanceId}/${shortened}`;
165
- }
166
-
167
- /**
168
- * @param {string} path
169
- * @returns string
170
- */
171
- static shortenPath(path) {
172
- const tmp = path.replaceAll(/(Q{(.*?)\})/g, '');
173
- // cut off leading slash
174
- const tmp1 = tmp.substring(1, tmp.length);
175
- // ### cut-off root node ref
176
- return tmp1.substring(tmp1.indexOf('/'), tmp.length);
177
- }
178
-
179
- /**
180
- * @param {string} dep
181
- * @returns {string}
182
- */
183
- static getBasePath(dep) {
184
- const split = dep.split(':');
185
- return split[0];
186
- }
187
329
  }