@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,188 @@
1
+ import type { LiveSubscription, LiveSubscriptionFactory } from "./live.ts"
2
+ import { publicationPath } from "./publication.ts"
3
+
4
+ const POLL_INTERVAL = 30_000
5
+ const MAX_BACKOFF = 5 * 60_000
6
+
7
+ type BuildRecord = Readonly<{
8
+ version: string
9
+ pages: string
10
+ }>
11
+
12
+ type PageMap = Readonly<{
13
+ version: 1
14
+ build: string
15
+ pages: Readonly<Record<string, string>>
16
+ }>
17
+
18
+ function currentPage() {
19
+ return document.documentElement.dataset.pmxPage
20
+ }
21
+
22
+ function currentRoute() {
23
+ const view = document.querySelector<HTMLElement>("[data-pmx-view]")
24
+ return view?.dataset.pmxRoute || location.pathname
25
+ }
26
+
27
+ function isBuildRecord(value: unknown): value is BuildRecord {
28
+ return (
29
+ typeof value === "object" &&
30
+ value !== null &&
31
+ "version" in value &&
32
+ typeof value.version === "string" &&
33
+ value.version.length > 0 &&
34
+ "pages" in value &&
35
+ typeof value.pages === "string" &&
36
+ value.pages.length > 0
37
+ )
38
+ }
39
+
40
+ function isPageMap(value: unknown, build: string): value is PageMap {
41
+ if (
42
+ typeof value !== "object" ||
43
+ value === null ||
44
+ !("version" in value) ||
45
+ value.version !== 1 ||
46
+ !("build" in value) ||
47
+ value.build !== build ||
48
+ !("pages" in value) ||
49
+ typeof value.pages !== "object" ||
50
+ value.pages === null
51
+ ) {
52
+ return false
53
+ }
54
+ return Object.values(value.pages).every(
55
+ (leaf) => typeof leaf === "string" && leaf.length > 0,
56
+ )
57
+ }
58
+
59
+ function jitter(delay: number) {
60
+ return Math.round(delay * (0.9 + Math.random() * 0.2))
61
+ }
62
+
63
+ export const createPollingLiveSubscription: LiveSubscriptionFactory = (
64
+ onSync,
65
+ ): LiveSubscription => {
66
+ let timer: ReturnType<typeof setTimeout> | undefined
67
+ let controller: AbortController | undefined
68
+ let loaded: { record: BuildRecord; map: PageMap } | undefined
69
+ let running = false
70
+ let failures = 0
71
+
72
+ const schedule = (delay = POLL_INTERVAL) => {
73
+ if (!running || timer || document.visibilityState === "hidden") return
74
+ timer = setTimeout(() => {
75
+ timer = undefined
76
+ void check()
77
+ }, jitter(delay))
78
+ }
79
+
80
+ const loadPageMap = async (record: BuildRecord, signal: AbortSignal) => {
81
+ if (
82
+ loaded?.record.version === record.version &&
83
+ loaded.record.pages === record.pages
84
+ ) {
85
+ return loaded.map
86
+ }
87
+ const url = new URL(record.pages, location.href)
88
+ if (url.origin !== location.origin) {
89
+ throw new Error("PathMX page map must use the publication origin.")
90
+ }
91
+ const response = await fetch(url, {
92
+ cache: "force-cache",
93
+ headers: { Accept: "application/json" },
94
+ signal,
95
+ })
96
+ if (!response.ok) {
97
+ throw new Error(`Page map returned ${response.status}.`)
98
+ }
99
+ const map: unknown = await response.json()
100
+ if (!isPageMap(map, record.version)) {
101
+ throw new Error("Invalid PathMX page map.")
102
+ }
103
+ loaded = { record, map }
104
+ return map
105
+ }
106
+
107
+ const pageIsCurrent = async (record: BuildRecord, signal: AbortSignal) => {
108
+ const map = await loadPageMap(record, signal)
109
+ return map.pages[currentRoute()] === currentPage()
110
+ }
111
+
112
+ const check = async () => {
113
+ if (!running) return
114
+ if (document.visibilityState === "hidden") return
115
+
116
+ controller?.abort()
117
+ const request = new AbortController()
118
+ controller = request
119
+ try {
120
+ const response = await fetch(
121
+ new URL(publicationPath("/.pmx/build.json"), location.origin),
122
+ {
123
+ cache: "no-store",
124
+ headers: { Accept: "application/json" },
125
+ signal: request.signal,
126
+ },
127
+ )
128
+ if (!response.ok) {
129
+ throw new Error(`Build check returned ${response.status}.`)
130
+ }
131
+ const record: unknown = await response.json()
132
+ if (!isBuildRecord(record)) {
133
+ throw new Error("Invalid PathMX build record.")
134
+ }
135
+ if (await pageIsCurrent(record, request.signal)) {
136
+ failures = 0
137
+ } else {
138
+ await onSync()
139
+ failures = (await pageIsCurrent(record, request.signal))
140
+ ? 0
141
+ : failures + 1
142
+ }
143
+ } catch (error) {
144
+ if (!request.signal.aborted) failures++
145
+ } finally {
146
+ if (controller === request) controller = undefined
147
+ const backoff = Math.min(
148
+ POLL_INTERVAL * 2 ** Math.min(failures, 4),
149
+ MAX_BACKOFF,
150
+ )
151
+ schedule(backoff)
152
+ }
153
+ }
154
+
155
+ const onVisibilityChange = () => {
156
+ if (!running) return
157
+ if (timer) clearTimeout(timer)
158
+ timer = undefined
159
+ if (document.visibilityState !== "visible") {
160
+ controller?.abort()
161
+ return
162
+ }
163
+ schedule(0)
164
+ }
165
+ document.addEventListener("visibilitychange", onVisibilityChange)
166
+
167
+ return {
168
+ bind() {
169
+ if (!running) {
170
+ running = true
171
+ schedule()
172
+ return
173
+ }
174
+ if (controller) return
175
+ if (timer) clearTimeout(timer)
176
+ timer = undefined
177
+ schedule(0)
178
+ },
179
+
180
+ close() {
181
+ running = false
182
+ if (timer) clearTimeout(timer)
183
+ timer = undefined
184
+ controller?.abort()
185
+ controller = undefined
186
+ },
187
+ }
188
+ }
@@ -0,0 +1,72 @@
1
+ export const viewTransitionType = {
2
+ navigation: "pmx-navigation",
3
+ traverse: "pmx-traverse",
4
+ forward: "pmx-forward",
5
+ back: "pmx-back",
6
+ } as const
7
+
8
+ export type ViewTransitionType =
9
+ (typeof viewTransitionType)[keyof typeof viewTransitionType]
10
+
11
+ export type ViewTransitionNavigation = {
12
+ type: "navigation" | "traverse"
13
+ currentIndex?: number
14
+ destinationIndex?: number
15
+ }
16
+
17
+ type ViewTransitionUpdate = () => void | Promise<void>
18
+
19
+ type StartViewTransition = {
20
+ (update?: ViewTransitionUpdate): ViewTransition
21
+ (options: { update?: ViewTransitionUpdate; types?: string[] }): ViewTransition
22
+ }
23
+
24
+ function historyIndex(value: number | undefined) {
25
+ return value == null || value < 0 ? undefined : value
26
+ }
27
+
28
+ export function navigationViewTransitionTypes(
29
+ navigation: ViewTransitionNavigation,
30
+ ): readonly ViewTransitionType[] {
31
+ const renderType = viewTransitionType[navigation.type]
32
+ if (navigation.type !== "traverse") {
33
+ return [renderType, viewTransitionType.forward]
34
+ }
35
+
36
+ const from = historyIndex(navigation.currentIndex)
37
+ const to = historyIndex(navigation.destinationIndex)
38
+ if (from == null || to == null || from === to) return [renderType]
39
+ return [
40
+ renderType,
41
+ to < from ? viewTransitionType.back : viewTransitionType.forward,
42
+ ]
43
+ }
44
+
45
+ function supportsTypedStart() {
46
+ try {
47
+ return CSS.supports("selector(:active-view-transition-type(pmx-forward))")
48
+ } catch {
49
+ return false
50
+ }
51
+ }
52
+
53
+ export function startPathMXViewTransition(
54
+ update: ViewTransitionUpdate,
55
+ types: readonly string[],
56
+ ) {
57
+ const start = document.startViewTransition.bind(
58
+ document,
59
+ ) as StartViewTransition
60
+ const listed = [...types]
61
+ const transition =
62
+ listed.length && supportsTypedStart()
63
+ ? start({ update, types: listed })
64
+ : start(update)
65
+ const set = (
66
+ transition as ViewTransition & { types?: { add(type: string): void } }
67
+ ).types
68
+ if (set) {
69
+ for (const type of listed) set.add(type)
70
+ }
71
+ return transition
72
+ }
@@ -0,0 +1,110 @@
1
+ const HEADER_SELECTOR = "[data-pmx-sticky-header]"
2
+ const HIDE_THRESHOLD = 12
3
+
4
+ type ViewportChrome = {
5
+ header: HTMLElement
6
+ observer: ResizeObserver
7
+ height: number
8
+ lastScrollY: number
9
+ direction: -1 | 0 | 1
10
+ directionDistance: number
11
+ hidden: boolean
12
+ sticky: boolean
13
+ }
14
+
15
+ function setInset(height: number) {
16
+ document.documentElement.style.setProperty(
17
+ "--pmx-viewport-inset-block-start",
18
+ `${height}px`,
19
+ )
20
+ }
21
+
22
+ function setHidden(chrome: ViewportChrome, hidden: boolean) {
23
+ chrome.hidden = hidden
24
+ chrome.header.toggleAttribute("data-pmx-hidden", hidden)
25
+ }
26
+
27
+ function measure(chrome: ViewportChrome) {
28
+ chrome.sticky = getComputedStyle(chrome.header).position === "sticky"
29
+ chrome.height = chrome.sticky
30
+ ? chrome.header.getBoundingClientRect().height
31
+ : 0
32
+ setInset(chrome.height)
33
+ if (!chrome.sticky) setHidden(chrome, false)
34
+ }
35
+
36
+ function createChrome(header: HTMLElement): ViewportChrome {
37
+ const chrome: ViewportChrome = {
38
+ header,
39
+ observer: new ResizeObserver(() => measure(chrome)),
40
+ height: 0,
41
+ lastScrollY: Math.max(0, scrollY),
42
+ direction: 0,
43
+ directionDistance: 0,
44
+ hidden: false,
45
+ sticky: false,
46
+ }
47
+ chrome.observer.observe(header)
48
+ measure(chrome)
49
+ return chrome
50
+ }
51
+
52
+ /** Coordinates opt-in sticky chrome with shared viewport CSS variables. */
53
+ export function installViewportChrome() {
54
+ let chrome: ViewportChrome | undefined
55
+ let listening = false
56
+
57
+ let scrollFrame = 0
58
+ const onScroll = () => {
59
+ if (!chrome || scrollFrame) return
60
+ scrollFrame = requestAnimationFrame(() => {
61
+ scrollFrame = 0
62
+ if (!chrome?.sticky) return
63
+
64
+ const current = Math.max(0, scrollY)
65
+ const delta = current - chrome.lastScrollY
66
+ chrome.lastScrollY = current
67
+ if (!delta) return
68
+
69
+ const direction = delta > 0 ? 1 : -1
70
+ if (direction !== chrome.direction) {
71
+ chrome.direction = direction
72
+ chrome.directionDistance = 0
73
+ }
74
+ chrome.directionDistance += Math.abs(delta)
75
+
76
+ if (current <= chrome.height || chrome.header.matches(":focus-within")) {
77
+ setHidden(chrome, false)
78
+ } else if (chrome.directionDistance >= HIDE_THRESHOLD) {
79
+ setHidden(chrome, direction > 0)
80
+ chrome.directionDistance = 0
81
+ }
82
+ })
83
+ }
84
+
85
+ const listen = (enabled: boolean) => {
86
+ if (listening === enabled) return
87
+ listening = enabled
88
+ if (enabled) addEventListener("scroll", onScroll, { passive: true })
89
+ else removeEventListener("scroll", onScroll)
90
+ }
91
+
92
+ const reconcile = () => {
93
+ const header = document.querySelector<HTMLElement>(HEADER_SELECTOR)
94
+ if (chrome?.header === header) {
95
+ setHidden(chrome, chrome.hidden)
96
+ measure(chrome)
97
+ return
98
+ }
99
+ chrome?.observer.disconnect()
100
+ chrome = header ? createChrome(header) : undefined
101
+ if (!chrome) setInset(0)
102
+ listen(chrome != null)
103
+ }
104
+
105
+ addEventListener("resize", () => chrome && measure(chrome))
106
+ document.addEventListener("pmx:render", () =>
107
+ requestAnimationFrame(reconcile),
108
+ )
109
+ reconcile()
110
+ }
@@ -0,0 +1,24 @@
1
+ import type { Plugin } from "./types.ts"
2
+
3
+ /** Baseline application Schema. Feature Plugins refine the node types they own. */
4
+ export function SchemaPlugin(): Plugin {
5
+ return {
6
+ id: "schema",
7
+ schema: {
8
+ sources: {
9
+ page: {
10
+ label: "Page",
11
+ description: "A general rendered document.",
12
+ icon: "file-text",
13
+ color: "slate",
14
+ },
15
+ map: {
16
+ label: "Map",
17
+ description: "Source-type schema and presentation metadata.",
18
+ icon: "map",
19
+ color: "teal",
20
+ },
21
+ },
22
+ },
23
+ }
24
+ }
@@ -0,0 +1,140 @@
1
+ import { cachedRouteTargets } from "../invalidation.ts"
2
+ import { parseLinks, type Source, type SourceLink } from "../source/index.ts"
3
+ import { directiveDefinition } from "./directive-definitions.ts"
4
+ import type { DependencyContext, Plugin } from "./types.ts"
5
+
6
+ const EXTERNAL = /^[a-z][a-z\d+.-]*:/i
7
+ type AuthoredScript = Readonly<{
8
+ type: "script"
9
+ source: string
10
+ module: true
11
+ }>
12
+
13
+ function resolveTarget(source: Source, target: string) {
14
+ return target.startsWith("/")
15
+ ? target
16
+ : new URL(target, `file://${source.path}`).pathname
17
+ }
18
+
19
+ function scriptLinks(links: readonly SourceLink[]) {
20
+ return links.filter((link) => link.directive?.name === "script")
21
+ }
22
+
23
+ function resolveScript(
24
+ source: Source,
25
+ link: SourceLink,
26
+ ): AuthoredScript | undefined {
27
+ if (link.directive?.name !== "script") return
28
+ const raw = link.href.split("#")[0]?.split("?")[0] ?? ""
29
+ if (
30
+ !raw ||
31
+ EXTERNAL.test(raw) ||
32
+ raw.startsWith("//") ||
33
+ raw.startsWith("~/")
34
+ )
35
+ return
36
+ const sourcePath = resolveTarget(source, raw)
37
+ if (!sourcePath.endsWith(".js")) return
38
+ return {
39
+ type: "script",
40
+ source: sourcePath,
41
+ module: true,
42
+ }
43
+ }
44
+
45
+ function pushUnique(assets: AuthoredScript[], asset: AuthoredScript) {
46
+ if (assets.some((item) => item.source === asset.source)) return
47
+ assets.push(asset)
48
+ }
49
+
50
+ function addScriptDependency(
51
+ dependencies: DependencyContext,
52
+ asset: AuthoredScript,
53
+ ) {
54
+ dependencies.depend({ type: "source", path: asset.source })
55
+ }
56
+
57
+ function sourceScriptLinks(source: Source) {
58
+ return source.blocks.length
59
+ ? source.blocks.flatMap((block) => block.links)
60
+ : source.links
61
+ }
62
+
63
+ function globalScripts(source: Source, links: readonly SourceLink[]) {
64
+ const assets: AuthoredScript[] = []
65
+ for (const link of scriptLinks(links)) {
66
+ if (!link.directive?.qualifiers.includes("global")) continue
67
+ const asset = resolveScript(source, link)
68
+ if (asset) pushUnique(assets, asset)
69
+ }
70
+ return assets
71
+ }
72
+
73
+ function scriptSignature(assets: readonly AuthoredScript[]) {
74
+ return assets.map((asset) => asset.source).join("\n")
75
+ }
76
+
77
+ function authoredGlobalSignature(source?: Source) {
78
+ return scriptSignature(
79
+ source ? globalScripts(source, parseLinks(source.body)) : [],
80
+ )
81
+ }
82
+
83
+ export function ScriptsPlugin(): Plugin {
84
+ const global = new Map<string, AuthoredScript[]>()
85
+
86
+ return {
87
+ id: "scripts",
88
+
89
+ index(source) {
90
+ const assets = globalScripts(source, sourceScriptLinks(source))
91
+ if (assets.length) global.set(source.path, assets)
92
+ else global.delete(source.path)
93
+ },
94
+
95
+ unindex(sourcePath) {
96
+ global.delete(sourcePath)
97
+ },
98
+
99
+ precompile(markdown, ctx) {
100
+ const scripts = scriptLinks(ctx.block?.links ?? ctx.source.links)
101
+ if (!scripts.length) return markdown
102
+ let current = markdown
103
+ for (const link of scripts) {
104
+ current = current.replace(directiveDefinition("script", link.href), "")
105
+ }
106
+ return current
107
+ },
108
+
109
+ invalidate({ before, after }, app) {
110
+ if (authoredGlobalSignature(before) === authoredGlobalSignature(after))
111
+ return
112
+ const pages = app.sources.builtPages()
113
+ return {
114
+ sourceIds: pages.map((page) => page.sourceId),
115
+ targets: cachedRouteTargets(pages),
116
+ }
117
+ },
118
+
119
+ page(html, ctx) {
120
+ for (const assets of global.values()) {
121
+ for (const asset of assets) addScriptDependency(ctx, asset)
122
+ }
123
+ for (const link of scriptLinks(ctx.source.links)) {
124
+ if (link.directive?.qualifiers.includes("global")) continue
125
+ const asset = resolveScript(ctx.source, link)
126
+ if (asset) {
127
+ ctx.addAsset(asset)
128
+ addScriptDependency(ctx, asset)
129
+ }
130
+ }
131
+ return html
132
+ },
133
+
134
+ documentAssets(ctx) {
135
+ for (const origin of [...global.keys()].sort()) {
136
+ for (const asset of global.get(origin) ?? []) ctx.addAsset(asset)
137
+ }
138
+ },
139
+ }
140
+ }