jtlt 0.3.0 → 0.4.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 (40) hide show
  1. package/CHANGES.md +9 -0
  2. package/README.md +46 -78
  3. package/demo/codemirror.esm.js +28242 -0
  4. package/demo/codemirror.js +94 -0
  5. package/demo/index.css +7 -0
  6. package/demo/index.html +11 -14
  7. package/demo/index.js +206 -26
  8. package/demo/vendor/jamilih/dist/jml.mjs +2341 -0
  9. package/demo/vendor/jhtml/src/SAJJ/SAJJ.ObjectArrayDelegator.js +356 -0
  10. package/demo/vendor/jhtml/src/SAJJ/SAJJ.Stringifier.js +186 -0
  11. package/demo/vendor/jhtml/src/SAJJ/SAJJ.js +746 -0
  12. package/demo/vendor/jhtml/src/SAJJ/testing/SAJJ.html +33 -0
  13. package/demo/vendor/jhtml/src/SAJJ/testing/SAJJ.testing.js +25 -0
  14. package/demo/vendor/jhtml/src/jhtml-browser.js +5 -0
  15. package/demo/vendor/jhtml/src/jhtml-node.cts +3 -0
  16. package/demo/vendor/jhtml/src/jhtml-node.js +8 -0
  17. package/demo/vendor/jhtml/src/jhtml-node.mts +1 -0
  18. package/demo/vendor/jhtml/src/jhtml.cts +3 -0
  19. package/demo/vendor/jhtml/src/jhtml.js +602 -0
  20. package/demo/vendor/jhtml/src/jhtml.mts +1 -0
  21. package/demo/vendor/jsonpath-plus/dist/index-browser-esm.js +2158 -0
  22. package/demo/vendor/simple-get-json/dist/index-es.js +151 -0
  23. package/dist/JSONPathTransformerContext.d.ts +88 -4
  24. package/dist/JSONPathTransformerContext.d.ts.map +1 -1
  25. package/dist/XPathTransformer.d.ts +2 -2
  26. package/dist/XPathTransformerContext.d.ts +92 -8
  27. package/dist/XPathTransformerContext.d.ts.map +1 -1
  28. package/dist/index.d.ts +4 -4
  29. package/dist/index.d.ts.map +1 -1
  30. package/docs/API.expanded.md +5 -3
  31. package/docs/API.md +1 -1
  32. package/docs/TO-DO.md +55 -31
  33. package/eslint.config.js +3 -1
  34. package/package.json +24 -4
  35. package/rollup.config.js +13 -0
  36. package/src/JSONPathTransformerContext.js +422 -4
  37. package/src/XPathTransformer.js +1 -1
  38. package/src/XPathTransformerContext.js +462 -11
  39. package/src/index.js +9 -6
  40. package/tsconfig.json +5 -2
