@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,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
|
+
}
|
package/plugins/index.ts
ADDED
|
@@ -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
|
+
}
|