@wildwinter/toolkit 0.2.3 → 0.2.5
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/CHANGELOG.md +21 -0
- package/README.md +14 -0
- package/conformance/extensions/defaults.json +1 -0
- package/conformance/extensions/manifest.json +314 -0
- package/conformance/extensions/values.json +527 -1
- package/dist/extensions.cjs +158 -2
- package/dist/extensions.cjs.map +1 -1
- package/dist/extensions.d.cts +54 -3
- package/dist/extensions.d.ts +54 -3
- package/dist/extensions.js +155 -3
- package/dist/extensions.js.map +1 -1
- package/dist/index.cjs +39 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +36 -1
- package/dist/index.js.map +1 -1
- package/extensions.md +28 -1
- package/package.json +1 -1
package/dist/extensions.cjs
CHANGED
|
@@ -24,7 +24,7 @@ var HOSTS = {
|
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
26
|
var SCALAR_TYPES = ["text", "multiline", "number", "boolean", "enum", "flags", "quality"];
|
|
27
|
-
var VALUE_TYPES = [...SCALAR_TYPES, "reference", "record", "map", "list"];
|
|
27
|
+
var VALUE_TYPES = [...SCALAR_TYPES, "reference", "pick", "record", "map", "list"];
|
|
28
28
|
function parseSource(text) {
|
|
29
29
|
const body = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
30
30
|
return JSON5__default.default.parse(body);
|
|
@@ -112,6 +112,26 @@ function valueProblems(decl, value, ctx = {}, path = "") {
|
|
|
112
112
|
out.push(err("no-such-node", path, `${where(path)} names ${decl.to} "${value}", which does not exist`));
|
|
113
113
|
}
|
|
114
114
|
break;
|
|
115
|
+
case "pick": {
|
|
116
|
+
if (typeof value !== "string") {
|
|
117
|
+
wrong("one of its choices, as text");
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
if (value === "") break;
|
|
121
|
+
const sources = decl.from ?? [];
|
|
122
|
+
const holding = sources.filter((s) => sourceHolds(s, value, ctx) === true);
|
|
123
|
+
if (holding.length > 1) {
|
|
124
|
+
out.push({
|
|
125
|
+
severity: "warning",
|
|
126
|
+
code: "pick-ambiguous",
|
|
127
|
+
path,
|
|
128
|
+
message: `${where(path)} is "${value}", which is ${holding.map(sourceWord).join(" and ")}`
|
|
129
|
+
});
|
|
130
|
+
} else if (holding.length === 0 && sources.every((s) => sourceHolds(s, value, ctx) !== void 0)) {
|
|
131
|
+
out.push(err("not-a-choice", path, `${where(path)} is "${value}", which is not one of its choices`));
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
115
135
|
case "record":
|
|
116
136
|
if (!isObject(value)) {
|
|
117
137
|
wrong("an object");
|
|
@@ -191,11 +211,69 @@ function entryProblems(target, entry, ctx = {}, path = "") {
|
|
|
191
211
|
}
|
|
192
212
|
return fieldsProblems(target.fields ?? [], entry, ctx, path);
|
|
193
213
|
}
|
|
214
|
+
function sourceHolds(source, value, ctx) {
|
|
215
|
+
if ("words" in source) return Array.isArray(source.words) && source.words.includes(value);
|
|
216
|
+
if ("to" in source) return ctx.exists === void 0 ? void 0 : ctx.exists(source.to, value);
|
|
217
|
+
if ("list" in source) return ctx.list === void 0 ? void 0 : (ctx.list(source.list) ?? []).includes(value);
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
function sourceWord(source) {
|
|
221
|
+
if ("words" in source) return "a fixed word";
|
|
222
|
+
if ("to" in source) return source.to === "cast" ? "a cast member" : `a ${source.to}`;
|
|
223
|
+
return `an entry of the list "${source.list}"`;
|
|
224
|
+
}
|
|
225
|
+
function pickChoices(decl, src) {
|
|
226
|
+
const out = [];
|
|
227
|
+
for (const source of decl.from ?? []) {
|
|
228
|
+
if ("words" in source) {
|
|
229
|
+
for (const w of source.words ?? []) if (typeof w === "string" && w !== "") out.push({ value: w, label: w, source });
|
|
230
|
+
} else if ("to" in source) {
|
|
231
|
+
for (const n of src.nodes?.(source.to) ?? []) if (n.id !== "") out.push({ value: n.id, label: n.label ?? n.id, source });
|
|
232
|
+
} else if ("list" in source) {
|
|
233
|
+
for (const e of src.list?.(source.list) ?? []) if (e !== "") out.push({ value: e, label: e, source });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
function pickClashes(decl, src, path = "") {
|
|
239
|
+
const by = /* @__PURE__ */ new Map();
|
|
240
|
+
for (const c of pickChoices(decl, src)) {
|
|
241
|
+
const held = by.get(c.value) ?? [];
|
|
242
|
+
if (!held.includes(c.source)) held.push(c.source);
|
|
243
|
+
by.set(c.value, held);
|
|
244
|
+
}
|
|
245
|
+
const out = [];
|
|
246
|
+
for (const [name, sources] of by) {
|
|
247
|
+
if (sources.length < 2) continue;
|
|
248
|
+
out.push({
|
|
249
|
+
severity: "warning",
|
|
250
|
+
code: "pick-clash",
|
|
251
|
+
path,
|
|
252
|
+
options: [name],
|
|
253
|
+
message: `"${name}" is ${sources.map(sourceWord).join(" and ")}, so a value naming it is ambiguous`
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
function pickLists(projectTarget, projectEntry) {
|
|
259
|
+
return (name) => {
|
|
260
|
+
const field = (projectTarget?.fields ?? []).find((f) => f.name === name);
|
|
261
|
+
if (field === void 0 || !isTextList(field)) return void 0;
|
|
262
|
+
const held = projectEntry !== void 0 && own(projectEntry, name) ? projectEntry[name] : void 0;
|
|
263
|
+
const value = Array.isArray(held) ? held : defaultOf(field);
|
|
264
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function isTextList(decl) {
|
|
268
|
+
return decl.type === "list" && decl.id === void 0 && decl.of?.type === "text";
|
|
269
|
+
}
|
|
194
270
|
function defaultOf(decl) {
|
|
195
271
|
if (decl.default !== void 0) return decl.default;
|
|
196
272
|
switch (decl.type) {
|
|
197
273
|
case "reference":
|
|
198
274
|
return "";
|
|
275
|
+
case "pick":
|
|
276
|
+
return "";
|
|
199
277
|
case "record":
|
|
200
278
|
return Object.fromEntries((decl.fields ?? []).map((f) => [f.name, defaultOf(f)]));
|
|
201
279
|
case "map":
|
|
@@ -288,7 +366,7 @@ function renameStages(target, entry) {
|
|
|
288
366
|
var ID = /^[a-z][a-z0-9-]*$/;
|
|
289
367
|
var VERSION = /^\d+\.\d+\.\d+$/;
|
|
290
368
|
var NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
291
|
-
var TYPE_KEYS = ["type", "values", "stages", "renamedStages", "to", "fields", "key", "of", "id", "min", "max", "valueHelp", "default"];
|
|
369
|
+
var TYPE_KEYS = ["type", "values", "stages", "renamedStages", "to", "from", "fields", "key", "of", "id", "min", "max", "valueHelp", "default"];
|
|
292
370
|
var FIELD_KEYS = /* @__PURE__ */ new Set([...TYPE_KEYS, "name", "label", "help", "required", "group", "editorOnly"]);
|
|
293
371
|
var CLAIM_KEYS = /* @__PURE__ */ new Set([...TYPE_KEYS, "setting", "label", "help", "required"]);
|
|
294
372
|
var BARE_KEYS = new Set(TYPE_KEYS);
|
|
@@ -338,9 +416,80 @@ function manifestProblems(manifest) {
|
|
|
338
416
|
}
|
|
339
417
|
targetProblems(host, kind, target, kindPath, settings, out);
|
|
340
418
|
}
|
|
419
|
+
pickListProblems(kinds, hostPath, out);
|
|
341
420
|
}
|
|
342
421
|
return out;
|
|
343
422
|
}
|
|
423
|
+
function pickSourcesSound(host, from, path, out) {
|
|
424
|
+
if (!Array.isArray(from) || from.length === 0) {
|
|
425
|
+
out.push(err2("pick-from", path, 'a pick lists its sources in "from": { "words": [...] }, { "to": a node kind }, or { "list": a project list }'));
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
let sound = true;
|
|
429
|
+
from.forEach((s, i) => {
|
|
430
|
+
const at = atIndex(path, i);
|
|
431
|
+
const keys = isObject(s) ? Object.keys(s).filter((k) => k === "words" || k === "to" || k === "list") : [];
|
|
432
|
+
if (!isObject(s) || keys.length !== 1) {
|
|
433
|
+
out.push(err2("pick-source", at, 'a source is one of { "words": [...] }, { "to": a node kind }, { "list": a project list }'));
|
|
434
|
+
sound = false;
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
unknownKeys(s, new Set(keys), at, out);
|
|
438
|
+
if (keys[0] === "words") {
|
|
439
|
+
const words = s["words"];
|
|
440
|
+
const seen = /* @__PURE__ */ new Set();
|
|
441
|
+
if (!Array.isArray(words) || words.length === 0) {
|
|
442
|
+
out.push(err2("pick-words", atKey(at, "words"), "fixed words are a list of at least one"));
|
|
443
|
+
sound = false;
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
words.forEach((w, j) => {
|
|
447
|
+
if (typeof w !== "string" || w.trim() === "") {
|
|
448
|
+
out.push(err2("value-empty", atIndex(atKey(at, "words"), j), "a word has a name"));
|
|
449
|
+
sound = false;
|
|
450
|
+
} else if (seen.has(w)) {
|
|
451
|
+
out.push(err2("value-duplicate", atIndex(atKey(at, "words"), j), `"${w}" is listed twice`));
|
|
452
|
+
sound = false;
|
|
453
|
+
} else seen.add(w);
|
|
454
|
+
});
|
|
455
|
+
} else if (keys[0] === "to") {
|
|
456
|
+
if (typeof s["to"] !== "string" || !HOSTS[host].references.includes(s["to"])) {
|
|
457
|
+
out.push(err2("reference-to", atKey(at, "to"), `a source's nodes are of a kind, one of ${HOSTS[host].references.join(", ")}`));
|
|
458
|
+
sound = false;
|
|
459
|
+
}
|
|
460
|
+
} else if (typeof s["list"] !== "string" || !NAME.test(s["list"])) {
|
|
461
|
+
out.push(err2("pick-list", atKey(at, "list"), "a list source names a field of the extension's project node"));
|
|
462
|
+
sound = false;
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
return sound;
|
|
466
|
+
}
|
|
467
|
+
function pickListProblems(kinds, hostPath, out) {
|
|
468
|
+
const project = isObject(kinds["project"]) && Array.isArray(kinds["project"]["fields"]) ? kinds["project"]["fields"] : [];
|
|
469
|
+
const lists = new Set(project.filter((f) => isObject(f) && isTextList(f)).map((f) => f.name));
|
|
470
|
+
const walk = (decl, path) => {
|
|
471
|
+
if (!isObject(decl)) return;
|
|
472
|
+
const d = decl;
|
|
473
|
+
if (d.type === "pick" && Array.isArray(d.from)) {
|
|
474
|
+
d.from.forEach((s, i) => {
|
|
475
|
+
if (isObject(s) && typeof s.list === "string" && !lists.has(s.list)) {
|
|
476
|
+
const name = s.list;
|
|
477
|
+
out.push(err2(
|
|
478
|
+
"pick-list",
|
|
479
|
+
atKey(atIndex(atKey(path, "from"), i), "list"),
|
|
480
|
+
`"${name}" is not a list of text on the extension's project node; declare it there, as { "name": "${name}", "type": "list", "of": { "type": "text" } }`
|
|
481
|
+
));
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
if (Array.isArray(d.fields)) d.fields.forEach((f, i) => walk(f, atIndex(atKey(path, "fields"), i)));
|
|
486
|
+
if (d.of !== void 0) walk(d.of, atKey(path, "of"));
|
|
487
|
+
};
|
|
488
|
+
for (const [kind, target] of Object.entries(kinds)) {
|
|
489
|
+
if (!isObject(target) || !Array.isArray(target["fields"])) continue;
|
|
490
|
+
target["fields"].forEach((f, i) => walk(f, atIndex(atKey(atKey(hostPath, kind), "fields"), i)));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
344
493
|
function unknownKeys(obj, known, path, out) {
|
|
345
494
|
for (const k of Object.keys(obj)) {
|
|
346
495
|
if (!known.has(k)) {
|
|
@@ -501,6 +650,9 @@ function typeProblems(host, decl, path, named, out, claim = false) {
|
|
|
501
650
|
sound = false;
|
|
502
651
|
}
|
|
503
652
|
break;
|
|
653
|
+
case "pick":
|
|
654
|
+
if (!pickSourcesSound(host, decl.from, at("from"), out)) sound = false;
|
|
655
|
+
break;
|
|
504
656
|
case "record":
|
|
505
657
|
if (!Array.isArray(decl.fields) || decl.fields.length === 0) {
|
|
506
658
|
out.push(err2("record-empty", at("fields"), "a record declares its fields"));
|
|
@@ -993,6 +1145,7 @@ exports.defaultOf = defaultOf;
|
|
|
993
1145
|
exports.effectiveClaim = effectiveClaim;
|
|
994
1146
|
exports.entryProblems = entryProblems;
|
|
995
1147
|
exports.extensionDataStrategy = extensionDataStrategy;
|
|
1148
|
+
exports.isTextList = isTextList;
|
|
996
1149
|
exports.keyedBy = keyedBy;
|
|
997
1150
|
exports.manifestProblems = manifestProblems;
|
|
998
1151
|
exports.mapOf = mapOf;
|
|
@@ -1003,6 +1156,9 @@ exports.mergeTree = mergeTree;
|
|
|
1003
1156
|
exports.objectOf = objectOf;
|
|
1004
1157
|
exports.orphanedEntries = orphanedEntries;
|
|
1005
1158
|
exports.parseManifest = parseManifest;
|
|
1159
|
+
exports.pickChoices = pickChoices;
|
|
1160
|
+
exports.pickClashes = pickClashes;
|
|
1161
|
+
exports.pickLists = pickLists;
|
|
1006
1162
|
exports.renameStages = renameStages;
|
|
1007
1163
|
exports.resolvedClaims = resolvedClaims;
|
|
1008
1164
|
exports.settingValues = settingValues;
|