@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.
@@ -18,7 +18,7 @@ declare const HOSTS: Readonly<Record<HostName, HostSpec>>;
18
18
  declare const SCALAR_TYPES: readonly ["text", "multiline", "number", "boolean", "enum", "flags", "quality"];
19
19
  type ScalarType = typeof SCALAR_TYPES[number];
20
20
  /** Every type a value can take. */
21
- declare const VALUE_TYPES: readonly ["text", "multiline", "number", "boolean", "enum", "flags", "quality", "reference", "record", "map", "list"];
21
+ declare const VALUE_TYPES: readonly ["text", "multiline", "number", "boolean", "enum", "flags", "quality", "reference", "pick", "record", "map", "list"];
22
22
  type ValueType = typeof VALUE_TYPES[number];
23
23
  /** A JSON value, as extension data holds it. */
24
24
  type JsonValue = null | boolean | number | string | JsonValue[] | {
@@ -39,6 +39,8 @@ interface TypeDecl {
39
39
  readonly renamedStages?: Readonly<Record<string, string>>;
40
40
  /** A reference's node kind. */
41
41
  readonly to?: string;
42
+ /** A pick's sources, in the order its choices are shown. */
43
+ readonly from?: readonly PickSource[];
42
44
  /** A record's fields. */
43
45
  readonly fields?: readonly FieldDecl[];
44
46
  /** A map's key. */
@@ -58,6 +60,17 @@ interface TypeDecl {
58
60
  /** Optional in source: the type's own default when absent (`defaultOf`). */
59
61
  readonly default?: unknown;
60
62
  }
63
+ /**
64
+ * Where a pick's choices come from: fixed words, the nodes of a reference kind (by id, as a reference holds
65
+ * them), or a list of text on the extension's own project node, by that field's name.
66
+ */
67
+ type PickSource = {
68
+ readonly words: readonly string[];
69
+ } | {
70
+ readonly to: string;
71
+ } | {
72
+ readonly list: string;
73
+ };
61
74
  /** A named field: a target's, or a record's. */
62
75
  interface FieldDecl extends TypeDecl {
63
76
  readonly name: string;
@@ -98,6 +111,23 @@ interface Manifest {
98
111
  readonly docs?: string;
99
112
  readonly settings?: readonly SettingDecl[];
100
113
  readonly targets: Readonly<Partial<Record<HostName, Readonly<Record<string, TargetDecl>>>>>;
114
+ /** Views the extension's installed package supplies (tier 2), each a web component a host opens in a window. */
115
+ readonly views?: readonly ViewDecl[];
116
+ /** Other extensions whose data this one's views read, by id. */
117
+ readonly reads?: readonly string[];
118
+ }
119
+ /** A view: a web component in the extension's installed package, opened by a host in a floating window. */
120
+ interface ViewDecl {
121
+ /** Lower case, digits and hyphens, unique within the manifest. */
122
+ readonly id: string;
123
+ /** The window's title and the menu item. */
124
+ readonly name: string;
125
+ /** The component's script, a relative path within the package, such as `view/sheet.js`. */
126
+ readonly entry: string;
127
+ /** The custom element the script defines, such as `pattershot-sheet`. */
128
+ readonly element: string;
129
+ /** The hosts that offer it; every host the manifest targets when absent. */
130
+ readonly hosts?: readonly HostName[];
101
131
  }
102
132
  /** One thing wrong, with where it is. A host words it in its own report shape. */
103
133
  interface Problem {
@@ -132,6 +162,11 @@ interface UnresolvedEntry {
132
162
  declare function parseManifest(text: string): Manifest;
133
163
  /** Every problem with a manifest, by path. An empty list means it is sound. */
134
164
  declare function manifestProblems(manifest: unknown): Problem[];
165
+ /** A package path a view's entry may name: relative, forward slashes, no `.` or `..` segment, a `.js` file. */
166
+ declare function isViewEntry(entry: unknown): entry is string;
167
+ /** The views a host offers from a manifest: those naming the host, or every view when it names none and the
168
+ * manifest targets the host. A view that fails its checks is left out. */
169
+ declare function viewsFor(manifest: Manifest, host: HostName): ViewDecl[];
135
170
  /** The host field each of a manifest's settings names, the project's choice
136
171
  * first, else the setting's default. */
137
172
  declare function settingValues(manifest: Manifest, chosen?: Readonly<Record<string, unknown>>): Record<string, string>;
@@ -162,16 +197,54 @@ declare function effectiveClaim<C extends ClaimDecl>(claim: C, hostDecl: {
162
197
  readonly default?: unknown;
163
198
  }): C;
164
199
 
165
- /** What the host knows about its nodes, for references. */
200
+ /** What the host knows about its nodes, for references, and about the extension's project lists, for picks. */
166
201
  interface ValueContext {
167
202
  /** Does a node of `kind` with `id` exist? Absent, references are not checked
168
203
  * (a host checking a manifest's default, say, has no nodes to hand). */
169
204
  readonly exists?: (kind: string, id: string) => boolean;
205
+ /** The entries of a list of text on the extension's own project node, by field name (`pickLists`), for a
206
+ * pick's `list` source. Absent, list sources are not checked. */
207
+ readonly list?: (name: string) => readonly string[] | undefined;
170
208
  }
171
209
  /** Every problem with `value` as a value of `decl`, paths relative to the value. */
172
210
  declare function valueProblems(decl: TypeDecl, value: unknown, ctx?: ValueContext, path?: string): Problem[];
173
211
  /** Every problem with one node's entry, against its target. */
174
212
  declare function entryProblems(target: TargetDecl, entry: unknown, ctx?: ValueContext, path?: string): Problem[];
213
+ /** What a host offers a pick: the nodes of a reference kind, and the extension's project lists. */
214
+ interface PickSources {
215
+ /** The nodes of `kind`, by id, with the name the author knows each by (the id when absent). */
216
+ readonly nodes?: (kind: string) => readonly {
217
+ readonly id: string;
218
+ readonly label?: string;
219
+ }[];
220
+ /** As `ValueContext.list`. */
221
+ readonly list?: (name: string) => readonly string[] | undefined;
222
+ }
223
+ /** One choice of a pick: the text a value holds, the name it is shown by, and where it comes from. */
224
+ interface PickChoice {
225
+ readonly value: string;
226
+ readonly label: string;
227
+ readonly source: PickSource;
228
+ }
229
+ /**
230
+ * Every choice a pick offers, source by source in its declared order, each source's own in its order. A name
231
+ * in two sources appears under each (`pickClashes` reports it); an empty entry is left out, as empty means
232
+ * automatic.
233
+ */
234
+ declare function pickChoices(decl: TypeDecl, src: PickSources): PickChoice[];
235
+ /**
236
+ * A pick's names that more than one of its sources holds (a character and a point of interest both called
237
+ * Gate): a warning each, once, for the project; each value naming one has its own from `valueProblems`.
238
+ */
239
+ declare function pickClashes(decl: TypeDecl, src: PickSources, path?: string): Problem[];
240
+ /**
241
+ * The lists of text a pick's `list` sources name, read off the extension's project entry: each a list field of
242
+ * the extension's `project` target holding text, the entry's own value else the field's default. Undefined for a
243
+ * name that is no such field.
244
+ */
245
+ declare function pickLists(projectTarget: TargetDecl | undefined, projectEntry: Readonly<Record<string, unknown>> | undefined): (name: string) => readonly string[] | undefined;
246
+ /** A list without ids holding text: what a pick's `list` source names. */
247
+ declare function isTextList(decl: TypeDecl): boolean;
175
248
  /**
176
249
  * A declaration's default: the one it gives, else its type's. A scalar's is
177
250
  * the kernel's (`defaultFor`: false, 0, "", the first enum value, no flags,
@@ -345,4 +418,4 @@ declare const atKey: (base: string, key: string) => string;
345
418
  /** `base` and then list position `i`. */
346
419
  declare const atIndex: (base: string, i: number) => string;
347
420
 
348
- export { ATOMIC, COMMENTS, type ClaimDecl, type Conflict, type ConflictKind, type DataMergeResult, type ExtensionData, type FieldDecl, HOSTS, type HostName, type HostSpec, type JsonValue, MANIFEST_FILE_SUFFIX, MANIFEST_SCHEMA, type Manifest, type MergeWarning, type Problem, type ResolvedClaim, SCALAR_TYPES, SET, type ScalarType, type SettingDecl, type StageRename, type Strategy, type TargetDecl, type TreeMergeOptions, type TreeMergeResult, type TypeDecl, UNRESOLVED_KEY, type UnresolvedEntry, VALUE_TYPES, type ValueContext, type ValueType, atIndex, atKey, claimClashes, compileEntry, compiledTarget, defaultOf, effectiveClaim, entryProblems, extensionDataStrategy, keyedBy, manifestProblems, mapOf, markUnresolved, mergeComments, mergeExtensionData, mergeTree, objectOf, orphanedEntries, parseManifest, renameStages, resolvedClaims, settingValues, stableStringify, strategyForTarget, strategyForType, targetOf, unresolvedOf, unresolvedProblems, valueProblems, withoutMark };
421
+ export { ATOMIC, COMMENTS, type ClaimDecl, type Conflict, type ConflictKind, type DataMergeResult, type ExtensionData, type FieldDecl, HOSTS, type HostName, type HostSpec, type JsonValue, MANIFEST_FILE_SUFFIX, MANIFEST_SCHEMA, type Manifest, type MergeWarning, type PickChoice, type PickSource, type PickSources, type Problem, type ResolvedClaim, SCALAR_TYPES, SET, type ScalarType, type SettingDecl, type StageRename, type Strategy, type TargetDecl, type TreeMergeOptions, type TreeMergeResult, type TypeDecl, UNRESOLVED_KEY, type UnresolvedEntry, VALUE_TYPES, type ValueContext, type ValueType, type ViewDecl, atIndex, atKey, claimClashes, compileEntry, compiledTarget, defaultOf, effectiveClaim, entryProblems, extensionDataStrategy, isTextList, isViewEntry, keyedBy, manifestProblems, mapOf, markUnresolved, mergeComments, mergeExtensionData, mergeTree, objectOf, orphanedEntries, parseManifest, pickChoices, pickClashes, pickLists, renameStages, resolvedClaims, settingValues, stableStringify, strategyForTarget, strategyForType, targetOf, unresolvedOf, unresolvedProblems, valueProblems, viewsFor, withoutMark };
@@ -18,7 +18,7 @@ declare const HOSTS: Readonly<Record<HostName, HostSpec>>;
18
18
  declare const SCALAR_TYPES: readonly ["text", "multiline", "number", "boolean", "enum", "flags", "quality"];
19
19
  type ScalarType = typeof SCALAR_TYPES[number];
20
20
  /** Every type a value can take. */
21
- declare const VALUE_TYPES: readonly ["text", "multiline", "number", "boolean", "enum", "flags", "quality", "reference", "record", "map", "list"];
21
+ declare const VALUE_TYPES: readonly ["text", "multiline", "number", "boolean", "enum", "flags", "quality", "reference", "pick", "record", "map", "list"];
22
22
  type ValueType = typeof VALUE_TYPES[number];
23
23
  /** A JSON value, as extension data holds it. */
24
24
  type JsonValue = null | boolean | number | string | JsonValue[] | {
@@ -39,6 +39,8 @@ interface TypeDecl {
39
39
  readonly renamedStages?: Readonly<Record<string, string>>;
40
40
  /** A reference's node kind. */
41
41
  readonly to?: string;
42
+ /** A pick's sources, in the order its choices are shown. */
43
+ readonly from?: readonly PickSource[];
42
44
  /** A record's fields. */
43
45
  readonly fields?: readonly FieldDecl[];
44
46
  /** A map's key. */
@@ -58,6 +60,17 @@ interface TypeDecl {
58
60
  /** Optional in source: the type's own default when absent (`defaultOf`). */
59
61
  readonly default?: unknown;
60
62
  }
63
+ /**
64
+ * Where a pick's choices come from: fixed words, the nodes of a reference kind (by id, as a reference holds
65
+ * them), or a list of text on the extension's own project node, by that field's name.
66
+ */
67
+ type PickSource = {
68
+ readonly words: readonly string[];
69
+ } | {
70
+ readonly to: string;
71
+ } | {
72
+ readonly list: string;
73
+ };
61
74
  /** A named field: a target's, or a record's. */
62
75
  interface FieldDecl extends TypeDecl {
63
76
  readonly name: string;
@@ -98,6 +111,23 @@ interface Manifest {
98
111
  readonly docs?: string;
99
112
  readonly settings?: readonly SettingDecl[];
100
113
  readonly targets: Readonly<Partial<Record<HostName, Readonly<Record<string, TargetDecl>>>>>;
114
+ /** Views the extension's installed package supplies (tier 2), each a web component a host opens in a window. */
115
+ readonly views?: readonly ViewDecl[];
116
+ /** Other extensions whose data this one's views read, by id. */
117
+ readonly reads?: readonly string[];
118
+ }
119
+ /** A view: a web component in the extension's installed package, opened by a host in a floating window. */
120
+ interface ViewDecl {
121
+ /** Lower case, digits and hyphens, unique within the manifest. */
122
+ readonly id: string;
123
+ /** The window's title and the menu item. */
124
+ readonly name: string;
125
+ /** The component's script, a relative path within the package, such as `view/sheet.js`. */
126
+ readonly entry: string;
127
+ /** The custom element the script defines, such as `pattershot-sheet`. */
128
+ readonly element: string;
129
+ /** The hosts that offer it; every host the manifest targets when absent. */
130
+ readonly hosts?: readonly HostName[];
101
131
  }
102
132
  /** One thing wrong, with where it is. A host words it in its own report shape. */
103
133
  interface Problem {
@@ -132,6 +162,11 @@ interface UnresolvedEntry {
132
162
  declare function parseManifest(text: string): Manifest;
133
163
  /** Every problem with a manifest, by path. An empty list means it is sound. */
134
164
  declare function manifestProblems(manifest: unknown): Problem[];
165
+ /** A package path a view's entry may name: relative, forward slashes, no `.` or `..` segment, a `.js` file. */
166
+ declare function isViewEntry(entry: unknown): entry is string;
167
+ /** The views a host offers from a manifest: those naming the host, or every view when it names none and the
168
+ * manifest targets the host. A view that fails its checks is left out. */
169
+ declare function viewsFor(manifest: Manifest, host: HostName): ViewDecl[];
135
170
  /** The host field each of a manifest's settings names, the project's choice
136
171
  * first, else the setting's default. */
137
172
  declare function settingValues(manifest: Manifest, chosen?: Readonly<Record<string, unknown>>): Record<string, string>;
@@ -162,16 +197,54 @@ declare function effectiveClaim<C extends ClaimDecl>(claim: C, hostDecl: {
162
197
  readonly default?: unknown;
163
198
  }): C;
164
199
 
165
- /** What the host knows about its nodes, for references. */
200
+ /** What the host knows about its nodes, for references, and about the extension's project lists, for picks. */
166
201
  interface ValueContext {
167
202
  /** Does a node of `kind` with `id` exist? Absent, references are not checked
168
203
  * (a host checking a manifest's default, say, has no nodes to hand). */
169
204
  readonly exists?: (kind: string, id: string) => boolean;
205
+ /** The entries of a list of text on the extension's own project node, by field name (`pickLists`), for a
206
+ * pick's `list` source. Absent, list sources are not checked. */
207
+ readonly list?: (name: string) => readonly string[] | undefined;
170
208
  }
171
209
  /** Every problem with `value` as a value of `decl`, paths relative to the value. */
172
210
  declare function valueProblems(decl: TypeDecl, value: unknown, ctx?: ValueContext, path?: string): Problem[];
173
211
  /** Every problem with one node's entry, against its target. */
174
212
  declare function entryProblems(target: TargetDecl, entry: unknown, ctx?: ValueContext, path?: string): Problem[];
213
+ /** What a host offers a pick: the nodes of a reference kind, and the extension's project lists. */
214
+ interface PickSources {
215
+ /** The nodes of `kind`, by id, with the name the author knows each by (the id when absent). */
216
+ readonly nodes?: (kind: string) => readonly {
217
+ readonly id: string;
218
+ readonly label?: string;
219
+ }[];
220
+ /** As `ValueContext.list`. */
221
+ readonly list?: (name: string) => readonly string[] | undefined;
222
+ }
223
+ /** One choice of a pick: the text a value holds, the name it is shown by, and where it comes from. */
224
+ interface PickChoice {
225
+ readonly value: string;
226
+ readonly label: string;
227
+ readonly source: PickSource;
228
+ }
229
+ /**
230
+ * Every choice a pick offers, source by source in its declared order, each source's own in its order. A name
231
+ * in two sources appears under each (`pickClashes` reports it); an empty entry is left out, as empty means
232
+ * automatic.
233
+ */
234
+ declare function pickChoices(decl: TypeDecl, src: PickSources): PickChoice[];
235
+ /**
236
+ * A pick's names that more than one of its sources holds (a character and a point of interest both called
237
+ * Gate): a warning each, once, for the project; each value naming one has its own from `valueProblems`.
238
+ */
239
+ declare function pickClashes(decl: TypeDecl, src: PickSources, path?: string): Problem[];
240
+ /**
241
+ * The lists of text a pick's `list` sources name, read off the extension's project entry: each a list field of
242
+ * the extension's `project` target holding text, the entry's own value else the field's default. Undefined for a
243
+ * name that is no such field.
244
+ */
245
+ declare function pickLists(projectTarget: TargetDecl | undefined, projectEntry: Readonly<Record<string, unknown>> | undefined): (name: string) => readonly string[] | undefined;
246
+ /** A list without ids holding text: what a pick's `list` source names. */
247
+ declare function isTextList(decl: TypeDecl): boolean;
175
248
  /**
176
249
  * A declaration's default: the one it gives, else its type's. A scalar's is
177
250
  * the kernel's (`defaultFor`: false, 0, "", the first enum value, no flags,
@@ -345,4 +418,4 @@ declare const atKey: (base: string, key: string) => string;
345
418
  /** `base` and then list position `i`. */
346
419
  declare const atIndex: (base: string, i: number) => string;
347
420
 
348
- export { ATOMIC, COMMENTS, type ClaimDecl, type Conflict, type ConflictKind, type DataMergeResult, type ExtensionData, type FieldDecl, HOSTS, type HostName, type HostSpec, type JsonValue, MANIFEST_FILE_SUFFIX, MANIFEST_SCHEMA, type Manifest, type MergeWarning, type Problem, type ResolvedClaim, SCALAR_TYPES, SET, type ScalarType, type SettingDecl, type StageRename, type Strategy, type TargetDecl, type TreeMergeOptions, type TreeMergeResult, type TypeDecl, UNRESOLVED_KEY, type UnresolvedEntry, VALUE_TYPES, type ValueContext, type ValueType, atIndex, atKey, claimClashes, compileEntry, compiledTarget, defaultOf, effectiveClaim, entryProblems, extensionDataStrategy, keyedBy, manifestProblems, mapOf, markUnresolved, mergeComments, mergeExtensionData, mergeTree, objectOf, orphanedEntries, parseManifest, renameStages, resolvedClaims, settingValues, stableStringify, strategyForTarget, strategyForType, targetOf, unresolvedOf, unresolvedProblems, valueProblems, withoutMark };
421
+ export { ATOMIC, COMMENTS, type ClaimDecl, type Conflict, type ConflictKind, type DataMergeResult, type ExtensionData, type FieldDecl, HOSTS, type HostName, type HostSpec, type JsonValue, MANIFEST_FILE_SUFFIX, MANIFEST_SCHEMA, type Manifest, type MergeWarning, type PickChoice, type PickSource, type PickSources, type Problem, type ResolvedClaim, SCALAR_TYPES, SET, type ScalarType, type SettingDecl, type StageRename, type Strategy, type TargetDecl, type TreeMergeOptions, type TreeMergeResult, type TypeDecl, UNRESOLVED_KEY, type UnresolvedEntry, VALUE_TYPES, type ValueContext, type ValueType, type ViewDecl, atIndex, atKey, claimClashes, compileEntry, compiledTarget, defaultOf, effectiveClaim, entryProblems, extensionDataStrategy, isTextList, isViewEntry, keyedBy, manifestProblems, mapOf, markUnresolved, mergeComments, mergeExtensionData, mergeTree, objectOf, orphanedEntries, parseManifest, pickChoices, pickClashes, pickLists, renameStages, resolvedClaims, settingValues, stableStringify, strategyForTarget, strategyForType, targetOf, unresolvedOf, unresolvedProblems, valueProblems, viewsFor, withoutMark };
@@ -18,7 +18,7 @@ var HOSTS = {
18
18
  }
19
19
  };
20
20
  var SCALAR_TYPES = ["text", "multiline", "number", "boolean", "enum", "flags", "quality"];
21
- var VALUE_TYPES = [...SCALAR_TYPES, "reference", "record", "map", "list"];
21
+ var VALUE_TYPES = [...SCALAR_TYPES, "reference", "pick", "record", "map", "list"];
22
22
 
23
23
  // src/extensions/path.ts
24
24
  var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
@@ -102,6 +102,26 @@ function valueProblems(decl, value, ctx = {}, path = "") {
102
102
  out.push(err("no-such-node", path, `${where(path)} names ${decl.to} "${value}", which does not exist`));
103
103
  }
104
104
  break;
105
+ case "pick": {
106
+ if (typeof value !== "string") {
107
+ wrong("one of its choices, as text");
108
+ break;
109
+ }
110
+ if (value === "") break;
111
+ const sources = decl.from ?? [];
112
+ const holding = sources.filter((s) => sourceHolds(s, value, ctx) === true);
113
+ if (holding.length > 1) {
114
+ out.push({
115
+ severity: "warning",
116
+ code: "pick-ambiguous",
117
+ path,
118
+ message: `${where(path)} is "${value}", which is ${holding.map(sourceWord).join(" and ")}`
119
+ });
120
+ } else if (holding.length === 0 && sources.every((s) => sourceHolds(s, value, ctx) !== void 0)) {
121
+ out.push(err("not-a-choice", path, `${where(path)} is "${value}", which is not one of its choices`));
122
+ }
123
+ break;
124
+ }
105
125
  case "record":
106
126
  if (!isObject(value)) {
107
127
  wrong("an object");
@@ -181,11 +201,69 @@ function entryProblems(target, entry, ctx = {}, path = "") {
181
201
  }
182
202
  return fieldsProblems(target.fields ?? [], entry, ctx, path);
183
203
  }
204
+ function sourceHolds(source, value, ctx) {
205
+ if ("words" in source) return Array.isArray(source.words) && source.words.includes(value);
206
+ if ("to" in source) return ctx.exists === void 0 ? void 0 : ctx.exists(source.to, value);
207
+ if ("list" in source) return ctx.list === void 0 ? void 0 : (ctx.list(source.list) ?? []).includes(value);
208
+ return false;
209
+ }
210
+ function sourceWord(source) {
211
+ if ("words" in source) return "a fixed word";
212
+ if ("to" in source) return source.to === "cast" ? "a cast member" : `a ${source.to}`;
213
+ return `an entry of the list "${source.list}"`;
214
+ }
215
+ function pickChoices(decl, src) {
216
+ const out = [];
217
+ for (const source of decl.from ?? []) {
218
+ if ("words" in source) {
219
+ for (const w of source.words ?? []) if (typeof w === "string" && w !== "") out.push({ value: w, label: w, source });
220
+ } else if ("to" in source) {
221
+ for (const n of src.nodes?.(source.to) ?? []) if (n.id !== "") out.push({ value: n.id, label: n.label ?? n.id, source });
222
+ } else if ("list" in source) {
223
+ for (const e of src.list?.(source.list) ?? []) if (e !== "") out.push({ value: e, label: e, source });
224
+ }
225
+ }
226
+ return out;
227
+ }
228
+ function pickClashes(decl, src, path = "") {
229
+ const by = /* @__PURE__ */ new Map();
230
+ for (const c of pickChoices(decl, src)) {
231
+ const held = by.get(c.value) ?? [];
232
+ if (!held.includes(c.source)) held.push(c.source);
233
+ by.set(c.value, held);
234
+ }
235
+ const out = [];
236
+ for (const [name, sources] of by) {
237
+ if (sources.length < 2) continue;
238
+ out.push({
239
+ severity: "warning",
240
+ code: "pick-clash",
241
+ path,
242
+ options: [name],
243
+ message: `"${name}" is ${sources.map(sourceWord).join(" and ")}, so a value naming it is ambiguous`
244
+ });
245
+ }
246
+ return out;
247
+ }
248
+ function pickLists(projectTarget, projectEntry) {
249
+ return (name) => {
250
+ const field = (projectTarget?.fields ?? []).find((f) => f.name === name);
251
+ if (field === void 0 || !isTextList(field)) return void 0;
252
+ const held = projectEntry !== void 0 && own(projectEntry, name) ? projectEntry[name] : void 0;
253
+ const value = Array.isArray(held) ? held : defaultOf(field);
254
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
255
+ };
256
+ }
257
+ function isTextList(decl) {
258
+ return decl.type === "list" && decl.id === void 0 && decl.of?.type === "text";
259
+ }
184
260
  function defaultOf(decl) {
185
261
  if (decl.default !== void 0) return decl.default;
186
262
  switch (decl.type) {
187
263
  case "reference":
188
264
  return "";
265
+ case "pick":
266
+ return "";
189
267
  case "record":
190
268
  return Object.fromEntries((decl.fields ?? []).map((f) => [f.name, defaultOf(f)]));
191
269
  case "map":
@@ -278,11 +356,13 @@ function renameStages(target, entry) {
278
356
  var ID = /^[a-z][a-z0-9-]*$/;
279
357
  var VERSION = /^\d+\.\d+\.\d+$/;
280
358
  var NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
281
- var TYPE_KEYS = ["type", "values", "stages", "renamedStages", "to", "fields", "key", "of", "id", "min", "max", "valueHelp", "default"];
359
+ var TYPE_KEYS = ["type", "values", "stages", "renamedStages", "to", "from", "fields", "key", "of", "id", "min", "max", "valueHelp", "default"];
282
360
  var FIELD_KEYS = /* @__PURE__ */ new Set([...TYPE_KEYS, "name", "label", "help", "required", "group", "editorOnly"]);
283
361
  var CLAIM_KEYS = /* @__PURE__ */ new Set([...TYPE_KEYS, "setting", "label", "help", "required"]);
284
362
  var BARE_KEYS = new Set(TYPE_KEYS);
285
- var MANIFEST_KEYS = /* @__PURE__ */ new Set(["schema", "id", "name", "version", "docs", "settings", "targets"]);
363
+ var MANIFEST_KEYS = /* @__PURE__ */ new Set(["schema", "id", "name", "version", "docs", "settings", "targets", "views", "reads"]);
364
+ var VIEW_KEYS = /* @__PURE__ */ new Set(["id", "name", "entry", "element", "hosts"]);
365
+ var ELEMENT = /^[a-z][a-z0-9._]*-[a-z0-9._-]*$/;
286
366
  var err2 = (code, path, message) => ({ severity: "error", code, path, message });
287
367
  function parseManifest(text) {
288
368
  return parseSource(text);
@@ -304,6 +384,8 @@ function manifestProblems(manifest) {
304
384
  }
305
385
  if (m["docs"] !== void 0 && typeof m["docs"] !== "string") out.push(err2("docs", "docs", "docs is a link, as text"));
306
386
  const settings = settingsProblems(m["settings"], out);
387
+ viewsProblems(m["views"], m["targets"], out);
388
+ readsProblems(m["reads"], m["id"], out);
307
389
  const targets = m["targets"];
308
390
  if (!isObject(targets) || Object.keys(targets).length === 0) {
309
391
  out.push(err2("no-targets", "targets", "the manifest targets no host"));
@@ -328,9 +410,146 @@ function manifestProblems(manifest) {
328
410
  }
329
411
  targetProblems(host, kind, target, kindPath, settings, out);
330
412
  }
413
+ pickListProblems(kinds, hostPath, out);
331
414
  }
332
415
  return out;
333
416
  }
417
+ function pickSourcesSound(host, from, path, out) {
418
+ if (!Array.isArray(from) || from.length === 0) {
419
+ out.push(err2("pick-from", path, 'a pick lists its sources in "from": { "words": [...] }, { "to": a node kind }, or { "list": a project list }'));
420
+ return false;
421
+ }
422
+ let sound = true;
423
+ from.forEach((s, i) => {
424
+ const at = atIndex(path, i);
425
+ const keys = isObject(s) ? Object.keys(s).filter((k) => k === "words" || k === "to" || k === "list") : [];
426
+ if (!isObject(s) || keys.length !== 1) {
427
+ out.push(err2("pick-source", at, 'a source is one of { "words": [...] }, { "to": a node kind }, { "list": a project list }'));
428
+ sound = false;
429
+ return;
430
+ }
431
+ unknownKeys(s, new Set(keys), at, out);
432
+ if (keys[0] === "words") {
433
+ const words = s["words"];
434
+ const seen = /* @__PURE__ */ new Set();
435
+ if (!Array.isArray(words) || words.length === 0) {
436
+ out.push(err2("pick-words", atKey(at, "words"), "fixed words are a list of at least one"));
437
+ sound = false;
438
+ return;
439
+ }
440
+ words.forEach((w, j) => {
441
+ if (typeof w !== "string" || w.trim() === "") {
442
+ out.push(err2("value-empty", atIndex(atKey(at, "words"), j), "a word has a name"));
443
+ sound = false;
444
+ } else if (seen.has(w)) {
445
+ out.push(err2("value-duplicate", atIndex(atKey(at, "words"), j), `"${w}" is listed twice`));
446
+ sound = false;
447
+ } else seen.add(w);
448
+ });
449
+ } else if (keys[0] === "to") {
450
+ if (typeof s["to"] !== "string" || !HOSTS[host].references.includes(s["to"])) {
451
+ out.push(err2("reference-to", atKey(at, "to"), `a source's nodes are of a kind, one of ${HOSTS[host].references.join(", ")}`));
452
+ sound = false;
453
+ }
454
+ } else if (typeof s["list"] !== "string" || !NAME.test(s["list"])) {
455
+ out.push(err2("pick-list", atKey(at, "list"), "a list source names a field of the extension's project node"));
456
+ sound = false;
457
+ }
458
+ });
459
+ return sound;
460
+ }
461
+ function pickListProblems(kinds, hostPath, out) {
462
+ const project = isObject(kinds["project"]) && Array.isArray(kinds["project"]["fields"]) ? kinds["project"]["fields"] : [];
463
+ const lists = new Set(project.filter((f) => isObject(f) && isTextList(f)).map((f) => f.name));
464
+ const walk = (decl, path) => {
465
+ if (!isObject(decl)) return;
466
+ const d = decl;
467
+ if (d.type === "pick" && Array.isArray(d.from)) {
468
+ d.from.forEach((s, i) => {
469
+ if (isObject(s) && typeof s.list === "string" && !lists.has(s.list)) {
470
+ const name = s.list;
471
+ out.push(err2(
472
+ "pick-list",
473
+ atKey(atIndex(atKey(path, "from"), i), "list"),
474
+ `"${name}" is not a list of text on the extension's project node; declare it there, as { "name": "${name}", "type": "list", "of": { "type": "text" } }`
475
+ ));
476
+ }
477
+ });
478
+ }
479
+ if (Array.isArray(d.fields)) d.fields.forEach((f, i) => walk(f, atIndex(atKey(path, "fields"), i)));
480
+ if (d.of !== void 0) walk(d.of, atKey(path, "of"));
481
+ };
482
+ for (const [kind, target] of Object.entries(kinds)) {
483
+ if (!isObject(target) || !Array.isArray(target["fields"])) continue;
484
+ target["fields"].forEach((f, i) => walk(f, atIndex(atKey(atKey(hostPath, kind), "fields"), i)));
485
+ }
486
+ }
487
+ function isViewEntry(entry) {
488
+ if (typeof entry !== "string" || !entry.endsWith(".js") || entry.includes("\\")) return false;
489
+ const parts = entry.split("/");
490
+ return parts.every((p) => p !== "" && p !== "." && p !== "..");
491
+ }
492
+ function viewsProblems(views, targets, out) {
493
+ if (views === void 0) return;
494
+ if (!Array.isArray(views)) {
495
+ out.push(err2("not-a-list", "views", "views are a list"));
496
+ return;
497
+ }
498
+ const ids = /* @__PURE__ */ new Set();
499
+ const elements = /* @__PURE__ */ new Set();
500
+ const targeted = isObject(targets) ? Object.keys(targets) : [];
501
+ views.forEach((v, i) => {
502
+ const at = atIndex("views", i);
503
+ if (!isObject(v)) {
504
+ out.push(err2("not-an-object", at, "a view is an object"));
505
+ return;
506
+ }
507
+ unknownKeys(v, VIEW_KEYS, at, out);
508
+ const id = v["id"];
509
+ 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"));
510
+ else if (ids.has(id)) out.push(err2("view-duplicate", atKey(at, "id"), `two views are called "${id}"`));
511
+ else ids.add(id);
512
+ 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"));
513
+ if (!isViewEntry(v["entry"])) {
514
+ 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"));
515
+ }
516
+ const element = v["element"];
517
+ if (typeof element !== "string" || !ELEMENT.test(element)) {
518
+ 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"));
519
+ } else if (elements.has(element)) out.push(err2("view-duplicate", atKey(at, "element"), `two views define <${element}>`));
520
+ else elements.add(element);
521
+ const hosts = v["hosts"];
522
+ if (hosts === void 0) return;
523
+ if (!Array.isArray(hosts) || hosts.length === 0) {
524
+ out.push(err2("view-hosts", atKey(at, "hosts"), "a view's hosts are a list of at least one host"));
525
+ return;
526
+ }
527
+ hosts.forEach((h, j) => {
528
+ const hp = atIndex(atKey(at, "hosts"), j);
529
+ 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"`));
530
+ 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` });
531
+ });
532
+ });
533
+ }
534
+ function readsProblems(reads, self, out) {
535
+ if (reads === void 0) return;
536
+ if (!Array.isArray(reads)) {
537
+ out.push(err2("not-a-list", "reads", "reads is a list of extension ids"));
538
+ return;
539
+ }
540
+ const seen = /* @__PURE__ */ new Set();
541
+ reads.forEach((r, i) => {
542
+ const at = atIndex("reads", i);
543
+ 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"));
544
+ 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" });
545
+ else if (seen.has(r)) out.push(err2("reads-duplicate", at, `"${r}" is listed twice`));
546
+ else seen.add(r);
547
+ });
548
+ }
549
+ function viewsFor(manifest, host) {
550
+ const targeted = Object.keys(manifest.targets ?? {});
551
+ 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)));
552
+ }
334
553
  function unknownKeys(obj, known, path, out) {
335
554
  for (const k of Object.keys(obj)) {
336
555
  if (!known.has(k)) {
@@ -491,6 +710,9 @@ function typeProblems(host, decl, path, named, out, claim = false) {
491
710
  sound = false;
492
711
  }
493
712
  break;
713
+ case "pick":
714
+ if (!pickSourcesSound(host, decl.from, at("from"), out)) sound = false;
715
+ break;
494
716
  case "record":
495
717
  if (!Array.isArray(decl.fields) || decl.fields.length === 0) {
496
718
  out.push(err2("record-empty", at("fields"), "a record declares its fields"));
@@ -965,6 +1187,6 @@ function orphanedEntries(data, exists) {
965
1187
  return out;
966
1188
  }
967
1189
 
968
- export { ATOMIC, COMMENTS, HOSTS, MANIFEST_FILE_SUFFIX, MANIFEST_SCHEMA, SCALAR_TYPES, SET, UNRESOLVED_KEY, VALUE_TYPES, atIndex, atKey, claimClashes, compileEntry, compiledTarget, defaultOf, effectiveClaim, entryProblems, extensionDataStrategy, keyedBy, manifestProblems, mapOf, markUnresolved, mergeComments, mergeExtensionData, mergeTree, objectOf, orphanedEntries, parseManifest, renameStages, resolvedClaims, settingValues, stableStringify, strategyForTarget, strategyForType, targetOf, unresolvedOf, unresolvedProblems, valueProblems, withoutMark };
1190
+ export { ATOMIC, COMMENTS, HOSTS, MANIFEST_FILE_SUFFIX, MANIFEST_SCHEMA, SCALAR_TYPES, SET, UNRESOLVED_KEY, VALUE_TYPES, atIndex, atKey, claimClashes, compileEntry, compiledTarget, defaultOf, effectiveClaim, entryProblems, extensionDataStrategy, isTextList, isViewEntry, keyedBy, manifestProblems, mapOf, markUnresolved, mergeComments, mergeExtensionData, mergeTree, objectOf, orphanedEntries, parseManifest, pickChoices, pickClashes, pickLists, renameStages, resolvedClaims, settingValues, stableStringify, strategyForTarget, strategyForType, targetOf, unresolvedOf, unresolvedProblems, valueProblems, viewsFor, withoutMark };
969
1191
  //# sourceMappingURL=extensions.js.map
970
1192
  //# sourceMappingURL=extensions.js.map