@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,287 @@
|
|
|
1
|
+
import { escapeHtml } from "../../util.ts"
|
|
2
|
+
import type { HtmlRewriteRule, HtmlStringRewrite } from "../../environment.ts"
|
|
3
|
+
import type { AppContext, PageContext, Plugin } from "../types.ts"
|
|
4
|
+
|
|
5
|
+
export type DatatypeChartType = "bar" | "sparkline" | "pie"
|
|
6
|
+
|
|
7
|
+
export type DatatypeExpression = {
|
|
8
|
+
type: DatatypeChartType
|
|
9
|
+
code: "b" | "l" | "p"
|
|
10
|
+
values: number[]
|
|
11
|
+
canonical: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type DatatypePluginOptions = {
|
|
15
|
+
autoWrap?: boolean
|
|
16
|
+
className?: string
|
|
17
|
+
width?: number
|
|
18
|
+
weight?: number
|
|
19
|
+
preload?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEFAULT_OPTIONS = {
|
|
23
|
+
autoWrap: true,
|
|
24
|
+
className: "pmx-datatype",
|
|
25
|
+
width: 100,
|
|
26
|
+
weight: 400,
|
|
27
|
+
preload: true,
|
|
28
|
+
} satisfies Required<DatatypePluginOptions>
|
|
29
|
+
|
|
30
|
+
const CHART_TYPE_BY_CODE = {
|
|
31
|
+
b: "bar",
|
|
32
|
+
l: "sparkline",
|
|
33
|
+
p: "pie",
|
|
34
|
+
} as const
|
|
35
|
+
|
|
36
|
+
const EXPRESSION = /\{([blp]):([^{}\n]+)\}/g
|
|
37
|
+
// Cheap pre-scan: EXPRESSION can only match where a literal `{b:`/`{l:`/`{p:`
|
|
38
|
+
// appears, so pages without charts skip the HTML transform entirely.
|
|
39
|
+
const EXPRESSION_HINT = /\{[blp]:/
|
|
40
|
+
const FONT_URL = new URL("./Datatype[wdth,wght].woff2", import.meta.url)
|
|
41
|
+
const LICENSE_URL = new URL("./OFL.txt", import.meta.url)
|
|
42
|
+
const HTML_ROOT = "pmx-datatype-root"
|
|
43
|
+
const HTML_ROOT_OPEN = `<${HTML_ROOT}>`
|
|
44
|
+
const HTML_ROOT_CLOSE = `</${HTML_ROOT}>`
|
|
45
|
+
const PROTECTED_ELEMENTS = ["code", "pre", "script", "style", "textarea"]
|
|
46
|
+
|
|
47
|
+
function clampAxis(value: number, min: number, max: number, fallback: number) {
|
|
48
|
+
if (!Number.isFinite(value)) return fallback
|
|
49
|
+
return Math.min(max, Math.max(min, value))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeClassName(value: string | undefined) {
|
|
53
|
+
const className = value?.trim()
|
|
54
|
+
return className || DEFAULT_OPTIONS.className
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function escapeCssString(value: string) {
|
|
58
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function escapeCssClass(value: string) {
|
|
62
|
+
return value
|
|
63
|
+
.split(/\s+/)
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.map((part) =>
|
|
66
|
+
part.replace(
|
|
67
|
+
/[^a-zA-Z0-9_-]/g,
|
|
68
|
+
(char) => `\\${char.codePointAt(0)!.toString(16)} `,
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
.map((part) => `.${part}`)
|
|
72
|
+
.join("")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function describeExpression(expression: DatatypeExpression) {
|
|
76
|
+
const values = expression.values.join(", ")
|
|
77
|
+
if (expression.type === "bar") return `Bar chart values ${values}`
|
|
78
|
+
if (expression.type === "sparkline") {
|
|
79
|
+
return `Sparkline values ${values}`
|
|
80
|
+
}
|
|
81
|
+
return `Pie chart value ${values}`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function renderSpan(expression: DatatypeExpression, className: string) {
|
|
85
|
+
return `<span class="${escapeHtml(className)}" data-pathmx-datatype="${expression.type}" role="img" aria-label="${escapeHtml(describeExpression(expression))}">${expression.canonical}</span>`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function parseDatatypeExpression(
|
|
89
|
+
input: string,
|
|
90
|
+
): DatatypeExpression | null {
|
|
91
|
+
const match = input.match(/^\{([blp]):([^{}\n]+)\}$/)
|
|
92
|
+
if (!match) return null
|
|
93
|
+
|
|
94
|
+
const code = match[1] as DatatypeExpression["code"]
|
|
95
|
+
const rawValues = match[2]!.split(",").map((value) => value.trim())
|
|
96
|
+
if (!rawValues.length || rawValues.some((value) => !value)) return null
|
|
97
|
+
if (code === "p" && rawValues.length !== 1) return null
|
|
98
|
+
if (code !== "p" && rawValues.length > 20) return null
|
|
99
|
+
|
|
100
|
+
const values = rawValues.map((value) =>
|
|
101
|
+
/^\d+$/.test(value) ? Number(value) : Number.NaN,
|
|
102
|
+
)
|
|
103
|
+
if (
|
|
104
|
+
values.some((value) => !Number.isInteger(value) || value < 0 || value > 100)
|
|
105
|
+
) {
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
type: CHART_TYPE_BY_CODE[code],
|
|
111
|
+
code,
|
|
112
|
+
values,
|
|
113
|
+
canonical: `{${code}:${values.join(",")}}`,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function transformText(text: string, className: string) {
|
|
118
|
+
// Runtime text chunks are already serialized for HTML insertion.
|
|
119
|
+
// Preserve their entities verbatim and escape only markup we introduce.
|
|
120
|
+
return text.replace(EXPRESSION, (match) => {
|
|
121
|
+
const expression = parseDatatypeExpression(match)
|
|
122
|
+
return expression ? renderSpan(expression, className) : match
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function transformDatatypeHtml(
|
|
127
|
+
html: string,
|
|
128
|
+
rewriteHtml: HtmlStringRewrite,
|
|
129
|
+
options: Pick<DatatypePluginOptions, "className"> = {},
|
|
130
|
+
) {
|
|
131
|
+
if (!EXPRESSION_HINT.test(html)) return html
|
|
132
|
+
const className = normalizeClassName(options.className)
|
|
133
|
+
let protectedDepth = 0
|
|
134
|
+
let text = ""
|
|
135
|
+
const rules: HtmlRewriteRule[] = []
|
|
136
|
+
|
|
137
|
+
for (const tag of PROTECTED_ELEMENTS) {
|
|
138
|
+
rules.push([
|
|
139
|
+
tag,
|
|
140
|
+
{
|
|
141
|
+
element(element) {
|
|
142
|
+
protectedDepth++
|
|
143
|
+
element.onEndTag(() => {
|
|
144
|
+
protectedDepth--
|
|
145
|
+
})
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
])
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
rules.push([
|
|
152
|
+
HTML_ROOT,
|
|
153
|
+
{
|
|
154
|
+
text(chunk) {
|
|
155
|
+
if (protectedDepth) return
|
|
156
|
+
text += chunk.text
|
|
157
|
+
chunk.remove()
|
|
158
|
+
if (!chunk.lastInTextNode) return
|
|
159
|
+
chunk.after(transformText(text, className), { html: true })
|
|
160
|
+
text = ""
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
])
|
|
164
|
+
|
|
165
|
+
const output = rewriteHtml(
|
|
166
|
+
`${HTML_ROOT_OPEN}${html}${HTML_ROOT_CLOSE}`,
|
|
167
|
+
rules,
|
|
168
|
+
)
|
|
169
|
+
return output.slice(HTML_ROOT_OPEN.length, -HTML_ROOT_CLOSE.length)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function buildCss(
|
|
173
|
+
className: string,
|
|
174
|
+
fontHref: string,
|
|
175
|
+
licenseHref: string,
|
|
176
|
+
width: number,
|
|
177
|
+
weight: number,
|
|
178
|
+
) {
|
|
179
|
+
const selector = escapeCssClass(className)
|
|
180
|
+
return `/* Datatype font license: ${escapeCssString(licenseHref)} */
|
|
181
|
+
@font-face {
|
|
182
|
+
font-family: "PathMX Datatype";
|
|
183
|
+
src: url("${escapeCssString(fontHref)}") format("woff2-variations");
|
|
184
|
+
font-weight: 100 900;
|
|
185
|
+
font-stretch: 50% 150%;
|
|
186
|
+
font-display: swap;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
${selector} {
|
|
190
|
+
font-family: "PathMX Datatype", "IBM Plex Mono", monospace;
|
|
191
|
+
font-variant-ligatures: common-ligatures contextual discretionary-ligatures;
|
|
192
|
+
font-feature-settings: "liga" 1, "calt" 1;
|
|
193
|
+
font-weight: ${weight};
|
|
194
|
+
font-stretch: ${width}%;
|
|
195
|
+
letter-spacing: 0;
|
|
196
|
+
line-height: 1;
|
|
197
|
+
white-space: nowrap;
|
|
198
|
+
display: inline-block;
|
|
199
|
+
vertical-align: -0.08em;
|
|
200
|
+
}
|
|
201
|
+
`
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
type PublishedAssets = {
|
|
205
|
+
fontHref: string
|
|
206
|
+
licenseHref: string
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function publishAssets(app: AppContext): Promise<PublishedAssets> {
|
|
210
|
+
const [font, license] = await Promise.all([
|
|
211
|
+
app.assets.load(FONT_URL),
|
|
212
|
+
app.assets.load(LICENSE_URL),
|
|
213
|
+
])
|
|
214
|
+
const [fontHref, licenseHref] = await Promise.all([
|
|
215
|
+
app.assets.publish("datatype-font.woff2", font, "font/woff2"),
|
|
216
|
+
app.assets.publish(
|
|
217
|
+
"datatype-OFL.txt",
|
|
218
|
+
license,
|
|
219
|
+
"text/plain; charset=utf-8",
|
|
220
|
+
),
|
|
221
|
+
])
|
|
222
|
+
return { fontHref, licenseHref }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function addAssets(
|
|
226
|
+
ctx: PageContext,
|
|
227
|
+
options: Required<DatatypePluginOptions>,
|
|
228
|
+
assets: PublishedAssets,
|
|
229
|
+
) {
|
|
230
|
+
ctx.addAsset({
|
|
231
|
+
type: "stylesheet",
|
|
232
|
+
name: "datatype.css",
|
|
233
|
+
text: buildCss(
|
|
234
|
+
options.className,
|
|
235
|
+
assets.fontHref,
|
|
236
|
+
assets.licenseHref,
|
|
237
|
+
options.width,
|
|
238
|
+
options.weight,
|
|
239
|
+
),
|
|
240
|
+
})
|
|
241
|
+
if (options.preload) {
|
|
242
|
+
ctx.addAsset({
|
|
243
|
+
type: "preload",
|
|
244
|
+
href: assets.fontHref,
|
|
245
|
+
as: "font",
|
|
246
|
+
contentType: "font/woff2",
|
|
247
|
+
crossorigin: true,
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function DatatypePlugin(
|
|
253
|
+
pluginOptions: DatatypePluginOptions = {},
|
|
254
|
+
): Plugin {
|
|
255
|
+
const options: Required<DatatypePluginOptions> = {
|
|
256
|
+
autoWrap: pluginOptions.autoWrap ?? DEFAULT_OPTIONS.autoWrap,
|
|
257
|
+
className: normalizeClassName(pluginOptions.className),
|
|
258
|
+
width: clampAxis(
|
|
259
|
+
pluginOptions.width ?? DEFAULT_OPTIONS.width,
|
|
260
|
+
50,
|
|
261
|
+
150,
|
|
262
|
+
DEFAULT_OPTIONS.width,
|
|
263
|
+
),
|
|
264
|
+
weight: clampAxis(
|
|
265
|
+
pluginOptions.weight ?? DEFAULT_OPTIONS.weight,
|
|
266
|
+
100,
|
|
267
|
+
900,
|
|
268
|
+
DEFAULT_OPTIONS.weight,
|
|
269
|
+
),
|
|
270
|
+
preload: pluginOptions.preload ?? DEFAULT_OPTIONS.preload,
|
|
271
|
+
}
|
|
272
|
+
let published: Promise<PublishedAssets> | undefined
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
id: "datatype",
|
|
276
|
+
postcompile(html, _ctx, app) {
|
|
277
|
+
if (!options.autoWrap || !EXPRESSION_HINT.test(html)) return html
|
|
278
|
+
return transformDatatypeHtml(html, app.html.rewrite, options)
|
|
279
|
+
},
|
|
280
|
+
async page(html, ctx, app) {
|
|
281
|
+
if (!html.includes("data-pathmx-datatype")) return html
|
|
282
|
+
published ??= publishAssets(app)
|
|
283
|
+
addAssets(ctx, options, await published)
|
|
284
|
+
return html
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { isFenceClose, matchFenceOpen } from "../source/index.ts"
|
|
2
|
+
import type { CompileContext, Plugin } from "./types.ts"
|
|
3
|
+
|
|
4
|
+
type DefinitionItem = {
|
|
5
|
+
term: string
|
|
6
|
+
definitions: string[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type ParsedDefinitionList = {
|
|
10
|
+
items: DefinitionItem[]
|
|
11
|
+
nextIndex: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DEFINITION = /^ {0,3}:[ \t]+(.*)$/
|
|
15
|
+
const DEFINITION_CANDIDATE = /(?:^|\n) {0,3}:[ \t]+/
|
|
16
|
+
const PART = /<div data-pmx-definition-list="\d+:(term|definition)"><\/div>\n?/g
|
|
17
|
+
|
|
18
|
+
function isBlank(line: string | undefined) {
|
|
19
|
+
return !line?.trim()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function definition(line: string | undefined) {
|
|
23
|
+
return line?.match(DEFINITION)?.[1]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isTerm(line: string | undefined) {
|
|
27
|
+
if (!line || isBlank(line) || /^(?: {4}|\t)/.test(line)) return false
|
|
28
|
+
const value = line.trimStart()
|
|
29
|
+
return !(
|
|
30
|
+
definition(line) !== undefined ||
|
|
31
|
+
matchFenceOpen(line) ||
|
|
32
|
+
/^(?:#{1,6}(?:[ \t]+|$)|>|[-+*][ \t]+|\d{1,9}[.)][ \t]+)/.test(value) ||
|
|
33
|
+
/^(?:[-*_][ \t]*){3,}$/.test(value)
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function definitionIndex(lines: string[], termIndex: number) {
|
|
38
|
+
if (!isTerm(lines[termIndex])) return
|
|
39
|
+
let index = termIndex + 1
|
|
40
|
+
if (isBlank(lines[index])) index++
|
|
41
|
+
if (definition(lines[index]) === undefined) return
|
|
42
|
+
return index
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function unindentDefinitionLine(line: string) {
|
|
46
|
+
if (line.startsWith("\t")) return line.slice(1)
|
|
47
|
+
if (line.startsWith(" ")) return line.slice(4)
|
|
48
|
+
return line
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseDefinition(lines: string[], markerIndex: number) {
|
|
52
|
+
const content = [definition(lines[markerIndex]) ?? ""]
|
|
53
|
+
let index = markerIndex + 1
|
|
54
|
+
|
|
55
|
+
while (index < lines.length) {
|
|
56
|
+
if (definition(lines[index]) !== undefined) break
|
|
57
|
+
|
|
58
|
+
if (!isBlank(lines[index])) {
|
|
59
|
+
content.push(unindentDefinitionLine(lines[index]!))
|
|
60
|
+
index++
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let next = index
|
|
65
|
+
while (next < lines.length && isBlank(lines[next])) next++
|
|
66
|
+
if (definition(lines[next]) !== undefined) {
|
|
67
|
+
index = next
|
|
68
|
+
break
|
|
69
|
+
}
|
|
70
|
+
if (/^(?: {4}|\t)/.test(lines[next] ?? "")) {
|
|
71
|
+
content.push(...lines.slice(index, next).map(() => ""))
|
|
72
|
+
index = next
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { markdown: content.join("\n").trimEnd(), nextIndex: index }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseItem(lines: string[], termIndex: number) {
|
|
82
|
+
const firstDefinition = definitionIndex(lines, termIndex)
|
|
83
|
+
if (firstDefinition === undefined) return
|
|
84
|
+
|
|
85
|
+
const definitions: string[] = []
|
|
86
|
+
let index = firstDefinition
|
|
87
|
+
while (definition(lines[index]) !== undefined) {
|
|
88
|
+
const parsed = parseDefinition(lines, index)
|
|
89
|
+
definitions.push(parsed.markdown)
|
|
90
|
+
index = parsed.nextIndex
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
item: { term: lines[termIndex]!.trim(), definitions },
|
|
95
|
+
nextIndex: index,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseDefinitionList(lines: string[], start: number) {
|
|
100
|
+
const items: DefinitionItem[] = []
|
|
101
|
+
let index = start
|
|
102
|
+
|
|
103
|
+
while (true) {
|
|
104
|
+
const parsed = parseItem(lines, index)
|
|
105
|
+
if (!parsed) break
|
|
106
|
+
items.push(parsed.item)
|
|
107
|
+
index = parsed.nextIndex
|
|
108
|
+
|
|
109
|
+
let next = index
|
|
110
|
+
while (next < lines.length && isBlank(lines[next])) next++
|
|
111
|
+
if (next === index || definitionIndex(lines, next) === undefined) break
|
|
112
|
+
index = next
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!items.length) return
|
|
116
|
+
return { items, nextIndex: index } satisfies ParsedDefinitionList
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function marker(index: number, type: "start" | "term" | "definition" | "end") {
|
|
120
|
+
return `<div data-pmx-definition-list="${index}:${type}"></div>`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function serialize(list: ParsedDefinitionList, index: number) {
|
|
124
|
+
const output = [marker(index, "start")]
|
|
125
|
+
for (const item of list.items) {
|
|
126
|
+
output.push("", marker(index, "term"), "", item.term)
|
|
127
|
+
for (const value of item.definitions) {
|
|
128
|
+
output.push("", marker(index, "definition"), "", value)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
output.push("", marker(index, "end"))
|
|
132
|
+
return output.join("\n")
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function transform(markdown: string) {
|
|
136
|
+
const lines = markdown.split(/\r?\n/)
|
|
137
|
+
const output: string[] = []
|
|
138
|
+
let listCount = 0
|
|
139
|
+
let fence: string | undefined
|
|
140
|
+
|
|
141
|
+
for (let index = 0; index < lines.length;) {
|
|
142
|
+
const line = lines[index] ?? ""
|
|
143
|
+
if (fence) {
|
|
144
|
+
output.push(line)
|
|
145
|
+
if (isFenceClose(line, fence)) fence = undefined
|
|
146
|
+
index++
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const open = matchFenceOpen(line)
|
|
151
|
+
if (open) {
|
|
152
|
+
output.push(line)
|
|
153
|
+
fence = open.marker
|
|
154
|
+
index++
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const list = parseDefinitionList(lines, index)
|
|
159
|
+
if (!list) {
|
|
160
|
+
output.push(line)
|
|
161
|
+
index++
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
output.push(serialize(list, listCount++))
|
|
166
|
+
index = list.nextIndex
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { markdown: output.join("\n"), listCount }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function unwrapParagraph(html: string) {
|
|
173
|
+
const value = html.trim()
|
|
174
|
+
if (!value.startsWith("<p>") || !value.endsWith("</p>")) return value
|
|
175
|
+
const inner = value.slice(3, -4)
|
|
176
|
+
return inner.includes("</p>") ? value : inner
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function renderList(body: string) {
|
|
180
|
+
const parts: { type: "term" | "definition"; html: string }[] = []
|
|
181
|
+
let type: "term" | "definition" | undefined
|
|
182
|
+
let cursor = 0
|
|
183
|
+
|
|
184
|
+
for (const match of body.matchAll(PART)) {
|
|
185
|
+
if (type) {
|
|
186
|
+
parts.push({ type, html: body.slice(cursor, match.index) })
|
|
187
|
+
}
|
|
188
|
+
type = match[1] as "term" | "definition"
|
|
189
|
+
cursor = match.index! + match[0].length
|
|
190
|
+
}
|
|
191
|
+
if (type) parts.push({ type, html: body.slice(cursor) })
|
|
192
|
+
|
|
193
|
+
const content = parts
|
|
194
|
+
.map((part) => {
|
|
195
|
+
const tag = part.type === "term" ? "dt" : "dd"
|
|
196
|
+
return `<${tag}>${unwrapParagraph(part.html)}</${tag}>`
|
|
197
|
+
})
|
|
198
|
+
.join("\n")
|
|
199
|
+
return `<dl>\n${content}\n</dl>`
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function restore(html: string, listCount: number) {
|
|
203
|
+
for (let index = 0; index < listCount; index++) {
|
|
204
|
+
const start = marker(index, "start")
|
|
205
|
+
const end = marker(index, "end")
|
|
206
|
+
const from = html.indexOf(start)
|
|
207
|
+
const to = html.indexOf(end, from + start.length)
|
|
208
|
+
if (from === -1 || to === -1) continue
|
|
209
|
+
const body = html.slice(from + start.length, to)
|
|
210
|
+
html = html.slice(0, from) + renderList(body) + html.slice(to + end.length)
|
|
211
|
+
}
|
|
212
|
+
return html
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Markdown Extra / GitLab-style `Term` + `: definition` lists. */
|
|
216
|
+
export function DefinitionListPlugin(): Plugin {
|
|
217
|
+
const listCountByContext = new WeakMap<CompileContext, number>()
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
id: "definition-list",
|
|
221
|
+
precompile(markdown, ctx) {
|
|
222
|
+
if (!DEFINITION_CANDIDATE.test(markdown)) {
|
|
223
|
+
listCountByContext.set(ctx, 0)
|
|
224
|
+
return markdown
|
|
225
|
+
}
|
|
226
|
+
const parsed = transform(markdown)
|
|
227
|
+
listCountByContext.set(ctx, parsed.listCount)
|
|
228
|
+
return parsed.markdown
|
|
229
|
+
},
|
|
230
|
+
postcompile(html, ctx) {
|
|
231
|
+
const listCount = listCountByContext.get(ctx) ?? 0
|
|
232
|
+
listCountByContext.delete(ctx)
|
|
233
|
+
return listCount ? restore(html, listCount) : html
|
|
234
|
+
},
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function escapeRegex(value: string) {
|
|
2
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/** Match one authored directive reference definition, including its newline. */
|
|
6
|
+
export function directiveDefinition(name: string, href: string) {
|
|
7
|
+
const directive = escapeRegex(name)
|
|
8
|
+
const dest = escapeRegex(href)
|
|
9
|
+
return new RegExp(
|
|
10
|
+
`^[ \\t]{0,3}\\[@${directive}[^\\]]*\\]:[ \\t]+(?:<${dest}>|${dest})(?:[ \\t]+(?:"[^"]*"|'[^']*'|\\([^)]*\\)))?[ \\t]*\\n?`,
|
|
11
|
+
"gm",
|
|
12
|
+
)
|
|
13
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
type ExternalLinkPolicy = "new-tab" | "parent" | "frame"
|
|
2
|
+
|
|
3
|
+
const descriptionId = "pmx-new-tab-description"
|
|
4
|
+
const defaultTargetAttribute = "data-pmx-default-target"
|
|
5
|
+
|
|
6
|
+
function addToken(value: string, token: string) {
|
|
7
|
+
const tokens = value.split(/\s+/).filter(Boolean)
|
|
8
|
+
if (!tokens.includes(token)) tokens.push(token)
|
|
9
|
+
return tokens.join(" ")
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function removeToken(value: string, token: string) {
|
|
13
|
+
return value
|
|
14
|
+
.split(/\s+/)
|
|
15
|
+
.filter((value) => value && value !== token)
|
|
16
|
+
.join(" ")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function markNewTab(link: HTMLAnchorElement) {
|
|
20
|
+
link.classList.add("pmx-link-new-tab")
|
|
21
|
+
link.rel = addToken(link.rel, "noopener")
|
|
22
|
+
link.setAttribute(
|
|
23
|
+
"aria-describedby",
|
|
24
|
+
addToken(link.getAttribute("aria-describedby") ?? "", descriptionId),
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function unmarkNewTab(link: HTMLAnchorElement) {
|
|
29
|
+
link.classList.remove("pmx-link-new-tab")
|
|
30
|
+
const describedBy = removeToken(
|
|
31
|
+
link.getAttribute("aria-describedby") ?? "",
|
|
32
|
+
descriptionId,
|
|
33
|
+
)
|
|
34
|
+
if (describedBy) link.setAttribute("aria-describedby", describedBy)
|
|
35
|
+
else link.removeAttribute("aria-describedby")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (window.self !== window.top) {
|
|
39
|
+
const root = document.documentElement
|
|
40
|
+
const basePath = root.dataset.pmxBase || "/"
|
|
41
|
+
const configured = root.dataset.pmxEmbeddingExternalLinks
|
|
42
|
+
const policy: ExternalLinkPolicy =
|
|
43
|
+
configured === "parent" || configured === "frame" ? configured : "new-tab"
|
|
44
|
+
|
|
45
|
+
const insidePublication = (target: URL) =>
|
|
46
|
+
target.origin === location.origin &&
|
|
47
|
+
(basePath === "/" ||
|
|
48
|
+
target.pathname === basePath ||
|
|
49
|
+
target.pathname.startsWith(`${basePath}/`))
|
|
50
|
+
|
|
51
|
+
const prepareExternalLinks = () => {
|
|
52
|
+
root.setAttribute("data-pmx-embedded", "")
|
|
53
|
+
for (const link of document.querySelectorAll<HTMLAnchorElement>(
|
|
54
|
+
"a[href]",
|
|
55
|
+
)) {
|
|
56
|
+
const target = new URL(link.href, location.href)
|
|
57
|
+
if (
|
|
58
|
+
(target.protocol !== "http:" && target.protocol !== "https:") ||
|
|
59
|
+
link.hasAttribute("download")
|
|
60
|
+
) {
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
const defaulted = link.hasAttribute(defaultTargetAttribute)
|
|
64
|
+
if (insidePublication(target)) {
|
|
65
|
+
if (defaulted) {
|
|
66
|
+
link.removeAttribute("target")
|
|
67
|
+
link.removeAttribute(defaultTargetAttribute)
|
|
68
|
+
unmarkNewTab(link)
|
|
69
|
+
}
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
if (link.hasAttribute("target") && !defaulted) continue
|
|
73
|
+
if (policy === "frame") {
|
|
74
|
+
link.target = "_self"
|
|
75
|
+
link.setAttribute(defaultTargetAttribute, policy)
|
|
76
|
+
unmarkNewTab(link)
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
link.target = policy === "parent" ? "_parent" : "_blank"
|
|
80
|
+
link.setAttribute(defaultTargetAttribute, policy)
|
|
81
|
+
if (policy === "new-tab") markNewTab(link)
|
|
82
|
+
else unmarkNewTab(link)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
prepareExternalLinks()
|
|
87
|
+
document.addEventListener("pmx:render", prepareExternalLinks)
|
|
88
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Plugin } from "../types.ts"
|
|
2
|
+
|
|
3
|
+
const clientEntry = new URL("./client.ts", import.meta.url)
|
|
4
|
+
|
|
5
|
+
export type EmbeddingExternalLinks = "new-tab" | "parent" | "frame"
|
|
6
|
+
|
|
7
|
+
export type EmbeddingPluginOptions = Readonly<{
|
|
8
|
+
/** How links outside the mounted PathMX publication behave inside an iframe. */
|
|
9
|
+
externalLinks?: EmbeddingExternalLinks
|
|
10
|
+
}>
|
|
11
|
+
|
|
12
|
+
const externalLinkPolicies = new Set<EmbeddingExternalLinks>([
|
|
13
|
+
"new-tab",
|
|
14
|
+
"parent",
|
|
15
|
+
"frame",
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
/** Make a PathMX publication behave predictably when framed beside another app. */
|
|
19
|
+
export function EmbeddingPlugin(options: EmbeddingPluginOptions = {}): Plugin {
|
|
20
|
+
const externalLinks = options.externalLinks ?? "new-tab"
|
|
21
|
+
if (!externalLinkPolicies.has(externalLinks)) {
|
|
22
|
+
throw new Error(`Invalid embedded external-link policy: ${externalLinks}`)
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
id: "embedding",
|
|
26
|
+
name: "EmbeddingPlugin",
|
|
27
|
+
async documentAssets(ctx) {
|
|
28
|
+
await ctx.addBrowserBundle(clientEntry, "embedding")
|
|
29
|
+
},
|
|
30
|
+
streamSelectors() {
|
|
31
|
+
return {
|
|
32
|
+
html: {
|
|
33
|
+
element(element) {
|
|
34
|
+
element.setAttribute(
|
|
35
|
+
"data-pmx-embedding-external-links",
|
|
36
|
+
externalLinks,
|
|
37
|
+
)
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
}
|