@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,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
|
+
}
|