@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.
- package/LICENSE.md +172 -0
- package/README.md +111 -0
- package/actions/actions.ts +307 -0
- package/actions/draft.ts +105 -0
- package/actions/transaction.ts +164 -0
- package/assets.ts +149 -0
- package/authority/access.ts +820 -0
- package/authority/actors.ts +271 -0
- package/authority/agents.ts +95 -0
- package/authority/credentials.ts +144 -0
- package/authority/model.ts +71 -0
- package/authority/sharing.ts +89 -0
- package/browser-bundle.ts +61 -0
- package/canonical.ts +41 -0
- package/cli/dev.ts +134 -0
- package/cli/export.ts +74 -0
- package/cli/index.ts +52 -0
- package/cli/lint.ts +164 -0
- package/cli/mv.ts +30 -0
- package/cli/perms.ts +182 -0
- package/cli/plugin-packages.ts +159 -0
- package/cli/plugins.ts +95 -0
- package/cli/query.ts +225 -0
- package/cli/readme.md +95 -0
- package/cli/release.ts +75 -0
- package/cli/rm.ts +22 -0
- package/cli/serve.ts +100 -0
- package/cli/tree.ts +131 -0
- package/cli/update.ts +78 -0
- package/cli/utils.ts +53 -0
- package/core.ts +96 -0
- package/database.ts +189 -0
- package/dependencies.ts +60 -0
- package/dist/pathmx.js +1224 -0
- package/document.ts +194 -0
- package/environment.ts +110 -0
- package/environments/bun-plugins.ts +68 -0
- package/environments/bun-publish.ts +194 -0
- package/environments/bun.ts +223 -0
- package/globals.d.ts +14 -0
- package/graph.ts +153 -0
- package/host/compiler.ts +314 -0
- package/host/create.ts +66 -0
- package/host/engine.ts +630 -0
- package/host/plugin-host.ts +487 -0
- package/host/render.ts +102 -0
- package/host/response.ts +20 -0
- package/host/shell.html +15 -0
- package/host/source-sync.ts +254 -0
- package/html-attributes.ts +15 -0
- package/html.ts +48 -0
- package/icons/LICENSE +43 -0
- package/icons/lucide.ts +54 -0
- package/index.ts +75 -0
- package/invalidation.ts +85 -0
- package/log.ts +84 -0
- package/ops/href.ts +87 -0
- package/ops/index.ts +9 -0
- package/ops/move-source.ts +114 -0
- package/ops/plan.ts +47 -0
- package/ops/remove-source.ts +70 -0
- package/package.json +46 -0
- package/path.ts +75 -0
- package/pattern.ts +314 -0
- package/perf.ts +88 -0
- package/plugins/actor-path.ts +43 -0
- package/plugins/actor-profile.ts +55 -0
- package/plugins/agent-browsing.ts +183 -0
- package/plugins/app-component.ts +23 -0
- package/plugins/block-layout/index.ts +78 -0
- package/plugins/block-layout/theme.css +90 -0
- package/plugins/callout/index.ts +236 -0
- package/plugins/callout/theme.css +138 -0
- package/plugins/canonical-links.ts +144 -0
- package/plugins/code-highlight/client.ts +89 -0
- package/plugins/code-highlight/index.ts +147 -0
- package/plugins/code-highlight/theme.css +89 -0
- package/plugins/context.ts +131 -0
- package/plugins/datatype/Datatype[wdth,wght].woff2 +0 -0
- package/plugins/datatype/OFL.txt +96 -0
- package/plugins/datatype/index.ts +287 -0
- package/plugins/definition-list.ts +236 -0
- package/plugins/directive-definitions.ts +13 -0
- package/plugins/embedding/client.ts +88 -0
- package/plugins/embedding/index.ts +43 -0
- package/plugins/image/index.ts +68 -0
- package/plugins/image/theme.css +43 -0
- package/plugins/include.ts +119 -0
- package/plugins/index.ts +91 -0
- package/plugins/input/authoring.ts +236 -0
- package/plugins/input/config.ts +85 -0
- package/plugins/input/index.ts +359 -0
- package/plugins/input/model.ts +173 -0
- package/plugins/input/state.ts +272 -0
- package/plugins/layout.ts +313 -0
- package/plugins/literate/client.ts +208 -0
- package/plugins/literate/index.ts +742 -0
- package/plugins/math/index.ts +140 -0
- package/plugins/meta.ts +42 -0
- package/plugins/navigation/index.ts +158 -0
- package/plugins/navigation/model.ts +364 -0
- package/plugins/preset.ts +65 -0
- package/plugins/query/index.ts +322 -0
- package/plugins/rss/index.ts +46 -0
- package/plugins/runtime/actions.ts +62 -0
- package/plugins/runtime/client.ts +8 -0
- package/plugins/runtime/document.ts +506 -0
- package/plugins/runtime/error.ts +154 -0
- package/plugins/runtime/heading-links.ts +31 -0
- package/plugins/runtime/index.ts +104 -0
- package/plugins/runtime/live.ts +51 -0
- package/plugins/runtime/navigation.ts +113 -0
- package/plugins/runtime/pending.ts +53 -0
- package/plugins/runtime/publication.ts +19 -0
- package/plugins/runtime/start.ts +177 -0
- package/plugins/runtime/static-client.ts +6 -0
- package/plugins/runtime/static-live.ts +188 -0
- package/plugins/runtime/view-transition.ts +72 -0
- package/plugins/runtime/viewport.ts +110 -0
- package/plugins/schema.ts +24 -0
- package/plugins/scripts.ts +140 -0
- package/plugins/source-preview/client.ts +256 -0
- package/plugins/source-preview/index.ts +132 -0
- package/plugins/source-preview/model.ts +11 -0
- package/plugins/source-preview/preview.css +107 -0
- package/plugins/styles.ts +145 -0
- package/plugins/theme/components.ts +10 -0
- package/plugins/theme/foundation.css +358 -0
- package/plugins/theme/index.ts +333 -0
- package/plugins/theme/prose.css +284 -0
- package/plugins/theme/scheme.js +63 -0
- package/plugins/theme/toggle.css +44 -0
- package/plugins/theme/view-transition.css +68 -0
- package/plugins/toc/client.ts +146 -0
- package/plugins/toc/index.ts +232 -0
- package/plugins/toc/theme.css +172 -0
- package/plugins/types.ts +538 -0
- package/problem.ts +41 -0
- package/project.ts +6 -0
- package/publish/build.ts +402 -0
- package/publish/concurrent.ts +32 -0
- package/publish/identity.ts +63 -0
- package/publish/index.ts +11 -0
- package/publish/location.ts +167 -0
- package/publish/model.ts +58 -0
- package/publish/plan.ts +300 -0
- package/publish/validate.ts +150 -0
- package/query/candidate-index.ts +101 -0
- package/query/dependency-index.ts +99 -0
- package/query/fields.ts +216 -0
- package/query/invalidation.ts +48 -0
- package/query/kernel.ts +776 -0
- package/query/model.ts +79 -0
- package/repository.ts +374 -0
- package/runtime-profile.ts +34 -0
- package/schema/model.ts +92 -0
- package/schema/registry.ts +642 -0
- package/schema/standard.ts +226 -0
- package/server/context.ts +94 -0
- package/server/css-imports.ts +66 -0
- package/server/http.ts +333 -0
- package/server/page.ts +21 -0
- package/server/protocols.ts +12 -0
- package/server/representations.ts +66 -0
- package/server/router.ts +131 -0
- package/server/routes.ts +202 -0
- package/server/serve.ts +5 -0
- package/server/watch.ts +120 -0
- package/source/blocks.ts +71 -0
- package/source/format.ts +164 -0
- package/source/index.ts +67 -0
- package/source/interpolate.ts +63 -0
- package/source/markdown/blocks.ts +46 -0
- package/source/markdown/comments.ts +32 -0
- package/source/markdown/fences.ts +67 -0
- package/source/markdown/headings.ts +88 -0
- package/source/markdown/index.ts +34 -0
- package/source/markdown/inline-code.ts +6 -0
- package/source/markdown/links.ts +326 -0
- package/source/markdown/lists.ts +223 -0
- package/source/markdown/prose.ts +74 -0
- package/source/markdown/scan.ts +79 -0
- package/source/markdown/text.ts +82 -0
- package/source/meta.ts +87 -0
- package/source/source.ts +121 -0
- package/source/topmatter.ts +57 -0
- package/source/utils.ts +21 -0
- package/source-diagnostic.ts +63 -0
- package/text-file.ts +26 -0
- package/util.ts +35 -0
- package/view/components.ts +499 -0
- package/view/model.ts +91 -0
- package/view/projection.ts +162 -0
- package/view/session.ts +146 -0
- 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
|
+
}
|