@es-joy/jsoe 0.28.0 → 0.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGES.md +4 -0
  2. package/dist/formats/schema.d.ts.map +1 -1
  3. package/dist/fundamentalTypes/arrayType.d.ts.map +1 -1
  4. package/dist/fundamentalTypes/stringType.d.ts.map +1 -1
  5. package/dist/index.js +2 -2
  6. package/dist/index.js.map +1 -1
  7. package/dist/typeChoices.d.ts.map +1 -1
  8. package/dist/types.d.ts.map +1 -1
  9. package/package.json +22 -9
  10. package/pnpm-workspace.yaml +31 -30
  11. package/src/formats/schema.js +7 -14
  12. package/src/fundamentalTypes/arrayType.js +14 -22
  13. package/src/fundamentalTypes/blobType.js +16 -16
  14. package/src/fundamentalTypes/fileType.js +13 -13
  15. package/src/fundamentalTypes/regexpType.js +1 -1
  16. package/src/fundamentalTypes/stringType.js +5 -0
  17. package/src/subTypes/blobHTMLType.js +1 -1
  18. package/src/superTypes/dommatrixType.js +1 -1
  19. package/src/superTypes/errorsSpecialType.js +1 -1
  20. package/src/typeChoices.js +1 -3
  21. package/src/types.js +0 -6
  22. package/src/utils/media.js +3 -3
  23. package/vendor/jamilih/dist/jml.mjs +898 -25
  24. package/vendor/zod/classic/checks.d.ts +1 -1
  25. package/vendor/zod/classic/checks.js +1 -1
  26. package/vendor/zod/classic/schemas.d.ts +2 -6
  27. package/vendor/zod/classic/schemas.js +9 -13
  28. package/vendor/zod/core/api.d.ts +8 -4
  29. package/vendor/zod/core/api.js +11 -23
  30. package/vendor/zod/core/checks.d.ts +16 -0
  31. package/vendor/zod/core/checks.js +31 -0
  32. package/vendor/zod/core/compile.js +20 -24
  33. package/vendor/zod/core/core.js +1 -1
  34. package/vendor/zod/core/json-schema-processors.d.ts +0 -1
  35. package/vendor/zod/core/json-schema-processors.js +8 -43
  36. package/vendor/zod/core/memoizer.js +2 -5
  37. package/vendor/zod/core/regexes.d.ts +1 -0
  38. package/vendor/zod/core/regexes.js +2 -0
  39. package/vendor/zod/core/schemas.d.ts +8 -23
  40. package/vendor/zod/core/schemas.js +39 -80
  41. package/vendor/zod/core/to-json-schema.d.ts +0 -3
  42. package/vendor/zod/core/to-json-schema.js +1 -1
  43. package/vendor/zod/core/util.js +0 -4
  44. package/vendor/zod/core/versions.js +1 -1
  45. package/vendor/zod/core/visit.js +0 -12
  46. package/vendor/zod/locales/ar.js +1 -0
  47. package/vendor/zod/locales/az.js +1 -0
  48. package/vendor/zod/locales/be.js +1 -0
  49. package/vendor/zod/locales/bg.js +1 -0
  50. package/vendor/zod/locales/bn.js +1 -0
  51. package/vendor/zod/locales/ca.js +1 -0
  52. package/vendor/zod/locales/ckb.js +1 -0
  53. package/vendor/zod/locales/cs.js +1 -0
  54. package/vendor/zod/locales/da.js +1 -0
  55. package/vendor/zod/locales/de.js +1 -0
  56. package/vendor/zod/locales/el.js +1 -0
  57. package/vendor/zod/locales/en.js +1 -0
  58. package/vendor/zod/locales/eo.js +1 -0
  59. package/vendor/zod/locales/es.js +1 -0
  60. package/vendor/zod/locales/fa.js +1 -0
  61. package/vendor/zod/locales/fi.js +1 -0
  62. package/vendor/zod/locales/fr-CA.js +1 -0
  63. package/vendor/zod/locales/fr.js +1 -0
  64. package/vendor/zod/locales/gu.js +1 -0
  65. package/vendor/zod/locales/he.js +1 -0
  66. package/vendor/zod/locales/hi.js +1 -0
  67. package/vendor/zod/locales/hr.js +1 -0
  68. package/vendor/zod/locales/hu.js +1 -0
  69. package/vendor/zod/locales/hy.js +1 -0
  70. package/vendor/zod/locales/id.js +1 -0
  71. package/vendor/zod/locales/is.js +1 -0
  72. package/vendor/zod/locales/it.js +1 -0
  73. package/vendor/zod/locales/ja.js +1 -0
  74. package/vendor/zod/locales/ka.js +1 -0
  75. package/vendor/zod/locales/km.js +1 -0
  76. package/vendor/zod/locales/kn.js +1 -0
  77. package/vendor/zod/locales/ko.js +1 -0
  78. package/vendor/zod/locales/lt.js +1 -0
  79. package/vendor/zod/locales/mk.js +1 -0
  80. package/vendor/zod/locales/ms.js +1 -0
  81. package/vendor/zod/locales/ne.js +1 -0
  82. package/vendor/zod/locales/nl.js +1 -0
  83. package/vendor/zod/locales/nn.js +1 -0
  84. package/vendor/zod/locales/no.js +1 -0
  85. package/vendor/zod/locales/ota.js +1 -0
  86. package/vendor/zod/locales/pl.js +1 -0
  87. package/vendor/zod/locales/ps.js +1 -0
  88. package/vendor/zod/locales/pt-BR.js +1 -0
  89. package/vendor/zod/locales/pt.js +1 -0
  90. package/vendor/zod/locales/ro.js +1 -0
  91. package/vendor/zod/locales/ru.js +1 -0
  92. package/vendor/zod/locales/sk.js +1 -0
  93. package/vendor/zod/locales/sl.js +1 -0
  94. package/vendor/zod/locales/sv.js +1 -0
  95. package/vendor/zod/locales/ta.js +1 -0
  96. package/vendor/zod/locales/tg.js +1 -0
  97. package/vendor/zod/locales/th.js +1 -0
  98. package/vendor/zod/locales/tk.js +1 -0
  99. package/vendor/zod/locales/tr.js +1 -0
  100. package/vendor/zod/locales/uk.js +1 -0
  101. package/vendor/zod/locales/ur.js +1 -0
  102. package/vendor/zod/locales/uz.js +1 -0
  103. package/vendor/zod/locales/vi.js +1 -0
  104. package/vendor/zod/locales/yo.js +1 -0
  105. package/vendor/zod/locales/zh-CN.js +1 -0
  106. package/vendor/zod/locales/zh-TW.js +1 -0
  107. package/vendor/zod/mini/checks.d.ts +1 -1
  108. package/vendor/zod/mini/checks.js +1 -1
  109. package/vendor/zod/mini/schemas.d.ts +2 -5
  110. package/vendor/zod/mini/schemas.js +9 -11
  111. package/vendor/zodexy/dist/esm/index.js +4 -33
  112. package/vendor/zodexy/dist/schema.zodexy.json +1 -17
