jtlt 0.2.0 → 0.3.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 (50) hide show
  1. package/CHANGES.md +18 -0
  2. package/README.md +16 -87
  3. package/demo/calltemplate-params-demo.js +138 -0
  4. package/demo/index.html +31 -0
  5. package/demo/index.js +30 -0
  6. package/demo/xpath2-placeholder.js +1 -0
  7. package/dist/AbstractJoiningTransformer.d.ts +83 -9
  8. package/dist/AbstractJoiningTransformer.d.ts.map +1 -1
  9. package/dist/DOMJoiningTransformer.d.ts +85 -25
  10. package/dist/DOMJoiningTransformer.d.ts.map +1 -1
  11. package/dist/JSONJoiningTransformer.d.ts +159 -51
  12. package/dist/JSONJoiningTransformer.d.ts.map +1 -1
  13. package/dist/JSONPathTransformer.d.ts +37 -38
  14. package/dist/JSONPathTransformer.d.ts.map +1 -1
  15. package/dist/JSONPathTransformerContext.d.ts +247 -121
  16. package/dist/JSONPathTransformerContext.d.ts.map +1 -1
  17. package/dist/StringJoiningTransformer.d.ts +132 -41
  18. package/dist/StringJoiningTransformer.d.ts.map +1 -1
  19. package/dist/XPathTransformer.d.ts +35 -20
  20. package/dist/XPathTransformer.d.ts.map +1 -1
  21. package/dist/XPathTransformerContext.d.ts +191 -99
  22. package/dist/XPathTransformerContext.d.ts.map +1 -1
  23. package/dist/index-browser.d.ts +4 -0
  24. package/dist/index-browser.d.ts.map +1 -0
  25. package/dist/index-node.d.ts +4 -0
  26. package/dist/index-node.d.ts.map +1 -0
  27. package/dist/index.d.ts +330 -57
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/types.d.ts +204 -0
  30. package/dist/types.d.ts.map +1 -0
  31. package/docs/API.expanded.md +167 -2
  32. package/docs/API.md +91 -1
  33. package/docs/TO-DO.md +144 -0
  34. package/docs/calltemplate-params.md +251 -0
  35. package/eslint.config.js +9 -5
  36. package/package.json +13 -7
  37. package/pnpm-workspace.yaml +1 -0
  38. package/src/AbstractJoiningTransformer.js +54 -15
  39. package/src/DOMJoiningTransformer.js +275 -28
  40. package/src/JSONJoiningTransformer.js +351 -70
  41. package/src/JSONPathTransformer.js +48 -30
  42. package/src/JSONPathTransformerContext.js +308 -104
  43. package/src/StringJoiningTransformer.js +311 -57
  44. package/src/XPathTransformer.js +27 -12
  45. package/src/XPathTransformerContext.js +467 -89
  46. package/src/index-browser.js +5 -0
  47. package/src/index-node.js +7 -0
  48. package/src/index.js +498 -97
  49. package/typings/xpath2-js.d.ts +40 -1
  50. package/src/types/xpath2-js.d.ts +0 -2
@@ -10,6 +10,43 @@ function _makeDatasetAttribute (n0) {
10
10
  return n0.charAt(0) + '-' + n0.charAt(1).toLowerCase();
11
11
  }
12
12
 
13
+ /**
14
+ * @callback ObjectCallback
15
+ * @this {JSONJoiningTransformer}
16
+ * @param {Record<string, unknown>} obj
17
+ * @returns {void}
18
+ */
19
+ /**
20
+ * @callback ArrayCallback
21
+ * @this {JSONJoiningTransformer}
22
+ * @param {any[]} arr
23
+ * @returns {void}
24
+ */
25
+ /**
26
+ * @template [T = "json"]
27
+ * @callback SimpleCallback
28
+ * @this {T extends "json" ? JSONJoiningTransformer :
29
+ * T extends "string" ? import('./StringJoiningTransformer.js').default
30
+ * : import('./DOMJoiningTransformer.js').default}
31
+ * @returns {void}
32
+ */
33
+
34
+ /**
35
+ * Attributes object for element() allowing standard string attributes
36
+ * plus special helpers: dataset (object) and $a (ordered attribute array).
37
+ * @typedef {Record<string, unknown> & {
38
+ * dataset?: Record<string, string>,
39
+ * $a?: Array<[string, string]>
40
+ * }} ElementAttributes
41
+ */
42
+
43
+ /**
44
+ * @typedef {{
45
+ * attsObj: Record<string, unknown>,
46
+ * jmlChildren: unknown[]
47
+ * }} ElementInfo
48
+ */
49
+
13
50
  /**
14
51
  * JSON-based joining transformer for building JSON/JavaScript objects.
15
52
  *
@@ -17,27 +54,35 @@ function _makeDatasetAttribute (n0) {
17
54
  * append() will push to arrays or shallow-merge into objects; string/number/
18
55
  * boolean/null add primitives accordingly. It does not perform HTML escaping
19
56
  * or string serialization; it builds real JS values.
57
+ * @extends {AbstractJoiningTransformer<"json">}
20
58
  */
