jtlt 0.5.0 → 0.7.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.
@@ -59,6 +59,10 @@ class XPathTransformerContext {
59
59
  this._currPath = undefined; // XPath string of current context
60
60
  /** @type {Record<string, any> | undefined} */
61
61
  this._params = undefined;
62
+ /** @type {string[]} */
63
+ this._preserveSpaceElements = [];
64
+ /** @type {string[]} */
65
+ this._stripSpaceElements = [];
62
66
  }
63
67
 
64
68
  /** @returns {import('./index.js').JoiningTransformer} */
@@ -66,6 +70,92 @@ class XPathTransformerContext {
66
70
  return this._config.joiningTransformer;
67
71
  }
68
72
 
73
+ /**
74
+ * Check if whitespace should be stripped for a given element.
75
+ * @param {Node} node - The node to check
76
+ * @returns {boolean}
77
+ */
78
+ _shouldStripSpace (node) {
79
+ if (node.nodeType !== 1) {
80
+ return false; // Only elements
81
+ }
82
+ const elementName = /** @type {Element} */ (node).localName;
83
+ // Check if in preserve list (takes precedence)
84
+ if (this._preserveSpaceElements.some((pattern) => {
85
+ return pattern === '*' || pattern === elementName;
86
+ })) {
87
+ return false;
88
+ }
89
+ // Check if in strip list
90
+ return this._stripSpaceElements.some((pattern) => {
91
+ return pattern === '*' || pattern === elementName;
92
+ });
93
+ }
94
+
95
+ /**
96
+ * Clone the DOM and strip whitespace-only text nodes from elements
97
+ * marked for stripping.
98
+ * @param {Node} node - The node to clone
99
+ * @returns {Node} The cloned node with whitespace stripped
100
+ */
101
+ _cloneAndStripWhitespace (node) {
102
+ // Deep clone the node
103
+ const cloned = node.cloneNode(true);
104
+
105
+ // Only process if we have strip-space declarations
106
+ if (this._stripSpaceElements.length === 0) {
107
+ return cloned;
108
+ }
109
+
110
+ // Recursively strip whitespace-only text nodes
111
+ const stripWhitespace = (/** @type {Node} */ n) => {
112
+ if (n.nodeType === 1) { // Element node
113
+ if (this._shouldStripSpace(n)) {
114
+ // Remove whitespace-only text node children
115
+ const childNodes = [...n.childNodes];
116
+ for (const child of childNodes) {
117
+ if (child.nodeType === 3) { // Text node
118
+ /* c8 ignore next 2 -- Branch inside loop already tested by
119
+ integration tests; c8 artifact */
120
+ const text = child.nodeValue || '';
121
+ // Check if text is whitespace-only
122
+ if (text.trim() === '') {
123
+ child.remove();
124
+ }
125
+ }
126
+ }
127
+ }
128
+ // Recurse into child elements
129
+ const children = [...n.childNodes];
130
+ for (const child of children) {
131
+ stripWhitespace(child);
132
+ }
133
+ } else if (n.nodeType === 9) { // Document node
134
+ // Process document's children (typically documentElement)
135
+ const children = [...n.childNodes];
136
+ for (const child of children) {
137
+ stripWhitespace(child);
138
+ }
139
+ }
140
+ };
141
+
142
+ stripWhitespace(cloned);
143
+ return cloned;
144
+ }
145
+
146
+ /**
147
+ * Apply whitespace stripping to the context node based on strip-space
148
+ * declarations. This clones the DOM and updates the context.
149
+ * @returns {this}
150
+ */
151
+ applyWhitespaceStripping () {
152
+ if (this._stripSpaceElements.length > 0) {
153
+ const stripped = this._cloneAndStripWhitespace(this._origNode);
154
+ this._contextNode = stripped;
155
+ }
156
+ return this;
157
+ }
158
+
69
159
  /**
70
160
  * Evaluate an XPath expression against the current context node.
71
161
  * @param {string} expr - XPath expression
@@ -76,12 +166,16 @@ class XPathTransformerContext {
76
166
  if (!expr) {
77
167
  return this._contextNode;
78
168
  }
169
+
170
+ // Ensure we're using the stripped DOM if strip-space is declared
171
+ const contextNode = this._contextNode;
172
+
79
173
  const version = this._config.xpathVersion ?? 1;
80
174
  if (version === 1) {
81
175
  // Use native XPath (browser-like); rely on DOM doc if available.
82
- const doc = this._contextNode && this._contextNode.ownerDocument
83
- ? this._contextNode.ownerDocument
84
- : (this._contextNode.nodeType === 9 ? this._contextNode : undefined);
176
+ const doc = contextNode && contextNode.ownerDocument
177
+ ? contextNode.ownerDocument
178
+ : (contextNode.nodeType === 9 ? contextNode : undefined);
85
179
  if (!doc || doc.nodeType !== 9) {
86
180
  throw new Error(
87
181
  'Native XPath unavailable for xpathVersion=1'
@@ -105,7 +199,7 @@ class XPathTransformerContext {
105
199
  );
106
200
  /* c8 ignore stop */
