@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,68 @@
1
+ import { parseDirectiveLabel } from "../../source/markdown/links.ts"
2
+ import type { AppContext, Plugin } from "../types.ts"
3
+ import stylesheet from "./theme.css" with { type: "text" }
4
+
5
+ type ImageRole = "content" | "background"
6
+ type ImageFit = "contain" | "cover"
7
+
8
+ type ImageDirective = Readonly<{
9
+ role: ImageRole
10
+ fit: ImageFit
11
+ }>
12
+
13
+ const IMAGE_DIRECTIVES: Readonly<Record<string, ImageDirective>> =
14
+ Object.freeze({
15
+ fit: { role: "content", fit: "contain" },
16
+ fill: { role: "content", fit: "cover" },
17
+ background: { role: "background", fit: "cover" },
18
+ "background.fit": { role: "background", fit: "contain" },
19
+ })
20
+
21
+ function imageDirective(alt: string) {
22
+ const parsed = parseDirectiveLabel(alt)
23
+ if (parsed.directive?.name.toLowerCase() !== "image") return
24
+ const qualifiers = parsed.directive.qualifiers
25
+ .map((qualifier) => qualifier.toLowerCase())
26
+ .join(".")
27
+ const directive = IMAGE_DIRECTIVES[qualifiers]
28
+ if (!directive) return
29
+ return { ...directive, alt: parsed.label ?? "" }
30
+ }
31
+
32
+ function rewriteImages(html: string, app: AppContext) {
33
+ if (!html.includes("<img")) return html
34
+ return app.html.rewrite(html, [
35
+ [
36
+ "img",
37
+ {
38
+ element(image) {
39
+ if (image.getAttribute("data-pmx-image")) return
40
+ const directive = imageDirective(image.getAttribute("alt") ?? "")
41
+ if (!directive) return
42
+ image.setAttribute("alt", directive.alt)
43
+ image.setAttribute("data-pmx-image", directive.role)
44
+ image.setAttribute("data-pmx-image-fit", directive.fit)
45
+ },
46
+ },
47
+ ],
48
+ ])
49
+ }
50
+
51
+ /** Compile portable Markdown image directives for any PathMX experience. */
52
+ export function ImagePlugin(): Plugin {
53
+ return {
54
+ id: "image",
55
+ postcompile(html, _ctx, app) {
56
+ return rewriteImages(html, app)
57
+ },
58
+ page(html, ctx) {
59
+ if (!html.includes("data-pmx-image")) return html
60
+ ctx.addAsset({
61
+ type: "stylesheet",
62
+ name: "image.css",
63
+ text: stylesheet,
64
+ })
65
+ return html
66
+ },
67
+ }
68
+ }
@@ -0,0 +1,43 @@
1
+ .pmx-document > [data-pmx-block]:has(> img[data-pmx-image="background"]),
2
+ .pmx-document
3
+ > [data-pmx-block]:has(> p > img[data-pmx-image="background"]:only-child) {
4
+ position: relative;
5
+ isolation: isolate;
6
+ }
7
+
8
+ .pmx-document
9
+ > [data-pmx-block]
10
+ > :is(
11
+ img[data-pmx-image="background"],
12
+ p:has(> img[data-pmx-image="background"]:only-child)
13
+ ) {
14
+ position: absolute;
15
+ z-index: -1;
16
+ inset: 0;
17
+ max-inline-size: none;
18
+ margin: 0;
19
+ overflow: hidden;
20
+ }
21
+
22
+ .pmx-document
23
+ > [data-pmx-block]
24
+ > :is(
25
+ img[data-pmx-image="background"],
26
+ p:has(> img[data-pmx-image="background"]:only-child)
27
+ )
28
+ > img,
29
+ .pmx-document > [data-pmx-block] > img[data-pmx-image="background"] {
30
+ display: block;
31
+ inline-size: 100%;
32
+ block-size: 100%;
33
+ max-block-size: none;
34
+ border-radius: 0;
35
+ }
36
+
37
+ img[data-pmx-image-fit="contain"] {
38
+ object-fit: contain;
39
+ }
40
+
41
+ img[data-pmx-image-fit="cover"] {
42
+ object-fit: cover;
43
+ }
@@ -0,0 +1,119 @@
1
+ import type { Source, SourceBlock } from "../source/index.ts"
2
+ import { wrapBlockFlow } from "../document.ts"
3
+ import { escapeHtml } from "../util.ts"
4
+ import type { AppContext, DependencyContext, Plugin } from "./types.ts"
5
+
6
+ const INCLUDE_SLOT = /<div data-pathmx-include="(\d+)"><\/div>/g
7
+
8
+ function escapeRegex(value: string) {
9
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
10
+ }
11
+
12
+ function includeMarkup(href: string) {
13
+ const dest = escapeRegex(href)
14
+ return new RegExp(
15
+ `\\[@include(?:\\.[A-Za-z][\\w-]*)*(?:\\s+[^\\]]+)?\\]\\(\\s*(?:<${dest}>|${dest})\\)`,
16
+ )
17
+ }
18
+
19
+ function resolveTarget(source: Source, target: string) {
20
+ return target.startsWith("/")
21
+ ? target
22
+ : new URL(target, `file://${source.path}`).pathname
23
+ }
24
+
25
+ function includeLabel(resolved: string, anchor?: string) {
26
+ return anchor ? `${resolved}#${anchor}` : resolved
27
+ }
28
+
29
+ function includedSectionId(blockPath: string) {
30
+ return `include-${encodeURIComponent(blockPath)}`
31
+ }
32
+
33
+ function occupiesPageBlock(markdown: string, href: string, scope: string) {
34
+ if (scope !== "page" || href.includes("#")) return false
35
+ const value = markdown.trim()
36
+ const match = value.match(includeMarkup(href))
37
+ return match?.index === 0 && match[0] === value
38
+ }
39
+
40
+ async function compileIncludedBlock(
41
+ included: Source,
42
+ block: SourceBlock,
43
+ app: AppContext,
44
+ dependencies: DependencyContext,
45
+ ) {
46
+ return app.compile.block(included, block, dependencies, {
47
+ id: includedSectionId(block.path),
48
+ })
49
+ }
50
+
51
+ async function compileInclude(
52
+ source: Source,
53
+ href: string,
54
+ app: AppContext,
55
+ dependencies: DependencyContext,
56
+ ) {
57
+ const hash = href.indexOf("#")
58
+ const target = hash === -1 ? href : href.slice(0, hash)
59
+ const anchor = hash === -1 ? undefined : href.slice(hash + 1)
60
+ const resolved = resolveTarget(source, target)
61
+ dependencies.depend({ type: "source", path: resolved })
62
+ const included = app.sources.get(resolved)
63
+ if (!included) {
64
+ return `<p>[Broken Include: ${escapeHtml(includeLabel(resolved, anchor))}]</p>`
65
+ }
66
+ if (!app.access.canEmbed(source.id, included.id)) {
67
+ return `<p>[Unavailable Include: ${escapeHtml(includeLabel(resolved, anchor))}]</p>`
68
+ }
69
+ if (anchor === undefined) {
70
+ if (!included.blocks.length) {
71
+ return `<p>[Broken Include: ${escapeHtml(resolved)}]</p>`
72
+ }
73
+ const parts: string[] = []
74
+ for (const block of included.blocks) {
75
+ parts.push(await compileIncludedBlock(included, block, app, dependencies))
76
+ }
77
+ return parts.join("\n")
78
+ }
79
+ const block = included.blocks.find((item) => item.id === anchor)
80
+ if (!block) {
81
+ return `<p>[Broken Include: ${escapeHtml(`${resolved}#${anchor}`)}]</p>`
82
+ }
83
+ return compileIncludedBlock(included, block, app, dependencies)
84
+ }
85
+
86
+ export function IncludePlugin(): Plugin {
87
+ return {
88
+ id: "include",
89
+ async precompile(markdown, ctx, app) {
90
+ const includes = (ctx.block?.links ?? ctx.source.links).filter(
91
+ (link) => link.directive?.name === "include",
92
+ )
93
+ if (!includes.length) return markdown
94
+
95
+ let current = markdown
96
+ for (const link of includes) {
97
+ const compiled = await compileInclude(ctx.source, link.href, app, ctx)
98
+ const expandsBlockFlow =
99
+ includes.length === 1 &&
100
+ occupiesPageBlock(markdown, link.href, ctx.scope) &&
101
+ compiled.trimStart().startsWith("<section ")
102
+ ctx.slots.push(expandsBlockFlow ? wrapBlockFlow(compiled) : compiled)
103
+ current = current.replace(
104
+ includeMarkup(link.href),
105
+ `\n\n<div data-pathmx-include="${ctx.slots.length - 1}"></div>\n\n`,
106
+ )
107
+ }
108
+ return current
109
+ },
110
+
111
+ postcompile(html, ctx) {
112
+ if (!ctx.slots.length) return html
113
+ return html.replace(
114
+ INCLUDE_SLOT,
115
+ (_match, index) => ctx.slots[Number(index)] ?? "",
116
+ )
117
+ },
118
+ }
119
+ }
@@ -0,0 +1,91 @@
1
+ import type { PluginFactory } from "./types.ts"
2
+ import { CalloutPlugin } from "./callout/index.ts"
3
+ import { DefinitionListPlugin } from "./definition-list.ts"
4
+ import { IncludePlugin } from "./include.ts"
5
+ import { CodeHighlightPlugin } from "./code-highlight/index.ts"
6
+ import { MathPlugin } from "./math/index.ts"
7
+ import { DatatypePlugin } from "./datatype/index.ts"
8
+ import { StylesPlugin } from "./styles.ts"
9
+ import { ScriptsPlugin } from "./scripts.ts"
10
+ import { ComponentTemplatePlugin } from "./literate/index.ts"
11
+ import { ActorPathPlugin } from "./actor-path.ts"
12
+ import { ActorProfilePlugin } from "./actor-profile.ts"
13
+ import { CanonicalLinkPlugin } from "./canonical-links.ts"
14
+ import { SourcePreviewPlugin } from "./source-preview/index.ts"
15
+ import { TableOfContentsPlugin } from "./toc/index.ts"
16
+ import { ThemePlugin } from "./theme/index.ts"
17
+ import { RuntimePlugin } from "./runtime/index.ts"
18
+ import { LayoutPlugin } from "./layout.ts"
19
+ import { NavigationPlugin } from "./navigation/index.ts"
20
+ import { InputPlugin, createInputPlugin } from "./input/index.ts"
21
+ import { AgentBrowsingPlugin } from "./agent-browsing.ts"
22
+ import { QueryPlugin } from "./query/index.ts"
23
+ import { EmbeddingPlugin } from "./embedding/index.ts"
24
+ import { ImagePlugin } from "./image/index.ts"
25
+ import { BlockLayoutPlugin } from "./block-layout/index.ts"
26
+
27
+ export {
28
+ AgentBrowsingPlugin,
29
+ ActorPathPlugin,
30
+ ActorProfilePlugin,
31
+ BlockLayoutPlugin,
32
+ CanonicalLinkPlugin,
33
+ CalloutPlugin,
34
+ CodeHighlightPlugin,
35
+ ComponentTemplatePlugin,
36
+ DatatypePlugin,
37
+ DefinitionListPlugin,
38
+ EmbeddingPlugin,
39
+ IncludePlugin,
40
+ ImagePlugin,
41
+ InputPlugin,
42
+ createInputPlugin,
43
+ LayoutPlugin,
44
+ MathPlugin,
45
+ QueryPlugin,
46
+ NavigationPlugin,
47
+ RuntimePlugin,
48
+ ScriptsPlugin,
49
+ SourcePreviewPlugin,
50
+ StylesPlugin,
51
+ TableOfContentsPlugin,
52
+ ThemePlugin,
53
+ }
54
+ export type { InputAuthoring, InputAuthoringResult } from "./input/authoring.ts"
55
+ export type { InputControl } from "./input/model.ts"
56
+ export type {
57
+ CanonicalLinkPluginOptions,
58
+ ExternalWebLinks,
59
+ } from "./canonical-links.ts"
60
+ export type {
61
+ EmbeddingExternalLinks,
62
+ EmbeddingPluginOptions,
63
+ } from "./embedding/index.ts"
64
+
65
+ /** Standard PathMX experience layered over createApp's portable foundation. */
66
+ export const defaultPlugins = Object.freeze([
67
+ CalloutPlugin,
68
+ QueryPlugin,
69
+ AgentBrowsingPlugin,
70
+ ThemePlugin,
71
+ DefinitionListPlugin,
72
+ IncludePlugin,
73
+ ImagePlugin,
74
+ BlockLayoutPlugin,
75
+ CodeHighlightPlugin,
76
+ MathPlugin,
77
+ DatatypePlugin,
78
+ StylesPlugin,
79
+ ScriptsPlugin,
80
+ TableOfContentsPlugin,
81
+ ComponentTemplatePlugin,
82
+ InputPlugin,
83
+ CanonicalLinkPlugin,
84
+ SourcePreviewPlugin,
85
+ LayoutPlugin,
86
+ NavigationPlugin,
87
+ EmbeddingPlugin,
88
+ ActorProfilePlugin,
89
+ ActorPathPlugin,
90
+ RuntimePlugin,
91
+ ] satisfies readonly PluginFactory[])
@@ -0,0 +1,236 @@
1
+ import {
2
+ formatSchemaIssues,
3
+ type StandardSchemaV1,
4
+ } from "../../schema/standard.ts"
5
+ import type { Source } from "../../source/index.ts"
6
+ import { authoredViewComponentUses } from "../../view/syntax.ts"
7
+ import type { ViewComponentUse } from "../types.ts"
8
+ import { inputResponseConfig } from "./config.ts"
9
+ import type { InputChoice, InputControl, InputText } from "./model.ts"
10
+
11
+ export type InputAuthoringResult = Readonly<{
12
+ controls: readonly InputControl[]
13
+ completion?: Readonly<{ key: string }>
14
+ issues?: readonly string[]
15
+ }>
16
+
17
+ export type InputAuthoring = Readonly<{
18
+ inspect(source: Source): InputAuthoringResult | undefined
19
+ }>
20
+
21
+ type Props = Readonly<Record<string, string>>
22
+
23
+ function propsSchema(
24
+ required: readonly string[],
25
+ optional: readonly string[] = [],
26
+ ): StandardSchemaV1<unknown, Props> {
27
+ const allowed = new Set([...required, ...optional])
28
+ return {
29
+ "~standard": {
30
+ version: 1,
31
+ vendor: "pathmx",
32
+ validate(value) {
33
+ const props = value as Record<string, unknown>
34
+ const issues: StandardSchemaV1.Issue[] = []
35
+ for (const name of required) {
36
+ if (typeof props?.[name] !== "string" || !props[name]) {
37
+ issues.push({ message: "Required", path: [name] })
38
+ }
39
+ }
40
+ for (const name of Object.keys(props ?? {})) {
41
+ if (!allowed.has(name)) {
42
+ issues.push({ message: "Unknown prop", path: [name] })
43
+ }
44
+ }
45
+ return issues.length ? { issues } : { value: props as Props }
46
+ },
47
+ },
48
+ }
49
+ }
50
+
51
+ export const optionProps = propsSchema(
52
+ ["name", "value", "label"],
53
+ ["required", "min"],
54
+ )
55
+ export const textProps = propsSchema(
56
+ ["name", "label"],
57
+ ["placeholder", "required"],
58
+ )
59
+ export const submitProps = propsSchema(["label"])
60
+
61
+ export function optionFrom(
62
+ use: ViewComponentUse,
63
+ type: InputChoice["type"],
64
+ ): InputChoice {
65
+ const min =
66
+ use.props.min == null || use.props.min === "" ? 0 : Number(use.props.min)
67
+ if (!Number.isInteger(min) || min < 0) {
68
+ throw new Error(`Invalid <x-input-${type}> min in ${use.origin.path}.`)
69
+ }
70
+ return {
71
+ type,
72
+ name: use.props.name!,
73
+ value: use.props.value!,
74
+ label: use.props.label!,
75
+ required: "required" in use.props,
76
+ min,
77
+ }
78
+ }
79
+
80
+ export function textFrom(
81
+ use: ViewComponentUse,
82
+ type: InputText["type"],
83
+ ): InputText {
84
+ return {
85
+ type,
86
+ name: use.props.name!,
87
+ label: use.props.label!,
88
+ ...(use.props.placeholder ? { placeholder: use.props.placeholder } : {}),
89
+ required: "required" in use.props,
90
+ }
91
+ }
92
+
93
+ function validateUse(
94
+ source: Source,
95
+ tag: string,
96
+ props: Readonly<Record<string, string>>,
97
+ schema: StandardSchemaV1,
98
+ ) {
99
+ const result = schema["~standard"].validate(props)
100
+ if (result instanceof Promise) {
101
+ throw new Error(
102
+ `Input View Component schema for <${tag}> must be synchronous.`,
103
+ )
104
+ }
105
+ return result.issues?.length
106
+ ? `Invalid <${tag}> props in ${source.path}: ${formatSchemaIssues(result.issues)}`
107
+ : undefined
108
+ }
109
+
110
+ function schemaFor(tag: string) {
111
+ if (tag === "x-input-text" || tag === "x-input-textarea") return textProps
112
+ if (tag === "x-input-radio" || tag === "x-input-checkbox") {
113
+ return optionProps
114
+ }
115
+ if (tag === "x-input-submit") return submitProps
116
+ }
117
+
118
+ function controlFrom(use: ViewComponentUse, tag: string) {
119
+ if (tag === "x-input-text") return textFrom(use, "text")
120
+ if (tag === "x-input-textarea") return textFrom(use, "textarea")
121
+ if (tag === "x-input-radio") return optionFrom(use, "radio")
122
+ if (tag === "x-input-checkbox") return optionFrom(use, "checkbox")
123
+ }
124
+
125
+ function inspectAuthoredInputSource(source: Source): InputAuthoringResult {
126
+ const controls: Array<InputChoice | InputText> = []
127
+ const issues: string[] = []
128
+ const completionBlocks: Array<Readonly<{ key: string }>> = []
129
+ const sourceConfig = inputResponseConfig(source)
130
+ if (
131
+ !sourceConfig &&
132
+ !source.body.includes("<x-input-") &&
133
+ !source.body.includes("input-response:")
134
+ ) {
135
+ return Object.freeze({
136
+ controls: Object.freeze(controls),
137
+ completion: undefined,
138
+ issues: Object.freeze(issues),
139
+ })
140
+ }
141
+ if (sourceConfig?.completion) {
142
+ issues.push(
143
+ `Input completion must be authored on a Block in ${source.path}.`,
144
+ )
145
+ }
146
+
147
+ for (const block of source.blocks) {
148
+ const config = inputResponseConfig(block)
149
+ if (config?.state) {
150
+ issues.push(
151
+ `Input state binding must be authored on the Source, not ${block.path}.`,
152
+ )
153
+ }
154
+ let recognizedUses = 0
155
+ const authoredUses = block.body.includes("<x-input-")
156
+ ? authoredViewComponentUses(block.body)
157
+ : []
158
+ for (const authored of authoredUses) {
159
+ const schema = schemaFor(authored.tag)
160
+ if (!schema) continue
161
+ recognizedUses++
162
+ const invalid = validateUse(source, authored.tag, authored.props, schema)
163
+ if (invalid) {
164
+ issues.push(invalid)
165
+ continue
166
+ }
167
+ const use: ViewComponentUse = { props: authored.props, origin: source }
168
+ try {
169
+ const control = controlFrom(use, authored.tag)
170
+ if (control) controls.push(control)
171
+ } catch (error) {
172
+ issues.push(error instanceof Error ? error.message : String(error))
173
+ }
174
+ }
175
+
176
+ if (!config?.completion) continue
177
+ const authoredId =
178
+ typeof block.data.id === "string" && block.data.id.trim() === block.id
179
+ if (!authoredId) {
180
+ issues.push(
181
+ `Input completion Block ${block.path} must author a stable id.`,
182
+ )
183
+ continue
184
+ }
185
+ const key = `input/${block.id}`
186
+ if (key.length > 256 || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(block.id)) {
187
+ issues.push(
188
+ `Invalid Input completion Block id ${block.id} in ${source.path}.`,
189
+ )
190
+ continue
191
+ }
192
+ if (!recognizedUses) {
193
+ issues.push(`Input completion Block ${block.path} has no Input controls.`)
194
+ continue
195
+ }
196
+ completionBlocks.push(Object.freeze({ key }))
197
+ }
198
+ if (completionBlocks.length > 1) {
199
+ issues.push(
200
+ `Input Source ${source.path} may declare only one completion Block.`,
201
+ )
202
+ }
203
+ if (completionBlocks.length && !controls.length) {
204
+ issues.push(`Input Source ${source.path} has no Input fields to complete.`)
205
+ }
206
+ return Object.freeze({
207
+ controls: Object.freeze(controls),
208
+ completion: completionBlocks.length === 1 ? completionBlocks[0] : undefined,
209
+ issues: Object.freeze(issues),
210
+ })
211
+ }
212
+
213
+ export function inspectInputSource(
214
+ source: Source,
215
+ authoring: readonly InputAuthoring[] = [],
216
+ ) {
217
+ const inspected = [
218
+ inspectAuthoredInputSource(source),
219
+ ...authoring.flatMap((contributor) => {
220
+ const contribution = contributor.inspect(source)
221
+ return contribution ? [contribution] : []
222
+ }),
223
+ ]
224
+ const completions = inspected.flatMap(({ completion }) =>
225
+ completion ? [completion] : [],
226
+ )
227
+ const issues = inspected.flatMap(({ issues }) => issues ?? [])
228
+ if (completions.length > 1) {
229
+ issues.push(`Input Source ${source.path} has multiple completion units.`)
230
+ }
231
+ return Object.freeze({
232
+ controls: Object.freeze(inspected.flatMap(({ controls }) => [...controls])),
233
+ completion: completions.length === 1 ? completions[0] : undefined,
234
+ issues: Object.freeze(issues),
235
+ })
236
+ }
@@ -0,0 +1,85 @@
1
+ import { canonicalSourceId } from "../../canonical.ts"
2
+ import type { PathMXDataSchema } from "../../schema/standard.ts"
3
+ import type { SourceNode } from "../../source/index.ts"
4
+
5
+ export type InputResponseConfig = Readonly<{
6
+ state?: string
7
+ completion?: true
8
+ }>
9
+
10
+ function issue(message: string, path?: string) {
11
+ return {
12
+ issues: [{ message, ...(path ? { path: [path] } : {}) }],
13
+ }
14
+ }
15
+
16
+ function parseInputResponseConfig(value: unknown) {
17
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
18
+ return issue("Expected an object")
19
+ }
20
+ const inputResponse = value as Record<string, unknown>
21
+ const extra = Object.keys(inputResponse).find(
22
+ (key) => key !== "state" && key !== "completion",
23
+ )
24
+ if (extra) return issue("Unknown Input Response field", extra)
25
+
26
+ let state: string | undefined
27
+ if (inputResponse.state !== undefined) {
28
+ if (
29
+ typeof inputResponse.state !== "string" ||
30
+ !inputResponse.state.trim()
31
+ ) {
32
+ return issue("Expected a canonical Source id", "state")
33
+ }
34
+ state = inputResponse.state.trim()
35
+ try {
36
+ canonicalSourceId(state)
37
+ } catch {
38
+ return issue("Expected a canonical Source id", "state")
39
+ }
40
+ }
41
+ if (
42
+ inputResponse.completion !== undefined &&
43
+ inputResponse.completion !== true
44
+ ) {
45
+ return issue("Expected true", "completion")
46
+ }
47
+ return {
48
+ value: Object.freeze({
49
+ ...(state ? { state } : {}),
50
+ ...(inputResponse.completion === true
51
+ ? { completion: true as const }
52
+ : {}),
53
+ }),
54
+ }
55
+ }
56
+
57
+ const InputResponseJsonSchema = Object.freeze({
58
+ type: "object",
59
+ additionalProperties: false,
60
+ properties: {
61
+ state: { type: "string", pattern: "^/" },
62
+ completion: { const: true },
63
+ },
64
+ })
65
+
66
+ export const InputResponsePropSchema: PathMXDataSchema<
67
+ unknown,
68
+ InputResponseConfig
69
+ > = {
70
+ "~standard": {
71
+ version: 1,
72
+ vendor: "pathmx",
73
+ validate: parseInputResponseConfig,
74
+ jsonSchema: {
75
+ input: () => InputResponseJsonSchema,
76
+ output: () => InputResponseJsonSchema,
77
+ },
78
+ },
79
+ }
80
+
81
+ export function inputResponseConfig(node: Pick<SourceNode, "data">) {
82
+ if (!Object.hasOwn(node.data, "input-response")) return
83
+ const result = parseInputResponseConfig(node.data["input-response"])
84
+ return "value" in result ? result.value : undefined
85
+ }