@wildwinter/toolkit 0.2.4 → 0.2.6
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 +22 -0
- package/conformance/extensions/defaults.json +1 -0
- package/conformance/extensions/manifest.json +546 -0
- package/conformance/extensions/values.json +527 -1
- package/dist/extensions.cjs +231 -3
- package/dist/extensions.cjs.map +1 -1
- package/dist/extensions.d.cts +76 -3
- package/dist/extensions.d.ts +76 -3
- package/dist/extensions.js +226 -4
- package/dist/extensions.js.map +1 -1
- package/extensions.md +50 -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,11 +366,13 @@ 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);
|
|
295
|
-
var MANIFEST_KEYS = /* @__PURE__ */ new Set(["schema", "id", "name", "version", "docs", "settings", "targets"]);
|
|
373
|
+
var MANIFEST_KEYS = /* @__PURE__ */ new Set(["schema", "id", "name", "version", "docs", "settings", "targets", "views", "reads"]);
|
|
374
|
+
var VIEW_KEYS = /* @__PURE__ */ new Set(["id", "name", "entry", "element", "hosts"]);
|
|
375
|
+
var ELEMENT = /^[a-z][a-z0-9._]*-[a-z0-9._-]*$/;
|
|
296
376
|
var err2 = (code, path, message) => ({ severity: "error", code, path, message });
|
|
297
377
|
function parseManifest(text) {
|
|
298
378
|
return parseSource(text);
|
|
@@ -314,6 +394,8 @@ function manifestProblems(manifest) {
|
|
|
314
394
|
}
|
|
315
395
|
if (m["docs"] !== void 0 && typeof m["docs"] !== "string") out.push(err2("docs", "docs", "docs is a link, as text"));
|
|
316
396
|
const settings = settingsProblems(m["settings"], out);
|
|
397
|
+
viewsProblems(m["views"], m["targets"], out);
|
|
398
|
+
readsProblems(m["reads"], m["id"], out);
|
|
317
399
|
const targets = m["targets"];
|
|
318
400
|
if (!isObject(targets) || Object.keys(targets).length === 0) {
|
|
319
401
|
out.push(err2("no-targets", "targets", "the manifest targets no host"));
|
|
@@ -338,9 +420,146 @@ function manifestProblems(manifest) {
|
|
|
338
420
|
}
|
|
339
421
|
targetProblems(host, kind, target, kindPath, settings, out);
|
|
340
422
|
}
|
|
423
|
+
pickListProblems(kinds, hostPath, out);
|
|
341
424
|
}
|
|
342
425
|
return out;
|
|
343
426
|
}
|
|
427
|
+
function pickSourcesSound(host, from, path, out) {
|
|
428
|
+
if (!Array.isArray(from) || from.length === 0) {
|
|
429
|
+
out.push(err2("pick-from", path, 'a pick lists its sources in "from": { "words": [...] }, { "to": a node kind }, or { "list": a project list }'));
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
let sound = true;
|
|
433
|
+
from.forEach((s, i) => {
|
|
434
|
+
const at = atIndex(path, i);
|
|
435
|
+
const keys = isObject(s) ? Object.keys(s).filter((k) => k === "words" || k === "to" || k === "list") : [];
|
|
436
|
+
if (!isObject(s) || keys.length !== 1) {
|
|
437
|
+
out.push(err2("pick-source", at, 'a source is one of { "words": [...] }, { "to": a node kind }, { "list": a project list }'));
|
|
438
|
+
sound = false;
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
unknownKeys(s, new Set(keys), at, out);
|
|
442
|
+
if (keys[0] === "words") {
|
|
443
|
+
const words = s["words"];
|
|
444
|
+
const seen = /* @__PURE__ */ new Set();
|
|
445
|
+
if (!Array.isArray(words) || words.length === 0) {
|
|
446
|
+
out.push(err2("pick-words", atKey(at, "words"), "fixed words are a list of at least one"));
|
|
447
|
+
sound = false;
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
words.forEach((w, j) => {
|
|
451
|
+
if (typeof w !== "string" || w.trim() === "") {
|
|
452
|
+
out.push(err2("value-empty", atIndex(atKey(at, "words"), j), "a word has a name"));
|
|
453
|
+
sound = false;
|
|
454
|
+
} else if (seen.has(w)) {
|
|
455
|
+
out.push(err2("value-duplicate", atIndex(atKey(at, "words"), j), `"${w}" is listed twice`));
|
|
456
|
+
sound = false;
|
|
457
|
+
} else seen.add(w);
|
|
458
|
+
});
|
|
459
|
+
} else if (keys[0] === "to") {
|
|
460
|
+
if (typeof s["to"] !== "string" || !HOSTS[host].references.includes(s["to"])) {
|
|
461
|
+
out.push(err2("reference-to", atKey(at, "to"), `a source's nodes are of a kind, one of ${HOSTS[host].references.join(", ")}`));
|
|
462
|
+
sound = false;
|
|
463
|
+
}
|
|
464
|
+
} else if (typeof s["list"] !== "string" || !NAME.test(s["list"])) {
|
|
465
|
+
out.push(err2("pick-list", atKey(at, "list"), "a list source names a field of the extension's project node"));
|
|
466
|
+
sound = false;
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
return sound;
|
|
470
|
+
}
|
|
471
|
+
function pickListProblems(kinds, hostPath, out) {
|
|
472
|
+
const project = isObject(kinds["project"]) && Array.isArray(kinds["project"]["fields"]) ? kinds["project"]["fields"] : [];
|
|
473
|
+
const lists = new Set(project.filter((f) => isObject(f) && isTextList(f)).map((f) => f.name));
|
|
474
|
+
const walk = (decl, path) => {
|
|
475
|
+
if (!isObject(decl)) return;
|
|
476
|
+
const d = decl;
|
|
477
|
+
if (d.type === "pick" && Array.isArray(d.from)) {
|
|
478
|
+
d.from.forEach((s, i) => {
|
|
479
|
+
if (isObject(s) && typeof s.list === "string" && !lists.has(s.list)) {
|
|
480
|
+
const name = s.list;
|
|
481
|
+
out.push(err2(
|
|
482
|
+
"pick-list",
|
|
483
|
+
atKey(atIndex(atKey(path, "from"), i), "list"),
|
|
484
|
+
`"${name}" is not a list of text on the extension's project node; declare it there, as { "name": "${name}", "type": "list", "of": { "type": "text" } }`
|
|
485
|
+
));
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
if (Array.isArray(d.fields)) d.fields.forEach((f, i) => walk(f, atIndex(atKey(path, "fields"), i)));
|
|
490
|
+
if (d.of !== void 0) walk(d.of, atKey(path, "of"));
|
|
491
|
+
};
|
|
492
|
+
for (const [kind, target] of Object.entries(kinds)) {
|
|
493
|
+
if (!isObject(target) || !Array.isArray(target["fields"])) continue;
|
|
494
|
+
target["fields"].forEach((f, i) => walk(f, atIndex(atKey(atKey(hostPath, kind), "fields"), i)));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function isViewEntry(entry) {
|
|
498
|
+
if (typeof entry !== "string" || !entry.endsWith(".js") || entry.includes("\\")) return false;
|
|
499
|
+
const parts = entry.split("/");
|
|
500
|
+
return parts.every((p) => p !== "" && p !== "." && p !== "..");
|
|
501
|
+
}
|
|
502
|
+
function viewsProblems(views, targets, out) {
|
|
503
|
+
if (views === void 0) return;
|
|
504
|
+
if (!Array.isArray(views)) {
|
|
505
|
+
out.push(err2("not-a-list", "views", "views are a list"));
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const ids = /* @__PURE__ */ new Set();
|
|
509
|
+
const elements = /* @__PURE__ */ new Set();
|
|
510
|
+
const targeted = isObject(targets) ? Object.keys(targets) : [];
|
|
511
|
+
views.forEach((v, i) => {
|
|
512
|
+
const at = atIndex("views", i);
|
|
513
|
+
if (!isObject(v)) {
|
|
514
|
+
out.push(err2("not-an-object", at, "a view is an object"));
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
unknownKeys(v, VIEW_KEYS, at, out);
|
|
518
|
+
const id = v["id"];
|
|
519
|
+
if (typeof id !== "string" || !ID.test(id)) out.push(err2("view-id", atKey(at, "id"), "a view's id is lower case letters, digits and hyphens, starting with a letter"));
|
|
520
|
+
else if (ids.has(id)) out.push(err2("view-duplicate", atKey(at, "id"), `two views are called "${id}"`));
|
|
521
|
+
else ids.add(id);
|
|
522
|
+
if (typeof v["name"] !== "string" || v["name"].trim() === "") out.push(err2("view-name", atKey(at, "name"), "a view has a name, shown as its window's title"));
|
|
523
|
+
if (!isViewEntry(v["entry"])) {
|
|
524
|
+
out.push(err2("view-entry", atKey(at, "entry"), "a view's entry is a .js file in the extension's package, as a relative path such as view/sheet.js"));
|
|
525
|
+
}
|
|
526
|
+
const element = v["element"];
|
|
527
|
+
if (typeof element !== "string" || !ELEMENT.test(element)) {
|
|
528
|
+
out.push(err2("view-element", atKey(at, "element"), "a view's element is a custom element name: lower case, starting with a letter, with a hyphen, such as probe-view"));
|
|
529
|
+
} else if (elements.has(element)) out.push(err2("view-duplicate", atKey(at, "element"), `two views define <${element}>`));
|
|
530
|
+
else elements.add(element);
|
|
531
|
+
const hosts = v["hosts"];
|
|
532
|
+
if (hosts === void 0) return;
|
|
533
|
+
if (!Array.isArray(hosts) || hosts.length === 0) {
|
|
534
|
+
out.push(err2("view-hosts", atKey(at, "hosts"), "a view's hosts are a list of at least one host"));
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
hosts.forEach((h, j) => {
|
|
538
|
+
const hp = atIndex(atKey(at, "hosts"), j);
|
|
539
|
+
if (typeof h !== "string" || !own(HOSTS, h)) out.push(err2("unknown-host", hp, `${JSON.stringify(h)} is not a host; a view opens in "patter" or "storylets"`));
|
|
540
|
+
else if (!targeted.includes(h)) out.push({ severity: "warning", code: "view-host-untargeted", path: hp, message: `the view opens in ${h}, but the manifest targets nothing there` });
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
function readsProblems(reads, self, out) {
|
|
545
|
+
if (reads === void 0) return;
|
|
546
|
+
if (!Array.isArray(reads)) {
|
|
547
|
+
out.push(err2("not-a-list", "reads", "reads is a list of extension ids"));
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const seen = /* @__PURE__ */ new Set();
|
|
551
|
+
reads.forEach((r, i) => {
|
|
552
|
+
const at = atIndex("reads", i);
|
|
553
|
+
if (typeof r !== "string" || !ID.test(r)) out.push(err2("reads-id", at, "an extension read is named by its id: lower case letters, digits and hyphens"));
|
|
554
|
+
else if (r === self) out.push({ severity: "warning", code: "reads-self", path: at, message: "an extension always reads its own data; this entry is ignored" });
|
|
555
|
+
else if (seen.has(r)) out.push(err2("reads-duplicate", at, `"${r}" is listed twice`));
|
|
556
|
+
else seen.add(r);
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
function viewsFor(manifest, host) {
|
|
560
|
+
const targeted = Object.keys(manifest.targets ?? {});
|
|
561
|
+
return (Array.isArray(manifest.views) ? manifest.views : []).filter((v) => isObject(v) && typeof v.id === "string" && ID.test(v.id) && typeof v.name === "string" && isViewEntry(v.entry) && typeof v.element === "string" && ELEMENT.test(v.element) && (v.hosts === void 0 ? targeted.includes(host) : Array.isArray(v.hosts) && v.hosts.includes(host)));
|
|
562
|
+
}
|
|
344
563
|
function unknownKeys(obj, known, path, out) {
|
|
345
564
|
for (const k of Object.keys(obj)) {
|
|
346
565
|
if (!known.has(k)) {
|
|
@@ -501,6 +720,9 @@ function typeProblems(host, decl, path, named, out, claim = false) {
|
|
|
501
720
|
sound = false;
|
|
502
721
|
}
|
|
503
722
|
break;
|
|
723
|
+
case "pick":
|
|
724
|
+
if (!pickSourcesSound(host, decl.from, at("from"), out)) sound = false;
|
|
725
|
+
break;
|
|
504
726
|
case "record":
|
|
505
727
|
if (!Array.isArray(decl.fields) || decl.fields.length === 0) {
|
|
506
728
|
out.push(err2("record-empty", at("fields"), "a record declares its fields"));
|
|
@@ -993,6 +1215,8 @@ exports.defaultOf = defaultOf;
|
|
|
993
1215
|
exports.effectiveClaim = effectiveClaim;
|
|
994
1216
|
exports.entryProblems = entryProblems;
|
|
995
1217
|
exports.extensionDataStrategy = extensionDataStrategy;
|
|
1218
|
+
exports.isTextList = isTextList;
|
|
1219
|
+
exports.isViewEntry = isViewEntry;
|
|
996
1220
|
exports.keyedBy = keyedBy;
|
|
997
1221
|
exports.manifestProblems = manifestProblems;
|
|
998
1222
|
exports.mapOf = mapOf;
|
|
@@ -1003,6 +1227,9 @@ exports.mergeTree = mergeTree;
|
|
|
1003
1227
|
exports.objectOf = objectOf;
|
|
1004
1228
|
exports.orphanedEntries = orphanedEntries;
|
|
1005
1229
|
exports.parseManifest = parseManifest;
|
|
1230
|
+
exports.pickChoices = pickChoices;
|
|
1231
|
+
exports.pickClashes = pickClashes;
|
|
1232
|
+
exports.pickLists = pickLists;
|
|
1006
1233
|
exports.renameStages = renameStages;
|
|
1007
1234
|
exports.resolvedClaims = resolvedClaims;
|
|
1008
1235
|
exports.settingValues = settingValues;
|
|
@@ -1013,6 +1240,7 @@ exports.targetOf = targetOf;
|
|
|
1013
1240
|
exports.unresolvedOf = unresolvedOf;
|
|
1014
1241
|
exports.unresolvedProblems = unresolvedProblems;
|
|
1015
1242
|
exports.valueProblems = valueProblems;
|
|
1243
|
+
exports.viewsFor = viewsFor;
|
|
1016
1244
|
exports.withoutMark = withoutMark;
|
|
1017
1245
|
//# sourceMappingURL=extensions.cjs.map
|
|
1018
1246
|
//# sourceMappingURL=extensions.cjs.map
|