@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,742 @@
1
+ import {
2
+ createBlock,
3
+ isExternalHref,
4
+ splitMarkdownBlocks,
5
+ type MarkdownCodeFence,
6
+ type Source,
7
+ } from "../../source/index.ts"
8
+ import type { View } from "../../view/model.ts"
9
+ import { queryRecordValue } from "../../query/fields.ts"
10
+ import { isIndexSource, publicPath } from "../../server/router.ts"
11
+ import { queryDependency } from "../../query/kernel.ts"
12
+ import type { SourceQueryRequest } from "../../query/model.ts"
13
+ import { posixPath } from "../../path.ts"
14
+ import {
15
+ defineViewComponent,
16
+ type AppContext,
17
+ type DependencyContext,
18
+ type Plugin,
19
+ } from "../types.ts"
20
+ import { actorProfileDependency } from "../actor-profile.ts"
21
+ import { escapeHtml } from "../../util.ts"
22
+ import type { HtmlStringRewrite } from "../../environment.ts"
23
+ import { parseHtmlAttributes } from "../../html-attributes.ts"
24
+ import {
25
+ authoredViewTag,
26
+ COMPONENT_ORIGIN_ATTR,
27
+ elementTagNames,
28
+ stampComponentOrigins,
29
+ stampComponentRoot,
30
+ } from "../../view/syntax.ts"
31
+
32
+ const clientEntry = new URL("./client.ts", import.meta.url)
33
+ const NO_COMPONENT_TAGS = new Set<string>()
34
+ const NO_COMPONENTS = new Map<string, ComponentTemplate>()
35
+ const LITERATE_QUERY_COMPONENT = "literate-components:query"
36
+ const LITERATE_QUERY_TAG = authoredViewTag(LITERATE_QUERY_COMPONENT)
37
+
38
+ export type ComponentTemplate = {
39
+ html: string
40
+ css: string
41
+ js: string
42
+ origin: string
43
+ references?: string[]
44
+ }
45
+
46
+ class ComponentRegistry extends Map<string, ComponentTemplate> {
47
+ revision = 0
48
+
49
+ override set(tag: string, template: ComponentTemplate) {
50
+ this.revision++
51
+ return super.set(tag, template)
52
+ }
53
+
54
+ override delete(tag: string) {
55
+ const deleted = super.delete(tag)
56
+ if (deleted) this.revision++
57
+ return deleted
58
+ }
59
+
60
+ override clear() {
61
+ if (!this.size) return
62
+ super.clear()
63
+ this.revision++
64
+ }
65
+ }
66
+
67
+ function importedComponents(
68
+ source: Source,
69
+ app: AppContext,
70
+ registry: ComponentRegistry,
71
+ ) {
72
+ const components = new Map<string, ComponentTemplate>()
73
+ const visited = new Set<string>()
74
+ const pending = [source]
75
+ while (pending.length) {
76
+ const importer = pending.pop()!
77
+ for (const link of importer.links) {
78
+ if (link.kind !== "definition" || !link.directive) continue
79
+ const href = link.href.split(/[?#]/, 1)[0]
80
+ if (!href || href.startsWith("~/") || /^[a-z][a-z\d+.-]*:/i.test(href))
81
+ continue
82
+ const path = href.startsWith("/")
83
+ ? app.sources.path(href)
84
+ : new URL(href, `file://${importer.path}`).pathname
85
+ const imported = app.sources.get(path)
86
+ if (!imported || imported.type !== "components" || visited.has(path))
87
+ continue
88
+ visited.add(path)
89
+ pending.push(imported)
90
+ for (const [tag, component] of registry) {
91
+ if (component.origin === path) components.set(tag, component)
92
+ }
93
+ }
94
+ }
95
+ return components
96
+ }
97
+
98
+ type ComponentIndex = {
99
+ key: number | string
100
+ matcher?: RegExp
101
+ tags: Set<string>
102
+ patterns: Map<string, { selfClosing: RegExp; wrapped: RegExp }>
103
+ }
104
+
105
+ const componentIndexes = new WeakMap<
106
+ Map<string, ComponentTemplate>,
107
+ ComponentIndex
108
+ >()
109
+
110
+ function escapeRegExp(value: string) {
111
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
112
+ }
113
+
114
+ function componentIndexKey(components: Map<string, ComponentTemplate>) {
115
+ return components instanceof ComponentRegistry
116
+ ? components.revision
117
+ : [...components.keys()].join("\0")
118
+ }
119
+
120
+ function componentIndex(components: Map<string, ComponentTemplate>) {
121
+ const key = componentIndexKey(components)
122
+ const cached = componentIndexes.get(components)
123
+ if (cached?.key === key) return cached
124
+
125
+ const tags = [...components.keys()]
126
+ const patterns = new Map<string, { selfClosing: RegExp; wrapped: RegExp }>()
127
+ for (const tag of tags) {
128
+ const escaped = escapeRegExp(tag)
129
+ patterns.set(tag, {
130
+ selfClosing: new RegExp(`<${escaped}(\\s[^>]*)?\\s*/>`, "gi"),
131
+ wrapped: new RegExp(
132
+ `<${escaped}(\\s[^>]*)?>([\\s\\S]*?)</${escaped}>`,
133
+ "gi",
134
+ ),
135
+ })
136
+ }
137
+
138
+ const index: ComponentIndex = {
139
+ key,
140
+ tags: new Set(tags),
141
+ matcher: tags.length
142
+ ? new RegExp(`<(?:${tags.map(escapeRegExp).join("|")})(?:\\s|>|/)`, "i")
143
+ : undefined,
144
+ patterns,
145
+ }
146
+ componentIndexes.set(components, index)
147
+ return index
148
+ }
149
+
150
+ function compileLiterateComponent(fences: MarkdownCodeFence[]) {
151
+ const html: string[] = []
152
+ const css: string[] = []
153
+ const js: string[] = []
154
+
155
+ for (const fence of fences) {
156
+ const lang = fence.lang.toLowerCase()
157
+ const body = fence.body.trim()
158
+ if (!lang || !body) continue
159
+ if (lang === "html" || lang === "htm") html.push(body)
160
+ else if (lang === "css") css.push(body)
161
+ else if (lang === "js" || lang === "javascript") js.push(body)
162
+ }
163
+
164
+ return {
165
+ html: html.join("\n"),
166
+ css: css.join("\n"),
167
+ js: js.join("\n"),
168
+ }
169
+ }
170
+
171
+ function canonicalTemplateUrl(value: string, origin: string, app: AppContext) {
172
+ const href = value.trim()
173
+ if (
174
+ !href ||
175
+ href.startsWith("/") ||
176
+ href.startsWith("#") ||
177
+ href.startsWith("?") ||
178
+ href.startsWith("~/") ||
179
+ href.includes("{{") ||
180
+ isExternalHref(href)
181
+ ) {
182
+ return value
183
+ }
184
+ const suffixAt = href.search(/[?#]/)
185
+ const pathname = suffixAt === -1 ? href : href.slice(0, suffixAt)
186
+ const suffix = suffixAt === -1 ? "" : href.slice(suffixAt)
187
+ const targetPath = posixPath.join(posixPath.dirname(origin), pathname)
188
+ const target = app.sources.get(targetPath)
189
+ return `${target && app.sources.isPage(target) ? publicPath(target) : targetPath}${suffix}`
190
+ }
191
+
192
+ function canonicalTemplateUrls(html: string, origin: string, app: AppContext) {
193
+ if (!html) return html
194
+ return app.html.rewrite(html, [
195
+ [
196
+ "[href], [src]",
197
+ {
198
+ element(element) {
199
+ for (const attribute of ["href", "src"] as const) {
200
+ const value = element.getAttribute(attribute)
201
+ if (value != null) {
202
+ element.setAttribute(
203
+ attribute,
204
+ canonicalTemplateUrl(value, origin, app),
205
+ )
206
+ }
207
+ }
208
+ },
209
+ },
210
+ ],
211
+ ])
212
+ }
213
+
214
+ function compileComponentTemplate(
215
+ fences: MarkdownCodeFence[],
216
+ origin: string,
217
+ app: AppContext,
218
+ ) {
219
+ const compiled = compileLiterateComponent(fences)
220
+ return {
221
+ ...compiled,
222
+ html: canonicalTemplateUrls(compiled.html, origin, app),
223
+ }
224
+ }
225
+
226
+ function normalizedTag(tag: string) {
227
+ return tag.trim().toLowerCase()
228
+ }
229
+
230
+ function blocksFor(source: Source, app: AppContext) {
231
+ if (source.blocks.length) return source.blocks
232
+ return splitMarkdownBlocks(source.body).map((content, ordinal) =>
233
+ createBlock(source, ordinal, content, {}, app.yaml),
234
+ )
235
+ }
236
+
237
+ function componentDefinitions(source: Source | undefined, app: AppContext) {
238
+ const definitions = new Map<
239
+ string,
240
+ ReturnType<typeof compileLiterateComponent>
241
+ >()
242
+ if (source?.type !== "components") return definitions
243
+
244
+ for (const block of blocksFor(source, app)) {
245
+ const tag =
246
+ typeof block.data.componentName === "string"
247
+ ? normalizedTag(block.data.componentName)
248
+ : ""
249
+ if (!tag) continue
250
+ const compiled = compileComponentTemplate(block.fences, source.path, app)
251
+ if (compiled.html || compiled.css || compiled.js) {
252
+ definitions.set(tag, compiled)
253
+ }
254
+ }
255
+ return definitions
256
+ }
257
+
258
+ function sameDefinition(
259
+ left: ReturnType<typeof compileLiterateComponent> | undefined,
260
+ right: ReturnType<typeof compileLiterateComponent> | undefined,
261
+ ) {
262
+ return (
263
+ left?.html === right?.html &&
264
+ left?.css === right?.css &&
265
+ left?.js === right?.js
266
+ )
267
+ }
268
+
269
+ function componentChanges(
270
+ before: Source | undefined,
271
+ after: Source | undefined,
272
+ app: AppContext,
273
+ ) {
274
+ const previous = componentDefinitions(before, app)
275
+ const current = componentDefinitions(after, app)
276
+ const tags = new Set([...previous.keys(), ...current.keys()])
277
+ const changed = [...tags].filter(
278
+ (tag) => !sameDefinition(previous.get(tag), current.get(tag)),
279
+ )
280
+ const introduced = [...current.keys()].filter((tag) => !previous.has(tag))
281
+ return { changed, introduced }
282
+ }
283
+
284
+ function componentDependency(tag: string) {
285
+ return {
286
+ type: "plugin" as const,
287
+ plugin: "literate-components",
288
+ key: normalizedTag(tag),
289
+ }
290
+ }
291
+
292
+ function componentTagPattern(tag: string) {
293
+ return new RegExp(`<${escapeRegExp(tag)}(?:\\s|>|/)`, "i")
294
+ }
295
+
296
+ function componentAttrs(attrs: Record<string, string>) {
297
+ return Object.fromEntries(
298
+ Object.entries(attrs).filter(
299
+ ([key]) =>
300
+ key !== "from" &&
301
+ key !== "as" &&
302
+ key !== "id" &&
303
+ key !== COMPONENT_ORIGIN_ATTR,
304
+ ),
305
+ )
306
+ }
307
+
308
+ type ComponentExpansionOptions = {
309
+ rewriteHtml: HtmlStringRewrite
310
+ dependencies?: DependencyContext
311
+ used?: Set<string>
312
+ allows?: (tag: string, origin?: string) => boolean
313
+ stamp?: { tag: string; includeAttrs: boolean }
314
+ originTags?: ReadonlySet<string>
315
+ }
316
+
317
+ function applyComponent(
318
+ html: string,
319
+ attrs: Record<string, string>,
320
+ children = "",
321
+ options: ComponentExpansionOptions,
322
+ ) {
323
+ const { dependencies, rewriteHtml, used, stamp, originTags } = options
324
+ const origin = attrs[COMPONENT_ORIGIN_ATTR]
325
+ const authoredAttrs = { ...attrs }
326
+ delete authoredAttrs[COMPONENT_ORIGIN_ATTR]
327
+ if (attrs.as) used?.add(attrs.as)
328
+ const slot = attrs.from
329
+ ? `<${LITERATE_QUERY_TAG} from="${escapeHtml(attrs.from)}" as="${escapeHtml(attrs.as ?? "")}" />`
330
+ : children
331
+ const expanded = html
332
+ .replace(/<yield\s*\/?>/gi, slot)
333
+ .replace(/\{\{\s*([\w-]+)\s*\}\}/g, (_match, key: string) =>
334
+ escapeHtml(authoredAttrs[key] ?? ""),
335
+ )
336
+ const stamped = stamp
337
+ ? stampComponentRoot(
338
+ expanded,
339
+ stamp.tag,
340
+ rewriteHtml,
341
+ stamp.includeAttrs ? componentAttrs(authoredAttrs) : undefined,
342
+ )
343
+ : expanded
344
+ return origin
345
+ ? stampComponentOrigins(
346
+ stamped,
347
+ origin,
348
+ originTags ?? NO_COMPONENT_TAGS,
349
+ rewriteHtml,
350
+ )
351
+ : stamped
352
+ }
353
+
354
+ export function expandComponents(
355
+ content: string,
356
+ components: Map<string, ComponentTemplate>,
357
+ options: Pick<
358
+ ComponentExpansionOptions,
359
+ "allows" | "dependencies" | "rewriteHtml" | "used"
360
+ >,
361
+ ) {
362
+ const index = componentIndex(components)
363
+ if (!index.matcher?.test(content)) return content
364
+
365
+ let current = content
366
+ for (let pass = 0; pass < 8; pass++) {
367
+ const before = current
368
+ for (const [tag, config] of components) {
369
+ const template = config.html
370
+ if (!template) continue
371
+ const patterns = index.patterns.get(tag)
372
+ if (!patterns) continue
373
+ const render = (match: string, raw: string, children = "") => {
374
+ const attrs = parseHtmlAttributes(raw)
375
+ if (
376
+ options.allows &&
377
+ !options.allows(tag, attrs[COMPONENT_ORIGIN_ATTR])
378
+ ) {
379
+ return match
380
+ }
381
+ options.used?.add(tag)
382
+ return applyComponent(template, attrs, children, {
383
+ ...options,
384
+ stamp: { tag, includeAttrs: Boolean(config.js) },
385
+ originTags: index.tags,
386
+ })
387
+ }
388
+ current = current.replace(patterns.selfClosing, (match, raw) => {
389
+ return render(match, raw)
390
+ })
391
+ current = current.replace(patterns.wrapped, (match, raw, children) => {
392
+ return render(match, raw, children)
393
+ })
394
+ }
395
+ if (current === before) break
396
+ }
397
+ return current
398
+ }
399
+
400
+ function wrapComponentScript(tag: string, body: string) {
401
+ const script = componentScript(tag, body)
402
+ return [
403
+ `<script data-pmx-component="${escapeHtml(tag)}">`,
404
+ script,
405
+ "</script>",
406
+ ].join("\n")
407
+ }
408
+
409
+ function componentScript(tag: string, body: string) {
410
+ const safe = body.replace(/<\/script/gi, "<\\/script")
411
+ return `(window.pmxq=window.pmxq||[]).push([${JSON.stringify(tag)},function(el,{$,$$,on,attrs,update,cleanup}){\n${safe}\n}])`
412
+ }
413
+
414
+ function assetsFor(
415
+ used: Set<string>,
416
+ components: Map<string, ComponentTemplate>,
417
+ ) {
418
+ const css: string[] = []
419
+ const scripts: string[] = []
420
+ for (const tag of used) {
421
+ const config = components.get(tag)
422
+ if (!config) continue
423
+ if (config.css) css.push(config.css)
424
+ if (config.js) scripts.push(wrapComponentScript(tag, config.js))
425
+ }
426
+ return { css, scripts }
427
+ }
428
+
429
+ function attrsFromData(data: Record<string, unknown>) {
430
+ return Object.entries(data)
431
+ .filter(
432
+ ([, value]) =>
433
+ typeof value === "string" ||
434
+ typeof value === "number" ||
435
+ typeof value === "boolean",
436
+ )
437
+ .map(([key, value]) => `${key}="${escapeHtml(String(value))}"`)
438
+ .join(" ")
439
+ }
440
+
441
+ const PROJECTED_SOURCE_FIELDS = new Set([
442
+ "title",
443
+ "subtitle",
444
+ "author",
445
+ "published",
446
+ "isbn",
447
+ "name",
448
+ "message",
449
+ "note",
450
+ "created",
451
+ "heart_count",
452
+ "component",
453
+ ])
454
+
455
+ function projectedSourceData(app: AppContext, source: Source) {
456
+ const data = Object.fromEntries(
457
+ Object.entries(source.data).filter(([field]) =>
458
+ PROJECTED_SOURCE_FIELDS.has(field),
459
+ ),
460
+ )
461
+ const actorId = typeof source.data.actor === "string" ? source.data.actor : ""
462
+ const profile = actorId ? app.actors.profile(actorId) : undefined
463
+ return {
464
+ ...data,
465
+ ...(actorId ? { actor: actorId } : {}),
466
+ ...(profile
467
+ ? {
468
+ actor_name: profile.name,
469
+ ...(profile.avatar ? { avatar: profile.avatar } : {}),
470
+ }
471
+ : {}),
472
+ }
473
+ }
474
+
475
+ function blockAttrs(data: Record<string, unknown>) {
476
+ return Object.fromEntries(
477
+ Object.entries(data).filter(
478
+ ([key]) => key !== "component" && key !== "type" && key !== "id",
479
+ ),
480
+ )
481
+ }
482
+
483
+ function selfRenderTag(data: Record<string, unknown>) {
484
+ return typeof data.component === "string" ? data.component.trim() : ""
485
+ }
486
+
487
+ function interpolateStructuredData(template: string, data: unknown) {
488
+ return template.replace(
489
+ /\{\{\s*data\.([A-Za-z_][\w-]*(?:\.[A-Za-z_][\w-]*)*)\s*\}\}/g,
490
+ (_match, path: string) => {
491
+ const value = queryRecordValue(data, path)
492
+ return value == null
493
+ ? ""
494
+ : escapeHtml(
495
+ typeof value === "object" ? JSON.stringify(value) : String(value),
496
+ )
497
+ },
498
+ )
499
+ }
500
+
501
+ async function renderFrom(
502
+ app: AppContext,
503
+ view: View,
504
+ request: SourceQueryRequest,
505
+ components: Map<string, ComponentTemplate>,
506
+ as?: string,
507
+ ) {
508
+ const tags = (await view.query.sources(request))
509
+ .filter((summary) => {
510
+ const source = app.sources.get(summary.id)
511
+ return source ? !isIndexSource(source) : true
512
+ })
513
+ .map((summary) => {
514
+ const source = app.sources.get(summary.id)
515
+ const attrs = attrsFromData({
516
+ ...(source ? projectedSourceData(app, source) : summary.data),
517
+ href: summary.href,
518
+ from: request.from ?? "/",
519
+ })
520
+ return `<${as || summary.type} ${attrs} />`
521
+ })
522
+ .join("")
523
+ return expandComponents(tags, components, { rewriteHtml: app.html.rewrite })
524
+ }
525
+
526
+ export function ComponentTemplatePlugin(): Plugin {
527
+ const components = new ComponentRegistry()
528
+ const QueryResults = defineViewComponent({
529
+ tag: LITERATE_QUERY_COMPONENT,
530
+ async compile(use, ctx, app) {
531
+ const spec = use.props.from ?? "/"
532
+ const from =
533
+ spec.startsWith("~/") || spec.startsWith("/")
534
+ ? spec
535
+ : posixPath.resolve(posixPath.dirname(use.origin.path), spec)
536
+ const request: SourceQueryRequest = {
537
+ from,
538
+ ...(use.props.as ? { type: use.props.as } : {}),
539
+ sort: [{ by: "meta.title" }],
540
+ }
541
+ const plan = await app.query.prepare("sources", request)
542
+ ctx.depend(queryDependency(plan))
543
+ return Object.freeze(request)
544
+ },
545
+ render(use, request, { view }, app) {
546
+ return renderFrom(
547
+ app,
548
+ view,
549
+ request,
550
+ importedComponents(use.origin, app, components),
551
+ use.props.as || undefined,
552
+ )
553
+ },
554
+ })
555
+
556
+ return {
557
+ id: "literate-components",
558
+ schema: {
559
+ sources: {
560
+ components: {
561
+ label: "Components",
562
+ description: "Authored Literate Component definitions.",
563
+ icon: "boxes",
564
+ color: "amber",
565
+ },
566
+ },
567
+ },
568
+ components: [QueryResults],
569
+ index(source, app) {
570
+ if (source.type !== "components") return
571
+
572
+ const definitions: {
573
+ tag: string
574
+ compiled: ReturnType<typeof compileLiterateComponent>
575
+ }[] = []
576
+ const collect = (tag: string, fences: MarkdownCodeFence[]) => {
577
+ tag = normalizedTag(tag)
578
+ const compiled = compileComponentTemplate(fences, source.path, app)
579
+ if (compiled.html || compiled.css || compiled.js) {
580
+ definitions.push({ tag, compiled })
581
+ }
582
+ }
583
+
584
+ for (const block of source.blocks) {
585
+ const tag =
586
+ typeof block.data.componentName === "string"
587
+ ? block.data.componentName.trim()
588
+ : ""
589
+ if (tag) collect(tag, block.fences)
590
+ }
591
+ app.components.claim(
592
+ source.path,
593
+ definitions.map(({ tag }) => tag),
594
+ )
595
+ for (const { tag, compiled } of definitions) {
596
+ components.set(tag, {
597
+ ...compiled,
598
+ origin: source.path,
599
+ references: [...elementTagNames(compiled.html)],
600
+ })
601
+ }
602
+ app.components.replaceData(
603
+ source.path,
604
+ definitions.map(({ tag }) => ({
605
+ tag,
606
+ available(origin: Source) {
607
+ return importedComponents(origin, app, components).has(tag)
608
+ },
609
+ async compile(origin, ctx) {
610
+ const imported = importedComponents(origin, app, components)
611
+ const template = imported.get(tag)
612
+ if (!template) {
613
+ throw new Error(
614
+ `Literate Component <${tag}> is not imported by ${origin.path}.`,
615
+ )
616
+ }
617
+ ctx.depend(componentDependency(tag))
618
+ for (const reference of template.references ?? []) {
619
+ if (!app.components.has(reference)) {
620
+ ctx.depend(componentDependency(reference))
621
+ }
622
+ }
623
+ if (template.css) {
624
+ ctx.addAsset({ type: "style", text: template.css })
625
+ }
626
+ if (template.js) {
627
+ ctx.addAsset({
628
+ type: "script",
629
+ text: componentScript(tag, template.js),
630
+ })
631
+ await ctx.addBrowserBundle(clientEntry, "components")
632
+ }
633
+ return { template, imported }
634
+ },
635
+ render(use, prepared, _ctx, runtimeApp) {
636
+ const { template, imported } = prepared as {
637
+ template: ComponentTemplate
638
+ imported: Map<string, ComponentTemplate>
639
+ }
640
+ const expanded = applyComponent(
641
+ interpolateStructuredData(template.html, use.data),
642
+ { ...use.props },
643
+ "",
644
+ {
645
+ rewriteHtml: runtimeApp.html.rewrite,
646
+ stamp: { tag, includeAttrs: Boolean(template.js) },
647
+ originTags: componentIndex(imported).tags,
648
+ },
649
+ )
650
+ return expandComponents(expanded, imported, {
651
+ rewriteHtml: runtimeApp.html.rewrite,
652
+ })
653
+ },
654
+ })),
655
+ )
656
+ },
657
+
658
+ unindex(sourcePath, app) {
659
+ app.components.release(sourcePath)
660
+ for (const [tag, config] of components) {
661
+ if (config.origin === sourcePath) components.delete(tag)
662
+ }
663
+ },
664
+
665
+ invalidate({ before, after }, app) {
666
+ const { changed, introduced } = componentChanges(before, after, app)
667
+ if (!changed.length) return
668
+ const patterns = introduced.map(componentTagPattern)
669
+ return {
670
+ dependencies: changed.map(componentDependency),
671
+ sourceIds: patterns.length
672
+ ? app.sources
673
+ .builtPages()
674
+ .filter((page) =>
675
+ patterns.some((pattern) => pattern.test(page.html)),
676
+ )
677
+ .map((page) => page.sourceId)
678
+ : [],
679
+ }
680
+ },
681
+
682
+ precompile(content, ctx, app) {
683
+ const { block } = ctx
684
+ const actorId =
685
+ typeof block?.data.actor === "string" ? block.data.actor : ""
686
+ if (actorId) ctx.depend(actorProfileDependency(actorId))
687
+ const tag = block ? selfRenderTag(block.data) : ""
688
+ if (tag) ctx.depend(componentDependency(tag))
689
+ if (
690
+ !block ||
691
+ content.trim() ||
692
+ !tag ||
693
+ !importedComponents(ctx.source, app, components).has(tag)
694
+ )
695
+ return content
696
+ const attrs = attrsFromData(blockAttrs(block.data))
697
+ const origin = `${COMPONENT_ORIGIN_ATTR}="${escapeHtml(ctx.source.path)}"`
698
+ return attrs ? `<${tag} ${origin} ${attrs} />` : `<${tag} ${origin} />`
699
+ },
700
+
701
+ page(html, ctx, app) {
702
+ const before = html
703
+ const used = new Set<string>()
704
+ const scopes = new Map<string, Map<string, ComponentTemplate>>()
705
+ const scope = (origin = ctx.source.path) => {
706
+ const cached = scopes.get(origin)
707
+ if (cached) return cached
708
+ const source = app.sources.get(origin)
709
+ const imported = source
710
+ ? importedComponents(source, app, components)
711
+ : NO_COMPONENTS
712
+ scopes.set(origin, imported)
713
+ return imported
714
+ }
715
+ html = expandComponents(html, components, {
716
+ allows: (tag, origin) => scope(origin).has(tag),
717
+ dependencies: ctx,
718
+ rewriteHtml: app.html.rewrite,
719
+ used,
720
+ })
721
+
722
+ for (const tag of used) {
723
+ ctx.depend(componentDependency(tag))
724
+ for (const reference of components.get(tag)?.references ?? []) {
725
+ if (app.components.has(reference)) continue
726
+ ctx.depend(componentDependency(reference))
727
+ }
728
+ }
729
+
730
+ if (html === before) return html
731
+ const { css, scripts } = assetsFor(used, components)
732
+ if (css.length) html += `\n<style>${css.join("\n")}</style>`
733
+ if (scripts.length) html += `\n${scripts.join("\n")}`
734
+ return html
735
+ },
736
+
737
+ async documentAssets(ctx) {
738
+ if (!ctx.page?.html.includes("<script data-pmx-component=")) return
739
+ await ctx.addBrowserBundle(clientEntry, "components")
740
+ },
741
+ }
742
+ }