@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.
Files changed (195) hide show
  1. package/LICENSE.md +172 -0
  2. package/README.md +111 -0
  3. package/actions/actions.ts +307 -0
  4. package/actions/draft.ts +105 -0
  5. package/actions/transaction.ts +164 -0
  6. package/assets.ts +149 -0
  7. package/authority/access.ts +820 -0
  8. package/authority/actors.ts +271 -0
  9. package/authority/agents.ts +95 -0
  10. package/authority/credentials.ts +144 -0
  11. package/authority/model.ts +71 -0
  12. package/authority/sharing.ts +89 -0
  13. package/browser-bundle.ts +61 -0
  14. package/canonical.ts +41 -0
  15. package/cli/dev.ts +134 -0
  16. package/cli/export.ts +74 -0
  17. package/cli/index.ts +52 -0
  18. package/cli/lint.ts +164 -0
  19. package/cli/mv.ts +30 -0
  20. package/cli/perms.ts +182 -0
  21. package/cli/plugin-packages.ts +159 -0
  22. package/cli/plugins.ts +95 -0
  23. package/cli/query.ts +225 -0
  24. package/cli/readme.md +95 -0
  25. package/cli/release.ts +75 -0
  26. package/cli/rm.ts +22 -0
  27. package/cli/serve.ts +100 -0
  28. package/cli/tree.ts +131 -0
  29. package/cli/update.ts +78 -0
  30. package/cli/utils.ts +53 -0
  31. package/core.ts +96 -0
  32. package/database.ts +189 -0
  33. package/dependencies.ts +60 -0
  34. package/dist/pathmx.js +1224 -0
  35. package/document.ts +194 -0
  36. package/environment.ts +110 -0
  37. package/environments/bun-plugins.ts +68 -0
  38. package/environments/bun-publish.ts +194 -0
  39. package/environments/bun.ts +223 -0
  40. package/globals.d.ts +14 -0
  41. package/graph.ts +153 -0
  42. package/host/compiler.ts +314 -0
  43. package/host/create.ts +66 -0
  44. package/host/engine.ts +630 -0
  45. package/host/plugin-host.ts +487 -0
  46. package/host/render.ts +102 -0
  47. package/host/response.ts +20 -0
  48. package/host/shell.html +15 -0
  49. package/host/source-sync.ts +254 -0
  50. package/html-attributes.ts +15 -0
  51. package/html.ts +48 -0
  52. package/icons/LICENSE +43 -0
  53. package/icons/lucide.ts +54 -0
  54. package/index.ts +75 -0
  55. package/invalidation.ts +85 -0
  56. package/log.ts +84 -0
  57. package/ops/href.ts +87 -0
  58. package/ops/index.ts +9 -0
  59. package/ops/move-source.ts +114 -0
  60. package/ops/plan.ts +47 -0
  61. package/ops/remove-source.ts +70 -0
  62. package/package.json +46 -0
  63. package/path.ts +75 -0
  64. package/pattern.ts +314 -0
  65. package/perf.ts +88 -0
  66. package/plugins/actor-path.ts +43 -0
  67. package/plugins/actor-profile.ts +55 -0
  68. package/plugins/agent-browsing.ts +183 -0
  69. package/plugins/app-component.ts +23 -0
  70. package/plugins/block-layout/index.ts +78 -0
  71. package/plugins/block-layout/theme.css +90 -0
  72. package/plugins/callout/index.ts +236 -0
  73. package/plugins/callout/theme.css +138 -0
  74. package/plugins/canonical-links.ts +144 -0
  75. package/plugins/code-highlight/client.ts +89 -0
  76. package/plugins/code-highlight/index.ts +147 -0
  77. package/plugins/code-highlight/theme.css +89 -0
  78. package/plugins/context.ts +131 -0
  79. package/plugins/datatype/Datatype[wdth,wght].woff2 +0 -0
  80. package/plugins/datatype/OFL.txt +96 -0
  81. package/plugins/datatype/index.ts +287 -0
  82. package/plugins/definition-list.ts +236 -0
  83. package/plugins/directive-definitions.ts +13 -0
  84. package/plugins/embedding/client.ts +88 -0
  85. package/plugins/embedding/index.ts +43 -0
  86. package/plugins/image/index.ts +68 -0
  87. package/plugins/image/theme.css +43 -0
  88. package/plugins/include.ts +119 -0
  89. package/plugins/index.ts +91 -0
  90. package/plugins/input/authoring.ts +236 -0
  91. package/plugins/input/config.ts +85 -0
  92. package/plugins/input/index.ts +359 -0
  93. package/plugins/input/model.ts +173 -0
  94. package/plugins/input/state.ts +272 -0
  95. package/plugins/layout.ts +313 -0
  96. package/plugins/literate/client.ts +208 -0
  97. package/plugins/literate/index.ts +742 -0
  98. package/plugins/math/index.ts +140 -0
  99. package/plugins/meta.ts +42 -0
  100. package/plugins/navigation/index.ts +158 -0
  101. package/plugins/navigation/model.ts +364 -0
  102. package/plugins/preset.ts +65 -0
  103. package/plugins/query/index.ts +322 -0
  104. package/plugins/rss/index.ts +46 -0
  105. package/plugins/runtime/actions.ts +62 -0
  106. package/plugins/runtime/client.ts +8 -0
  107. package/plugins/runtime/document.ts +506 -0
  108. package/plugins/runtime/error.ts +154 -0
  109. package/plugins/runtime/heading-links.ts +31 -0
  110. package/plugins/runtime/index.ts +104 -0
  111. package/plugins/runtime/live.ts +51 -0
  112. package/plugins/runtime/navigation.ts +113 -0
  113. package/plugins/runtime/pending.ts +53 -0
  114. package/plugins/runtime/publication.ts +19 -0
  115. package/plugins/runtime/start.ts +177 -0
  116. package/plugins/runtime/static-client.ts +6 -0
  117. package/plugins/runtime/static-live.ts +188 -0
  118. package/plugins/runtime/view-transition.ts +72 -0
  119. package/plugins/runtime/viewport.ts +110 -0
  120. package/plugins/schema.ts +24 -0
  121. package/plugins/scripts.ts +140 -0
  122. package/plugins/source-preview/client.ts +256 -0
  123. package/plugins/source-preview/index.ts +132 -0
  124. package/plugins/source-preview/model.ts +11 -0
  125. package/plugins/source-preview/preview.css +107 -0
  126. package/plugins/styles.ts +145 -0
  127. package/plugins/theme/components.ts +10 -0
  128. package/plugins/theme/foundation.css +358 -0
  129. package/plugins/theme/index.ts +333 -0
  130. package/plugins/theme/prose.css +284 -0
  131. package/plugins/theme/scheme.js +63 -0
  132. package/plugins/theme/toggle.css +44 -0
  133. package/plugins/theme/view-transition.css +68 -0
  134. package/plugins/toc/client.ts +146 -0
  135. package/plugins/toc/index.ts +232 -0
  136. package/plugins/toc/theme.css +172 -0
  137. package/plugins/types.ts +538 -0
  138. package/problem.ts +41 -0
  139. package/project.ts +6 -0
  140. package/publish/build.ts +402 -0
  141. package/publish/concurrent.ts +32 -0
  142. package/publish/identity.ts +63 -0
  143. package/publish/index.ts +11 -0
  144. package/publish/location.ts +167 -0
  145. package/publish/model.ts +58 -0
  146. package/publish/plan.ts +300 -0
  147. package/publish/validate.ts +150 -0
  148. package/query/candidate-index.ts +101 -0
  149. package/query/dependency-index.ts +99 -0
  150. package/query/fields.ts +216 -0
  151. package/query/invalidation.ts +48 -0
  152. package/query/kernel.ts +776 -0
  153. package/query/model.ts +79 -0
  154. package/repository.ts +374 -0
  155. package/runtime-profile.ts +34 -0
  156. package/schema/model.ts +92 -0
  157. package/schema/registry.ts +642 -0
  158. package/schema/standard.ts +226 -0
  159. package/server/context.ts +94 -0
  160. package/server/css-imports.ts +66 -0
  161. package/server/http.ts +333 -0
  162. package/server/page.ts +21 -0
  163. package/server/protocols.ts +12 -0
  164. package/server/representations.ts +66 -0
  165. package/server/router.ts +131 -0
  166. package/server/routes.ts +202 -0
  167. package/server/serve.ts +5 -0
  168. package/server/watch.ts +120 -0
  169. package/source/blocks.ts +71 -0
  170. package/source/format.ts +164 -0
  171. package/source/index.ts +67 -0
  172. package/source/interpolate.ts +63 -0
  173. package/source/markdown/blocks.ts +46 -0
  174. package/source/markdown/comments.ts +32 -0
  175. package/source/markdown/fences.ts +67 -0
  176. package/source/markdown/headings.ts +88 -0
  177. package/source/markdown/index.ts +34 -0
  178. package/source/markdown/inline-code.ts +6 -0
  179. package/source/markdown/links.ts +326 -0
  180. package/source/markdown/lists.ts +223 -0
  181. package/source/markdown/prose.ts +74 -0
  182. package/source/markdown/scan.ts +79 -0
  183. package/source/markdown/text.ts +82 -0
  184. package/source/meta.ts +87 -0
  185. package/source/source.ts +121 -0
  186. package/source/topmatter.ts +57 -0
  187. package/source/utils.ts +21 -0
  188. package/source-diagnostic.ts +63 -0
  189. package/text-file.ts +26 -0
  190. package/util.ts +35 -0
  191. package/view/components.ts +499 -0
  192. package/view/model.ts +91 -0
  193. package/view/projection.ts +162 -0
  194. package/view/session.ts +146 -0
  195. 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,11 @@
1
+ export type SourcePreview = Readonly<{
2
+ id: string
3
+ href: string
4
+ title: string
5
+ description?: string
6
+ image?: string
7
+ source?: Readonly<{
8
+ href: string
9
+ title: string
10
+ }>
11
+ }>
@@ -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
+ })