107
201
  const resultObj = docTyped.evaluate(
108
- expr, this._contextNode, resolver, type, null
202
+ expr, contextNode, resolver, type, null
109
203
  );
110
204
  if (asNodes) {
111
205
  /** @type {Node[]} */
@@ -150,7 +244,7 @@ class XPathTransformerContext {
150
244
  }
151
245
  if (version === 2) {
152
246
  // Version 2: xpath2.js
153
- const result = xpath2.evaluate(expr, this._contextNode);
247
+ const result = xpath2.evaluate(expr, contextNode);
154
248
  if (asNodes) {
155
249
  // eslint-disable-next-line @stylistic/max-len -- Long
156
250
  /* c8 ignore next -- array wrap/identity branch counted in other tests */
@@ -163,7 +257,7 @@ class XPathTransformerContext {
163
257
  // eslint-disable-next-line @stylistic/max-len -- Long
164
258
  // eslint-disable-next-line import/no-named-as-default-member -- Only as default
165
259
  const result = fontoxpath.evaluateXPath(
166
- expr, this._contextNode, undefined, undefined,
260
+ expr, contextNode, undefined, undefined,
167
261
  // Non-deprecated, predictable all results
168
262
  14 // ReturnType.ALL_RESULTS
169
263
  );
@@ -1233,6 +1327,43 @@ class XPathTransformerContext {
1233
1327
  this._getJoiningTransformer().propValue(prop, val);
1234
1328
  return this;
1235
1329
  }
1330
+ /**
1331
+ * Alias for propValue(). Append a key-value pair to the current map/object.
1332
+ * @param {string} prop Property name
1333
+ * @param {any} val Value
1334
+ * @returns {XPathTransformerContext}
1335
+ */
1336
+ mapEntry (prop, val) {
1337
+ return this.propValue(prop, val);
1338
+ }
1339
+ /**
1340
+ * Declare elements for which whitespace-only text nodes should be preserved.
1341
+ * Equivalent to xsl:preserve-space.
1342
+ * @param {string|string[]} elements - Element name(s) or patterns
1343
+ * @returns {XPathTransformerContext}
1344
+ */
1345
+ preserveSpace (elements) {
1346
+ const elemArray = Array.isArray(elements) ? elements : [elements];
1347
+ this._preserveSpaceElements.push(...elemArray);
1348
+ return this;
1349
+ }
1350
+ /**
1351
+ * Declare elements for which whitespace-only text nodes should be stripped.
1352
+ * Equivalent to xsl:strip-space.
1353
+ * This automatically clones the DOM and removes whitespace-only text nodes.
1354
+ * @param {string|string[]} elements - Element name(s) or patterns
1355
+ * @returns {XPathTransformerContext}
1356
+ */
1357
+ stripSpace (elements) {
1358
+ const elemArray = Array.isArray(elements) ? elements : [elements];
1359
+ const wasEmpty = this._stripSpaceElements.length === 0;
1360
+ this._stripSpaceElements.push(...elemArray);
1361
+ // Apply stripping if this is the first strip-space declaration
1362
+ if (wasEmpty && this._stripSpaceElements.length > 0) {
1363
+ this.applyWhitespaceStripping();
1364
+ }
1365
+ return this;
1366
+ }
1236
1367
  /**
1237
1368
  * Append object.
1238
1369
  * @param {Record<string, unknown>|
@@ -1253,6 +1384,23 @@ class XPathTransformerContext {
1253
1384
  propSets);
1254
1385
  return this;
1255
1386
  }
1387
+ /**
1388
+ * Alias for object(). Append an object/map.
1389
+ * @param {Record<string, unknown>|
1390
+ * ((this: XPathTransformerContext) => void)} objOrCb Object or callback
1391
+ * @param {((this: XPathTransformerContext) => void)|
1392
+ * any[]} [cbOrUsePropertySets] Callback or property sets
1393
+ * @param {any[]|
1394
+ * Record<string, unknown>} [usePropertySetsOrPropSets]
1395
+ * Property sets or props
1396
+ * @param {Record<string, unknown>} [propSets] Additional property sets
1397
+ * @returns {XPathTransformerContext}
1398
+ */
1399
+ map (objOrCb, cbOrUsePropertySets, usePropertySetsOrPropSets, propSets) {
1400
+ return this.object(
1401
+ objOrCb, cbOrUsePropertySets, usePropertySetsOrPropSets, propSets
1402
+ );
1403
+ }
1256
1404
  /**
1257
1405
  * Append array.
1258
1406
  * @param {any[]|
@@ -1290,6 +1438,16 @@ class XPathTransformerContext {
1290
1438
  return this;
1291
1439
  }
1292
1440
 
1441
+ /**
1442
+ * @param {string} name
1443
+ * @param {Record<string, string>} attributes
1444
+ * @returns {this}
1445
+ */
1446
+ attributeSet (name, attributes) {
1447
+ this._getJoiningTransformer().attributeSet(name, attributes);
1448
+ return this;
1449
+ }
1450
+
1293
1451
  /**
1294
1452
  * Append element.
1295
1453
  * @param {string} name Tag name
@@ -1298,11 +1456,13 @@ class XPathTransformerContext {
1298
1456
  * @param {any[]|((this: XPathTransformerContext)=>void)} [children]
1299
1457
  * Children
1300
1458
  * @param {(this: XPathTransformerContext)=>void} [cb] Callback
1459
+ * @param {string[]} [useAttributeSets] - Attribute set names to apply
1301
1460
  * @returns {XPathTransformerContext}
1302
1461
  */
1303
- element (name, atts, children, cb) {
1304
- // @ts-expect-error - Union of transformers creates intersection types
1305
- this._getJoiningTransformer().element(name, atts, children, cb);
1462
+ element (name, atts, children, cb, useAttributeSets) {
1463
+ /** @type {any} */ (this._getJoiningTransformer()).element(
1464
+ name, atts, children, cb, useAttributeSets
1465
+ );
1306
1466
  return this;
1307
1467
  }
1308
1468
 
@@ -1556,6 +1716,26 @@ class XPathTransformerContext {
1556
1716
  return this;
1557
1717
  }
1558
1718
 
1719
+ /**
1720
+ * Assert that a test condition is true, throwing an error if it fails.
1721
+ * Equivalent to xsl:assert. Evaluates an XPath expression using the
1722
+ * same truthiness rules as if() and choose().
1723
+ * @param {string} test - XPath expression to test
1724
+ * @param {string} [message] - Optional error message to include
1725
+ * @returns {XPathTransformerContext}
1726
+ * @throws {Error} When the test expression evaluates to false
1727
+ */
1728
+ assert (test, message) {
1729
+ const passes = this._passesIf(test);
1730
+ if (!passes) {
1731
+ const errorMsg = message
1732
+ ? `Assertion failed: ${message}`
1733
+ : `Assertion failed: ${test}`;
1734
+ throw new Error(errorMsg);
1735
+ }
1736
+ return this;
1737
+ }
1738
+
1559
1739
  /**
1560
1740
  * Analyze a string with a regular expression, equivalent to
1561
1741
  * xsl:analyze-string. Processes matching and non-matching substrings