@pathmx/core 0.5.0

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.
Files changed (195) hide show
  1. package/LICENSE.md +172 -0
  2. package/README.md +111 -0
  3. package/actions/actions.ts +307 -0
  4. package/actions/draft.ts +105 -0
  5. package/actions/transaction.ts +164 -0
  6. package/assets.ts +149 -0
  7. package/authority/access.ts +820 -0
  8. package/authority/actors.ts +271 -0
  9. package/authority/agents.ts +95 -0
  10. package/authority/credentials.ts +144 -0
  11. package/authority/model.ts +71 -0
  12. package/authority/sharing.ts +89 -0
  13. package/browser-bundle.ts +61 -0
  14. package/canonical.ts +41 -0
  15. package/cli/dev.ts +134 -0
  16. package/cli/export.ts +74 -0
  17. package/cli/index.ts +52 -0
  18. package/cli/lint.ts +164 -0
  19. package/cli/mv.ts +30 -0
  20. package/cli/perms.ts +182 -0
  21. package/cli/plugin-packages.ts +159 -0
  22. package/cli/plugins.ts +95 -0
  23. package/cli/query.ts +225 -0
  24. package/cli/readme.md +95 -0
  25. package/cli/release.ts +75 -0
  26. package/cli/rm.ts +22 -0
  27. package/cli/serve.ts +100 -0
  28. package/cli/tree.ts +131 -0
  29. package/cli/update.ts +78 -0
  30. package/cli/utils.ts +53 -0
  31. package/core.ts +96 -0
  32. package/database.ts +189 -0
  33. package/dependencies.ts +60 -0
  34. package/dist/pathmx.js +1224 -0
  35. package/document.ts +194 -0
  36. package/environment.ts +110 -0
  37. package/environments/bun-plugins.ts +68 -0
  38. package/environments/bun-publish.ts +194 -0
  39. package/environments/bun.ts +223 -0
  40. package/globals.d.ts +14 -0
  41. package/graph.ts +153 -0
  42. package/host/compiler.ts +314 -0
  43. package/host/create.ts +66 -0
  44. package/host/engine.ts +630 -0
  45. package/host/plugin-host.ts +487 -0
  46. package/host/render.ts +102 -0
  47. package/host/response.ts +20 -0
  48. package/host/shell.html +15 -0
  49. package/host/source-sync.ts +254 -0
  50. package/html-attributes.ts +15 -0
  51. package/html.ts +48 -0
  52. package/icons/LICENSE +43 -0
  53. package/icons/lucide.ts +54 -0
  54. package/index.ts +75 -0
  55. package/invalidation.ts +85 -0
  56. package/log.ts +84 -0
  57. package/ops/href.ts +87 -0
  58. package/ops/index.ts +9 -0
  59. package/ops/move-source.ts +114 -0
  60. package/ops/plan.ts +47 -0
  61. package/ops/remove-source.ts +70 -0
  62. package/package.json +46 -0
  63. package/path.ts +75 -0
  64. package/pattern.ts +314 -0
  65. package/perf.ts +88 -0
  66. package/plugins/actor-path.ts +43 -0
  67. package/plugins/actor-profile.ts +55 -0
  68. package/plugins/agent-browsing.ts +183 -0
  69. package/plugins/app-component.ts +23 -0
  70. package/plugins/block-layout/index.ts +78 -0
  71. package/plugins/block-layout/theme.css +90 -0
  72. package/plugins/callout/index.ts +236 -0
  73. package/plugins/callout/theme.css +138 -0
  74. package/plugins/canonical-links.ts +144 -0
  75. package/plugins/code-highlight/client.ts +89 -0
  76. package/plugins/code-highlight/index.ts +147 -0
  77. package/plugins/code-highlight/theme.css +89 -0
  78. package/plugins/context.ts +131 -0
  79. package/plugins/datatype/Datatype[wdth,wght].woff2 +0 -0
  80. package/plugins/datatype/OFL.txt +96 -0
  81. package/plugins/datatype/index.ts +287 -0
  82. package/plugins/definition-list.ts +236 -0
  83. package/plugins/directive-definitions.ts +13 -0
  84. package/plugins/embedding/client.ts +88 -0
  85. package/plugins/embedding/index.ts +43 -0
  86. package/plugins/image/index.ts +68 -0
  87. package/plugins/image/theme.css +43 -0
  88. package/plugins/include.ts +119 -0
  89. package/plugins/index.ts +91 -0
  90. package/plugins/input/authoring.ts +236 -0
  91. package/plugins/input/config.ts +85 -0
  92. package/plugins/input/index.ts +359 -0
  93. package/plugins/input/model.ts +173 -0
  94. package/plugins/input/state.ts +272 -0
  95. package/plugins/layout.ts +313 -0
  96. package/plugins/literate/client.ts +208 -0
  97. package/plugins/literate/index.ts +742 -0
  98. package/plugins/math/index.ts +140 -0
  99. package/plugins/meta.ts +42 -0
  100. package/plugins/navigation/index.ts +158 -0
  101. package/plugins/navigation/model.ts +364 -0
  102. package/plugins/preset.ts +65 -0
  103. package/plugins/query/index.ts +322 -0
  104. package/plugins/rss/index.ts +46 -0
  105. package/plugins/runtime/actions.ts +62 -0
  106. package/plugins/runtime/client.ts +8 -0
  107. package/plugins/runtime/document.ts +506 -0
  108. package/plugins/runtime/error.ts +154 -0
  109. package/plugins/runtime/heading-links.ts +31 -0
  110. package/plugins/runtime/index.ts +104 -0
  111. package/plugins/runtime/live.ts +51 -0
  112. package/plugins/runtime/navigation.ts +113 -0
  113. package/plugins/runtime/pending.ts +53 -0
  114. package/plugins/runtime/publication.ts +19 -0
  115. package/plugins/runtime/start.ts +177 -0
  116. package/plugins/runtime/static-client.ts +6 -0
  117. package/plugins/runtime/static-live.ts +188 -0
  118. package/plugins/runtime/view-transition.ts +72 -0
  119. package/plugins/runtime/viewport.ts +110 -0
  120. package/plugins/schema.ts +24 -0
  121. package/plugins/scripts.ts +140 -0
  122. package/plugins/source-preview/client.ts +256 -0
  123. package/plugins/source-preview/index.ts +132 -0
  124. package/plugins/source-preview/model.ts +11 -0
  125. package/plugins/source-preview/preview.css +107 -0
  126. package/plugins/styles.ts +145 -0
  127. package/plugins/theme/components.ts +10 -0
  128. package/plugins/theme/foundation.css +358 -0
  129. package/plugins/theme/index.ts +333 -0
  130. package/plugins/theme/prose.css +284 -0
  131. package/plugins/theme/scheme.js +63 -0
  132. package/plugins/theme/toggle.css +44 -0
  133. package/plugins/theme/view-transition.css +68 -0
  134. package/plugins/toc/client.ts +146 -0
  135. package/plugins/toc/index.ts +232 -0
  136. package/plugins/toc/theme.css +172 -0
  137. package/plugins/types.ts +538 -0
  138. package/problem.ts +41 -0
  139. package/project.ts +6 -0
  140. package/publish/build.ts +402 -0
  141. package/publish/concurrent.ts +32 -0
  142. package/publish/identity.ts +63 -0
  143. package/publish/index.ts +11 -0
  144. package/publish/location.ts +167 -0
  145. package/publish/model.ts +58 -0
  146. package/publish/plan.ts +300 -0
  147. package/publish/validate.ts +150 -0
  148. package/query/candidate-index.ts +101 -0
  149. package/query/dependency-index.ts +99 -0
  150. package/query/fields.ts +216 -0
  151. package/query/invalidation.ts +48 -0
  152. package/query/kernel.ts +776 -0
  153. package/query/model.ts +79 -0
  154. package/repository.ts +374 -0
  155. package/runtime-profile.ts +34 -0
  156. package/schema/model.ts +92 -0
  157. package/schema/registry.ts +642 -0
  158. package/schema/standard.ts +226 -0
  159. package/server/context.ts +94 -0
  160. package/server/css-imports.ts +66 -0
  161. package/server/http.ts +333 -0
  162. package/server/page.ts +21 -0
  163. package/server/protocols.ts +12 -0
  164. package/server/representations.ts +66 -0
  165. package/server/router.ts +131 -0
  166. package/server/routes.ts +202 -0
  167. package/server/serve.ts +5 -0
  168. package/server/watch.ts +120 -0
  169. package/source/blocks.ts +71 -0
  170. package/source/format.ts +164 -0
  171. package/source/index.ts +67 -0
  172. package/source/interpolate.ts +63 -0
  173. package/source/markdown/blocks.ts +46 -0
  174. package/source/markdown/comments.ts +32 -0
  175. package/source/markdown/fences.ts +67 -0
  176. package/source/markdown/headings.ts +88 -0
  177. package/source/markdown/index.ts +34 -0
  178. package/source/markdown/inline-code.ts +6 -0
  179. package/source/markdown/links.ts +326 -0
  180. package/source/markdown/lists.ts +223 -0
  181. package/source/markdown/prose.ts +74 -0
  182. package/source/markdown/scan.ts +79 -0
  183. package/source/markdown/text.ts +82 -0
  184. package/source/meta.ts +87 -0
  185. package/source/source.ts +121 -0
  186. package/source/topmatter.ts +57 -0
  187. package/source/utils.ts +21 -0
  188. package/source-diagnostic.ts +63 -0
  189. package/text-file.ts +26 -0
  190. package/util.ts +35 -0
  191. package/view/components.ts +499 -0
  192. package/view/model.ts +91 -0
  193. package/view/projection.ts +162 -0
  194. package/view/session.ts +146 -0
  195. package/view/syntax.ts +372 -0
