@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,65 @@
|
|
|
1
|
+
import type { Plugin, PluginFactory } from "./types.ts"
|
|
2
|
+
|
|
3
|
+
/** An ordered, nestable composition recipe with no runtime identity of its own. */
|
|
4
|
+
export type PluginPreset = readonly PluginContribution[]
|
|
5
|
+
|
|
6
|
+
export type PluginContribution = Plugin | PluginFactory | PluginPreset
|
|
7
|
+
|
|
8
|
+
type PluginLeaf = Plugin | PluginFactory
|
|
9
|
+
|
|
10
|
+
/** Define an immutable Plugin composition recipe. */
|
|
11
|
+
export function definePreset(
|
|
12
|
+
contributions: readonly PluginContribution[],
|
|
13
|
+
): PluginPreset {
|
|
14
|
+
return Object.freeze([...contributions])
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isPlugin(value: unknown): value is Plugin {
|
|
18
|
+
return (
|
|
19
|
+
typeof value === "object" &&
|
|
20
|
+
value !== null &&
|
|
21
|
+
"id" in value &&
|
|
22
|
+
typeof value.id === "string"
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Flatten nested presets in declaration order before Plugin registration. */
|
|
27
|
+
export function expandPluginContributions(
|
|
28
|
+
contributions: readonly unknown[],
|
|
29
|
+
): readonly PluginLeaf[] {
|
|
30
|
+
const leaves: PluginLeaf[] = []
|
|
31
|
+
const active = new Set<readonly unknown[]>()
|
|
32
|
+
|
|
33
|
+
function visit(contribution: unknown, location: string) {
|
|
34
|
+
if (Array.isArray(contribution)) {
|
|
35
|
+
if (active.has(contribution)) {
|
|
36
|
+
throw new Error(`Cyclic PluginPreset at ${location}.`)
|
|
37
|
+
}
|
|
38
|
+
active.add(contribution)
|
|
39
|
+
contribution.forEach((nested, index) =>
|
|
40
|
+
visit(nested, `${location}[${index}]`),
|
|
41
|
+
)
|
|
42
|
+
active.delete(contribution)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
if (typeof contribution === "function" || isPlugin(contribution)) {
|
|
46
|
+
leaves.push(contribution as PluginLeaf)
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Invalid Plugin contribution at ${location}: expected a Plugin, PluginFactory, or PluginPreset.`,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
contributions.forEach((contribution, index) =>
|
|
55
|
+
visit(contribution, `plugins[${index}]`),
|
|
56
|
+
)
|
|
57
|
+
return Object.freeze(leaves)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function instantiatePlugin(plugin: PluginLeaf): Plugin {
|
|
61
|
+
if (typeof plugin !== "function") return plugin
|
|
62
|
+
const instance = plugin()
|
|
63
|
+
if (!instance.name && plugin.name) instance.name = plugin.name
|
|
64
|
+
return instance
|
|
65
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { posixPath } from "../../path.ts"
|
|
2
|
+
import { queryRecordValue, type QueryField } from "../../query/fields.ts"
|
|
3
|
+
import { queryDependency, queryPlanFields } from "../../query/kernel.ts"
|
|
4
|
+
import type { QueryRequest, SourceQueryRequest } from "../../query/model.ts"
|
|
5
|
+
import { isFenceClose, matchFenceOpen } from "../../source/index.ts"
|
|
6
|
+
import { escapeHtml } from "../../util.ts"
|
|
7
|
+
import { authoredViewTag } from "../../view/syntax.ts"
|
|
8
|
+
import {
|
|
9
|
+
defineViewComponent,
|
|
10
|
+
type AppContext,
|
|
11
|
+
type CompileContext,
|
|
12
|
+
type PageContext,
|
|
13
|
+
type Plugin,
|
|
14
|
+
type ViewContext,
|
|
15
|
+
} from "../types.ts"
|
|
16
|
+
|
|
17
|
+
const QUERY_COMPONENT = "query:results"
|
|
18
|
+
const QUERY_TAG = authoredViewTag(QUERY_COMPONENT)
|
|
19
|
+
|
|
20
|
+
type StockView =
|
|
21
|
+
| Readonly<{ type: "list" }>
|
|
22
|
+
| Readonly<{ type: "table"; columns: readonly string[] }>
|
|
23
|
+
| Readonly<{
|
|
24
|
+
type: "components"
|
|
25
|
+
component: string
|
|
26
|
+
props: Readonly<Record<string, string>>
|
|
27
|
+
}>
|
|
28
|
+
|
|
29
|
+
type CompiledStockView =
|
|
30
|
+
| Readonly<{ type: "list" }>
|
|
31
|
+
| Readonly<{ type: "table"; columns: readonly QueryField[] }>
|
|
32
|
+
| (Extract<StockView, { type: "components" }> &
|
|
33
|
+
Readonly<{ prepared: unknown }>)
|
|
34
|
+
|
|
35
|
+
type CompiledQuery = Readonly<{
|
|
36
|
+
query: string
|
|
37
|
+
request: QueryRequest<unknown>
|
|
38
|
+
view: CompiledStockView
|
|
39
|
+
}>
|
|
40
|
+
|
|
41
|
+
function record(value: unknown, subject: string) {
|
|
42
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) {
|
|
43
|
+
throw new Error(`${subject} must be an object.`)
|
|
44
|
+
}
|
|
45
|
+
return value as Record<string, unknown>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function componentProps(value: unknown) {
|
|
49
|
+
if (value === undefined) return Object.freeze({})
|
|
50
|
+
const props = record(value, "Query component props")
|
|
51
|
+
return Object.freeze(
|
|
52
|
+
Object.fromEntries(
|
|
53
|
+
Object.entries(props).map(([key, item]) => {
|
|
54
|
+
if (
|
|
55
|
+
item == null ||
|
|
56
|
+
(typeof item !== "string" &&
|
|
57
|
+
typeof item !== "number" &&
|
|
58
|
+
typeof item !== "boolean")
|
|
59
|
+
) {
|
|
60
|
+
throw new Error(`Query component prop ${key} must be a scalar.`)
|
|
61
|
+
}
|
|
62
|
+
return [key, String(item)]
|
|
63
|
+
}),
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function requireKeys(
|
|
69
|
+
value: Readonly<Record<string, unknown>>,
|
|
70
|
+
allowed: readonly string[],
|
|
71
|
+
subject: string,
|
|
72
|
+
) {
|
|
73
|
+
for (const key of Object.keys(value)) {
|
|
74
|
+
if (!allowed.includes(key))
|
|
75
|
+
throw new Error(`Unknown ${subject} field: ${key}`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function viewConfig(value: unknown): StockView {
|
|
80
|
+
if (value === undefined) return Object.freeze({ type: "list" })
|
|
81
|
+
if (typeof value === "string") {
|
|
82
|
+
if (value === "list") return Object.freeze({ type: "list" })
|
|
83
|
+
throw new Error(`Unsupported query view: ${value}`)
|
|
84
|
+
}
|
|
85
|
+
const config = record(value, "Query view")
|
|
86
|
+
const type = config.type ?? "list"
|
|
87
|
+
if (type === "list") {
|
|
88
|
+
requireKeys(config, ["type"], "query list view")
|
|
89
|
+
return Object.freeze({ type })
|
|
90
|
+
}
|
|
91
|
+
if (type === "table") {
|
|
92
|
+
requireKeys(config, ["type", "columns"], "query table view")
|
|
93
|
+
const columns = config.columns ?? []
|
|
94
|
+
if (
|
|
95
|
+
!Array.isArray(columns) ||
|
|
96
|
+
columns.some((column) => typeof column !== "string" || !column.trim())
|
|
97
|
+
) {
|
|
98
|
+
throw new Error("Query table columns must be a list of field names.")
|
|
99
|
+
}
|
|
100
|
+
return Object.freeze({
|
|
101
|
+
type,
|
|
102
|
+
columns: Object.freeze(columns.map((column) => column.trim())),
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
if (type === "components") {
|
|
106
|
+
requireKeys(config, ["type", "component", "props"], "query components view")
|
|
107
|
+
if (typeof config.component !== "string" || !config.component.trim()) {
|
|
108
|
+
throw new Error("Query components view requires component.")
|
|
109
|
+
}
|
|
110
|
+
return Object.freeze({
|
|
111
|
+
type,
|
|
112
|
+
component: config.component.trim(),
|
|
113
|
+
props: componentProps(config.props),
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
throw new Error(`Unsupported query view: ${String(type)}`)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function authoredScope(value: unknown, ctx: CompileContext) {
|
|
120
|
+
const base = posixPath.dirname(ctx.source.path)
|
|
121
|
+
if (value === undefined || value === ".") return base
|
|
122
|
+
if (typeof value !== "string") throw new Error("Query from must be a string.")
|
|
123
|
+
if (value.startsWith("~/") || value.startsWith("/")) return value
|
|
124
|
+
return posixPath.resolve(base, value)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseConfig(body: string, ctx: CompileContext, app: AppContext) {
|
|
128
|
+
const parsed = record(app.yaml.parse(body) ?? {}, "Query fence")
|
|
129
|
+
const query = parsed.query ?? "sources"
|
|
130
|
+
if (typeof query !== "string" || !query.trim()) {
|
|
131
|
+
throw new Error("Query fence query must be a string.")
|
|
132
|
+
}
|
|
133
|
+
const view = viewConfig(parsed.view)
|
|
134
|
+
const request: Record<string, unknown> = {
|
|
135
|
+
...parsed,
|
|
136
|
+
from: authoredScope(parsed.from, ctx),
|
|
137
|
+
}
|
|
138
|
+
delete request.query
|
|
139
|
+
delete request.view
|
|
140
|
+
return { query: query.trim(), request, view }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function compileQueryFence(
|
|
144
|
+
config: ReturnType<typeof parseConfig>,
|
|
145
|
+
ctx: PageContext,
|
|
146
|
+
app: AppContext,
|
|
147
|
+
) {
|
|
148
|
+
const plan = await app.query.prepare(config.query, config.request)
|
|
149
|
+
ctx.depend(queryDependency(plan))
|
|
150
|
+
const base = {
|
|
151
|
+
query: config.query,
|
|
152
|
+
request: Object.freeze({ ...config.request }),
|
|
153
|
+
}
|
|
154
|
+
if (config.view.type === "table") {
|
|
155
|
+
const available = queryPlanFields(app.schema, plan)
|
|
156
|
+
const byPath = new Map(available.map((field) => [field.path, field]))
|
|
157
|
+
const paths = config.view.columns.length
|
|
158
|
+
? config.view.columns
|
|
159
|
+
: available.slice(0, 8).map(({ path }) => path)
|
|
160
|
+
const columns = paths.map((path) => {
|
|
161
|
+
const field = byPath.get(path)
|
|
162
|
+
if (!field) throw new Error(`Unknown query table column: ${path}`)
|
|
163
|
+
return field
|
|
164
|
+
})
|
|
165
|
+
return Object.freeze({
|
|
166
|
+
...base,
|
|
167
|
+
view: Object.freeze({ type: "table", columns: Object.freeze(columns) }),
|
|
168
|
+
}) satisfies CompiledQuery
|
|
169
|
+
}
|
|
170
|
+
if (config.view.type === "components") {
|
|
171
|
+
const prepared = await app.components.prepareData(
|
|
172
|
+
config.view.component,
|
|
173
|
+
config.view.props,
|
|
174
|
+
ctx.source,
|
|
175
|
+
ctx,
|
|
176
|
+
)
|
|
177
|
+
return Object.freeze({
|
|
178
|
+
...base,
|
|
179
|
+
view: Object.freeze({ ...config.view, prepared }),
|
|
180
|
+
}) satisfies CompiledQuery
|
|
181
|
+
}
|
|
182
|
+
return Object.freeze({
|
|
183
|
+
...base,
|
|
184
|
+
view: config.view,
|
|
185
|
+
}) satisfies CompiledQuery
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function transformQueryFences(
|
|
189
|
+
markdown: string,
|
|
190
|
+
ctx: CompileContext,
|
|
191
|
+
app: AppContext,
|
|
192
|
+
) {
|
|
193
|
+
if (!/query/i.test(markdown)) return markdown
|
|
194
|
+
const lines = markdown.split(/\r?\n/)
|
|
195
|
+
const output: string[] = []
|
|
196
|
+
for (let index = 0; index < lines.length; index++) {
|
|
197
|
+
const line = lines[index] ?? ""
|
|
198
|
+
const fence = matchFenceOpen(line)
|
|
199
|
+
if (!fence) {
|
|
200
|
+
output.push(line)
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
let close = index + 1
|
|
204
|
+
while (close < lines.length && !isFenceClose(lines[close]!, fence.marker)) {
|
|
205
|
+
close++
|
|
206
|
+
}
|
|
207
|
+
if (close === lines.length) {
|
|
208
|
+
output.push(...lines.slice(index))
|
|
209
|
+
break
|
|
210
|
+
}
|
|
211
|
+
const language = fence.info.split(/\s+/, 1)[0]?.toLowerCase()
|
|
212
|
+
if (language !== "query") {
|
|
213
|
+
output.push(...lines.slice(index, close + 1))
|
|
214
|
+
index = close
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
const config = parseConfig(
|
|
218
|
+
lines.slice(index + 1, close).join("\n"),
|
|
219
|
+
ctx,
|
|
220
|
+
app,
|
|
221
|
+
)
|
|
222
|
+
const encoded = encodeURIComponent(JSON.stringify(config))
|
|
223
|
+
output.push(
|
|
224
|
+
`${fence.indent}<${QUERY_TAG} config="${escapeHtml(encoded)}" />`,
|
|
225
|
+
)
|
|
226
|
+
index = close
|
|
227
|
+
}
|
|
228
|
+
return output.join("\n")
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function resultLabel(result: unknown) {
|
|
232
|
+
for (const field of ["meta.title", "title", "label", "name", "id"]) {
|
|
233
|
+
const value = queryRecordValue(result, field)
|
|
234
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
235
|
+
return String(value)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return JSON.stringify(result)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function renderList(results: readonly unknown[]) {
|
|
242
|
+
return `<ul class="pmx-query-list">${results
|
|
243
|
+
.map((result) => {
|
|
244
|
+
const label = escapeHtml(resultLabel(result))
|
|
245
|
+
const href = queryRecordValue(result, "href")
|
|
246
|
+
return `<li>${typeof href === "string" ? `<a href="${escapeHtml(href)}">${label}</a>` : label}</li>`
|
|
247
|
+
})
|
|
248
|
+
.join("")}</ul>`
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function renderTable(
|
|
252
|
+
results: readonly unknown[],
|
|
253
|
+
columns: readonly QueryField[],
|
|
254
|
+
) {
|
|
255
|
+
const headings = columns
|
|
256
|
+
.map((column) => `<th>${escapeHtml(column.label ?? column.path)}</th>`)
|
|
257
|
+
.join("")
|
|
258
|
+
const rows = results
|
|
259
|
+
.map(
|
|
260
|
+
(result) =>
|
|
261
|
+
`<tr>${columns
|
|
262
|
+
.map(({ path }) => {
|
|
263
|
+
const value = queryRecordValue(result, path)
|
|
264
|
+
const text =
|
|
265
|
+
value == null
|
|
266
|
+
? ""
|
|
267
|
+
: typeof value === "object"
|
|
268
|
+
? JSON.stringify(value)
|
|
269
|
+
: String(value)
|
|
270
|
+
return `<td>${escapeHtml(text)}</td>`
|
|
271
|
+
})
|
|
272
|
+
.join("")}</tr>`,
|
|
273
|
+
)
|
|
274
|
+
.join("")
|
|
275
|
+
return `<table class="pmx-query-table"><thead><tr>${headings}</tr></thead><tbody>${rows}</tbody></table>`
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function executeQuery(compiled: CompiledQuery, ctx: ViewContext) {
|
|
279
|
+
if (compiled.query === "sources") {
|
|
280
|
+
return ctx.view.query.sources(compiled.request as SourceQueryRequest)
|
|
281
|
+
}
|
|
282
|
+
if (compiled.query === "blocks") {
|
|
283
|
+
return ctx.view.query.blocks(compiled.request as SourceQueryRequest)
|
|
284
|
+
}
|
|
285
|
+
return ctx.view.query.run(compiled.query, compiled.request)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function QueryPlugin(): Plugin {
|
|
289
|
+
const Results = defineViewComponent({
|
|
290
|
+
tag: QUERY_COMPONENT,
|
|
291
|
+
async compile(use, ctx, app) {
|
|
292
|
+
const raw = use.props.config
|
|
293
|
+
if (!raw) throw new Error(`Missing query config in ${use.origin.path}.`)
|
|
294
|
+
const config = JSON.parse(decodeURIComponent(raw)) as ReturnType<
|
|
295
|
+
typeof parseConfig
|
|
296
|
+
>
|
|
297
|
+
return compileQueryFence(config, ctx, app)
|
|
298
|
+
},
|
|
299
|
+
async render(_use, compiled, ctx, app) {
|
|
300
|
+
const results = await executeQuery(compiled, ctx)
|
|
301
|
+
const view = compiled.view
|
|
302
|
+
if (view.type === "list") return renderList(results)
|
|
303
|
+
if (view.type === "table") {
|
|
304
|
+
return renderTable(results, view.columns)
|
|
305
|
+
}
|
|
306
|
+
const rendered = await Promise.all(
|
|
307
|
+
results.map((result) =>
|
|
308
|
+
app.components.renderData(view.prepared, result, ctx),
|
|
309
|
+
),
|
|
310
|
+
)
|
|
311
|
+
return `<div class="pmx-query-components">${rendered.join("")}</div>`
|
|
312
|
+
},
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
id: "query",
|
|
317
|
+
components: [Results],
|
|
318
|
+
precompile(markdown, ctx, app) {
|
|
319
|
+
return transformQueryFences(markdown, ctx, app)
|
|
320
|
+
},
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// @ts-nocheck just prototyping here... don't take any of this too literally...
|
|
2
|
+
|
|
3
|
+
const rssPlugin = definePlugin({
|
|
4
|
+
id: "rss",
|
|
5
|
+
name: "RSS",
|
|
6
|
+
// these routes attach to the server runtime at /api/rss/<route>
|
|
7
|
+
// I'm not sure if routes should have default scoping/perms or not?
|
|
8
|
+
routes: {
|
|
9
|
+
// default is GET
|
|
10
|
+
"/feed/:blogId": (req, context) => {
|
|
11
|
+
// not real code, just a shape/idea
|
|
12
|
+
const blogId = req.params.blogId
|
|
13
|
+
const graph = context.graph
|
|
14
|
+
const posts = graph.query({ blog: blogId, type: "post" })
|
|
15
|
+
const rss = posts.map((post) => ({
|
|
16
|
+
title: post.title,
|
|
17
|
+
description: post.description,
|
|
18
|
+
url: post.url,
|
|
19
|
+
date: post.date,
|
|
20
|
+
}))
|
|
21
|
+
return new Response(JSON.stringify(rss), {
|
|
22
|
+
headers: {
|
|
23
|
+
"Content-Type": "application/rss+xml",
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
},
|
|
27
|
+
"POST /subscribe": (req, context) => {
|
|
28
|
+
const { email } = await req.json()
|
|
29
|
+
// DB our source here?
|
|
30
|
+
context.db.insert("subscriptions").values({ email }).execute()
|
|
31
|
+
return new Response(JSON.stringify({ message: "Subscribed!" }), {
|
|
32
|
+
headers: {
|
|
33
|
+
"Content-Type": "application/json",
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
actions: {
|
|
39
|
+
post: ({ title, body, blog }, context: ActionContext) => {
|
|
40
|
+
// add a .post source
|
|
41
|
+
},
|
|
42
|
+
createBlog: ({ title }: { title: string }, context: ActionContext) => {
|
|
43
|
+
// create a new blog source
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clearActionError,
|
|
3
|
+
describeActionError,
|
|
4
|
+
readActionRejection,
|
|
5
|
+
showActionError,
|
|
6
|
+
} from "./error.ts"
|
|
7
|
+
import type { RuntimeActionInstaller } from "./start.ts"
|
|
8
|
+
|
|
9
|
+
function isActionForm(form: HTMLFormElement) {
|
|
10
|
+
try {
|
|
11
|
+
return new URL(form.action, location.href).pathname.startsWith("/actions/")
|
|
12
|
+
} catch {
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const installActions: RuntimeActionInstaller = (runtime) => {
|
|
18
|
+
document.addEventListener("submit", async (event) => {
|
|
19
|
+
const form = event.target
|
|
20
|
+
if (
|
|
21
|
+
!(form instanceof HTMLFormElement) ||
|
|
22
|
+
!isActionForm(form) ||
|
|
23
|
+
runtime.isNavigating()
|
|
24
|
+
) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
event.preventDefault()
|
|
28
|
+
if (form.getAttribute("data-pmx-failed") === "uncertain") return
|
|
29
|
+
const from = new URL(location.href)
|
|
30
|
+
const submitter = event instanceof SubmitEvent ? event.submitter : null
|
|
31
|
+
const body = new URLSearchParams(
|
|
32
|
+
[...new FormData(form, submitter)].filter(
|
|
33
|
+
(entry): entry is [string, string] => typeof entry[1] === "string",
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
const load = runtime.begin({ form, submitter })
|
|
37
|
+
if (!load) return
|
|
38
|
+
clearActionError(form)
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(form.action, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
Accept: "application/problem+json, text/html",
|
|
44
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
45
|
+
Referer: from.href,
|
|
46
|
+
},
|
|
47
|
+
body,
|
|
48
|
+
redirect: "follow",
|
|
49
|
+
signal: load.signal,
|
|
50
|
+
})
|
|
51
|
+
if (!response.ok) throw await readActionRejection(response)
|
|
52
|
+
await load.apply(response, from)
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (load.signal.aborted) return
|
|
55
|
+
load.finish()
|
|
56
|
+
showActionError(form, submitter, describeActionError(error))
|
|
57
|
+
console.error("PathMX action refresh failed.", error)
|
|
58
|
+
} finally {
|
|
59
|
+
load.finish()
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
}
|