@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,223 @@
|
|
|
1
|
+
import { mkdir, unlink } from "node:fs/promises"
|
|
2
|
+
import { dirname, join, posix } from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
import { Database } from "bun:sqlite"
|
|
5
|
+
import { YAML } from "bun"
|
|
6
|
+
import type {
|
|
7
|
+
SqlConnection,
|
|
8
|
+
SqlDatabase,
|
|
9
|
+
SqlParams,
|
|
10
|
+
SqlResult,
|
|
11
|
+
SqlValue,
|
|
12
|
+
} from "../database.ts"
|
|
13
|
+
import type {
|
|
14
|
+
BundledAsset,
|
|
15
|
+
HtmlRewrite,
|
|
16
|
+
PathMXEnvironment,
|
|
17
|
+
} from "../environment.ts"
|
|
18
|
+
|
|
19
|
+
export { publishToDirectory, writePublication } from "./bun-publish.ts"
|
|
20
|
+
|
|
21
|
+
function positional(params: SqlParams): params is readonly SqlValue[] {
|
|
22
|
+
return Array.isArray(params)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isBusy(error: unknown): error is { code: "SQLITE_BUSY" } {
|
|
26
|
+
return (
|
|
27
|
+
typeof error === "object" &&
|
|
28
|
+
error !== null &&
|
|
29
|
+
"code" in error &&
|
|
30
|
+
error.code === "SQLITE_BUSY"
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function retryBusy<T>(run: () => T, timeoutMs = 5_000): Promise<T> {
|
|
35
|
+
const deadline = Date.now() + timeoutMs
|
|
36
|
+
while (true) {
|
|
37
|
+
try {
|
|
38
|
+
return run()
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (!isBusy(error) || Date.now() >= deadline) throw error
|
|
41
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function bunDatabase(path: string): SqlConnection {
|
|
47
|
+
const database = new Database(path, { create: true, strict: true })
|
|
48
|
+
database.exec("PRAGMA foreign_keys = ON")
|
|
49
|
+
database.exec("PRAGMA journal_mode = WAL")
|
|
50
|
+
// A synchronous busy wait blocks the JS thread and can deadlock the async
|
|
51
|
+
// transaction holding the competing connection. Retry by yielding instead.
|
|
52
|
+
database.exec("PRAGMA busy_timeout = 0")
|
|
53
|
+
|
|
54
|
+
let tail = Promise.resolve()
|
|
55
|
+
const enqueue = <T>(run: () => T | Promise<T>) => {
|
|
56
|
+
const result = tail.then(run, run)
|
|
57
|
+
tail = result.then(
|
|
58
|
+
() => undefined,
|
|
59
|
+
() => undefined,
|
|
60
|
+
)
|
|
61
|
+
return result
|
|
62
|
+
}
|
|
63
|
+
const direct = (allowTransaction: boolean): SqlDatabase =>
|
|
64
|
+
Object.freeze({
|
|
65
|
+
all: async <Row>(sql: string, params?: SqlParams) => {
|
|
66
|
+
return retryBusy(() => {
|
|
67
|
+
if (!params) return database.query<Row, []>(sql).all()
|
|
68
|
+
return positional(params)
|
|
69
|
+
? database.query<Row, SqlValue[]>(sql).all(...params)
|
|
70
|
+
: database
|
|
71
|
+
.query<Row, Record<string, SqlValue>>(sql)
|
|
72
|
+
.all({ ...params })
|
|
73
|
+
})
|
|
74
|
+
},
|
|
75
|
+
get: async <Row>(sql: string, params?: SqlParams) => {
|
|
76
|
+
const row = await retryBusy(() =>
|
|
77
|
+
!params
|
|
78
|
+
? database.query<Row, []>(sql).get()
|
|
79
|
+
: positional(params)
|
|
80
|
+
? database.query<Row, SqlValue[]>(sql).get(...params)
|
|
81
|
+
: database
|
|
82
|
+
.query<Row, Record<string, SqlValue>>(sql)
|
|
83
|
+
.get({ ...params }),
|
|
84
|
+
)
|
|
85
|
+
return row ?? undefined
|
|
86
|
+
},
|
|
87
|
+
run: async (sql: string, params?: SqlParams): Promise<SqlResult> => {
|
|
88
|
+
const result = await retryBusy(() =>
|
|
89
|
+
!params
|
|
90
|
+
? database.query<never, []>(sql).run()
|
|
91
|
+
: positional(params)
|
|
92
|
+
? database.query<never, SqlValue[]>(sql).run(...params)
|
|
93
|
+
: database
|
|
94
|
+
.query<never, Record<string, SqlValue>>(sql)
|
|
95
|
+
.run({ ...params }),
|
|
96
|
+
)
|
|
97
|
+
return {
|
|
98
|
+
changes: result.changes,
|
|
99
|
+
...(result.lastInsertRowid != null
|
|
100
|
+
? { lastInsertRowId: result.lastInsertRowid }
|
|
101
|
+
: {}),
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
exec: async (sql: string) => {
|
|
105
|
+
database.exec(sql)
|
|
106
|
+
},
|
|
107
|
+
transaction: async <T>(run: (database: SqlDatabase) => Promise<T>) => {
|
|
108
|
+
if (!allowTransaction) {
|
|
109
|
+
throw new Error("Nested database transactions are not supported.")
|
|
110
|
+
}
|
|
111
|
+
await retryBusy(() => database.exec("BEGIN IMMEDIATE"))
|
|
112
|
+
try {
|
|
113
|
+
const result = await run(direct(false))
|
|
114
|
+
await retryBusy(() => database.exec("COMMIT"))
|
|
115
|
+
return result
|
|
116
|
+
} catch (error) {
|
|
117
|
+
database.exec("ROLLBACK")
|
|
118
|
+
throw error
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
const raw = direct(true)
|
|
123
|
+
return Object.freeze({
|
|
124
|
+
all: <Row>(sql: string, params?: SqlParams) =>
|
|
125
|
+
enqueue(() => raw.all<Row>(sql, params)),
|
|
126
|
+
get: <Row>(sql: string, params?: SqlParams) =>
|
|
127
|
+
enqueue(() => raw.get<Row>(sql, params)),
|
|
128
|
+
run: (sql: string, params?: SqlParams) =>
|
|
129
|
+
enqueue(() => raw.run(sql, params)),
|
|
130
|
+
exec: (sql: string) => enqueue(() => raw.exec(sql)),
|
|
131
|
+
transaction: <T>(run: (database: SqlDatabase) => Promise<T>) =>
|
|
132
|
+
enqueue(() => raw.transaction(run)),
|
|
133
|
+
close: () =>
|
|
134
|
+
enqueue(() => {
|
|
135
|
+
database.close(false)
|
|
136
|
+
}),
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function bundledAssetType(contentType: string) {
|
|
141
|
+
if (contentType.includes("javascript")) return "script" as const
|
|
142
|
+
if (contentType.includes("css")) return "style" as const
|
|
143
|
+
return "file" as const
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function rewriteHtml(input: string, rules: Parameters<HtmlRewrite>[1]): string
|
|
147
|
+
function rewriteHtml(
|
|
148
|
+
input: Response,
|
|
149
|
+
rules: Parameters<HtmlRewrite>[1],
|
|
150
|
+
): Response
|
|
151
|
+
function rewriteHtml(
|
|
152
|
+
input: string | Response,
|
|
153
|
+
rules: Parameters<HtmlRewrite>[1],
|
|
154
|
+
) {
|
|
155
|
+
const rewriter = new HTMLRewriter()
|
|
156
|
+
for (const [selector, handler] of rules) {
|
|
157
|
+
rewriter.on(
|
|
158
|
+
selector,
|
|
159
|
+
handler as HTMLRewriterTypes.HTMLRewriterElementContentHandlers,
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
if (typeof input === "string") return rewriter.transform(input)
|
|
163
|
+
return rewriter.transform(input)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const bunEnvironment: PathMXEnvironment = {
|
|
167
|
+
renderMarkdown: (markdown) =>
|
|
168
|
+
Bun.markdown.html(markdown, {
|
|
169
|
+
headings: { ids: true },
|
|
170
|
+
noIndentedCodeBlocks: true,
|
|
171
|
+
}),
|
|
172
|
+
rewriteHtml,
|
|
173
|
+
loadAsset: (url) => Bun.file(url),
|
|
174
|
+
bundle: async (entrypoint) => {
|
|
175
|
+
const result = await Bun.build({
|
|
176
|
+
entrypoints: [
|
|
177
|
+
entrypoint instanceof URL ? fileURLToPath(entrypoint) : entrypoint,
|
|
178
|
+
],
|
|
179
|
+
target: "browser",
|
|
180
|
+
minify: true,
|
|
181
|
+
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
182
|
+
throw: true,
|
|
183
|
+
})
|
|
184
|
+
return Promise.all(
|
|
185
|
+
result.outputs.map(async (artifact): Promise<BundledAsset> => {
|
|
186
|
+
const path = artifact.path.replace(/^\.\//, "")
|
|
187
|
+
const type = bundledAssetType(artifact.type)
|
|
188
|
+
return type === "file"
|
|
189
|
+
? { path, type, blob: artifact }
|
|
190
|
+
: { path, type, text: await artifact.text() }
|
|
191
|
+
}),
|
|
192
|
+
)
|
|
193
|
+
},
|
|
194
|
+
loadModule: async (specifier) => {
|
|
195
|
+
if (!(await Bun.file(specifier).exists())) return
|
|
196
|
+
return import(specifier)
|
|
197
|
+
},
|
|
198
|
+
path: posix,
|
|
199
|
+
yaml: {
|
|
200
|
+
parse: YAML.parse,
|
|
201
|
+
stringify: (value) => YAML.stringify(value, null, 2),
|
|
202
|
+
},
|
|
203
|
+
storage: {
|
|
204
|
+
list: (root) => [
|
|
205
|
+
...new Bun.Glob("**/*").scanSync({ cwd: root, onlyFiles: true }),
|
|
206
|
+
],
|
|
207
|
+
readText: (path) => Bun.file(path).text(),
|
|
208
|
+
readBlob: (path) => Bun.file(path),
|
|
209
|
+
writeText: async (path, content) => {
|
|
210
|
+
await mkdir(dirname(path), { recursive: true })
|
|
211
|
+
await Bun.write(path, content)
|
|
212
|
+
},
|
|
213
|
+
delete: (path) => unlink(path),
|
|
214
|
+
exists: (path) => Bun.file(path).exists(),
|
|
215
|
+
},
|
|
216
|
+
database: {
|
|
217
|
+
path: join,
|
|
218
|
+
open: async (path) => {
|
|
219
|
+
await mkdir(dirname(path), { recursive: true })
|
|
220
|
+
return bunDatabase(path)
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
}
|
package/globals.d.ts
ADDED
package/graph.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { Repository } from "./repository"
|
|
2
|
+
import { resolveRoute } from "./server/router"
|
|
3
|
+
import {
|
|
4
|
+
interpolateText,
|
|
5
|
+
isExternalHref,
|
|
6
|
+
type Source,
|
|
7
|
+
type LinkDirective,
|
|
8
|
+
type SourceLinkType,
|
|
9
|
+
type SourceNode,
|
|
10
|
+
} from "./source"
|
|
11
|
+
|
|
12
|
+
function splitLinkTarget(href: string) {
|
|
13
|
+
const hash = href.indexOf("#")
|
|
14
|
+
const withoutFragment = hash === -1 ? href : href.slice(0, hash)
|
|
15
|
+
const query = withoutFragment.indexOf("?")
|
|
16
|
+
return {
|
|
17
|
+
pathname: query === -1 ? withoutFragment : withoutFragment.slice(0, query),
|
|
18
|
+
query: query === -1 ? undefined : withoutFragment.slice(query + 1),
|
|
19
|
+
fragment: hash === -1 ? undefined : href.slice(hash + 1),
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type GraphEdge = {
|
|
24
|
+
from: string
|
|
25
|
+
href: string
|
|
26
|
+
type: SourceLinkType
|
|
27
|
+
label?: string
|
|
28
|
+
directive?: LinkDirective
|
|
29
|
+
to?: string
|
|
30
|
+
fragment?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type LinkResolution = {
|
|
34
|
+
type: SourceLinkType
|
|
35
|
+
targetPath?: string
|
|
36
|
+
fragment?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class Graph {
|
|
40
|
+
private repository: Repository
|
|
41
|
+
nodes = new Map<string, SourceNode>()
|
|
42
|
+
private outgoing = new Map<string, GraphEdge[]>()
|
|
43
|
+
private incoming = new Map<string, GraphEdge[]>()
|
|
44
|
+
|
|
45
|
+
constructor(repository: Repository) {
|
|
46
|
+
this.repository = repository
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Classify authored Source/Block links and rebuild adjacency. */
|
|
50
|
+
async rebuild() {
|
|
51
|
+
this.nodes.clear()
|
|
52
|
+
this.outgoing.clear()
|
|
53
|
+
this.incoming.clear()
|
|
54
|
+
for (const source of this.repository.all()) {
|
|
55
|
+
await this.indexSource(source)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async indexSource(source: Source) {
|
|
60
|
+
await this.indexNode(source)
|
|
61
|
+
for (const block of source.blocks) this.nodes.set(block.path, block)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
linksFrom(path: string) {
|
|
65
|
+
return this.outgoing.get(path) ?? []
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
linksTo(path: string) {
|
|
69
|
+
return this.incoming.get(path) ?? []
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
unresolvedLinks() {
|
|
73
|
+
return [...this.outgoing.values()]
|
|
74
|
+
.flat()
|
|
75
|
+
.filter((edge) => edge.type === "unresolved")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
neighbors(path: string) {
|
|
79
|
+
return this.linksFrom(path)
|
|
80
|
+
.map((edge) => (edge.to ? this.nodes.get(edge.to) : undefined))
|
|
81
|
+
.filter((node): node is SourceNode => node != null)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
backlinks(path: string) {
|
|
85
|
+
return this.linksTo(path)
|
|
86
|
+
.map((edge) => this.nodes.get(edge.from))
|
|
87
|
+
.filter((node): node is SourceNode => node != null)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private async indexNode(node: SourceNode) {
|
|
91
|
+
this.nodes.set(node.path, node)
|
|
92
|
+
const edges: GraphEdge[] = []
|
|
93
|
+
for (const link of node.links) {
|
|
94
|
+
const resolved = await this.resolveLink(
|
|
95
|
+
node,
|
|
96
|
+
interpolateText(link.href, node.data),
|
|
97
|
+
)
|
|
98
|
+
const edge: GraphEdge = {
|
|
99
|
+
from: node.path,
|
|
100
|
+
href: link.href,
|
|
101
|
+
type: resolved.type,
|
|
102
|
+
label: link.label,
|
|
103
|
+
directive: link.directive,
|
|
104
|
+
to: resolved.targetPath,
|
|
105
|
+
fragment: resolved.fragment,
|
|
106
|
+
}
|
|
107
|
+
edges.push(edge)
|
|
108
|
+
if (edge.type === "source" && edge.to) this.addIncoming(edge.to, edge)
|
|
109
|
+
}
|
|
110
|
+
this.outgoing.set(node.path, edges)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private pageSourceAt(targetPath: string) {
|
|
114
|
+
const route = resolveRoute(this.repository, targetPath)
|
|
115
|
+
return route?.type === "page" ? route.source : undefined
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private addIncoming(path: string, edge: GraphEdge) {
|
|
119
|
+
const list = this.incoming.get(path)
|
|
120
|
+
if (list) list.push(edge)
|
|
121
|
+
else this.incoming.set(path, [edge])
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async resolveLink(
|
|
125
|
+
source: SourceNode,
|
|
126
|
+
href: string,
|
|
127
|
+
): Promise<LinkResolution> {
|
|
128
|
+
if (isExternalHref(href)) return { type: "external" }
|
|
129
|
+
|
|
130
|
+
if (href.startsWith("~/")) return { type: "actor-relative" }
|
|
131
|
+
|
|
132
|
+
const { pathname, query, fragment } = splitLinkTarget(href)
|
|
133
|
+
|
|
134
|
+
if (!pathname && (query !== undefined || fragment !== undefined))
|
|
135
|
+
return { type: "fragment", targetPath: source.path, fragment }
|
|
136
|
+
if (!pathname) return { type: "unresolved" }
|
|
137
|
+
|
|
138
|
+
const targetPath = pathname.startsWith("/")
|
|
139
|
+
? this.repository.environment.path.normalize(pathname)
|
|
140
|
+
: this.repository.environment.path.join(
|
|
141
|
+
this.repository.environment.path.dirname(source.path),
|
|
142
|
+
pathname,
|
|
143
|
+
)
|
|
144
|
+
// match URL semantics: directory links resolve to their index source
|
|
145
|
+
const linkedSource =
|
|
146
|
+
this.repository.atPath(targetPath) ?? this.pageSourceAt(targetPath)
|
|
147
|
+
if (linkedSource)
|
|
148
|
+
return { type: "source", targetPath: linkedSource.path, fragment }
|
|
149
|
+
if (await this.repository.isFile(targetPath))
|
|
150
|
+
return { type: "asset", targetPath, fragment }
|
|
151
|
+
return { type: "unresolved", targetPath, fragment }
|
|
152
|
+
}
|
|
153
|
+
}
|
package/host/compiler.ts
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CompiledPage,
|
|
3
|
+
CompileContext,
|
|
4
|
+
PageContext,
|
|
5
|
+
} from "../plugins/types.ts"
|
|
6
|
+
import {
|
|
7
|
+
deriveSourceMeta,
|
|
8
|
+
interpolate,
|
|
9
|
+
type Source,
|
|
10
|
+
type SourceBlock,
|
|
11
|
+
} from "../source/index.ts"
|
|
12
|
+
import {
|
|
13
|
+
dependencyKey,
|
|
14
|
+
DependencyIndex,
|
|
15
|
+
PageDependencies,
|
|
16
|
+
type Dependency,
|
|
17
|
+
type DependencyContext,
|
|
18
|
+
} from "../dependencies.ts"
|
|
19
|
+
import {
|
|
20
|
+
applyHeadingPermalinks,
|
|
21
|
+
headingIdsForBlock,
|
|
22
|
+
preserveTaskInstructions,
|
|
23
|
+
unwrapBlockFlow,
|
|
24
|
+
wrapBlockSection,
|
|
25
|
+
wrapPageArticle,
|
|
26
|
+
} from "../document.ts"
|
|
27
|
+
import { isPageSource, publicPath } from "../server/router.ts"
|
|
28
|
+
import { escapeHtml } from "../util.ts"
|
|
29
|
+
import { elementTagNames } from "../view/syntax.ts"
|
|
30
|
+
import type { Engine } from "./engine.ts"
|
|
31
|
+
import type { PluginHost, CompiledViewPlan } from "./plugin-host.ts"
|
|
32
|
+
|
|
33
|
+
/** Owns compilation state, page caches, dependencies, and View plans. */
|
|
34
|
+
export class PageCompiler {
|
|
35
|
+
readonly pageCache = new Map<string, CompiledPage>()
|
|
36
|
+
private compiling = new Set<string>()
|
|
37
|
+
private pageBuilds = new WeakMap<Source, Promise<CompiledPage>>()
|
|
38
|
+
private dependencies = new DependencyIndex()
|
|
39
|
+
private viewPlans = new Map<string, CompiledViewPlan>()
|
|
40
|
+
private sourceElementTags = new Map<string, ReadonlySet<string>>()
|
|
41
|
+
|
|
42
|
+
constructor(
|
|
43
|
+
private engine: Engine,
|
|
44
|
+
private plugins: PluginHost,
|
|
45
|
+
) {}
|
|
46
|
+
|
|
47
|
+
indexSource(source: Source) {
|
|
48
|
+
this.sourceElementTags.set(source.path, elementTagNames(source.content))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
unindexSource(path: string) {
|
|
52
|
+
this.sourceElementTags.delete(path)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
dependentsOf(dependency: string | Dependency) {
|
|
56
|
+
return this.dependencies.dependentsOf(
|
|
57
|
+
typeof dependency === "string" ? dependency : dependencyKey(dependency),
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
dependenciesOf(path: string) {
|
|
62
|
+
return this.dependencies.dependenciesOf(path)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
planFor(path: string) {
|
|
66
|
+
return this.viewPlans.get(path)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
isPage(source: Source) {
|
|
70
|
+
return isPageSource(source)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
compilePage(source: Source) {
|
|
74
|
+
return this.engine.perf.measureAsync(
|
|
75
|
+
"compile.page",
|
|
76
|
+
async () => {
|
|
77
|
+
const dependencies = new PageDependencies()
|
|
78
|
+
let html = wrapPageArticle(
|
|
79
|
+
source,
|
|
80
|
+
await this.compilePageBody(source, dependencies),
|
|
81
|
+
)
|
|
82
|
+
const { assets, addAsset, addBrowserBundle } =
|
|
83
|
+
this.engine.documentAssetBag()
|
|
84
|
+
const ctx: PageContext = {
|
|
85
|
+
source,
|
|
86
|
+
addAsset,
|
|
87
|
+
addBrowserBundle,
|
|
88
|
+
depend: dependencies.depend,
|
|
89
|
+
}
|
|
90
|
+
html = await this.plugins.page(html, ctx, this.engine)
|
|
91
|
+
const compiled = await this.plugins.compileViewComponents(
|
|
92
|
+
html,
|
|
93
|
+
source,
|
|
94
|
+
ctx,
|
|
95
|
+
this.engine,
|
|
96
|
+
)
|
|
97
|
+
html = compiled.html
|
|
98
|
+
this.dependencies.replaceDependent(source.path, dependencies.keys)
|
|
99
|
+
if (compiled.plan) {
|
|
100
|
+
this.viewPlans.set(source.path, compiled.plan)
|
|
101
|
+
} else {
|
|
102
|
+
this.viewPlans.delete(source.path)
|
|
103
|
+
}
|
|
104
|
+
return Object.freeze({
|
|
105
|
+
html,
|
|
106
|
+
assets: Object.freeze([...assets]),
|
|
107
|
+
sourceId: source.id,
|
|
108
|
+
route: publicPath(source),
|
|
109
|
+
})
|
|
110
|
+
},
|
|
111
|
+
source.path,
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Return the cached page, compiling it once on the first request. */
|
|
116
|
+
ensurePage(source: Source) {
|
|
117
|
+
const cached = this.pageCache.get(source.path)
|
|
118
|
+
if (cached) return Promise.resolve(cached)
|
|
119
|
+
return this.cachePage(source)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private cachePage(source: Source) {
|
|
123
|
+
const existing = this.pageBuilds.get(source)
|
|
124
|
+
if (existing) return existing
|
|
125
|
+
|
|
126
|
+
const promise = this.compilePage(source)
|
|
127
|
+
.then((page) => {
|
|
128
|
+
if (this.engine.repo.atPath(source.path) === source) {
|
|
129
|
+
this.pageCache.set(source.path, page)
|
|
130
|
+
}
|
|
131
|
+
return page
|
|
132
|
+
})
|
|
133
|
+
.finally(() => this.pageBuilds.delete(source))
|
|
134
|
+
this.pageBuilds.set(source, promise)
|
|
135
|
+
return promise
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private async compilePageBody(
|
|
139
|
+
source: Source,
|
|
140
|
+
dependencies: DependencyContext,
|
|
141
|
+
) {
|
|
142
|
+
const parts: string[] = []
|
|
143
|
+
if (source.blocks.length) {
|
|
144
|
+
for (const block of source.blocks) {
|
|
145
|
+
parts.push(await this.compileBlock(source, block, dependencies))
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
parts.push(
|
|
149
|
+
wrapBlockSection(
|
|
150
|
+
{
|
|
151
|
+
id: "block-0",
|
|
152
|
+
path: source.path + "#block-0",
|
|
153
|
+
type: "block",
|
|
154
|
+
},
|
|
155
|
+
await this.compileContent(
|
|
156
|
+
source,
|
|
157
|
+
source.body,
|
|
158
|
+
source.data,
|
|
159
|
+
undefined,
|
|
160
|
+
dependencies,
|
|
161
|
+
),
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
return parts.filter(Boolean).join("\n")
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Compile a Source as a document without recursively applying its caller. */
|
|
169
|
+
async compileDocument(
|
|
170
|
+
source: Source,
|
|
171
|
+
dependencies: DependencyContext,
|
|
172
|
+
excludedPluginId: string,
|
|
173
|
+
) {
|
|
174
|
+
const parts: string[] = []
|
|
175
|
+
if (source.blocks.length) {
|
|
176
|
+
for (const block of source.blocks) {
|
|
177
|
+
parts.push(await this.compileBlockContent(source, block, dependencies))
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
parts.push(
|
|
181
|
+
await this.compileContent(
|
|
182
|
+
source,
|
|
183
|
+
source.body,
|
|
184
|
+
source.data,
|
|
185
|
+
undefined,
|
|
186
|
+
dependencies,
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
let html = parts.filter(Boolean).join("\n")
|
|
191
|
+
const { assets, addAsset, addBrowserBundle } =
|
|
192
|
+
this.engine.documentAssetBag()
|
|
193
|
+
const ctx: PageContext = {
|
|
194
|
+
source,
|
|
195
|
+
addAsset,
|
|
196
|
+
addBrowserBundle,
|
|
197
|
+
depend: dependencies.depend.bind(dependencies),
|
|
198
|
+
}
|
|
199
|
+
html = await this.plugins.page(html, ctx, this.engine, excludedPluginId)
|
|
200
|
+
return { html, assets }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async compileBlock(
|
|
204
|
+
source: Source,
|
|
205
|
+
block: SourceBlock,
|
|
206
|
+
dependencies: DependencyContext = new PageDependencies(),
|
|
207
|
+
options?: { id?: string },
|
|
208
|
+
scope: CompileContext["scope"] = "page",
|
|
209
|
+
) {
|
|
210
|
+
if (this.compiling.has(block.path)) {
|
|
211
|
+
return "<p>[Circular Include: " + escapeHtml(block.path) + "]</p>"
|
|
212
|
+
}
|
|
213
|
+
this.compiling.add(block.path)
|
|
214
|
+
try {
|
|
215
|
+
const inner = await this.compileBlockContent(
|
|
216
|
+
source,
|
|
217
|
+
block,
|
|
218
|
+
dependencies,
|
|
219
|
+
scope,
|
|
220
|
+
)
|
|
221
|
+
const flow = unwrapBlockFlow(inner)
|
|
222
|
+
if (flow !== undefined) return flow
|
|
223
|
+
return wrapBlockSection(block, inner, options)
|
|
224
|
+
} finally {
|
|
225
|
+
this.compiling.delete(block.path)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private compileBlockContent(
|
|
230
|
+
source: Source,
|
|
231
|
+
block: SourceBlock,
|
|
232
|
+
dependencies: DependencyContext,
|
|
233
|
+
scope: CompileContext["scope"] = "page",
|
|
234
|
+
) {
|
|
235
|
+
return this.compileContent(
|
|
236
|
+
source,
|
|
237
|
+
block.body,
|
|
238
|
+
{ ...source.data, ...block.data },
|
|
239
|
+
block,
|
|
240
|
+
dependencies,
|
|
241
|
+
scope,
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Compile-time interpolation context: authored data plus source fields. */
|
|
246
|
+
private interpolationContext(source: Source, data: Source["data"]) {
|
|
247
|
+
return {
|
|
248
|
+
...data,
|
|
249
|
+
source: {
|
|
250
|
+
id: source.id,
|
|
251
|
+
path: source.path,
|
|
252
|
+
name: source.name,
|
|
253
|
+
type: source.type,
|
|
254
|
+
href: publicPath(source),
|
|
255
|
+
...deriveSourceMeta(source),
|
|
256
|
+
},
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private async compileContent(
|
|
261
|
+
source: Source,
|
|
262
|
+
markdown: string,
|
|
263
|
+
data: Source["data"],
|
|
264
|
+
block?: SourceBlock,
|
|
265
|
+
dependencies: DependencyContext = new PageDependencies(),
|
|
266
|
+
scope: CompileContext["scope"] = "page",
|
|
267
|
+
) {
|
|
268
|
+
const ctx: CompileContext = {
|
|
269
|
+
source,
|
|
270
|
+
block,
|
|
271
|
+
scope,
|
|
272
|
+
slots: [],
|
|
273
|
+
elements: this.sourceElementTags.get(source.path) ?? new Set(),
|
|
274
|
+
depend: dependencies.depend,
|
|
275
|
+
}
|
|
276
|
+
let content = this.plugins.compileVariables(markdown, ctx)
|
|
277
|
+
content = interpolate(content, this.interpolationContext(source, data))
|
|
278
|
+
content = await this.plugins.precompile(content, ctx, this.engine)
|
|
279
|
+
let html = this.engine.environment.renderMarkdown(content)
|
|
280
|
+
html = preserveTaskInstructions(html)
|
|
281
|
+
if (block) {
|
|
282
|
+
const headings = headingIdsForBlock(source, block)
|
|
283
|
+
html = applyHeadingPermalinks(html, headings, block.headings)
|
|
284
|
+
}
|
|
285
|
+
return this.plugins.postcompile(html, ctx, this.engine)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
buildCache() {
|
|
289
|
+
return this.engine.perf.measureAsync("cache.build", async () => {
|
|
290
|
+
for (const source of this.engine.repo.all()) {
|
|
291
|
+
if (this.isPage(source)) await this.ensurePage(source)
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async recompilePages(paths: Iterable<string>) {
|
|
297
|
+
const seen = new Set<string>()
|
|
298
|
+
for (const pagePath of paths) {
|
|
299
|
+
if (seen.has(pagePath)) continue
|
|
300
|
+
seen.add(pagePath)
|
|
301
|
+
const page =
|
|
302
|
+
this.engine.repo.atPath(pagePath) ?? this.engine.repo.get(pagePath)
|
|
303
|
+
if (!page || !this.isPage(page)) continue
|
|
304
|
+
if (!this.pageCache.has(page.path)) continue
|
|
305
|
+
await this.cachePage(page)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
forgetCompiled(path: string) {
|
|
310
|
+
this.pageCache.delete(path)
|
|
311
|
+
this.viewPlans.delete(path)
|
|
312
|
+
this.dependencies.deleteDependent(path)
|
|
313
|
+
}
|
|
314
|
+
}
|