@pathmx/core 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/LICENSE.md +172 -0
  2. package/README.md +111 -0
  3. package/actions/actions.ts +307 -0
  4. package/actions/draft.ts +105 -0
  5. package/actions/transaction.ts +164 -0
  6. package/assets.ts +149 -0
  7. package/authority/access.ts +820 -0
  8. package/authority/actors.ts +271 -0
  9. package/authority/agents.ts +95 -0
  10. package/authority/credentials.ts +144 -0
  11. package/authority/model.ts +71 -0
  12. package/authority/sharing.ts +89 -0
  13. package/browser-bundle.ts +61 -0
  14. package/canonical.ts +41 -0
  15. package/cli/dev.ts +134 -0
  16. package/cli/export.ts +74 -0
  17. package/cli/index.ts +52 -0
  18. package/cli/lint.ts +164 -0
  19. package/cli/mv.ts +30 -0
  20. package/cli/perms.ts +182 -0
  21. package/cli/plugin-packages.ts +159 -0
  22. package/cli/plugins.ts +95 -0
  23. package/cli/query.ts +225 -0
  24. package/cli/readme.md +95 -0
  25. package/cli/release.ts +75 -0
  26. package/cli/rm.ts +22 -0
  27. package/cli/serve.ts +100 -0
  28. package/cli/tree.ts +131 -0
  29. package/cli/update.ts +78 -0
  30. package/cli/utils.ts +53 -0
  31. package/core.ts +96 -0
  32. package/database.ts +189 -0
  33. package/dependencies.ts +60 -0
  34. package/dist/pathmx.js +1224 -0
  35. package/document.ts +194 -0
  36. package/environment.ts +110 -0
  37. package/environments/bun-plugins.ts +68 -0
  38. package/environments/bun-publish.ts +194 -0
  39. package/environments/bun.ts +223 -0
  40. package/globals.d.ts +14 -0
  41. package/graph.ts +153 -0
  42. package/host/compiler.ts +314 -0
  43. package/host/create.ts +66 -0
  44. package/host/engine.ts +630 -0
  45. package/host/plugin-host.ts +487 -0
  46. package/host/render.ts +102 -0
  47. package/host/response.ts +20 -0
  48. package/host/shell.html +15 -0
  49. package/host/source-sync.ts +254 -0
  50. package/html-attributes.ts +15 -0
  51. package/html.ts +48 -0
  52. package/icons/LICENSE +43 -0
  53. package/icons/lucide.ts +54 -0
  54. package/index.ts +75 -0
  55. package/invalidation.ts +85 -0
  56. package/log.ts +84 -0
  57. package/ops/href.ts +87 -0
  58. package/ops/index.ts +9 -0
  59. package/ops/move-source.ts +114 -0
  60. package/ops/plan.ts +47 -0
  61. package/ops/remove-source.ts +70 -0
  62. package/package.json +46 -0
  63. package/path.ts +75 -0
  64. package/pattern.ts +314 -0
  65. package/perf.ts +88 -0
  66. package/plugins/actor-path.ts +43 -0
  67. package/plugins/actor-profile.ts +55 -0
  68. package/plugins/agent-browsing.ts +183 -0
  69. package/plugins/app-component.ts +23 -0
  70. package/plugins/block-layout/index.ts +78 -0
  71. package/plugins/block-layout/theme.css +90 -0
  72. package/plugins/callout/index.ts +236 -0
  73. package/plugins/callout/theme.css +138 -0
  74. package/plugins/canonical-links.ts +144 -0
  75. package/plugins/code-highlight/client.ts +89 -0
  76. package/plugins/code-highlight/index.ts +147 -0
  77. package/plugins/code-highlight/theme.css +89 -0
  78. package/plugins/context.ts +131 -0
  79. package/plugins/datatype/Datatype[wdth,wght].woff2 +0 -0
  80. package/plugins/datatype/OFL.txt +96 -0
  81. package/plugins/datatype/index.ts +287 -0
  82. package/plugins/definition-list.ts +236 -0
  83. package/plugins/directive-definitions.ts +13 -0
  84. package/plugins/embedding/client.ts +88 -0
  85. package/plugins/embedding/index.ts +43 -0
  86. package/plugins/image/index.ts +68 -0
  87. package/plugins/image/theme.css +43 -0
  88. package/plugins/include.ts +119 -0
  89. package/plugins/index.ts +91 -0
  90. package/plugins/input/authoring.ts +236 -0
  91. package/plugins/input/config.ts +85 -0
  92. package/plugins/input/index.ts +359 -0
  93. package/plugins/input/model.ts +173 -0
  94. package/plugins/input/state.ts +272 -0
  95. package/plugins/layout.ts +313 -0
  96. package/plugins/literate/client.ts +208 -0
  97. package/plugins/literate/index.ts +742 -0
  98. package/plugins/math/index.ts +140 -0
  99. package/plugins/meta.ts +42 -0
  100. package/plugins/navigation/index.ts +158 -0
  101. package/plugins/navigation/model.ts +364 -0
  102. package/plugins/preset.ts +65 -0
  103. package/plugins/query/index.ts +322 -0
  104. package/plugins/rss/index.ts +46 -0
  105. package/plugins/runtime/actions.ts +62 -0
  106. package/plugins/runtime/client.ts +8 -0
  107. package/plugins/runtime/document.ts +506 -0
  108. package/plugins/runtime/error.ts +154 -0
  109. package/plugins/runtime/heading-links.ts +31 -0
  110. package/plugins/runtime/index.ts +104 -0
  111. package/plugins/runtime/live.ts +51 -0
  112. package/plugins/runtime/navigation.ts +113 -0
  113. package/plugins/runtime/pending.ts +53 -0
  114. package/plugins/runtime/publication.ts +19 -0
  115. package/plugins/runtime/start.ts +177 -0
  116. package/plugins/runtime/static-client.ts +6 -0
  117. package/plugins/runtime/static-live.ts +188 -0
  118. package/plugins/runtime/view-transition.ts +72 -0
  119. package/plugins/runtime/viewport.ts +110 -0
  120. package/plugins/schema.ts +24 -0
  121. package/plugins/scripts.ts +140 -0
  122. package/plugins/source-preview/client.ts +256 -0
  123. package/plugins/source-preview/index.ts +132 -0
  124. package/plugins/source-preview/model.ts +11 -0
  125. package/plugins/source-preview/preview.css +107 -0
  126. package/plugins/styles.ts +145 -0
  127. package/plugins/theme/components.ts +10 -0
  128. package/plugins/theme/foundation.css +358 -0
  129. package/plugins/theme/index.ts +333 -0
  130. package/plugins/theme/prose.css +284 -0
  131. package/plugins/theme/scheme.js +63 -0
  132. package/plugins/theme/toggle.css +44 -0
  133. package/plugins/theme/view-transition.css +68 -0
  134. package/plugins/toc/client.ts +146 -0
  135. package/plugins/toc/index.ts +232 -0
  136. package/plugins/toc/theme.css +172 -0
  137. package/plugins/types.ts +538 -0
  138. package/problem.ts +41 -0
  139. package/project.ts +6 -0
  140. package/publish/build.ts +402 -0
  141. package/publish/concurrent.ts +32 -0
  142. package/publish/identity.ts +63 -0
  143. package/publish/index.ts +11 -0
  144. package/publish/location.ts +167 -0
  145. package/publish/model.ts +58 -0
  146. package/publish/plan.ts +300 -0
  147. package/publish/validate.ts +150 -0
  148. package/query/candidate-index.ts +101 -0
  149. package/query/dependency-index.ts +99 -0
  150. package/query/fields.ts +216 -0
  151. package/query/invalidation.ts +48 -0
  152. package/query/kernel.ts +776 -0
  153. package/query/model.ts +79 -0
  154. package/repository.ts +374 -0
  155. package/runtime-profile.ts +34 -0
  156. package/schema/model.ts +92 -0
  157. package/schema/registry.ts +642 -0
  158. package/schema/standard.ts +226 -0
  159. package/server/context.ts +94 -0
  160. package/server/css-imports.ts +66 -0
  161. package/server/http.ts +333 -0
  162. package/server/page.ts +21 -0
  163. package/server/protocols.ts +12 -0
  164. package/server/representations.ts +66 -0
  165. package/server/router.ts +131 -0
  166. package/server/routes.ts +202 -0
  167. package/server/serve.ts +5 -0
  168. package/server/watch.ts +120 -0
  169. package/source/blocks.ts +71 -0
  170. package/source/format.ts +164 -0
  171. package/source/index.ts +67 -0
  172. package/source/interpolate.ts +63 -0
  173. package/source/markdown/blocks.ts +46 -0
  174. package/source/markdown/comments.ts +32 -0
  175. package/source/markdown/fences.ts +67 -0
  176. package/source/markdown/headings.ts +88 -0
  177. package/source/markdown/index.ts +34 -0
  178. package/source/markdown/inline-code.ts +6 -0
  179. package/source/markdown/links.ts +326 -0
  180. package/source/markdown/lists.ts +223 -0
  181. package/source/markdown/prose.ts +74 -0
  182. package/source/markdown/scan.ts +79 -0
  183. package/source/markdown/text.ts +82 -0
  184. package/source/meta.ts +87 -0
  185. package/source/source.ts +121 -0
  186. package/source/topmatter.ts +57 -0
  187. package/source/utils.ts +21 -0
  188. package/source-diagnostic.ts +63 -0
  189. package/text-file.ts +26 -0
  190. package/util.ts +35 -0
  191. package/view/components.ts +499 -0
  192. package/view/model.ts +91 -0
  193. package/view/projection.ts +162 -0
  194. package/view/session.ts +146 -0
  195. package/view/syntax.ts +372 -0
@@ -0,0 +1,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
+ }