@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,104 @@
|
|
|
1
|
+
import { matchesInvalidation } from "../../invalidation.ts"
|
|
2
|
+
import { publicPath } from "../../server/router.ts"
|
|
3
|
+
import { route, type Plugin, type ServerContext } from "../types.ts"
|
|
4
|
+
|
|
5
|
+
const clientEntry = new URL("./client.ts", import.meta.url)
|
|
6
|
+
const staticClientEntry = new URL("./static-client.ts", import.meta.url)
|
|
7
|
+
const LIVE_HEARTBEAT_INTERVAL = 30_000
|
|
8
|
+
|
|
9
|
+
function serveLive(
|
|
10
|
+
ctx: ServerContext,
|
|
11
|
+
req: Request,
|
|
12
|
+
route: string,
|
|
13
|
+
actor?: string,
|
|
14
|
+
privateResponse = false,
|
|
15
|
+
) {
|
|
16
|
+
const encoder = new TextEncoder()
|
|
17
|
+
let unsubscribe: (() => void) | undefined
|
|
18
|
+
let heartbeat: ReturnType<typeof setInterval> | undefined
|
|
19
|
+
const cleanup = () => {
|
|
20
|
+
unsubscribe?.()
|
|
21
|
+
unsubscribe = undefined
|
|
22
|
+
if (heartbeat) clearInterval(heartbeat)
|
|
23
|
+
heartbeat = undefined
|
|
24
|
+
}
|
|
25
|
+
const stream = new ReadableStream({
|
|
26
|
+
start(controller) {
|
|
27
|
+
controller.enqueue(encoder.encode(": connected\n\n"))
|
|
28
|
+
heartbeat = setInterval(() => {
|
|
29
|
+
try {
|
|
30
|
+
controller.enqueue(encoder.encode(": keep-alive\n\n"))
|
|
31
|
+
} catch {
|
|
32
|
+
cleanup()
|
|
33
|
+
}
|
|
34
|
+
}, LIVE_HEARTBEAT_INTERVAL)
|
|
35
|
+
unsubscribe = ctx.onSync((event) => {
|
|
36
|
+
if (!matchesInvalidation(event, route, actor)) return
|
|
37
|
+
try {
|
|
38
|
+
// EventSource only dispatches events that contain a data field.
|
|
39
|
+
// Keep it opaque: routing already happened on the server.
|
|
40
|
+
controller.enqueue(encoder.encode("event: sync\ndata: changed\n\n"))
|
|
41
|
+
} catch {
|
|
42
|
+
cleanup()
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
req.signal.addEventListener(
|
|
46
|
+
"abort",
|
|
47
|
+
() => {
|
|
48
|
+
cleanup()
|
|
49
|
+
try {
|
|
50
|
+
controller.close()
|
|
51
|
+
} catch {
|
|
52
|
+
// already closed
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{ once: true },
|
|
56
|
+
)
|
|
57
|
+
},
|
|
58
|
+
cancel() {
|
|
59
|
+
cleanup()
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
const headers = new Headers({
|
|
63
|
+
"Content-Type": "text/event-stream",
|
|
64
|
+
"Cache-Control": privateResponse ? "private, no-store" : "no-cache",
|
|
65
|
+
Connection: "keep-alive",
|
|
66
|
+
})
|
|
67
|
+
if (privateResponse) headers.set("Vary", "Cookie")
|
|
68
|
+
return new Response(stream, { headers })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function RuntimePlugin(): Plugin {
|
|
72
|
+
return {
|
|
73
|
+
id: "runtime",
|
|
74
|
+
name: "RuntimePlugin",
|
|
75
|
+
routes: [
|
|
76
|
+
route.get("live", async (ctx) => {
|
|
77
|
+
const { request, url } = ctx
|
|
78
|
+
const requested = url.searchParams.get("route")
|
|
79
|
+
const source = requested ? ctx.resolve(requested) : undefined
|
|
80
|
+
if (!source) {
|
|
81
|
+
return new Response("Unknown live route.", { status: 400 })
|
|
82
|
+
}
|
|
83
|
+
const session = await ctx.session()
|
|
84
|
+
const access = await ctx.readAccess(source)
|
|
85
|
+
if (access === "denied") {
|
|
86
|
+
return new Response("Not Found", { status: 404 })
|
|
87
|
+
}
|
|
88
|
+
return serveLive(
|
|
89
|
+
ctx,
|
|
90
|
+
request,
|
|
91
|
+
publicPath(source),
|
|
92
|
+
session.type === "authenticated" ? session.actor.id : undefined,
|
|
93
|
+
access === "private" || session.type === "authenticated",
|
|
94
|
+
)
|
|
95
|
+
}),
|
|
96
|
+
],
|
|
97
|
+
async documentAssets(ctx, app) {
|
|
98
|
+
await ctx.addBrowserBundle(
|
|
99
|
+
app.runtime.target === "static" ? staticClientEntry : clientEntry,
|
|
100
|
+
"runtime",
|
|
101
|
+
)
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type LiveSubscription = {
|
|
2
|
+
bind(): void
|
|
3
|
+
close(): void
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type LiveSync = () => void | Promise<void>
|
|
7
|
+
export type LiveSubscriptionFactory = (onSync: LiveSync) => LiveSubscription
|
|
8
|
+
|
|
9
|
+
function liveTarget() {
|
|
10
|
+
const view = document.querySelector("[data-pmx-view]")
|
|
11
|
+
if (!(view instanceof HTMLElement)) return
|
|
12
|
+
const route = view.dataset.pmxRoute
|
|
13
|
+
if (!route) return
|
|
14
|
+
return { route }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createLiveSubscription(onSync: LiveSync): LiveSubscription {
|
|
18
|
+
let connection: { key: string; source: EventSource } | undefined
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
bind() {
|
|
22
|
+
const target = liveTarget()
|
|
23
|
+
if (!target) return
|
|
24
|
+
const key = target.route
|
|
25
|
+
if (connection?.key === key) return
|
|
26
|
+
|
|
27
|
+
connection?.source.close()
|
|
28
|
+
const url = new URL("/api/runtime/live", location.href)
|
|
29
|
+
url.searchParams.set("route", target.route)
|
|
30
|
+
const source = new EventSource(url)
|
|
31
|
+
let interrupted = false
|
|
32
|
+
source.addEventListener("error", () => {
|
|
33
|
+
interrupted = true
|
|
34
|
+
})
|
|
35
|
+
source.addEventListener("open", () => {
|
|
36
|
+
if (!interrupted || connection?.source !== source) return
|
|
37
|
+
// Reconcile what may have changed while offline. The runtime refresh
|
|
38
|
+
// will request a reload only when browser assets are incompatible.
|
|
39
|
+
interrupted = false
|
|
40
|
+
void onSync()
|
|
41
|
+
})
|
|
42
|
+
source.addEventListener("sync", () => void onSync())
|
|
43
|
+
connection = { key, source }
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
close() {
|
|
47
|
+
connection?.source.close()
|
|
48
|
+
connection = undefined
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { reportRuntimeError } from "./error.ts"
|
|
2
|
+
import { navigationViewTransitionTypes } from "./view-transition.ts"
|
|
3
|
+
import { publicationContains, publicationPath } from "./publication.ts"
|
|
4
|
+
|
|
5
|
+
export type NavigationVisit = (
|
|
6
|
+
destination: URL,
|
|
7
|
+
options: {
|
|
8
|
+
type: "navigation" | "traverse"
|
|
9
|
+
from: URL
|
|
10
|
+
signal: AbortSignal
|
|
11
|
+
viewTransition: boolean
|
|
12
|
+
viewTransitionTypes: readonly string[]
|
|
13
|
+
},
|
|
14
|
+
) => Promise<{ type: "rendered" } | { type: "native"; url: URL }>
|
|
15
|
+
|
|
16
|
+
type BrowserNavigateEvent = Event & {
|
|
17
|
+
canIntercept: boolean
|
|
18
|
+
destination: { url: string; sameDocument: boolean; index?: number }
|
|
19
|
+
downloadRequest: string | null
|
|
20
|
+
formData: FormData | null
|
|
21
|
+
hashChange: boolean
|
|
22
|
+
hasUAVisualTransition: boolean
|
|
23
|
+
navigationType: "push" | "reload" | "replace" | "traverse"
|
|
24
|
+
signal: AbortSignal
|
|
25
|
+
sourceElement: Element | null
|
|
26
|
+
intercept(options: { handler(): Promise<void> }): void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type BrowserNavigation = EventTarget & {
|
|
30
|
+
currentEntry?: { index: number }
|
|
31
|
+
addEventListener(
|
|
32
|
+
type: "navigate",
|
|
33
|
+
listener: (event: BrowserNavigateEvent) => void,
|
|
34
|
+
): void
|
|
35
|
+
removeEventListener(
|
|
36
|
+
type: "navigate",
|
|
37
|
+
listener: (event: BrowserNavigateEvent) => void,
|
|
38
|
+
): void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function navigationApi() {
|
|
42
|
+
return (window as Window & { navigation?: BrowserNavigation }).navigation
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function shouldIntercept(event: BrowserNavigateEvent) {
|
|
46
|
+
if (!event.canIntercept || event.hashChange) return false
|
|
47
|
+
if (event.destination.sameDocument && event.navigationType !== "traverse") {
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
if (event.downloadRequest !== null || event.formData !== null) return false
|
|
51
|
+
if (event.navigationType === "reload") return false
|
|
52
|
+
|
|
53
|
+
const destination = new URL(event.destination.url)
|
|
54
|
+
if (destination.origin !== location.origin) return false
|
|
55
|
+
if (!publicationContains(destination.pathname)) return false
|
|
56
|
+
if (destination.pathname.startsWith(publicationPath("/.pmx/"))) return false
|
|
57
|
+
if (destination.pathname.startsWith(publicationPath("/actions/")))
|
|
58
|
+
return false
|
|
59
|
+
if (destination.pathname.endsWith(".md")) return false
|
|
60
|
+
if (event.sourceElement?.closest("[data-pmx-reload], [rel~='external']")) {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function installNavigation(visit: NavigationVisit) {
|
|
67
|
+
const browserNavigation = navigationApi()
|
|
68
|
+
if (!browserNavigation) return
|
|
69
|
+
|
|
70
|
+
const navigate = (event: BrowserNavigateEvent) => {
|
|
71
|
+
if (!shouldIntercept(event)) return
|
|
72
|
+
const from = new URL(location.href)
|
|
73
|
+
const type = event.navigationType === "traverse" ? "traverse" : "navigation"
|
|
74
|
+
// Indexes are only reliable here. After intercept, currentEntry is the destination.
|
|
75
|
+
const viewTransitionTypes = navigationViewTransitionTypes({
|
|
76
|
+
type,
|
|
77
|
+
currentIndex: browserNavigation.currentEntry?.index,
|
|
78
|
+
destinationIndex: event.destination.index,
|
|
79
|
+
})
|
|
80
|
+
event.intercept({
|
|
81
|
+
async handler() {
|
|
82
|
+
try {
|
|
83
|
+
const result = await visit(new URL(event.destination.url), {
|
|
84
|
+
type,
|
|
85
|
+
from,
|
|
86
|
+
signal: event.signal,
|
|
87
|
+
viewTransition: !event.hasUAVisualTransition,
|
|
88
|
+
viewTransitionTypes,
|
|
89
|
+
})
|
|
90
|
+
if (result.type === "rendered" || event.signal.aborted) return
|
|
91
|
+
browserNavigation.removeEventListener("navigate", navigate)
|
|
92
|
+
location.replace(result.url)
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (event.signal.aborted) return
|
|
95
|
+
reportRuntimeError({
|
|
96
|
+
type,
|
|
97
|
+
outcome: "native",
|
|
98
|
+
message:
|
|
99
|
+
"The enhanced navigation failed. Loading the destination normally.",
|
|
100
|
+
})
|
|
101
|
+
console.error(
|
|
102
|
+
"PathMX navigation failed; loading the document.",
|
|
103
|
+
error,
|
|
104
|
+
)
|
|
105
|
+
browserNavigation.removeEventListener("navigate", navigate)
|
|
106
|
+
location.reload()
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
browserNavigation.addEventListener("navigate", navigate)
|
|
113
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { RenderType } from "./document.ts"
|
|
2
|
+
|
|
3
|
+
export type PendingTarget = {
|
|
4
|
+
form: HTMLFormElement
|
|
5
|
+
submitter: HTMLElement | null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type PendingDetail = {
|
|
9
|
+
pending: boolean
|
|
10
|
+
type: RenderType
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function dispatchPending(detail: PendingDetail) {
|
|
14
|
+
document.dispatchEvent(
|
|
15
|
+
new CustomEvent<PendingDetail>("pmx:pending", { detail }),
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function markViewBusy(busy: boolean) {
|
|
20
|
+
const view = document.querySelector("[data-pmx-view]")
|
|
21
|
+
if (!(view instanceof HTMLElement)) return
|
|
22
|
+
if (busy) view.setAttribute("aria-busy", "true")
|
|
23
|
+
else view.removeAttribute("aria-busy")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function startPending(type: RenderType, target?: PendingTarget) {
|
|
27
|
+
const root = document.documentElement
|
|
28
|
+
const previousSubmitterState =
|
|
29
|
+
target?.submitter?.getAttribute("aria-disabled")
|
|
30
|
+
|
|
31
|
+
root.dataset.pmxPending = type
|
|
32
|
+
markViewBusy(true)
|
|
33
|
+
target?.form.setAttribute("data-pmx-pending", "")
|
|
34
|
+
target?.submitter?.setAttribute("data-pmx-pending", "")
|
|
35
|
+
target?.submitter?.setAttribute("aria-disabled", "true")
|
|
36
|
+
dispatchPending({ pending: true, type })
|
|
37
|
+
|
|
38
|
+
let finished = false
|
|
39
|
+
return () => {
|
|
40
|
+
if (finished) return
|
|
41
|
+
finished = true
|
|
42
|
+
if (root.dataset.pmxPending === type) delete root.dataset.pmxPending
|
|
43
|
+
markViewBusy(false)
|
|
44
|
+
target?.form.removeAttribute("data-pmx-pending")
|
|
45
|
+
target?.submitter?.removeAttribute("data-pmx-pending")
|
|
46
|
+
if (previousSubmitterState == null) {
|
|
47
|
+
target?.submitter?.removeAttribute("aria-disabled")
|
|
48
|
+
} else {
|
|
49
|
+
target?.submitter?.setAttribute("aria-disabled", previousSubmitterState)
|
|
50
|
+
}
|
|
51
|
+
dispatchPending({ pending: false, type })
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function publicationBasePath() {
|
|
2
|
+
const value = document.documentElement.dataset.pmxBase
|
|
3
|
+
return value?.startsWith("/") ? value : "/"
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function publicationPath(pathname: string) {
|
|
7
|
+
const basePath = publicationBasePath()
|
|
8
|
+
if (basePath === "/") return pathname
|
|
9
|
+
return pathname === "/" ? basePath : `${basePath}${pathname}`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function publicationContains(pathname: string) {
|
|
13
|
+
const basePath = publicationBasePath()
|
|
14
|
+
return (
|
|
15
|
+
basePath === "/" ||
|
|
16
|
+
pathname === basePath ||
|
|
17
|
+
pathname.startsWith(`${basePath}/`)
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fetchPathMXDocument,
|
|
3
|
+
PathMXReloadRequiredError,
|
|
4
|
+
readPathMXDocument,
|
|
5
|
+
reconcilePathMXDocument,
|
|
6
|
+
type RenderType,
|
|
7
|
+
} from "./document.ts"
|
|
8
|
+
import type { LiveSubscriptionFactory } from "./live.ts"
|
|
9
|
+
import { installNavigation, type NavigationVisit } from "./navigation.ts"
|
|
10
|
+
import { installViewportChrome } from "./viewport.ts"
|
|
11
|
+
import { startPending, type PendingTarget } from "./pending.ts"
|
|
12
|
+
import { reportRuntimeError } from "./error.ts"
|
|
13
|
+
import { installHeadingLinks } from "./heading-links.ts"
|
|
14
|
+
|
|
15
|
+
type ActiveLoad = {
|
|
16
|
+
controller: AbortController
|
|
17
|
+
signal: AbortSignal
|
|
18
|
+
type: RenderType
|
|
19
|
+
finishPending: () => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type RuntimeOptions = Readonly<{
|
|
23
|
+
actions?: RuntimeActionInstaller
|
|
24
|
+
live: LiveSubscriptionFactory
|
|
25
|
+
}>
|
|
26
|
+
|
|
27
|
+
type RuntimeActionLoad = Readonly<{
|
|
28
|
+
signal: AbortSignal
|
|
29
|
+
apply(response: Response, from: URL): Promise<void>
|
|
30
|
+
finish(): void
|
|
31
|
+
}>
|
|
32
|
+
|
|
33
|
+
export type RuntimeActionInstaller = (runtime: {
|
|
34
|
+
isNavigating(): boolean
|
|
35
|
+
begin(target: PendingTarget): RuntimeActionLoad | undefined
|
|
36
|
+
}) => void
|
|
37
|
+
|
|
38
|
+
export function startRuntime(options: RuntimeOptions) {
|
|
39
|
+
let active: ActiveLoad | undefined
|
|
40
|
+
let liveQueued = false
|
|
41
|
+
|
|
42
|
+
function begin(
|
|
43
|
+
type: RenderType,
|
|
44
|
+
signal?: AbortSignal,
|
|
45
|
+
target?: PendingTarget,
|
|
46
|
+
) {
|
|
47
|
+
active?.finishPending()
|
|
48
|
+
active?.controller.abort()
|
|
49
|
+
const controller = new AbortController()
|
|
50
|
+
const load: ActiveLoad = {
|
|
51
|
+
controller,
|
|
52
|
+
signal: signal
|
|
53
|
+
? AbortSignal.any([controller.signal, signal])
|
|
54
|
+
: controller.signal,
|
|
55
|
+
type,
|
|
56
|
+
finishPending: startPending(type, target),
|
|
57
|
+
}
|
|
58
|
+
active = load
|
|
59
|
+
return load
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function finish(load: ActiveLoad) {
|
|
63
|
+
if (active !== load) return
|
|
64
|
+
active = undefined
|
|
65
|
+
load.finishPending()
|
|
66
|
+
if (liveQueued) queueMicrotask(runLiveRefresh)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isCurrent(load: ActiveLoad) {
|
|
70
|
+
return active === load && !load.signal.aborted
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isNavigating() {
|
|
74
|
+
return active?.type === "navigation" || active?.type === "traverse"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function sameRequestUrl(left: URL, right: URL) {
|
|
78
|
+
return (
|
|
79
|
+
left.origin === right.origin &&
|
|
80
|
+
left.pathname === right.pathname &&
|
|
81
|
+
left.search === right.search
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function apply(
|
|
86
|
+
load: ActiveLoad,
|
|
87
|
+
loaded: Awaited<ReturnType<typeof fetchPathMXDocument>>,
|
|
88
|
+
settings: {
|
|
89
|
+
from: URL
|
|
90
|
+
viewTransition: boolean
|
|
91
|
+
viewTransitionTypes?: readonly string[]
|
|
92
|
+
},
|
|
93
|
+
) {
|
|
94
|
+
if (!isCurrent(load)) return
|
|
95
|
+
await reconcilePathMXDocument(loaded, {
|
|
96
|
+
type: load.type,
|
|
97
|
+
from: settings.from,
|
|
98
|
+
viewTransition: settings.viewTransition,
|
|
99
|
+
viewTransitionTypes: settings.viewTransitionTypes ?? [],
|
|
100
|
+
})
|
|
101
|
+
live.bind()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const visit: NavigationVisit = async (destination, settings) => {
|
|
105
|
+
const load = begin(settings.type, settings.signal)
|
|
106
|
+
try {
|
|
107
|
+
const loaded = await fetchPathMXDocument(destination, load.signal)
|
|
108
|
+
if (!sameRequestUrl(loaded.responseUrl, destination)) {
|
|
109
|
+
return { type: "native", url: loaded.responseUrl }
|
|
110
|
+
}
|
|
111
|
+
await apply(load, loaded, settings)
|
|
112
|
+
return { type: "rendered" }
|
|
113
|
+
} finally {
|
|
114
|
+
finish(load)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function refreshLive() {
|
|
119
|
+
liveQueued = true
|
|
120
|
+
if (!active) queueMicrotask(runLiveRefresh)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function runLiveRefresh() {
|
|
124
|
+
if (active || !liveQueued) return
|
|
125
|
+
liveQueued = false
|
|
126
|
+
const load = begin("live")
|
|
127
|
+
const url = new URL(location.href)
|
|
128
|
+
try {
|
|
129
|
+
const loaded = await fetchPathMXDocument(url, load.signal, "no-store")
|
|
130
|
+
await apply(load, loaded, { from: url, viewTransition: false })
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (load.signal.aborted) return
|
|
133
|
+
if (error instanceof PathMXReloadRequiredError) {
|
|
134
|
+
liveQueued = false
|
|
135
|
+
location.reload()
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
reportRuntimeError({
|
|
139
|
+
type: "live",
|
|
140
|
+
outcome: "stale",
|
|
141
|
+
message:
|
|
142
|
+
"The live refresh failed. The current page remains available and may be stale.",
|
|
143
|
+
})
|
|
144
|
+
console.error("PathMX live refresh failed.", error)
|
|
145
|
+
} finally {
|
|
146
|
+
finish(load)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const live = options.live(refreshLive)
|
|
151
|
+
|
|
152
|
+
options.actions?.({
|
|
153
|
+
isNavigating,
|
|
154
|
+
begin(target) {
|
|
155
|
+
if (active?.type === "action") return
|
|
156
|
+
const load = begin("action", undefined, target)
|
|
157
|
+
return {
|
|
158
|
+
signal: load.signal,
|
|
159
|
+
async apply(response, from) {
|
|
160
|
+
if (!sameRequestUrl(from, new URL(location.href))) return
|
|
161
|
+
const loaded = await readPathMXDocument(response, from)
|
|
162
|
+
await apply(load, loaded, { from, viewTransition: false })
|
|
163
|
+
},
|
|
164
|
+
finish() {
|
|
165
|
+
finish(load)
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
live.bind()
|
|
172
|
+
installHeadingLinks()
|
|
173
|
+
installViewportChrome()
|
|
174
|
+
installNavigation(visit)
|
|
175
|
+
addEventListener("pagehide", () => live.close())
|
|
176
|
+
addEventListener("pageshow", () => live.bind())
|
|
177
|
+
}
|