@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.
@@ -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;