jtlt 0.1.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.
Files changed (43) hide show
  1. package/.editorconfig +16 -0
  2. package/CHANGES.md +5 -0
  3. package/LICENSE-MIT.txt +21 -0
  4. package/README.md +534 -0
  5. package/dist/AbstractJoiningTransformer.d.ts +42 -0
  6. package/dist/AbstractJoiningTransformer.d.ts.map +1 -0
  7. package/dist/DOMJoiningTransformer.d.ts +113 -0
  8. package/dist/DOMJoiningTransformer.d.ts.map +1 -0
  9. package/dist/JSONJoiningTransformer.d.ts +160 -0
  10. package/dist/JSONJoiningTransformer.d.ts.map +1 -0
  11. package/dist/JSONPathTransformer.d.ts +95 -0
  12. package/dist/JSONPathTransformer.d.ts.map +1 -0
  13. package/dist/JSONPathTransformerContext.d.ts +263 -0
  14. package/dist/JSONPathTransformerContext.d.ts.map +1 -0
  15. package/dist/StringJoiningTransformer.d.ts +168 -0
  16. package/dist/StringJoiningTransformer.d.ts.map +1 -0
  17. package/dist/XPathTransformer.d.ts +51 -0
  18. package/dist/XPathTransformer.d.ts.map +1 -0
  19. package/dist/XPathTransformerContext.d.ts +260 -0
  20. package/dist/XPathTransformerContext.d.ts.map +1 -0
  21. package/dist/XSLTStyleJSONPathResolver.d.ts +16 -0
  22. package/dist/XSLTStyleJSONPathResolver.d.ts.map +1 -0
  23. package/dist/index.d.ts +168 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/docs/API.expanded.md +263 -0
  26. package/docs/API.md +69 -0
  27. package/eslint.config.js +30 -0
  28. package/package.json +53 -0
  29. package/pnpm-workspace.yaml +3 -0
  30. package/src/AbstractJoiningTransformer.js +73 -0
  31. package/src/DOMJoiningTransformer.js +237 -0
  32. package/src/JSONJoiningTransformer.js +472 -0
  33. package/src/JSONPathTransformer.js +159 -0
  34. package/src/JSONPathTransformerContext.js +807 -0
  35. package/src/StringJoiningTransformer.js +589 -0
  36. package/src/XPathTransformer.js +94 -0
  37. package/src/XPathTransformerContext.js +496 -0
  38. package/src/XSLTStyleJSONPathResolver.js +39 -0
  39. package/src/index.js +299 -0
  40. package/src/types/xpath2-js.d.ts +2 -0
  41. package/tsconfig-prod.json +19 -0
  42. package/tsconfig.json +14 -0
  43. package/typings/xpath2-js.d.ts +2 -0