@@ -1,3 +1,781 @@
1
+ /**
2
+ * The recognized keys on the optional leading options object passed to `jml()`.
3
+ *
4
+ * An object in first-argument position is only treated as an options object
5
+ * when it carries at least one of these keys.
6
+ *
7
+ * `$mode` (reserved for a future SVG/XML mode) and `$state` (the internal
8
+ * traversal marker) are deliberately absent: they are reserved names that
9
+ * `jml()` rejects when author-supplied. Templating dialects layered on Jamilih
10
+ * may use any other `$`-prefixed key freely; `validateJamilih`'s
11
+ * `allowableOptions` governs validation of those.
12
+ * @type {string[]}
13
+ */
14
+ const possibleOptions = ['$plugins', '$Map'];
15
+
16
+ /**
17
+ * Structural + policy validator for Jamilih input.
18
+ *
19
+ * Window-free and dependency-free: it re-implements the `jml()` argument
20
+ * grammar as a non-mutating recursive checker and never calls `jml()` or
21
+ * touches a DOM. Its only import is the shared {@link possibleOptions} list.
22
+ */
23
+
24
+
25
+ /**
26
+ * @typedef {"javascript"|"json"} ValidateJamilihFormat
27
+ */
28
+
29
+ /**
30
+ * @typedef {object} ValidateJamilihOptions
31
+ * @property {ValidateJamilihFormat} [format] Default `"javascript"`. `"json"`
32
+ * additionally requires every value to be losslessly JSON round-trippable.
33
+ * @property {boolean} [allowInnerHTML] Default `true`. When `false`, an
34
+ * `innerHTML` attribute key is rejected.
35
+ * @property {boolean} [allowDOM] Default `true`; forced `false` when
36
+ * `format` is `"json"`. When effectively `false`, raw DOM nodes are rejected.
37
+ * @property {string[]} [allowableOptions] Whitelist of extensible
38
+ * `$`-prefixed properties accepted wherever `$` magic is read. Sentinels:
39
+ * `"default"` expands to the builtin option keys, `"any"` / `"*"` permits
40
+ * any unknown `$`-key. Omitting the option is equivalent to `["default"]`.
41
+ * @property {boolean} [failFast] Default `false`. When `true`, stop at the
42
+ * first error.
43
+ */
44
+
45
+ /**
46
+ * @typedef {object} JamilihValidationError
47
+ * @property {string} code Stable enum, e.g. `"BAD_CHILD"`.
48
+ * @property {string} message Human-readable description.
49
+ * @property {string} path JSON-pointer-ish location, e.g. `"/2/0/1"`.
50
+ */
51
+
52
+ /**
53
+ * @typedef {object} JamilihValidationResult
54
+ * @property {boolean} valid
55
+ * @property {JamilihValidationError[]} errors
56
+ */
57
+
58
+ const FAIL_FAST = Symbol('validateJamilih.failFast');
59
+ const RESERVED_NAMES = ['$state', '$mode'];
60
+
61
+ // Node-producing keys a first-argument object may carry (mirrors `jml()`).
62
+ const FIRST_ARG_NODE_KEYS = ['#', '$text', '$document', '$DOCTYPE', '$attribute'];
63
+
64
+ // `$`-keys `_checkAtts` / the first-argument object handling recognize. Valid
65
+ // only as an attributes object or an array-wrapped first-argument object, not
66
+ // as a bare object child.
67
+ const MAGIC_ATTR_BUILTINS = ['$on', '$symbol', '$define', '$data', '$custom', '$shadow', '$attribute', '$text', '$document', '$DOCTYPE'];
68
+ const TEXTUAL_TYPES = ['string', 'number', 'boolean'];
69
+ const NON_NODE_PRIMITIVE_TYPES = ['function', 'symbol', 'bigint'];
70
+
71
+ // First-argument special strings and how many following string args they take.
72
+ const SPECIAL_STRING_ARGC = {
73
+ '!': 1,
74
+ '&': 1,
75
+ '#': 1,
76
+ '#x': 1,
77
+ '?': 2,
78
+ '![': 1
79
+ };
80
+
81
+ /**
82
+ * @param {unknown} v
83
+ * @returns {boolean}
84
+ */
85
+ function isDOMNode(v) {
86
+ return Boolean(v && typeof v === 'object' && typeof (/** @type {{nodeType?: unknown}} */v).nodeType === 'number');
87
+ }
88
+
89
+ /**
90
+ * @param {unknown} v
91
+ * @returns {boolean}
92
+ */
93
+ function isMapLike(v) {
94
+ return /^\[object (?:Weak)?Map\]$/u.test(Object.prototype.toString.call(v));
95
+ }
96
+
97
+ /**
98
+ * @param {unknown} v
99
+ * @returns {boolean}
100
+ */
101
+ function isPlainObject(v) {
102
+ if (!v || typeof v !== 'object' || Array.isArray(v)) {
103
+ return false;
104
+ }
105
+ const proto = Object.getPrototypeOf(v);
106
+ return proto === Object.prototype || proto === null;
107
+ }
108
+
109
+ /**
110
+ * @param {string} key
111
+ * @returns {boolean}
112
+ */
113
+ function isPluginKey(key) {
114
+ return key.startsWith('$_');
115
+ }
116
+
117
+ /**
118
+ * @param {unknown} v
119
+ * @returns {boolean}
120
+ */
121
+ function isNullish(v) {
122
+ return v === null || v === undefined;
123
+ }
124
+
125
+ /**
126
+ * @param {ValidateJamilihOptions} options
127
+ * @throws {TypeError} If `allowableOptions` is not an array.
128
+ * @returns {{allowAny: boolean, allowable: Set<string>, emptyPolicy: boolean}}
129
+ */
130
+ function resolveAllowable(options) {
131
+ const provided = Object.hasOwn(options, 'allowableOptions');
132
+ const raw = provided ? options.allowableOptions : ['default'];
133
+ if (!Array.isArray(raw)) {
134
+ throw new TypeError('`allowableOptions` must be an array of strings');
135
+ }
136
+ let allowAny = false;
137
+ const allowable = new Set();
138
+ for (const entry of raw) {
139
+ if (entry === 'any' || entry === '*') {
140
+ allowAny = true;
141
+ } else if (entry === 'default') {
142
+ for (const k of possibleOptions) {
143
+ allowable.add(k);
144
+ }
145
+ } else {
146
+ allowable.add(entry);
147
+ }
148
+ }
149
+ return {
150
+ allowAny,
151
+ allowable,
152
+ emptyPolicy: provided && raw.length === 0
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Validate a single-array Jamilih structure against structural and policy
158
+ * rules.
159
+ * @param {unknown} structure The JSON-serializable Jamilih array form.
160
+ * @param {ValidateJamilihOptions} [options]
161
+ * @throws {TypeError} If `format` or `allowableOptions` is invalid.
162
+ * @returns {JamilihValidationResult}
163
+ */
164
+ const validateJamilih = (structure, options = {}) => {
165
+ const format = options.format ?? 'javascript';
166
+ if (format !== 'javascript' && format !== 'json') {
167
+ throw new TypeError(`Unknown \`format\`: ${JSON.stringify(format)}`);
168
+ }
169
+ const allowInnerHTML = options.allowInnerHTML !== false;
170
+ const failFast = options.failFast === true;
171
+ const {
172
+ allowAny,
173
+ allowable,
174
+ emptyPolicy
175
+ } = resolveAllowable(options);
176
+
177
+ // `json` can never carry a DOM node; the explicit combination is a conflict.
178
+ const domConflict = format === 'json' && options.allowDOM === true;
179
+ const allowDOM = format === 'json' ? false : options.allowDOM !== false;
180
+
181
+ /** @type {JamilihValidationError[]} */
182
+ const errors = [];
183
+
184
+ /**
185
+ * @param {string} code
186
+ * @param {string} message
187
+ * @param {(string|number)[]} pathParts
188
+ * @returns {void}
189
+ */
190
+ const err = (code, message, pathParts) => {
191
+ errors.push({
192
+ code,
193
+ message,
194
+ path: pathParts.length ? '/' + pathParts.join('/') : '/'
195
+ });
196
+ if (failFast) {
197
+ throw FAIL_FAST;
198
+ }
199
+ };
200
+
201
+ /**
202
+ * @param {string} key
203
+ * @returns {boolean}
204
+ */
205
+ const isReserved = key => RESERVED_NAMES.includes(key);
206
+
207
+ /**
208
+ * @param {string} key
209
+ * @returns {boolean}
210
+ */
211
+ const isOptionKey = key => possibleOptions.includes(key);
212
+
213
+ /**
214
+ * Deep JSON round-trippability check for `format: "json"`.
215
+ * @param {unknown} v
216
+ * @param {(string|number)[]} path
217
+ * @returns {void}
218
+ */
219
+ const checkJSONValue = (v, path) => {
220
+ if (v === null) {
221
+ return;
222
+ }
223
+ const t = typeof v;
224
+ if (t === 'string' || t === 'boolean') {
225
+ return;
226
+ }
227
+ if (t === 'number') {
228
+ if (!Number.isFinite(v)) {
229
+ err('NON_JSON_VALUE', '`NaN`/`Infinity` is not valid JSON', path);
230
+ }
231
+ return;
232
+ }
233
+ if (t === 'undefined') {
234
+ err('NON_JSON_VALUE', '`undefined` is not valid JSON', path);
235
+ return;
236
+ }
237
+ if (NON_NODE_PRIMITIVE_TYPES.includes(t)) {
238
+ err('NON_JSON_VALUE', `A ${t} value is not valid JSON`, path);
239
+ return;
240
+ }
241
+ if (isDOMNode(v)) {
242
+ err('NON_JSON_VALUE', 'A DOM node is not valid JSON', path);
243
+ return;
244
+ }
245
+ if (isMapLike(v)) {
246
+ err('NON_JSON_VALUE', 'A Map/WeakMap is not valid JSON', path);
247
+ return;
248
+ }
249
+ if (Array.isArray(v)) {
250
+ v.forEach((item, i) => checkJSONValue(item, [...path, i]));
251
+ return;
252
+ }
253
+ if (!isPlainObject(v)) {
254
+ err('NON_JSON_VALUE', 'A non-plain object (Date/RegExp/class instance) is not valid JSON', path);
255
+ return;
256
+ }
257
+ for (const [k, item] of Object.entries(/** @type {Record<string, unknown>} */v)) {
258
+ checkJSONValue(item, [...path, k]);
259
+ }
260
+ };
261
+
262
+ /**
263
+ * @param {string} key
264
+ * @param {unknown} val
265
+ * @param {(string|number)[]} path
266
+ * @returns {void}
267
+ */
268
+ const checkExtensibleMagicKey = (key, val, path) => {
269
+ if (allowAny || allowable.has(key)) {
270
+ if (format === 'json') {
271
+ checkJSONValue(val, path);
272
+ }
273
+ return;
274
+ }
275
+ err('UNKNOWN_MAGIC_PROPERTY', `Unknown \`$\`-prefixed property \`${key}\` is not permitted by \`allowableOptions\``, path);
276
+ };
277
+
278
+ /**
279
+ * @param {Record<string, unknown>} obj
280
+ * @param {(string|number)[]} path
281
+ * @returns {void}
282
+ */
283
+ const flagMisplacedOptions = (obj, path) => {
284
+ for (const k of Object.keys(obj)) {
285
+ if (k === '$state' || isOptionKey(k)) {
286
+ err('MISPLACED_OPTIONS_OBJECT', `\`${k}\` is a root-only option and cannot appear on a nested array head`, [...path, k]);
287
+ }
288
+ }
289
+ };
290
+
291
+ /**
292
+ * @param {unknown} val
293
+ * @param {(string|number)[]} path
294
+ * @returns {void}
295
+ */
296
+ const validatePluginsOption = (val, path) => {
297
+ if (!Array.isArray(val)) {
298
+ err('BAD_ATTRIBUTES_OBJECT', '`$plugins` must be an array', path);
299
+ return;
300
+ }
301
+ val.forEach((p, i) => {
302
+ if (!p || typeof p !== 'object') {
303
+ err('BAD_ATTRIBUTES_OBJECT', '`$plugins` entries must be objects', [...path, i]);
304
+ return;
305
+ }
306
+ const plugin = /** @type {{name?: unknown, set?: unknown}} */p;
307
+ if (typeof plugin.name !== 'string' || !plugin.name.startsWith('$_')) {
308
+ err('BAD_ATTRIBUTES_OBJECT', 'Plugin `name` must be a string beginning with `$_`', [...path, i, 'name']);
309
+ }
310
+ if (format === 'json') {
311
+ err('NON_JSON_CONSTRUCT', 'Plugins require a `set` function; not valid in `format: "json"`', [...path, i, 'set']);
312
+ } else if (typeof plugin.set !== 'function') {
313
+ err('BAD_ATTRIBUTES_OBJECT', 'Plugin `set` must be a function', [...path, i, 'set']);
314
+ }
315
+ });
316
+ };
317
+
318
+ /**
319
+ * @param {unknown} val
320
+ * @param {(string|number)[]} path
321
+ * @returns {void}
322
+ */
323
+ const validateMapOption = (val, path) => {
324
+ if (format === 'json') {
325
+ err('NON_JSON_CONSTRUCT', '`$Map` carries a Map/WeakMap; not valid in `format: "json"`', path);
326
+ return;
327
+ }
328
+ const ok = isMapLike(val) || Array.isArray(val) && (val[0] === undefined || isMapLike(val[0])) || isPlainObject(val) && Object.hasOwn(/** @type {object} */val, 'root');
329
+ if (!ok) {
330
+ err('BAD_DATA', '`$Map` must be `[map, value]` or `{root: [...], ...}`', path);
331
+ }
332
+ };
333
+
334
+ /**
335
+ * @param {unknown} val
336
+ * @param {(string|number)[]} path
337
+ * @returns {void}
338
+ */
339
+ const validateOnObject = (val, path) => {
340
+ if (!isPlainObject(val)) {
341
+ err('BAD_ON_HANDLER', '`$on` must be an object of event handlers', path);
342
+ return;
343
+ }
344
+ const entries = Object.entries(/** @type {Record<string, unknown>} */val);
345
+ if (entries.length === 0) {
346
+ return; // documented no-op
347
+ }
348
+ if (format === 'json') {
349
+ err('NON_JSON_CONSTRUCT', '`$on` handlers are functions; not valid in `format: "json"`', path);
350
+ return;
351
+ }
352
+ for (const [evt, handler] of entries) {
353
+ const okHandler = typeof handler === 'function' || Array.isArray(handler) && typeof handler[0] === 'function';
354
+ if (!okHandler) {
355
+ err('BAD_ON_HANDLER', `\`$on.${evt}\` must be a function or \`[function, capturing]\``, [...path, evt]);
356
+ }
357
+ }
358
+ };
359
+
360
+ /**
361
+ * @param {unknown} val
362
+ * @param {(string|number)[]} path
363
+ * @returns {void}
364
+ */
365
+ const validateShadowObject = (val, path) => {
366
+ if (!isPlainObject(val)) {
367
+ err('BAD_SHADOW', '`$shadow` must be an object', path);
368
+ return;
369
+ }
370
+ const obj = /** @type {Record<string, unknown>} */val;
371
+ for (const slot of ['template', 'content']) {
372
+ if (!Object.hasOwn(obj, slot)) {
373
+ continue;
374
+ }
375
+ const v = obj[slot];
376
+ if (Array.isArray(v)) {
377
+ validateChildrenContainer(v, [...path, slot]);
378
+ } else if (isDOMNode(v)) {
379
+ if (!allowDOM) {
380
+ err('DOM_NODE_NOT_ALLOWED', `\`$shadow.${slot}\` DOM node is not allowed`, [...path, slot]);
381
+ }
382
+ } else if (typeof v !== 'string' && typeof v !== 'boolean' && !isNullish(v)) {
383
+ err('BAD_SHADOW', `\`$shadow.${slot}\` must be a Jamilih array, selector string, or DOM node`, [...path, slot]);
384
+ }
385
+ }
386
+ };
387
+
388
+ /**
389
+ * @param {unknown} val
390
+ * @param {(string|number)[]} path
391
+ * @returns {void}
392
+ */
393
+ const validateDocumentObject = (val, path) => {
394
+ if (!isPlainObject(val)) {
395
+ err('BAD_ATTRIBUTES_OBJECT', '`$document` must be an object', path);
396
+ return;
397
+ }
398
+ const obj = /** @type {Record<string, unknown>} */val;
399
+ for (const slot of ['childNodes', 'head', 'body']) {
400
+ if (Array.isArray(obj[slot])) {
401
+ validateChildrenContainer(/** @type {unknown[]} */obj[slot], [...path, slot]);
402
+ }
403
+ }
404
+ };
405
+
406
+ /**
407
+ * @param {Record<string, unknown>} obj
408
+ * @param {(string|number)[]} path
409
+ * @param {boolean} isChildHead
410
+ * @returns {void}
411
+ */
412
+ const validateAttributesObject = (obj, path, isChildHead) => {
413
+ for (const [key, val] of Object.entries(obj)) {
414
+ const at = [...path, key];
415
+ if (key === '#') {
416
+ if (Array.isArray(val)) {
417
+ validateChildrenContainer(val, at);
418
+ } else {
419
+ err('BAD_ATTRIBUTES_OBJECT', '`#` must hold an array of children', at);
420
+ }
421
+ continue;
422
+ }
423
+ if (isReserved(key)) {
424
+ err('RESERVED_OPTION', `\`${key}\` is reserved by Jamilih and may not be supplied`, at);
425
+ continue;
426
+ }
427
+ if (isOptionKey(key)) {
428
+ err('MISPLACED_OPTIONS_OBJECT', `\`${key}\` is a root-only option and cannot appear ${isChildHead ? 'on a nested array head' : 'in an attributes object'}`, at);
429
+ continue;
430
+ }
431
+ if (key === 'innerHTML') {
432
+ if (!allowInnerHTML) {
433
+ err('INNERHTML_NOT_ALLOWED', '`innerHTML` is not allowed (`allowInnerHTML: false`)', at);
434
+ } else if (typeof val !== 'string' && !isNullish(val)) {
435
+ err('BAD_ATTRIBUTES_OBJECT', '`innerHTML` must be a string', at);
436
+ }
437
+ continue;
438
+ }
439
+ if (key === '$text') {
440
+ if (typeof val !== 'string') {
441
+ err('BAD_ATTRIBUTES_OBJECT', '`$text` must be a string', at);
442
+ }
443
+ continue;
444
+ }
445
+ if (key === '$attribute') {
446
+ const bad = !Array.isArray(val) || val.length < 2 || val.length > 3 || val.slice(1).some(s => !isNullish(s) && typeof s !== 'string');
447
+ if (bad) {
448
+ err('BAD_ATTRIBUTE_NODE', '`$attribute` must be `[namespace, name, value?]`', at);
449
+ }
450
+ continue;
451
+ }
452
+ if (key === '$DOCTYPE') {
453
+ if (!isPlainObject(val) || typeof (/** @type {{name?: unknown}} */val).name !== 'string') {
454
+ err('BAD_DOCTYPE', '`$DOCTYPE` must be an object with a string `name`', at);
455
+ }
456
+ continue;
457
+ }
458
+ if (key === '$document') {
459
+ validateDocumentObject(val, at);
460
+ continue;
461
+ }
462
+ if (key === '$on') {
463
+ validateOnObject(val, at);
464
+ continue;
465
+ }
466
+ if (key === '$symbol') {
467
+ if (format === 'json') {
468
+ err('NON_JSON_CONSTRUCT', '`$symbol` is not valid in `format: "json"`', at);
469
+ } else {
470
+ const bad = !Array.isArray(val) || val.length !== 2 || typeof val[0] !== 'string' && typeof val[0] !== 'symbol' || typeof val[1] !== 'function' && !isPlainObject(val[1]);
471
+ if (bad) {
472
+ err('BAD_SYMBOL', '`$symbol` must be `[symbol|string, function|object]`', at);
473
+ }
474
+ }
475
+ continue;
476
+ }
477
+ if (key === '$define') {
478
+ if (format === 'json') {
479
+ err('NON_JSON_CONSTRUCT', '`$define` is not valid in `format: "json"`', at);
480
+ } else if (typeof val !== 'function' && !isPlainObject(val) && !Array.isArray(val)) {
481
+ err('BAD_DEFINE', '`$define` must be a function, mixin object, or array', at);
482
+ }
483
+ continue;
484
+ }
485
+ if (key === '$data') {
486
+ if (format === 'json') {
487
+ err('NON_JSON_CONSTRUCT', '`$data` requires an options `Map`; not valid in `format: "json"`', at);
488
+ } else if (val !== true && !Array.isArray(val) && !isMapLike(val) && !isPlainObject(val)) {
489
+ err('BAD_DATA', '`$data` must be `true`, an array, a Map, or a data object', at);
490
+ }
491
+ continue;
492
+ }
493
+ if (key === '$custom') {
494
+ if (val && (typeof val === 'object' || typeof val === 'function') && Object.prototype.propertyIsEnumerable.call(val, '__proto__')) {
495
+ err('BAD_CUSTOM_PROTO', '`$custom` may not define `__proto__`', at);
496
+ }
497
+ if (format === 'json') {
498
+ checkJSONValue(val, at);
499
+ }
500
+ continue;
501
+ }
502
+ if (key === '$shadow') {
503
+ validateShadowObject(val, at);
504
+ continue;
505
+ }
506
+ if (isPluginKey(key)) {
507
+ if (format === 'json') {
508
+ checkJSONValue(val, at);
509
+ }
510
+ continue;
511
+ }
512
+ if (!key.startsWith('$') && key.startsWith('on')) {
513
+ if (format === 'json' && typeof val === 'function') {
514
+ err('NON_JSON_CONSTRUCT', `\`${key}\` handler function is not valid in \`format: "json"\``, at);
515
+ }
516
+ continue;
517
+ }
518
+ if (key.startsWith('$')) {
519
+ checkExtensibleMagicKey(key, val, at);
520
+ continue;
521
+ }
522
+ // Ordinary attribute (including `class`, `style`, `dataset`, ...). In
523
+ // `javascript` mode jml() coerces the value via `setAttribute`, so
524
+ // nothing further is enforced here.
525
+ if (format === 'json') {
526
+ checkJSONValue(val, at);
527
+ }
528
+ }
529
+ };
530
+
531
+ /**
532
+ * A children-array container: each entry is a child node.
533
+ * @param {unknown[]} arr
534
+ * @param {(string|number)[]} path
535
+ * @returns {void}
536
+ */
537
+ const validateChildrenContainer = (arr, path) => {
538
+ arr.forEach((child, j) => {
539
+ const at = [...path, j];
540
+ if (isNullish(child)) {
541
+ err('BAD_CHILD', '`null`/`undefined` is not a valid child', at);
542
+ return;
543
+ }
544
+ const t = typeof child;
545
+ if (TEXTUAL_TYPES.includes(t)) {
546
+ if (format === 'json' && t === 'number' && !Number.isFinite(child)) {
547
+ err('NON_JSON_VALUE', '`NaN`/`Infinity` text child is not valid JSON', at);
548
+ }
549
+ return;
550
+ }
551
+ if (NON_NODE_PRIMITIVE_TYPES.includes(t)) {
552
+ err('BAD_CHILD', `A bare ${t} is not a valid child`, at);
553
+ return;
554
+ }
555
+ if (isDOMNode(child)) {
556
+ if (!allowDOM) {
557
+ err('DOM_NODE_NOT_ALLOWED', 'Raw DOM node children are not allowed', at);
558
+ }
559
+ return;
560
+ }
561
+ if (Array.isArray(child)) {
562
+ if (child.length === 0) {
563
+ err('BAD_CHILD', 'A child array must not be empty', at);
564
+ return;
565
+ }
566
+ const head = child[0];
567
+ if (typeof head !== 'string' && !isPlainObject(head)) {
568
+ err('BAD_CHILD', 'A child array must be headed by a string or object', at);
569
+ return;
570
+ }
571
+ validateArgSequence(child, 0, at, true);
572
+ return;
573
+ }
574
+ if (isPlainObject(child)) {
575
+ const obj = /** @type {Record<string, unknown>} */child;
576
+ if (Object.hasOwn(obj, '#')) {
577
+ if (Array.isArray(obj['#'])) {
578
+ validateChildrenContainer(/** @type {unknown[]} */obj['#'], [...at, '#']);
579
+ } else {
580
+ err('BAD_CHILD', 'A `#` fragment child must hold an array', [...at, '#']);
581
+ }
582
+ return;
583
+ }
584
+ const keys = Object.keys(obj);
585
+ if (keys.length > 0 && keys.every(k => k.startsWith('$'))) {
586
+ // Templating-dialect placeholder child; policy-checked, opaque value.
587
+ flagMisplacedOptions(obj, at);
588
+ for (const k of keys) {
589
+ if (MAGIC_ATTR_BUILTINS.includes(k)) {
590
+ err('BAD_CHILD', `\`${k}\` produces a node and must be array-wrapped (\`[{${k}: ...}]\`), not a bare object child`, [...at, k]);
591
+ } else if (!isReserved(k) && !isOptionKey(k)) {
592
+ checkExtensibleMagicKey(k, obj[k], [...at, k]);
593
+ }
594
+ }
595
+ return;
596
+ }
597
+ err('BAD_CHILD', 'A plain object child must be a `#` fragment or `$`-magic placeholder', at);
598
+ return;
599
+ }
600
+ if (isMapLike(child)) {
601
+ err('BAD_CHILD', 'A Map/WeakMap is not a valid child', at);
602
+ return;
603
+ }
604
+ err('BAD_CHILD', 'Unrecognized child value', at);
605
+ });
606
+ };
607
+
608
+ /**
609
+ * @param {unknown[]} args
610
+ * @param {number} i
611
+ * @param {(string|number)[]} path
612
+ * @returns {number} Number of *additional* args consumed after `i`.
613
+ */
614
+ const validateStringArg = (args, i, path) => {
615
+ const arg = /** @type {string} */args[i];
616
+ if (arg === '' || !Object.hasOwn(SPECIAL_STRING_ARGC, arg)) {
617
+ // Fragment marker or an ordinary element name; nothing to consume here.
618
+ return 0;
619
+ }
620
+ const need = SPECIAL_STRING_ARGC[(/** @type {keyof typeof SPECIAL_STRING_ARGC} */arg)];
621
+ for (let k = 1; k <= need; k++) {
622
+ const follow = args[i + k];
623
+ if (isNullish(follow) || typeof follow !== 'string' && typeof follow !== 'number') {
624
+ err(arg === '?' ? 'BAD_PROCESSING_INSTRUCTION' : 'BAD_SPECIAL_ARG', `\`${arg}\` must be followed by ${need} string argument${need > 1 ? 's' : ''}`, [...path, i]);
625
+ return 0;
626
+ }
627
+ }
628
+ return need;
629
+ };
630
+
631
+ /**
632
+ * @param {unknown[]} args
633
+ * @param {number} start
634
+ * @param {(string|number)[]} path
635
+ * @param {boolean} [headIsChild] The object at `start`, if any, is the head
636
+ * of a nested child array (so root-only options there are misplaced).
637
+ * @returns {void}
638
+ */
639
+ const validateArgSequence = (args, start, path, headIsChild = false) => {
640
+ let i = start;
641
+ while (i < args.length) {
642
+ const arg = args[i];
643
+ const at = [...path, i];
644
+ if (arg === null) {
645
+ if (i !== args.length - 1) {
646
+ err('MISPLACED_NULL', '`null` is only allowed as the final argument', at);
647
+ }
648
+ i += 1;
649
+ continue;
650
+ }
651
+ if (arg === undefined) {
652
+ err('BAD_FIRST_ARG', '`undefined` is not a valid Jamilih argument', at);
653
+ i += 1;
654
+ continue;
655
+ }
656
+ const t = typeof arg;
657
+ if (t === 'string') {
658
+ i += 1 + validateStringArg(args, i, path);
659
+ continue;
660
+ }
661
+ if (t !== 'object') {
662
+ err('UNKNOWN_TYPE', `Unexpected ${t} in argument position`, at);
663
+ i += 1;
664
+ continue;
665
+ }
666
+ if (isDOMNode(arg)) {
667
+ if (!allowDOM) {
668
+ err('DOM_NODE_NOT_ALLOWED', 'Raw DOM nodes are not allowed here', at);
669
+ }
670
+ } else if (Array.isArray(arg)) {
671
+ validateChildrenContainer(arg, at);
672
+ } else if (isMapLike(arg)) {
673
+ err('DOM_NODE_NOT_ALLOWED', 'A Map/WeakMap is not a valid Jamilih argument', at);
674
+ } else if (isPlainObject(arg)) {
675
+ validateAttributesObject(/** @type {Record<string, unknown>} */arg, at, headIsChild && i === start);
676
+ } else {
677
+ err('UNKNOWN_TYPE', 'Unrecognized argument type', at);
678
+ }
679
+ i += 1;
680
+ }
681
+ };
682
+
683
+ /**
684
+ * @param {Record<string, unknown>} obj
685
+ * @param {(string|number)[]} path
686
+ * @returns {void}
687
+ */
688
+ const validateOptionsObject = (obj, path) => {
689
+ if (emptyPolicy) {
690
+ err('OPTIONS_OBJECT_NOT_ALLOWED', 'A leading options object is not permitted (`allowableOptions: []`)', path);
691
+ return;
692
+ }
693
+ for (const [key, val] of Object.entries(obj)) {
694
+ if (isReserved(key)) {
695
+ err('RESERVED_OPTION', `\`${key}\` is reserved by Jamilih and may not be supplied`, [...path, key]);
696
+ continue;
697
+ }
698
+ if (isOptionKey(key)) {
699
+ if (!allowAny && !allowable.has(key)) {
700
+ err('DISALLOWED_OPTION', `Option \`${key}\` is not permitted by \`allowableOptions\``, [...path, key]);
701
+ } else if (key === '$plugins') {
702
+ validatePluginsOption(val, [...path, key]);
703
+ } else if (key === '$Map') {
704
+ validateMapOption(val, [...path, key]);
705
+ }
706
+ continue;
707
+ }
708
+ if (!key.startsWith('$')) {
709
+ err('ATTRIBUTES_BEFORE_ELEMENT', 'A genuine attribute may not appear on an object before an element', [...path, key]);
710
+ continue;
711
+ }
712
+ checkExtensibleMagicKey(key, val, [...path, key]);
713
+ }
714
+ };
715
+
716
+ /**
717
+ * @param {unknown} struct
718
+ * @returns {void}
719
+ */
720
+ const validateStructure = struct => {
721
+ if (!Array.isArray(struct)) {
722
+ err('NOT_ARRAY', 'Jamilih structure must be an array', []);
723
+ return;
724
+ }
725
+ if (struct.length === 0) {
726
+ err('EMPTY_ARRAY', 'Jamilih structure must not be empty', []);
727
+ return;
728
+ }
729
+ let argStart = 0;
730
+ const first = struct[0];
731
+ if (isPlainObject(first) && !isDOMNode(first)) {
732
+ const firstObj = /** @type {Record<string, unknown>} */first;
733
+ const keys = Object.keys(firstObj);
734
+ if (possibleOptions.some(k => keys.includes(k))) {
735
+ validateOptionsObject(firstObj, ['0']);
736
+ argStart = 1;
737
+ } else if (keys.some(k => RESERVED_NAMES.includes(k))) {
738
+ for (const k of keys) {
739
+ if (RESERVED_NAMES.includes(k)) {
740
+ err('RESERVED_OPTION', `\`${k}\` is reserved by Jamilih and may not be supplied`, ['0', k]);
741
+ }
742
+ }
743
+ argStart = 1;
744
+ } else if (keys.some(k => FIRST_ARG_NODE_KEYS.includes(k))) ; else if (keys.every(k => k.startsWith('$'))) {
745
+ // Dialect-only / empty object: base Jamilih skips it.
746
+ argStart = 1;
747
+ } else {
748
+ err('ATTRIBUTES_BEFORE_ELEMENT', 'A genuine attribute may not appear on an object before an element', ['0']);
749
+ argStart = 1;
750
+ }
751
+ }
752
+ validateArgSequence(struct, argStart, []);
753
+ };
754
+ try {
755
+ if (domConflict) {
756
+ err('OPTION_CONFLICT', '`allowDOM: true` conflicts with `format: "json"`; DOM nodes stay rejected', []);
757
+ }
758
+ validateStructure(structure);
759
+ } catch (e) {
760
+ /* c8 ignore next 3 -- defensive: only the fail-fast sentinel is caught here */
761
+ if (e !== FAIL_FAST) {
762
+ throw e;
763
+ }
764
+ }
765
+ return {
766
+ valid: errors.length === 0,
767
+ errors
768
+ };
769
+ };
770
+
771
+ /**
772
+ * Boolean predicate wrapper around {@link validateJamilih}.
773
+ * @param {unknown} structure
774
+ * @param {ValidateJamilihOptions} [options]
775
+ * @returns {boolean}
776
+ */
777
+ const isValidJamilih = (structure, options) => validateJamilih(structure, options).valid;
778
+
1
779
  /* eslint-disable unicorn/prefer-global-this -- Easier */
2
780
  /* eslint-disable unicorn/no-break-in-nested-loop -- Performance to avoid function? */
3
781
  /* eslint-disable sonarjs/no-control-regex -- Intentional */
@@ -23,6 +801,7 @@ Other Todos:
23
801
  0. Redo browser testing of jml
24
802
  */
25
803
 
804
+
26
805
  /**
27
806
  * @typedef {Window & {DocumentFragment: typeof DocumentFragment}} HTMLWindow
28
807
  */
@@ -81,11 +860,13 @@ let doc = typeof document !== 'undefined' && document || win?.document;
81
860
 
82
861
  // STATIC PROPERTIES
83
862
 
84
- const possibleOptions = ['$plugins',
85
- // '$mode', // Todo (SVG/XML)
86
- // '$state', // Used internally
87
- '$map' // Add any other options here
88
- ];
863
+ /**
864
+ * Brands `opts` objects that `jml()` created or adopted internally, so a
865
+ * user-supplied `$state` (which would corrupt root detection) can be told
866
+ * apart from the internal traversal marker of the same name.
867
+ * @type {WeakSet<object>}
868
+ */
869
+ const internalOpts = new WeakSet();
89
870
  const NS_HTML = 'http://www.w3.org/1999/xhtml',
90
871
  hyphenForCamelCase = /-([a-z])/gu;
91
872
  const ATTR_MAP = new Map([['maxlength', 'maxLength'], ['minlength', 'minLength'], ['readonly', 'readOnly']]);
@@ -241,7 +1022,8 @@ function _isHTMLElement(item) {
241
1022
  * @static
242
1023
  * @param {string|JamilihAttributes|JamilihArray|JamilihChildren|
243
1024
  * JamilihDocumentFragment|JamilihAttributeNode|
244
- * JamilihOptions|HTMLElement|Document|DocumentFragment|null|undefined} item
1025
+ * JamilihOptions|JamilihDialectObject|HTMLElement|Document|DocumentFragment|
1026
+ * null|undefined} item
245
1027
  * @returns {"string"|"null"|"array"|"element"|"fragment"|"object"|
246
1028
  * "symbol"|"bigint"|"function"|"number"|"boolean"|"undefined"|
247
1029
  * "document"|"processing-instruction"|"non-container node"}
@@ -344,11 +1126,22 @@ function _childrenToJML(node) {
344
1126
  };
345
1127
  }
346
1128
 
1129
+ /**
1130
+ * The `$`-prefixed properties a templating dialect has registered on the
1131
+ * globally augmentable {@link JamilihDialectProperties} interface (see
1132
+ * `src/jamilih-dialect.d.ts`). This is the escape hatch that lets those keys
1133
+ * appear on a leading Jamilih object and as a bare `$`-only child; a `$`-key
1134
+ * that has not been registered (and any non-`$` attribute) stays a type
1135
+ * error. Resolves to `never` until the interface is augmented, so it adds
1136
+ * nothing to a union in the default (strict) case.
1137
+ * @typedef {[keyof JamilihDialectProperties] extends [never] ? never : Partial<JamilihDialectProperties>} JamilihDialectObject
1138
+ */
1139
+
347
1140
  /**
348
1141
  * Keep this in sync with `JamilihArray`'s first argument (minus `Document`).
349
1142
  * @typedef {JamilihDoc|JamilihDoctype|JamilihTextNode|
350
- * JamilihAttributeNode|JamilihOptions|ElementName|HTMLElement|
351
- * JamilihDocumentFragment
1143
+ * JamilihAttributeNode|JamilihOptions|JamilihDialectObject|ElementName|
1144
+ * HTMLElement|JamilihDocumentFragment
352
1145
  * } JamilihFirstArg
353
1146
  */
354
1147
 
@@ -691,6 +1484,9 @@ function _DOMfromJMLOrString (childNodeJML) {
691
1484
  */
692
1485
 
693
1486
  /**
1487
+ * `JamilihDialectObject` is deliberately absent: this type is reused for the
1488
+ * attributes position, where every recognized `$`-magic key must keep its
1489
+ * specific value type.
694
1490
  * @typedef {Document|ElementName|HTMLElement|DocumentFragment|
695
1491
  * JamilihDocumentFragment|JamilihDoc|JamilihDoctype|JamilihTextNode|
696
1492
  * JamilihAttributeNode} JamilihFirstArgument
@@ -709,7 +1505,7 @@ function _DOMfromJMLOrString (childNodeJML) {
709
1505
  * @typedef {(
710
1506
  * JamilihArray|JamilihArrayLike|TextNodeString|HTMLElement|Comment|
711
1507
  * ProcessingInstruction|Text|DocumentFragment|JamilihProcessingInstruction|
712
- * JamilihDocumentFragment|PluginReference
1508
+ * JamilihDocumentFragment|PluginReference|JamilihDialectObject
713
1509
  * )[]} JamilihChildren
714
1510
  */
715
1511
 
@@ -735,7 +1531,7 @@ function _DOMfromJMLOrString (childNodeJML) {
735
1531
  * The sixth last optional argument is null, used to indicate an array of elements
736
1532
  * should be returned.
737
1533
  * @typedef {[
738
- * JamilihOptions|JamilihFirstArgument,
1534
+ * JamilihOptions|JamilihFirstArgument|JamilihDialectObject,
739
1535
  * (JamilihFirstArgument|
740
1536
  * JamilihAttributes|
741
1537
  * JamilihChildren|
@@ -770,10 +1566,17 @@ function _DOMfromJMLOrString (childNodeJML) {
770
1566
  */
771
1567
 
772
1568
  /**
773
- * @typedef {object} JamilihOptions
774
- * @property {TraversalState} [$state]
775
- * @property {JamilihPlugin[]} [$plugins]
776
- * @property {MapWithRoot|[Map<HTMLElement,UserArg>|WeakMap<HTMLElement,UserArg>, UserArg]} [$map]
1569
+ * The optional leading object. `$state` is internal and `$mode` is reserved
1570
+ * (both throw when author-supplied). A templating dialect's own `$`-keys are
1571
+ * accepted here only after they are registered on the globally augmentable
1572
+ * {@link JamilihDialectProperties} interface (see `src/jamilih-dialect.d.ts`);
1573
+ * an unregistered `$`-key — and any genuine non-`$` attribute — stays a
1574
+ * compile error.
1575
+ * @typedef {{
1576
+ * $state?: TraversalState,
1577
+ * $plugins?: JamilihPlugin[],
1578
+ * $Map?: MapWithRoot|[Map<HTMLElement,UserArg>|WeakMap<HTMLElement,UserArg>, UserArg]
1579
+ * }} JamilihOptions
777
1580
  */
778
1581
 
779
1582
  /**
@@ -1124,9 +1927,16 @@ const jml = function jml(...args) {
1124
1927
  }
1125
1928
  case '$state':
1126
1929
  {
1127
- // Handled internally
1930
+ // Handled internally; only valid on a jml-created `opts` object
1931
+ if (!internalOpts.has(atts)) {
1932
+ throw new TypeError(`\`$state\` is set internally by Jamilih and may not be supplied; args: ${JSON.stringify(args)}`);
1933
+ }
1128
1934
  break;
1129
1935
  }
1936
+ case '$mode':
1937
+ {
1938
+ throw new TypeError(`\`$mode\` is reserved for future use and not yet implemented; args: ${JSON.stringify(args)}`);
1939
+ }
1130
1940
  case 'is':
1131
1941
  {
1132
1942
  // Currently only in Chrome
@@ -1434,10 +2244,13 @@ const jml = function jml(...args) {
1434
2244
  };
1435
2245
  recurse(/** @type {DatasetAttributeObject} */attVal, '');
1436
2246
  break;
1437
- // Todo: Disable this by default unless configuration explicitly allows (for security)
1438
2247
  }
1439
2248
  // #if IS_REMOVE
1440
2249
  // Don't remove this `if` block (for sake of no-innerHTML build)
2250
+ // Security: this assigns `innerHTML` directly, so its value must be
2251
+ // trusted or sanitized by the caller. The `jamilih` package also
2252
+ // publishes a `jml-noinnerh` build with this sink removed, and
2253
+ // `validateJamilih({allowInnerHTML: false})` rejects the key.
1441
2254
  case 'innerHTML':
1442
2255
  if (!_isNullish(attVal)) {
1443
2256
  // eslint-disable-next-line no-unsanitized/property
@@ -1515,6 +2328,13 @@ const jml = function jml(...args) {
1515
2328
  });
1516
2329
  break;
1517
2330
  }
2331
+ if (att.startsWith('$')) {
2332
+ // Unrecognized `$`-prefixed key: reserved for templating dialects
2333
+ // layered on Jamilih (and for future Jamilih use); base Jamilih
2334
+ // ignores it rather than attempting `setAttribute` (which would
2335
+ // throw on the invalid `$` name character).
2336
+ break;
2337
+ }
1518
2338
  attVal = checkPluginValue(elem, att, /** @type {string} */attVal, opts);
1519
2339
  elem.setAttribute(att, attVal);
1520
2340
  break;
@@ -1537,13 +2357,30 @@ const jml = function jml(...args) {
1537
2357
  let argStart = 0;
1538
2358
  if (_getType(args[0]) === 'object' && Object.keys(args[0]).some(key => possibleOptions.includes(key))) {
1539
2359
  opts = /** @type {JamilihOptions} */args[0];
2360
+ if (!internalOpts.has(opts)) {
2361
+ // `$mode` is reserved for a future SVG/XML mode and does nothing yet;
2362
+ // `$state` is set internally and would corrupt root detection.
2363
+ if (Object.hasOwn(opts, '$mode')) {
2364
+ throw new TypeError(`\`$mode\` is reserved for future use and not yet implemented; args: ${JSON.stringify(args)}`);
2365
+ }
2366
+ if (Object.hasOwn(opts, '$state')) {
2367
+ throw new TypeError(`\`$state\` is set internally by Jamilih and may not be supplied; args: ${JSON.stringify(args)}`);
2368
+ }
2369
+ // The options object may carry only `$`-prefixed keys (recognized
2370
+ // options or dialect magic Jamilih ignores); a plain key is a
2371
+ // misplaced attribute with no element to receive it.
2372
+ if (Object.keys(opts).some(k => !k.startsWith('$'))) {
2373
+ throw new TypeError(`Attributes may not be supplied before an element; args: ${JSON.stringify(args)}`);
2374
+ }
2375
+ internalOpts.add(opts);
2376
+ }
1540
2377
  if (opts.$state === undefined) {
1541
2378
  isRoot = true;
1542
2379
  opts.$state = 'root';
1543
2380
  }
1544
- if (Array.isArray(opts.$map)) {
1545
- opts.$map = {
1546
- root: opts.$map
2381
+ if (Array.isArray(opts.$Map)) {
2382
+ opts.$Map = {
2383
+ root: opts.$Map
1547
2384
  };
1548
2385
  }
1549
2386
  if ('$plugins' in opts) {
@@ -1567,9 +2404,35 @@ const jml = function jml(...args) {
1567
2404
  opts = {
1568
2405
  $state: undefined
1569
2406
  };
2407
+ internalOpts.add(opts);
2408
+ // A user-supplied plain object in first-argument position that is *not*
2409
+ // an options object (handled above). It may only be a node-producing
2410
+ // first-arg object (`#`, `$text`, `$document`, `$DOCTYPE`, `$attribute`)
2411
+ // or a purely `$`-prefixed object (dialect magic Jamilih skips); a
2412
+ // genuine attribute there has no element to attach to.
2413
+ const leading = /** @type {Record<string, unknown>} */args[0];
2414
+ if (_getType(args[0]) === 'object' && !internalOpts.has(leading)) {
2415
+ if (Object.hasOwn(leading, '$mode')) {
2416
+ throw new TypeError(`\`$mode\` is reserved for future use and not yet implemented; args: ${JSON.stringify(args)}`);
2417
+ }
2418
+ if (Object.hasOwn(leading, '$state')) {
2419
+ throw new TypeError(`\`$state\` is set internally by Jamilih and may not be supplied; args: ${JSON.stringify(args)}`);
2420
+ }
2421
+ const leadingKeys = Object.keys(leading);
2422
+ const firstArgNodeObject = leadingKeys.some(k => {
2423
+ return ['#', '$text', '$document', '$DOCTYPE', '$attribute'].includes(k);
2424
+ });
2425
+ if (!firstArgNodeObject) {
2426
+ if (leadingKeys.every(k => k.startsWith('$'))) {
2427
+ argStart = 1;
2428
+ } else {
2429
+ throw new TypeError(`Attributes may not be supplied before an element; args: ${JSON.stringify(args)}`);
2430
+ }
2431
+ }
2432
+ }
1570
2433
  }
1571
2434
  const argc = args.length;
1572
- const defaultMap = opts.$map && /** @type {MapWithRoot} */opts.$map.root;
2435
+ const defaultMap = opts.$Map && /** @type {MapWithRoot} */opts.$Map.root;
1573
2436
 
1574
2437
  /**
1575
2438
  * @param {true|string[]|Map<HTMLElement, UserArg>|WeakMap<HTMLElement, UserArg>|DataAttributeObject|[Map<HTMLElement, UserArg>|WeakMap<HTMLElement, UserArg>|undefined, DataAttributeObject|UserArg]} dataVal
@@ -1585,7 +2448,7 @@ const jml = function jml(...args) {
1585
2448
  // Array of strings mapping to default
1586
2449
  if (typeof dataVal[0] === 'string') {
1587
2450
  dataVal.forEach(dVal => {
1588
- setMap(/** @type {MapWithRoot} */opts.$map[dVal]);
2451
+ setMap(/** @type {MapWithRoot} */opts.$Map[dVal]);
1589
2452
  });
1590
2453
  return;
1591
2454
  // Array of Map and non-map data object
@@ -1820,6 +2683,15 @@ const jml = function jml(...args) {
1820
2683
  if (!('nodeType' in childContent)) {
1821
2684
  newChildContent = /** @type {string} */
1822
2685
  checkPluginValue(elem, null, childContent, opts, 'children');
2686
+ const pluginClaimed = newChildContent !== undefined && /** @type {unknown} */newChildContent !== childContent;
2687
+ const childKeys = Object.keys(childContent);
2688
+ if (
2689
+ // No plugin claimed the object, and every key is `$`-prefixed
2690
+ // magic Jamilih does not know: treat as a no-op
2691
+ // (templating-dialect placeholder).
2692
+ !pluginClaimed && childKeys.length > 0 && childKeys.every(k => k.startsWith('$'))) {
2693
+ break;
2694
+ }
1823
2695
  }
1824
2696
  _appendNode(elem, /** @type {string|HTMLElement|DocumentFragment|Comment} */
1825
2697
  newChildContent || childContent);
@@ -1834,7 +2706,7 @@ const jml = function jml(...args) {
1834
2706
  }
1835
2707
  }
1836
2708
  const ret = nodes[0] || elem;
1837
- if (isRoot && opts.$map && /** @type {MapWithRoot} */opts.$map.root) {
2709
+ if (isRoot && opts.$Map && /** @type {MapWithRoot} */opts.$Map.root) {
1838
2710
  setMap(true);
1839
2711
  }
1840
2712
 
@@ -2509,7 +3381,7 @@ jml.WeakMap = JamilihWeakMap;
2509
3381
  jml.weak = function (obj, ...args) {
2510
3382
  const map = new JamilihWeakMap();
2511
3383
  const elem = jml({
2512
- $map: [map, obj]
3384
+ $Map: [map, obj]
2513
3385
  }, ...args);
2514
3386
  return [map, (/** @type {HTMLElement} */elem)];
2515
3387
  };
@@ -2523,7 +3395,7 @@ jml.weak = function (obj, ...args) {
2523
3395
  jml.strong = function (obj, ...args) {
2524
3396
  const map = new JamilihMap();
2525
3397
  const elem = jml({
2526
- $map: [map, obj]
3398
+ $Map: [map, obj]
2527
3399
  }, ...args);
2528
3400
  return [map, (/** @type {HTMLElement} */elem)];
2529
3401
  };
@@ -2602,6 +3474,7 @@ const getWindow = () => {
2602
3474
  return win;
2603
3475
  };
2604
3476
  jml.getWindow = getWindow;
3477
+ jml.validateJamilih = validateJamilih;
2605
3478
 
2606
3479
  /**
2607
3480
  * Does not run Jamilih so can be further processed.
@@ -2629,4 +3502,4 @@ if (doc && doc.body) {
2629
3502
  }
2630
3503
  const nbsp = '\u{A0}'; // Very commonly needed in templates
2631
3504
 
2632
- export { $, $$, DOMException, body, getWindow, glue, jml, nbsp, setWindow, toDOM, toDOMString, toHTML, toJML, toJMLString, toXML, toXMLDOMString };
3505
+ export { $, $$, DOMException, body, getWindow, glue, isValidJamilih, jml, nbsp, setWindow, toDOM, toDOMString, toHTML, toJML, toJMLString, toXML, toXMLDOMString, validateJamilih };