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