21
59
  class JSONJoiningTransformer extends AbstractJoiningTransformer {
22
60
  /**
23
- * @param {any[]|object} [o] - Initial object or array
24
- * @param {object} [cfg] - Configuration object
61
+ * @param {any[]|Record<string, unknown>} [o] - Initial object or array
62
+ * @param {import('./AbstractJoiningTransformer.js').
63
+ * JSONJoiningTransformerConfig} [cfg] - Configuration object
25
64
  */
26
65
  constructor (o, cfg) {
27
66
  super(cfg);
28
- /** @type {any[]|object} */
67
+ /** @type {any[]|Record<string, unknown>} */
29
68
  this._obj = o || [];
30
69
  /** @type {boolean | undefined} */
31
70
  this._objPropState = undefined;
32
71
  /** @type {boolean | undefined} */
33
72
  this._arrItemState = undefined;
34
- /** @type {{attsObj: Record<string, any>, jmlChildren: any[]}[]} */
73
+ /** @type {ElementInfo[]} */
35
74
  this._elementStack = [];
75
+ /** @type {Record<string, unknown>} */
76
+ this.propertySets = {};
77
+ /** @type {any[]} */
78
+ this._docs = [];
79
+ /** @type {Array<{href: string, document: any, format?: string}>} */
80
+ this._resultDocuments = [];
36
81
  }
37
82
 
38
83
  /**
39
84
  * Directly appends an item to the internal array without checks.
40
- * @param {*} item - Item to append
85
+ * @param {any} item - Item to append
41
86
  * @returns {void}
42
87
  */
43
88
  rawAppend (item) {
@@ -46,7 +91,7 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
46
91
 
47
92
  /**
48
93
  * Appends an item to the current object or array.
49
- * @param {*} item - Item to append
94
+ * @param {any} item - Item to append
50
95
  * @returns {JSONJoiningTransformer}
51
96
  */
52
97
  append (item) {
@@ -66,11 +111,16 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
66
111
  * Gets the current object or array. If unwrapSingleResult config option is
67
112
  * enabled and the root array contains exactly one element, returns that
68
113
  * element directly (unwrapped).
69
- * @returns {any[]|object|any}
114
+ * @returns {any[]|Record<string, unknown>|any}
70
115
  */
71
116
  get () {
117
+ // If exposeDocuments is set, return the array of documents
118
+ if (this._cfg.exposeDocuments) {
119
+ return this._docs;
120
+ }
121
+ // Removed this._doc logic; use this._docs only
72
122
  // Unwrap single-element arrays at the root level if configured
73
- if (this._cfg && /** @type {any} */ (this._cfg).unwrapSingleResult &&
123
+ if (this._cfg.unwrapSingleResult &&
74
124
  Array.isArray(this._obj) && this._obj.length === 1) {
75
125
  return this._obj[0];
76
126
  }
@@ -80,7 +130,7 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
80
130
  /**
81
131
  * Sets a property value on the current object.
82
132
  * @param {string} prop - Property name
83
- * @param {*} val - Property value
133
+ * @param {any} val - Property value
84
134
  * @returns {void}
85
135
  */
86
136
  propValue (prop, val) {
@@ -94,16 +144,12 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
94
144
 
95
145
  /* c8 ignore next 13 -- JSDoc block incorrectly counted as coverable by c8 */
96
146
  /**
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
147
+ * @param {Record<string, unknown>|ObjectCallback} [objOrCb]
148
+ * Seed object or callback.
149
+ * @param {ObjectCallback|any[]} [cbOrUsePropertySets] Callback or sets.
150
+ * @param {any[]|Record<string, unknown>} [usePropertySetsOrPropSets]
151
+ * Sets or prop sets.
152
+ * @param {Record<string, unknown>} [propSets] Key-value pairs to add.
107
153
  * @returns {JSONJoiningTransformer}
108
154
  */
109
155
  object (objOrCb, cbOrUsePropertySets, usePropertySetsOrPropSets, propSets) {
@@ -124,12 +170,14 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
124
170
  obj = {};
125
171
  cb = objOrCb;
126
172
  usePropertySets = /** @type {any[]} */ (cbOrUsePropertySets);
127
- propSetsToUse = /** @type {object} */ (usePropertySetsOrPropSets);
173
+ propSetsToUse = /** @type {Record<string, unknown>} */ (
174
+ usePropertySetsOrPropSets
175
+ );
128
176
  } else {
129
177
  // Seed object provided: object(obj, cb, usePropertySets, propSets)
130
178
  // Clone seed object to avoid mutating the original
131
179
  obj = objOrCb ? {...objOrCb} : {};
132
- cb = /** @type {Function} */ (cbOrUsePropertySets);
180
+ cb = /** @type {ObjectCallback} */ (cbOrUsePropertySets);
133
181
  usePropertySets = /** @type {any[]} */ (usePropertySetsOrPropSets);
134
182
  propSetsToUse = propSets;
135
183
  }
@@ -144,7 +192,6 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
144
192
  Object.assign(obj, propSetsToUse);
145
193
  }
146
194
 
147
- /** @type {any} */
148
195
  const oldObjPropState = this._objPropState;
149
196
  this._objPropState = true;
150
197
  this._obj = obj; // Set current object so propValue() works
@@ -162,9 +209,8 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
162
209
 
163
210
  /**
164
211
  * 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)
212
+ * @param {any[]|ArrayCallback} [arrOrCb] Seed array or callback.
213
+ * @param {ArrayCallback} [cb] Callback when first arg was array.
168
214
  * @returns {JSONJoiningTransformer}
169
215
  */
170
216
  array (arrOrCb, cb) {
@@ -200,8 +246,8 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
200
246
 
201
247
  /**
202
248
  * Appends a string value.
203
- * @param {string} str - String value
204
- * @param {Function} [cb] - Callback function (unused)
249
+ * @param {string} str String value.
250
+ * @param {SimpleCallback} [cb] Unused callback.
205
251
  * @returns {JSONJoiningTransformer}
206
252
  */
207
253
  string (str, cb) {
@@ -244,7 +290,7 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
244
290
  * @returns {JSONJoiningTransformer}
245
291
  */
246
292
  undefined () {
247
- if (this._cfg && /** @type {any} */ (this._cfg).mode !== 'JavaScript') {
293
+ if (this._cfg.mode !== 'JavaScript') {
248
294
  throw new Error(
249
295
  'undefined is not allowed unless added in JavaScript mode'
250
296
  );
@@ -259,7 +305,7 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
259
305
  * @returns {JSONJoiningTransformer}
260
306
  */
261
307
  nonfiniteNumber (num) {
262
- if (this._cfg && /** @type {any} */ (this._cfg).mode !== 'JavaScript') {
308
+ if (this._cfg.mode !== 'JavaScript') {
263
309
  throw new Error(
264
310
  'Non-finite numbers are not allowed unless added in JavaScript mode'
265
311
  );
@@ -270,11 +316,11 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
270
316
 
271
317
  /**
272
318
  * Appends a function value (JavaScript mode only).
273
- * @param {Function} func - Function to append
319
+ * @param {(...args: any[]) => any} func Function to append.
274
320
  * @returns {JSONJoiningTransformer}
275
321
  */
276
322
  function (func) {
277
- if (this._cfg && /** @type {any} */ (this._cfg).mode !== 'JavaScript') {
323
+ if (this._cfg.mode !== 'JavaScript') {
278
324
  throw new Error(
279
325
  'function is not allowed unless added in JavaScript mode'
280
326
  );
@@ -284,49 +330,67 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
284
330
  }
285
331
 
286
332
  /**
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
333
+ * @param {import('./StringJoiningTransformer.js').OutputConfig} cfg
334
+ * @returns {JSONJoiningTransformer}
335
+ */
336
+ output (cfg) {
337
+ // We wait until first element is set in `element()` to add
338
+ // XML declaration and DOCTYPE as latter depends on root element
339
+ this._outputConfig = cfg;
340
+
341
+ // Use for file extension if making downloadable?
342
+ this.mediaType = cfg.mediaType;
343
+ return this;
344
+ }
345
+
346
+ /**
347
+ * Build a Jamilih-style element JSON array and append to current container.
348
+ * Result form: ['tag', {attr: 'val'}, child1, child2, ...]
349
+ * Helpers: dataset -> data-*; $a -> ordered attributes.
350
+ * Supported signatures mirror StringJoiningTransformer.element.
351
+ * @param {string|Element} elName Element name or Element-like.
352
+ * @param {ElementAttributes|any[]|SimpleCallback} [atts]
353
+ * Attrs, children, or cb.
354
+ * @param {any[]|SimpleCallback} [childNodes] Children or cb.
355
+ * @param {SimpleCallback} [cb] Builder callback.
295
356
  * @returns {JSONJoiningTransformer}
296
357
  */
297
358
  element (elName, atts, childNodes, cb) {
298
359
  this._requireSameChildren('json', 'element');
360
+ const isRoot = !this.root;
361
+ if (isRoot) {
362
+ this.root = elName;
363
+ }
299
364
  // Normalize arguments similarly to StringJoiningTransformer.element
300
365
  if (Array.isArray(atts)) {
301
- cb = /** @type {Function} */ (childNodes);
366
+ cb = /** @type {SimpleCallback} */ (childNodes);
302
367
  childNodes = atts;
303
368
  atts = {};
304
369
  } else if (typeof atts === 'function') {
305
- cb = /** @type {Function} */ (atts);
370
+ cb = /** @type {SimpleCallback} */ (atts);
306
371
  childNodes = [];
307
372
  atts = {};
308
373
  }
309
374
  if (typeof childNodes === 'function') {
310
- cb = /** @type {Function} */ (childNodes);
375
+ cb = /** @type {SimpleCallback} */ (childNodes);
311
376
  childNodes = [];
312
377
  }
313
378
 
379
+ const elementName = typeof elName === 'string'
380
+ ? elName
381
+ : elName.localName;
314
382
  // Element-like object (DOM Element) -> extract attributes
315
383
  if (typeof elName === 'object' && elName && 'attributes' in elName) {
316
- /** @type {Record<string, any>} */
384
+ /** @type {Record<string, string>} */
317
385
  const objAtts = {};
318
- // @ts-ignore - treat elName as Element-like
319
386
  [...elName.attributes].forEach((att) => {
320
387
  objAtts[att.name] = att.value;
321
388
  });
322
389
  atts = Object.assign(objAtts, atts);
323
- // @ts-ignore
324
- elName = /** @type {any} */ (elName).nodeName;
325
390
  }
326
391
 
327
- /** @type {Record<string, any>} */
328
- let attsObj = /** @type {any} */ (atts) || {};
329
- /** @type {any[]} */
392
+ let attsObj = atts || {};
393
+ /** @type {import('jamilih').JamilihChildren} */
330
394
  const jmlChildren = [];
331
395
 
332
396
  // Preprocess special attribute helpers present directly on attsObj
@@ -343,9 +407,9 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
343
407
  delete attsObj.dataset;
344
408
  }
345
409
  if (Array.isArray(attsObj.$a)) {
346
- attsObj.$a.forEach((pair) => {
410
+ (/** @type {unknown[][]} */ (attsObj.$a)).forEach((pair) => {
347
411
  if (Array.isArray(pair) && pair.length > 1) {
348
- attsObj[pair[0]] = pair[1];
412
+ attsObj[String(pair[0])] = pair[1];
349
413
  }
350
414
  });
351
415
  delete attsObj.$a;
@@ -361,7 +425,7 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
361
425
  // Push current state onto a stack
362
426
  this._elementStack.push({attsObj, jmlChildren});
363
427
  cb.call(this);
364
- const state = /** @type {any} */ (this._elementStack.pop());
428
+ const state = /** @type {ElementInfo} */ (this._elementStack.pop());
365
429
  ({attsObj} = state);
366
430
  // Children may have been mutated by nested element()/text();
367
431
  // already in jmlChildren
@@ -369,15 +433,56 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
369
433
 
370
434
  // Build Jamilih array
371
435
  /** @type {any[]} */
372
- const jmlEl = [elName];
436
+ const jmlEl = [elementName];
373
437
  if (Object.keys(attsObj).length) {
374
438
  jmlEl.push(attsObj);
375
439
  }
376
440
  jmlEl.push(...jmlChildren);
377
441
 
442
+ if (isRoot) {
443
+ // todo: indent, cdataSectionElements
444
+ const {
445
+ omitXmlDeclaration, doctypePublic, doctypeSystem, method
446
+ } = this._outputConfig ?? {};
447
+
448
+ const dtd = {$DOCTYPE: {
449
+ name: elementName,
450
+ publicId: doctypePublic ?? null, // Public ID (optional)
451
+ systemId: doctypeSystem ?? null // System ID (optional)
452
+ }};
453
+
454
+ let xmlDeclaration;
455
+ /* c8 ignore start -- third OR condition short-circuits */
456
+ if (!omitXmlDeclaration && (
457
+ method === 'xml' || method === 'xhtml' || omitXmlDeclaration === false)
458
+ ) {
459
+ const {version, encoding, standalone} = this._outputConfig ?? {};
460
+
461
+ xmlDeclaration = {
462
+ version,
463
+ encoding,
464
+ standalone
465
+ };
466
+ }
467
+ /* c8 ignore stop */
468
+
469
+ const doc = {$document: {
470
+ ...(xmlDeclaration ? {xmlDeclaration} : {}),
471
+ childNodes: [
472
+ ...(method === 'xml' || method === 'xhtml' ? [dtd] : []),
473
+ jmlEl
474
+ ]
475
+ }};
476
+
477
+ // Removed this._doc; use this._docs only
478
+ if (this._cfg.exposeDocuments) {
479
+ this._docs.push(doc);
480
+ }
481
+ }
482
+
378
483
  // If inside a parent element, append as its child; otherwise append to root
379
484
  if (this._elementStack.length) {
380
- const top = /** @type {any} */ (this._elementStack.at(-1));
485
+ const top = /** @type {ElementInfo} */ (this._elementStack.at(-1));
381
486
  top.jmlChildren.push(jmlEl);
382
487
  } else {
383
488
  this.append(jmlEl);
@@ -390,7 +495,8 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
390
495
  * a callback-driven element(). When not in an element callback context,
391
496
  * throws. Supports the same dataset/$a helpers as string joiner.
392
497
  * @param {string} name - Attribute name (or helper: dataset, $a)
393
- * @param {string|object|any[]} val - Attribute value or helper object
498
+ * @param {string|Record<string, unknown>|unknown[]} val
499
+ * Attribute value or helper object
394
500
  * @returns {JSONJoiningTransformer}
395
501
  */
396
502
  attribute (name, val) {
@@ -398,23 +504,24 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
398
504
  // No-op outside an element() callback (JSON joiner semantics)
399
505
  return this;
400
506
  }
401
- const top = /** @type {any} */ (this._elementStack.at(-1));
507
+ const top = /** @type {ElementInfo} */ (this._elementStack.at(-1));
402
508
  const {attsObj} = top;
403
509
  if (name === 'dataset' && val && typeof val === 'object' &&
404
510
  !Array.isArray(val)
405
511
  ) {
406
- for (const k in val) {
407
- if (Object.hasOwn(val, k)) {
512
+ const datasetObj = /** @type {Record<string, unknown>} */ (val);
513
+ for (const k in datasetObj) {
514
+ if (Object.hasOwn(datasetObj, k)) {
408
515
  const dashed = k.replaceAll(camelCase, _makeDatasetAttribute);
409
- attsObj['data-' + dashed] = (/** @type {any} */ (val))[k];
516
+ attsObj['data-' + dashed] = datasetObj[k];
410
517
  }
411
518
  }
412
519
  return this;
413
520
  }
414
521
  if (name === '$a' && Array.isArray(val)) {
415
- val.forEach((pair) => {
522
+ (/** @type {unknown[][]} */ (val)).forEach((pair) => {
416
523
  if (Array.isArray(pair) && pair.length > 1) {
417
- attsObj[pair[0]] = pair[1];
524
+ attsObj[String(pair[0])] = pair[1];
418
525
  }
419
526
  });
420
527
  return this;
@@ -423,6 +530,43 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
423
530
  return this;
424
531
  }
425
532
 
533
+ /**
534
+ * Adds a comment node (string) as a child within the current
535
+ * element() callback context. Outside of an element callback,
536
+ * simply appends the comment to the current array/object like string().
537
+ * @param {string} txt - Comment content
538
+ * @returns {JSONJoiningTransformer}
539
+ */
540
+ comment (txt) {
541
+ if (this._elementStack.length) {
542
+ const top = /** @type {ElementInfo} */ (this._elementStack.at(-1));
543
+ const {jmlChildren} = top;
544
+ jmlChildren.push(['!', txt]);
545
+ return this;
546
+ }
547
+ // No-op outside element context in JSON joiner
548
+ return this;
549
+ }
550
+
551
+ /**
552
+ * Adds a comment node (string) as a child within the current
553
+ * element() callback context. Outside of an element callback,
554
+ * simply appends the comment to the current array/object like string().
555
+ * @param {string} target - processing instruction content
556
+ * @param {string} data - processing instruction content
557
+ * @returns {JSONJoiningTransformer}
558
+ */
559
+ processingInstruction (target, data) {
560
+ if (this._elementStack.length) {
561
+ const top = /** @type {ElementInfo} */ (this._elementStack.at(-1));
562
+ const {jmlChildren} = top;
563
+ jmlChildren.push(['?', target, data]);
564
+ return this;
565
+ }
566
+ // No-op outside element context in JSON joiner
567
+ return this;
568
+ }
569
+
426
570
  /**
427
571
  * Adds a text node (string) as a child within the current element() callback
428
572
  * context. Outside of an element callback, simply appends the text to the
@@ -432,9 +576,9 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
432
576
  */
433
577
  text (txt) {
434
578
  if (this._elementStack.length) {
435
- const top = /** @type {any} */ (this._elementStack.at(-1));
579
+ const top = /** @type {ElementInfo} */ (this._elementStack.at(-1));
436
580
  const {jmlChildren} = top;
437
- jmlChildren.push(txt);
581
+ jmlChildren.push(['!', txt]);
438
582
  return this;
439
583
  }
440
584
  // No-op outside element context in JSON joiner
@@ -452,17 +596,154 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
452
596
  }
453
597
 
454
598
  /**
455
- * Helper method to use property sets (to be implemented).
456
- * @param {object} obj - Object to apply property set to
599
+ * Creates a new JSON document and executes a callback in its context.
600
+ * Similar to XSLT's xsl:document, this allows templates to generate
601
+ * multiple output documents. The created document is pushed to this._docs
602
+ * and will be included in the result when exposeDocuments is true.
603
+ *
604
+ * @param {(this: JSONJoiningTransformer) => void} cb
605
+ * Callback that builds the document content
606
+ * @param {import('./StringJoiningTransformer.js').OutputConfig} [cfg]
607
+ * Output configuration for the document (encoding, doctype, etc.)
608
+ * @returns {JSONJoiningTransformer}
609
+ */
610
+ document (cb, cfg) {
611
+ // Save current state
612
+ /** @type {any} */
613
+ const oldRoot = this.root;
614
+ /** @type {any} */
615
+ const oldOutputConfig = this._outputConfig;
616
+ const oldObj = this._obj;
617
+ const oldElementStack = this._elementStack;
618
+
619
+ // Reset state for new document
620
+ this.root = undefined;
621
+ /** @type {any} */
622
+ this._outputConfig = cfg;
623
+ this._obj = [];
624
+ this._elementStack = [];
625
+
626
+ // Execute callback to build document content
627
+ cb.call(this);
628
+
629
+ // Restore previous state
630
+ this.root = oldRoot;
631
+ this._outputConfig = oldOutputConfig;
632
+ this._obj = oldObj;
633
+ this._elementStack = oldElementStack;
634
+
635
+ return this;
636
+ }
637
+
638
+ /**
639
+ * Creates a new result document with metadata (href, format).
640
+ * Similar to XSLT's xsl:result-document, this allows templates to generate
641
+ * multiple output documents with associated metadata like URIs. The created
642
+ * document is stored in this._resultDocuments with the provided href.
643
+ *
644
+ * @param {string} href - URI/path for the result document
645
+ * @param {(this: JSONJoiningTransformer) => void} cb
646
+ * Callback that builds the document content
647
+ * @param {import('./StringJoiningTransformer.js').OutputConfig} [cfg]
648
+ * Output configuration for the document (encoding, doctype, format, etc.)
649
+ * @returns {JSONJoiningTransformer}
650
+ */
651
+ resultDocument (href, cb, cfg) {
652
+ // Save current state
653
+ /** @type {any} */
654
+ const oldRoot = this.root;
655
+ /** @type {any} */
656
+ const oldOutputConfig = this._outputConfig;
657
+ const oldObj = this._obj;
658
+ const oldElementStack = this._elementStack;
659
+
660
+ // Reset state for new document
661
+ this.root = undefined;
662
+ /** @type {any} */
663
+ this._outputConfig = cfg;
664
+ this._obj = [];
665
+ this._elementStack = [];
666
+
667
+ // Execute callback to build document content
668
+ cb.call(this);
669
+
670
+ // Get the created document from _docs or construct from current state
671
+ let resultDoc;
672
+ if (this._docs.length > 0) {
673
+ // Document was created with exposeDocuments flag
674
+ resultDoc = this._docs.at(-1);
675
+ } else if (this._outputConfig) {
676
+ // We have output config but document wasn't pushed to _docs
677
+ // Create the document wrapper manually
678
+ const {
679
+ omitXmlDeclaration, doctypePublic, doctypeSystem, method
680
+ } = this._outputConfig;
681
+
682
+ const elementData = Array.isArray(this._obj) && this._obj.length > 0
683
+ ? this._obj[0]
684
+ : this._obj;
685
+
686
+ const elementName = Array.isArray(elementData) ? elementData[0] : 'root';
687
+
688
+ const dtd = {$DOCTYPE: {
689
+ name: elementName,
690
+ publicId: doctypePublic ?? null,
691
+ systemId: doctypeSystem ?? null
692
+ }};
693
+
694
+ let xmlDeclaration;
695
+ /* c8 ignore start -- third OR condition short-circuits */
696
+ if (!omitXmlDeclaration && (
697
+ method === 'xml' || method === 'xhtml' || omitXmlDeclaration === false)
698
+ ) {
699
+ const {version, encoding, standalone} = this._outputConfig;
700
+ xmlDeclaration = {
701
+ version,
702
+ encoding,
703
+ standalone
704
+ };
705
+ }
706
+ /* c8 ignore stop */
707
+
708
+ resultDoc = {$document: {
709
+ ...(xmlDeclaration ? {xmlDeclaration} : {}),
710
+ childNodes: [
711
+ ...(method === 'xml' || method === 'xhtml' ? [dtd] : []),
712
+ elementData
713
+ ]
714
+ }};
715
+ } else {
716
+ // No output config, just use the raw object/array
717
+ resultDoc = this._obj;
718
+ }
719
+
720
+ // Store with metadata, using the output config that was set during callback
721
+ this._resultDocuments.push({
722
+ href,
723
+ document: resultDoc,
724
+ format: this._outputConfig?.method || cfg?.method
725
+ });
726
+
727
+ // Restore previous state
728
+ this.root = oldRoot;
729
+ this._outputConfig = oldOutputConfig;
730
+ this._obj = oldObj;
731
+ this._elementStack = oldElementStack;
732
+
733
+ return this;
734
+ }
735
+
736
+ /**
737
+ * Helper method to use property sets.
738
+ * @param {Record<string, unknown>} obj - Object to which to apply
739
+ * property set
457
740
  * @param {string} psName - Property set name
458
- * @returns {object}
741
+ * @returns {Record<string, unknown>}
459
742
  */
460
743
  _usePropertySets (obj, psName) {
461
744
  // 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]);
745
+ if (this.propertySets[psName]) {
746
+ return Object.assign(obj, this.propertySets[psName]);
466
747
  }
467
748
  return obj;
468
749
  }