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.
- package/CHANGES.md +9 -0
- package/README.md +46 -78
- package/demo/codemirror.esm.js +28242 -0
- package/demo/codemirror.js +94 -0
- package/demo/index.css +7 -0
- package/demo/index.html +11 -14
- package/demo/index.js +206 -26
- package/demo/vendor/jamilih/dist/jml.mjs +2341 -0
- package/demo/vendor/jhtml/src/SAJJ/SAJJ.ObjectArrayDelegator.js +356 -0
- package/demo/vendor/jhtml/src/SAJJ/SAJJ.Stringifier.js +186 -0
- package/demo/vendor/jhtml/src/SAJJ/SAJJ.js +746 -0
- package/demo/vendor/jhtml/src/SAJJ/testing/SAJJ.html +33 -0
- package/demo/vendor/jhtml/src/SAJJ/testing/SAJJ.testing.js +25 -0
- package/demo/vendor/jhtml/src/jhtml-browser.js +5 -0
- package/demo/vendor/jhtml/src/jhtml-node.cts +3 -0
- package/demo/vendor/jhtml/src/jhtml-node.js +8 -0
- package/demo/vendor/jhtml/src/jhtml-node.mts +1 -0
- package/demo/vendor/jhtml/src/jhtml.cts +3 -0
- package/demo/vendor/jhtml/src/jhtml.js +602 -0
- package/demo/vendor/jhtml/src/jhtml.mts +1 -0
- package/demo/vendor/jsonpath-plus/dist/index-browser-esm.js +2158 -0
- package/demo/vendor/simple-get-json/dist/index-es.js +151 -0
- package/dist/JSONPathTransformerContext.d.ts +88 -4
- package/dist/JSONPathTransformerContext.d.ts.map +1 -1
- package/dist/XPathTransformer.d.ts +2 -2
- package/dist/XPathTransformerContext.d.ts +92 -8
- package/dist/XPathTransformerContext.d.ts.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/docs/API.expanded.md +5 -3
- package/docs/API.md +1 -1
- package/docs/TO-DO.md +55 -31
- package/eslint.config.js +3 -1
- package/package.json +24 -4
- package/rollup.config.js +13 -0
- package/src/JSONPathTransformerContext.js +422 -4
- package/src/XPathTransformer.js +1 -1
- package/src/XPathTransformerContext.js +462 -11
- package/src/index.js +9 -6
- 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('"', '"') + '"');
|
|
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'] // Ӓ // 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'] // ሺ // 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('"', '"')}"`;
|
|
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 };
|