@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,256 @@
|
|
|
1
|
+
import type { SourcePreview } from "./model.ts"
|
|
2
|
+
import "./preview.css"
|
|
3
|
+
|
|
4
|
+
const selector = "a[data-pmx-source-preview]"
|
|
5
|
+
const showDelay = 300
|
|
6
|
+
const hideDelay = 120
|
|
7
|
+
const edge = 12
|
|
8
|
+
const gap = 4
|
|
9
|
+
|
|
10
|
+
type PreviewElements = Readonly<{
|
|
11
|
+
root: HTMLAnchorElement
|
|
12
|
+
image: HTMLImageElement
|
|
13
|
+
source: HTMLElement
|
|
14
|
+
title: HTMLElement
|
|
15
|
+
description: HTMLElement
|
|
16
|
+
}>
|
|
17
|
+
|
|
18
|
+
let elements: PreviewElements | undefined
|
|
19
|
+
let activeLink: HTMLAnchorElement | undefined
|
|
20
|
+
let showTimer: number | undefined
|
|
21
|
+
let hideTimer: number | undefined
|
|
22
|
+
let requestVersion = 0
|
|
23
|
+
const summaries = new Map<string, Promise<SourcePreview | undefined>>()
|
|
24
|
+
|
|
25
|
+
function linkFrom(target: EventTarget | null) {
|
|
26
|
+
if (!(target instanceof Element)) return
|
|
27
|
+
const link = target.closest(selector)
|
|
28
|
+
return link instanceof HTMLAnchorElement ? link : undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function clearTimer(timer: number | undefined) {
|
|
32
|
+
if (timer !== undefined) window.clearTimeout(timer)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ensurePreview() {
|
|
36
|
+
if (elements) return elements
|
|
37
|
+
|
|
38
|
+
const root = document.createElement("a")
|
|
39
|
+
root.id = "pmx-source-preview"
|
|
40
|
+
root.className = "pmx-source-preview"
|
|
41
|
+
root.popover = "manual"
|
|
42
|
+
root.tabIndex = -1
|
|
43
|
+
|
|
44
|
+
const card = document.createElement("span")
|
|
45
|
+
card.className = "pmx-source-preview-card"
|
|
46
|
+
|
|
47
|
+
const image = document.createElement("img")
|
|
48
|
+
image.className = "pmx-source-preview-image"
|
|
49
|
+
image.alt = ""
|
|
50
|
+
image.hidden = true
|
|
51
|
+
|
|
52
|
+
const body = document.createElement("span")
|
|
53
|
+
body.className = "pmx-source-preview-body"
|
|
54
|
+
|
|
55
|
+
const source = document.createElement("span")
|
|
56
|
+
source.className = "pmx-source-preview-source"
|
|
57
|
+
|
|
58
|
+
const title = document.createElement("strong")
|
|
59
|
+
title.className = "pmx-source-preview-title"
|
|
60
|
+
|
|
61
|
+
const description = document.createElement("span")
|
|
62
|
+
description.className = "pmx-source-preview-description"
|
|
63
|
+
|
|
64
|
+
body.append(source, title, description)
|
|
65
|
+
card.append(image, body)
|
|
66
|
+
root.append(card)
|
|
67
|
+
root.addEventListener("pointerenter", () => clearTimer(hideTimer))
|
|
68
|
+
root.addEventListener("pointerleave", (event) => {
|
|
69
|
+
const next = event.relatedTarget
|
|
70
|
+
if (next instanceof Node && activeLink?.contains(next)) return
|
|
71
|
+
scheduleHide()
|
|
72
|
+
})
|
|
73
|
+
root.addEventListener("click", hide)
|
|
74
|
+
document.body.append(root)
|
|
75
|
+
elements = { root, image, source, title, description }
|
|
76
|
+
return elements
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isOpen(element: HTMLElement) {
|
|
80
|
+
return element.hasAttribute("data-open")
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function openPopover(element: HTMLElement) {
|
|
84
|
+
element.setAttribute("data-open", "")
|
|
85
|
+
try {
|
|
86
|
+
element.showPopover()
|
|
87
|
+
} catch {
|
|
88
|
+
// The data attribute supplies a non-Popover fallback.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function closePopover(element: HTMLElement) {
|
|
93
|
+
element.removeAttribute("data-open")
|
|
94
|
+
try {
|
|
95
|
+
element.hidePopover()
|
|
96
|
+
} catch {
|
|
97
|
+
// It may already be closed or the browser may not implement Popover.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function hide() {
|
|
102
|
+
requestVersion++
|
|
103
|
+
clearTimer(showTimer)
|
|
104
|
+
clearTimer(hideTimer)
|
|
105
|
+
showTimer = undefined
|
|
106
|
+
hideTimer = undefined
|
|
107
|
+
activeLink = undefined
|
|
108
|
+
if (elements) closePopover(elements.root)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function scheduleHide() {
|
|
112
|
+
clearTimer(showTimer)
|
|
113
|
+
clearTimer(hideTimer)
|
|
114
|
+
hideTimer = window.setTimeout(hide, hideDelay)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function fetchSummary(id: string) {
|
|
118
|
+
const cached = summaries.get(id)
|
|
119
|
+
if (cached) return cached
|
|
120
|
+
const pending = fetch(
|
|
121
|
+
`/api/source-preview/summary?id=${encodeURIComponent(id)}`,
|
|
122
|
+
{ headers: { Accept: "application/json" } },
|
|
123
|
+
)
|
|
124
|
+
.then((response) => (response.ok ? response.json() : undefined))
|
|
125
|
+
.then((value) => value as SourcePreview | undefined)
|
|
126
|
+
.catch(() => undefined)
|
|
127
|
+
summaries.set(id, pending)
|
|
128
|
+
return pending
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function renderSummary(summary: SourcePreview) {
|
|
132
|
+
const { root, image, source, title, description } = ensurePreview()
|
|
133
|
+
root.href = summary.href
|
|
134
|
+
root.setAttribute("aria-label", `Open ${summary.title}`)
|
|
135
|
+
source.textContent = summary.source?.title ?? ""
|
|
136
|
+
source.hidden = !summary.source
|
|
137
|
+
title.textContent = summary.title
|
|
138
|
+
description.textContent = summary.description ?? ""
|
|
139
|
+
description.hidden = !summary.description
|
|
140
|
+
|
|
141
|
+
image.hidden = !summary.image
|
|
142
|
+
image.removeAttribute("src")
|
|
143
|
+
if (summary.image) {
|
|
144
|
+
image.onerror = () => {
|
|
145
|
+
image.hidden = true
|
|
146
|
+
if (activeLink && isOpen(root)) positionPreview(activeLink, root)
|
|
147
|
+
}
|
|
148
|
+
image.src = summary.image
|
|
149
|
+
}
|
|
150
|
+
return root
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function clamp(value: number, min: number, max: number) {
|
|
154
|
+
return Math.min(Math.max(value, min), Math.max(min, max))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function positionPreview(link: HTMLAnchorElement, element: HTMLElement) {
|
|
158
|
+
element.style.left = "0px"
|
|
159
|
+
element.style.top = "0px"
|
|
160
|
+
const linkRect = link.getBoundingClientRect()
|
|
161
|
+
const previewRect = element.getBoundingClientRect()
|
|
162
|
+
const above = linkRect.top - edge
|
|
163
|
+
const below = innerHeight - linkRect.bottom - edge
|
|
164
|
+
const placement =
|
|
165
|
+
below >= previewRect.height || below >= above ? "below" : "above"
|
|
166
|
+
const top =
|
|
167
|
+
placement === "below"
|
|
168
|
+
? linkRect.bottom + gap
|
|
169
|
+
: linkRect.top - previewRect.height - gap
|
|
170
|
+
const left = clamp(
|
|
171
|
+
linkRect.left + linkRect.width / 2 - previewRect.width / 2,
|
|
172
|
+
edge,
|
|
173
|
+
innerWidth - previewRect.width - edge,
|
|
174
|
+
)
|
|
175
|
+
const arrow = clamp(
|
|
176
|
+
linkRect.left + linkRect.width / 2 - left,
|
|
177
|
+
18,
|
|
178
|
+
previewRect.width - 18,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
element.dataset.placement = placement
|
|
182
|
+
element.style.left = `${left}px`
|
|
183
|
+
element.style.top = `${clamp(top, edge, innerHeight - previewRect.height - edge)}px`
|
|
184
|
+
element.style.setProperty("--pmx-source-preview-arrow-x", `${arrow}px`)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function show(link: HTMLAnchorElement, version: number) {
|
|
188
|
+
const id = link.dataset.pmxSourcePreview
|
|
189
|
+
if (!id) return
|
|
190
|
+
const summary = await fetchSummary(id)
|
|
191
|
+
if (!summary || version !== requestVersion || activeLink !== link) return
|
|
192
|
+
|
|
193
|
+
const element = renderSummary(summary)
|
|
194
|
+
openPopover(element)
|
|
195
|
+
positionPreview(link, element)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function scheduleShow(link: HTMLAnchorElement, delay = showDelay) {
|
|
199
|
+
if (activeLink === link && elements && isOpen(elements.root)) {
|
|
200
|
+
clearTimer(hideTimer)
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
clearTimer(showTimer)
|
|
204
|
+
clearTimer(hideTimer)
|
|
205
|
+
activeLink = link
|
|
206
|
+
const version = ++requestVersion
|
|
207
|
+
showTimer = window.setTimeout(() => void show(link, version), delay)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const hover = matchMedia("(hover: hover) and (pointer: fine)")
|
|
211
|
+
document.addEventListener("pointerover", (event) => {
|
|
212
|
+
if (!hover.matches || event.pointerType === "touch") return
|
|
213
|
+
const link = linkFrom(event.target)
|
|
214
|
+
if (!link) return
|
|
215
|
+
const previous = event.relatedTarget
|
|
216
|
+
if (previous instanceof Node && link.contains(previous)) return
|
|
217
|
+
scheduleShow(link)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
document.addEventListener("pointerout", (event) => {
|
|
221
|
+
if (!hover.matches) return
|
|
222
|
+
const link = linkFrom(event.target)
|
|
223
|
+
if (!link || link !== activeLink) return
|
|
224
|
+
const next = event.relatedTarget
|
|
225
|
+
if (
|
|
226
|
+
next instanceof Node &&
|
|
227
|
+
(link.contains(next) || elements?.root.contains(next))
|
|
228
|
+
) {
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
scheduleHide()
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
document.addEventListener("focusin", (event) => {
|
|
235
|
+
const link = linkFrom(event.target)
|
|
236
|
+
if (link) scheduleShow(link, 0)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
document.addEventListener("focusout", (event) => {
|
|
240
|
+
const link = linkFrom(event.target)
|
|
241
|
+
if (!link || link !== activeLink) return
|
|
242
|
+
const next = event.relatedTarget
|
|
243
|
+
if (next instanceof Node && elements?.root.contains(next)) return
|
|
244
|
+
scheduleHide()
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
document.addEventListener("keydown", (event) => {
|
|
248
|
+
if (event.key === "Escape" && elements && isOpen(elements.root)) hide()
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
document.addEventListener("pmx:render", () => {
|
|
252
|
+
summaries.clear()
|
|
253
|
+
hide()
|
|
254
|
+
})
|
|
255
|
+
addEventListener("resize", hide, { passive: true })
|
|
256
|
+
addEventListener("scroll", hide, { passive: true })
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { withReadCache } from "../../host/response.ts"
|
|
2
|
+
import { publicPath } from "../../server/router.ts"
|
|
3
|
+
import {
|
|
4
|
+
route,
|
|
5
|
+
type AppContext,
|
|
6
|
+
type Plugin,
|
|
7
|
+
type ViewContext,
|
|
8
|
+
} from "../types.ts"
|
|
9
|
+
import type { SourcePreview } from "./model.ts"
|
|
10
|
+
|
|
11
|
+
export type { SourcePreview } from "./model.ts"
|
|
12
|
+
|
|
13
|
+
const clientEntry = new URL("./client.ts", import.meta.url)
|
|
14
|
+
const previewAttribute = "data-pmx-source-preview"
|
|
15
|
+
|
|
16
|
+
function sourceForHref(href: string, ctx: ViewContext, app: AppContext) {
|
|
17
|
+
const authoredHref = href.trim()
|
|
18
|
+
if (!authoredHref) return
|
|
19
|
+
let url: URL
|
|
20
|
+
try {
|
|
21
|
+
url = new URL(authoredHref, `http://pathmx.local${ctx.route || "/"}`)
|
|
22
|
+
} catch {
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
if (url.origin !== "http://pathmx.local") return
|
|
26
|
+
const target = app.sources.resolve(url.pathname)
|
|
27
|
+
const source = target ? ctx.view.readSource(target.id) : undefined
|
|
28
|
+
if (!target || !source) return
|
|
29
|
+
|
|
30
|
+
let fragment: string | undefined
|
|
31
|
+
if (url.hash.length > 1) {
|
|
32
|
+
try {
|
|
33
|
+
fragment = decodeURIComponent(url.hash.slice(1))
|
|
34
|
+
} catch {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const block = fragment
|
|
39
|
+
? source.blocks.find((candidate) => candidate.id === fragment)
|
|
40
|
+
: undefined
|
|
41
|
+
if (fragment && !block && target.id === ctx.source.id) return
|
|
42
|
+
return `${target.id}${fragment ? `#${fragment}` : ""}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function previewReference(value: string) {
|
|
46
|
+
const hash = value.indexOf("#")
|
|
47
|
+
return hash === -1
|
|
48
|
+
? { id: value }
|
|
49
|
+
: { id: value.slice(0, hash), fragment: value.slice(hash + 1) }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function withFragment(href: string, fragment: string | undefined) {
|
|
53
|
+
return fragment ? `${href}#${encodeURIComponent(fragment)}` : href
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function notFound() {
|
|
57
|
+
return new Response("Not Found", { status: 404 })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Add viewer-authorized Source summaries to internal links on demand. */
|
|
61
|
+
export function SourcePreviewPlugin(): Plugin {
|
|
62
|
+
return {
|
|
63
|
+
id: "source-preview",
|
|
64
|
+
|
|
65
|
+
routes: [
|
|
66
|
+
route.get("summary", async (ctx) => {
|
|
67
|
+
const reference = ctx.url.searchParams.get("id")?.trim()
|
|
68
|
+
if (!reference) return notFound()
|
|
69
|
+
const resolved = previewReference(reference)
|
|
70
|
+
if (!resolved.id) return notFound()
|
|
71
|
+
const target = ctx.source(resolved.id)
|
|
72
|
+
if (!target) return notFound()
|
|
73
|
+
const href = publicPath(target)
|
|
74
|
+
if (ctx.resolve(href)?.id !== target.id) return notFound()
|
|
75
|
+
|
|
76
|
+
const source = (await ctx.view()).readSource(target.id)
|
|
77
|
+
if (!source) return notFound()
|
|
78
|
+
const block = resolved.fragment
|
|
79
|
+
? source.blocks.find(
|
|
80
|
+
(candidate) => candidate.id === resolved.fragment,
|
|
81
|
+
)
|
|
82
|
+
: undefined
|
|
83
|
+
|
|
84
|
+
const access = await ctx.readAccess(target)
|
|
85
|
+
if (access === "denied") return notFound()
|
|
86
|
+
const title = block?.meta.title || source.meta.title
|
|
87
|
+
const description = block?.meta.description ?? source.meta.description
|
|
88
|
+
const image = block?.meta.image ?? source.meta.image
|
|
89
|
+
|
|
90
|
+
const preview: SourcePreview = {
|
|
91
|
+
id: reference,
|
|
92
|
+
href: withFragment(href, resolved.fragment),
|
|
93
|
+
title,
|
|
94
|
+
...(description ? { description } : {}),
|
|
95
|
+
...(image ? { image } : {}),
|
|
96
|
+
...(block ? { source: { href, title: source.meta.title } } : {}),
|
|
97
|
+
}
|
|
98
|
+
const response = Response.json(preview, {
|
|
99
|
+
headers: { "Cache-Control": "no-cache" },
|
|
100
|
+
})
|
|
101
|
+
return withReadCache(response, access, await ctx.session())
|
|
102
|
+
}),
|
|
103
|
+
],
|
|
104
|
+
|
|
105
|
+
streamSelectors(ctx, app) {
|
|
106
|
+
let proseDisabledDepth = 0
|
|
107
|
+
return {
|
|
108
|
+
'[data-pmx-prose="off"]': {
|
|
109
|
+
element(element) {
|
|
110
|
+
proseDisabledDepth++
|
|
111
|
+
element.onEndTag(() => {
|
|
112
|
+
proseDisabledDepth--
|
|
113
|
+
})
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
".pmx-prose a[href]": {
|
|
117
|
+
element(element) {
|
|
118
|
+
if (proseDisabledDepth) return
|
|
119
|
+
if (element.getAttribute("download") != null) return
|
|
120
|
+
const href = element.getAttribute("href")
|
|
121
|
+
const target = href ? sourceForHref(href, ctx, app) : undefined
|
|
122
|
+
if (target) element.setAttribute(previewAttribute, target)
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
async documentAssets(ctx) {
|
|
129
|
+
await ctx.addBrowserBundle(clientEntry, "source-preview")
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
.pmx-source-preview {
|
|
2
|
+
position: fixed;
|
|
3
|
+
inset: auto;
|
|
4
|
+
z-index: 1000;
|
|
5
|
+
box-sizing: border-box;
|
|
6
|
+
width: min(22rem, calc(100vw - 1.5rem));
|
|
7
|
+
max-height: calc(100vh - 1.5rem);
|
|
8
|
+
margin: 0;
|
|
9
|
+
padding-block: 0.45rem;
|
|
10
|
+
overflow: visible;
|
|
11
|
+
border: 0;
|
|
12
|
+
background: transparent;
|
|
13
|
+
color: var(--pmx-color-fg);
|
|
14
|
+
text-decoration: none;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.pmx-source-preview:not([data-open]) {
|
|
18
|
+
display: none;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.pmx-source-preview::before {
|
|
22
|
+
position: absolute;
|
|
23
|
+
left: var(--pmx-source-preview-arrow-x);
|
|
24
|
+
z-index: 1;
|
|
25
|
+
box-sizing: border-box;
|
|
26
|
+
width: 0.65rem;
|
|
27
|
+
height: 0.65rem;
|
|
28
|
+
background: var(--pmx-color-surface);
|
|
29
|
+
content: "";
|
|
30
|
+
transform: translateX(-50%) rotate(45deg);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.pmx-source-preview[data-placement="below"]::before {
|
|
34
|
+
top: 0.15rem;
|
|
35
|
+
border-block-start: 1px solid var(--pmx-color-border);
|
|
36
|
+
border-inline-start: 1px solid var(--pmx-color-border);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.pmx-source-preview[data-placement="above"]::before {
|
|
40
|
+
bottom: 0.15rem;
|
|
41
|
+
border-block-end: 1px solid var(--pmx-color-border);
|
|
42
|
+
border-inline-end: 1px solid var(--pmx-color-border);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.pmx-source-preview-card,
|
|
46
|
+
.pmx-source-preview-body,
|
|
47
|
+
.pmx-source-preview-source,
|
|
48
|
+
.pmx-source-preview-title,
|
|
49
|
+
.pmx-source-preview-description {
|
|
50
|
+
display: block;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.pmx-source-preview-card {
|
|
54
|
+
max-height: calc(100vh - 2.4rem);
|
|
55
|
+
overflow: auto;
|
|
56
|
+
overscroll-behavior: contain;
|
|
57
|
+
border: 1px solid var(--pmx-color-border);
|
|
58
|
+
border-radius: var(--pmx-radius-lg);
|
|
59
|
+
background: var(--pmx-color-surface);
|
|
60
|
+
box-shadow: var(--pmx-shadow-md);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.pmx-source-preview-image {
|
|
64
|
+
display: block;
|
|
65
|
+
width: 100%;
|
|
66
|
+
aspect-ratio: 2 / 1;
|
|
67
|
+
object-fit: cover;
|
|
68
|
+
border-block-end: 1px solid var(--pmx-color-border);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.pmx-source-preview-image[hidden] {
|
|
72
|
+
display: none;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.pmx-source-preview-body {
|
|
76
|
+
padding: 0.9rem 1rem 1rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.pmx-source-preview-title {
|
|
80
|
+
font-family: var(--pmx-font-heading);
|
|
81
|
+
font-size: 1rem;
|
|
82
|
+
font-weight: 700;
|
|
83
|
+
line-height: 1.25;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.pmx-source-preview-source {
|
|
87
|
+
margin-block-end: 0.35rem;
|
|
88
|
+
color: var(--pmx-color-muted);
|
|
89
|
+
font-size: 0.75rem;
|
|
90
|
+
font-weight: 600;
|
|
91
|
+
line-height: 1.25;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.pmx-source-preview-source[hidden] {
|
|
95
|
+
display: none;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.pmx-source-preview-description {
|
|
99
|
+
margin-block-start: 0.45rem;
|
|
100
|
+
color: var(--pmx-color-muted);
|
|
101
|
+
font-size: 0.925rem;
|
|
102
|
+
line-height: 1.45;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.pmx-source-preview-description[hidden] {
|
|
106
|
+
display: none;
|
|
107
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { cachedRouteTargets } from "../invalidation.ts"
|
|
2
|
+
import { parseLinks, type Source, type SourceLink } from "../source/index.ts"
|
|
3
|
+
import type {
|
|
4
|
+
DependencyContext,
|
|
5
|
+
DocumentAsset,
|
|
6
|
+
DocumentStylesheet,
|
|
7
|
+
Plugin,
|
|
8
|
+
} from "./types.ts"
|
|
9
|
+
import { directiveDefinition } from "./directive-definitions.ts"
|
|
10
|
+
|
|
11
|
+
const MEDIA = new Set(["print", "screen"])
|
|
12
|
+
const EXTERNAL = /^[a-z][a-z\d+.-]*:/i
|
|
13
|
+
|
|
14
|
+
function resolveTarget(source: Source, target: string) {
|
|
15
|
+
return target.startsWith("/")
|
|
16
|
+
? target
|
|
17
|
+
: new URL(target, `file://${source.path}`).pathname
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function styleLinks(source: Source, links: readonly SourceLink[]) {
|
|
21
|
+
return links.filter((link) => link.directive?.name === "styles")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolveStylesheet(
|
|
25
|
+
source: Source,
|
|
26
|
+
link: SourceLink,
|
|
27
|
+
): DocumentStylesheet | undefined {
|
|
28
|
+
if (link.directive?.name !== "styles") return
|
|
29
|
+
const raw = link.href.split("#")[0]?.split("?")[0] ?? ""
|
|
30
|
+
if (
|
|
31
|
+
!raw ||
|
|
32
|
+
EXTERNAL.test(raw) ||
|
|
33
|
+
raw.startsWith("//") ||
|
|
34
|
+
raw.startsWith("~/")
|
|
35
|
+
)
|
|
36
|
+
return
|
|
37
|
+
const href = resolveTarget(source, raw)
|
|
38
|
+
if (!href.endsWith(".css")) return
|
|
39
|
+
const media = link.directive.qualifiers.find((item) => MEDIA.has(item))
|
|
40
|
+
return { type: "stylesheet", href, ...(media ? { media } : {}) }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stylesheetHref(asset: DocumentAsset | DocumentStylesheet) {
|
|
44
|
+
return asset.type === "stylesheet" && "href" in asset ? asset.href : undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function pushUnique(assets: DocumentAsset[], asset: DocumentStylesheet) {
|
|
48
|
+
const href = stylesheetHref(asset)
|
|
49
|
+
if (href && assets.some((item) => stylesheetHref(item) === href)) return
|
|
50
|
+
assets.push(asset)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function addStylesheetDependency(
|
|
54
|
+
dependencies: DependencyContext,
|
|
55
|
+
asset: DocumentStylesheet,
|
|
56
|
+
) {
|
|
57
|
+
const href = stylesheetHref(asset)
|
|
58
|
+
if (href) dependencies.depend({ type: "source", path: href })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function sourceStyleLinks(source: Source) {
|
|
62
|
+
return source.blocks.length
|
|
63
|
+
? source.blocks.flatMap((block) => block.links)
|
|
64
|
+
: source.links
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function globalStylesheets(source: Source, links: readonly SourceLink[]) {
|
|
68
|
+
const assets: DocumentStylesheet[] = []
|
|
69
|
+
for (const link of styleLinks(source, links)) {
|
|
70
|
+
if (!link.directive?.qualifiers.includes("global")) continue
|
|
71
|
+
const asset = resolveStylesheet(source, link)
|
|
72
|
+
if (asset) pushUnique(assets, asset)
|
|
73
|
+
}
|
|
74
|
+
return assets
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function stylesheetSignature(assets: readonly DocumentStylesheet[]) {
|
|
78
|
+
return assets
|
|
79
|
+
.map((asset) => `${stylesheetHref(asset) ?? ""}\0${asset.media ?? ""}`)
|
|
80
|
+
.join("\n")
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function authoredGlobalSignature(source?: Source) {
|
|
84
|
+
return stylesheetSignature(
|
|
85
|
+
source ? globalStylesheets(source, parseLinks(source.body)) : [],
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function StylesPlugin(): Plugin {
|
|
90
|
+
const global = new Map<string, DocumentStylesheet[]>()
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
id: "styles",
|
|
94
|
+
index(source) {
|
|
95
|
+
const assets = globalStylesheets(source, sourceStyleLinks(source))
|
|
96
|
+
// Only track contributing origins: page() walks this map for every
|
|
97
|
+
// page, so empty entries would add O(sources) work per page.
|
|
98
|
+
if (assets.length) global.set(source.path, assets)
|
|
99
|
+
else global.delete(source.path)
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
unindex(sourcePath) {
|
|
103
|
+
global.delete(sourcePath)
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
precompile(markdown, ctx) {
|
|
107
|
+
const styles = styleLinks(
|
|
108
|
+
ctx.source,
|
|
109
|
+
ctx.block?.links ?? ctx.source.links,
|
|
110
|
+
)
|
|
111
|
+
if (!styles.length) return markdown
|
|
112
|
+
let current = markdown
|
|
113
|
+
for (const link of styles)
|
|
114
|
+
current = current.replace(directiveDefinition("styles", link.href), "")
|
|
115
|
+
return current
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
invalidate({ before, after }, app) {
|
|
119
|
+
if (authoredGlobalSignature(before) === authoredGlobalSignature(after))
|
|
120
|
+
return
|
|
121
|
+
return { targets: cachedRouteTargets(app.sources.builtPages()) }
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
page(html, ctx) {
|
|
125
|
+
for (const assets of global.values()) {
|
|
126
|
+
for (const asset of assets) addStylesheetDependency(ctx, asset)
|
|
127
|
+
}
|
|
128
|
+
for (const link of styleLinks(ctx.source, ctx.source.links)) {
|
|
129
|
+
if (link.directive?.qualifiers.includes("global")) continue
|
|
130
|
+
const asset = resolveStylesheet(ctx.source, link)
|
|
131
|
+
if (asset) {
|
|
132
|
+
ctx.addAsset(asset)
|
|
133
|
+
addStylesheetDependency(ctx, asset)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return html
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
documentAssets(ctx) {
|
|
140
|
+
for (const origin of [...global.keys()].sort()) {
|
|
141
|
+
for (const asset of global.get(origin) ?? []) ctx.addAsset(asset)
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { moonIconSvg, sunIconSvg } from "../../icons/lucide.ts"
|
|
2
|
+
import { defineViewComponent } from "../types.ts"
|
|
3
|
+
|
|
4
|
+
export const SchemeToggleComponent = defineViewComponent<void>({
|
|
5
|
+
tag: "theme:scheme-toggle",
|
|
6
|
+
client: new URL("./scheme.js", import.meta.url),
|
|
7
|
+
render() {
|
|
8
|
+
return `<button class="pmx-scheme-toggle" type="button" aria-label="Toggle color scheme" title="Toggle color scheme"><span data-pmx-scheme-icon="light">${sunIconSvg}</span><span data-pmx-scheme-icon="dark">${moonIconSvg}</span></button>`
|
|
9
|
+
},
|
|
10
|
+
})
|