@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.
- package/CHANGES.md +4 -0
- package/dist/formats/schema.d.ts.map +1 -1
- package/dist/fundamentalTypes/arrayType.d.ts.map +1 -1
- package/dist/fundamentalTypes/stringType.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/typeChoices.d.ts.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +22 -9
- package/pnpm-workspace.yaml +31 -30
- package/src/formats/schema.js +7 -14
- package/src/fundamentalTypes/arrayType.js +14 -22
- package/src/fundamentalTypes/blobType.js +16 -16
- package/src/fundamentalTypes/fileType.js +13 -13
- package/src/fundamentalTypes/regexpType.js +1 -1
- package/src/fundamentalTypes/stringType.js +5 -0
- package/src/subTypes/blobHTMLType.js +1 -1
- package/src/superTypes/dommatrixType.js +1 -1
- package/src/superTypes/errorsSpecialType.js +1 -1
- package/src/typeChoices.js +1 -3
- package/src/types.js +0 -6
- package/src/utils/media.js +3 -3
- package/vendor/jamilih/dist/jml.mjs +898 -25
- package/vendor/zod/classic/checks.d.ts +1 -1
- package/vendor/zod/classic/checks.js +1 -1
- package/vendor/zod/classic/schemas.d.ts +2 -6
- package/vendor/zod/classic/schemas.js +9 -13
- package/vendor/zod/core/api.d.ts +8 -4
- package/vendor/zod/core/api.js +11 -23
- package/vendor/zod/core/checks.d.ts +16 -0
- package/vendor/zod/core/checks.js +31 -0
- package/vendor/zod/core/compile.js +20 -24
- package/vendor/zod/core/core.js +1 -1
- package/vendor/zod/core/json-schema-processors.d.ts +0 -1
- package/vendor/zod/core/json-schema-processors.js +8 -43
- package/vendor/zod/core/memoizer.js +2 -5
- package/vendor/zod/core/regexes.d.ts +1 -0
- package/vendor/zod/core/regexes.js +2 -0
- package/vendor/zod/core/schemas.d.ts +8 -23
- package/vendor/zod/core/schemas.js +39 -80
- package/vendor/zod/core/to-json-schema.d.ts +0 -3
- package/vendor/zod/core/to-json-schema.js +1 -1
- package/vendor/zod/core/util.js +0 -4
- package/vendor/zod/core/versions.js +1 -1
- package/vendor/zod/core/visit.js +0 -12
- package/vendor/zod/locales/ar.js +1 -0
- package/vendor/zod/locales/az.js +1 -0
- package/vendor/zod/locales/be.js +1 -0
- package/vendor/zod/locales/bg.js +1 -0
- package/vendor/zod/locales/bn.js +1 -0
- package/vendor/zod/locales/ca.js +1 -0
- package/vendor/zod/locales/ckb.js +1 -0
- package/vendor/zod/locales/cs.js +1 -0
- package/vendor/zod/locales/da.js +1 -0
- package/vendor/zod/locales/de.js +1 -0
- package/vendor/zod/locales/el.js +1 -0
- package/vendor/zod/locales/en.js +1 -0
- package/vendor/zod/locales/eo.js +1 -0
- package/vendor/zod/locales/es.js +1 -0
- package/vendor/zod/locales/fa.js +1 -0
- package/vendor/zod/locales/fi.js +1 -0
- package/vendor/zod/locales/fr-CA.js +1 -0
- package/vendor/zod/locales/fr.js +1 -0
- package/vendor/zod/locales/gu.js +1 -0
- package/vendor/zod/locales/he.js +1 -0
- package/vendor/zod/locales/hi.js +1 -0
- package/vendor/zod/locales/hr.js +1 -0
- package/vendor/zod/locales/hu.js +1 -0
- package/vendor/zod/locales/hy.js +1 -0
- package/vendor/zod/locales/id.js +1 -0
- package/vendor/zod/locales/is.js +1 -0
- package/vendor/zod/locales/it.js +1 -0
- package/vendor/zod/locales/ja.js +1 -0
- package/vendor/zod/locales/ka.js +1 -0
- package/vendor/zod/locales/km.js +1 -0
- package/vendor/zod/locales/kn.js +1 -0
- package/vendor/zod/locales/ko.js +1 -0
- package/vendor/zod/locales/lt.js +1 -0
- package/vendor/zod/locales/mk.js +1 -0
- package/vendor/zod/locales/ms.js +1 -0
- package/vendor/zod/locales/ne.js +1 -0
- package/vendor/zod/locales/nl.js +1 -0
- package/vendor/zod/locales/nn.js +1 -0
- package/vendor/zod/locales/no.js +1 -0
- package/vendor/zod/locales/ota.js +1 -0
- package/vendor/zod/locales/pl.js +1 -0
- package/vendor/zod/locales/ps.js +1 -0
- package/vendor/zod/locales/pt-BR.js +1 -0
- package/vendor/zod/locales/pt.js +1 -0
- package/vendor/zod/locales/ro.js +1 -0
- package/vendor/zod/locales/ru.js +1 -0
- package/vendor/zod/locales/sk.js +1 -0
- package/vendor/zod/locales/sl.js +1 -0
- package/vendor/zod/locales/sv.js +1 -0
- package/vendor/zod/locales/ta.js +1 -0
- package/vendor/zod/locales/tg.js +1 -0
- package/vendor/zod/locales/th.js +1 -0
- package/vendor/zod/locales/tk.js +1 -0
- package/vendor/zod/locales/tr.js +1 -0
- package/vendor/zod/locales/uk.js +1 -0
- package/vendor/zod/locales/ur.js +1 -0
- package/vendor/zod/locales/uz.js +1 -0
- package/vendor/zod/locales/vi.js +1 -0
- package/vendor/zod/locales/yo.js +1 -0
- package/vendor/zod/locales/zh-CN.js +1 -0
- package/vendor/zod/locales/zh-TW.js +1 -0
- package/vendor/zod/mini/checks.d.ts +1 -1
- package/vendor/zod/mini/checks.js +1 -1
- package/vendor/zod/mini/schemas.d.ts +2 -5
- package/vendor/zod/mini/schemas.js +9 -11
- package/vendor/zodexy/dist/esm/index.js +4 -33
- 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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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|
|
|
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|
|
|
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
|
-
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
* @
|
|
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.$
|
|
1545
|
-
opts.$
|
|
1546
|
-
root: opts.$
|
|
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.$
|
|
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.$
|
|
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.$
|
|
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
|
-
$
|
|
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
|
-
$
|
|
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 };
|