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