@es-joy/jsoe 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1692 @@
1
+ /*
2
+ Possible todos:
3
+ 0. Add XSLT to JML-string stylesheet (or even vice versa)
4
+ 0. IE problem: Add JsonML code to handle name attribute (during element creation)
5
+ 0. Element-specific: IE object-param handling
6
+
7
+ Todos inspired by JsonML: https://github.com/mckamey/jsonml/blob/master/jsonml-html.js
8
+
9
+ 0. duplicate attributes?
10
+ 0. expand ATTR_MAP
11
+ 0. equivalent of markup, to allow strings to be embedded within an object (e.g., {$value: '<div>id</div>'}); advantage over innerHTML in that it wouldn't need to work as the entire contents (nor destroy any existing content or handlers)
12
+ 0. More validation?
13
+ 0. JsonML DOM Level 0 listener
14
+ 0. Whitespace trimming?
15
+
16
+ JsonML element-specific:
17
+ 0. table appending
18
+ 0. canHaveChildren necessary? (attempts to append to script and img)
19
+
20
+ Other Todos:
21
+ 0. Note to self: Integrate research from other jml notes
22
+ 0. Allow Jamilih to be seeded with an existing element, so as to be able to add/modify attributes and children
23
+ 0. Allow array as single first argument
24
+ 0. Settle on whether need to use null as last argument to return array (or fragment) or other way to allow appending? Options object at end instead to indicate whether returning array, fragment, first element, etc.?
25
+ 0. Allow building of generic XML (pass configuration object)
26
+ 0. Allow building content internally as a string (though allowing DOM methods, etc.?)
27
+ 0. Support JsonML empty string element name to represent fragments?
28
+ 0. Redo browser testing of jml (including ensuring IE7 can work even if test framework can't work)
29
+ */
30
+
31
+ // istanbul ignore next
32
+ let win = typeof window !== 'undefined' && window;
33
+ // istanbul ignore next
34
+ let doc = typeof document !== 'undefined' && document || win && win.document;
35
+
36
+ // STATIC PROPERTIES
37
+
38
+ const possibleOptions = ['$plugins',
39
+ // '$mode', // Todo (SVG/XML)
40
+ // '$state', // Used internally
41
+ '$map' // Add any other options here
42
+ ];
43
+
44
+ const NS_HTML = 'http://www.w3.org/1999/xhtml',
45
+ hyphenForCamelCase = /-([a-z])/gu;
46
+ const ATTR_MAP = {
47
+ maxlength: 'maxLength',
48
+ minlength: 'minLength',
49
+ readonly: 'readOnly'
50
+ };
51
+
52
+ // We define separately from ATTR_DOM for clarity (and parity with JsonML) but no current need
53
+ // We don't set attribute esp. for boolean atts as we want to allow setting of `undefined`
54
+ // (e.g., from an empty variable) on templates to have no effect
55
+ const BOOL_ATTS = ['checked', 'defaultChecked', 'defaultSelected', 'disabled', 'indeterminate', 'open',
56
+ // Dialog elements
57
+ 'readOnly', 'selected'];
58
+
59
+ // From JsonML
60
+ const ATTR_DOM = [...BOOL_ATTS, 'accessKey',
61
+ // HTMLElement
62
+ 'async', 'autocapitalize',
63
+ // HTMLElement
64
+ 'autofocus', 'contentEditable',
65
+ // HTMLElement through ElementContentEditable
66
+ 'defaultValue', 'defer', 'draggable',
67
+ // HTMLElement
68
+ 'formnovalidate', 'hidden',
69
+ // HTMLElement
70
+ 'innerText',
71
+ // HTMLElement
72
+ 'inputMode',
73
+ // HTMLElement through ElementContentEditable
74
+ 'ismap', 'multiple', 'novalidate', 'pattern', 'required', 'spellcheck',
75
+ // HTMLElement
76
+ 'translate',
77
+ // HTMLElement
78
+ 'value', 'willvalidate'];
79
+ // Todo: Add more to this as useful for templating
80
+ // to avoid setting through nullish value
81
+ const NULLABLES = ['autocomplete', 'dir',
82
+ // HTMLElement
83
+ 'integrity',
84
+ // script, link
85
+ 'lang',
86
+ // HTMLElement
87
+ 'max', 'min', 'minLength', 'maxLength', 'title' // HTMLElement
88
+ ];
89
+
90
+ const $ = sel => doc.querySelector(sel);
91
+ const $$ = sel => [...doc.querySelectorAll(sel)];
92
+
93
+ /**
94
+ * Retrieve the (lower-cased) HTML name of a node.
95
+ * @static
96
+ * @param {Node} node The HTML node
97
+ * @returns {string} The lower-cased node name
98
+ */
99
+ function _getHTMLNodeName(node) {
100
+ return node.nodeName && node.nodeName.toLowerCase();
101
+ }
102
+
103
+ /**
104
+ * Apply styles if this is a style tag.
105
+ * @static
106
+ * @param {Node} node The element to check whether it is a style tag
107
+ * @returns {void}
108
+ */
109
+ function _applyAnyStylesheet(node) {
110
+ // Only used in IE
111
+ // istanbul ignore else
112
+ if (!doc.createStyleSheet) {
113
+ return;
114
+ }
115
+ // istanbul ignore next
116
+ if (_getHTMLNodeName(node) === 'style') {
117
+ // IE
118
+ const ss = doc.createStyleSheet(); // Create a stylesheet to actually do something useful
119
+ ss.cssText = node.cssText;
120
+ // We continue to add the style tag, however
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Need this function for IE since options weren't otherwise getting added.
126
+ * @private
127
+ * @static
128
+ * @param {Element} parent The parent to which to append the element
129
+ * @param {Node} child The element or other node to append to the parent
130
+ * @throws {Error} Rethrow if problem with `append` and unhandled
131
+ * @returns {void}
132
+ */
133
+ function _appendNode(parent, child) {
134
+ const parentName = _getHTMLNodeName(parent);
135
+
136
+ // IE only
137
+ // istanbul ignore if
138
+ if (doc.createStyleSheet) {
139
+ if (parentName === 'script') {
140
+ parent.text = child.nodeValue;
141
+ return;
142
+ }
143
+ if (parentName === 'style') {
144
+ parent.cssText = child.nodeValue; // This will not apply it--just make it available within the DOM cotents
145
+ return;
146
+ }
147
+ }
148
+ if (parentName === 'template') {
149
+ parent.content.append(child);
150
+ return;
151
+ }
152
+ try {
153
+ parent.append(child); // IE9 is now ok with this
154
+ } catch (e) {
155
+ // istanbul ignore next
156
+ const childName = _getHTMLNodeName(child);
157
+ // istanbul ignore next
158
+ if (parentName === 'select' && childName === 'option') {
159
+ try {
160
+ // Since this is now DOM Level 4 standard behavior (and what IE7+ can handle), we try it first
161
+ parent.add(child);
162
+ } catch (err) {
163
+ // DOM Level 2 did require a second argument, so we try it too just in case the user is using an older version of Firefox, etc.
164
+ parent.add(child, null); // IE7 has a problem with this, but IE8+ is ok
165
+ }
166
+
167
+ return;
168
+ }
169
+ // istanbul ignore next
170
+ throw e;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Attach event in a cross-browser fashion.
176
+ * @static
177
+ * @param {Element} el DOM element to which to attach the event
178
+ * @param {string} type The DOM event (without 'on') to attach to the element
179
+ * @param {EventListener} handler The event handler to attach to the element
180
+ * @param {boolean} [capturing] Whether or not the event should be
181
+ * capturing (W3C-browsers only); default is false; NOT IN USE
182
+ * @returns {void}
183
+ */
184
+ function _addEvent(el, type, handler, capturing) {
185
+ el.addEventListener(type, handler, Boolean(capturing));
186
+ }
187
+
188
+ /**
189
+ * Creates a text node of the result of resolving an entity or character reference.
190
+ * @param {'entity'|'decimal'|'hexadecimal'} type Type of reference
191
+ * @param {string} prefix Text to prefix immediately after the "&"
192
+ * @param {string} arg The body of the reference
193
+ * @throws {TypeError}
194
+ * @returns {Text} The text node of the resolved reference
195
+ */
196
+ function _createSafeReference(type, prefix, arg) {
197
+ // For security reasons related to innerHTML, we ensure this string only
198
+ // contains potential entity characters
199
+ if (!/^\w+$/u.test(arg)) {
200
+ throw new TypeError(`Bad ${type} reference; with prefix "${prefix}" and arg "${arg}"`);
201
+ }
202
+ const elContainer = doc.createElement('div');
203
+ // Todo: No workaround for XML?
204
+ // eslint-disable-next-line no-unsanitized/property
205
+ elContainer.innerHTML = '&' + prefix + arg + ';';
206
+ return doc.createTextNode(elContainer.innerHTML);
207
+ }
208
+
209
+ /**
210
+ * @param {string} n0 Whole expression match (including "-")
211
+ * @param {string} n1 Lower-case letter match
212
+ * @returns {string} Uppercased letter
213
+ */
214
+ function _upperCase(n0, n1) {
215
+ return n1.toUpperCase();
216
+ }
217
+
218
+ // Todo: Make as public utility
219
+ /**
220
+ * @param {ArbitraryValue} o
221
+ * @returns {boolean}
222
+ */
223
+ function _isNullish(o) {
224
+ return o === null || o === undefined;
225
+ }
226
+
227
+ // Todo: Make as public utility, but also return types for undefined, boolean, number, document, etc.
228
+ /**
229
+ * @private
230
+ * @static
231
+ * @param {string|JamilihAttributes|JamilihArray|Element|DocumentFragment} item
232
+ * @returns {"string"|"null"|"array"|"element"|"fragment"|"object"|"symbol"|"function"|"number"|"boolean"}
233
+ */
234
+ function _getType(item) {
235
+ const type = typeof item;
236
+ switch (type) {
237
+ case 'object':
238
+ if (item === null) {
239
+ return 'null';
240
+ }
241
+ if (Array.isArray(item)) {
242
+ return 'array';
243
+ }
244
+ if ('nodeType' in item) {
245
+ switch (item.nodeType) {
246
+ case 1:
247
+ return 'element';
248
+ case 9:
249
+ return 'document';
250
+ case 11:
251
+ return 'fragment';
252
+ default:
253
+ return 'non-container node';
254
+ }
255
+ }
256
+ // Fallthrough
257
+ default:
258
+ return type;
259
+ }
260
+ }
261
+
262
+ /**
263
+ * @private
264
+ * @static
265
+ * @param {DocumentFragment} frag
266
+ * @param {Node} node
267
+ * @returns {DocumentFragment}
268
+ */
269
+ function _fragReducer(frag, node) {
270
+ frag.append(node);
271
+ return frag;
272
+ }
273
+
274
+ /**
275
+ * @private
276
+ * @static
277
+ * @param {Object<{string:string}>} xmlnsObj
278
+ * @returns {string}
279
+ */
280
+ function _replaceDefiner(xmlnsObj) {
281
+ return function (n0) {
282
+ let retStr = xmlnsObj[''] ? ' xmlns="' + xmlnsObj[''] + '"' : n0; // Preserve XHTML
283
+ for (const [ns, xmlnsVal] of Object.entries(xmlnsObj)) {
284
+ if (ns !== '') {
285
+ retStr += ' xmlns:' + ns + '="' + xmlnsVal + '"';
286
+ }
287
+ }
288
+ return retStr;
289
+ };
290
+ }
291
+
292
+ /**
293
+ * @typedef {JamilihAttributes} AttributeArray
294
+ * @property {string} 0 The key
295
+ * @property {string} 1 The value
296
+ */
297
+
298
+ /**
299
+ * @callback ChildrenToJMLCallback
300
+ * @param {JamilihArray|Jamilih} childNodeJML
301
+ * @param {Integer} i
302
+ * @returns {void}
303
+ */
304
+
305
+ /**
306
+ * @private
307
+ * @static
308
+ * @param {Node} node
309
+ * @returns {ChildrenToJMLCallback}
310
+ */
311
+ function _childrenToJML(node) {
312
+ return function (childNodeJML, i) {
313
+ const cn = node.childNodes[i];
314
+ const j = Array.isArray(childNodeJML) ? jml(...childNodeJML) : jml(childNodeJML);
315
+ cn.replaceWith(j);
316
+ };
317
+ }
318
+
319
+ /**
320
+ * @callback JamilihAppender
321
+ * @param {JamilihArray} childJML
322
+ * @returns {void}
323
+ */
324
+
325
+ /**
326
+ * @private
327
+ * @static
328
+ * @param {Node} node
329
+ * @returns {JamilihAppender}
330
+ */
331
+ function _appendJML(node) {
332
+ return function (childJML) {
333
+ if (Array.isArray(childJML)) {
334
+ node.append(jml(...childJML));
335
+ } else {
336
+ node.append(jml(childJML));
337
+ }
338
+ };
339
+ }
340
+
341
+ /**
342
+ * @callback appender
343
+ * @param {string|JamilihArray} childJML
344
+ * @returns {void}
345
+ */
346
+
347
+ /**
348
+ * @private
349
+ * @static
350
+ * @param {Node} node
351
+ * @returns {appender}
352
+ */
353
+ function _appendJMLOrText(node) {
354
+ return function (childJML) {
355
+ if (typeof childJML === 'string') {
356
+ node.append(childJML);
357
+ } else if (Array.isArray(childJML)) {
358
+ node.append(jml(...childJML));
359
+ } else {
360
+ node.append(jml(childJML));
361
+ }
362
+ };
363
+ }
364
+
365
+ /**
366
+ * @private
367
+ * @static
368
+ */
369
+ /*
370
+ function _DOMfromJMLOrString (childNodeJML) {
371
+ if (typeof childNodeJML === 'string') {
372
+ return doc.createTextNode(childNodeJML);
373
+ }
374
+ return jml(...childNodeJML);
375
+ }
376
+ */
377
+
378
+ /**
379
+ * @typedef {Element|DocumentFragment} JamilihReturn
380
+ */
381
+
382
+ /**
383
+ * @typedef {PlainObject<string, string>} JamilihAttributes
384
+ */
385
+
386
+ /**
387
+ * @typedef {GenericArray} JamilihArray
388
+ * @property {string} 0 The element to create (by lower-case name)
389
+ * @property {JamilihAttributes} [1] Attributes to add with the key as the
390
+ * attribute name and value as the attribute value; important for IE where
391
+ * the input element's type cannot be added later after already added to the page
392
+ * @param {Element[]} [children] The optional children of this element
393
+ * (but raw DOM elements required to be specified within arrays since
394
+ * could not otherwise be distinguished from siblings being added)
395
+ * @param {Element} [parent] The optional parent to which to attach the element
396
+ * (always the last unless followed by null, in which case it is the
397
+ * second-to-last)
398
+ * @param {null} [returning] Can use null to indicate an array of elements
399
+ * should be returned
400
+ */
401
+
402
+ /**
403
+ * @typedef {PlainObject} JamilihOptions
404
+ * @property {"root"|"attributeValue"|"fragment"|"children"|"fragmentChildren"} $state
405
+ */
406
+
407
+ /**
408
+ * @param {Element} elem
409
+ * @param {string} att
410
+ * @param {string} attVal
411
+ * @param {JamilihOptions} opts
412
+ * @returns {void}
413
+ */
414
+ function checkPluginValue(elem, att, attVal, opts) {
415
+ opts.$state = 'attributeValue';
416
+ if (attVal && typeof attVal === 'object') {
417
+ const matchingPlugin = getMatchingPlugin(opts, Object.keys(attVal)[0]);
418
+ if (matchingPlugin) {
419
+ return matchingPlugin.set({
420
+ opts,
421
+ element: elem,
422
+ attribute: {
423
+ name: att,
424
+ value: attVal
425
+ }
426
+ });
427
+ }
428
+ }
429
+ return attVal;
430
+ }
431
+
432
+ /**
433
+ * @param {JamilihOptions} opts
434
+ * @param {string} item
435
+ * @returns {JamilihPlugin}
436
+ */
437
+ function getMatchingPlugin(opts, item) {
438
+ return opts.$plugins && opts.$plugins.find(p => {
439
+ return p.name === item;
440
+ });
441
+ }
442
+
443
+ /**
444
+ * Creates an XHTML or HTML element (XHTML is preferred, but only in browsers
445
+ * that support); any element after element can be omitted, and any subsequent
446
+ * type or types added afterwards.
447
+ * @param {...JamilihArray} args
448
+ * @returns {JamilihReturn} The newly created (and possibly already appended)
449
+ * element or array of elements
450
+ */
451
+ const jml = function jml(...args) {
452
+ let elem = doc.createDocumentFragment();
453
+ /**
454
+ *
455
+ * @param {Object<{string: string}>} atts
456
+ * @throws {TypeError}
457
+ * @returns {void}
458
+ */
459
+ function _checkAtts(atts) {
460
+ for (let [att, attVal] of Object.entries(atts)) {
461
+ att = att in ATTR_MAP ? ATTR_MAP[att] : att;
462
+ if (NULLABLES.includes(att)) {
463
+ attVal = checkPluginValue(elem, att, attVal, opts);
464
+ if (!_isNullish(attVal)) {
465
+ elem[att] = attVal;
466
+ }
467
+ continue;
468
+ } else if (ATTR_DOM.includes(att)) {
469
+ attVal = checkPluginValue(elem, att, attVal, opts);
470
+ elem[att] = attVal;
471
+ continue;
472
+ }
473
+ switch (att) {
474
+ /*
475
+ Todos:
476
+ 0. JSON mode to prevent event addition
477
+ 0. {$xmlDocument: []} // doc.implementation.createDocument
478
+ 0. Accept array for any attribute with first item as prefix and second as value?
479
+ 0. {$: ['xhtml', 'div']} for prefixed elements
480
+ case '$': // Element with prefix?
481
+ nodes[nodes.length] = elem = doc.createElementNS(attVal[0], attVal[1]);
482
+ break;
483
+ */
484
+ case '#':
485
+ {
486
+ // Document fragment
487
+ opts.$state = 'fragmentChilden';
488
+ nodes[nodes.length] = jml(opts, attVal);
489
+ break;
490
+ }
491
+ case '$shadow':
492
+ {
493
+ const {
494
+ open,
495
+ closed
496
+ } = attVal;
497
+ let {
498
+ content,
499
+ template
500
+ } = attVal;
501
+ const shadowRoot = elem.attachShadow({
502
+ mode: closed || open === false ? 'closed' : 'open'
503
+ });
504
+ if (template) {
505
+ if (Array.isArray(template)) {
506
+ template = _getType(template[0]) === 'object' ? jml('template', ...template, doc.body) : jml('template', template, doc.body);
507
+ } else if (typeof template === 'string') {
508
+ template = $(template);
509
+ }
510
+ jml(template.content.cloneNode(true), shadowRoot);
511
+ } else {
512
+ if (!content) {
513
+ content = open || closed;
514
+ }
515
+ if (content && typeof content !== 'boolean') {
516
+ if (Array.isArray(content)) {
517
+ jml({
518
+ '#': content
519
+ }, shadowRoot);
520
+ } else {
521
+ jml(content, shadowRoot);
522
+ }
523
+ }
524
+ }
525
+ break;
526
+ }
527
+ case '$state':
528
+ {
529
+ // Handled internally
530
+ break;
531
+ }
532
+ case 'is':
533
+ {
534
+ // Currently only in Chrome
535
+ // Handled during element creation
536
+ break;
537
+ }
538
+ case '$custom':
539
+ {
540
+ Object.assign(elem, attVal);
541
+ break;
542
+ }
543
+ /* istanbul ignore next */
544
+ case '$define':
545
+ {
546
+ const localName = elem.localName.toLowerCase();
547
+ // Note: customized built-ins sadly not working yet
548
+ const customizedBuiltIn = !localName.includes('-');
549
+
550
+ // We check attribute in case this is a preexisting DOM element
551
+ // const {is} = atts;
552
+ let is;
553
+ if (customizedBuiltIn) {
554
+ is = elem.getAttribute('is');
555
+ if (!is) {
556
+ if (!{}.hasOwnProperty.call(atts, 'is')) {
557
+ throw new TypeError(`Expected \`is\` with \`$define\` on built-in; args: ${JSON.stringify(args)}`);
558
+ }
559
+ atts.is = checkPluginValue(elem, 'is', atts.is, opts);
560
+ elem.setAttribute('is', atts.is);
561
+ ({
562
+ is
563
+ } = atts);
564
+ }
565
+ }
566
+ const def = customizedBuiltIn ? is : localName;
567
+ if (window.customElements.get(def)) {
568
+ break;
569
+ }
570
+ const getConstructor = cnstrct => {
571
+ const baseClass = options && options.extends ? doc.createElement(options.extends).constructor : customizedBuiltIn ? doc.createElement(localName).constructor : window.HTMLElement;
572
+
573
+ /**
574
+ * Class wrapping base class.
575
+ */
576
+ return cnstrct ? class extends baseClass {
577
+ /**
578
+ * Calls user constructor.
579
+ */
580
+ constructor() {
581
+ super();
582
+ cnstrct.call(this);
583
+ }
584
+ } : class extends baseClass {};
585
+ };
586
+ let cnstrctr, options, mixin;
587
+ if (Array.isArray(attVal)) {
588
+ if (attVal.length <= 2) {
589
+ [cnstrctr, options] = attVal;
590
+ if (typeof options === 'string') {
591
+ // Todo: Allow creating a definition without using it;
592
+ // that may be the only reason to have a string here which
593
+ // differs from the `localName` anyways
594
+ options = {
595
+ extends: options
596
+ };
597
+ } else if (options && !{}.hasOwnProperty.call(options, 'extends')) {
598
+ mixin = options;
599
+ }
600
+ if (typeof cnstrctr === 'object') {
601
+ mixin = cnstrctr;
602
+ cnstrctr = getConstructor();
603
+ }
604
+ } else {
605
+ [cnstrctr, mixin, options] = attVal;
606
+ if (typeof options === 'string') {
607
+ options = {
608
+ extends: options
609
+ };
610
+ }
611
+ }
612
+ } else if (typeof attVal === 'function') {
613
+ cnstrctr = attVal;
614
+ } else {
615
+ mixin = attVal;
616
+ cnstrctr = getConstructor();
617
+ }
618
+ if (!cnstrctr.toString().startsWith('class')) {
619
+ cnstrctr = getConstructor(cnstrctr);
620
+ }
621
+ if (!options && customizedBuiltIn) {
622
+ options = {
623
+ extends: localName
624
+ };
625
+ }
626
+ if (mixin) {
627
+ Object.entries(mixin).forEach(([methodName, method]) => {
628
+ cnstrctr.prototype[methodName] = method;
629
+ });
630
+ }
631
+ // console.log('def', def, '::', typeof options === 'object' ? options : undefined);
632
+ window.customElements.define(def, cnstrctr, typeof options === 'object' ? options : undefined);
633
+ break;
634
+ }
635
+ case '$symbol':
636
+ {
637
+ const [symbol, func] = attVal;
638
+ if (typeof func === 'function') {
639
+ const funcBound = func.bind(elem);
640
+ if (typeof symbol === 'string') {
641
+ elem[Symbol.for(symbol)] = funcBound;
642
+ } else {
643
+ elem[symbol] = funcBound;
644
+ }
645
+ } else {
646
+ const obj = func;
647
+ obj.elem = elem;
648
+ if (typeof symbol === 'string') {
649
+ elem[Symbol.for(symbol)] = obj;
650
+ } else {
651
+ elem[symbol] = obj;
652
+ }
653
+ }
654
+ break;
655
+ }
656
+ case '$data':
657
+ {
658
+ setMap(attVal);
659
+ break;
660
+ }
661
+ case '$attribute':
662
+ {
663
+ // Attribute node
664
+ const node = attVal.length === 3 ? doc.createAttributeNS(attVal[0], attVal[1]) : doc.createAttribute(attVal[0]);
665
+ node.value = attVal[attVal.length - 1];
666
+ nodes[nodes.length] = node;
667
+ break;
668
+ }
669
+ case '$text':
670
+ {
671
+ // Todo: Also allow as jml(['a text node']) (or should that become a fragment)?
672
+ const node = doc.createTextNode(attVal);
673
+ nodes[nodes.length] = node;
674
+ break;
675
+ }
676
+ case '$document':
677
+ {
678
+ // Todo: Conditionally create XML document
679
+ const node = doc.implementation.createHTMLDocument();
680
+ if (attVal.childNodes) {
681
+ // Remove any extra nodes created by createHTMLDocument().
682
+ const j = attVal.childNodes.length;
683
+ while (node.childNodes[j]) {
684
+ const cn = node.childNodes[j];
685
+ cn.remove();
686
+ // `j` should stay the same as removing will cause node to be present
687
+ }
688
+
689
+ attVal.childNodes.forEach(_childrenToJML(node));
690
+ } else {
691
+ if (attVal.$DOCTYPE) {
692
+ const dt = {
693
+ $DOCTYPE: attVal.$DOCTYPE
694
+ };
695
+ const doctype = jml(dt);
696
+ node.firstChild.replaceWith(doctype);
697
+ }
698
+ const html = node.childNodes[1];
699
+ const head = html.childNodes[0];
700
+ const body = html.childNodes[1];
701
+ if (attVal.title || attVal.head) {
702
+ const meta = doc.createElement('meta');
703
+ // eslint-disable-next-line unicorn/text-encoding-identifier-case -- HTML
704
+ meta.setAttribute('charset', 'utf-8');
705
+ head.append(meta);
706
+ if (attVal.title) {
707
+ node.title = attVal.title; // Appends after meta
708
+ }
709
+
710
+ if (attVal.head) {
711
+ attVal.head.forEach(_appendJML(head));
712
+ }
713
+ }
714
+ if (attVal.body) {
715
+ attVal.body.forEach(_appendJMLOrText(body));
716
+ }
717
+ }
718
+ nodes[nodes.length] = node;
719
+ break;
720
+ }
721
+ case '$DOCTYPE':
722
+ {
723
+ const node = doc.implementation.createDocumentType(attVal.name, attVal.publicId || '', attVal.systemId || '');
724
+ nodes[nodes.length] = node;
725
+ break;
726
+ }
727
+ case '$on':
728
+ {
729
+ // Events
730
+ // Allow for no-op by defaulting to `{}`
731
+ for (let [p2, val] of Object.entries(attVal || {})) {
732
+ if (typeof val === 'function') {
733
+ val = [val, false];
734
+ }
735
+ if (typeof val[0] !== 'function') {
736
+ throw new TypeError(`Expect a function for \`$on\`; args: ${JSON.stringify(args)}`);
737
+ }
738
+ _addEvent(elem, p2, val[0], val[1]); // element, event name, handler, capturing
739
+ }
740
+
741
+ break;
742
+ }
743
+ case 'className':
744
+ case 'class':
745
+ attVal = checkPluginValue(elem, att, attVal, opts);
746
+ if (!_isNullish(attVal)) {
747
+ elem.className = attVal;
748
+ }
749
+ break;
750
+ case 'dataset':
751
+ {
752
+ // Map can be keyed with hyphenated or camel-cased properties
753
+ const recurse = (atVal, startProp) => {
754
+ let prop = '';
755
+ const pastInitialProp = startProp !== '';
756
+ Object.keys(atVal).forEach(key => {
757
+ const value = atVal[key];
758
+ prop = pastInitialProp ? startProp + key.replace(hyphenForCamelCase, _upperCase).replace(/^([a-z])/u, _upperCase) : startProp + key.replace(hyphenForCamelCase, _upperCase);
759
+ if (value === null || typeof value !== 'object') {
760
+ if (!_isNullish(value)) {
761
+ elem.dataset[prop] = value;
762
+ }
763
+ prop = startProp;
764
+ return;
765
+ }
766
+ recurse(value, prop);
767
+ });
768
+ };
769
+ recurse(attVal, '');
770
+ break;
771
+ // Todo: Disable this by default unless configuration explicitly allows (for security)
772
+ }
773
+ // #if IS_REMOVE
774
+ // Don't remove this `if` block (for sake of no-innerHTML build)
775
+ case 'innerHTML':
776
+ if (!_isNullish(attVal)) {
777
+ // eslint-disable-next-line no-unsanitized/property
778
+ elem.innerHTML = attVal;
779
+ }
780
+ break;
781
+ // #endif
782
+ case 'htmlFor':
783
+ case 'for':
784
+ if (elStr === 'label') {
785
+ attVal = checkPluginValue(elem, att, attVal, opts);
786
+ if (!_isNullish(attVal)) {
787
+ elem.htmlFor = attVal;
788
+ }
789
+ break;
790
+ }
791
+ attVal = checkPluginValue(elem, att, attVal, opts);
792
+ elem.setAttribute(att, attVal);
793
+ break;
794
+ case 'xmlns':
795
+ // Already handled
796
+ break;
797
+ default:
798
+ {
799
+ if (att.startsWith('on')) {
800
+ attVal = checkPluginValue(elem, att, attVal, opts);
801
+ elem[att] = attVal;
802
+ // _addEvent(elem, att.slice(2), attVal, false); // This worked, but perhaps the user wishes only one event
803
+ break;
804
+ }
805
+ if (att === 'style') {
806
+ attVal = checkPluginValue(elem, att, attVal, opts);
807
+ if (_isNullish(attVal)) {
808
+ break;
809
+ }
810
+ if (typeof attVal === 'object') {
811
+ for (const [p2, styleVal] of Object.entries(attVal)) {
812
+ if (!_isNullish(styleVal)) {
813
+ // Todo: Handle aggregate properties like "border"
814
+ if (p2 === 'float') {
815
+ elem.style.cssFloat = styleVal;
816
+ elem.style.styleFloat = styleVal; // Harmless though we could make conditional on older IE instead
817
+ } else {
818
+ elem.style[p2.replace(hyphenForCamelCase, _upperCase)] = styleVal;
819
+ }
820
+ }
821
+ }
822
+ break;
823
+ }
824
+
825
+ // setAttribute unfortunately erases any existing styles
826
+ elem.setAttribute(att, attVal);
827
+ /*
828
+ // The following reorders which is troublesome for serialization, e.g., as used in our testing
829
+ if (elem.style.cssText !== undefined) {
830
+ elem.style.cssText += attVal;
831
+ } else { // Opera
832
+ elem.style += attVal;
833
+ }
834
+ */
835
+ break;
836
+ }
837
+ const matchingPlugin = getMatchingPlugin(opts, att);
838
+ if (matchingPlugin) {
839
+ matchingPlugin.set({
840
+ opts,
841
+ element: elem,
842
+ attribute: {
843
+ name: att,
844
+ value: attVal
845
+ }
846
+ });
847
+ break;
848
+ }
849
+ attVal = checkPluginValue(elem, att, attVal, opts);
850
+ elem.setAttribute(att, attVal);
851
+ break;
852
+ }
853
+ }
854
+ }
855
+ }
856
+ const nodes = [];
857
+ let elStr;
858
+ let opts;
859
+ let isRoot = false;
860
+ if (_getType(args[0]) === 'object' && Object.keys(args[0]).some(key => possibleOptions.includes(key))) {
861
+ opts = args[0];
862
+ if (opts.$state === undefined) {
863
+ isRoot = true;
864
+ opts.$state = 'root';
865
+ }
866
+ if (opts.$map && !opts.$map.root && opts.$map.root !== false) {
867
+ opts.$map = {
868
+ root: opts.$map
869
+ };
870
+ }
871
+ if ('$plugins' in opts) {
872
+ if (!Array.isArray(opts.$plugins)) {
873
+ throw new TypeError(`\`$plugins\` must be an array; args: ${JSON.stringify(args)}`);
874
+ }
875
+ opts.$plugins.forEach(pluginObj => {
876
+ if (!pluginObj || typeof pluginObj !== 'object') {
877
+ throw new TypeError(`Plugin must be an object; args: ${JSON.stringify(args)}`);
878
+ }
879
+ if (!pluginObj.name || !pluginObj.name.startsWith('$_')) {
880
+ throw new TypeError(`Plugin object name must be present and begin with \`$_\`; args: ${JSON.stringify(args)}`);
881
+ }
882
+ if (typeof pluginObj.set !== 'function') {
883
+ throw new TypeError(`Plugin object must have a \`set\` method; args: ${JSON.stringify(args)}`);
884
+ }
885
+ });
886
+ }
887
+ args = args.slice(1);
888
+ } else {
889
+ opts = {
890
+ $state: undefined
891
+ };
892
+ }
893
+ const argc = args.length;
894
+ const defaultMap = opts.$map && opts.$map.root;
895
+ const setMap = dataVal => {
896
+ let map, obj;
897
+ // Boolean indicating use of default map and object
898
+ if (dataVal === true) {
899
+ [map, obj] = defaultMap;
900
+ } else if (Array.isArray(dataVal)) {
901
+ // Array of strings mapping to default
902
+ if (typeof dataVal[0] === 'string') {
903
+ dataVal.forEach(dVal => {
904
+ setMap(opts.$map[dVal]);
905
+ });
906
+ return;
907
+ // Array of Map and non-map data object
908
+ }
909
+
910
+ map = dataVal[0] || defaultMap[0];
911
+ obj = dataVal[1] || defaultMap[1];
912
+ // Map
913
+ } else if (/^\[object (?:Weak)?Map\]$/u.test([].toString.call(dataVal))) {
914
+ map = dataVal;
915
+ obj = defaultMap[1];
916
+ // Non-map data object
917
+ } else {
918
+ map = defaultMap[0];
919
+ obj = dataVal;
920
+ }
921
+ map.set(elem, obj);
922
+ };
923
+ for (let i = 0; i < argc; i++) {
924
+ let arg = args[i];
925
+ const type = _getType(arg);
926
+ switch (type) {
927
+ case 'null':
928
+ // null always indicates a place-holder (only needed for last argument if want array returned)
929
+ if (i === argc - 1) {
930
+ _applyAnyStylesheet(nodes[0]); // We have to execute any stylesheets even if not appending or otherwise IE will never apply them
931
+ // Todo: Fix to allow application of stylesheets of style tags within fragments?
932
+ return nodes.length <= 1 ? nodes[0]
933
+ // eslint-disable-next-line unicorn/no-array-callback-reference
934
+ : nodes.reduce(_fragReducer, doc.createDocumentFragment()); // nodes;
935
+ }
936
+
937
+ throw new TypeError(`\`null\` values not allowed except as final Jamilih argument; index ${i} on args: ${JSON.stringify(args)}`);
938
+ case 'string':
939
+ // Strings normally indicate elements
940
+ switch (arg) {
941
+ case '!':
942
+ nodes[nodes.length] = doc.createComment(args[++i]);
943
+ break;
944
+ case '?':
945
+ {
946
+ arg = args[++i];
947
+ let procValue = args[++i];
948
+ const val = procValue;
949
+ if (val && typeof val === 'object') {
950
+ procValue = [];
951
+ for (const [p, procInstVal] of Object.entries(val)) {
952
+ procValue.push(p + '=' + '"' +
953
+ // https://www.w3.org/TR/xml-stylesheet/#NT-PseudoAttValue
954
+ procInstVal.replace(/"/gu, '&quot;') + '"');
955
+ }
956
+ procValue = procValue.join(' ');
957
+ }
958
+ // Firefox allows instructions with ">" in this method, but not if placed directly!
959
+ try {
960
+ nodes[nodes.length] = doc.createProcessingInstruction(arg, procValue);
961
+ } catch (e) {
962
+ // Getting NotSupportedError in IE, so we try to imitate a processing instruction with a comment
963
+ // innerHTML didn't work
964
+ // var elContainer = doc.createElement('div');
965
+ // elContainer.innerHTML = '<?' + doc.createTextNode(arg + ' ' + procValue).nodeValue + '?>';
966
+ // nodes[nodes.length] = elContainer.innerHTML;
967
+ // Todo: any other way to resolve? Just use XML?
968
+ nodes[nodes.length] = doc.createComment('?' + arg + ' ' + procValue + '?');
969
+ }
970
+ break;
971
+ // Browsers don't support doc.createEntityReference, so we just use this as a convenience
972
+ }
973
+ case '&':
974
+ nodes[nodes.length] = _createSafeReference('entity', '', args[++i]);
975
+ break;
976
+ case '#':
977
+ // // Decimal character reference - ['#', '01234'] // &#01234; // probably easier to use JavaScript Unicode escapes
978
+ nodes[nodes.length] = _createSafeReference('decimal', arg, String(args[++i]));
979
+ break;
980
+ case '#x':
981
+ // Hex character reference - ['#x', '123a'] // &#x123a; // probably easier to use JavaScript Unicode escapes
982
+ nodes[nodes.length] = _createSafeReference('hexadecimal', arg, args[++i]);
983
+ break;
984
+ case '![':
985
+ // '![', ['escaped <&> text'] // <![CDATA[escaped <&> text]]>
986
+ // CDATA valid in XML only, so we'll just treat as text for mutual compatibility
987
+ // Todo: config (or detection via some kind of doc.documentType property?) of whether in XML
988
+ try {
989
+ nodes[nodes.length] = doc.createCDATASection(args[++i]);
990
+ } catch (e2) {
991
+ nodes[nodes.length] = doc.createTextNode(args[i]); // i already incremented
992
+ }
993
+
994
+ break;
995
+ case '':
996
+ nodes[nodes.length] = elem = doc.createDocumentFragment();
997
+ // Todo: Report to plugins
998
+ opts.$state = 'fragment';
999
+ break;
1000
+ default:
1001
+ {
1002
+ // An element
1003
+ elStr = arg;
1004
+ const atts = args[i + 1];
1005
+ if (_getType(atts) === 'object' && atts.is) {
1006
+ const {
1007
+ is
1008
+ } = atts;
1009
+ // istanbul ignore next
1010
+ elem = doc.createElementNS ? doc.createElementNS(NS_HTML, elStr, {
1011
+ is
1012
+ }) : doc.createElement(elStr, {
1013
+ is
1014
+ });
1015
+ } else /* istanbul ignore else */if (doc.createElementNS) {
1016
+ elem = doc.createElementNS(NS_HTML, elStr);
1017
+ } else {
1018
+ elem = doc.createElement(elStr);
1019
+ }
1020
+ // Todo: Report to plugins
1021
+ opts.$state = 'element';
1022
+ nodes[nodes.length] = elem; // Add to parent
1023
+ break;
1024
+ }
1025
+ }
1026
+ break;
1027
+ case 'object':
1028
+ {
1029
+ // Non-DOM-element objects indicate attribute-value pairs
1030
+ const atts = arg;
1031
+ if (atts.xmlns !== undefined) {
1032
+ // We handle this here, as otherwise may lose events, etc.
1033
+ // As namespace of element already set as XHTML, we need to change the namespace
1034
+ // elem.setAttribute('xmlns', atts.xmlns); // Doesn't work
1035
+ // Can't set namespaceURI dynamically, renameNode() is not supported, and setAttribute() doesn't work to change the namespace, so we resort to this hack
1036
+ const replacer = typeof atts.xmlns === 'object' ? _replaceDefiner(atts.xmlns) : ' xmlns="' + atts.xmlns + '"';
1037
+ // try {
1038
+ // Also fix DOMParser to work with text/html
1039
+ elem = nodes[nodes.length - 1] = new win.DOMParser().parseFromString(new win.XMLSerializer().serializeToString(elem)
1040
+ // Mozilla adds XHTML namespace
1041
+ .replace(' xmlns="' + NS_HTML + '"', replacer), 'application/xml').documentElement;
1042
+ // Todo: Report to plugins
1043
+ opts.$state = 'element';
1044
+ // }catch(e) {alert(elem.outerHTML);throw e;}
1045
+ }
1046
+
1047
+ _checkAtts(atts);
1048
+ break;
1049
+ }
1050
+ case 'document':
1051
+ case 'fragment':
1052
+ case 'element':
1053
+ /*
1054
+ 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)
1055
+ 2) Individual elements (DOM elements or sequences of string[/object/array]) get added to parent first-in, first-added
1056
+ */
1057
+ if (i === 0) {
1058
+ // Allow wrapping of element, fragment, or document
1059
+ elem = arg;
1060
+ // Todo: Report to plugins
1061
+ opts.$state = 'element';
1062
+ }
1063
+ if (i === argc - 1 || i === argc - 2 && args[i + 1] === null) {
1064
+ // parent
1065
+ const elsl = nodes.length;
1066
+ for (let k = 0; k < elsl; k++) {
1067
+ _appendNode(arg, nodes[k]);
1068
+ }
1069
+ // Todo: Apply stylesheets if any style tags were added elsewhere besides the first element?
1070
+ _applyAnyStylesheet(nodes[0]); // We have to execute any stylesheets even if not appending or otherwise IE will never apply them
1071
+ } else {
1072
+ nodes[nodes.length] = arg;
1073
+ }
1074
+ break;
1075
+ case 'array':
1076
+ {
1077
+ // Arrays or arrays of arrays indicate child nodes
1078
+ const child = arg;
1079
+ const cl = child.length;
1080
+ for (let j = 0; j < cl; j++) {
1081
+ // Go through children array container to handle elements
1082
+ const childContent = child[j];
1083
+ const childContentType = typeof childContent;
1084
+ if (_isNullish(childContent)) {
1085
+ throw new TypeError(`Bad children (parent array: ${JSON.stringify(args)}; index ${j} of child: ${JSON.stringify(child)})`);
1086
+ }
1087
+ switch (childContentType) {
1088
+ // Todo: determine whether null or function should have special handling or be converted to text
1089
+ case 'string':
1090
+ case 'number':
1091
+ case 'boolean':
1092
+ _appendNode(elem, doc.createTextNode(childContent));
1093
+ break;
1094
+ default:
1095
+ if (Array.isArray(childContent)) {
1096
+ // Arrays representing child elements
1097
+ opts.$state = 'children';
1098
+ _appendNode(elem, jml(opts, ...childContent));
1099
+ } else if (childContent['#']) {
1100
+ // Fragment
1101
+ opts.$state = 'fragmentChildren';
1102
+ _appendNode(elem, jml(opts, childContent['#']));
1103
+ } else {
1104
+ // Single DOM element children
1105
+ const newChildContent = checkPluginValue(elem, null, childContent, opts);
1106
+ _appendNode(elem, newChildContent);
1107
+ }
1108
+ break;
1109
+ }
1110
+ }
1111
+ break;
1112
+ }
1113
+ default:
1114
+ throw new TypeError(`Unexpected type: ${type}; arg: ${arg}; index ${i} on args: ${JSON.stringify(args)}`);
1115
+ }
1116
+ }
1117
+ const ret = nodes[0] || elem;
1118
+ if (isRoot && opts.$map && opts.$map.root) {
1119
+ setMap(true);
1120
+ }
1121
+ return ret;
1122
+ };
1123
+
1124
+ /**
1125
+ * Converts a DOM object or a string of HTML into a Jamilih object (or string).
1126
+ * @param {string|HTMLElement} dom If a string, will parse as document
1127
+ * @param {PlainObject} [config] Configuration object
1128
+ * @param {boolean} [config.stringOutput=false] Whether to output the Jamilih object as a string.
1129
+ * @param {boolean} [config.reportInvalidState=true] If true (the default), will report invalid state errors
1130
+ * @param {boolean} [config.stripWhitespace=false] Strip whitespace for text nodes
1131
+ * @throws {TypeError}
1132
+ * @returns {JamilihArray|string} Array containing the elements which represent
1133
+ * a Jamilih object, or, if `stringOutput` is true, it will be the stringified
1134
+ * version of such an object
1135
+ */
1136
+ jml.toJML = function (dom, {
1137
+ stringOutput = false,
1138
+ reportInvalidState = true,
1139
+ stripWhitespace = false
1140
+ } = {}) {
1141
+ if (typeof dom === 'string') {
1142
+ dom = new win.DOMParser().parseFromString(dom, 'text/html'); // todo: Give option for XML once implemented and change JSDoc to allow for Element
1143
+ }
1144
+
1145
+ const ret = [];
1146
+ let parent = ret;
1147
+ let parentIdx = 0;
1148
+
1149
+ /**
1150
+ * @param {string} msg
1151
+ * @throws {DOMException}
1152
+ * @returns {void}
1153
+ */
1154
+ function invalidStateError(msg) {
1155
+ // These are probably only necessary if working with text/html
1156
+ /* eslint-disable no-shadow, unicorn/custom-error-definition */
1157
+ /**
1158
+ * Polyfill for `DOMException`.
1159
+ */
1160
+ class DOMException extends Error {
1161
+ /* eslint-enable no-shadow, unicorn/custom-error-definition */
1162
+ /**
1163
+ * @param {string} message
1164
+ * @param {string} name
1165
+ */
1166
+ constructor(message, name) {
1167
+ super(message);
1168
+ // eslint-disable-next-line unicorn/custom-error-definition
1169
+ this.name = name;
1170
+ }
1171
+ }
1172
+ if (reportInvalidState) {
1173
+ // INVALID_STATE_ERR per section 9.3 XHTML 5: http://www.w3.org/TR/html5/the-xhtml-syntax.html
1174
+ const e = new DOMException(msg, 'INVALID_STATE_ERR');
1175
+ e.code = 11;
1176
+ throw e;
1177
+ }
1178
+ }
1179
+
1180
+ /**
1181
+ *
1182
+ * @param {DocumentType|Entity} obj
1183
+ * @param {Node} node
1184
+ * @returns {void}
1185
+ */
1186
+ function addExternalID(obj, node) {
1187
+ if (node.systemId.includes('"') && node.systemId.includes("'")) {
1188
+ invalidStateError('systemId cannot have both single and double quotes.');
1189
+ }
1190
+ const {
1191
+ publicId,
1192
+ systemId
1193
+ } = node;
1194
+ if (systemId) {
1195
+ obj.systemId = systemId;
1196
+ }
1197
+ if (publicId) {
1198
+ obj.publicId = publicId;
1199
+ }
1200
+ }
1201
+
1202
+ /**
1203
+ * @typedef {any} ArbitraryValue
1204
+ */
1205
+
1206
+ /**
1207
+ *
1208
+ * @param {ArbitraryValue} val
1209
+ * @returns {void}
1210
+ */
1211
+ function set(val) {
1212
+ parent[parentIdx] = val;
1213
+ parentIdx++;
1214
+ }
1215
+
1216
+ /**
1217
+ * @returns {void}
1218
+ */
1219
+ function setChildren() {
1220
+ set([]);
1221
+ parent = parent[parentIdx - 1];
1222
+ parentIdx = 0;
1223
+ }
1224
+
1225
+ /**
1226
+ *
1227
+ * @param {string} prop1
1228
+ * @param {string} prop2
1229
+ * @returns {void}
1230
+ */
1231
+ function setObj(prop1, prop2) {
1232
+ parent = parent[parentIdx - 1][prop1];
1233
+ parentIdx = 0;
1234
+ if (prop2) {
1235
+ parent = parent[prop2];
1236
+ }
1237
+ }
1238
+
1239
+ /**
1240
+ *
1241
+ * @param {Node} node
1242
+ * @param {Object<{string: string}>} namespaces
1243
+ * @throws {TypeError}
1244
+ * @returns {void}
1245
+ */
1246
+ function parseDOM(node, namespaces) {
1247
+ // namespaces = clone(namespaces) || {}; // Ensure we're working with a copy, so different levels in the hierarchy can treat it differently
1248
+
1249
+ /*
1250
+ if ((node.prefix && node.prefix.includes(':')) || (node.localName && node.localName.includes(':'))) {
1251
+ invalidStateError('Prefix cannot have a colon');
1252
+ }
1253
+ */
1254
+
1255
+ const type = 'nodeType' in node ? node.nodeType : null;
1256
+ namespaces = {
1257
+ ...namespaces
1258
+ };
1259
+ const xmlChars = /^([\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD]|[\uD800-\uDBFF][\uDC00-\uDFFF])*$/u; // eslint-disable-line no-control-regex
1260
+ if ([2, 3, 4, 7, 8].includes(type) && !xmlChars.test(node.nodeValue)) {
1261
+ invalidStateError('Node has bad XML character value');
1262
+ }
1263
+ let tmpParent, tmpParentIdx;
1264
+
1265
+ /**
1266
+ * @returns {void}
1267
+ */
1268
+ function setTemp() {
1269
+ tmpParent = parent;
1270
+ tmpParentIdx = parentIdx;
1271
+ }
1272
+ /**
1273
+ * @returns {void}
1274
+ */
1275
+ function resetTemp() {
1276
+ parent = tmpParent;
1277
+ parentIdx = tmpParentIdx;
1278
+ parentIdx++; // Increment index in parent container of this element
1279
+ }
1280
+
1281
+ switch (type) {
1282
+ case 1:
1283
+ {
1284
+ // ELEMENT
1285
+ setTemp();
1286
+ const nodeName = node.nodeName.toLowerCase(); // Todo: for XML, should not lower-case
1287
+
1288
+ setChildren(); // Build child array since elements are, except at the top level, encapsulated in arrays
1289
+ set(nodeName);
1290
+ const start = {};
1291
+ let hasNamespaceDeclaration = false;
1292
+ if (namespaces[node.prefix || ''] !== node.namespaceURI) {
1293
+ namespaces[node.prefix || ''] = node.namespaceURI;
1294
+ if (node.prefix) {
1295
+ start['xmlns:' + node.prefix] = node.namespaceURI;
1296
+ } else if (node.namespaceURI) {
1297
+ start.xmlns = node.namespaceURI;
1298
+ } else {
1299
+ start.xmlns = null;
1300
+ }
1301
+ hasNamespaceDeclaration = true;
1302
+ }
1303
+ if (node.attributes.length) {
1304
+ set([...node.attributes].reduce(function (obj, att) {
1305
+ 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
1306
+ return obj;
1307
+ }, start));
1308
+ } else if (hasNamespaceDeclaration) {
1309
+ set(start);
1310
+ }
1311
+ const {
1312
+ childNodes
1313
+ } = node;
1314
+ if (childNodes.length) {
1315
+ setChildren(); // Element children array container
1316
+ [...childNodes].forEach(function (childNode) {
1317
+ parseDOM(childNode, namespaces);
1318
+ });
1319
+ }
1320
+ resetTemp();
1321
+ break;
1322
+ }
1323
+ case undefined: // Treat as attribute node until this is fixed: https://github.com/jsdom/jsdom/issues/1641 / https://github.com/jsdom/jsdom/pull/1822
1324
+ case 2:
1325
+ // ATTRIBUTE (should only get here if passing in an attribute node)
1326
+ set({
1327
+ $attribute: [node.namespaceURI, node.name, node.value]
1328
+ });
1329
+ break;
1330
+ case 3:
1331
+ // TEXT
1332
+ if (stripWhitespace && /^\s+$/u.test(node.nodeValue)) {
1333
+ set('');
1334
+ return;
1335
+ }
1336
+ set(node.nodeValue);
1337
+ break;
1338
+ case 4:
1339
+ // CDATA
1340
+ if (node.nodeValue.includes(']]' + '>')) {
1341
+ invalidStateError('CDATA cannot end with closing ]]>');
1342
+ }
1343
+ set(['![', node.nodeValue]);
1344
+ break;
1345
+ case 5:
1346
+ // ENTITY REFERENCE (though not in browsers (was already resolved
1347
+ // anyways), ok to keep for parity with our "entity" shorthand)
1348
+ set(['&', node.nodeName]);
1349
+ break;
1350
+ case 7:
1351
+ // PROCESSING INSTRUCTION
1352
+ if (/^xml$/iu.test(node.target)) {
1353
+ invalidStateError('Processing instructions cannot be "xml".');
1354
+ }
1355
+ if (node.target.includes('?>')) {
1356
+ invalidStateError('Processing instruction targets cannot include ?>');
1357
+ }
1358
+ if (node.target.includes(':')) {
1359
+ invalidStateError('The processing instruction target cannot include ":"');
1360
+ }
1361
+ if (node.data.includes('?>')) {
1362
+ invalidStateError('Processing instruction data cannot include ?>');
1363
+ }
1364
+ set(['?', node.target, node.data]); // Todo: Could give option to attempt to convert value back into object if has pseudo-attributes
1365
+ break;
1366
+ case 8:
1367
+ // COMMENT
1368
+ if (node.nodeValue.includes('--') || node.nodeValue.length && node.nodeValue.lastIndexOf('-') === node.nodeValue.length - 1) {
1369
+ invalidStateError('Comments cannot include --');
1370
+ }
1371
+ set(['!', node.nodeValue]);
1372
+ break;
1373
+ case 9:
1374
+ {
1375
+ // DOCUMENT
1376
+ setTemp();
1377
+ const docObj = {
1378
+ $document: {
1379
+ childNodes: []
1380
+ }
1381
+ };
1382
+ set(docObj); // doc.implementation.createHTMLDocument
1383
+
1384
+ // Set position to fragment's array children
1385
+ setObj('$document', 'childNodes');
1386
+ const {
1387
+ childNodes
1388
+ } = node;
1389
+ if (!childNodes.length) {
1390
+ invalidStateError('Documents must have a child node');
1391
+ }
1392
+ // set({$xmlDocument: []}); // doc.implementation.createDocument // Todo: use this conditionally
1393
+
1394
+ [...childNodes].forEach(function (childNode) {
1395
+ // Can't just do documentElement as there may be doctype, comments, etc.
1396
+ // No need for setChildren, as we have already built the container array
1397
+ parseDOM(childNode, namespaces);
1398
+ });
1399
+ resetTemp();
1400
+ break;
1401
+ }
1402
+ case 10:
1403
+ {
1404
+ // DOCUMENT TYPE
1405
+ setTemp();
1406
+
1407
+ // Can create directly by doc.implementation.createDocumentType
1408
+ const start = {
1409
+ $DOCTYPE: {
1410
+ name: node.name
1411
+ }
1412
+ };
1413
+ const pubIdChar = /^(\u0020|\u000D|\u000A|[a-zA-Z0-9]|[-'()+,./:=?;!*#@$_%])*$/u; // eslint-disable-line no-control-regex
1414
+ if (!pubIdChar.test(node.publicId)) {
1415
+ invalidStateError('A publicId must have valid characters.');
1416
+ }
1417
+ addExternalID(start.$DOCTYPE, node);
1418
+ // 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
1419
+ set(start); // Auto-generate the internalSubset instead?
1420
+
1421
+ resetTemp();
1422
+ break;
1423
+ }
1424
+ case 11:
1425
+ {
1426
+ // DOCUMENT FRAGMENT
1427
+ setTemp();
1428
+ set({
1429
+ '#': []
1430
+ });
1431
+
1432
+ // Set position to fragment's array children
1433
+ setObj('#');
1434
+ const {
1435
+ childNodes
1436
+ } = node;
1437
+ [...childNodes].forEach(function (childNode) {
1438
+ // No need for setChildren, as we have already built the container array
1439
+ parseDOM(childNode, namespaces);
1440
+ });
1441
+ resetTemp();
1442
+ break;
1443
+ }
1444
+ default:
1445
+ throw new TypeError('Not an XML type');
1446
+ }
1447
+ }
1448
+ parseDOM(dom, {});
1449
+ if (stringOutput) {
1450
+ return JSON.stringify(ret[0]);
1451
+ }
1452
+ return ret[0];
1453
+ };
1454
+ jml.toJMLString = function (dom, config) {
1455
+ return jml.toJML(dom, Object.assign(config || {}, {
1456
+ stringOutput: true
1457
+ }));
1458
+ };
1459
+
1460
+ /**
1461
+ *
1462
+ * @param {...JamilihArray} args
1463
+ * @returns {JamilihReturn}
1464
+ */
1465
+ jml.toDOM = function (...args) {
1466
+ // Alias for jml()
1467
+ return jml(...args);
1468
+ };
1469
+
1470
+ /**
1471
+ *
1472
+ * @param {...JamilihArray} args
1473
+ * @returns {string}
1474
+ */
1475
+ jml.toHTML = function (...args) {
1476
+ // Todo: Replace this with version of jml() that directly builds a string
1477
+ const ret = jml(...args);
1478
+ // Todo: deal with serialization of properties like 'selected',
1479
+ // 'checked', 'value', 'defaultValue', 'for', 'dataset', 'on*',
1480
+ // 'style'! (i.e., need to build a string ourselves)
1481
+ return ret.outerHTML;
1482
+ };
1483
+
1484
+ /**
1485
+ *
1486
+ * @param {...JamilihArray} args
1487
+ * @returns {string}
1488
+ */
1489
+ jml.toDOMString = function (...args) {
1490
+ // Alias for jml.toHTML for parity with jml.toJMLString
1491
+ return jml.toHTML(...args);
1492
+ };
1493
+
1494
+ /**
1495
+ *
1496
+ * @param {...JamilihArray} args
1497
+ * @returns {string}
1498
+ */
1499
+ jml.toXML = function (...args) {
1500
+ const ret = jml(...args);
1501
+ return new win.XMLSerializer().serializeToString(ret);
1502
+ };
1503
+
1504
+ /**
1505
+ *
1506
+ * @param {...JamilihArray} args
1507
+ * @returns {string}
1508
+ */
1509
+ jml.toXMLDOMString = function (...args) {
1510
+ // Alias for jml.toXML for parity with jml.toJMLString
1511
+ return jml.toXML(...args);
1512
+ };
1513
+
1514
+ /**
1515
+ * Element-aware wrapper for `Map`.
1516
+ */
1517
+ class JamilihMap extends Map {
1518
+ /**
1519
+ * @param {string|Element} elem
1520
+ * @returns {ArbitraryValue}
1521
+ */
1522
+ get(elem) {
1523
+ elem = typeof elem === 'string' ? $(elem) : elem;
1524
+ return super.get.call(this, elem);
1525
+ }
1526
+ /**
1527
+ * @param {string|Element} elem
1528
+ * @param {ArbitraryValue} value
1529
+ * @returns {ArbitraryValue}
1530
+ */
1531
+ set(elem, value) {
1532
+ elem = typeof elem === 'string' ? $(elem) : elem;
1533
+ return super.set.call(this, elem, value);
1534
+ }
1535
+ /**
1536
+ * @param {string|Element} elem
1537
+ * @param {string} methodName
1538
+ * @param {...ArbitraryValue} args
1539
+ * @returns {ArbitraryValue}
1540
+ */
1541
+ invoke(elem, methodName, ...args) {
1542
+ elem = typeof elem === 'string' ? $(elem) : elem;
1543
+ return this.get(elem)[methodName](elem, ...args);
1544
+ }
1545
+ }
1546
+
1547
+ /**
1548
+ * Element-aware wrapper for `WeakMap`.
1549
+ */
1550
+ class JamilihWeakMap extends WeakMap {
1551
+ /**
1552
+ * @param {string|Element} elem
1553
+ * @returns {ArbitraryValue}
1554
+ */
1555
+ get(elem) {
1556
+ elem = typeof elem === 'string' ? $(elem) : elem;
1557
+ return super.get.call(this, elem);
1558
+ }
1559
+ /**
1560
+ * @param {string|Element} elem
1561
+ * @param {ArbitraryValue} value
1562
+ * @returns {ArbitraryValue}
1563
+ */
1564
+ set(elem, value) {
1565
+ elem = typeof elem === 'string' ? $(elem) : elem;
1566
+ return super.set.call(this, elem, value);
1567
+ }
1568
+ /**
1569
+ * @param {string|Element} elem
1570
+ * @param {string} methodName
1571
+ * @param {...ArbitraryValue} args
1572
+ * @returns {ArbitraryValue}
1573
+ */
1574
+ invoke(elem, methodName, ...args) {
1575
+ elem = typeof elem === 'string' ? $(elem) : elem;
1576
+ return this.get(elem)[methodName](elem, ...args);
1577
+ }
1578
+ }
1579
+ jml.Map = JamilihMap;
1580
+ jml.WeakMap = JamilihWeakMap;
1581
+
1582
+ /**
1583
+ * @typedef {GenericArray} MapAndElementArray
1584
+ * @property {JamilihWeakMap|JamilihMap} 0
1585
+ * @property {Element} 1
1586
+ */
1587
+
1588
+ /**
1589
+ * @param {GenericObject} obj
1590
+ * @param {...JamilihArray} args
1591
+ * @returns {MapAndElementArray}
1592
+ */
1593
+ jml.weak = function (obj, ...args) {
1594
+ const map = new JamilihWeakMap();
1595
+ const elem = jml({
1596
+ $map: [map, obj]
1597
+ }, ...args);
1598
+ return [map, elem];
1599
+ };
1600
+
1601
+ /**
1602
+ * @param {ArbitraryValue} obj
1603
+ * @param {...JamilihArray} args
1604
+ * @returns {MapAndElementArray}
1605
+ */
1606
+ jml.strong = function (obj, ...args) {
1607
+ const map = new JamilihMap();
1608
+ const elem = jml({
1609
+ $map: [map, obj]
1610
+ }, ...args);
1611
+ return [map, elem];
1612
+ };
1613
+
1614
+ /**
1615
+ * @param {string|Element} elem If a string, will be interpreted as a selector
1616
+ * @param {symbol|string} sym If a string, will be used with `Symbol.for`
1617
+ * @returns {ArbitraryValue} The value associated with the symbol
1618
+ */
1619
+ jml.symbol = jml.sym = jml.for = function (elem, sym) {
1620
+ elem = typeof elem === 'string' ? $(elem) : elem;
1621
+ return elem[typeof sym === 'symbol' ? sym : Symbol.for(sym)];
1622
+ };
1623
+
1624
+ /**
1625
+ * @param {string|Element} elem If a string, will be interpreted as a selector
1626
+ * @param {symbol|string|Map|WeakMap} symOrMap If a string, will be used with `Symbol.for`
1627
+ * @param {string|any} methodName Can be `any` if the symbol or map directly
1628
+ * points to a function (it is then used as the first argument).
1629
+ * @param {ArbitraryValue[]} args
1630
+ * @returns {ArbitraryValue}
1631
+ */
1632
+ jml.command = function (elem, symOrMap, methodName, ...args) {
1633
+ elem = typeof elem === 'string' ? $(elem) : elem;
1634
+ let func;
1635
+ if (['symbol', 'string'].includes(typeof symOrMap)) {
1636
+ func = jml.sym(elem, symOrMap);
1637
+ if (typeof func === 'function') {
1638
+ return func(methodName, ...args); // Already has `this` bound to `elem`
1639
+ }
1640
+
1641
+ return func[methodName](...args);
1642
+ }
1643
+ func = symOrMap.get(elem);
1644
+ if (typeof func === 'function') {
1645
+ return func.call(elem, methodName, ...args);
1646
+ }
1647
+ return func[methodName](elem, ...args);
1648
+ // return func[methodName].call(elem, ...args);
1649
+ };
1650
+
1651
+ /**
1652
+ * Expects properties `document`, `XMLSerializer`, and `DOMParser`.
1653
+ * Also updates `body` with `document.body`.
1654
+ * @param {Window} wind
1655
+ * @returns {void}
1656
+ */
1657
+ jml.setWindow = wind => {
1658
+ win = wind;
1659
+ doc = win.document;
1660
+ if (doc && doc.body) {
1661
+ ({
1662
+ body
1663
+ } = doc);
1664
+ }
1665
+ };
1666
+
1667
+ /**
1668
+ * @returns {Window}
1669
+ */
1670
+ jml.getWindow = () => {
1671
+ return win;
1672
+ };
1673
+
1674
+ /**
1675
+ * Does not run Jamilih so can be further processed.
1676
+ * @param {JamilihArray} jmlArray
1677
+ * @param {string|JamilihArray|Element} glu
1678
+ * @returns {Element}
1679
+ */
1680
+ function glue(jmlArray, glu) {
1681
+ return [...jmlArray].reduce((arr, item) => {
1682
+ arr.push(item, glu);
1683
+ return arr;
1684
+ }, []).slice(0, -1);
1685
+ }
1686
+
1687
+ // istanbul ignore next
1688
+ let body = doc && doc.body; // eslint-disable-line import/no-mutable-exports
1689
+
1690
+ const nbsp = '\u00A0'; // Very commonly needed in templates
1691
+
1692
+ export { $, $$, body, jml as default, glue, jml, nbsp };