@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
package/server/http.ts
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActionNotFoundError,
|
|
3
|
+
ActionUnavailableError,
|
|
4
|
+
ActionValidationError,
|
|
5
|
+
dispatchAction,
|
|
6
|
+
type ActionInput,
|
|
7
|
+
} from "../actions/actions.ts"
|
|
8
|
+
import type { Engine } from "../host/engine.ts"
|
|
9
|
+
import { actionProblem } from "../problem.ts"
|
|
10
|
+
import type { Source } from "../source/index.ts"
|
|
11
|
+
import {
|
|
12
|
+
anonymousPrincipal,
|
|
13
|
+
principalFor,
|
|
14
|
+
type CredentialSession,
|
|
15
|
+
type Principal,
|
|
16
|
+
} from "../authority/model.ts"
|
|
17
|
+
import { renderPage } from "../host/render.ts"
|
|
18
|
+
import { privateResponse, withReadCache } from "../host/response.ts"
|
|
19
|
+
import { pageNotFound, requestScheme } from "./page.ts"
|
|
20
|
+
import { AGENT_BROWSING_PLUGIN_ID, protocolRoute } from "./protocols.ts"
|
|
21
|
+
import { prefersMarkdown, withMarkdownAlternate } from "./representations.ts"
|
|
22
|
+
import { resolveRoute } from "./router.ts"
|
|
23
|
+
|
|
24
|
+
const anonymousSession = Object.freeze({
|
|
25
|
+
type: "anonymous",
|
|
26
|
+
signIn: null,
|
|
27
|
+
}) satisfies CredentialSession
|
|
28
|
+
|
|
29
|
+
function formInput(body: string) {
|
|
30
|
+
const input: ActionInput = {}
|
|
31
|
+
for (const [name, value] of new URLSearchParams(body)) {
|
|
32
|
+
const current = input[name]
|
|
33
|
+
input[name] =
|
|
34
|
+
current == null
|
|
35
|
+
? value
|
|
36
|
+
: Array.isArray(current)
|
|
37
|
+
? [...current, value]
|
|
38
|
+
: [current, value]
|
|
39
|
+
}
|
|
40
|
+
return input
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveTarget(
|
|
44
|
+
app: Engine,
|
|
45
|
+
input: ActionInput,
|
|
46
|
+
referer: string | null,
|
|
47
|
+
) {
|
|
48
|
+
const targetInput = input._target
|
|
49
|
+
const hinted = typeof targetInput === "string" ? targetInput.trim() : ""
|
|
50
|
+
if (hinted) {
|
|
51
|
+
const source = app.resolve(hinted) ?? app.repo.get(hinted)
|
|
52
|
+
return { source, required: true }
|
|
53
|
+
}
|
|
54
|
+
if (!referer) return
|
|
55
|
+
try {
|
|
56
|
+
return { source: app.resolve(new URL(referer).pathname), required: false }
|
|
57
|
+
} catch {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sameOriginMutation(req: Request, url: URL) {
|
|
63
|
+
const origin = req.headers.get("origin")
|
|
64
|
+
if (origin) return origin === url.origin
|
|
65
|
+
const referer = req.headers.get("referer")
|
|
66
|
+
if (!referer) return false
|
|
67
|
+
try {
|
|
68
|
+
return new URL(referer).origin === url.origin
|
|
69
|
+
} catch {
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function handleAction(
|
|
75
|
+
app: Engine,
|
|
76
|
+
req: Request,
|
|
77
|
+
url: URL,
|
|
78
|
+
session: CredentialSession,
|
|
79
|
+
) {
|
|
80
|
+
if (req.method !== "POST") {
|
|
81
|
+
return new Response("Method Not Allowed", { status: 405 })
|
|
82
|
+
}
|
|
83
|
+
if (session.type !== "authenticated") return pageNotFound()
|
|
84
|
+
if (!sameOriginMutation(req, url)) {
|
|
85
|
+
return actionProblem(req, 403, "Cross-origin mutation denied.")
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const name = url.pathname.slice("/actions/".length)
|
|
89
|
+
const input = formInput(await req.text())
|
|
90
|
+
const resolvedTarget = resolveTarget(app, input, req.headers.get("referer"))
|
|
91
|
+
const target = resolvedTarget?.source
|
|
92
|
+
if (resolvedTarget?.required && !target) return pageNotFound()
|
|
93
|
+
if (
|
|
94
|
+
target &&
|
|
95
|
+
!app.hasSourceCapability(
|
|
96
|
+
{ type: "actor", actor: session.actor },
|
|
97
|
+
target,
|
|
98
|
+
"execute",
|
|
99
|
+
)
|
|
100
|
+
) {
|
|
101
|
+
return pageNotFound()
|
|
102
|
+
}
|
|
103
|
+
let outcome: Awaited<ReturnType<typeof dispatchAction>>
|
|
104
|
+
try {
|
|
105
|
+
outcome = await dispatchAction(app, name, input, session.actor, target)
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error instanceof ActionNotFoundError) {
|
|
108
|
+
return actionProblem(req, 404, error.message)
|
|
109
|
+
}
|
|
110
|
+
if (error instanceof ActionValidationError) {
|
|
111
|
+
return actionProblem(req, 400, error.message)
|
|
112
|
+
}
|
|
113
|
+
if (error instanceof ActionUnavailableError) {
|
|
114
|
+
return actionProblem(req, 403, error.message)
|
|
115
|
+
}
|
|
116
|
+
const message = error instanceof Error ? error.message : "Action failed."
|
|
117
|
+
return actionProblem(req, 400, message)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (req.headers.get("accept")?.includes("application/json")) {
|
|
121
|
+
return privateResponse(
|
|
122
|
+
Response.json({
|
|
123
|
+
ok: true,
|
|
124
|
+
action: name,
|
|
125
|
+
target: target?.id,
|
|
126
|
+
output: outcome.value,
|
|
127
|
+
receipt: { applied: outcome.commit.applied.length },
|
|
128
|
+
}),
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return privateResponse(
|
|
133
|
+
new Response(null, {
|
|
134
|
+
status: 303,
|
|
135
|
+
headers: { Location: req.headers.get("referer") || "/" },
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function handleRequest(app: Engine, req: Request) {
|
|
141
|
+
return handleRoutedRequest(app, req, new URL(req.url))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** @internal Render a canonical route at a different public publication URL. */
|
|
145
|
+
export async function handleRequestAtPath(
|
|
146
|
+
app: Engine,
|
|
147
|
+
req: Request,
|
|
148
|
+
pathname: string,
|
|
149
|
+
) {
|
|
150
|
+
const publicUrl = new URL(req.url)
|
|
151
|
+
const routeUrl = new URL(pathname, publicUrl.origin)
|
|
152
|
+
return handleRoutedRequest(app, req, routeUrl, publicUrl)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function handleRoutedRequest(
|
|
156
|
+
app: Engine,
|
|
157
|
+
req: Request,
|
|
158
|
+
url: URL,
|
|
159
|
+
publicUrl = url,
|
|
160
|
+
) {
|
|
161
|
+
const start = performance.now()
|
|
162
|
+
|
|
163
|
+
return app.perf.measureAsync(
|
|
164
|
+
"http.request",
|
|
165
|
+
async () => {
|
|
166
|
+
const response = await dispatchRequest(app, req, url, publicUrl)
|
|
167
|
+
const ms = performance.now() - start
|
|
168
|
+
response.headers.set("Server-Timing", `render;dur=${ms.toFixed(1)}`)
|
|
169
|
+
app.log.debug("http.perf", {
|
|
170
|
+
path: url.pathname,
|
|
171
|
+
ms: Number(ms.toFixed(1)),
|
|
172
|
+
status: response.status,
|
|
173
|
+
})
|
|
174
|
+
return response
|
|
175
|
+
},
|
|
176
|
+
publicUrl.pathname,
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function dispatchRequest(
|
|
181
|
+
app: Engine,
|
|
182
|
+
req: Request,
|
|
183
|
+
url: URL,
|
|
184
|
+
publicUrl: URL,
|
|
185
|
+
) {
|
|
186
|
+
if (req.method === "GET" || req.method === "HEAD") {
|
|
187
|
+
const served = serveGenerated(app, url.pathname, req.method === "HEAD")
|
|
188
|
+
if (served) return served
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (
|
|
192
|
+
protocolRoute(url.pathname) &&
|
|
193
|
+
(req.method === "GET" || req.method === "HEAD")
|
|
194
|
+
) {
|
|
195
|
+
const authored = await serveAsset(
|
|
196
|
+
app,
|
|
197
|
+
url.pathname,
|
|
198
|
+
req.method === "HEAD",
|
|
199
|
+
anonymousSession,
|
|
200
|
+
anonymousPrincipal,
|
|
201
|
+
)
|
|
202
|
+
if (authored) return authored
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const serverRoute = await app.serverRoute(req, url, publicUrl)
|
|
206
|
+
if (serverRoute) return serverRoute
|
|
207
|
+
if (url.pathname === "/api" || url.pathname.startsWith("/api/")) {
|
|
208
|
+
return pageNotFound()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const session = await app.resolveCredentials(req)
|
|
212
|
+
const principal = principalFor(session)
|
|
213
|
+
return dispatch(app, req, url, session, principal)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function serveGenerated(app: Engine, pathname: string, head: boolean) {
|
|
217
|
+
if (!pathname.startsWith("/.pmx/")) return
|
|
218
|
+
const asset = app.servedAsset(pathname)
|
|
219
|
+
if (!asset) return
|
|
220
|
+
return new Response(head ? null : asset.body, {
|
|
221
|
+
headers: {
|
|
222
|
+
"Content-Type": asset.contentType,
|
|
223
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function dispatch(
|
|
229
|
+
app: Engine,
|
|
230
|
+
req: Request,
|
|
231
|
+
url: URL,
|
|
232
|
+
session: CredentialSession,
|
|
233
|
+
principal: Principal,
|
|
234
|
+
) {
|
|
235
|
+
if (url.pathname.startsWith("/actions/")) {
|
|
236
|
+
return handleAction(app, req, url, session)
|
|
237
|
+
}
|
|
238
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
239
|
+
return new Response("Method Not Allowed", { status: 405 })
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// One canonical URL per page: the edge normalizes trailing slashes so
|
|
243
|
+
// canonicalPath stays strict and cache keys stay unique.
|
|
244
|
+
if (url.pathname.length > 1 && url.pathname.endsWith("/")) {
|
|
245
|
+
return new Response(null, {
|
|
246
|
+
status: 308,
|
|
247
|
+
headers: { Location: url.pathname.replace(/\/+$/, "") + url.search },
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const route = resolveRoute(app.repo, url.pathname)
|
|
252
|
+
if (route?.type === "source") {
|
|
253
|
+
return serveSource(
|
|
254
|
+
app,
|
|
255
|
+
req.method === "HEAD",
|
|
256
|
+
session,
|
|
257
|
+
principal,
|
|
258
|
+
route.source,
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
if (route?.type === "page") {
|
|
262
|
+
const agentBrowsing = app.hasPlugin(AGENT_BROWSING_PLUGIN_ID)
|
|
263
|
+
if (agentBrowsing && prefersMarkdown(req)) {
|
|
264
|
+
return withMarkdownAlternate(
|
|
265
|
+
serveSource(
|
|
266
|
+
app,
|
|
267
|
+
req.method === "HEAD",
|
|
268
|
+
session,
|
|
269
|
+
principal,
|
|
270
|
+
route.source,
|
|
271
|
+
),
|
|
272
|
+
route.source.path,
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
let response = await renderPage(app, session, principal, route.source, {
|
|
276
|
+
scheme: requestScheme(req),
|
|
277
|
+
})
|
|
278
|
+
if (req.method === "HEAD") response = new Response(null, response)
|
|
279
|
+
return agentBrowsing
|
|
280
|
+
? withMarkdownAlternate(response, route.source.path)
|
|
281
|
+
: response
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
(await serveAsset(
|
|
286
|
+
app,
|
|
287
|
+
url.pathname,
|
|
288
|
+
req.method === "HEAD",
|
|
289
|
+
session,
|
|
290
|
+
principal,
|
|
291
|
+
)) ?? pageNotFound()
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function serveSource(
|
|
296
|
+
app: Engine,
|
|
297
|
+
head: boolean,
|
|
298
|
+
session: CredentialSession,
|
|
299
|
+
principal: Principal,
|
|
300
|
+
source: Source,
|
|
301
|
+
) {
|
|
302
|
+
const access = app.readAccess(principal, source)
|
|
303
|
+
if (access === "denied") return pageNotFound()
|
|
304
|
+
return withReadCache(
|
|
305
|
+
new Response(head ? null : source.content, {
|
|
306
|
+
headers: { "Content-Type": "text/markdown; charset=utf-8" },
|
|
307
|
+
}),
|
|
308
|
+
access,
|
|
309
|
+
session,
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function serveAsset(
|
|
314
|
+
app: Engine,
|
|
315
|
+
pathname: string,
|
|
316
|
+
head: boolean,
|
|
317
|
+
session: CredentialSession,
|
|
318
|
+
principal: Principal,
|
|
319
|
+
) {
|
|
320
|
+
let path: string
|
|
321
|
+
try {
|
|
322
|
+
path = decodeURIComponent(pathname)
|
|
323
|
+
} catch {
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
if (path.includes("..")) return
|
|
327
|
+
const storagePath = app.repo.storagePath(path)
|
|
328
|
+
if (!storagePath || !(await app.repo.isFile(path))) return
|
|
329
|
+
const access = app.readAccess(principal, path)
|
|
330
|
+
if (access === "denied") return pageNotFound()
|
|
331
|
+
const body = head ? null : await app.environment.storage.readBlob(storagePath)
|
|
332
|
+
return withReadCache(new Response(body), access, session)
|
|
333
|
+
}
|
package/server/page.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const SCHEME_COOKIE = "pathmx-scheme"
|
|
2
|
+
|
|
3
|
+
export { pageNotFound } from "../host/render.ts"
|
|
4
|
+
|
|
5
|
+
function cookieValue(header: string | null, name: string) {
|
|
6
|
+
if (!header) return
|
|
7
|
+
for (const part of header.split(";")) {
|
|
8
|
+
const equals = part.indexOf("=")
|
|
9
|
+
if (equals === -1 || part.slice(0, equals).trim() !== name) continue
|
|
10
|
+
try {
|
|
11
|
+
return decodeURIComponent(part.slice(equals + 1).trim())
|
|
12
|
+
} catch {
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function requestScheme(request: Request) {
|
|
19
|
+
const value = cookieValue(request.headers.get("cookie"), SCHEME_COOKIE)
|
|
20
|
+
if (value === "light" || value === "dark") return value
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const AGENT_BROWSING_PLUGIN_ID = "agent-browsing"
|
|
2
|
+
|
|
3
|
+
const protocolRoutes: Readonly<Record<string, string>> = Object.freeze({
|
|
4
|
+
"/llms.txt": `/api/${AGENT_BROWSING_PLUGIN_ID}/llms.txt`,
|
|
5
|
+
"/robots.txt": `/api/${AGENT_BROWSING_PLUGIN_ID}/robots.txt`,
|
|
6
|
+
"/sitemap.xml": `/api/${AGENT_BROWSING_PLUGIN_ID}/sitemap.xml`,
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
/** @internal Map core-owned standard URLs onto their namespaced Plugin route. */
|
|
10
|
+
export function protocolRoute(pathname: string) {
|
|
11
|
+
return protocolRoutes[pathname]
|
|
12
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
function acceptedQuality(header: string, target: string) {
|
|
2
|
+
const [targetType, targetSubtype] = target.split("/")
|
|
3
|
+
let quality = 0
|
|
4
|
+
let specificity = -1
|
|
5
|
+
|
|
6
|
+
for (const entry of header.split(",")) {
|
|
7
|
+
const [range = "", ...parameters] = entry.split(";")
|
|
8
|
+
const [type, subtype] = range.trim().toLowerCase().split("/")
|
|
9
|
+
if (
|
|
10
|
+
!type ||
|
|
11
|
+
!subtype ||
|
|
12
|
+
(type !== "*" && type !== targetType) ||
|
|
13
|
+
(subtype !== "*" && subtype !== targetSubtype)
|
|
14
|
+
) {
|
|
15
|
+
continue
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const candidateSpecificity =
|
|
19
|
+
(type === "*" ? 0 : 1) + (subtype === "*" ? 0 : 1)
|
|
20
|
+
const q = parameters
|
|
21
|
+
.map((parameter) => parameter.trim().match(/^q\s*=\s*(.+)$/i)?.[1])
|
|
22
|
+
.find((value) => value !== undefined)
|
|
23
|
+
const parsed = q === undefined ? 1 : Number(q)
|
|
24
|
+
const candidateQuality =
|
|
25
|
+
Number.isFinite(parsed) && parsed >= 0 && parsed <= 1 ? parsed : 0
|
|
26
|
+
|
|
27
|
+
if (
|
|
28
|
+
candidateSpecificity > specificity ||
|
|
29
|
+
(candidateSpecificity === specificity && candidateQuality > quality)
|
|
30
|
+
) {
|
|
31
|
+
specificity = candidateSpecificity
|
|
32
|
+
quality = candidateQuality
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return quality
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function prefersMarkdown(request: Request) {
|
|
40
|
+
const accept = request.headers.get("Accept")
|
|
41
|
+
if (!accept) return false
|
|
42
|
+
const markdown = acceptedQuality(accept, "text/markdown")
|
|
43
|
+
return markdown > 0 && markdown > acceptedQuality(accept, "text/html")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function appendVary(headers: Headers, name: string) {
|
|
47
|
+
const values = (headers.get("Vary") ?? "")
|
|
48
|
+
.split(",")
|
|
49
|
+
.map((value) => value.trim())
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
if (!values.some((value) => value.toLowerCase() === name.toLowerCase())) {
|
|
52
|
+
values.push(name)
|
|
53
|
+
}
|
|
54
|
+
headers.set("Vary", values.join(", "))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function withMarkdownAlternate(response: Response, sourcePath: string) {
|
|
58
|
+
if (response.status >= 400) return response
|
|
59
|
+
appendVary(response.headers, "Accept")
|
|
60
|
+
const markdownPath = new URL(sourcePath, "http://pathmx.local").pathname
|
|
61
|
+
response.headers.append(
|
|
62
|
+
"Link",
|
|
63
|
+
`<${markdownPath}>; rel="alternate"; type="text/markdown"`,
|
|
64
|
+
)
|
|
65
|
+
return response
|
|
66
|
+
}
|
package/server/router.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { Repository } from "../repository.ts"
|
|
2
|
+
import { resolveSourceId, type Source } from "../source/index.ts"
|
|
3
|
+
import { tryCanonicalPath } from "../canonical.ts"
|
|
4
|
+
import { isActorSource } from "../authority/actors.ts"
|
|
5
|
+
|
|
6
|
+
type IndexRouteTable = {
|
|
7
|
+
revision: number
|
|
8
|
+
byDirectory: Map<string, Source[]>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const indexRouteTables = new WeakMap<Repository, IndexRouteTable>()
|
|
12
|
+
|
|
13
|
+
export type Route =
|
|
14
|
+
{ type: "page"; source: Source } | { type: "source"; source: Source }
|
|
15
|
+
|
|
16
|
+
// Index-ness is name position only. Types and qualifiers are domain-defined
|
|
17
|
+
// (`index` may be a valid type), so the name slot is the only unambiguous
|
|
18
|
+
// place routing semantics can live — the route table stays inferable from
|
|
19
|
+
// the file listing alone.
|
|
20
|
+
export function isIndexSource(source: Source) {
|
|
21
|
+
return source.name === "index"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isPageSource(source: Source) {
|
|
25
|
+
return (
|
|
26
|
+
source.type !== "layout" &&
|
|
27
|
+
source.type !== "navigation" &&
|
|
28
|
+
!isActorSource(source)
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function sourceDirectory(source: Source) {
|
|
33
|
+
const slash = source.path.lastIndexOf("/")
|
|
34
|
+
return slash <= 0 ? "/" : source.path.slice(0, slash)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function publicPath(source: Source) {
|
|
38
|
+
if (isPageSource(source) && isIndexSource(source))
|
|
39
|
+
return sourceDirectory(source)
|
|
40
|
+
return source.id
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sourceById(repo: Repository, path: string) {
|
|
44
|
+
return repo.atPath(path) ?? repo.get(resolveSourceId(path, {}))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizePathname(pathname: string) {
|
|
48
|
+
let path: string
|
|
49
|
+
try {
|
|
50
|
+
path = decodeURIComponent(pathname)
|
|
51
|
+
} catch {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
return tryCanonicalPath(path || "/")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resolveRoute(
|
|
58
|
+
repo: Repository,
|
|
59
|
+
pathname: string,
|
|
60
|
+
): Route | undefined {
|
|
61
|
+
const path = normalizePathname(pathname)
|
|
62
|
+
|
|
63
|
+
if (!path) return
|
|
64
|
+
|
|
65
|
+
if (path.endsWith(".md")) {
|
|
66
|
+
const source = sourceById(repo, path)
|
|
67
|
+
return source ? { type: "source", source } : undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const exact = repo.get(resolveSourceId(path, {}))
|
|
71
|
+
if (exact && isPageSource(exact) && exact.id === path)
|
|
72
|
+
return { type: "page", source: exact }
|
|
73
|
+
|
|
74
|
+
const index = indexCandidates(repo, path)[0]
|
|
75
|
+
|
|
76
|
+
if (index) return { type: "page", source: index }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Ties break by resolved id in codepoint order (not localeCompare — host
|
|
80
|
+
// locale must not change routing). Bare `index.md` participates under its
|
|
81
|
+
// default-typed id.
|
|
82
|
+
function indexCandidates(repo: Repository, dir: string) {
|
|
83
|
+
return indexRouteTable(repo).byDirectory.get(dir) ?? []
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sortIndexCandidates(sources: Source[]) {
|
|
87
|
+
return sources.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function indexRouteTable(repo: Repository) {
|
|
91
|
+
const cached = indexRouteTables.get(repo)
|
|
92
|
+
if (cached?.revision === repo.revision) return cached
|
|
93
|
+
|
|
94
|
+
const byDirectory = new Map<string, Source[]>()
|
|
95
|
+
for (const source of repo.all()) {
|
|
96
|
+
if (!isPageSource(source) || !isIndexSource(source)) continue
|
|
97
|
+
const dir = sourceDirectory(source)
|
|
98
|
+
const candidates = byDirectory.get(dir)
|
|
99
|
+
if (candidates) candidates.push(source)
|
|
100
|
+
else byDirectory.set(dir, [source])
|
|
101
|
+
}
|
|
102
|
+
for (const candidates of byDirectory.values()) {
|
|
103
|
+
sortIndexCandidates(candidates)
|
|
104
|
+
}
|
|
105
|
+
const table = { revision: repo.revision, byDirectory }
|
|
106
|
+
indexRouteTables.set(repo, table)
|
|
107
|
+
return table
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type IndexRouteConflict = {
|
|
111
|
+
dir: string
|
|
112
|
+
winner: Source
|
|
113
|
+
shadowed: Source[]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Directories where more than one index source claims the route. */
|
|
117
|
+
export function indexRouteConflicts(repo: Repository) {
|
|
118
|
+
const conflicts: IndexRouteConflict[] = []
|
|
119
|
+
const { byDirectory } = indexRouteTable(repo)
|
|
120
|
+
for (const dir of [...byDirectory.keys()].sort()) {
|
|
121
|
+
const candidates = byDirectory.get(dir)!
|
|
122
|
+
if (candidates.length > 1) {
|
|
123
|
+
conflicts.push({
|
|
124
|
+
dir,
|
|
125
|
+
winner: candidates[0]!,
|
|
126
|
+
shadowed: candidates.slice(1),
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return conflicts
|
|
131
|
+
}
|