@@ -0,0 +1,237 @@
1
+ import * as JHTML from 'jhtml';
2
+ import AbstractJoiningTransformer from './AbstractJoiningTransformer.js';
3
+
4
+ /**
5
+ * Joining transformer that accumulates into a DOM tree.
6
+ *
7
+ * This transformer appends strings and nodes to a DocumentFragment/Element.
8
+ * It expects templates to build DOM nodes explicitly (e.g., via element(),
9
+ * attribute(), and text()), though string/number/boolean will append text
10
+ * nodes for convenience.
11
+ */
12
+ class DOMJoiningTransformer extends AbstractJoiningTransformer {
13
+ /**
14
+ * @param {DocumentFragment|Element} o - Initial DOM node
15
+ * @param {object} cfg - Configuration object
16
+ * @param {object} [cfg.document] - Document object
17
+ */
18
+ constructor (o, cfg) {
19
+ super(cfg); // Include this in any subclass of AbstractJoiningTransformer
20
+ this._dom = o ||
21
+ /** @type {any} */ (cfg).document.createDocumentFragment();
22
+ }
23
+
24
+ /**
25
+ * @param {Node} item
26
+ * @returns {void}
27
+ */
28
+ rawAppend (item) {
29
+ this._dom.append(item);
30
+ }
31
+
32
+ /**
33
+ * @param {string|Node} item - Item to append
34
+ * @returns {void}
35
+ */
36
+ append (item) {
37
+ this._dom.append(item);
38
+ }
39
+
40
+ /**
41
+ * @returns {DocumentFragment|Element}
42
+ */
43
+ get () {
44
+ return this._dom;
45
+ }
46
+
47
+ /**
48
+ * @param {string} prop - Property name
49
+ * @param {*} val - Property value
50
+ * @returns {void}
51
+ */
52
+ // eslint-disable-next-line class-methods-use-this -- Incomplete?
53
+ propValue (prop, val) {
54
+ //
55
+ }
56
+
57
+ /**
58
+ * @param {object} obj - Object to serialize
59
+ * @param {Function} [cb] - Callback function.
60
+ * @param {any[]} [usePropertySets] - Property sets to use
61
+ * @param {object} [propSets] - Additional property sets
62
+ * @returns {DOMJoiningTransformer}
63
+ */
64
+ object (obj, cb, usePropertySets, propSets) {
65
+ this._requireSameChildren('dom', 'object');
66
+ if (this._cfg && /** @type {any} */ (this._cfg).JHTMLForJSON) {
67
+ this.append(JHTML.toJHTMLDOM(/** @type {any} */ (obj)));
68
+ } else {
69
+ // Todo: set current position and deal with children
70
+ this.append('');
71
+ }
72
+ return this;
73
+ }
74
+
75
+ /**
76
+ * @param {any[]|Element} arr
77
+ * @param {Function} [cb] - Callback function
78
+ * @returns {DOMJoiningTransformer}
79
+ */
80
+ array (arr, cb) {
81
+ this._requireSameChildren('dom', 'array');
82
+ if (this._cfg && /** @type {any} */ (this._cfg).JHTMLForJSON) {
83
+ this.append(JHTML.toJHTMLDOM(/** @type {any} */ (arr)));
84
+ } else {
85
+ // Todo: set current position and deal with children
86
+ this.append('');
87
+ }
88
+ return this;
89
+ }
90
+
91
+ /**
92
+ * @param {string} str - String value
93
+ * @param {Function} cb - Callback function (unused)
94
+ * @returns {DOMJoiningTransformer}
95
+ */
96
+ string (str, cb) {
97
+ // Todo: Conditionally add as JHTML (and in subsequent methods as well)
98
+ this.append(str);
99
+ return this;
100
+ }
101
+
102
+ /**
103
+ * @param {number} num - Number value
104
+ * @returns {DOMJoiningTransformer}
105
+ */
106
+ number (num) {
107
+ this.append(num.toString());
108
+ return this;
109
+ }
110
+
111
+ /**
112
+ * @param {boolean} bool
113
+ * @returns {DOMJoiningTransformer}
114
+ */
115
+ boolean (bool) {
116
+ this.append(bool ? 'true' : 'false');
117
+ return this;
118
+ }
119
+
120
+ /**
121
+ * @returns {DOMJoiningTransformer}
122
+ */
123
+ null () {
124
+ this.append('null');
125
+ return this;
126
+ }
127
+
128
+ /**
129
+ * @returns {DOMJoiningTransformer}
130
+ */
131
+ undefined () {
132
+ if (this._cfg && /** @type {any} */ (this._cfg).mode !== 'JavaScript') {
133
+ throw new Error(
134
+ 'undefined is not allowed unless added in JavaScript mode'
135
+ );
136
+ }
137
+ this.append('undefined');
138
+ return this;
139
+ }
140
+
141
+ /**
142
+ * @param {number} num - Non-finite number (NaN, Infinity, -Infinity)
143
+ * @returns {DOMJoiningTransformer}
144
+ */
145
+ nonfiniteNumber (num) {
146
+ if (this._cfg && /** @type {any} */ (this._cfg).mode !== 'JavaScript') {
147
+ throw new Error(
148
+ 'Non-finite numbers are not allowed unless added in JavaScript mode'
149
+ );
150
+ }
151
+ this.append(num.toString());
152
+ return this;
153
+ }
154
+
155
+ /**
156
+ * @param {Function} func - Function to stringify
157
+ * @returns {DOMJoiningTransformer}
158
+ */
159
+ function (func) {
160
+ if (this._cfg && /** @type {any} */ (this._cfg).mode !== 'JavaScript') {
161
+ throw new Error(
162
+ 'function is not allowed unless added in JavaScript mode'
163
+ );
164
+ }
165
+ this.append(func.toString());
166
+ return this;
167
+ }
168
+
169
+ /**
170
+ * @param {string} elName - Element name
171
+ * @param {object} [atts] - Attributes object
172
+ * @param {Function} [cb] - Callback function
173
+ * @returns {DOMJoiningTransformer}
174
+ */
175
+ element (elName, atts, cb) {
176
+ // Todo: allow third argument to be array following Jamilih (also let
177
+ // "atts" follow Jamilih)
178
+ // Todo: allow for cfg to produce Jamilih DOM output or hXML
179
+ // Todo: allow separate XML DOM one with XML String and hXML conversions
180
+ // (HTML to XHTML is inevitably safe?)
181
+
182
+ const el = this._cfg &&
183
+ /** @type {any} */ (this._cfg).document.createElement(elName);
184
+ for (const att in atts) {
185
+ if (Object.hasOwn(atts, att)) {
186
+ /** @type {Record<string, any>} */
187
+ const attsObj = /** @type {any} */ (atts);
188
+ el.setAttribute(att, attsObj[att]);
189
+ }
190
+ }
191
+ this.append(el);
192
+
193
+ const oldDOM = this._dom;
194
+
195
+ this._dom = el;
196
+ if (cb) {
197
+ cb.call(this);
198
+ }
199
+ this._dom = oldDOM;
200
+
201
+ return this;
202
+ }
203
+
204
+ /**
205
+ * @param {string} name
206
+ * @param {string} val
207
+ * @returns {DOMJoiningTransformer}
208
+ */
209
+ attribute (name, val) {
210
+ if (!this._dom || typeof this._dom !== 'object' ||
211
+ this._dom.nodeType !== 1) {
212
+ throw new Error('You may only set an attribute on an element');
213
+ }
214
+ (/** @type {Element} */ (this._dom)).setAttribute(name, val);
215
+ return this;
216
+ }
217
+
218
+ /**
219
+ * @param {string} txt - Text content
220
+ * @returns {DOMJoiningTransformer}
221
+ */
222
+ text (txt) {
223
+ this.append(txt);
224
+ return this;
225
+ }
226
+
227
+ /**
228
+ * @param {string} str
229
+ * @returns {DOMJoiningTransformer}
230
+ */
231
+ plainText (str) {
232
+ this.text(str);
233
+ return this;
234
+ }
235
+ }
236
+
237
+ export default DOMJoiningTransformer;
@@ -0,0 +1,472 @@
1
+ import AbstractJoiningTransformer from './AbstractJoiningTransformer.js';
2
+
3
+ // Regex and helper for converting dataset camelCase to dash-lower
4
+ const camelCase = /[a-z][A-Z]/gv;
5
+ /**
6
+ * @param {string} n0
7
+ * @returns {string}
8
+ */
9
+ function _makeDatasetAttribute (n0) {
10
+ return n0.charAt(0) + '-' + n0.charAt(1).toLowerCase();
11
+ }
12
+
13
+ /**
14
+ * JSON-based joining transformer for building JSON/JavaScript objects.
15
+ *
16
+ * This joiner accumulates into an in-memory JSON value (object or array).
17
+ * append() will push to arrays or shallow-merge into objects; string/number/
18
+ * boolean/null add primitives accordingly. It does not perform HTML escaping
19
+ * or string serialization; it builds real JS values.
20
+ */
21
+ class JSONJoiningTransformer extends AbstractJoiningTransformer {
22
+ /**
23
+ * @param {any[]|object} [o] - Initial object or array
24
+ * @param {object} [cfg] - Configuration object
25
+ */
26
+ constructor (o, cfg) {
27
+ super(cfg);
28
+ /** @type {any[]|object} */
29
+ this._obj = o || [];
30
+ /** @type {boolean | undefined} */
31
+ this._objPropState = undefined;
32
+ /** @type {boolean | undefined} */
33
+ this._arrItemState = undefined;
34
+ /** @type {{attsObj: Record<string, any>, jmlChildren: any[]}[]} */
35
+ this._elementStack = [];
36
+ }
37
+
38
+ /**
39
+ * Directly appends an item to the internal array without checks.
40
+ * @param {*} item - Item to append
41
+ * @returns {void}
42
+ */
43
+ rawAppend (item) {
44
+ /** @type {any[]} */ (this._obj).push(item);
45
+ }
46
+
47
+ /**
48
+ * Appends an item to the current object or array.
49
+ * @param {*} item - Item to append
50
+ * @returns {JSONJoiningTransformer}
51
+ */
52
+ append (item) {
53
+ // Todo: allow for first time
54
+ if (!this._obj || typeof this._obj !== 'object') {
55
+ throw new Error('You cannot append to a scalar or empty value.');
56
+ }
57
+ if (Array.isArray(this._obj)) {
58
+ this._obj.push(item);
59
+ } else {
60
+ Object.assign(this._obj, item);
61
+ }
62
+ return this;
63
+ }
64
+
65
+ /**
66
+ * Gets the current object or array. If unwrapSingleResult config option is
67
+ * enabled and the root array contains exactly one element, returns that
68
+ * element directly (unwrapped).
69
+ * @returns {any[]|object|any}
70
+ */
71
+ get () {
72
+ // Unwrap single-element arrays at the root level if configured
73
+ if (this._cfg && /** @type {any} */ (this._cfg).unwrapSingleResult &&
74
+ Array.isArray(this._obj) && this._obj.length === 1) {
75
+ return this._obj[0];
76
+ }
77
+ return this._obj;
78
+ }
79
+
80
+ /**
81
+ * Sets a property value on the current object.
82
+ * @param {string} prop - Property name
83
+ * @param {*} val - Property value
84
+ * @returns {void}
85
+ */
86
+ propValue (prop, val) {
87
+ if (!this._objPropState) {
88
+ throw new Error(
89
+ 'propValue() can only be called after an object state has been set up.'
90
+ );
91
+ }
92
+ (/** @type {Record<string, any>} */ (this._obj))[prop] = val;
93
+ }
94
+
95
+ /* c8 ignore next 13 -- JSDoc block incorrectly counted as coverable by c8 */
96
+ /**
97
+ * @param {object|Function} [objOrCb] - Seed object to start with, or
98
+ * callback if no seed provided
99
+ * @param {Function|any[]} [cbOrUsePropertySets] - Callback to be executed
100
+ * on this transformer but with a context nested within the newly created
101
+ * object, or array of property set names if first arg was an object
102
+ * @param {any[]|object} [usePropertySetsOrPropSets] - Array of string
103
+ * property set names to copy onto the new object, or propSets if second
104
+ * arg was a callback
105
+ * @param {object} [propSets] - An object of key-value pairs to copy onto
106
+ * the new object
107
+ * @returns {JSONJoiningTransformer}
108
+ */
109
+ object (objOrCb, cbOrUsePropertySets, usePropertySetsOrPropSets, propSets) {
110
+ // eslint-disable-next-line unicorn/no-this-assignment -- Temporary
111
+ const that = this;
112
+ // Todo: Conditionally add as JHTML-based jml (and in subsequent methods
113
+ // as well)
114
+ const tempObj = this._obj;
115
+
116
+ // Determine if first arg is a seed object or callback
117
+ let obj;
118
+ let cb;
119
+ let usePropertySets;
120
+ let propSetsToUse;
121
+
122
+ if (typeof objOrCb === 'function') {
123
+ // No seed object: object(cb, usePropertySets, propSets)
124
+ obj = {};
125
+ cb = objOrCb;
126
+ usePropertySets = /** @type {any[]} */ (cbOrUsePropertySets);
127
+ propSetsToUse = /** @type {object} */ (usePropertySetsOrPropSets);
128
+ } else {
129
+ // Seed object provided: object(obj, cb, usePropertySets, propSets)
130
+ // Clone seed object to avoid mutating the original
131
+ obj = objOrCb ? {...objOrCb} : {};
132
+ cb = /** @type {Function} */ (cbOrUsePropertySets);
133
+ usePropertySets = /** @type {any[]} */ (usePropertySetsOrPropSets);
134
+ propSetsToUse = propSets;
135
+ }
136
+
137
+ if (usePropertySets !== undefined) {
138
+ const merged = usePropertySets.reduce((o, psName) => {
139
+ return that._usePropertySets(o, psName); // Todo: Put in right scope
140
+ }, {});
141
+ Object.assign(obj, merged);
142
+ }
143
+ if (propSetsToUse !== undefined) {
144
+ Object.assign(obj, propSetsToUse);
145
+ }
146
+
147
+ /** @type {any} */
148
+ const oldObjPropState = this._objPropState;
149
+ this._objPropState = true;
150
+ this._obj = obj; // Set current object so propValue() works
151
+ // We pass the object, but user should usually use other methods
152
+ if (cb) {
153
+ cb.call(this, obj);
154
+ }
155
+ // Append after callback so object has all properties, but before
156
+ // restoring tempObj
157
+ this._obj = tempObj;
158
+ this.append(obj);
159
+ this._objPropState = oldObjPropState;
160
+ return this;
161
+ }
162
+
163
+ /**
164
+ * Creates a new array and executes a callback in its context.
165
+ * @param {any[]|Function} [arrOrCb] - Seed array to start with, or callback
166
+ * if no seed provided
167
+ * @param {Function} [cb] - Callback function (if first arg was a seed array)
168
+ * @returns {JSONJoiningTransformer}
169
+ */
170
+ array (arrOrCb, cb) {
171
+ const tempObj = this._obj;
172
+
173
+ // Determine if first arg is a seed array or callback
174
+ /** @type {any[]} */
175
+ let arr;
176
+ let callback;
177
+
178
+ if (typeof arrOrCb === 'function') {
179
+ // No seed array: array(cb)
180
+ arr = [];
181
+ callback = arrOrCb;
182
+ } else {
183
+ // Seed array provided: array(arr, cb)
184
+ // Clone seed array to avoid mutating the original
185
+ arr = arrOrCb ? [...arrOrCb] : [];
186
+ callback = cb;
187
+ }
188
+
189
+ this._obj = arr; // Set current array so append() works
190
+ // We pass the array, but user should usually use other methods
191
+ if (callback) {
192
+ callback.call(this, arr);
193
+ }
194
+ // Append after callback so array has all items, but before
195
+ // restoring tempObj
196
+ this._obj = tempObj;
197
+ this.append(arr); // Todo: set current position and deal with children
198
+ return this;
199
+ }
200
+
201
+ /**
202
+ * Appends a string value.
203
+ * @param {string} str - String value
204
+ * @param {Function} [cb] - Callback function (unused)
205
+ * @returns {JSONJoiningTransformer}
206
+ */
207
+ string (str, cb) {
208
+ this._requireSameChildren('json', 'string');
209
+ this.append(str);
210
+ return this;
211
+ }
212
+
213
+ /**
214
+ * Appends a number value.
215
+ * @param {number} num - Number value
216
+ * @returns {JSONJoiningTransformer}
217
+ */
218
+ number (num) {
219
+ this.append(num);
220
+ return this;
221
+ }
222
+
223
+ /**
224
+ * Appends a boolean value.
225
+ * @param {boolean} bool - Boolean value
226
+ * @returns {JSONJoiningTransformer}
227
+ */
228
+ boolean (bool) {
229
+ this.append(bool);
230
+ return this;
231
+ }
232
+
233
+ /**
234
+ * Appends a null value.
235
+ * @returns {JSONJoiningTransformer}
236
+ */
237
+ null () {
238
+ this.append(null);
239
+ return this;
240
+ }
241
+
242
+ /**
243
+ * Appends an undefined value (JavaScript mode only).
244
+ * @returns {JSONJoiningTransformer}
245
+ */
246
+ undefined () {
247
+ if (this._cfg && /** @type {any} */ (this._cfg).mode !== 'JavaScript') {
248
+ throw new Error(
249
+ 'undefined is not allowed unless added in JavaScript mode'
250
+ );
251
+ }
252
+ this.append(undefined);
253
+ return this;
254
+ }
255
+
256
+ /**
257
+ * Appends a non-finite number (JavaScript mode only).
258
+ * @param {number} num - Non-finite number (NaN, Infinity, -Infinity)
259
+ * @returns {JSONJoiningTransformer}
260
+ */
261
+ nonfiniteNumber (num) {
262
+ if (this._cfg && /** @type {any} */ (this._cfg).mode !== 'JavaScript') {
263
+ throw new Error(
264
+ 'Non-finite numbers are not allowed unless added in JavaScript mode'
265
+ );
266
+ }
267
+ this.append(num);
268
+ return this;
269
+ }
270
+
271
+ /**
272
+ * Appends a function value (JavaScript mode only).
273
+ * @param {Function} func - Function to append
274
+ * @returns {JSONJoiningTransformer}
275
+ */
276
+ function (func) {
277
+ if (this._cfg && /** @type {any} */ (this._cfg).mode !== 'JavaScript') {
278
+ throw new Error(
279
+ 'function is not allowed unless added in JavaScript mode'
280
+ );
281
+ }
282
+ this.append(func);
283
+ return this;
284
+ }
285
+
286
+ /**
287
+ * Build a Jamilih-style element JSON array and append to current container.
288
+ * Result form: ['tag', {attr: 'val'}, child1, child2, ...]
289
+ * Helpers: dataset -> data-*; $a -> ordered attributes.
290
+ * Supported signatures mirror StringJoiningTransformer.element.
291
+ * @param {string|Element|object} elName - Element name or Element-like
292
+ * @param {object|any[]|Function} [atts] - Attributes object or children or cb
293
+ * @param {any[]|Function} [childNodes] - Child nodes array or callback
294
+ * @param {Function} [cb] - Callback for building children/attributes
295
+ * @returns {JSONJoiningTransformer}
296
+ */
297
+ element (elName, atts, childNodes, cb) {
298
+ this._requireSameChildren('json', 'element');
299
+ // Normalize arguments similarly to StringJoiningTransformer.element
300
+ if (Array.isArray(atts)) {
301
+ cb = /** @type {Function} */ (childNodes);
302
+ childNodes = atts;
303
+ atts = {};
304
+ } else if (typeof atts === 'function') {
305
+ cb = /** @type {Function} */ (atts);
306
+ childNodes = [];
307
+ atts = {};
308
+ }
309
+ if (typeof childNodes === 'function') {
310
+ cb = /** @type {Function} */ (childNodes);
311
+ childNodes = [];
312
+ }
313
+
314
+ // Element-like object (DOM Element) -> extract attributes
315
+ if (typeof elName === 'object' && elName && 'attributes' in elName) {
316
+ /** @type {Record<string, any>} */
317
+ const objAtts = {};
318
+ // @ts-ignore - treat elName as Element-like
319
+ [...elName.attributes].forEach((att) => {
320
+ objAtts[att.name] = att.value;
321
+ });
322
+ atts = Object.assign(objAtts, atts);
323
+ // @ts-ignore
324
+ elName = /** @type {any} */ (elName).nodeName;
325
+ }
326
+
327
+ /** @type {Record<string, any>} */
328
+ let attsObj = /** @type {any} */ (atts) || {};
329
+ /** @type {any[]} */
330
+ const jmlChildren = [];
331
+
332
+ // Preprocess special attribute helpers present directly on attsObj
333
+ if (attsObj.dataset && typeof attsObj.dataset === 'object' &&
334
+ !Array.isArray(attsObj.dataset)
335
+ ) {
336
+ const ds = attsObj.dataset;
337
+ for (const k in ds) {
338
+ if (Object.hasOwn(ds, k)) {
339
+ const dashed = k.replaceAll(camelCase, _makeDatasetAttribute);
340
+ attsObj['data-' + dashed] = ds[k];
341
+ }
342
+ }
343
+ delete attsObj.dataset;
344
+ }
345
+ if (Array.isArray(attsObj.$a)) {
346
+ attsObj.$a.forEach((pair) => {
347
+ if (Array.isArray(pair) && pair.length > 1) {
348
+ attsObj[pair[0]] = pair[1];
349
+ }
350
+ });
351
+ delete attsObj.$a;
352
+ }
353
+
354
+ // If children provided as array, copy (may be primitives or nested JML)
355
+ if (Array.isArray(childNodes) && childNodes.length) {
356
+ jmlChildren.push(...childNodes);
357
+ }
358
+
359
+ // Callback-driven building (attribute/text/nested element mutate stack)
360
+ if (cb) {
361
+ // Push current state onto a stack
362
+ this._elementStack.push({attsObj, jmlChildren});
363
+ cb.call(this);
364
+ const state = /** @type {any} */ (this._elementStack.pop());
365
+ ({attsObj} = state);
366
+ // Children may have been mutated by nested element()/text();
367
+ // already in jmlChildren
368
+ }
369
+
370
+ // Build Jamilih array
371
+ /** @type {any[]} */
372
+ const jmlEl = [elName];
373
+ if (Object.keys(attsObj).length) {
374
+ jmlEl.push(attsObj);
375
+ }
376
+ jmlEl.push(...jmlChildren);
377
+
378
+ // If inside a parent element, append as its child; otherwise append to root
379
+ if (this._elementStack.length) {
380
+ const top = /** @type {any} */ (this._elementStack.at(-1));
381
+ top.jmlChildren.push(jmlEl);
382
+ } else {
383
+ this.append(jmlEl);
384
+ }
385
+ return this;
386
+ }
387
+
388
+ /**
389
+ * Adds/updates an attribute for the most recently open element built via
390
+ * a callback-driven element(). When not in an element callback context,
391
+ * throws. Supports the same dataset/$a helpers as string joiner.
392
+ * @param {string} name - Attribute name (or helper: dataset, $a)
393
+ * @param {string|object|any[]} val - Attribute value or helper object
394
+ * @returns {JSONJoiningTransformer}
395
+ */
396
+ attribute (name, val) {
397
+ if (!this._elementStack.length) {
398
+ // No-op outside an element() callback (JSON joiner semantics)
399
+ return this;
400
+ }
401
+ const top = /** @type {any} */ (this._elementStack.at(-1));
402
+ const {attsObj} = top;
403
+ if (name === 'dataset' && val && typeof val === 'object' &&
404
+ !Array.isArray(val)
405
+ ) {
406
+ for (const k in val) {
407
+ if (Object.hasOwn(val, k)) {
408
+ const dashed = k.replaceAll(camelCase, _makeDatasetAttribute);
409
+ attsObj['data-' + dashed] = (/** @type {any} */ (val))[k];
410
+ }
411
+ }
412
+ return this;
413
+ }
414
+ if (name === '$a' && Array.isArray(val)) {
415
+ val.forEach((pair) => {
416
+ if (Array.isArray(pair) && pair.length > 1) {
417
+ attsObj[pair[0]] = pair[1];
418
+ }
419
+ });
420
+ return this;
421
+ }
422
+ attsObj[name] = val;
423
+ return this;
424
+ }
425
+
426
+ /**
427
+ * Adds a text node (string) as a child within the current element() callback
428
+ * context. Outside of an element callback, simply appends the text to the
429
+ * current array/object like string().
430
+ * @param {string} txt - Text content
431
+ * @returns {JSONJoiningTransformer}
432
+ */
433
+ text (txt) {
434
+ if (this._elementStack.length) {
435
+ const top = /** @type {any} */ (this._elementStack.at(-1));
436
+ const {jmlChildren} = top;
437
+ jmlChildren.push(txt);
438
+ return this;
439
+ }
440
+ // No-op outside element context in JSON joiner
441
+ return this;
442
+ }
443
+
444
+ /**
445
+ * Appends plain text as a string.
446
+ * @param {string} str - Plain text string
447
+ * @returns {JSONJoiningTransformer}
448
+ */
449
+ plainText (str) {
450
+ this.string(str);
451
+ return this;
452
+ }
453
+
454
+ /**
455
+ * Helper method to use property sets (to be implemented).
456
+ * @param {object} obj - Object to apply property set to
457
+ * @param {string} psName - Property set name
458
+ * @returns {object}
459
+ */
460
+ _usePropertySets (obj, psName) {
461
+ // Merge the named property set (if present) into the provided object
462
+ if (this && /** @type {any} */ (this).propertySets &&
463
+ /** @type {any} */ (this).propertySets[psName]
464
+ ) {
465
+ return Object.assign(obj, /** @type {any} */ (this).propertySets[psName]);
466
+ }
467
+ return obj;
468
+ }
469
+ }
470
+
471
+ export default JSONJoiningTransformer;
472
+