@@ -0,0 +1,359 @@
1
+ import { defineAction } from "../../actions/actions.ts"
2
+ import { html, type Html } from "../../html.ts"
3
+ import { publicPath } from "../../server/router.ts"
4
+ import { SourceDiagnosticError } from "../../source-diagnostic.ts"
5
+ import {
6
+ defineViewComponent,
7
+ defineViewVariable,
8
+ type AppContext,
9
+ type Plugin,
10
+ type PluginInvalidationTarget,
11
+ type Source,
12
+ type ViewContext,
13
+ } from "../types.ts"
14
+ import {
15
+ inspectInputSource,
16
+ type InputAuthoring,
17
+ optionFrom,
18
+ optionProps,
19
+ submitProps,
20
+ textFrom,
21
+ textProps,
22
+ } from "./authoring.ts"
23
+ import { InputModelIndex, type InputChoice, type InputText } from "./model.ts"
24
+ import { InputStateIndex, type InputValue, type InputValues } from "./state.ts"
25
+ import { InputResponsePropSchema, inputResponseConfig } from "./config.ts"
26
+
27
+ type InputProjection = Readonly<{
28
+ values?: InputValues
29
+ editable: boolean
30
+ }>
31
+
32
+ const PENDING_EVIDENCE = Object.freeze({})
33
+
34
+ function selected(value: InputValue | undefined, option: string) {
35
+ return Array.isArray(value) ? value.includes(option) : value === option
36
+ }
37
+
38
+ function presence(name: string, enabled: boolean): Html {
39
+ return enabled ? html` ${name}` : ""
40
+ }
41
+
42
+ function renderOption(
43
+ option: InputChoice,
44
+ inputValue: InputValue | undefined,
45
+ editable: boolean,
46
+ ) {
47
+ const checked = presence("checked", selected(inputValue, option.value))
48
+ const required = presence("required", option.required)
49
+ const disabled = presence("disabled", !editable)
50
+ return html`<label class="input-option"
51
+ ><input
52
+ type="${option.type}"
53
+ name="${option.name}"
54
+ value="${option.value}"
55
+ ${checked}${required}${disabled}
56
+ /><span>${option.label}</span></label
57
+ >`
58
+ }
59
+
60
+ function renderText(
61
+ control: InputText,
62
+ inputValue: InputValue | undefined,
63
+ editable: boolean,
64
+ ) {
65
+ const value = typeof inputValue === "string" ? inputValue : ""
66
+ const required = presence("required", control.required)
67
+ const disabled = presence("disabled", !editable)
68
+ const placeholder = control.placeholder
69
+ ? html` placeholder="${control.placeholder}"`
70
+ : ""
71
+ return control.type === "textarea"
72
+ ? html`<label class="input-field input-textarea"
73
+ ><span>${control.label}</span
74
+ ><textarea name="${control.name}" ${placeholder}${required}${disabled}>
75
+ ${value}</textarea>
76
+ </label>`
77
+ : html`<label class="input-field input-text"
78
+ ><span>${control.label}</span
79
+ ><input
80
+ type="text"
81
+ name="${control.name}"
82
+ value="${value}"
83
+ ${placeholder}${required}${disabled}
84
+ /></label>`
85
+ }
86
+
87
+ function inputDiagnostic(source: Source, message: string) {
88
+ return new SourceDiagnosticError({
89
+ source: source.path,
90
+ plugin: "input",
91
+ message,
92
+ })
93
+ }
94
+
95
+ function inputPlugin(
96
+ models: InputModelIndex,
97
+ states: InputStateIndex,
98
+ authoring: readonly InputAuthoring[],
99
+ ): Plugin {
100
+ const projected = new WeakMap<ViewContext, Map<string, InputProjection>>()
101
+ const resolveState = (origin: Source, actorId?: string) => {
102
+ try {
103
+ return states.resolve(origin, actorId)
104
+ } catch (error) {
105
+ throw inputDiagnostic(
106
+ origin,
107
+ error instanceof Error ? error.message : String(error),
108
+ )
109
+ }
110
+ }
111
+
112
+ const projectionFor = (
113
+ origin: Source,
114
+ view: ViewContext,
115
+ app: AppContext,
116
+ ) => {
117
+ let byOrigin = projected.get(view)
118
+ if (!byOrigin) {
119
+ byOrigin = new Map()
120
+ projected.set(view, byOrigin)
121
+ }
122
+ const cached = byOrigin.get(origin.id)
123
+ if (cached) return cached
124
+
125
+ const actorId =
126
+ view.session.type === "authenticated" ? view.session.actor.id : undefined
127
+ const principal = view.view.principal
128
+ const binding = resolveState(origin, actorId)
129
+ const stored = binding.state
130
+ const readable = stored
131
+ ? app.access.can(principal, stored.source, "read")
132
+ : false
133
+ const writable = stored
134
+ ? app.access.can(principal, stored.source, "write")
135
+ : binding.type === "actor-relative" && actorId != null
136
+ const projection: InputProjection = {
137
+ values:
138
+ stored && readable
139
+ ? models.validate(origin.path, stored.values)
140
+ : undefined,
141
+ editable: writable && app.access.can(principal, origin, "execute"),
142
+ }
143
+ byOrigin.set(origin.id, projection)
144
+ return projection
145
+ }
146
+
147
+ const option = (type: InputChoice["type"]) =>
148
+ defineViewComponent({
149
+ tag: `input:${type}`,
150
+ props: optionProps,
151
+ compile(use) {
152
+ // View compilation runs after indexing, so this validates any bound State.
153
+ resolveState(use.origin)
154
+ return optionFrom(use, type)
155
+ },
156
+ render(use, compiled, view, app) {
157
+ const projection = projectionFor(use.origin, view, app)
158
+ return renderOption(
159
+ compiled,
160
+ projection.values?.[use.props.name!],
161
+ projection.editable,
162
+ )
163
+ },
164
+ })
165
+
166
+ const text = (type: InputText["type"]) =>
167
+ defineViewComponent({
168
+ tag: `input:${type}`,
169
+ props: textProps,
170
+ compile(use) {
171
+ resolveState(use.origin)
172
+ return textFrom(use, type)
173
+ },
174
+ render(use, compiled, view, app) {
175
+ const projection = projectionFor(use.origin, view, app)
176
+ return renderText(
177
+ compiled,
178
+ projection.values?.[use.props.name!],
179
+ projection.editable,
180
+ )
181
+ },
182
+ })
183
+
184
+ const submit = defineViewComponent({
185
+ tag: "input:submit",
186
+ props: submitProps,
187
+ render(use, _compiled, view, app) {
188
+ const disabled = presence(
189
+ "disabled",
190
+ !projectionFor(use.origin, view, app).editable,
191
+ )
192
+ return html`<button
193
+ type="submit"
194
+ formaction="/actions/input:submit"
195
+ formmethod="post"
196
+ ${disabled}
197
+ >
198
+ ${use.props.label}
199
+ </button>`
200
+ },
201
+ })
202
+
203
+ const submitAction = defineAction({
204
+ title: "Save response",
205
+ description: "Save the current actor's response to this Source.",
206
+ affordance: ({ target, principal, can }) =>
207
+ models.has(target.path) && principal.type === "actor" && can("execute"),
208
+ authorize: ({ target }) =>
209
+ Boolean(target && models.has(target.path)) ||
210
+ "Input Action requires an originating Source.",
211
+ async execute(input, { actor, target, tx, yaml }) {
212
+ if (!target)
213
+ throw new Error("Input Action requires an originating Source.")
214
+ const values = models.validate(target.path, input)
215
+ await states.write(tx, actor, target, values, yaml)
216
+ },
217
+ })
218
+
219
+ return {
220
+ id: "input",
221
+ name: "InputPlugin",
222
+ schema: {
223
+ props: { "input-response": InputResponsePropSchema },
224
+ actions: { submit: submitAction },
225
+ },
226
+ components: [
227
+ text("text"),
228
+ text("textarea"),
229
+ option("radio"),
230
+ option("checkbox"),
231
+ submit,
232
+ ],
233
+ variables: [
234
+ defineViewVariable({
235
+ namespace: "input",
236
+ render(use, view, app) {
237
+ if (use.path.length !== 1) return
238
+ const value = projectionFor(use.origin, view, app).values?.[
239
+ use.path[0]!
240
+ ]
241
+ return Array.isArray(value) ? value.join(", ") : value
242
+ },
243
+ }),
244
+ ],
245
+ index(source, app) {
246
+ const inspected = inspectInputSource(source, authoring)
247
+ if (inspected.issues[0]) {
248
+ throw inputDiagnostic(source, inspected.issues[0])
249
+ }
250
+ try {
251
+ models.replace(source, inspected.controls, inspected.completion)
252
+ } catch (error) {
253
+ throw inputDiagnostic(
254
+ source,
255
+ error instanceof Error ? error.message : String(error),
256
+ )
257
+ }
258
+ states.index(source, app)
259
+ },
260
+ unindex(sourcePath) {
261
+ states.unindex(sourcePath)
262
+ models.unindex(sourcePath)
263
+ },
264
+ lint(report, app) {
265
+ for (const source of app.sources.all()) {
266
+ for (const issue of inspectInputSource(source, authoring).issues) {
267
+ report(source, issue)
268
+ }
269
+ if (!inputResponseConfig(source)?.state) continue
270
+ try {
271
+ states.resolve(source)
272
+ } catch (error) {
273
+ report(source, error instanceof Error ? error.message : String(error))
274
+ }
275
+ }
276
+ for (const state of states.records()) {
277
+ if (state.statusIssue) {
278
+ report(
279
+ state.source,
280
+ `Invalid Input state in ${state.source.path}: ${state.statusIssue}`,
281
+ )
282
+ }
283
+ const origin = app.sources.get(state.originId)
284
+ if (!origin) {
285
+ report(
286
+ state.source,
287
+ `Input State ${state.source.path} targets missing Source ${state.originId}.`,
288
+ )
289
+ continue
290
+ }
291
+ try {
292
+ models.validate(origin.path, state.values)
293
+ } catch (error) {
294
+ report(
295
+ state.source,
296
+ error instanceof Error ? error.message : String(error),
297
+ )
298
+ }
299
+ }
300
+ },
301
+ invalidate(transition, app) {
302
+ const targets: PluginInvalidationTarget[] = []
303
+ for (const source of [transition.before, transition.after]) {
304
+ const record = states.transition(source, app)
305
+ if (!record) continue
306
+ const origin = app.sources.get(record.originId)
307
+ if (!origin) continue
308
+ targets.push(
309
+ states.isBound(origin, record.source.id)
310
+ ? { type: "route", route: publicPath(origin) }
311
+ : {
312
+ type: "route",
313
+ route: publicPath(origin),
314
+ actors: [record.actorId],
315
+ },
316
+ )
317
+ }
318
+ return targets.length ? { targets } : undefined
319
+ },
320
+ }
321
+ }
322
+
323
+ export function createInputPlugin(
324
+ options: Readonly<{ authoring?: readonly InputAuthoring[] }> = {},
325
+ ) {
326
+ const models = new InputModelIndex()
327
+ const states = new InputStateIndex()
328
+ const authoring = Object.freeze([...(options.authoring ?? [])])
329
+ return Object.freeze({
330
+ plugin: () => inputPlugin(models, states, authoring),
331
+ completion: Object.freeze({
332
+ declarations(source: Source) {
333
+ const target = models.completion(source.id)
334
+ return target ? Object.freeze([Object.freeze({ key: target.key })]) : []
335
+ },
336
+ read(actorId: string, ref: Readonly<{ source: string; key: string }>) {
337
+ const target = models.completion(ref.source)
338
+ if (!target || target.key !== ref.key) return
339
+ const state = states.forActor(actorId, ref.source)
340
+ if (!state?.submittedAt) return PENDING_EVIDENCE
341
+ try {
342
+ models.validateSource(ref.source, state.values)
343
+ return Object.freeze({
344
+ fact: Object.freeze({
345
+ outcome: "complete" as const,
346
+ occurredAt: state.submittedAt,
347
+ }),
348
+ })
349
+ } catch {
350
+ return PENDING_EVIDENCE
351
+ }
352
+ },
353
+ }),
354
+ })
355
+ }
356
+
357
+ export function InputPlugin(): Plugin {
358
+ return createInputPlugin().plugin()
359
+ }
@@ -0,0 +1,173 @@
1
+ import type { ActionInput } from "../../actions/actions.ts"
2
+ import type { Source } from "../../source/index.ts"
3
+ import type { InputValues } from "./state.ts"
4
+
5
+ export type InputChoice = Readonly<{
6
+ type: "radio" | "checkbox"
7
+ name: string
8
+ value: string
9
+ label: string
10
+ required: boolean
11
+ min: number
12
+ }>
13
+
14
+ export type InputText = Readonly<{
15
+ type: "text" | "textarea"
16
+ name: string
17
+ label: string
18
+ placeholder?: string
19
+ required: boolean
20
+ }>
21
+
22
+ export type InputControl = InputChoice | InputText
23
+
24
+ type InputChoiceField = {
25
+ type: InputChoice["type"]
26
+ options: Set<string>
27
+ required: boolean
28
+ min: number
29
+ }
30
+
31
+ type InputTextField = {
32
+ type: InputText["type"]
33
+ required: boolean
34
+ }
35
+
36
+ type InputField = InputChoiceField | InputTextField
37
+
38
+ function isChoice(control: InputControl): control is InputChoice {
39
+ return control.type === "radio" || control.type === "checkbox"
40
+ }
41
+
42
+ function isChoiceField(field: InputField): field is InputChoiceField {
43
+ return field.type === "radio" || field.type === "checkbox"
44
+ }
45
+
46
+ function textValue(name: string, required: boolean, raw: unknown) {
47
+ if (raw == null || raw === "") {
48
+ if (required) throw new Error(`Input field ${name} is required.`)
49
+ return
50
+ }
51
+ if (typeof raw !== "string") {
52
+ throw new Error(`Invalid value for Input field ${name}.`)
53
+ }
54
+ return raw
55
+ }
56
+
57
+ export class InputModelIndex {
58
+ private sources = new Map<string, Map<string, InputField>>()
59
+ private sourceIdByPath = new Map<string, string>()
60
+ private sourcePathById = new Map<string, string>()
61
+ private completionBySourceId = new Map<string, Readonly<{ key: string }>>()
62
+
63
+ has(sourcePath: string) {
64
+ return Boolean(this.sources.get(sourcePath)?.size)
65
+ }
66
+
67
+ unindex(sourcePath: string) {
68
+ this.sources.delete(sourcePath)
69
+ const sourceId = this.sourceIdByPath.get(sourcePath)
70
+ if (!sourceId) return
71
+ this.sourceIdByPath.delete(sourcePath)
72
+ this.sourcePathById.delete(sourceId)
73
+ this.completionBySourceId.delete(sourceId)
74
+ }
75
+
76
+ replace(
77
+ source: Source,
78
+ controls: readonly InputControl[],
79
+ completion?: Readonly<{ key: string }>,
80
+ ) {
81
+ this.unindex(source.path)
82
+ const fields = new Map<string, InputField>()
83
+ for (const control of controls) this.add(fields, source.path, control)
84
+ if (!fields.size && !completion) return
85
+ if (fields.size) this.sources.set(source.path, fields)
86
+ this.sourceIdByPath.set(source.path, source.id)
87
+ this.sourcePathById.set(source.id, source.path)
88
+ if (completion) this.completionBySourceId.set(source.id, completion)
89
+ }
90
+
91
+ private add(
92
+ fields: Map<string, InputField>,
93
+ sourcePath: string,
94
+ control: InputControl,
95
+ ) {
96
+ let field = fields.get(control.name)
97
+ if (!field) {
98
+ field = isChoice(control)
99
+ ? {
100
+ type: control.type,
101
+ options: new Set(),
102
+ required: control.required,
103
+ min: control.min,
104
+ }
105
+ : { type: control.type, required: control.required }
106
+ fields.set(control.name, field)
107
+ }
108
+ if (field.type !== control.type) {
109
+ throw new Error(
110
+ `Input field ${control.name} in ${sourcePath} mixes ${field.type} and ${control.type}.`,
111
+ )
112
+ }
113
+ field.required ||= control.required
114
+ if (isChoice(control) && isChoiceField(field)) {
115
+ field.options.add(control.value)
116
+ field.min = Math.max(field.min, control.min)
117
+ }
118
+ }
119
+
120
+ completion(sourceId: string) {
121
+ return this.completionBySourceId.get(sourceId)
122
+ }
123
+
124
+ validateSource(sourceId: string, values: InputValues) {
125
+ const path = this.sourcePathById.get(sourceId)
126
+ if (!path) throw new Error(`No indexed Input Source ${sourceId}.`)
127
+ return this.validate(path, values)
128
+ }
129
+
130
+ validate(sourcePath: string, input: ActionInput): InputValues {
131
+ const fields = this.sources.get(sourcePath)
132
+ if (!fields?.size)
133
+ throw new Error(`No indexed Input fields in ${sourcePath}.`)
134
+ const unknown = Object.keys(input).filter(
135
+ (name) => name !== "_target" && !fields.has(name),
136
+ )
137
+ if (unknown.length)
138
+ throw new Error(`Unknown Input field: ${unknown.join(", ")}.`)
139
+
140
+ const values: InputValues = {}
141
+ for (const [name, field] of fields) {
142
+ const raw = input[name]
143
+ if (!isChoiceField(field)) {
144
+ const value = textValue(name, field.required, raw)
145
+ if (value != null) values[name] = value
146
+ continue
147
+ }
148
+ if (field.type === "radio") {
149
+ if (raw == null || raw === "") {
150
+ if (field.required)
151
+ throw new Error(`Input field ${name} is required.`)
152
+ continue
153
+ }
154
+ if (Array.isArray(raw) || !field.options.has(raw)) {
155
+ throw new Error(`Invalid value for Input field ${name}.`)
156
+ }
157
+ values[name] = raw
158
+ continue
159
+ }
160
+
161
+ const selected = raw == null ? [] : Array.isArray(raw) ? raw : [raw]
162
+ const unique = [...new Set(selected)]
163
+ if (unique.some((value) => !field.options.has(value))) {
164
+ throw new Error(`Invalid value for Input field ${name}.`)
165
+ }
166
+ if (unique.length < Math.max(field.min, field.required ? 1 : 0)) {
167
+ throw new Error(`Input field ${name} requires more selections.`)
168
+ }
169
+ values[name] = unique
170
+ }
171
+ return values
172
+ }
173
+ }