@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
package/server/routes.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { Engine } from "../host/engine.ts"
|
|
2
|
+
import type { Plugin, ServerRoute } from "../plugins/types.ts"
|
|
3
|
+
import { createServerContext } from "./context.ts"
|
|
4
|
+
import { protocolRoute } from "./protocols.ts"
|
|
5
|
+
|
|
6
|
+
type RegisteredRoute = ServerRoute & {
|
|
7
|
+
plugin: string
|
|
8
|
+
relative: string
|
|
9
|
+
prefix?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const API_ROOT = "/api"
|
|
13
|
+
|
|
14
|
+
const METHOD_ORDER = [
|
|
15
|
+
"GET",
|
|
16
|
+
"HEAD",
|
|
17
|
+
"POST",
|
|
18
|
+
"PUT",
|
|
19
|
+
"PATCH",
|
|
20
|
+
"DELETE",
|
|
21
|
+
"OPTIONS",
|
|
22
|
+
] as const
|
|
23
|
+
const ROUTE_METHODS = new Set([
|
|
24
|
+
"GET",
|
|
25
|
+
"POST",
|
|
26
|
+
"PUT",
|
|
27
|
+
"PATCH",
|
|
28
|
+
"DELETE",
|
|
29
|
+
"OPTIONS",
|
|
30
|
+
"*",
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
function canonicalRoutePath(path: string) {
|
|
34
|
+
if (path.startsWith("/") || path.includes("?") || path.includes("#")) {
|
|
35
|
+
throw new Error(`Invalid server route path: ${path}`)
|
|
36
|
+
}
|
|
37
|
+
const wildcard = path === "*" || path.endsWith("/*")
|
|
38
|
+
const base = path === "*" ? "" : wildcard ? path.slice(0, -2) : path
|
|
39
|
+
if (base.includes("*") || base.includes("\\") || /%2f|%5c/i.test(base)) {
|
|
40
|
+
throw new Error(`Invalid server route path: ${path}`)
|
|
41
|
+
}
|
|
42
|
+
const mounted = `${API_ROOT}/${base}`
|
|
43
|
+
const url = new URL(mounted, "http://pathmx.local")
|
|
44
|
+
if (
|
|
45
|
+
url.pathname !== mounted ||
|
|
46
|
+
url.search ||
|
|
47
|
+
url.hash ||
|
|
48
|
+
base.split("/").includes("..")
|
|
49
|
+
) {
|
|
50
|
+
throw new Error(`Invalid server route path: ${path}`)
|
|
51
|
+
}
|
|
52
|
+
return wildcard ? (base ? `${base}/*` : "*") : base
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function mountedRoute(plugin: string, path: string) {
|
|
56
|
+
const suffix = path ? `/${path}` : ""
|
|
57
|
+
return `${API_ROOT}/${plugin}${suffix}`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function relativeRequestPath(plugin: string, pathname: string) {
|
|
61
|
+
const root = `${API_ROOT}/${plugin}`
|
|
62
|
+
return pathname === root ? "" : pathname.slice(root.length + 1)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function prefixFor(path: string) {
|
|
66
|
+
return path.endsWith("/*") ? path.slice(0, -1) : undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function methodsOverlap(a: ServerRoute["method"], b: ServerRoute["method"]) {
|
|
70
|
+
return a === "*" || b === "*" || a === b
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function pathsOverlap(a: RegisteredRoute, b: RegisteredRoute) {
|
|
74
|
+
const aPrefix = a.prefix
|
|
75
|
+
const bPrefix = b.prefix
|
|
76
|
+
if (!aPrefix && !bPrefix) return a.path === b.path
|
|
77
|
+
if (aPrefix && bPrefix) {
|
|
78
|
+
return aPrefix.startsWith(bPrefix) || bPrefix.startsWith(aPrefix)
|
|
79
|
+
}
|
|
80
|
+
if (aPrefix) return b.path.startsWith(aPrefix)
|
|
81
|
+
if (bPrefix) return a.path.startsWith(bPrefix)
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function overlapMessage(a: RegisteredRoute, b: RegisteredRoute) {
|
|
86
|
+
return `Overlapping server routes: ${a.method} ${a.path} (${a.plugin}) and ${b.method} ${b.path} (${b.plugin})`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function withoutBody(response: Response) {
|
|
90
|
+
return new Response(null, {
|
|
91
|
+
status: response.status,
|
|
92
|
+
statusText: response.statusText,
|
|
93
|
+
headers: response.headers,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class ServerRouteRegistry {
|
|
98
|
+
private routes: RegisteredRoute[] = []
|
|
99
|
+
private exact = new Map<string, RegisteredRoute[]>()
|
|
100
|
+
private prefixes: RegisteredRoute[] = []
|
|
101
|
+
|
|
102
|
+
prepare(plugin: Plugin) {
|
|
103
|
+
const pending = (plugin.routes ?? []).map((route) => {
|
|
104
|
+
if (!ROUTE_METHODS.has(route.method)) {
|
|
105
|
+
throw new Error(`Invalid server route method: ${route.method}`)
|
|
106
|
+
}
|
|
107
|
+
const relative = canonicalRoutePath(route.path)
|
|
108
|
+
const path = mountedRoute(plugin.id, relative)
|
|
109
|
+
return {
|
|
110
|
+
...route,
|
|
111
|
+
path,
|
|
112
|
+
relative,
|
|
113
|
+
plugin: plugin.id,
|
|
114
|
+
prefix: prefixFor(path),
|
|
115
|
+
} satisfies RegisteredRoute
|
|
116
|
+
})
|
|
117
|
+
const candidates = [...this.routes]
|
|
118
|
+
for (const route of pending) {
|
|
119
|
+
for (const existing of candidates) {
|
|
120
|
+
if (
|
|
121
|
+
pathsOverlap(route, existing) &&
|
|
122
|
+
(route.path !== existing.path ||
|
|
123
|
+
methodsOverlap(route.method, existing.method))
|
|
124
|
+
) {
|
|
125
|
+
throw new Error(overlapMessage(existing, route))
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
candidates.push(route)
|
|
129
|
+
}
|
|
130
|
+
return pending
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
register(routes: readonly RegisteredRoute[]) {
|
|
134
|
+
for (const route of routes) {
|
|
135
|
+
this.routes.push(route)
|
|
136
|
+
if (route.prefix) {
|
|
137
|
+
this.prefixes.push(route)
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
const exact = this.exact.get(route.path) ?? []
|
|
141
|
+
exact.push(route)
|
|
142
|
+
this.exact.set(route.path, exact)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async dispatch(
|
|
147
|
+
request: Request,
|
|
148
|
+
url: URL,
|
|
149
|
+
app: Engine,
|
|
150
|
+
publicUrl: URL = url,
|
|
151
|
+
) {
|
|
152
|
+
const pathname = protocolRoute(url.pathname) ?? url.pathname
|
|
153
|
+
const exact = this.exact.get(pathname)
|
|
154
|
+
const matches = exact ? [...exact] : []
|
|
155
|
+
if (!exact) {
|
|
156
|
+
for (const route of this.prefixes) {
|
|
157
|
+
const prefix = route.prefix
|
|
158
|
+
if (prefix && pathname.startsWith(prefix)) matches.push(route)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!matches.length) return
|
|
162
|
+
|
|
163
|
+
const method = request.method.toUpperCase()
|
|
164
|
+
const route = matches.find(
|
|
165
|
+
(candidate) =>
|
|
166
|
+
candidate.method === "*" ||
|
|
167
|
+
candidate.method === method ||
|
|
168
|
+
(method === "HEAD" && candidate.method === "GET"),
|
|
169
|
+
)
|
|
170
|
+
if (!route) {
|
|
171
|
+
const allowed = new Set(
|
|
172
|
+
matches.flatMap((candidate) =>
|
|
173
|
+
candidate.method === "GET" ? ["GET", "HEAD"] : [candidate.method],
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
return new Response("Method Not Allowed", {
|
|
177
|
+
status: 405,
|
|
178
|
+
headers: {
|
|
179
|
+
Allow: METHOD_ORDER.filter((candidate) =>
|
|
180
|
+
allowed.has(candidate),
|
|
181
|
+
).join(", "),
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const response = await app.perf.measureAsync(
|
|
187
|
+
`route.${route.plugin}`,
|
|
188
|
+
async () =>
|
|
189
|
+
route.handle(
|
|
190
|
+
createServerContext(
|
|
191
|
+
app,
|
|
192
|
+
route.plugin,
|
|
193
|
+
relativeRequestPath(route.plugin, pathname),
|
|
194
|
+
request,
|
|
195
|
+
publicUrl,
|
|
196
|
+
),
|
|
197
|
+
),
|
|
198
|
+
route.path,
|
|
199
|
+
)
|
|
200
|
+
return method === "HEAD" ? withoutBody(response) : response
|
|
201
|
+
}
|
|
202
|
+
}
|
package/server/serve.ts
ADDED
package/server/watch.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import watcher from "@parcel/watcher"
|
|
2
|
+
import { realpathSync } from "node:fs"
|
|
3
|
+
import { relative } from "node:path"
|
|
4
|
+
import type { Engine } from "../host/engine.ts"
|
|
5
|
+
import { SourceDiagnosticError } from "../source-diagnostic.ts"
|
|
6
|
+
import { CssImportIndex } from "./css-imports.ts"
|
|
7
|
+
|
|
8
|
+
type UnresolvedLink = {
|
|
9
|
+
source: string
|
|
10
|
+
href: string
|
|
11
|
+
target?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function unresolvedLinks(app: Engine) {
|
|
15
|
+
const unresolved = new Map<string, UnresolvedLink>()
|
|
16
|
+
for (const edge of app.graph.unresolvedLinks()) {
|
|
17
|
+
const key = `${edge.from}\0${edge.href}`
|
|
18
|
+
unresolved.set(key, {
|
|
19
|
+
source: edge.from,
|
|
20
|
+
href: edge.href,
|
|
21
|
+
target: edge.to,
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
return unresolved
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function reportLinkChanges(
|
|
28
|
+
app: Engine,
|
|
29
|
+
previous?: ReadonlyMap<string, UnresolvedLink>,
|
|
30
|
+
) {
|
|
31
|
+
const current = unresolvedLinks(app)
|
|
32
|
+
for (const [key, link] of current) {
|
|
33
|
+
if (previous?.has(key)) continue
|
|
34
|
+
app.log.warn("link.unresolved", link)
|
|
35
|
+
}
|
|
36
|
+
if (previous) {
|
|
37
|
+
for (const [key, link] of previous) {
|
|
38
|
+
if (current.has(key)) continue
|
|
39
|
+
app.log.info("link.resolved", { source: link.source, href: link.href })
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return current
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function watchPaths(app: Engine, root: string) {
|
|
46
|
+
const base = realpathSync(root)
|
|
47
|
+
const cssImports = new CssImportIndex()
|
|
48
|
+
let queue = Promise.resolve()
|
|
49
|
+
let unresolved = reportLinkChanges(app)
|
|
50
|
+
|
|
51
|
+
const indexStylesheet = async (rel: string) => {
|
|
52
|
+
const path = app.repo.graphPath(rel)
|
|
53
|
+
const storagePath = app.repo.storagePath(path)
|
|
54
|
+
if (!storagePath) return cssImports.replace(path)
|
|
55
|
+
try {
|
|
56
|
+
cssImports.replace(
|
|
57
|
+
path,
|
|
58
|
+
await app.environment.storage.readText(storagePath),
|
|
59
|
+
)
|
|
60
|
+
} catch {
|
|
61
|
+
cssImports.replace(path)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const initializeStylesheets = async () => {
|
|
66
|
+
const files = [...(await app.environment.storage.list(base))].filter(
|
|
67
|
+
(file) => file.endsWith(".css"),
|
|
68
|
+
)
|
|
69
|
+
for (let start = 0; start < files.length; start += 16) {
|
|
70
|
+
await Promise.all(files.slice(start, start + 16).map(indexStylesheet))
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const enqueue = (work: () => Promise<void>) => {
|
|
75
|
+
queue = queue.then(work).catch((error: Error) => {
|
|
76
|
+
if (error instanceof SourceDiagnosticError) {
|
|
77
|
+
app.log.error("source.invalid", error.diagnostic)
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
app.log.error("watch.error", { message: error.message })
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const subscription = initializeStylesheets().then(() =>
|
|
85
|
+
watcher.subscribe(base, (error, events) => {
|
|
86
|
+
if (error) {
|
|
87
|
+
app.log.error("watch.error", { message: error.message })
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
enqueue(async () => {
|
|
91
|
+
const changedStyles = new Set<string>()
|
|
92
|
+
for (const event of events) {
|
|
93
|
+
const rel = relative(base, event.path)
|
|
94
|
+
if (!rel || rel.startsWith("..")) continue
|
|
95
|
+
app.log.debug("watch", { file: rel, type: event.type })
|
|
96
|
+
if (rel.endsWith(".md")) await app.syncFromStorage(rel)
|
|
97
|
+
else if (rel.endsWith(".css")) {
|
|
98
|
+
await indexStylesheet(rel)
|
|
99
|
+
for (const path of cssImports.affected(app.repo.graphPath(rel))) {
|
|
100
|
+
changedStyles.add(path)
|
|
101
|
+
}
|
|
102
|
+
} else if (rel.endsWith(".js")) app.invalidateAsset(rel)
|
|
103
|
+
}
|
|
104
|
+
if (changedStyles.size) app.invalidateAssets(changedStyles)
|
|
105
|
+
unresolved = reportLinkChanges(app, unresolved)
|
|
106
|
+
})
|
|
107
|
+
}),
|
|
108
|
+
)
|
|
109
|
+
subscription.catch((error: Error) =>
|
|
110
|
+
app.log.error("watch.error", { message: error.message }),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
subscription,
|
|
115
|
+
async close() {
|
|
116
|
+
const sub = await subscription.catch(() => undefined)
|
|
117
|
+
await sub?.unsubscribe()
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
}
|
package/source/blocks.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { parseLinkFragments } from "./markdown/links.ts"
|
|
2
|
+
import { parseMarkdownLists } from "./markdown/lists.ts"
|
|
3
|
+
import { scanMarkdownStructure } from "./markdown/scan.ts"
|
|
4
|
+
import type { PathMXYaml } from "../environment.ts"
|
|
5
|
+
import { SourceDiagnosticError } from "../source-diagnostic.ts"
|
|
6
|
+
import type { Source, SourceNode } from "./source.ts"
|
|
7
|
+
import {
|
|
8
|
+
splitTopmatter,
|
|
9
|
+
type Topmatter,
|
|
10
|
+
YamlMapParseError,
|
|
11
|
+
} from "./topmatter.ts"
|
|
12
|
+
|
|
13
|
+
export type SourceBlock = SourceNode
|
|
14
|
+
|
|
15
|
+
function authoredType(data: Topmatter) {
|
|
16
|
+
return typeof data.type === "string" && data.type.trim()
|
|
17
|
+
? data.type.trim()
|
|
18
|
+
: "block"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function authoredId(data: Topmatter) {
|
|
22
|
+
return typeof data.id === "string" && data.id.trim()
|
|
23
|
+
? data.id.trim()
|
|
24
|
+
: undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createBlock(
|
|
28
|
+
parent: Source,
|
|
29
|
+
ordinal: number,
|
|
30
|
+
content: string,
|
|
31
|
+
topmatter: Topmatter = {},
|
|
32
|
+
yaml?: PathMXYaml,
|
|
33
|
+
): SourceBlock {
|
|
34
|
+
let split: ReturnType<typeof splitTopmatter>
|
|
35
|
+
try {
|
|
36
|
+
split = splitTopmatter(content, yaml)
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (!(error instanceof YamlMapParseError)) throw error
|
|
39
|
+
throw new SourceDiagnosticError({
|
|
40
|
+
source: parent.path,
|
|
41
|
+
plugin: "source",
|
|
42
|
+
message: `Invalid Block topmatter in ${parent.path} (block ${ordinal + 1}): ${error.message}`,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
const data = { ...split.data, ...topmatter }
|
|
46
|
+
const body = split.body.trim()
|
|
47
|
+
const id = authoredId(data) ?? `block-${ordinal}`
|
|
48
|
+
const structure = scanMarkdownStructure(body)
|
|
49
|
+
let listStructure: ReturnType<typeof parseMarkdownLists> | undefined
|
|
50
|
+
const parseListStructure = () => (listStructure ??= parseMarkdownLists(body))
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
id,
|
|
54
|
+
path: `${parent.path}#${id}`,
|
|
55
|
+
type: authoredType(data),
|
|
56
|
+
data,
|
|
57
|
+
links: parseLinkFragments(structure.linkFragments),
|
|
58
|
+
headings: structure.headings,
|
|
59
|
+
fences: structure.fences,
|
|
60
|
+
/** Lists are cached only when a plugin asks for their structure. */
|
|
61
|
+
get lists() {
|
|
62
|
+
return parseListStructure().lists
|
|
63
|
+
},
|
|
64
|
+
/** Tasks share the lazy list parse instead of rescanning Markdown. */
|
|
65
|
+
get tasks() {
|
|
66
|
+
return parseListStructure().tasks
|
|
67
|
+
},
|
|
68
|
+
content,
|
|
69
|
+
body,
|
|
70
|
+
}
|
|
71
|
+
}
|
package/source/format.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { PathMXYaml } from "../environment.ts"
|
|
2
|
+
import { splitMarkdownBlocks } from "./markdown/blocks.ts"
|
|
3
|
+
import { splitFrontmatter, splitTopmatter } from "./topmatter.ts"
|
|
4
|
+
import type { Topmatter } from "./topmatter.ts"
|
|
5
|
+
|
|
6
|
+
export type BlockDraft = {
|
|
7
|
+
id?: string
|
|
8
|
+
type?: string
|
|
9
|
+
data?: Topmatter
|
|
10
|
+
body?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type BlockUpdate =
|
|
14
|
+
BlockDraft | ((current: Readonly<BlockDraft>) => BlockDraft)
|
|
15
|
+
|
|
16
|
+
function compact(data: Topmatter) {
|
|
17
|
+
return Object.fromEntries(
|
|
18
|
+
Object.entries(data).filter(
|
|
19
|
+
([, value]) => value !== undefined && value !== "",
|
|
20
|
+
),
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatFrontmatter(data: Topmatter, yaml: PathMXYaml) {
|
|
25
|
+
const fields = compact(data)
|
|
26
|
+
if (!Object.keys(fields).length) return ""
|
|
27
|
+
return `---\n${yaml.stringify(fields).trimEnd()}\n---\n`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function formatBlock(block: BlockDraft, yaml: PathMXYaml) {
|
|
31
|
+
const data = compact({
|
|
32
|
+
...block.data,
|
|
33
|
+
...(block.id ? { id: block.id } : {}),
|
|
34
|
+
...(block.type ? { type: block.type } : {}),
|
|
35
|
+
})
|
|
36
|
+
const comment = Object.keys(data).length
|
|
37
|
+
? `<!--\n${yaml.stringify(data).trimEnd()}\n-->`
|
|
38
|
+
: ""
|
|
39
|
+
const body = (block.body ?? "").trim()
|
|
40
|
+
return [comment, body].filter(Boolean).join("\n\n")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function appendBlockSection(
|
|
44
|
+
content: string,
|
|
45
|
+
block: BlockDraft,
|
|
46
|
+
yaml: PathMXYaml,
|
|
47
|
+
) {
|
|
48
|
+
const { data, body } = splitFrontmatter(content, yaml)
|
|
49
|
+
const sections = splitMarkdownBlocks(body)
|
|
50
|
+
sections.push(formatBlock(block, yaml))
|
|
51
|
+
return joinSections(data, sections, yaml)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sameTypeIndex(sections: string[], type: string | undefined) {
|
|
55
|
+
if (!type) return 0
|
|
56
|
+
const index = sections.findIndex(
|
|
57
|
+
(section) =>
|
|
58
|
+
new RegExp(`(?:^|\\n)type:\\s*${type}\\s*$`, "m").test(section) ||
|
|
59
|
+
section.includes(`<${type}`),
|
|
60
|
+
)
|
|
61
|
+
return index === -1 ? sections.length : index
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Insert a block before the first existing block of the same type (newest first). */
|
|
65
|
+
export function prependBlockSection(
|
|
66
|
+
content: string,
|
|
67
|
+
block: BlockDraft,
|
|
68
|
+
yaml: PathMXYaml,
|
|
69
|
+
) {
|
|
70
|
+
const { data, body } = splitFrontmatter(content, yaml)
|
|
71
|
+
const sections = splitMarkdownBlocks(body)
|
|
72
|
+
sections.splice(
|
|
73
|
+
sameTypeIndex(sections, block.type),
|
|
74
|
+
0,
|
|
75
|
+
formatBlock(block, yaml),
|
|
76
|
+
)
|
|
77
|
+
return `${formatFrontmatter(data, yaml)}${sections.join("\n\n---\n\n")}\n`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function formatNewSource(
|
|
81
|
+
data: Topmatter,
|
|
82
|
+
yaml: PathMXYaml,
|
|
83
|
+
blocks: BlockDraft[] = [],
|
|
84
|
+
) {
|
|
85
|
+
const front = formatFrontmatter(data, yaml)
|
|
86
|
+
if (!blocks.length) return front
|
|
87
|
+
const body = blocks
|
|
88
|
+
.map((block) => formatBlock(block, yaml))
|
|
89
|
+
.join("\n\n---\n\n")
|
|
90
|
+
return `${front}${front ? "\n" : ""}${body}\n`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function patchFrontmatter(
|
|
94
|
+
content: string,
|
|
95
|
+
patch: Topmatter,
|
|
96
|
+
yaml: PathMXYaml,
|
|
97
|
+
) {
|
|
98
|
+
const { data, body } = splitFrontmatter(content, yaml)
|
|
99
|
+
return `${formatFrontmatter({ ...data, ...patch }, yaml)}${body}`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function sectionId(section: string, ordinal: number, yaml: PathMXYaml) {
|
|
103
|
+
const { data } = splitTopmatter(section, yaml)
|
|
104
|
+
return typeof data.id === "string" && data.id.trim()
|
|
105
|
+
? data.id.trim()
|
|
106
|
+
: `block-${ordinal}`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function sectionIndex(sections: string[], id: string, yaml: PathMXYaml) {
|
|
110
|
+
const index = sections.findIndex(
|
|
111
|
+
(section, ordinal) => sectionId(section, ordinal, yaml) === id,
|
|
112
|
+
)
|
|
113
|
+
if (index === -1) throw new Error(`Block not found: ${id}`)
|
|
114
|
+
return index
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function joinSections(data: Topmatter, sections: string[], yaml: PathMXYaml) {
|
|
118
|
+
const front = formatFrontmatter(data, yaml)
|
|
119
|
+
if (!sections.length) return front
|
|
120
|
+
return `${front}${sections.join("\n\n---\n\n")}\n`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Merge topmatter into an existing block; replace body when `patch.body` is set. */
|
|
124
|
+
export function updateBlockSection(
|
|
125
|
+
content: string,
|
|
126
|
+
id: string,
|
|
127
|
+
update: BlockUpdate,
|
|
128
|
+
yaml: PathMXYaml,
|
|
129
|
+
) {
|
|
130
|
+
const { data, body } = splitFrontmatter(content, yaml)
|
|
131
|
+
const sections = splitMarkdownBlocks(body)
|
|
132
|
+
const index = sectionIndex(sections, id, yaml)
|
|
133
|
+
const current = splitTopmatter(sections[index]!, yaml)
|
|
134
|
+
const { id: currentId, type: currentType, ...currentData } = current.data
|
|
135
|
+
const snapshot: BlockDraft = {
|
|
136
|
+
...(typeof currentId === "string" ? { id: currentId } : {}),
|
|
137
|
+
...(typeof currentType === "string" ? { type: currentType } : {}),
|
|
138
|
+
data: currentData,
|
|
139
|
+
body: current.body,
|
|
140
|
+
}
|
|
141
|
+
const patch = typeof update === "function" ? update(snapshot) : update
|
|
142
|
+
sections[index] = formatBlock(
|
|
143
|
+
{
|
|
144
|
+
id: patch.id ?? snapshot.id,
|
|
145
|
+
type: patch.type ?? snapshot.type,
|
|
146
|
+
data: { ...snapshot.data, ...patch.data },
|
|
147
|
+
body: patch.body !== undefined ? patch.body : current.body,
|
|
148
|
+
},
|
|
149
|
+
yaml,
|
|
150
|
+
)
|
|
151
|
+
return joinSections(data, sections, yaml)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function removeBlockSection(
|
|
155
|
+
content: string,
|
|
156
|
+
id: string,
|
|
157
|
+
yaml: PathMXYaml,
|
|
158
|
+
) {
|
|
159
|
+
const { data, body } = splitFrontmatter(content, yaml)
|
|
160
|
+
const sections = splitMarkdownBlocks(body)
|
|
161
|
+
const index = sectionIndex(sections, id, yaml)
|
|
162
|
+
sections.splice(index, 1)
|
|
163
|
+
return joinSections(data, sections, yaml)
|
|
164
|
+
}
|
package/source/index.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export {
|
|
2
|
+
DEFAULT_SOURCE_TYPE,
|
|
3
|
+
createSource,
|
|
4
|
+
indexSourceDocument,
|
|
5
|
+
resolveSourceId,
|
|
6
|
+
rollupSourceIndexes,
|
|
7
|
+
sourceIdFromPath,
|
|
8
|
+
type Source,
|
|
9
|
+
type SourceNode,
|
|
10
|
+
} from "./source.ts"
|
|
11
|
+
export { createBlock, type SourceBlock } from "./blocks.ts"
|
|
12
|
+
export {
|
|
13
|
+
assignHeadingIds,
|
|
14
|
+
collectHeadings,
|
|
15
|
+
isExternalHref,
|
|
16
|
+
isFenceClose,
|
|
17
|
+
matchFenceOpen,
|
|
18
|
+
parseCodeFences,
|
|
19
|
+
parseLinkDirective,
|
|
20
|
+
parseLinks,
|
|
21
|
+
rewriteLinks,
|
|
22
|
+
unlinkLinks,
|
|
23
|
+
splitMarkdownBlocks,
|
|
24
|
+
type FenceOpen,
|
|
25
|
+
type LinkDirective,
|
|
26
|
+
type MarkdownCodeFence,
|
|
27
|
+
type MarkdownHeading,
|
|
28
|
+
type MarkdownList,
|
|
29
|
+
type MarkdownListItem,
|
|
30
|
+
type MarkdownListLink,
|
|
31
|
+
type MarkdownListParse,
|
|
32
|
+
type MarkdownTask,
|
|
33
|
+
type SourceLink,
|
|
34
|
+
type SourceLinkKind,
|
|
35
|
+
type SourceLinkType,
|
|
36
|
+
markdownListItemLink,
|
|
37
|
+
parseMarkdownLists,
|
|
38
|
+
} from "./markdown/index.ts"
|
|
39
|
+
export {
|
|
40
|
+
appendBlockSection,
|
|
41
|
+
prependBlockSection,
|
|
42
|
+
removeBlockSection,
|
|
43
|
+
updateBlockSection,
|
|
44
|
+
formatBlock,
|
|
45
|
+
formatFrontmatter,
|
|
46
|
+
formatNewSource,
|
|
47
|
+
patchFrontmatter,
|
|
48
|
+
type BlockDraft,
|
|
49
|
+
type BlockUpdate,
|
|
50
|
+
} from "./format.ts"
|
|
51
|
+
export { interpolate, interpolateText } from "./interpolate.ts"
|
|
52
|
+
export {
|
|
53
|
+
deriveBlockMeta,
|
|
54
|
+
deriveSourceMeta,
|
|
55
|
+
type BlockMeta,
|
|
56
|
+
type SourceMeta,
|
|
57
|
+
} from "./meta.ts"
|
|
58
|
+
export {
|
|
59
|
+
parseYamlMap,
|
|
60
|
+
splitFrontmatter,
|
|
61
|
+
splitTopmatter,
|
|
62
|
+
type Topmatter,
|
|
63
|
+
} from "./topmatter.ts"
|
|
64
|
+
export {
|
|
65
|
+
parseSourceTypeAndQualifiers,
|
|
66
|
+
type SourcePathNameMeta,
|
|
67
|
+
} from "./utils.ts"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Topmatter } from "./topmatter.ts"
|
|
2
|
+
import { isFenceClose, matchFenceOpen } from "./markdown/fences.ts"
|
|
3
|
+
import { escapeHtml } from "../util.ts"
|
|
4
|
+
|
|
5
|
+
const TOKEN = /\{\{\s*([A-Za-z_][\w-]*(?:\.[A-Za-z_][\w-]*)*)\s*\}\}/g
|
|
6
|
+
|
|
7
|
+
function lookup(data: Topmatter, path: string) {
|
|
8
|
+
let current: unknown = data
|
|
9
|
+
for (const key of path.split(".")) {
|
|
10
|
+
if (!current || typeof current !== "object" || Array.isArray(current))
|
|
11
|
+
return ""
|
|
12
|
+
current = (current as Record<string, unknown>)[key]
|
|
13
|
+
}
|
|
14
|
+
if (
|
|
15
|
+
typeof current === "string" ||
|
|
16
|
+
typeof current === "number" ||
|
|
17
|
+
typeof current === "boolean"
|
|
18
|
+
) {
|
|
19
|
+
return String(current)
|
|
20
|
+
}
|
|
21
|
+
return ""
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function interpolateLine(line: string, data: Topmatter) {
|
|
25
|
+
return line.replace(TOKEN, (_match, path: string) =>
|
|
26
|
+
escapeHtml(lookup(data, path)),
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Fill `{{ key }}` tokens in a plain-text value without HTML-escaping.
|
|
32
|
+
* For derived values (meta text, attributes) that are escaped at render time.
|
|
33
|
+
*/
|
|
34
|
+
export function interpolateText(text: string, data: Topmatter) {
|
|
35
|
+
return text.replace(TOKEN, (_match, path: string) => lookup(data, path))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Replace `{{ key }}` / `{{ nested.key }}` from a node's data bag.
|
|
40
|
+
* Missing paths become `""`. Fenced code is left untouched.
|
|
41
|
+
*/
|
|
42
|
+
export function interpolate(markdown: string, data: Topmatter) {
|
|
43
|
+
const lines = markdown.split(/\r?\n/)
|
|
44
|
+
const out: string[] = []
|
|
45
|
+
let marker: string | undefined
|
|
46
|
+
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
if (marker) {
|
|
49
|
+
out.push(line)
|
|
50
|
+
if (isFenceClose(line, marker)) marker = undefined
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
const fence = matchFenceOpen(line)
|
|
54
|
+
if (fence) {
|
|
55
|
+
marker = fence.marker
|
|
56
|
+
out.push(line)
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
out.push(interpolateLine(line, data))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return out.join("\n")
|
|
63
|
+
}
|