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
@@ -2,11 +2,32 @@ import {jml} from 'jamilih';
2
2
  import * as JHTML from 'jhtml';
3
3
  import AbstractJoiningTransformer from './AbstractJoiningTransformer.js';
4
4
 
5
+ /**
6
+ * @typedef {{
7
+ * encoding?: string,
8
+ * indent?: boolean,
9
+ * omitXmlDeclaration?: boolean,
10
+ * doctypePublic?: string,
11
+ * doctypeSystem?: string,
12
+ * cdataSectionElements?: string[]
13
+ * mediaType?: string,
14
+ * version?: string,
15
+ * standalone?: boolean,
16
+ * method?: "xml"|"html"|"text"|"json"|"xhtml"
17
+ * }} OutputConfig
18
+ */
19
+
20
+ /**
21
+ * @callback SimpleCallback
22
+ * @this {StringJoiningTransformer}
23
+ * @returns {void}
24
+ */
25
+
5
26
  const camelCase = /[a-z][A-Z]/gv;
6
27
 
7
28
  /**
8
29
  * Type guard to detect DOM Elements.
9
- * @param {*} item
30
+ * @param {any} item
10
31
  * @returns {item is Element}
11
32
  */
12
33
  function _isElement (item) {
@@ -21,6 +42,15 @@ function _makeDatasetAttribute (n0) {
21
42
  return n0.charAt(0) + '-' + n0.charAt(1).toLowerCase();
22
43
  }
23
44
 
45
+ /**
46
+ * Attributes object for element() allowing standard string attributes
47
+ * plus special helpers: dataset (object) and $a (ordered attribute array).
48
+ * @typedef {Record<string, unknown> & {
49
+ * dataset?: Record<string, string>,
50
+ * $a?: Array<[string, string]>
51
+ * }} ElementAttributes
52
+ */
53
+
24
54
  /**
25
55
  *
26
56
  */
@@ -50,11 +80,13 @@ function _makeDatasetAttribute (n0) {
50
80
  * name mapping rules differ, etc.).
51
81
  * - cfg.preEscapedAttributes: skip escaping attribute values.
52
82
  * - cfg.JHTMLForJSON / cfg.mode: affect how object()/array() serialize.
83
+ * @extends {AbstractJoiningTransformer<"string">}
53
84
  */
54
85
  class StringJoiningTransformer extends AbstractJoiningTransformer {
55
86
  /**
56
87
  * @param {string} s - Initial string
57
- * @param {object} cfg - Configuration object
88
+ * @param {import('./AbstractJoiningTransformer.js').
89
+ * StringJoiningTransformerConfig} [cfg] - Configuration object
58
90
  */
59
91
  constructor (s, cfg) {
60
92
  super(cfg); // Include this in any subclass of AbstractJoiningTransformer
@@ -74,12 +106,18 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
74
106
  this._arr = [];
75
107
  /** @type {string | undefined} */
76
108
  this._strTemp = undefined;
77
- /** @type {Record<string, any>} */
109
+ /** @type {Record<string, unknown>} */
78
110
  this.propertySets = {};
111
+ /** @type {string[]} */
112
+ this._docs = [];
113
+ /** @type {boolean} */
114
+ this._insideDocument = false;
115
+ /** @type {Array<{href: string, document: string, format?: string}>} */
116
+ this._resultDocuments = [];
79
117
  }
80
118
 
81
119
  /**
82
- * @param {string|*} s - String or value to append
120
+ * @param {string|any} s - String or value to append
83
121
  * @returns {StringJoiningTransformer}
84
122
  */
85
123
  append (s) {
@@ -104,15 +142,24 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
104
142
  }
105
143
 
106
144
  /**
107
- * @returns {string}
145
+ * @returns {string|string[]}
108
146
  */
109
147
  get () {
148
+ if (this._cfg.exposeDocuments) {
149
+ // If we built a document outside of document() calls and haven't
150
+ // pushed it yet, push the current string
151
+ if (this.root && !this._insideDocument && this._str &&
152
+ !this._docs.includes(this._str)) {
153
+ this._docs.push(this._str);
154
+ }
155
+ return this._docs;
156
+ }
110
157
  return this._str;
111
158
  }
112
159
 
113
160
  /**
114
161
  * @param {string} prop - Property name
115
- * @param {*} val - Property value
162
+ * @param {any} val - Property value
116
163
  * @returns {StringJoiningTransformer}
117
164
  */
118
165
  propValue (prop, val) {
@@ -127,7 +174,7 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
127
174
 
128
175
  /**
129
176
  * @param {string} prop - Property name
130
- * @param {Function} cb - Callback function
177
+ * @param {(this: StringJoiningTransformer) => void} cb - Callback function
131
178
  * @returns {StringJoiningTransformer}
132
179
  */
133
180
  propOnly (prop, cb) {
@@ -154,10 +201,10 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
154
201
  }
155
202
 
156
203
  /**
157
- * @param {object|Element} obj - Object to serialize
158
- * @param {Function} cb - Callback function
204
+ * @param {Record<string, unknown>|Element} obj - Object to serialize
205
+ * @param {(this: StringJoiningTransformer) => void} cb - Callback function
159
206
  * @param {any[]} [usePropertySets] - Property sets to use
160
- * @param {object} [propSets] - Additional property sets
207
+ * @param {Record<string, unknown>} [propSets] - Additional property sets
161
208
  * @returns {StringJoiningTransformer}
162
209
  */
163
210
  object (obj, cb, usePropertySets, propSets) {
@@ -173,7 +220,7 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
173
220
  /** @type {any} */
174
221
  const oldObj = this._obj;
175
222
  this._obj = _isElement(obj)
176
- ? JHTML.toJSONObject(obj, {mode: this._cfg?.mode})
223
+ ? JHTML.toJSONObject(obj, {mode: this._cfg.mode})
177
224
  : obj || {};
178
225
 
179
226
  // Todo: Allow in this and subsequent JSON methods ability to create
@@ -197,9 +244,9 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
197
244
  // Not ready to serialize yet as still inside another array or object
198
245
  if (oldObjPropState || this._arrItemState) {
199
246
  this.append(this._obj);
200
- } else if (this._cfg && this._cfg.JHTMLForJSON) {
247
+ } else if (this._cfg.JHTMLForJSON) {
201
248
  this.append(JHTML.toJHTMLString(this._obj));
202
- } else if (this._cfg && this._cfg.mode !== 'JavaScript') {
249
+ } else if (this._cfg.mode !== 'JavaScript') {
203
250
  // Allow this method to operate on non-finite numbers and functions
204
251
  const stringifier = new JHTML.Stringifier({mode: 'JavaScript'});
205
252
  this.append(stringifier.walkJSONObject(this._obj));
@@ -212,7 +259,7 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
212
259
 
213
260
  /**
214
261
  * @param {any[]|Element} [arr] - Array to serialize
215
- * @param {Function} [cb] - Callback function
262
+ * @param {(this: StringJoiningTransformer) => void} [cb] - Callback function
216
263
  * @returns {StringJoiningTransformer}
217
264
  */
218
265
  array (arr, cb) {
@@ -223,7 +270,7 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
223
270
  const oldArr = this._arr;
224
271
  // Todo: copy array?
225
272
  this._arr = _isElement(arr)
226
- ? /** @type {any[]} */ (JHTML.toJSONObject(arr, {mode: this._cfg?.mode}))
273
+ ? /** @type {any[]} */ (JHTML.toJSONObject(arr, {mode: this._cfg.mode}))
227
274
  : arr || [];
228
275
 
229
276
  /** @type {any} */
@@ -245,9 +292,9 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
245
292
  this.append(this._arr);
246
293
  /* c8 ignore next 2 -- JHTMLForJSON is a specialized output mode rarely used
247
294
  * in combination with nested array building at the root level. */
248
- } else if (this._cfg && this._cfg.JHTMLForJSON) {
295
+ } else if (this._cfg.JHTMLForJSON) {
249
296
  this.append(JHTML.toJHTMLString(this._arr));
250
- } else if (this._cfg && this._cfg.mode !== 'JavaScript') {
297
+ } else if (this._cfg.mode !== 'JavaScript') {
251
298
  // Allow this method to operate on non-finite numbers and functions
252
299
  const stringifier = new JHTML.Stringifier({mode: 'JavaScript'});
253
300
  this.append(stringifier.walkJSONObject(this._arr));
@@ -259,8 +306,9 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
259
306
  }
260
307
 
261
308
  /**
262
- * @param {string|Element|object} str - String value or element
263
- * @param {Function} [cb] - Callback function
309
+ * @param {string|Element|Record<string, unknown>} str
310
+ * String value or element
311
+ * @param {(this: StringJoiningTransformer) => void} [cb] - Callback function
264
312
  * @returns {StringJoiningTransformer}
265
313
  */
266
314
  string (str, cb) {
@@ -269,7 +317,7 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
269
317
  // If a callback is provided, it composes a nested string value first.
270
318
  if (_isElement(str)) {
271
319
  str = /** @type {any} */ (
272
- JHTML.toJSONObject(str, {mode: this._cfg?.mode})
320
+ JHTML.toJSONObject(str, {mode: this._cfg.mode})
273
321
  );
274
322
  }
275
323
 
@@ -292,14 +340,15 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
292
340
  }
293
341
 
294
342
  /**
295
- * @param {number|Element|object} num - Number value or element
343
+ * @param {number|Element|Record<string, unknown>} num
344
+ * Number value or element
296
345
  * @returns {StringJoiningTransformer}
297
346
  */
298
347
  number (num) {
299
348
  // Appends the number as a string; no localization/formatting is applied.
300
349
  if (_isElement(num)) {
301
350
  num = /** @type {any} */ (
302
- JHTML.toJSONObject(num, {mode: this._cfg?.mode})
351
+ JHTML.toJSONObject(num, {mode: this._cfg.mode})
303
352
  );
304
353
  }
305
354
  this.append(num.toString());
@@ -307,14 +356,15 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
307
356
  }
308
357
 
309
358
  /**
310
- * @param {boolean|Element|object} bool - Boolean value or element
359
+ * @param {boolean|Element|Record<string, unknown>} bool
360
+ * Boolean value or element
311
361
  * @returns {StringJoiningTransformer}
312
362
  */
313
363
  boolean (bool) {
314
364
  // Appends 'true' or 'false'.
315
365
  if (_isElement(bool)) {
316
366
  bool = /** @type {any} */ (
317
- JHTML.toJSONObject(bool, {mode: this._cfg?.mode})
367
+ JHTML.toJSONObject(bool, {mode: this._cfg.mode})
318
368
  );
319
369
  }
320
370
  this.append(bool ? 'true' : 'false');
@@ -335,7 +385,7 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
335
385
  */
336
386
  undefined () {
337
387
  // Appends the literal 'undefined' (only in JavaScript mode).
338
- if (this._cfg && this._cfg.mode !== 'JavaScript') {
388
+ if (this._cfg.mode !== 'JavaScript') {
339
389
  throw new Error(
340
390
  'undefined is not allowed unless added in JavaScript mode'
341
391
  );
@@ -350,14 +400,14 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
350
400
  */
351
401
  nonfiniteNumber (num) {
352
402
  // Appends NaN/Infinity/-Infinity as-is (only in JavaScript mode).
353
- if (this._cfg && this._cfg.mode !== 'JavaScript') {
403
+ if (this._cfg.mode !== 'JavaScript') {
354
404
  throw new Error(
355
405
  'Non-finite numbers are not allowed unless added in JavaScript mode'
356
406
  );
357
407
  }
358
408
  if (_isElement(num)) {
359
409
  num = /** @type {any} */ (
360
- JHTML.toJSONObject(num, {mode: this._cfg?.mode})
410
+ JHTML.toJSONObject(num, {mode: this._cfg.mode})
361
411
  );
362
412
  }
363
413
  this.append(num.toString());
@@ -365,19 +415,19 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
365
415
  }
366
416
 
367
417
  /**
368
- * @param {Function|Element} func - Function to stringify
418
+ * @param {((...args: any[]) => any)|Element} func - Function to stringify
369
419
  * @returns {StringJoiningTransformer}
370
420
  */
371
421
  function (func) {
372
422
  // Appends function source (only in JavaScript mode).
373
- if (this._cfg && this._cfg.mode !== 'JavaScript') {
423
+ if (this._cfg.mode !== 'JavaScript') {
374
424
  throw new Error(
375
425
  'function is not allowed unless added in JavaScript mode'
376
426
  );
377
427
  }
378
428
  if (_isElement(func)) {
379
429
  func = /** @type {any} */ (
380
- JHTML.toJSONObject(func, {mode: this._cfg?.mode})
430
+ JHTML.toJSONObject(func, {mode: this._cfg.mode})
381
431
  );
382
432
  }
383
433
  this.append(func.toString());
@@ -385,13 +435,75 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
385
435
  }
386
436
 
387
437
  /**
388
- * @param {string|object} elName - Element name or element object
389
- * @param {object} [atts] - Element attributes
438
+ * @param {OutputConfig} cfg
439
+ * @returns {StringJoiningTransformer}
440
+ */
441
+ output (cfg) {
442
+ // We wait until first element is set in `element()` to add
443
+ // XML declaration and DOCTYPE as latter depends on root element
444
+ this._outputConfig = cfg;
445
+
446
+ // Use for file extension if making downloadable?
447
+ this.mediaType = cfg.mediaType;
448
+ return this;
449
+ }
450
+
451
+ /**
452
+ * @param {string|Element} elName - Element name or element object
453
+ * @param {ElementAttributes} [atts] - Element attributes
390
454
  * @param {any[]} [childNodes] - Child nodes
391
- * @param {Function} [cb] - Callback function
455
+ * @param {(this: StringJoiningTransformer) => void} [cb] - Callback function
392
456
  * @returns {StringJoiningTransformer}
393
457
  */
394
458
  element (elName, atts, childNodes, cb) {
459
+ // If a parent element's start tag is still open, close it before
460
+ // starting a new element to ensure valid nesting.
461
+ if (this._openTagState) {
462
+ this.append('>');
463
+ this._openTagState = false;
464
+ }
465
+
466
+ if (!this.root) {
467
+ this.root = elName;
468
+
469
+ // todo: indent, cdataSectionElements
470
+ const {
471
+ omitXmlDeclaration, doctypePublic, doctypeSystem, method
472
+ } = this._outputConfig ?? {};
473
+
474
+ let xmlDeclaration = '';
475
+ /* c8 ignore start -- third OR condition short-circuits */
476
+ if (!omitXmlDeclaration && (
477
+ method === 'xml' || method === 'xhtml' || omitXmlDeclaration === false)
478
+ ) {
479
+ const {version, encoding, standalone} = this._outputConfig ?? {};
480
+ xmlDeclaration = `<?xml${
481
+ version ? ` version="${version}"` : ''
482
+ }${
483
+ encoding ? ` encoding="${encoding}"` : ''
484
+ }${
485
+ standalone ? ` standalone="yes"` : ''
486
+ }?>\n`;
487
+ }
488
+ /* c8 ignore stop */
489
+
490
+ let doctype = '';
491
+ if (doctypePublic !== undefined || doctypeSystem !== undefined) {
492
+ /* c8 ignore start -- nested ternary attribution issue */
493
+ doctype = `<!DOCTYPE ${elName}${
494
+ doctypePublic
495
+ ? ` PUBLIC "${doctypePublic}" "${doctypeSystem}"`
496
+ : doctypeSystem
497
+ ? ` SYSTEM "${doctypeSystem}"`
498
+ : ''
499
+ }>\n`;
500
+ /* c8 ignore stop */
501
+ }
502
+
503
+ this._str = xmlDeclaration + doctype + this._str;
504
+ // Document pushing is handled by document() method or get()
505
+ }
506
+
395
507
  // Emits an HTML/XML element using Jamilih under the hood, or allows a
396
508
  // callback to build attributes/children incrementally. Attribute values
397
509
  // are escaped unless cfg.preEscapedAttributes is true. When a callback is
@@ -399,23 +511,26 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
399
511
  // eslint-disable-next-line unicorn/no-this-assignment -- Temporary
400
512
  const that = this;
401
513
  if (Array.isArray(atts)) {
402
- cb = /** @type {Function} */ (/** @type {unknown} */ (childNodes));
514
+ cb = /** @type {(this: StringJoiningTransformer) => void} */ (
515
+ /** @type {unknown} */ (childNodes)
516
+ );
403
517
  childNodes = atts;
404
518
  atts = {};
405
519
  } else if (typeof atts === 'function') {
406
- cb = atts;
520
+ cb = /** @type {(this: StringJoiningTransformer) => void} */ (atts);
407
521
  childNodes = [];
408
522
  atts = {};
409
523
  }
410
524
  if (typeof childNodes === 'function') {
411
- cb = childNodes;
525
+ cb = /** @type {(this: StringJoiningTransformer) => void} */ (childNodes);
412
526
  childNodes = [];
413
527
  }
414
528
 
415
529
  // Todo: allow for cfg to produce Jamilih string output or hXML
416
530
  // string output
417
- const method = this._cfg && this._cfg.xmlElements ? 'toXML' : 'toHTML';
531
+ const method = this._cfg.xmlElements ? 'toXML' : 'toHTML';
418
532
  if (!cb) {
533
+ // Ensure any open parent start tag was closed (handled above)
419
534
  // Note that Jamilih currently has an issue with 'selected', 'checked',
420
535
  // 'value', 'defaultValue', 'for', 'on*', 'style' (workaround: pass
421
536
  // an empty callback as the last argument to element())
@@ -428,7 +543,7 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
428
543
  }
429
544
 
430
545
  if (typeof elName === 'object') {
431
- /** @type {Record<string, any>} */
546
+ /** @type {Record<string, string>} */
432
547
  const objAtts = {};
433
548
  /** @type {any} */
434
549
  const elObj = elName;
@@ -444,9 +559,13 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
444
559
  const oldTagState = this._openTagState;
445
560
  this._openTagState = true;
446
561
  if (atts) {
447
- const attsObj = /** @type {Record<string, any>} */ (atts);
562
+ const attsObj = /** @type {Record<string, unknown>} */ (atts);
448
563
  Object.keys(attsObj).forEach((att) => {
449
- that.attribute(att, attsObj[att], false);
564
+ that.attribute(
565
+ att,
566
+ /** @type {string|Record<string, unknown>} */ (attsObj[att]),
567
+ false
568
+ );
450
569
  });
451
570
  }
452
571
  if (childNodes && childNodes.length) {
@@ -468,7 +587,8 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
468
587
 
469
588
  /**
470
589
  * @param {string} name - Attribute name
471
- * @param {string|object} val - Attribute value
590
+ * @param {string|Record<string, unknown>|
591
+ * string[][]} val - Attribute value
472
592
  * @param {boolean} [avoidAttEscape] - Whether to avoid escaping the
473
593
  * attribute value
474
594
  * @returns {StringJoiningTransformer}
@@ -486,9 +606,9 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
486
606
  );
487
607
  }
488
608
 
489
- if (!this._cfg || !this._cfg.xmlElements) {
609
+ if (!this._cfg.xmlElements) {
490
610
  if (typeof val === 'object') {
491
- /** @type {Record<string, any>} */
611
+ /** @type {Record<string, unknown>} */
492
612
  const valObj = /** @type {any} */ (val);
493
613
  switch (name) {
494
614
  case 'dataset': {
@@ -496,16 +616,22 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
496
616
  that.attribute(
497
617
  'data-' + att.replaceAll(
498
618
  camelCase, _makeDatasetAttribute
499
- ), valObj[att], false
619
+ ),
620
+ /** @type {string|Record<string, unknown>} */ (valObj[att]),
621
+ false
500
622
  );
501
623
  });
502
624
  break;
503
625
  }
504
626
  case '$a': { // Ordered attributes
505
- /** @type {any[]} */
627
+ /** @type {unknown[][]} */
506
628
  const valArr = /** @type {any} */ (val);
507
629
  valArr.forEach(function (attArr) {
508
- that.attribute(attArr[0], attArr[1], false);
630
+ that.attribute(
631
+ String(attArr[0]),
632
+ /** @type {string|Record<string, unknown>} */ (attArr[1]),
633
+ false
634
+ );
509
635
  });
510
636
  break;
511
637
  }
@@ -519,7 +645,7 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
519
645
 
520
646
  /** @type {string} */
521
647
  const valStr = /** @type {any} */ (val);
522
- val = ((this._cfg && this._cfg.preEscapedAttributes) || avoidAttEscape)
648
+ val = (this._cfg.preEscapedAttributes || avoidAttEscape)
523
649
  ? valStr
524
650
  : valStr.replaceAll('&', '&amp;').replaceAll('"', '&quot;');
525
651
  this.append(' ' + name + '="' + val + '"');
@@ -542,12 +668,31 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
542
668
  }
543
669
 
544
670
  /**
545
- * Unlike text(), does not escape for HTML; unlike string(), does not perform
546
- * JSON stringification; unlike append(), does not do other checks (but still
547
- * varies in its role across transformers).
548
- * @param {string} str
549
- * @returns {StringJoiningTransformer}
550
- */
671
+ * @param {string} text
672
+ * @returns {StringJoiningTransformer}}
673
+ */
674
+ comment (text) {
675
+ this.append(`<!--${text}-->`);
676
+ return this;
677
+ }
678
+
679
+ /**
680
+ * @param {string} target
681
+ * @param {string} data
682
+ * @returns {StringJoiningTransformer}}
683
+ */
684
+ processingInstruction (target, data) {
685
+ this.append(`<?${target} ${data}?>`);
686
+ return this;
687
+ }
688
+
689
+ /**
690
+ * Unlike text(), does not escape for HTML; unlike string(), does not perform
691
+ * JSON stringification; unlike append(), does not do other checks (but
692
+ * still varies in its role across transformers).
693
+ * @param {string} str
694
+ * @returns {StringJoiningTransformer}
695
+ */
551
696
  rawAppend (str) {
552
697
  // Lowest-level append: bypasses append() semantics and state checks.
553
698
  this._str += str;
@@ -566,11 +711,122 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
566
711
  return this;
567
712
  }
568
713
 
714
+ /**
715
+ * Creates a new string document and executes a callback in its context.
716
+ * Similar to XSLT's xsl:document, this allows templates to generate
717
+ * multiple output documents. The created document is pushed to this._docs
718
+ * and will be included in the result when exposeDocuments is true.
719
+ *
720
+ * @param {(this: StringJoiningTransformer) => void} cb
721
+ * Callback that builds the document content
722
+ * @param {OutputConfig} [cfg]
723
+ * Output configuration for the document (encoding, doctype, etc.)
724
+ * @returns {StringJoiningTransformer}
725
+ */
726
+ document (cb, cfg) {
727
+ // If there's a current document being built (this.root is set and _str
728
+ // has content), save it to _docs before starting a new document
729
+ if (this._cfg.exposeDocuments && this.root && this._str &&
730
+ !this._docs.includes(this._str)) {
731
+ this._docs.push(this._str);
732
+ }
733
+
734
+ // Save current state
735
+ /** @type {any} */
736
+ const oldRoot = this.root;
737
+ /** @type {any} */
738
+ const oldOutputConfig = this._outputConfig;
739
+ const oldStr = this._str;
740
+ /** @type {any} */
741
+ const oldOpenTagState = this._openTagState;
742
+
743
+ // Reset state for new document
744
+ this.root = undefined;
745
+ /** @type {any} */
746
+ this._outputConfig = cfg;
747
+ this._str = '';
748
+ this._openTagState = false;
749
+ this._insideDocument = true;
750
+
751
+ // Execute callback to build document content
752
+ cb.call(this);
753
+
754
+ // Save the newly created document string
755
+ const newDoc = this._str;
756
+ this._docs.push(newDoc);
757
+
758
+ // Restore previous state
759
+ this.root = oldRoot;
760
+ this._outputConfig = oldOutputConfig;
761
+ this._str = oldStr;
762
+ this._openTagState = oldOpenTagState;
763
+ this._insideDocument = false;
764
+
765
+ return this;
766
+ }
767
+
768
+ /**
769
+ * Creates a new result document with metadata (href, format).
770
+ * Similar to XSLT's xsl:result-document, this allows templates to generate
771
+ * multiple output documents with associated metadata like URIs. The created
772
+ * document is stored in this._resultDocuments with the provided href.
773
+ *
774
+ * @param {string} href - URI/path for the result document
775
+ * @param {(this: StringJoiningTransformer) => void} cb
776
+ * Callback that builds the document content
777
+ * @param {OutputConfig} [cfg]
778
+ * Output configuration for the document (encoding, doctype, format, etc.)
779
+ * @returns {StringJoiningTransformer}
780
+ */
781
+ resultDocument (href, cb, cfg) {
782
+ // If there's a current document being built, save it first
783
+ if (this.root && this._str && !this._docs.includes(this._str)) {
784
+ this._docs.push(this._str);
785
+ }
786
+
787
+ // Save current state
788
+ /** @type {any} */
789
+ const oldRoot = this.root;
790
+ /** @type {any} */
791
+ const oldOutputConfig = this._outputConfig;
792
+ const oldStr = this._str;
793
+ /** @type {any} */
794
+ const oldOpenTagState = this._openTagState;
795
+
796
+ // Reset state for new document
797
+ this.root = undefined;
798
+ /** @type {any} */
799
+ this._outputConfig = cfg;
800
+ this._str = '';
801
+ this._openTagState = false;
802
+ this._insideDocument = true;
803
+
804
+ // Execute callback to build document content
805
+ cb.call(this);
806
+
807
+ // Save the newly created document string with metadata
808
+ const resultDoc = this._str;
809
+ this._resultDocuments.push({
810
+ href,
811
+ document: resultDoc,
812
+ format: this._outputConfig?.method || cfg?.method
813
+ });
814
+
815
+ // Restore previous state
816
+ this.root = oldRoot;
817
+ this._outputConfig = oldOutputConfig;
818
+ this._str = oldStr;
819
+ this._openTagState = oldOpenTagState;
820
+ this._insideDocument = false;
821
+
822
+ return this;
823
+ }
824
+
569
825
  /**
570
826
  * Helper method to use property sets.
571
- * @param {object} obj - Object to apply property set to
827
+ * @param {Record<string, unknown>} obj - Object to apply property set to
572
828
  * @param {string} psName - Property set name
573
- * @returns {object}
829
+ * @returns {Record<string, unknown>}
574
830
  */
575
831
  _usePropertySets (obj, psName) {
576
832
  // Merge named property set from this.propertySets into obj
@@ -584,6 +840,4 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
584
840
  }
585
841
  }
586
842
 
587
- // Todo: Implement comment(), processingInstruction(), etc.
588
-
589
843
  export default StringJoiningTransformer;