@@ -0,0 +1,2341 @@
1
+ /* eslint-disable sonarjs/updated-loop-counter -- Ok */
2
+ /* eslint-disable unicorn/prefer-global-this -- Easier */
3
+ /* eslint-disable sonarjs/no-control-regex -- Intentional */
4
+ /*
5
+ Possible todos:
6
+ 0. Add XSLT to JML-string stylesheet (or even vice versa)
7
+
8
+ Todos inspired by JsonML: https://github.com/mckamey/jsonml/blob/master/jsonml-html.js
9
+ 0. expand ATTR_MAP
10
+
11
+ Other Todos:
12
+ 0. Note to self: Integrate research from other jml notes
13
+ 0. Allow Jamilih to be seeded with an existing element, so as to be able to
14
+ add/modify attributes and children
15
+ 0. Allow array as single first argument
16
+ 0. Settle on whether need to use null as last argument to return array (or
17
+ fragment) or other way to allow appending? Options object at end instead
18
+ to indicate whether returning array, fragment, first element, etc.?
19
+ 0. Allow building of generic XML (pass configuration object)
20
+ 0. Allow building content internally as a string (though allowing DOM methods, etc.?)
21
+ 0. Support JsonML empty string element name to represent fragments?
22
+ 0. Redo browser testing of jml
23
+ */
24
+
25
+ /**
26
+ * @typedef {Window & {DocumentFragment: any}} HTMLWindow
27
+ */
28
+
29
+ /**
30
+ * @typedef {any} ArbitraryValue
31
+ */
32
+
33
+ /**
34
+ * @typedef {number} Integer
35
+ */
36
+
37
+ /**
38
+ * @typedef {{
39
+ * element: Document|HTMLElement|DocumentFragment,
40
+ * attribute: {name: string|null, value: JamilihAttValue},
41
+ * opts: JamilihOptions
42
+ * }} PluginSettings
43
+ */
44
+
45
+ /**
46
+ * @typedef {object} JamilihPlugin
47
+ * @property {string} name
48
+ * @property {(opts: PluginSettings) => string|Promise<void>} set
49
+ */
50
+
51
+ /**
52
+ * @type {import('jsdom').DOMWindow|HTMLWindow|undefined}
53
+ */
54
+ let win;
55
+
56
+ /* c8 ignore next 3 */
57
+ if (typeof window !== 'undefined' && window) {
58
+ win = window;
59
+ }
60
+
61
+ /* c8 ignore next */
62
+ let doc = typeof document !== 'undefined' && document || win?.document;
63
+
64
+ // STATIC PROPERTIES
65
+
66
+ const possibleOptions = ['$plugins',
67
+ // '$mode', // Todo (SVG/XML)
68
+ // '$state', // Used internally
69
+ '$map' // Add any other options here
70
+ ];
71
+ const NS_HTML = 'http://www.w3.org/1999/xhtml',
72
+ hyphenForCamelCase = /-([a-z])/gu;
73
+ const ATTR_MAP = new Map([['maxlength', 'maxLength'], ['minlength', 'minLength'], ['readonly', 'readOnly']]);
74
+
75
+ // We define separately from ATTR_DOM for clarity (and parity with JsonML) but no current need
76
+ // We don't set attribute esp. for boolean atts as we want to allow setting of `undefined`
77
+ // (e.g., from an empty variable) on templates to have no effect
78
+ const BOOL_ATTS = ['checked', 'defaultChecked', 'defaultSelected', 'disabled', 'indeterminate', 'open',
79
+ // Dialog elements
80
+ 'readOnly', 'selected'];
81
+
82
+ // From JsonML
83
+ const ATTR_DOM = new Set([...BOOL_ATTS, 'accessKey',
84
+ // HTMLElement
85
+ 'async', 'autocapitalize',
86
+ // HTMLElement
87
+ 'autofocus', 'contentEditable',
88
+ // HTMLElement through ElementContentEditable
89
+ 'defaultValue', 'defer', 'draggable',
90
+ // HTMLElement
91
+ 'formnovalidate', 'hidden',
92
+ // HTMLElement
93
+ 'innerText',
94
+ // HTMLElement
95
+ 'inputMode',
96
+ // HTMLElement through ElementContentEditable
97
+ 'ismap', 'multiple', 'novalidate', 'pattern', 'required', 'spellcheck',
98
+ // HTMLElement
99
+ 'translate',
100
+ // HTMLElement
101
+ 'value', 'willvalidate']);
102
+ // Todo: Add more to this as useful for templating
103
+ // to avoid setting through nullish value
104
+ const NULLABLES = new Set(['autocomplete', 'dir',
105
+ // HTMLElement
106
+ 'integrity',
107
+ // script, link
108
+ 'lang',
109
+ // HTMLElement
110
+ 'max', 'min', 'minLength', 'maxLength', 'title' // HTMLElement
111
+ ]);
112
+
113
+ /**
114
+ * @param {string} sel
115
+ * @returns {HTMLElement|null}
116
+ */
117
+ const $ = sel => {
118
+ if (!doc) {
119
+ throw new Error('No document object');
120
+ }
121
+ return doc.querySelector(sel);
122
+ };
123
+
124
+ /**
125
+ * @param {string} sel
126
+ * @returns {HTMLElement[]}
127
+ */
128
+ const $$ = sel => {
129
+ if (!doc) {
130
+ throw new Error('No document object');
131
+ }
132
+ return [...(/** @type {NodeListOf<HTMLElement>} */doc.querySelectorAll(sel))];
133
+ };
134
+
135
+ /**
136
+ * @private
137
+ * @static
138
+ * @param {Document|DocumentFragment|HTMLElement} parent The parent to which to append the element
139
+ * @param {Node|string} child The element or other node to append to the parent
140
+ * @throws {Error} Rethrow if problem with `append` and unhandled
141
+ * @returns {void}
142
+ */
143
+ function _appendNode(parent, child) {
144
+ const parentName = parent.nodeName?.toLowerCase();
145
+ if (parentName === 'template') {
146
+ /** @type {HTMLTemplateElement} */parent.content.append(child);
147
+ return;
148
+ }
149
+ parent.append(child); // IE9 is now ok with this
150
+ }
151
+
152
+ /**
153
+ * Attach event in a cross-browser fashion.
154
+ * @static
155
+ * @param {HTMLElement} el DOM element to which to attach the event
156
+ * @param {string} type The DOM event (without 'on') to attach to the element
157
+ * @param {(evt: Event & {target: HTMLElement}) => void} handler The event handler to attach to the element
158
+ * @param {boolean} [capturing] Whether or not the event should be
159
+ * capturing (W3C-browsers only); default is false; NOT IN USE
160
+ * @returns {void}
161
+ */
162
+ function _addEvent(el, type, handler, capturing) {
163
+ // @ts-expect-error It's ok
164
+ el.addEventListener(type, handler, Boolean(capturing));
165
+ }
166
+
167
+ /**
168
+ * Creates a text node of the result of resolving an entity or character reference.
169
+ * @param {'entity'|'decimal'|'hexadecimal'} type Type of reference
170
+ * @param {string} prefix Text to prefix immediately after the "&"
171
+ * @param {string} arg The body of the reference
172
+ * @throws {TypeError}
173
+ * @returns {Text} The text node of the resolved reference
174
+ */
175
+ function _createSafeReference(type, prefix, arg) {
176
+ /* c8 ignore next 3 */
177
+ if (!doc) {
178
+ throw new Error('No document defined');
179
+ }
180
+ // For security reasons related to innerHTML, we ensure this string only
181
+ // contains potential entity characters
182
+ if (!/^\w+$/u.test(arg)) {
183
+ throw new TypeError(`Bad ${type} reference; with prefix "${prefix}" and arg "${arg}"`);
184
+ }
185
+ const elContainer = doc.createElement('div');
186
+ // Todo: No workaround for XML?
187
+ // // eslint-disable-next-line no-unsanitized/property
188
+ elContainer.innerHTML = '&' + prefix + arg + ';';
189
+ return doc.createTextNode(elContainer.innerHTML);
190
+ }
191
+
192
+ /**
193
+ * @param {string} n0 Whole expression match (including "-")
194
+ * @param {string} n1 Lower-case letter match
195
+ * @returns {string} Uppercased letter
196
+ */
197
+ function _upperCase(n0, n1) {
198
+ return n1.toUpperCase();
199
+ }
200
+
201
+ // Todo: Make as public utility
202
+ /**
203
+ * @param {ArbitraryValue} o
204
+ * @returns {boolean}
205
+ */
206
+ function _isNullish(o) {
207
+ return o === null || o === undefined;
208
+ }
209
+
210
+ // Todo: Make as public utility, but also return types for undefined, boolean, number, document, etc.
211
+ /**
212
+ * @private
213
+ * @static
214
+ * @param {string|JamilihAttributes|JamilihArray|JamilihChildren|
215
+ * JamilihDocumentFragment|JamilihAttributeNode|
216
+ * JamilihOptions|HTMLElement|Document|DocumentFragment|null|undefined} item
217
+ * @returns {"string"|"null"|"array"|"element"|"fragment"|"object"|
218
+ * "symbol"|"bigint"|"function"|"number"|"boolean"|"undefined"|
219
+ * "document"|"processing-instruction"|"non-container node"}
220
+ */
221
+ function _getType(item) {
222
+ const type = typeof item;
223
+
224
+ // Appease TS
225
+ if (typeof item === 'string' || typeof item === 'undefined') {
226
+ return 'string';
227
+ }
228
+ switch (type) {
229
+ case 'object':
230
+ if (item === null) {
231
+ return 'null';
232
+ }
233
+ if (Array.isArray(item)) {
234
+ return 'array';
235
+ }
236
+ if ('nodeType' in item) {
237
+ switch (item.nodeType) {
238
+ case 1:
239
+ return 'element';
240
+ case 7:
241
+ return 'processing-instruction';
242
+ case 9:
243
+ return 'document';
244
+ case 11:
245
+ return 'fragment';
246
+ default:
247
+ return 'non-container node';
248
+ }
249
+ }
250
+ // Fallthrough
251
+ default:
252
+ return type;
253
+ }
254
+ }
255
+
256
+ /**
257
+ * @private
258
+ * @static
259
+ * @param {DocumentFragment} frag
260
+ * @param {Node} node
261
+ * @returns {DocumentFragment}
262
+ */
263
+ function _fragReducer(frag, node) {
264
+ frag.append(node);
265
+ return frag;
266
+ }
267
+
268
+ /**
269
+ * @private
270
+ * @static
271
+ * @param {Object<string, string>} xmlnsObj
272
+ * @returns {(...n: string[]) => string}
273
+ */
274
+ function _replaceDefiner(xmlnsObj) {
275
+ /**
276
+ * @param {string[]} n
277
+ * @returns {string}
278
+ */
279
+ return function (...n) {
280
+ const n0 = n[0];
281
+ let retStr = xmlnsObj[''] ? ' xmlns="' + xmlnsObj[''] + '"' : n0; // Preserve XHTML
282
+ for (const [ns, xmlnsVal] of Object.entries(xmlnsObj)) {
283
+ if (ns !== '') {
284
+ retStr += ' xmlns:' + ns + '="' + xmlnsVal + '"';
285
+ }
286
+ }
287
+ return retStr;
288
+ };
289
+ }
290
+
291
+ /**
292
+ * @callback ChildrenToJMLCallback
293
+ * @param {JamilihArray|JamilihChildType|string} childNodeJML
294
+ * @param {Integer} i
295
+ * @returns {void}
296
+ */
297
+
298
+ /**
299
+ * @private
300
+ * @static
301
+ * @param {Node} node
302
+ * @returns {ChildrenToJMLCallback}
303
+ */
304
+ function _childrenToJML(node) {
305
+ return function (childNodeJML, i) {
306
+ const cn = node.childNodes[i];
307
+ const j = Array.isArray(childNodeJML) ? jml(...(/** @type {JamilihArray} */childNodeJML)) : jml(childNodeJML);
308
+ cn.replaceWith(j);
309
+ };
310
+ }
311
+
312
+ /**
313
+ * Keep this in sync with `JamilihArray`'s first argument (minus `Document`).
314
+ * @typedef {JamilihDoc|JamilihDoctype|JamilihTextNode|
315
+ * JamilihAttributeNode|JamilihOptions|ElementName|HTMLElement|
316
+ * JamilihDocumentFragment
317
+ * } JamilihFirstArg
318
+ */
319
+
320
+ /**
321
+ * @callback JamilihAppender
322
+ * @param {JamilihArray|JamilihFirstArg|Node|TextNodeString} childJML
323
+ * @returns {void}
324
+ */
325
+
326
+ /**
327
+ * @private
328
+ * @static
329
+ * @param {ParentNode} node
330
+ * @returns {JamilihAppender}
331
+ */
332
+ function _appendJML(node) {
333
+ return function (childJML) {
334
+ if (typeof childJML === 'string' || typeof childJML === 'number') {
335
+ throw new TypeError('Unexpected text string/number in the head');
336
+ }
337
+ if (Array.isArray(childJML)) {
338
+ node.append(jml(...childJML));
339
+ } else if (typeof childJML === 'object' && 'nodeType' in childJML) {
340
+ node.append(childJML);
341
+ } else {
342
+ node.append(jml(childJML));
343
+ }
344
+ };
345
+ }
346
+
347
+ /**
348
+ * @callback appender
349
+ * @param {JamilihArray|JamilihFirstArg|Node|TextNodeString} childJML
350
+ * @returns {void}
351
+ */
352
+
353
+ /**
354
+ * @private
355
+ * @static
356
+ * @param {ParentNode} node
357
+ * @returns {appender}
358
+ */
359
+ function _appendJMLOrText(node) {
360
+ return function (childJML) {
361
+ if (typeof childJML === 'string' || typeof childJML === 'number') {
362
+ node.append(String(childJML));
363
+ } else if (Array.isArray(childJML)) {
364
+ node.append(jml(...childJML));
365
+ } else if (typeof childJML === 'object' && 'nodeType' in childJML) {
366
+ node.append(childJML);
367
+ } else {
368
+ node.append(jml(childJML));
369
+ }
370
+ };
371
+ }
372
+
373
+ /**
374
+ * @private
375
+ * @static
376
+ */
377
+ /*
378
+ function _DOMfromJMLOrString (childNodeJML) {
379
+ if (typeof childNodeJML === 'string') {
380
+ return doc.createTextNode(childNodeJML);
381
+ }
382
+ return jml(...childNodeJML);
383
+ }
384
+ */
385
+
386
+ /**
387
+ * @typedef {HTMLElement|DocumentFragment|Comment|Attr|
388
+ * Text|Document|DocumentType|ProcessingInstruction|CDATASection} JamilihReturn
389
+ */
390
+ // 'string|JamilihOptions|JamilihDocumentFragment|JamilihAttributes|(string|JamilihArray)[]
391
+
392
+ /**
393
+ * Can either be an array of:
394
+ * 1. JamilihAttributes followed by an array of JamilihArrays or Elements.
395
+ * (Cannot be multiple single JamilihArrays despite TS type).
396
+ * 2. Any number of JamilihArrays.
397
+ * @typedef {[(JamilihAttributes|JamilihArray|JamilihArray[]|HTMLElement), ...(JamilihArray|JamilihArray[]|HTMLElement)[]]} TemplateJamilihArray
398
+ */
399
+
400
+ /**
401
+ * @typedef {(JamilihArray|HTMLElement)[]} ShadowRootJamilihArrayContainer
402
+ */
403
+
404
+ /**
405
+ * @typedef {{
406
+ * open?: boolean|ShadowRootJamilihArrayContainer,
407
+ * closed?: boolean|ShadowRootJamilihArrayContainer,
408
+ * template?: string|HTMLTemplateElement|TemplateJamilihArray,
409
+ * content?: ShadowRootJamilihArrayContainer|DocumentFragment
410
+ * }} JamilihShadowRootObject
411
+ */
412
+
413
+ /**
414
+ * @typedef {{[key: string]: string}} XmlnsAttributeObject
415
+ */
416
+
417
+ /**
418
+ * @typedef {null|XmlnsAttributeObject} XmlnsAttributeValue
419
+ */
420
+
421
+ /**
422
+ * @typedef {{
423
+ * [key: string]: string|number|null|undefined|DatasetAttributeObject
424
+ * }} DatasetAttributeObject
425
+ */
426
+
427
+ /**
428
+ * @typedef {string|undefined|{[key: string]: string|null}} StyleAttributeValue
429
+ */
430
+
431
+ /**
432
+ * @typedef {(this: HTMLElement, event: Event & {target: HTMLElement}) => void} EventHandler
433
+ */
434
+
435
+ /**
436
+ * @typedef {{
437
+ * [key: string]: EventHandler|[EventHandler, boolean]
438
+ * }} OnAttributeObject
439
+ */
440
+
441
+ /**
442
+ * @typedef {{
443
+ * $on?: OnAttributeObject|null
444
+ * }} OnAttribute
445
+ */
446
+
447
+ /**
448
+ * @typedef {boolean} BooleanAttribute
449
+ */
450
+
451
+ /**
452
+ * @typedef {((this: HTMLElement, event?: Event) => void)} HandlerAttributeValue
453
+ */
454
+
455
+ /* eslint-disable jsdoc/valid-types -- jsdoc-type-pratt-parser Bug */
456
+ /**
457
+ * @typedef {{
458
+ * [key: string]: HandlerAttributeValue
459
+ * }} OnHandlerObject
460
+ */
461
+
462
+ /**
463
+ * @typedef {number} StringifiableNumber
464
+ */
465
+
466
+ /**
467
+ * @typedef {{
468
+ * name: string,
469
+ * systemId?: string,
470
+ * publicId?: string
471
+ * }} JamilihDocumentType
472
+ */
473
+
474
+ /**
475
+ * @typedef {string|{extends?: string}} DefineOptions
476
+ */
477
+
478
+ /**
479
+ * @typedef {{[key: string]: string|number|boolean|((this: DefineMixin, ...args: any[]) => any)}} DefineMixin
480
+ */
481
+
482
+ /**
483
+ * @typedef {{
484
+ * new (): HTMLElement;
485
+ * prototype: HTMLElement & {[key: string]: any}
486
+ * }} DefineConstructor
487
+ */
488
+ /* eslint-enable jsdoc/valid-types -- https://github.com/jsdoc-type-pratt-parser/jsdoc-type-pratt-parser/issues/131 */
489
+
490
+ /**
491
+ * @typedef {(this: HTMLElement) => void} DefineUserConstructor
492
+ */
493
+
494
+ /**
495
+ * @typedef {[DefineConstructor|DefineUserConstructor|DefineMixin, DefineOptions?]|[DefineConstructor|DefineUserConstructor, DefineMixin?, DefineOptions?]} DefineObjectArray
496
+ */
497
+
498
+ /**
499
+ * @typedef {DefineObjectArray|DefineConstructor|DefineMixin|DefineUserConstructor} DefineObject
500
+ */
501
+
502
+ /**
503
+ * @typedef {{elem?: HTMLElement, [key: string]: any}} SymbolObject
504
+ */
505
+
506
+ /**
507
+ * @typedef {[symbol|string, ((this: HTMLElement, ...args: any[]) => any)|SymbolObject]} SymbolArray
508
+ */
509
+
510
+ /**
511
+ * @typedef {null|undefined} NullableAttributeValue
512
+ */
513
+
514
+ /**
515
+ * @typedef {[string, object]|string|{[key: string]: any}} PluginValue
516
+ */
517
+
518
+ /**
519
+ * @typedef {(string|NullableAttributeValue|BooleanAttribute|
520
+ * JamilihArray|JamilihShadowRootObject|StringifiableNumber|
521
+ * JamilihDocumentType|JamilihDocument|XmlnsAttributeValue|
522
+ * OnAttributeObject|
523
+ * HandlerAttributeValue|DefineObject|SymbolArray|PluginReference|
524
+ * PluginValue
525
+ * )} JamilihAttValue
526
+ */
527
+
528
+ /**
529
+ * @typedef {{
530
+ * [key: string]: string|number|((this: HTMLElement, ...args: any[]) => any)
531
+ * }} DataAttributeObject
532
+ */
533
+
534
+ /**
535
+ * @typedef {{
536
+ * $data?: true|string[]|Map<any, any>|WeakMap<any, any>|DataAttributeObject|
537
+ * [undefined, DataAttributeObject]|
538
+ * [Map<any, any>|WeakMap<any, any>|undefined, DataAttributeObject]
539
+ * }} DataAttribute
540
+ */
541
+
542
+ /**
543
+ * @typedef {{
544
+ * dataset?: DatasetAttributeObject
545
+ * }} DatasetAttribute
546
+ */
547
+
548
+ /**
549
+ * @typedef {{
550
+ * style?: StyleAttributeValue
551
+ * }} StyleAttribute
552
+ */
553
+
554
+ /**
555
+ * @typedef {{
556
+ * $shadow?: JamilihShadowRootObject
557
+ * }} JamilihShadowRootAttribute
558
+ */
559
+
560
+ /* eslint-disable jsdoc/valid-types -- jsdoc-type-pratt-parser Bug */
561
+ /**
562
+ * @typedef {{
563
+ * is?: string|null,
564
+ * $define?: DefineObject
565
+ * }} DefineAttribute
566
+ */
567
+ /* eslint-enable jsdoc/valid-types -- jsdoc-type-pratt-parser Bug */
568
+
569
+ /**
570
+ * @typedef {{
571
+ * $custom?: {[key: string]: any}
572
+ * }} CustomAttribute
573
+ */
574
+
575
+ /**
576
+ * @typedef {{
577
+ * $symbol?: SymbolArray
578
+ * }} SymbolAttribute
579
+ */
580
+
581
+ /**
582
+ * @typedef {{
583
+ * xmlns?: string|null|XmlnsAttributeObject
584
+ * }} XmlnsAttribute
585
+ */
586
+
587
+ /**
588
+ * `OnHandlerObject &` wasn't working, so added `HandlerAttributeValue`.
589
+ * @typedef {DataAttribute & StyleAttribute & JamilihShadowRootAttribute &
590
+ * DefineAttribute & DatasetAttribute & CustomAttribute & SymbolAttribute &
591
+ * OnAttribute & XmlnsAttribute &
592
+ * Partial<JamilihAttributeNode> & Partial<JamilihTextNode> &
593
+ * Partial<JamilihDoc> & Partial<JamilihDoctype> & {
594
+ * [key: string]: JamilihAttValue|HandlerAttributeValue,
595
+ * }} JamilihAttributes
596
+ */
597
+
598
+ /**
599
+ * @typedef {{
600
+ * title?: string,
601
+ * xmlDeclaration?: {
602
+ * version: string,
603
+ * encoding: string,
604
+ * standalone: boolean
605
+ * },
606
+ * childNodes?: JamilihChildType[],
607
+ * $DOCTYPE?: JamilihDocumentType,
608
+ * head?: JamilihChildren
609
+ * body?: JamilihChildren
610
+ * }} JamilihDocument
611
+ */
612
+
613
+ /**
614
+ * @typedef {{
615
+ * $document: JamilihDocument
616
+ * }} JamilihDoc
617
+ */
618
+
619
+ /**
620
+ * @typedef {{$DOCTYPE: JamilihDocumentType}} JamilihDoctype
621
+ */
622
+
623
+ /**
624
+ * @typedef {JamilihArray|TextNodeString|HTMLElement} JamilihDocumentFragmentContent
625
+ */
626
+
627
+ /**
628
+ * @typedef {{'#': JamilihDocumentFragmentContent[]}} JamilihDocumentFragment
629
+ */
630
+
631
+ /**
632
+ * @typedef {string} ElementName
633
+ */
634
+
635
+ /**
636
+ * @typedef {string|number} TextNodeString
637
+ */
638
+
639
+ /**
640
+ * @typedef {{[key: string]: string}} PluginReference
641
+ */
642
+
643
+ /**
644
+ * @typedef {(
645
+ * JamilihArray|TextNodeString|HTMLElement|Comment|ProcessingInstruction|
646
+ * Text|DocumentFragment|JamilihProcessingInstruction|JamilihDocumentFragment|
647
+ * PluginReference
648
+ * )[]} JamilihChildren
649
+ */
650
+
651
+ // Todo: DocumentType, Comment, ProcessingInstruction, Text
652
+ // Todo: JamilihCDATANode, JamilihComment, JamilihProcessingInstruction
653
+ /**
654
+ * @typedef {Document|ElementName|HTMLElement|DocumentFragment|
655
+ * JamilihDocumentFragment|JamilihDoc|JamilihDoctype|JamilihTextNode|
656
+ * JamilihAttributeNode} JamilihFirstArgument
657
+ */
658
+
659
+ /**
660
+ * This would be clearer with overrides, but using as typedef.
661
+ *
662
+ * The optional 0th argument is an Jamilih options object or fragment.
663
+ *
664
+ * The first argument is the element to create (by lower-case name) or DOM element.
665
+ *
666
+ * The second optional argument are attributes to add with the key as the
667
+ * attribute name and value as the attribute value.
668
+ * The third optional argument are an array of children for this element
669
+ * (but raw DOM elements are required to be specified within arrays since
670
+ * could not otherwise be distinguished from siblings being added).
671
+ * The fourth optional argument are a sequence of sibling Elements, represented
672
+ * as DOM elements, or string/attributes/children sequences.
673
+ * The fifth optional argument is the parent to which to attach the element
674
+ * (always the last unless followed by null, in which case it is the
675
+ * second-to-last).
676
+ * The sixth last optional argument is null, used to indicate an array of elements
677
+ * should be returned.
678
+ * @typedef {[
679
+ * JamilihOptions|JamilihFirstArgument,
680
+ * (JamilihFirstArgument|
681
+ * JamilihAttributes|
682
+ * JamilihChildren|
683
+ * HTMLElement|ShadowRoot|
684
+ * null)?,
685
+ * (JamilihAttributes|
686
+ * JamilihChildren|
687
+ * HTMLElement|ShadowRoot|
688
+ * ElementName|null)?,
689
+ * ...(JamilihAttributes|
690
+ * JamilihChildren|
691
+ * HTMLElement|ShadowRoot|
692
+ * ElementName|null)[]
693
+ * ]} JamilihArray
694
+ */
695
+
696
+ /**
697
+ * @typedef {[
698
+ * (string|HTMLElement|ShadowRoot), (JamilihArray[]|JamilihAttributes|HTMLElement|ShadowRoot|null)?, ...(JamilihArray[]|HTMLElement|JamilihAttributes|ShadowRoot|null)[]
699
+ * ]} JamilihArrayPostOptions
700
+ */
701
+
702
+ /**
703
+ * @typedef {{
704
+ * root: [Map<HTMLElement,any>|WeakMap<HTMLElement,any>, any],
705
+ * [key: string]: [Map<HTMLElement,any>|WeakMap<HTMLElement,any>, any]
706
+ * }} MapWithRoot
707
+ */
708
+
709
+ /**
710
+ * @typedef {"root"|"attributeValue"|"element"|"fragment"|"children"|"fragmentChildren"} TraversalState
711
+ */
712
+
713
+ /**
714
+ * @typedef {object} JamilihOptions
715
+ * @property {TraversalState} [$state]
716
+ * @property {JamilihPlugin[]} [$plugins]
717
+ * @property {MapWithRoot|[Map<HTMLElement,any>|WeakMap<HTMLElement,any>, any]} [$map]
718
+ */
719
+
720
+ /**
721
+ * @param {Document|HTMLElement|DocumentFragment} elem
722
+ * @param {string|null} att
723
+ * @param {JamilihAttValue} attVal
724
+ * @param {JamilihOptions} opts
725
+ * @param {TraversalState} [state]
726
+ * @returns {Promise<void>|string|null}
727
+ */
728
+ function checkPluginValue(elem, att, attVal, opts, state) {
729
+ opts.$state = state ?? 'attributeValue';
730
+ if (attVal && typeof attVal === 'object') {
731
+ const matchingPlugin = getMatchingPlugin(opts, Object.keys(attVal)[0]);
732
+ if (matchingPlugin) {
733
+ return matchingPlugin.set({
734
+ opts,
735
+ element: elem,
736
+ attribute: {
737
+ name: att,
738
+ value: attVal
739
+ }
740
+ });
741
+ }
742
+ }
743
+ return /** @type {string} */attVal;
744
+ }
745
+
746
+ /**
747
+ * @param {JamilihOptions} opts
748
+ * @param {string} pluginName
749
+ * @returns {JamilihPlugin|undefined}
750
+ */
751
+ function getMatchingPlugin(opts, pluginName) {
752
+ return opts.$plugins && opts.$plugins.find(p => {
753
+ return p.name === pluginName;
754
+ });
755
+ }
756
+
757
+ /* eslint-disable jsdoc/valid-types -- pratt parser bug */
758
+ /**
759
+ * @template T
760
+ * @typedef {T[keyof T]} ValueOf
761
+ */
762
+ /* eslint-enable jsdoc/valid-types -- pratt parser bug */
763
+
764
+ /* eslint-disable jsdoc/valid-types -- pratt parser bug */
765
+ /**
766
+ * Creates an XHTML or HTML element (XHTML is preferred, but only in browsers
767
+ * that support); any element after element can be omitted, and any subsequent
768
+ * type or types added afterwards.
769
+ * @template {JamilihArray} T
770
+ * @param {T} args
771
+ * @returns {T extends [keyof HTMLElementTagNameMap, any?, any?, any?]
772
+ * ? HTMLElementTagNameMap[T[0]] : JamilihReturn}
773
+ * The newly created (and possibly already appended)
774
+ * element or array of elements
775
+ */
776
+ const jml = function jml(...args) {
777
+ /* eslint-enable jsdoc/valid-types -- pratt parser bug */
778
+ if (!win) {
779
+ throw new Error('No window object');
780
+ }
781
+ if (!doc) {
782
+ throw new Error('No document object');
783
+ }
784
+
785
+ /** @type {(Document|DocumentFragment|HTMLElement) & {[key: string]: any}} */
786
+ let elem = doc.createDocumentFragment();
787
+ /**
788
+ *
789
+ * @param {JamilihAttributes} atts
790
+ * @throws {TypeError}
791
+ * @returns {void}
792
+ */
793
+ function _checkAtts(atts) {
794
+ /* c8 ignore next 3 */
795
+ if (!doc) {
796
+ throw new Error('No document object');
797
+ }
798
+ for (let [att, attVal] of Object.entries(atts)) {
799
+ att = ATTR_MAP.has(att) ? String(ATTR_MAP.get(att)) : att;
800
+
801
+ /**
802
+ * @typedef {any} ElementExpando
803
+ */
804
+
805
+ if (NULLABLES.has(att)) {
806
+ attVal = checkPluginValue(elem, att, /** @type {string|JamilihArray} */attVal, opts);
807
+ if (!_isNullish(attVal)) {
808
+ /** @type {ElementExpando} */elem[att] = attVal;
809
+ }
810
+ continue;
811
+ } else if (ATTR_DOM.has(att)) {
812
+ attVal = checkPluginValue(elem, att, /** @type {string|JamilihArray} */attVal, opts);
813
+ /** @type {ElementExpando} */
814
+ elem[att] = attVal;
815
+ continue;
816
+ }
817
+ switch (att) {
818
+ /*
819
+ Todos:
820
+ 0. JSON mode to prevent event addition
821
+ 0. {$xmlDocument: []} // doc.implementation.createDocument
822
+ 0. Accept array for any attribute with first item as prefix and second as value?
823
+ 0. {$: ['xhtml', 'div']} for prefixed elements
824
+ case '$': // Element with prefix?
825
+ nodes[nodes.length] = elem = doc.createElementNS(attVal[0], attVal[1]);
826
+ break;
827
+ */
828
+ case '#':
829
+ {
830
+ // Document fragment
831
+ opts.$state = 'fragmentChildren';
832
+ nodes[nodes.length] = jml(opts, /** @type {JamilihArray[]} */attVal);
833
+ break;
834
+ }
835
+ case '$shadow':
836
+ {
837
+ const {
838
+ open,
839
+ closed
840
+ } = /** @type {JamilihShadowRootObject} */attVal;
841
+ let {
842
+ content,
843
+ template
844
+ } = /** @type {JamilihShadowRootObject} */attVal;
845
+ const shadowRoot = /** @type {HTMLElement} */elem.attachShadow({
846
+ mode: closed || open === false ? 'closed' : 'open'
847
+ });
848
+ if (template) {
849
+ if (Array.isArray(template)) {
850
+ template = /** @type {HTMLTemplateElement} */
851
+ _getType(template[0]) === 'object' ? jml('template', ...(
852
+ /**
853
+ * @type {[
854
+ * JamilihAttributes, ...(JamilihArray[]|HTMLElement)[]
855
+ * ]}
856
+ */
857
+ template), doc.body) : jml('template',
858
+ /**
859
+ * @type {JamilihArray[]|HTMLElement}
860
+ */
861
+ template, doc.body);
862
+ } else if (typeof template === 'string') {
863
+ template = /** @type {HTMLTemplateElement} */$(template);
864
+ }
865
+ jml(/** @type {HTMLTemplateElement} */
866
+ /** @type {HTMLTemplateElement} */template.content.cloneNode(true), shadowRoot);
867
+ } else {
868
+ if (!content) {
869
+ if (open !== true) {
870
+ content = open || typeof closed === 'boolean' ? content : closed;
871
+ }
872
+ }
873
+ if (content && typeof content !== 'boolean') {
874
+ if (Array.isArray(content)) {
875
+ jml({
876
+ '#': content
877
+ }, shadowRoot);
878
+ } else {
879
+ jml(content, shadowRoot);
880
+ }
881
+ }
882
+ }
883
+ break;
884
+ }
885
+ case '$state':
886
+ {
887
+ // Handled internally
888
+ break;
889
+ }
890
+ case 'is':
891
+ {
892
+ // Currently only in Chrome
893
+ // Handled during element creation
894
+ break;
895
+ }
896
+ case '$custom':
897
+ {
898
+ Object.assign(elem, attVal);
899
+ break;
900
+ }
901
+ case '$define':
902
+ {
903
+ if (!('localName' in elem)) {
904
+ throw new Error('Element expected for `$define`');
905
+ }
906
+ const localName = elem.localName.toLowerCase();
907
+ // Note: customized built-ins sadly not working yet
908
+ const customizedBuiltIn = !localName.includes('-');
909
+
910
+ // We check attribute in case this is a preexisting DOM element
911
+ // const {is} = atts;
912
+ let is;
913
+ if (customizedBuiltIn) {
914
+ is = elem.getAttribute('is');
915
+ if (!is) {
916
+ if (!Object.hasOwn(atts, 'is')) {
917
+ throw new TypeError(`Expected \`is\` with \`$define\` on built-in; args: ${JSON.stringify(args)}`);
918
+ }
919
+ atts.is = /** @type {string} */checkPluginValue(elem, 'is', atts.is, opts);
920
+ elem.setAttribute('is', atts.is);
921
+ ({
922
+ is
923
+ } = atts);
924
+ }
925
+ }
926
+ const def = customizedBuiltIn ? (/** @type {string} */is) : localName;
927
+ if (window.customElements.get(def)) {
928
+ break;
929
+ }
930
+
931
+ /**
932
+ * @param {DefineUserConstructor} [cnstrct]
933
+ * @returns {DefineConstructor}
934
+ */
935
+ const getConstructor = cnstrct => {
936
+ /* c8 ignore next 3 */
937
+ if (!doc) {
938
+ throw new Error('No document object');
939
+ }
940
+ const baseClass = typeof options === 'object' && typeof options.extends === 'string' ? (/** @type {typeof HTMLElement} */doc.createElement(options.extends).constructor) : customizedBuiltIn ? (/** @type {typeof HTMLElement} */doc.createElement(localName).constructor) : window.HTMLElement;
941
+
942
+ /**
943
+ * Class wrapping base class.
944
+ */
945
+ return cnstrct ? class extends baseClass {
946
+ /**
947
+ * Calls user constructor.
948
+ */
949
+ constructor() {
950
+ super();
951
+ /** @type {DefineUserConstructor} */
952
+ cnstrct.call(this);
953
+ }
954
+ } : class extends baseClass {};
955
+ };
956
+
957
+ /** @type {DefineConstructor|DefineUserConstructor|DefineMixin} */
958
+ let cnstrctr;
959
+
960
+ /**
961
+ * @type {DefineOptions|undefined}
962
+ */
963
+ let options;
964
+ let mixin;
965
+ const defineObj = /** @type {DefineObject} */attVal;
966
+ if (Array.isArray(defineObj)) {
967
+ if (defineObj.length <= 2) {
968
+ [cnstrctr, options] = defineObj;
969
+ if (typeof options === 'string') {
970
+ // Todo: Allow creating a definition without using it;
971
+ // that may be the only reason to have a string here which
972
+ // differs from the `localName` anyways
973
+ options = {
974
+ extends: options
975
+ };
976
+ } else if (options && !Object.hasOwn(options, 'extends')) {
977
+ mixin = options;
978
+ }
979
+ if (typeof cnstrctr === 'object') {
980
+ mixin = cnstrctr;
981
+ cnstrctr = getConstructor();
982
+ }
983
+ } else {
984
+ [cnstrctr, mixin, options] = defineObj;
985
+ if (typeof options === 'string') {
986
+ options = {
987
+ extends: options
988
+ };
989
+ }
990
+ }
991
+ } else if (typeof defineObj === 'function') {
992
+ cnstrctr = /** @type {DefineConstructor} */defineObj;
993
+ } else {
994
+ mixin = defineObj;
995
+ cnstrctr = getConstructor();
996
+ }
997
+ if (!cnstrctr.toString().startsWith('class')) {
998
+ cnstrctr = getConstructor(/** @type {DefineUserConstructor} */cnstrctr);
999
+ }
1000
+ if (!options && customizedBuiltIn) {
1001
+ options = {
1002
+ extends: localName
1003
+ };
1004
+ }
1005
+ if (mixin) {
1006
+ Object.entries(mixin).forEach(([methodName, method]) => {
1007
+ /** @type {DefineConstructor} */cnstrctr.prototype[methodName] = method;
1008
+ });
1009
+ }
1010
+ // console.log('def', def, '::', typeof options === 'object' ? options : undefined);
1011
+ window.customElements.define(def, /** @type {DefineConstructor} */cnstrctr, typeof options === 'object' ? options : undefined);
1012
+ break;
1013
+ }
1014
+ case '$symbol':
1015
+ {
1016
+ const [symbol, func] = /** @type {SymbolArray} */attVal;
1017
+ if (typeof func === 'function') {
1018
+ const funcBound = func.bind(/** @type {HTMLElement} */elem);
1019
+ if (typeof symbol === 'string') {
1020
+ // @ts-expect-error
1021
+ elem[Symbol.for(symbol)] = funcBound;
1022
+ } else {
1023
+ // @ts-expect-error
1024
+ elem[symbol] = funcBound;
1025
+ }
1026
+ } else {
1027
+ const obj = func;
1028
+ obj.elem = /** @type {HTMLElement} */elem;
1029
+ if (typeof symbol === 'string') {
1030
+ // @ts-expect-error
1031
+ elem[Symbol.for(symbol)] = obj;
1032
+ } else {
1033
+ // @ts-expect-error
1034
+ elem[symbol] = obj;
1035
+ }
1036
+ }
1037
+ break;
1038
+ }
1039
+ case '$data':
1040
+ {
1041
+ setMap(/** @type {true|string[]|Map<any, any>|WeakMap<any, any>|DataAttributeObject} */
1042
+ attVal);
1043
+ break;
1044
+ }
1045
+ case '$attribute':
1046
+ {
1047
+ // Attribute node
1048
+ const attr = /** @type {JamilihAttributeNodeValue} */attVal;
1049
+ const node = attr.length === 3 ? doc.createAttributeNS(attr[0], attr[1]) : doc.createAttribute(/** @type {string} */attr[0]);
1050
+ node.value = /** @type {string} */attr.at(-1);
1051
+ nodes[nodes.length] = node;
1052
+ break;
1053
+ }
1054
+ case '$text':
1055
+ {
1056
+ // Todo: Also allow as jml(['a text node']) (or should that become a fragment)?
1057
+ const node = doc.createTextNode(/** @type {string} */attVal);
1058
+ nodes[nodes.length] = node;
1059
+ break;
1060
+ }
1061
+ case '$document':
1062
+ {
1063
+ // Todo: Conditionally create XML document
1064
+ const docNode = doc.implementation.createHTMLDocument();
1065
+ if (!attVal) {
1066
+ throw new Error('Bad attribute value');
1067
+ }
1068
+ const jamlihDoc = /** @type {JamilihDocument} */attVal;
1069
+ if (jamlihDoc.childNodes) {
1070
+ // Remove any extra nodes created by createHTMLDocument().
1071
+ const j = jamlihDoc.childNodes.length;
1072
+ while (docNode.childNodes[j]) {
1073
+ const cn = docNode.childNodes[j];
1074
+ cn.remove();
1075
+ // `j` should stay the same as removing will cause node to be present
1076
+ }
1077
+ jamlihDoc.childNodes.forEach(_childrenToJML(docNode));
1078
+ } else {
1079
+ if (jamlihDoc.$DOCTYPE) {
1080
+ const dt = {
1081
+ $DOCTYPE: jamlihDoc.$DOCTYPE
1082
+ };
1083
+ const doctype = jml(dt);
1084
+ docNode.firstChild?.replaceWith(doctype);
1085
+ }
1086
+ const html = docNode.querySelector('html');
1087
+ const head = html?.querySelector('head');
1088
+ const body = html?.querySelector('body');
1089
+ if (jamlihDoc.title || jamlihDoc.head) {
1090
+ const meta = doc.createElement('meta');
1091
+ // eslint-disable-next-line unicorn/text-encoding-identifier-case -- HTML
1092
+ meta.setAttribute('charset', 'utf-8');
1093
+ head?.append(meta);
1094
+ if (jamlihDoc.title) {
1095
+ docNode.title = jamlihDoc.title; // Appends after meta
1096
+ }
1097
+ if (jamlihDoc.head && head) {
1098
+ // each child of `head` is:
1099
+ // (JamilihArray|TextNodeString|HTMLElement|Comment|ProcessingInstruction|
1100
+ // Text|DocumentFragment|JamilihProcessingInstruction|JamilihDocumentFragment)
1101
+
1102
+ // * @typedef {JamilihDoc|JamilihDoctype|JamilihTextNode|
1103
+ // * JamilihAttributeNode|JamilihOptions|ElementName|HTMLElement|
1104
+ // * JamilihDocumentFragment
1105
+ // * } JamilihFirstArg
1106
+ // appender childJML param is: JamilihArray|JamilihFirstArg
1107
+
1108
+ jamlihDoc.head.forEach(_appendJML(head));
1109
+ }
1110
+ }
1111
+ if (jamlihDoc.body && body) {
1112
+ jamlihDoc.body.forEach(_appendJMLOrText(body));
1113
+ }
1114
+ }
1115
+ if (jamlihDoc.xmlDeclaration) {
1116
+ const {
1117
+ version,
1118
+ encoding,
1119
+ standalone
1120
+ } = jamlihDoc.xmlDeclaration;
1121
+ const xmlDeclarationData = `${version ? ` version="${version}"` : ''}${encoding ? ` encoding="${encoding}"` : ''}${standalone ? ` standalone="yes"` : ''}`.slice(1);
1122
+ const xmlDeclaration = doc.createProcessingInstruction('xml', xmlDeclarationData);
1123
+ docNode.insertBefore(xmlDeclaration, docNode.firstChild);
1124
+ }
1125
+ nodes[nodes.length] = docNode;
1126
+ break;
1127
+ }
1128
+ case '$DOCTYPE':
1129
+ {
1130
+ const doctype = /** @type {JamilihDocumentType} */attVal;
1131
+ const node = doc.implementation.createDocumentType(doctype.name, doctype.publicId || '', doctype.systemId || '');
1132
+ nodes[nodes.length] = node;
1133
+ break;
1134
+ }
1135
+ case '$on':
1136
+ {
1137
+ // Events
1138
+ // Allow for no-op by defaulting to `{}`
1139
+ // eslint-disable-next-line prefer-const -- Ok as mixed
1140
+ for (let [p2, val] of Object.entries(/** @type {OnAttributeObject} */attVal || {})) {
1141
+ if (typeof val === 'function') {
1142
+ val = [val, false];
1143
+ }
1144
+ if (typeof val[0] !== 'function') {
1145
+ throw new TypeError(`Expect a function for \`$on\`; args: ${JSON.stringify(args)}`);
1146
+ }
1147
+ _addEvent(/** @type {HTMLElement} */elem, p2, val[0], val[1]); // element, event name, handler, capturing
1148
+ }
1149
+ break;
1150
+ }
1151
+ case 'className':
1152
+ case 'class':
1153
+ attVal = checkPluginValue(elem, att, /** @type {string} */attVal, opts);
1154
+ if (!_isNullish(attVal)) {
1155
+ elem.className = attVal;
1156
+ }
1157
+ break;
1158
+ case 'dataset':
1159
+ {
1160
+ // Map can be keyed with hyphenated or camel-cased properties
1161
+ /**
1162
+ * @param {DatasetAttributeObject} atVal
1163
+ * @param {string} startProp
1164
+ * @returns {void}
1165
+ */
1166
+ const recurse = (atVal, startProp) => {
1167
+ let prop = '';
1168
+ const pastInitialProp = startProp !== '';
1169
+ Object.keys(atVal).forEach(key => {
1170
+ const value = atVal[key];
1171
+ prop = pastInitialProp ? startProp + key.replaceAll(hyphenForCamelCase, _upperCase).replace(/^([a-z])/u, _upperCase) : startProp + key.replaceAll(hyphenForCamelCase, _upperCase);
1172
+ if (value === null || typeof value !== 'object') {
1173
+ if (!_isNullish(value)) {
1174
+ elem.dataset[prop] = value;
1175
+ }
1176
+ prop = startProp;
1177
+ return;
1178
+ }
1179
+ recurse(value, prop);
1180
+ });
1181
+ };
1182
+ recurse(/** @type {DatasetAttributeObject} */attVal, '');
1183
+ break;
1184
+ // Todo: Disable this by default unless configuration explicitly allows (for security)
1185
+ }
1186
+ // #if IS_REMOVE
1187
+ // Don't remove this `if` block (for sake of no-innerHTML build)
1188
+ case 'innerHTML':
1189
+ if (!_isNullish(attVal)) {
1190
+ // // eslint-disable-next-line no-unsanitized/property
1191
+ elem.innerHTML = attVal;
1192
+ }
1193
+ break;
1194
+ // #endif
1195
+ case 'htmlFor':
1196
+ case 'for':
1197
+ if (elStr === 'label') {
1198
+ attVal = checkPluginValue(elem, att, /** @type {string} */attVal, opts);
1199
+ if (!_isNullish(attVal)) {
1200
+ elem.htmlFor = attVal;
1201
+ }
1202
+ break;
1203
+ }
1204
+ attVal = checkPluginValue(elem, att, /** @type {string} */attVal, opts);
1205
+ elem.setAttribute(att, attVal);
1206
+ break;
1207
+ case 'xmlns':
1208
+ // Already handled
1209
+ break;
1210
+ default:
1211
+ {
1212
+ if (att.startsWith('on')) {
1213
+ attVal = checkPluginValue(elem, att, /** @type {HandlerAttributeValue} */attVal, opts);
1214
+ elem[att] = attVal;
1215
+ // _addEvent(elem, att.slice(2), attVal, false); // This worked, but perhaps the user wishes only one event
1216
+ break;
1217
+ }
1218
+ if (att === 'style') {
1219
+ attVal = /** @type {string} */
1220
+ checkPluginValue(elem, att, /** @type {StyleAttributeValue} */attVal, opts);
1221
+ if (_isNullish(attVal)) {
1222
+ break;
1223
+ }
1224
+ if (typeof attVal === 'object') {
1225
+ for (const [p2, styleVal] of Object.entries(attVal)) {
1226
+ if (!_isNullish(styleVal)) {
1227
+ // Todo: Handle aggregate properties like "border"
1228
+ if (p2 === 'float') {
1229
+ elem.style.cssFloat = styleVal;
1230
+ elem.style.styleFloat = styleVal; // Harmless though we could make conditional on older IE instead
1231
+ } else {
1232
+ elem.style[p2.replaceAll(hyphenForCamelCase, _upperCase)] = styleVal;
1233
+ }
1234
+ }
1235
+ }
1236
+ break;
1237
+ }
1238
+
1239
+ // setAttribute unfortunately erases any existing styles
1240
+ elem.setAttribute(att, attVal);
1241
+ /*
1242
+ // The following reorders which is troublesome for serialization, e.g., as used in our testing
1243
+ if (elem.style.cssText !== undefined) {
1244
+ elem.style.cssText += attVal;
1245
+ } else { // Opera
1246
+ elem.style += attVal;
1247
+ }
1248
+ */
1249
+ break;
1250
+ }
1251
+ const pluginName = att;
1252
+ const matchingPlugin = getMatchingPlugin(opts, pluginName);
1253
+ if (matchingPlugin) {
1254
+ matchingPlugin.set({
1255
+ opts,
1256
+ element: (/** @type {HTMLElement} */nodes[0]),
1257
+ attribute: {
1258
+ name: pluginName,
1259
+ value: (/** @type {PluginReference} */attVal)
1260
+ }
1261
+ });
1262
+ break;
1263
+ }
1264
+ attVal = checkPluginValue(elem, att, /** @type {string} */attVal, opts);
1265
+ elem.setAttribute(att, attVal);
1266
+ break;
1267
+ }
1268
+ }
1269
+ }
1270
+ }
1271
+
1272
+ /**
1273
+ * @type {JamilihReturn[]}
1274
+ */
1275
+ const nodes = [];
1276
+
1277
+ /** @type {string} */
1278
+ let elStr;
1279
+
1280
+ /** @type {JamilihOptions} */
1281
+ let opts;
1282
+ let isRoot = false;
1283
+ let argStart = 0;
1284
+ if (_getType(args[0]) === 'object' && Object.keys(args[0]).some(key => possibleOptions.includes(key))) {
1285
+ opts = /** @type {JamilihOptions} */args[0];
1286
+ if (opts.$state === undefined) {
1287
+ isRoot = true;
1288
+ opts.$state = 'root';
1289
+ }
1290
+ if (Array.isArray(opts.$map)) {
1291
+ opts.$map = {
1292
+ root: opts.$map
1293
+ };
1294
+ }
1295
+ if ('$plugins' in opts) {
1296
+ if (!Array.isArray(opts.$plugins)) {
1297
+ throw new TypeError(`\`$plugins\` must be an array; args: ${JSON.stringify(args)}`);
1298
+ }
1299
+ opts.$plugins.forEach(pluginObj => {
1300
+ if (!pluginObj || typeof pluginObj !== 'object') {
1301
+ throw new TypeError(`Plugin must be an object; args: ${JSON.stringify(args)}`);
1302
+ }
1303
+ if (!pluginObj.name || !pluginObj.name.startsWith('$_')) {
1304
+ throw new TypeError(`Plugin object name must be present and begin with \`$_\`; args: ${JSON.stringify(args)}`);
1305
+ }
1306
+ if (typeof pluginObj.set !== 'function') {
1307
+ throw new TypeError(`Plugin object must have a \`set\` method; args: ${JSON.stringify(args)}`);
1308
+ }
1309
+ });
1310
+ }
1311
+ argStart = 1;
1312
+ } else {
1313
+ opts = {
1314
+ $state: undefined
1315
+ };
1316
+ }
1317
+ const argc = args.length;
1318
+ const defaultMap = opts.$map && /** @type {MapWithRoot} */opts.$map.root;
1319
+
1320
+ /**
1321
+ * @param {true|string[]|Map<any, any>|WeakMap<any, any>|DataAttributeObject} dataVal
1322
+ * @returns {void}
1323
+ */
1324
+ const setMap = dataVal => {
1325
+ let map, obj;
1326
+ const defMap = /** @type {[Map<HTMLElement, any> | WeakMap<HTMLElement, any>, any]} */defaultMap;
1327
+ // Boolean indicating use of default map and object
1328
+ if (dataVal === true) {
1329
+ [map, obj] = defMap;
1330
+ } else if (Array.isArray(dataVal)) {
1331
+ // Array of strings mapping to default
1332
+ if (typeof dataVal[0] === 'string') {
1333
+ dataVal.forEach(dVal => {
1334
+ setMap(/** @type {MapWithRoot} */opts.$map[dVal]);
1335
+ });
1336
+ return;
1337
+ // Array of Map and non-map data object
1338
+ }
1339
+ map = dataVal[0] || defMap[0];
1340
+ obj = dataVal[1] || defMap[1];
1341
+ // Map
1342
+ } else if (/^\[object (?:Weak)?Map\]$/u.test([].toString.call(dataVal))) {
1343
+ map = dataVal;
1344
+ obj = defMap[1];
1345
+ // Non-map data object
1346
+ } else {
1347
+ map = defMap[0];
1348
+ obj = dataVal;
1349
+ }
1350
+ /** @type {Map<HTMLElement, any> | WeakMap<HTMLElement, any>} */
1351
+ map.set(/** @type {HTMLElement} */
1352
+ elem, obj);
1353
+ };
1354
+ for (let i = argStart; i < argc; i++) {
1355
+ let arg = args[i];
1356
+ const type = _getType(arg);
1357
+ switch (type) {
1358
+ case 'null':
1359
+ // null always indicates a place-holder (only needed for last argument if want array returned)
1360
+ if (i === argc - 1) {
1361
+ // Casting needing unless changing `jml()` signature with overloads
1362
+ return /** @type {ArbitraryValue} */nodes.length <= 1 ? nodes[0]
1363
+ // eslint-disable-next-line unicorn/no-array-callback-reference
1364
+ : nodes.reduce(_fragReducer, doc.createDocumentFragment()); // nodes;
1365
+ }
1366
+ throw new TypeError(`\`null\` values not allowed except as final Jamilih argument; index ${i} on args: ${JSON.stringify(args)}`);
1367
+ case 'string':
1368
+ // Strings normally indicate elements
1369
+ switch (arg) {
1370
+ case '!':
1371
+ nodes[nodes.length] = doc.createComment(/** @type {string} */args[++i]);
1372
+ break;
1373
+ case '?':
1374
+ {
1375
+ arg = /** @type {string} */args[++i];
1376
+ let procValue = /** @type {string} */args[++i];
1377
+ const val = procValue;
1378
+ if (val && typeof val === 'object') {
1379
+ const procValues = [];
1380
+ for (const [p, procInstVal] of Object.entries(val)) {
1381
+ procValues.push(p + '=' + '"' +
1382
+ // https://www.w3.org/TR/xml-stylesheet/#NT-PseudoAttValue
1383
+ procInstVal.replaceAll('"', '&quot;') + '"');
1384
+ }
1385
+ procValue = procValues.join(' ');
1386
+ }
1387
+ // Firefox allows instructions with ">" in this method, but not if placed directly!
1388
+ try {
1389
+ nodes[nodes.length] = doc.createProcessingInstruction(arg, procValue);
1390
+ } catch (e) {
1391
+ // Getting NotSupportedError in IE, so we try to imitate a processing instruction with a comment
1392
+ // innerHTML didn't work
1393
+ // var elContainer = doc.createElement('div');
1394
+ // elContainer.innerHTML = '<?' + doc.createTextNode(arg + ' ' + procValue).nodeValue + '?>';
1395
+ // nodes[nodes.length] = elContainer.innerHTML;
1396
+ // Todo: any other way to resolve? Just use XML?
1397
+ nodes[nodes.length] = doc.createComment('?' + arg + ' ' + procValue + '?');
1398
+ }
1399
+ break;
1400
+ // Browsers don't support doc.createEntityReference, so we just use this as a convenience
1401
+ }
1402
+ case '&':
1403
+ nodes[nodes.length] = _createSafeReference('entity', '', /** @type {string} */
1404
+ args[++i]);
1405
+ break;
1406
+ case '#':
1407
+ // // Decimal character reference - ['#', '01234'] // &#01234; // probably easier to use JavaScript Unicode escapes
1408
+ nodes[nodes.length] = _createSafeReference('decimal', arg, String(args[++i]));
1409
+ break;
1410
+ case '#x':
1411
+ // Hex character reference - ['#x', '123a'] // &#x123a; // probably easier to use JavaScript Unicode escapes
1412
+ nodes[nodes.length] = _createSafeReference('hexadecimal', arg, /** @type {string} */
1413
+ args[++i]);
1414
+ break;
1415
+ case '![':
1416
+ // '![', ['escaped <&> text'] // <![CDATA[escaped <&> text]]>
1417
+ // CDATA valid in XML only, so we'll just treat as text for mutual compatibility
1418
+ // Todo: config (or detection via some kind of doc.documentType property?) of whether in XML
1419
+ try {
1420
+ nodes[nodes.length] = doc.createCDATASection(/** @type {string} */args[++i]);
1421
+ } catch (e2) {
1422
+ nodes[nodes.length] = doc.createTextNode(/** @type {string} */
1423
+ args[i]); // i already incremented
1424
+ }
1425
+ break;
1426
+ case '':
1427
+ nodes[nodes.length] = elem = doc.createDocumentFragment();
1428
+ // Todo: Report to plugins
1429
+ opts.$state = 'fragment';
1430
+ break;
1431
+ default:
1432
+ {
1433
+ // An element
1434
+ elStr = /** @type {string} */arg;
1435
+ const atts = args[i + 1];
1436
+ if (atts && _getType(atts) === 'object' && /** @type {JamilihAttributes} */atts.is) {
1437
+ const {
1438
+ is
1439
+ } = /** @type {JamilihAttributes} */atts;
1440
+ /* c8 ignore next 4 */
1441
+ elem = doc.createElementNS
1442
+ // Should create separate file for this
1443
+ /* eslint-disable object-shorthand -- Casting */ ? (/** @type {HTMLElement} */doc.createElementNS(NS_HTML, elStr, {
1444
+ is: (/** @type {string} */is)
1445
+ })
1446
+ /* c8 ignore next 1 */) : doc.createElement(elStr, {
1447
+ is: (/** @type {string} */is)
1448
+ });
1449
+ /* eslint-enable object-shorthand -- Casting */
1450
+ } else /* c8 ignore next */if (doc.createElementNS) {
1451
+ elem = doc.createElementNS(NS_HTML, elStr);
1452
+ /* c8 ignore next 3 */
1453
+ } else {
1454
+ elem = doc.createElement(elStr);
1455
+ }
1456
+ // Todo: Report to plugins
1457
+ opts.$state = 'element';
1458
+ nodes[nodes.length] = elem; // Add to parent
1459
+ break;
1460
+ }
1461
+ }
1462
+ break;
1463
+ case 'object':
1464
+ {
1465
+ // Non-DOM-element objects indicate attribute-value pairs
1466
+ /* c8 ignore next 3 */
1467
+ if (!arg || typeof arg !== 'object') {
1468
+ throw new Error('Null should not reach here');
1469
+ }
1470
+ const atts = arg;
1471
+ if ('xmlns' in atts) {
1472
+ // We handle this here, as otherwise may lose events, etc.
1473
+ // As namespace of element already set as XHTML, we need to change the namespace
1474
+ // elem.setAttribute('xmlns', atts.xmlns); // Doesn't work
1475
+ // Can't set namespaceURI dynamically, renameNode() is not supported, and setAttribute() doesn't work to change the namespace, so we resort to this hack
1476
+ const xmlnsObj = /** @type {XmlnsAttributeObject} */atts;
1477
+ const replacer = xmlnsObj.xmlns && typeof xmlnsObj.xmlns === 'object' ? _replaceDefiner(xmlnsObj.xmlns) : ' xmlns="' + xmlnsObj.xmlns + '"';
1478
+ // try {
1479
+ // Also fix DOMParser to work with text/html
1480
+ elem = nodes[nodes.length - 1] =
1481
+ // Why doesn't `HTMLWindow` have `DOMParser`?
1482
+ new /** @type {import('jsdom').DOMWindow} */win.DOMParser().parseFromString(new /** @type {import('jsdom').DOMWindow} */win.XMLSerializer().serializeToString(elem).
1483
+ // Mozilla adds XHTML namespace
1484
+ replace(' xmlns="' + NS_HTML + '"',
1485
+ // Needed to cast here, despite either overload working
1486
+ /** @type {string} */
1487
+ replacer), 'application/xml').documentElement;
1488
+ // Todo: Report to plugins
1489
+ opts.$state = 'element';
1490
+ // }catch(e) {alert(elem.outerHTML);throw e;}
1491
+ }
1492
+ _checkAtts(/** @type {JamilihAttributes} */atts);
1493
+ break;
1494
+ }
1495
+ case 'processing-instruction':
1496
+ case 'document':
1497
+ case 'fragment':
1498
+ case 'element':
1499
+ /*
1500
+ 1) Last element always the parent (put null if don't want parent and want to return array) unless only atts and children (no other elements)
1501
+ 2) Individual elements (DOM elements or sequences of string[/object/array]) get added to parent first-in, first-added
1502
+ */
1503
+ if (i === 0) {
1504
+ // Allow wrapping of element, fragment, or document
1505
+ elem = /** @type {Document|DocumentFragment|HTMLElement} */arg;
1506
+ // Todo: Report to plugins and change for document/fragment
1507
+ opts.$state = 'element';
1508
+ }
1509
+ if (i === argc - 1 || i === argc - 2 && args[i + 1] === null) {
1510
+ // parent
1511
+ const elsl = nodes.length;
1512
+ for (let k = 0; k < elsl; k++) {
1513
+ _appendNode(/** @type {Document|DocumentFragment|HTMLElement} */arg, nodes[k]);
1514
+ }
1515
+ } else {
1516
+ nodes[nodes.length] = /** @type {Document|DocumentFragment|HTMLElement} */arg;
1517
+ }
1518
+ break;
1519
+ case 'array':
1520
+ {
1521
+ // Arrays or arrays of arrays indicate child nodes
1522
+ const child = /** @type {JamilihChildren} */arg;
1523
+ const cl = child.length;
1524
+ for (let j = 0; j < cl; j++) {
1525
+ // Go through children array container to handle elements
1526
+ const childContent = child[j];
1527
+ const childContentType = typeof childContent;
1528
+ if (childContent === null || _isNullish(childContent)) {
1529
+ throw new TypeError(`Bad children (parent array: ${JSON.stringify(args)}; index ${j} of child: ${JSON.stringify(child)})`);
1530
+ }
1531
+ switch (childContentType) {
1532
+ // Todo: determine whether null or function should have special handling or be converted to text
1533
+ case 'string':
1534
+ case 'number':
1535
+ case 'boolean':
1536
+ _appendNode(elem, doc.createTextNode(String(childContent)));
1537
+ break;
1538
+ default:
1539
+ // bigint, symbol, function
1540
+ if (typeof childContent !== 'object') {
1541
+ throw new TypeError(`Bad children (parent array: ${JSON.stringify(args)}; index ${j} of child: ${JSON.stringify(child)})`);
1542
+ }
1543
+ if (Array.isArray(childContent)) {
1544
+ // Arrays representing child elements
1545
+ opts.$state = 'children';
1546
+ _appendNode(elem, jml(opts, ...childContent));
1547
+ } else if ('#' in childContent) {
1548
+ // Fragment
1549
+ opts.$state = 'fragmentChildren';
1550
+ _appendNode(elem, jml(opts, childContent['#']));
1551
+ } else {
1552
+ // Single DOM element children or plugin
1553
+ let newChildContent;
1554
+ if (!('nodeType' in childContent)) {
1555
+ newChildContent = /** @type {string} */
1556
+ checkPluginValue(elem, null, childContent, opts, 'children');
1557
+ }
1558
+ _appendNode(elem, /** @type {string|HTMLElement|DocumentFragment|Comment} */
1559
+ newChildContent || childContent);
1560
+ }
1561
+ break;
1562
+ }
1563
+ }
1564
+ break;
1565
+ }
1566
+ default:
1567
+ throw new TypeError(`Unexpected type: ${type}; arg: ${arg}; index ${i} on args: ${JSON.stringify(args)}`);
1568
+ }
1569
+ }
1570
+ const ret = nodes[0] || elem;
1571
+ if (isRoot && opts.$map && /** @type {MapWithRoot} */opts.$map.root) {
1572
+ setMap(true);
1573
+ }
1574
+
1575
+ // Casting needing unless changing `jml()` signature with overloads
1576
+ return /** @type {ArbitraryValue} */ret;
1577
+ };
1578
+
1579
+ /**
1580
+ * Configuration object.
1581
+ * @typedef {object} ToJmlConfig
1582
+ * @property {boolean} [stringOutput=false] Whether to output the Jamilih object as a string.
1583
+ * @property {boolean} [reportInvalidState=true] If true (the default), will report invalid state errors
1584
+ * @property {boolean} [stripWhitespace=false] Strip whitespace for text nodes
1585
+ */
1586
+
1587
+ /**
1588
+ * @typedef {[namespace: string|null, name: string, value?: string]} JamilihAttributeNodeValue
1589
+ */
1590
+
1591
+ /**
1592
+ * @typedef {{
1593
+ * $attribute: JamilihAttributeNodeValue
1594
+ * }} JamilihAttributeNode
1595
+ */
1596
+
1597
+ /**
1598
+ * @typedef {{
1599
+ * $text: string
1600
+ * }} JamilihTextNode
1601
+ */
1602
+
1603
+ /**
1604
+ * @typedef {['![', string]} JamilihCDATANode
1605
+ */
1606
+
1607
+ /**
1608
+ * @typedef {['&', string]} JamilihEntityReference
1609
+ */
1610
+
1611
+ /**
1612
+ * @typedef {[code: '?', target: string, value: string]} JamilihProcessingInstruction
1613
+ */
1614
+
1615
+ /**
1616
+ * @typedef {[code: '!', value: string]} JamilihComment
1617
+ */
1618
+
1619
+ /**
1620
+ * @typedef {{
1621
+ * nodeType: number,
1622
+ * nodeName: string
1623
+ * }} Entity
1624
+ */
1625
+
1626
+ /* eslint-disable no-shadow, unicorn/custom-error-definition */
1627
+ /**
1628
+ * Polyfill for `DOMException`.
1629
+ */
1630
+ class DOMException extends Error {
1631
+ /* eslint-enable no-shadow, unicorn/custom-error-definition */
1632
+ /**
1633
+ * @param {string} message
1634
+ * @param {string} name
1635
+ */
1636
+ constructor(message, name) {
1637
+ super(message);
1638
+ this.code = 0;
1639
+ // eslint-disable-next-line unicorn/custom-error-definition
1640
+ this.name = name;
1641
+ }
1642
+ }
1643
+
1644
+ /**
1645
+ * @typedef {JamilihArray|JamilihDoctype|
1646
+ * JamilihCDATANode|JamilihEntityReference|JamilihProcessingInstruction|
1647
+ * JamilihComment|JamilihDocumentFragment} JamilihChildType
1648
+ */
1649
+
1650
+ /**
1651
+ * @typedef {JamilihDoc|JamilihAttributeNode|JamilihChildType} JamilihType
1652
+ */
1653
+
1654
+ /**
1655
+ * Converts a DOM object or a string of HTML into a Jamilih object (or string).
1656
+ * @param {string|HTMLElement|Node|Entity} nde If a string, will parse as document
1657
+ * @param {ToJmlConfig} [config] Configuration object
1658
+ * @throws {TypeError}
1659
+ * @returns {JamilihType|string} Array containing the elements which represent
1660
+ * a Jamilih object, or, if `stringOutput` is true, it will be the stringified
1661
+ * version of such an object
1662
+ */
1663
+ jml.toJML = function (nde, {
1664
+ stringOutput = false,
1665
+ reportInvalidState = true,
1666
+ stripWhitespace = false
1667
+ } = {}) {
1668
+ if (!win) {
1669
+ throw new Error('No window object set');
1670
+ }
1671
+ if (typeof nde === 'string') {
1672
+ nde = new /** @type {import('jsdom').DOMWindow} */win.DOMParser().parseFromString(nde, 'text/html'); // todo: Give option for XML once implemented and change JSDoc to allow for Element
1673
+ }
1674
+ const dom = /** @type {HTMLElement|Node|Entity} */nde;
1675
+
1676
+ /**
1677
+ * @todo Find more specific type than `any`
1678
+ * @typedef {{[key: (number|string)]: any}} IndexableObject
1679
+ */
1680
+
1681
+ const ret = /** @type {IndexableObject} */[];
1682
+ let parent = ret;
1683
+ let parentIdx = 0;
1684
+
1685
+ /**
1686
+ * @param {string} msg
1687
+ * @throws {DOMException}
1688
+ * @returns {void}
1689
+ */
1690
+ function invalidStateError(msg) {
1691
+ // These are probably only necessary if working with text/html
1692
+ if (reportInvalidState) {
1693
+ // INVALID_STATE_ERR per section 9.3 XHTML 5: http://www.w3.org/TR/html5/the-xhtml-syntax.html
1694
+ const e = new DOMException(msg, 'INVALID_STATE_ERR');
1695
+ e.code = 11;
1696
+ throw e;
1697
+ }
1698
+ }
1699
+
1700
+ /**
1701
+ *
1702
+ * @param {JamilihDocumentType} obj
1703
+ * @param {DocumentType} node
1704
+ * @returns {void}
1705
+ */
1706
+ function addExternalID(obj, node) {
1707
+ if (node.systemId.includes('"') && node.systemId.includes("'")) {
1708
+ invalidStateError('systemId cannot have both single and double quotes.');
1709
+ }
1710
+ const {
1711
+ publicId,
1712
+ systemId
1713
+ } = node;
1714
+ if (systemId) {
1715
+ obj.systemId = systemId;
1716
+ }
1717
+ if (publicId) {
1718
+ obj.publicId = publicId;
1719
+ }
1720
+ }
1721
+
1722
+ /**
1723
+ *
1724
+ * @param {ArbitraryValue} val
1725
+ * @returns {void}
1726
+ */
1727
+ function set(val) {
1728
+ parent[parentIdx] = val;
1729
+ parentIdx++;
1730
+ }
1731
+
1732
+ /**
1733
+ * @returns {void}
1734
+ */
1735
+ function setChildren() {
1736
+ set([]);
1737
+ parent = parent[parentIdx - 1];
1738
+ parentIdx = 0;
1739
+ }
1740
+
1741
+ /**
1742
+ *
1743
+ * @param {string} prop1
1744
+ * @param {string} [prop2]
1745
+ * @returns {void}
1746
+ */
1747
+ function setObj(prop1, prop2) {
1748
+ parent = parent[parentIdx - 1][prop1];
1749
+ parentIdx = 0;
1750
+ if (prop2) {
1751
+ parent = parent[prop2];
1752
+ }
1753
+ }
1754
+
1755
+ /**
1756
+ *
1757
+ * @param {Node|Entity} nodeOrEntity
1758
+ * @param {Object<string, string|null>} namespaces
1759
+ * @throws {TypeError}
1760
+ * @returns {void}
1761
+ */
1762
+ function parseDOM(nodeOrEntity, namespaces) {
1763
+ // namespaces = clone(namespaces) || {}; // Ensure we're working with a copy, so different levels in the hierarchy can treat it differently
1764
+
1765
+ /*
1766
+ if ((nodeOrEntity.prefix && nodeOrEntity.prefix.includes(':')) || (nodeOrEntity.localName && nodeOrEntity.localName.includes(':'))) {
1767
+ invalidStateError('Prefix cannot have a colon');
1768
+ }
1769
+ */
1770
+
1771
+ const type = 'nodeType' in nodeOrEntity ? nodeOrEntity.nodeType : null;
1772
+ if (!type) {
1773
+ throw new TypeError('Not an XML type');
1774
+ }
1775
+ if (type === 5) {
1776
+ // ENTITY REFERENCE (though not in browsers (was already resolved
1777
+ // anyways), ok to keep for parity with our "entity" shorthand)
1778
+ set(['&', nodeOrEntity.nodeName]);
1779
+ return;
1780
+ }
1781
+ namespaces = {
1782
+ ...namespaces
1783
+ };
1784
+ const xmlChars = /^([\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD]|[\uD800-\uDBFF][\uDC00-\uDFFF])*$/u; // eslint-disable-line no-control-regex
1785
+ if ([2, 3, 4, 7, 8].includes(type) && /** @type {Node} */nodeOrEntity.nodeValue && !xmlChars.test(/** @type {Node} */nodeOrEntity.nodeValue)) {
1786
+ invalidStateError('Node has bad XML character value');
1787
+ }
1788
+
1789
+ /**
1790
+ * @type {IndexableObject}
1791
+ */
1792
+ let tmpParent;
1793
+
1794
+ /**
1795
+ * @type {Integer}
1796
+ */
1797
+ let tmpParentIdx;
1798
+
1799
+ /**
1800
+ * @returns {void}
1801
+ */
1802
+ function setTemp() {
1803
+ tmpParent = parent;
1804
+ tmpParentIdx = parentIdx;
1805
+ }
1806
+ /**
1807
+ * @returns {void}
1808
+ */
1809
+ function resetTemp() {
1810
+ parent = tmpParent;
1811
+ parentIdx = tmpParentIdx;
1812
+ parentIdx++; // Increment index in parent container of this element
1813
+ }
1814
+ switch (type) {
1815
+ case 1:
1816
+ {
1817
+ // ELEMENT
1818
+ const node = /** @type {HTMLElement} */nodeOrEntity;
1819
+ setTemp();
1820
+ const nodeName = node.nodeName.toLowerCase(); // Todo: for XML, should not lower-case
1821
+
1822
+ setChildren(); // Build child array since elements are, except at the top level, encapsulated in arrays
1823
+ set(nodeName);
1824
+
1825
+ /**
1826
+ * @type {{[key: string]: string|null} & {xmlns?: string|null}}
1827
+ */
1828
+ const start = {};
1829
+ let hasNamespaceDeclaration = false;
1830
+ if (namespaces[node.prefix || ''] !== node.namespaceURI) {
1831
+ namespaces[node.prefix || ''] = node.namespaceURI;
1832
+ if (node.prefix) {
1833
+ start['xmlns:' + node.prefix] = node.namespaceURI;
1834
+ } else if (node.namespaceURI) {
1835
+ start.xmlns = node.namespaceURI;
1836
+ } else {
1837
+ start.xmlns = null;
1838
+ }
1839
+ hasNamespaceDeclaration = true;
1840
+ }
1841
+ if (node.attributes.length) {
1842
+ set([...node.attributes].reduce(function (obj, att) {
1843
+ obj[att.name] = att.value; // Attr.nodeName and Attr.nodeValue are deprecated as of DOM4 as Attr no longer inherits from Node, so we can safely use name and value
1844
+ return obj;
1845
+ }, start));
1846
+ } else if (hasNamespaceDeclaration) {
1847
+ set(start);
1848
+ }
1849
+ const {
1850
+ childNodes
1851
+ } = node;
1852
+ if (childNodes.length) {
1853
+ setChildren(); // Element children array container
1854
+ [...childNodes].forEach(function (childNode) {
1855
+ parseDOM(childNode, namespaces);
1856
+ });
1857
+ }
1858
+ resetTemp();
1859
+ break;
1860
+ }
1861
+ case 2:
1862
+ {
1863
+ // ATTRIBUTE (should only get here if passing in an attribute node)
1864
+ const node = /** @type {Attr} */nodeOrEntity;
1865
+ set({
1866
+ $attribute: [node.namespaceURI, node.name, node.value]
1867
+ });
1868
+ break;
1869
+ }
1870
+ case 3:
1871
+ {
1872
+ // TEXT
1873
+ const node = /** @type {Text} */nodeOrEntity;
1874
+ /* c8 ignore next 3 */
1875
+ if (!node.nodeValue) {
1876
+ throw new Error('Unexpected null comment value');
1877
+ }
1878
+ if (stripWhitespace && /^\s+$/u.test(node.nodeValue)) {
1879
+ set('');
1880
+ return;
1881
+ }
1882
+ set(node.nodeValue);
1883
+ break;
1884
+ }
1885
+ case 4:
1886
+ {
1887
+ // CDATA
1888
+ const node = /** @type {CDATASection} */nodeOrEntity;
1889
+ if (node.nodeValue?.includes(']]' + '>')) {
1890
+ invalidStateError('CDATA cannot end with closing ]]>');
1891
+ }
1892
+ set(['![', node.nodeValue]);
1893
+ break;
1894
+ }
1895
+ // case 5:
1896
+ // Handled earlier
1897
+ case 7:
1898
+ {
1899
+ // PROCESSING INSTRUCTION
1900
+ const node = /** @type {ProcessingInstruction} */nodeOrEntity;
1901
+ if (/^xml$/iu.test(node.target)) {
1902
+ invalidStateError('Processing instructions cannot be "xml".');
1903
+ }
1904
+ if (node.target.includes('?>')) {
1905
+ invalidStateError('Processing instruction targets cannot include ?>');
1906
+ }
1907
+ if (node.target.includes(':')) {
1908
+ invalidStateError('The processing instruction target cannot include ":"');
1909
+ }
1910
+ if (node.data.includes('?>')) {
1911
+ invalidStateError('Processing instruction data cannot include ?>');
1912
+ }
1913
+ set(['?', node.target, node.data]); // Todo: Could give option to attempt to convert value back into object if has pseudo-attributes
1914
+ break;
1915
+ }
1916
+ case 8:
1917
+ {
1918
+ // COMMENT
1919
+ const node = /** @type {Comment} */nodeOrEntity;
1920
+ /* c8 ignore next 3 */
1921
+ if (!node.nodeValue) {
1922
+ throw new Error('Unexpected null comment value');
1923
+ }
1924
+ if (node.nodeValue.includes('--') || node.nodeValue.length && node.nodeValue.lastIndexOf('-') === node.nodeValue.length - 1) {
1925
+ invalidStateError('Comments cannot include --');
1926
+ }
1927
+ set(['!', node.nodeValue]);
1928
+ break;
1929
+ }
1930
+ case 9:
1931
+ {
1932
+ // DOCUMENT
1933
+ const node = /** @type {Document} */nodeOrEntity;
1934
+ setTemp();
1935
+ const docObj = {
1936
+ $document: {
1937
+ childNodes: []
1938
+ }
1939
+ };
1940
+ set(docObj); // doc.implementation.createHTMLDocument
1941
+
1942
+ // Set position to fragment's array children
1943
+ setObj('$document', 'childNodes');
1944
+ const {
1945
+ childNodes
1946
+ } = node;
1947
+ if (!childNodes.length) {
1948
+ invalidStateError('Documents must have a child node');
1949
+ }
1950
+ // set({$xmlDocument: []}); // doc.implementation.createDocument // Todo: use this conditionally
1951
+
1952
+ [...childNodes].forEach(function (childNode) {
1953
+ // Can't just do documentElement as there may be doctype, comments, etc.
1954
+ // No need for setChildren, as we have already built the container array
1955
+ parseDOM(childNode, namespaces);
1956
+ });
1957
+ resetTemp();
1958
+ break;
1959
+ }
1960
+ case 10:
1961
+ {
1962
+ // DOCUMENT TYPE
1963
+ const node = /** @type {DocumentType} */nodeOrEntity;
1964
+ setTemp();
1965
+
1966
+ // Can create directly by doc.implementation.createDocumentType
1967
+ const start = {
1968
+ $DOCTYPE: {
1969
+ name: /** @type {DocumentType} */node.name
1970
+ }
1971
+ };
1972
+ const pubIdChar = /^(\u0020|\u000D|\u000A|[a-zA-Z0-9]|[-'()+,./:=?;!*#@$_%])*$/u; // eslint-disable-line no-control-regex
1973
+ if (!pubIdChar.test(/** @type {DocumentType} */node.publicId)) {
1974
+ invalidStateError('A publicId must have valid characters.');
1975
+ }
1976
+ addExternalID(start.$DOCTYPE, node);
1977
+ // Fit in internal subset along with entities?: probably don't need as these would only differ if from DTD, and we're not rebuilding the DTD
1978
+ set(start); // Auto-generate the internalSubset instead?
1979
+
1980
+ resetTemp();
1981
+ break;
1982
+ }
1983
+ case 11:
1984
+ {
1985
+ // DOCUMENT FRAGMENT
1986
+ const node = /** @type {DocumentFragment} */nodeOrEntity;
1987
+ setTemp();
1988
+ set({
1989
+ '#': []
1990
+ });
1991
+
1992
+ // Set position to fragment's array children
1993
+ setObj('#');
1994
+ const {
1995
+ childNodes
1996
+ } = node;
1997
+ [...childNodes].forEach(function (childNode) {
1998
+ // No need for setChildren, as we have already built the container array
1999
+ parseDOM(childNode, namespaces);
2000
+ });
2001
+ resetTemp();
2002
+ break;
2003
+ }
2004
+ default:
2005
+ throw new TypeError('Not an XML type');
2006
+ }
2007
+ }
2008
+ parseDOM(dom, {});
2009
+ if (stringOutput) {
2010
+ return JSON.stringify(ret[0]);
2011
+ }
2012
+ return ret[0];
2013
+ };
2014
+
2015
+ /**
2016
+ * @param {string|HTMLElement} dom
2017
+ * @param {ToJmlConfig} [config]
2018
+ * @returns {string}
2019
+ */
2020
+ jml.toJMLString = function (dom, config) {
2021
+ return /** @type {string} */jml.toJML(dom, Object.assign(config || {}, {
2022
+ stringOutput: true
2023
+ }));
2024
+ };
2025
+
2026
+ /**
2027
+ *
2028
+ * @param {JamilihArray} args
2029
+ * @returns {JamilihReturn}
2030
+ */
2031
+ jml.toDOM = function (...args) {
2032
+ // Alias for jml()
2033
+ return jml(...args);
2034
+ };
2035
+
2036
+ /**
2037
+ *
2038
+ * @param {JamilihArray} args
2039
+ * @returns {string}
2040
+ */
2041
+ jml.toHTML = function (...args) {
2042
+ // Todo: Replace this with version of jml() that directly builds a string
2043
+ const ret = jml(...args);
2044
+ switch (ret.nodeType) {
2045
+ case 1:
2046
+ {
2047
+ // Element
2048
+ // Todo: deal with serialization of properties like 'selected',
2049
+ // 'checked', 'value', 'defaultValue', 'for', 'dataset', 'on*',
2050
+ // 'style'! (i.e., need to build a string ourselves)
2051
+ return /** @type {HTMLElement} */ret.outerHTML;
2052
+ }
2053
+ case 2:
2054
+ {
2055
+ // ATTR
2056
+ return `${/** @type {Attr} */ret.name}="${/** @type {Attr} */ret.value.replaceAll('"', '&quot;')}"`;
2057
+ }
2058
+ case 3:
2059
+ {
2060
+ // TEXT
2061
+ // Fallthrough
2062
+ // } case 4: { // CDATA
2063
+ /* c8 ignore next 3 */
2064
+ if (!ret.nodeValue) {
2065
+ throw new TypeError('Unexpected null Text node');
2066
+ }
2067
+ return /** @type {Text|CDATASection} */ret.nodeValue;
2068
+ // case 5: // Entity Reference Node
2069
+ // No 6: Entity Node
2070
+ // No 12: Notation Node
2071
+ }
2072
+ case 7:
2073
+ {
2074
+ // PROCESSING INSTRUCTION
2075
+ const node = /** @type {ProcessingInstruction} */ret;
2076
+ return `<?${node.target} ${node.data}?>`;
2077
+ // } case 8: { // Comment
2078
+ // return `<!--${ret.nodeValue}-->`;
2079
+ // eslint-disable-next-line sonarjs/no-fallthrough
2080
+ }
2081
+ case 9:
2082
+ case 11:
2083
+ {
2084
+ // DOCUMENT FRAGMENT
2085
+ const node = /** @type {DocumentFragment} */ret;
2086
+ return [...node.childNodes].map(childNode => {
2087
+ return jml.toHTML(/** @type {JamilihFirstArgument} */childNode);
2088
+ }).join('');
2089
+ }
2090
+ case 10:
2091
+ {
2092
+ // DOCUMENT TYPE
2093
+ const node = /** @type {DocumentType} */ret;
2094
+ return `<!DOCTYPE ${node.name}${node.publicId ? ` PUBLIC "${node.publicId}" "${node.systemId}"` : node.systemId ? ` SYSTEM "${node.systemId}"` : ``}>`;
2095
+ /* c8 ignore next 3 */
2096
+ }
2097
+ default:
2098
+ throw new Error('Unexpected node type');
2099
+ }
2100
+ };
2101
+
2102
+ /**
2103
+ *
2104
+ * @param {JamilihArray} args
2105
+ * @returns {string}
2106
+ */
2107
+ jml.toDOMString = function (...args) {
2108
+ // Alias for jml.toHTML for parity with jml.toJMLString
2109
+ return jml.toHTML(...args);
2110
+ };
2111
+
2112
+ /**
2113
+ *
2114
+ * @param {JamilihArray} args
2115
+ * @returns {string}
2116
+ */
2117
+ jml.toXML = function (...args) {
2118
+ if (!win) {
2119
+ throw new Error('No window object set');
2120
+ }
2121
+ const ret = jml(...args);
2122
+ return new /** @type {import('jsdom').DOMWindow} */win.XMLSerializer().serializeToString(ret);
2123
+ };
2124
+
2125
+ /**
2126
+ *
2127
+ * @param {JamilihArray} args
2128
+ * @returns {string}
2129
+ */
2130
+ jml.toXMLDOMString = function (...args) {
2131
+ // Alias for jml.toXML for parity with jml.toJMLString
2132
+ return jml.toXML(...args);
2133
+ };
2134
+
2135
+ /**
2136
+ * Element-aware wrapper for `Map`.
2137
+ */
2138
+ class JamilihMap extends Map {
2139
+ /**
2140
+ * @param {?(string|HTMLElement)} element
2141
+ * @returns {ArbitraryValue}
2142
+ */
2143
+ get(element) {
2144
+ const elem = typeof element === 'string' ? $(element) : element;
2145
+ return super.get.call(this, elem);
2146
+ }
2147
+ /**
2148
+ * @param {string|HTMLElement} element
2149
+ * @param {ArbitraryValue} value
2150
+ * @returns {ArbitraryValue}
2151
+ */
2152
+ set(element, value) {
2153
+ const elem = typeof element === 'string' ? $(element) : element;
2154
+ return super.set.call(this, elem, value);
2155
+ }
2156
+ /**
2157
+ * @param {string|HTMLElement} element
2158
+ * @param {string} methodName
2159
+ * @param {...ArbitraryValue} args
2160
+ * @returns {ArbitraryValue}
2161
+ */
2162
+ invoke(element, methodName, ...args) {
2163
+ const elem = typeof element === 'string' ? $(element) : element;
2164
+ return this.get(elem)[methodName](elem, ...args);
2165
+ }
2166
+ }
2167
+
2168
+ /**
2169
+ * Element-aware wrapper for `WeakMap`.
2170
+ * @extends {WeakMap<any>}
2171
+ */
2172
+ class JamilihWeakMap extends WeakMap {
2173
+ /**
2174
+ * @param {HTMLElement} element
2175
+ * @returns {ArbitraryValue}
2176
+ */
2177
+ get(element) {
2178
+ const elem = typeof element === 'string' ? $(element) : element;
2179
+ if (!elem) {
2180
+ throw new Error("Can't find the element");
2181
+ }
2182
+ return super.get.call(this, elem);
2183
+ }
2184
+ /**
2185
+ * @param {HTMLElement} element
2186
+ * @param {ArbitraryValue} value
2187
+ * @returns {ArbitraryValue}
2188
+ */
2189
+ set(element, value) {
2190
+ const elem = typeof element === 'string' ? $(element) : element;
2191
+ if (!elem) {
2192
+ throw new Error("Can't find the element");
2193
+ }
2194
+ return super.set.call(this, elem, value);
2195
+ }
2196
+ /**
2197
+ * @param {string|HTMLElement} element
2198
+ * @param {string} methodName
2199
+ * @param {...ArbitraryValue} args
2200
+ * @returns {ArbitraryValue}
2201
+ */
2202
+ invoke(element, methodName, ...args) {
2203
+ const elem = typeof element === 'string' ? $(element) : element;
2204
+ if (!elem) {
2205
+ throw new Error("Can't find the element");
2206
+ }
2207
+ return this.get(elem)[methodName](elem, ...args);
2208
+ }
2209
+ }
2210
+ jml.Map = JamilihMap;
2211
+ jml.WeakMap = JamilihWeakMap;
2212
+
2213
+ /**
2214
+ * @typedef {[JamilihWeakMap|JamilihMap, HTMLElement]} MapAndElementArray
2215
+ */
2216
+
2217
+ /**
2218
+ * @param {{[key: string]: any}} obj
2219
+ * @param {JamilihArrayPostOptions} args
2220
+ * @returns {MapAndElementArray}
2221
+ */
2222
+ jml.weak = function (obj, ...args) {
2223
+ const map = new JamilihWeakMap();
2224
+ const elem = jml({
2225
+ $map: [map, obj]
2226
+ }, ...args);
2227
+ return [map, (/** @type {HTMLElement} */elem)];
2228
+ };
2229
+
2230
+ /**
2231
+ * @param {ArbitraryValue} obj
2232
+ * @param {JamilihArrayPostOptions} args
2233
+ * @returns {MapAndElementArray}
2234
+ */
2235
+ jml.strong = function (obj, ...args) {
2236
+ const map = new JamilihMap();
2237
+ const elem = jml({
2238
+ $map: [map, obj]
2239
+ }, ...args);
2240
+ return [map, (/** @type {HTMLElement} */elem)];
2241
+ };
2242
+
2243
+ /**
2244
+ * @param {string|HTMLElement} element If a string, will be interpreted as a selector
2245
+ * @param {symbol|string} sym If a string, will be used with `Symbol.for`
2246
+ * @returns {ArbitraryValue} The value associated with the symbol
2247
+ */
2248
+ jml.symbol = jml.sym = jml.for = function (element, sym) {
2249
+ const elem = typeof element === 'string' ? $(element) : element;
2250
+
2251
+ // @ts-expect-error Should be ok
2252
+ return elem[typeof sym === 'symbol' ? sym : Symbol.for(sym)];
2253
+ };
2254
+
2255
+ /**
2256
+ * @typedef {((elem: HTMLElement, ...args: any[]) => void)|{[key: string]: (elem: HTMLElement, ...args: any[]) => void}} MapCommand
2257
+ */
2258
+
2259
+ /**
2260
+ * @param {?(string|HTMLElement)} elem If a string, will be interpreted as a selector
2261
+ * @param {symbol|string|Map<HTMLElement, MapCommand>|WeakMap<HTMLElement, MapCommand>} symOrMap If a string, will be used with `Symbol.for`
2262
+ * @param {string|any} methodName Can be `any` if the symbol or map directly
2263
+ * points to a function (it is then used as the first argument).
2264
+ * @param {ArbitraryValue[]} args
2265
+ * @returns {ArbitraryValue}
2266
+ */
2267
+ jml.command = function (elem, symOrMap, methodName, ...args) {
2268
+ elem = typeof elem === 'string' ? $(elem) : elem;
2269
+ if (!elem) {
2270
+ throw new Error('No element found');
2271
+ }
2272
+ let func;
2273
+ if (['symbol', 'string'].includes(typeof symOrMap)) {
2274
+ func = jml.sym(elem, /** @type {symbol|string} */symOrMap);
2275
+ if (typeof func === 'function') {
2276
+ return func(methodName, ...args); // Already has `this` bound to `elem`
2277
+ }
2278
+ return func[methodName](...args);
2279
+ }
2280
+ func = /** @type {Map<HTMLElement, MapCommand>|WeakMap<HTMLElement, MapCommand>} */symOrMap.get(elem);
2281
+ if (!func) {
2282
+ throw new Error('No map found');
2283
+ }
2284
+ if (typeof func === 'function') {
2285
+ return func.call(elem, methodName, ...args);
2286
+ }
2287
+ return func[methodName](elem, ...args);
2288
+ // return func[methodName].call(elem, ...args);
2289
+ };
2290
+
2291
+ /**
2292
+ * Expects properties `document`, `XMLSerializer`, and `DOMParser`.
2293
+ * Also updates `body` with `document.body`.
2294
+ * @param {import('jsdom').DOMWindow|HTMLWindow|undefined} wind
2295
+ * @returns {void}
2296
+ */
2297
+ jml.setWindow = wind => {
2298
+ win = wind;
2299
+ doc = win?.document;
2300
+ if (doc && doc.body) {
2301
+ // eslint-disable-next-line prefer-destructuring -- Needed for typing
2302
+ body = /** @type {HTMLBodyElement} */doc.body;
2303
+ }
2304
+ };
2305
+
2306
+ /**
2307
+ * @returns {import('jsdom').DOMWindow|HTMLWindow}
2308
+ */
2309
+ jml.getWindow = () => {
2310
+ if (!win) {
2311
+ throw new Error('No window object set');
2312
+ }
2313
+ return win;
2314
+ };
2315
+
2316
+ /**
2317
+ * Does not run Jamilih so can be further processed.
2318
+ * @param {ArbitraryValue[]} array
2319
+ * @param {ArbitraryValue} glu
2320
+ * @returns {ArbitraryValue[]}
2321
+ */
2322
+ function glue(array, glu) {
2323
+ return [...array].reduce((arr, item) => {
2324
+ arr.push(item, glu);
2325
+ return arr;
2326
+ }, []).slice(0, -1);
2327
+ }
2328
+
2329
+ /**
2330
+ * @type {HTMLBodyElement}
2331
+ */
2332
+ let body; // // eslint-disable-line import/no-mutable-exports
2333
+
2334
+ /* c8 ignore next 4 */
2335
+ if (doc && doc.body) {
2336
+ // eslint-disable-next-line prefer-destructuring -- Needed for type
2337
+ body = /** @type {HTMLBodyElement} */doc.body;
2338
+ }
2339
+ const nbsp = '\u00A0'; // Very commonly needed in templates
2340
+
2341
+ export { $, $$, DOMException, body, jml as default, glue, jml, nbsp };