@suzumiyaaoba/mdxr 0.1.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 (236) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/dist/cli.d.mts +1 -0
  4. package/dist/cli.mjs +2902 -0
  5. package/dist/components.d.mts +791 -0
  6. package/dist/components.mjs +2 -0
  7. package/dist/config-SI9IyFiC.mjs +184 -0
  8. package/dist/doc-context-CEqzMYKv.d.mts +568 -0
  9. package/dist/index.d.mts +20 -0
  10. package/dist/index.mjs +4 -0
  11. package/dist/ui-GKFD0mx5.mjs +15986 -0
  12. package/package.json +131 -0
  13. package/skill/SKILL.md +49 -0
  14. package/skill/references/components/charts.md +172 -0
  15. package/skill/references/components/document.md +98 -0
  16. package/skill/references/components/forms.md +35 -0
  17. package/skill/references/components/investigation.md +198 -0
  18. package/skill/references/components/layout.md +68 -0
  19. package/skill/references/components/output.md +158 -0
  20. package/skill/references/components/planning.md +155 -0
  21. package/skill/references/components/reports.md +252 -0
  22. package/skill/references/components/shadcn.md +19 -0
  23. package/skill/references/components.md +186 -0
  24. package/skill/references/extending.md +48 -0
  25. package/src/ask-sheet.ts +40 -0
  26. package/src/assets/css.ts +392 -0
  27. package/src/assets/scripts.ts +78 -0
  28. package/src/catalog.ts +287 -0
  29. package/src/cli.ts +178 -0
  30. package/src/client/doc-events.ts +1108 -0
  31. package/src/client/entry.ts +27 -0
  32. package/src/client-js.ts +47 -0
  33. package/src/component-map.ts +47 -0
  34. package/src/components/ui/accordion.tsx +77 -0
  35. package/src/components/ui/alert-dialog.tsx +185 -0
  36. package/src/components/ui/alert.tsx +76 -0
  37. package/src/components/ui/aspect-ratio.tsx +22 -0
  38. package/src/components/ui/attachment.tsx +208 -0
  39. package/src/components/ui/avatar.tsx +106 -0
  40. package/src/components/ui/badge.tsx +52 -0
  41. package/src/components/ui/breadcrumb.tsx +121 -0
  42. package/src/components/ui/bubble.tsx +128 -0
  43. package/src/components/ui/button-group.tsx +88 -0
  44. package/src/components/ui/button.tsx +58 -0
  45. package/src/components/ui/calendar.tsx +226 -0
  46. package/src/components/ui/card.tsx +102 -0
  47. package/src/components/ui/carousel.tsx +246 -0
  48. package/src/components/ui/chart.tsx +379 -0
  49. package/src/components/ui/checkbox.tsx +27 -0
  50. package/src/components/ui/collapsible.tsx +19 -0
  51. package/src/components/ui/combobox.tsx +298 -0
  52. package/src/components/ui/command.tsx +193 -0
  53. package/src/components/ui/context-menu.tsx +271 -0
  54. package/src/components/ui/dialog.tsx +159 -0
  55. package/src/components/ui/direction.tsx +4 -0
  56. package/src/components/ui/drawer.tsx +227 -0
  57. package/src/components/ui/dropdown-menu.tsx +269 -0
  58. package/src/components/ui/empty.tsx +104 -0
  59. package/src/components/ui/field.tsx +237 -0
  60. package/src/components/ui/fieldset.tsx +32 -0
  61. package/src/components/ui/frame.tsx +87 -0
  62. package/src/components/ui/hover-card.tsx +50 -0
  63. package/src/components/ui/input-group.tsx +159 -0
  64. package/src/components/ui/input-otp.tsx +83 -0
  65. package/src/components/ui/input.tsx +19 -0
  66. package/src/components/ui/item.tsx +202 -0
  67. package/src/components/ui/kbd.tsx +26 -0
  68. package/src/components/ui/label.tsx +19 -0
  69. package/src/components/ui/marker.tsx +71 -0
  70. package/src/components/ui/menubar.tsx +284 -0
  71. package/src/components/ui/message-scroller.tsx +128 -0
  72. package/src/components/ui/message.tsx +91 -0
  73. package/src/components/ui/meter.tsx +80 -0
  74. package/src/components/ui/native-select.tsx +64 -0
  75. package/src/components/ui/navigation-menu.tsx +170 -0
  76. package/src/components/ui/pagination.tsx +133 -0
  77. package/src/components/ui/popover.tsx +87 -0
  78. package/src/components/ui/progress.tsx +82 -0
  79. package/src/components/ui/questionnaire.tsx +328 -0
  80. package/src/components/ui/radio-group.tsx +35 -0
  81. package/src/components/ui/resizable.tsx +49 -0
  82. package/src/components/ui/scroll-area.tsx +50 -0
  83. package/src/components/ui/select.tsx +201 -0
  84. package/src/components/ui/separator.tsx +22 -0
  85. package/src/components/ui/sheet.tsx +135 -0
  86. package/src/components/ui/sidebar.tsx +730 -0
  87. package/src/components/ui/skeleton.tsx +13 -0
  88. package/src/components/ui/slider.tsx +51 -0
  89. package/src/components/ui/spinner.tsx +16 -0
  90. package/src/components/ui/switch.tsx +31 -0
  91. package/src/components/ui/table.tsx +113 -0
  92. package/src/components/ui/tabs.tsx +82 -0
  93. package/src/components/ui/textarea.tsx +17 -0
  94. package/src/components/ui/toast.tsx +229 -0
  95. package/src/components/ui/toggle-group.tsx +87 -0
  96. package/src/components/ui/toggle.tsx +43 -0
  97. package/src/components/ui/tooltip.tsx +65 -0
  98. package/src/components.ts +78 -0
  99. package/src/config.ts +56 -0
  100. package/src/define.ts +122 -0
  101. package/src/doc-context.ts +20 -0
  102. package/src/editor.ts +94 -0
  103. package/src/format-error.ts +78 -0
  104. package/src/guards.ts +84 -0
  105. package/src/hooks/use-mobile.ts +21 -0
  106. package/src/html.ts +91 -0
  107. package/src/hydrate/export-index.ts +232 -0
  108. package/src/hydrate/import-scan.ts +169 -0
  109. package/src/hydrate/plugins.ts +118 -0
  110. package/src/hydrate/runtime-module.ts +145 -0
  111. package/src/hydrate-runtime.ts +67 -0
  112. package/src/hydrate.ts +147 -0
  113. package/src/index.ts +16 -0
  114. package/src/init.ts +57 -0
  115. package/src/langs.ts +148 -0
  116. package/src/lines.ts +53 -0
  117. package/src/load-user-module.ts +191 -0
  118. package/src/mdx.ts +248 -0
  119. package/src/paths.ts +36 -0
  120. package/src/rehype/shiki.ts +533 -0
  121. package/src/remark/alerts.ts +53 -0
  122. package/src/remark/ast.ts +96 -0
  123. package/src/remark/callouts.ts +25 -0
  124. package/src/remark/code-file.ts +85 -0
  125. package/src/remark/code-meta.ts +21 -0
  126. package/src/remark/directives.ts +170 -0
  127. package/src/remark/file-paths.ts +64 -0
  128. package/src/remark/headings.ts +131 -0
  129. package/src/remark/no-js.ts +40 -0
  130. package/src/render.ts +337 -0
  131. package/src/serve.ts +322 -0
  132. package/src/styles/globals.css +134 -0
  133. package/src/styles/shadcn.css +641 -0
  134. package/src/tailwind.ts +119 -0
  135. package/src/ui/approvals.tsx +76 -0
  136. package/src/ui/ask-question.tsx +386 -0
  137. package/src/ui/ask.tsx +206 -0
  138. package/src/ui/attrs.ts +51 -0
  139. package/src/ui/audit.tsx +139 -0
  140. package/src/ui/bar-chart.tsx +334 -0
  141. package/src/ui/benchmarks.tsx +143 -0
  142. package/src/ui/bits.tsx +537 -0
  143. package/src/ui/board.tsx +173 -0
  144. package/src/ui/bridge.tsx +207 -0
  145. package/src/ui/bumps.tsx +178 -0
  146. package/src/ui/callout.tsx +106 -0
  147. package/src/ui/changes.tsx +89 -0
  148. package/src/ui/chart-bits.tsx +52 -0
  149. package/src/ui/chart.ts +577 -0
  150. package/src/ui/checks.tsx +203 -0
  151. package/src/ui/child-index.tsx +44 -0
  152. package/src/ui/children.ts +43 -0
  153. package/src/ui/chips.tsx +49 -0
  154. package/src/ui/cmd.tsx +27 -0
  155. package/src/ui/columns.tsx +31 -0
  156. package/src/ui/comments.tsx +544 -0
  157. package/src/ui/compare.tsx +67 -0
  158. package/src/ui/decision.tsx +79 -0
  159. package/src/ui/deps.tsx +78 -0
  160. package/src/ui/details.tsx +43 -0
  161. package/src/ui/diff-parse.ts +307 -0
  162. package/src/ui/diff.tsx +458 -0
  163. package/src/ui/diffstat.tsx +58 -0
  164. package/src/ui/due.tsx +65 -0
  165. package/src/ui/effort.tsx +29 -0
  166. package/src/ui/endpoints.tsx +118 -0
  167. package/src/ui/envvars.tsx +96 -0
  168. package/src/ui/figure.tsx +37 -0
  169. package/src/ui/file-icon.ts +1020 -0
  170. package/src/ui/file-link.ts +32 -0
  171. package/src/ui/file-ref.tsx +39 -0
  172. package/src/ui/files.tsx +112 -0
  173. package/src/ui/findings.tsx +85 -0
  174. package/src/ui/flow.tsx +73 -0
  175. package/src/ui/funnel.tsx +121 -0
  176. package/src/ui/gantt.tsx +443 -0
  177. package/src/ui/gauges.tsx +135 -0
  178. package/src/ui/glossary.tsx +29 -0
  179. package/src/ui/graph-layout.ts +149 -0
  180. package/src/ui/graph-specs.tsx +102 -0
  181. package/src/ui/graph.tsx +278 -0
  182. package/src/ui/grid.tsx +119 -0
  183. package/src/ui/hypothesis.tsx +94 -0
  184. package/src/ui/icon.tsx +80 -0
  185. package/src/ui/incident.tsx +130 -0
  186. package/src/ui/index.ts +342 -0
  187. package/src/ui/ins-del.tsx +41 -0
  188. package/src/ui/json.tsx +190 -0
  189. package/src/ui/layout.ts +9 -0
  190. package/src/ui/line-chart.tsx +251 -0
  191. package/src/ui/matrix.tsx +208 -0
  192. package/src/ui/meta.tsx +73 -0
  193. package/src/ui/option.tsx +64 -0
  194. package/src/ui/owner.tsx +40 -0
  195. package/src/ui/packages.tsx +104 -0
  196. package/src/ui/pathway.tsx +96 -0
  197. package/src/ui/phase.tsx +41 -0
  198. package/src/ui/pie-chart.tsx +170 -0
  199. package/src/ui/plan.tsx +77 -0
  200. package/src/ui/pre.tsx +175 -0
  201. package/src/ui/priority.tsx +51 -0
  202. package/src/ui/props.tsx +77 -0
  203. package/src/ui/quadrant.tsx +172 -0
  204. package/src/ui/radar.tsx +200 -0
  205. package/src/ui/ref.tsx +160 -0
  206. package/src/ui/release.tsx +178 -0
  207. package/src/ui/req.tsx +42 -0
  208. package/src/ui/review.tsx +133 -0
  209. package/src/ui/risk.tsx +67 -0
  210. package/src/ui/sankey.tsx +287 -0
  211. package/src/ui/scatter.tsx +237 -0
  212. package/src/ui/schema.tsx +113 -0
  213. package/src/ui/score.tsx +105 -0
  214. package/src/ui/search.tsx +120 -0
  215. package/src/ui/series.tsx +38 -0
  216. package/src/ui/severity.tsx +69 -0
  217. package/src/ui/shadcn.tsx +216 -0
  218. package/src/ui/spark.tsx +86 -0
  219. package/src/ui/stack.tsx +57 -0
  220. package/src/ui/stats.tsx +63 -0
  221. package/src/ui/status-badge.tsx +66 -0
  222. package/src/ui/statuspage.tsx +238 -0
  223. package/src/ui/steps.tsx +74 -0
  224. package/src/ui/summary.tsx +41 -0
  225. package/src/ui/symbol-ref.tsx +73 -0
  226. package/src/ui/terminal.tsx +117 -0
  227. package/src/ui/tests.tsx +218 -0
  228. package/src/ui/timeline.tsx +63 -0
  229. package/src/ui/toc.tsx +56 -0
  230. package/src/ui/tones.ts +187 -0
  231. package/src/ui/trace.tsx +69 -0
  232. package/src/ui/tree.tsx +281 -0
  233. package/src/ui/treemap.tsx +128 -0
  234. package/src/ui/venn.tsx +258 -0
  235. package/src/ui/verdict.tsx +78 -0
  236. package/src/ui/waterfall.tsx +142 -0
package/dist/cli.mjs ADDED
@@ -0,0 +1,2902 @@
1
+ #!/usr/bin/env node
2
+ import { F as shadcnModules, Fl as parseDiff, Fu as fenceFilename, Gu as own, Hu as isComponent, Iu as fenceLang, Ku as urlScheme, Lu as parseLineRange, Nl as langForPath, Pu as takeUsedIcons, Ru as splitPathLines, Uu as isRecord, Wu as nonEmpty, cu as isStatus, gc as PlanHeader, jl as SKIP_LANGS, ou as STATUSES, t as builtinComponents, zu as DocContext } from "./ui-GKFD0mx5.mjs";
3
+ import { a as loadUserModule, c as srcDir, i as importBundledCode, n as loadConfig, o as resolveModuleEntry, r as SHARED_PACKAGES, s as pkgRoot } from "./config-SI9IyFiC.mjs";
4
+ import module from "node:module";
5
+ import fsp, { access, readFile, readdir, writeFile } from "node:fs/promises";
6
+ import path from "node:path";
7
+ import { cac } from "cac";
8
+ import * as valibot from "valibot";
9
+ import { Fragment, createElement } from "react";
10
+ import { icons } from "@iconify-json/lucide";
11
+ import { icons as icons$1 } from "@iconify-json/vscode-icons";
12
+ import fs, { existsSync, readFileSync } from "node:fs";
13
+ import { build } from "esbuild";
14
+ import os from "node:os";
15
+ import { compile } from "@mdx-js/mdx";
16
+ import { renderToStaticMarkup, renderToString } from "react-dom/server";
17
+ import rehypeKatex from "rehype-katex";
18
+ import remarkDirective from "remark-directive";
19
+ import remarkFrontmatter from "remark-frontmatter";
20
+ import remarkGfm from "remark-gfm";
21
+ import remarkMath from "remark-math";
22
+ import { VFile } from "vfile";
23
+ import { matter } from "vfile-matter";
24
+ import { transformerMetaHighlight, transformerMetaWordHighlight, transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight, transformerNotationWordHighlight, transformerRemoveNotationEscape } from "@shikijs/transformers";
25
+ import { createHighlighter } from "shiki";
26
+ import { visit } from "unist-util-visit";
27
+ import { visitParents } from "unist-util-visit-parents";
28
+ import { compile as compile$1, optimize } from "@tailwindcss/node";
29
+ import { Scanner } from "@tailwindcss/oxide";
30
+ import { once } from "node:events";
31
+ import http from "node:http";
32
+ //#region src/catalog.ts
33
+ /** Structural view of a valibot schema — enough for catalog introspection. */
34
+ const describeSchema = (schema) => {
35
+ if (!isRecord(schema)) return "unknown";
36
+ if (schema.type === "array") return `${describeSchema(schema.item)}[]`;
37
+ if (schema.type === "literal") return JSON.stringify(schema.literal);
38
+ if (schema.type === "picklist" && Array.isArray(schema.options)) return schema.options.map((o) => JSON.stringify(o)).join(" | ");
39
+ if (schema.type === "union" && Array.isArray(schema.options)) return schema.options.map(describeSchema).join(" | ");
40
+ if (Array.isArray(schema.pipe)) {
41
+ const constrained = schema.pipe.find((s) => isRecord(s) && (s.type === "picklist" || s.type === "union" || s.type === "literal"));
42
+ if (constrained !== void 0) return describeSchema(constrained);
43
+ }
44
+ return typeof schema.type === "string" ? schema.type : "unknown";
45
+ };
46
+ const WRAPPER_TYPES = /* @__PURE__ */ new Set([
47
+ "nullable",
48
+ "nullish",
49
+ "optional"
50
+ ]);
51
+ const unwrapProp = (raw) => {
52
+ let s = isRecord(raw) ? raw : {};
53
+ let required = true;
54
+ let def;
55
+ while (typeof s.type === "string" && WRAPPER_TYPES.has(s.type)) {
56
+ required = false;
57
+ def ??= s.default;
58
+ s = isRecord(s.wrapped) ? s.wrapped : {};
59
+ }
60
+ return {
61
+ def,
62
+ required,
63
+ s
64
+ };
65
+ };
66
+ const propsOf = (schema) => {
67
+ if (!isRecord(schema) || !isRecord(schema.entries)) return {};
68
+ const out = {};
69
+ for (const [key, raw] of Object.entries(schema.entries)) {
70
+ const { def, required, s } = unwrapProp(raw);
71
+ Object.defineProperty(out, key, {
72
+ configurable: true,
73
+ enumerable: true,
74
+ value: {
75
+ required,
76
+ type: describeSchema(s),
77
+ ...def === void 0 ? {} : { default: def }
78
+ },
79
+ writable: true
80
+ });
81
+ }
82
+ return out;
83
+ };
84
+ const entryOf = (name, comp, source) => ({
85
+ description: comp.__mdxr?.description,
86
+ name,
87
+ props: propsOf(comp.__mdxr?.schema),
88
+ source
89
+ });
90
+ const catalogEntries = (project = {}) => {
91
+ const entries = [];
92
+ for (const [name, comp] of Object.entries(builtinComponents)) {
93
+ if (!/^[A-Z]/u.test(name)) continue;
94
+ const overridden = Object.hasOwn(project, name);
95
+ const shown = overridden ? project[name] ?? comp : comp;
96
+ entries.push(entryOf(name, shown, overridden ? "project" : "builtin"));
97
+ }
98
+ for (const [name, comp] of Object.entries(project)) if (!Object.hasOwn(builtinComponents, name)) entries.push(entryOf(name, comp, "project"));
99
+ return entries;
100
+ };
101
+ /** Conventions the agent can use without JSX (kept in sync with remark plugins). */
102
+ const CONVENTIONS = [
103
+ {
104
+ result: "Callout (with kind)",
105
+ syntax: ":::note | :::tip | :::warning | :::danger | :::decision | :::goal | :::nongoal | :::question | :::answer"
106
+ },
107
+ {
108
+ result: "Phase",
109
+ syntax: ":::phase{title=\"...\" status=\"doing\"}"
110
+ },
111
+ {
112
+ result: "Steps container",
113
+ syntax: ":::steps"
114
+ },
115
+ {
116
+ result: "Flow (numbered call/execution chain)",
117
+ syntax: ":::flow{title=\"...\"}"
118
+ },
119
+ {
120
+ result: "Findings container / Finding block",
121
+ syntax: ":::findings · :::finding{confidence=\"inferred\" title=\"...\"}"
122
+ },
123
+ {
124
+ result: "Files container (related files)",
125
+ syntax: ":::files"
126
+ },
127
+ {
128
+ result: "Deps container (dependency edges)",
129
+ syntax: ":::deps"
130
+ },
131
+ {
132
+ result: "Tests container (test report)",
133
+ syntax: ":::tests"
134
+ },
135
+ {
136
+ result: "Endpoints container (API routes)",
137
+ syntax: ":::endpoints"
138
+ },
139
+ {
140
+ result: "Board container (kanban)",
141
+ syntax: ":::board"
142
+ },
143
+ {
144
+ result: "Graph container (static node/edge SVG, dagre layout)",
145
+ syntax: ":::graph{title=\"...\" direction=\"right\"}"
146
+ },
147
+ {
148
+ result: "Waterfall container (timing bars)",
149
+ syntax: ":::waterfall"
150
+ },
151
+ {
152
+ result: "Gantt container (date-based schedule)",
153
+ syntax: ":::gantt{title=\"...\"}"
154
+ },
155
+ {
156
+ result: "Matrix container (comparison grid)",
157
+ syntax: ":::matrix"
158
+ },
159
+ {
160
+ result: "Timeline",
161
+ syntax: ":::timeline{title=\"...\"}"
162
+ },
163
+ {
164
+ result: "Review container (code review report)",
165
+ syntax: ":::review{title=\"...\" verdict=\"changes\"}"
166
+ },
167
+ {
168
+ result: "Checks container (CI/verification status)",
169
+ syntax: ":::checks"
170
+ },
171
+ {
172
+ result: "Audit container (vulnerability inventory)",
173
+ syntax: ":::audit{tool=\"npm audit\"}"
174
+ },
175
+ {
176
+ result: "Bumps container (dependency upgrades)",
177
+ syntax: ":::bumps"
178
+ },
179
+ {
180
+ result: "Packages container (dependency inventory)",
181
+ syntax: ":::packages"
182
+ },
183
+ {
184
+ result: "Gauges container (percent-bar rows: coverage, scores)",
185
+ syntax: ":::gauges"
186
+ },
187
+ {
188
+ result: "Benchmarks container (before/after comparisons)",
189
+ syntax: ":::benchmarks{better=\"lower\"}"
190
+ },
191
+ {
192
+ result: "Schema container (DB table definitions)",
193
+ syntax: ":::schema"
194
+ },
195
+ {
196
+ result: "EnvVars container (environment variable docs)",
197
+ syntax: ":::envvars"
198
+ },
199
+ {
200
+ result: "StatusPage container (service health rollup)",
201
+ syntax: ":::statuspage"
202
+ },
203
+ {
204
+ result: "Uptime container (status-page day bars)",
205
+ syntax: ":::uptime{pct=\"99.9\"}"
206
+ },
207
+ {
208
+ result: "Release container (changelog block)",
209
+ syntax: ":::release{version=\"v1.0.0\"}"
210
+ },
211
+ {
212
+ result: "Pathway container (version/env stepper)",
213
+ syntax: ":::pathway{title=\"...\"}"
214
+ },
215
+ {
216
+ result: "Incident header (postmortem card)",
217
+ syntax: ":::incident{title=\"...\" severity=\"high\"}"
218
+ },
219
+ {
220
+ result: "Verdict banner (approve/pass/warn/fail conclusion)",
221
+ syntax: ":::verdict{status=\"approve\"}"
222
+ },
223
+ {
224
+ result: "chart containers (quantitative visualizations)",
225
+ syntax: ":::barchart · :::linechart · :::piechart · :::scatter · :::radar · :::funnel · :::quadrant · :::bridge · :::treemap · :::sankey · :::venn"
226
+ },
227
+ {
228
+ result: "table of contents (auto from headings)",
229
+ syntax: ":::toc"
230
+ },
231
+ {
232
+ result: "Callout (GitHub alert)",
233
+ syntax: "> [!NOTE] / [!WARNING] / ..."
234
+ },
235
+ {
236
+ result: "diagram (mermaid via CDN)",
237
+ syntax: "```mermaid fenced block"
238
+ },
239
+ {
240
+ result: "unified-diff cards (per-file headers, hunk line numbers)",
241
+ syntax: "```diff or ```patch fenced block"
242
+ },
243
+ {
244
+ result: "math (KaTeX via CDN)",
245
+ syntax: "$…$ inline / $$…$$ block"
246
+ },
247
+ {
248
+ result: "code block (syntax-highlighted) with filename header",
249
+ syntax: "```lang title=\"file.ts\""
250
+ },
251
+ {
252
+ result: "FileRef chip (editor link when the file exists)",
253
+ syntax: "`src/x.ts` inline code naming a real file"
254
+ },
255
+ {
256
+ result: "code line highlights / line numbers",
257
+ syntax: "```ts {1,3-5} · ```ts ln · /word/ in meta"
258
+ },
259
+ {
260
+ result: "in-code markers (stripped from output)",
261
+ syntax: "// [!code hl] · [!code ++] · [!code --] · [!code warning] · [!code error] · [!code focus] · [!code word:x]"
262
+ },
263
+ {
264
+ result: "embed a real file as a code block",
265
+ syntax: "<CodeFile path=\"src/x.ts\" lines=\"40-52\" />"
266
+ },
267
+ {
268
+ result: "styled markdown",
269
+ syntax: "GFM tables, task lists, strikethrough, footnotes"
270
+ },
271
+ {
272
+ result: "Iconify icon (inline SVG, lucide + vscode-icons bundled)",
273
+ syntax: "<Icon name=\"lucide:rocket\"> or class=\"icon-[lucide--rocket]\""
274
+ }
275
+ ];
276
+ const formatCatalog = (entries) => {
277
+ const lines = ["Components:", ""];
278
+ for (const e of entries) {
279
+ const props = Object.entries(e.props).map(([k, p]) => `${k}${p.required ? "" : "?"}: ${p.type}`).join(", ");
280
+ lines.push(` <${e.name}${props === "" ? "" : ` ${props}`}>`);
281
+ if (e.description !== void 0) lines.push(` ${e.description}`);
282
+ if (e.source === "project") lines.push(" (project-defined)");
283
+ lines.push("");
284
+ }
285
+ lines.push("Conventions:");
286
+ for (const c of CONVENTIONS) lines.push(` ${c.syntax} → ${c.result}`);
287
+ return lines.join("\n");
288
+ };
289
+ //#endregion
290
+ //#region src/format-error.ts
291
+ /**
292
+ * Validate the `--format` flag: omitted and "text" mean human-readable
293
+ * errors, "json" switches failure output to a machine-readable object.
294
+ */
295
+ const parseErrorFormat = (format) => {
296
+ if (format === void 0 || format === "text") return "text";
297
+ if (format === "json") return "json";
298
+ throw new Error(`invalid --format "${format}" (expected text|json)`);
299
+ };
300
+ const formatError = (err) => {
301
+ if (err instanceof Error) {
302
+ const e = err;
303
+ const loc = e.line === null || e.line === void 0 ? "" : `:${e.line}:${e.column ?? 0}`;
304
+ return `${e.file ?? ""}${loc} ${e.message}`.trim();
305
+ }
306
+ return String(err);
307
+ };
308
+ const levenshtein = (a, b) => {
309
+ const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array.from({ length: b.length }, () => 0)]);
310
+ for (let j = 1; j <= b.length; j += 1) dp[0][j] = j;
311
+ for (let i = 1; i <= a.length; i += 1) for (let j = 1; j <= b.length; j += 1) dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
312
+ return dp[a.length][b.length];
313
+ };
314
+ /**
315
+ * MDX's "Expected component `<X>`" errors get a friendlier message: the
316
+ * closest catalog name (edit distance ≤ 3) plus the full list, so the agent
317
+ * can self-correct without a `mdxr catalog` round-trip.
318
+ */
319
+ const enhanceRenderError = (err, componentNames) => {
320
+ if (!(err instanceof Error)) return err;
321
+ const name = /Expected component [`"'](?<name>\w+)[`"']/u.exec(err.message)?.groups?.name;
322
+ if (name === void 0) return err;
323
+ const names = componentNames.filter((n) => /^[A-Z]/u.test(n));
324
+ const [nearest] = names.map((n) => ({
325
+ d: levenshtein(name.toLowerCase(), n.toLowerCase()),
326
+ n
327
+ })).toSorted((x, y) => x.d - y.d);
328
+ const hint = nearest !== void 0 && nearest.d <= 3 ? ` Did you mean <${nearest.n}>?` : "";
329
+ return new Error(`Unknown component <${name}>.${hint} Available: ${names.join(", ")}. Add custom components via mdxr.config.ts.`, { cause: err });
330
+ };
331
+ //#endregion
332
+ //#region src/init.ts
333
+ const SKILL_DIRS = {
334
+ agents: {
335
+ global: ".agents/skills/mdxr",
336
+ local: ".agents/skills/mdxr"
337
+ },
338
+ claude: {
339
+ global: ".claude/skills/mdxr",
340
+ local: ".claude/skills/mdxr"
341
+ },
342
+ devin: {
343
+ global: ".config/devin/skills/mdxr",
344
+ local: ".devin/skills/mdxr"
345
+ }
346
+ };
347
+ const installSkill = async (opts) => {
348
+ const tool = opts.tool ?? "agents";
349
+ const global = opts.global === true;
350
+ const base = global ? os.homedir() : process.cwd();
351
+ const dests = (tool === "all" ? Object.keys(SKILL_DIRS) : [tool]).map((t) => {
352
+ const dirs = own(SKILL_DIRS, t);
353
+ if (dirs === void 0) throw new Error(`unknown tool: ${t} (expected agents|claude|devin|all)`);
354
+ return path.join(base, global ? dirs.global : dirs.local);
355
+ });
356
+ for (const dest of dests) if (fs.existsSync(dest) && opts.force !== true) throw new Error(`${dest} already exists (use --force to overwrite)`);
357
+ return await Promise.all(dests.map(async (dest) => {
358
+ await fsp.mkdir(path.dirname(dest), { recursive: true });
359
+ if (opts.force === true) await fsp.rm(dest, {
360
+ force: true,
361
+ recursive: true
362
+ });
363
+ await fsp.cp(path.join(pkgRoot, "skill"), dest, {
364
+ force: true,
365
+ recursive: true
366
+ });
367
+ return dest;
368
+ }));
369
+ };
370
+ //#endregion
371
+ //#region src/client-js.ts
372
+ let cache;
373
+ const buildClientJs = async () => {
374
+ return (await build({
375
+ absWorkingDir: pkgRoot,
376
+ bundle: true,
377
+ format: "iife",
378
+ logLevel: "silent",
379
+ minify: true,
380
+ platform: "browser",
381
+ stdin: {
382
+ contents: "import \"./client/entry.js\";",
383
+ loader: "js",
384
+ resolveDir: path.join(pkgRoot, "src"),
385
+ sourcefile: "mdxr-client.js"
386
+ },
387
+ target: "es2022",
388
+ write: false
389
+ })).outputFiles[0]?.text ?? "";
390
+ };
391
+ /**
392
+ * Vanilla JS inlined into every document (copy/answer/save buttons, the
393
+ * live Markdown answer sheet, the theme toggle). Bundled from
394
+ * `src/client/entry.ts` at render time — real source, so the script is
395
+ * type-checked and unit-testable, and bundler changes can't silently
396
+ * break it the way `fn.toString()` serialization could. Memoized: the
397
+ * bundle is identical for every render in a process.
398
+ */
399
+ const clientJs = async () => {
400
+ cache ??= buildClientJs();
401
+ try {
402
+ return await cache;
403
+ } catch (error) {
404
+ cache = void 0;
405
+ throw error;
406
+ }
407
+ };
408
+ //#endregion
409
+ //#region src/component-map.ts
410
+ /**
411
+ * `into[k] = v` with `k === "__proto__"` would silently replace the map's
412
+ * prototype instead of registering the component — define it as data.
413
+ */
414
+ const put = (into, k, v) => {
415
+ Object.defineProperty(into, k, {
416
+ configurable: true,
417
+ enumerable: true,
418
+ value: v,
419
+ writable: true
420
+ });
421
+ };
422
+ /** A default-exported object is treated as a `{ Name: Component }` map. */
423
+ const mergeDefaultMap = (val, into) => {
424
+ if (!isRecord(val)) return;
425
+ for (const [k, v] of Object.entries(val)) if (isComponent(v)) put(into, k, v);
426
+ };
427
+ /**
428
+ * Merge a user module's exports into a ComponentMap. A default-exported
429
+ * object is treated as a `{ Name: Component }` map; PascalCase named exports
430
+ * that are functions count directly. Shared by the SSR loader (render.ts)
431
+ * and the hydration bundle (hydrate-runtime.ts) — node-free on purpose.
432
+ */
433
+ const mergeUserComponents = (mod, into = {}) => {
434
+ for (const [key, val] of Object.entries(mod)) if (key === "default") mergeDefaultMap(val, into);
435
+ else if (isComponent(val) && /^[A-Z]/u.test(key)) put(into, key, val);
436
+ return into;
437
+ };
438
+ /** Mermaid bootstrap (module). Only injected when the document uses it. */
439
+ const MERMAID_JS = `
440
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
441
+ mermaid.initialize({
442
+ startOnLoad: false,
443
+ theme: matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'default',
444
+ });
445
+ await mermaid.run({ nodes: document.querySelectorAll('.mermaid') });
446
+ `;
447
+ /**
448
+ * KaTeX stylesheet (fonts resolve relative to this URL on the CDN). Only
449
+ * linked when the document contains math — same CDN-on-demand model as
450
+ * mermaid. Version matches the bundled `katex` dependency.
451
+ */
452
+ const KATEX_CDN_URL = "https://cdn.jsdelivr.net/npm/katex@0.18.7/dist/katex.min.css";
453
+ /** Live-reload snippet injected by `mdxr serve` only. */
454
+ const LIVE_RELOAD_JS = `
455
+ var es = new EventSource('/__mdxr_events');
456
+ es.addEventListener('reload', function () { location.reload(); });
457
+ `;
458
+ /**
459
+ * The shadcn theme maps `dark:` to the `.dark` class, so dark mode is
460
+ * class-based rather than media-based. This head script applies the class
461
+ * before first paint: a stored choice from the theme toggle
462
+ * (localStorage `mdxr-theme`) wins, otherwise the document tracks
463
+ * `prefers-color-scheme` — and keeps tracking it only while no explicit
464
+ * choice is stored.
465
+ */
466
+ const THEME_JS = `
467
+ var q = matchMedia('(prefers-color-scheme: dark)');
468
+ var stored = null;
469
+ try { stored = localStorage.getItem('mdxr-theme'); } catch (e) {}
470
+ document.documentElement.classList.toggle(
471
+ 'dark',
472
+ stored === 'dark' || (stored !== 'light' && q.matches)
473
+ );
474
+ q.addEventListener('change', function (e) {
475
+ var s = null;
476
+ try { s = localStorage.getItem('mdxr-theme'); } catch (e2) {}
477
+ if (s !== 'light' && s !== 'dark') {
478
+ document.documentElement.classList.toggle('dark', e.matches);
479
+ }
480
+ });
481
+ // This script runs in <head>, before the toggle button is parsed —
482
+ // reflect the stored mode on it once the DOM exists.
483
+ addEventListener('DOMContentLoaded', function () {
484
+ var s = null;
485
+ try { s = localStorage.getItem('mdxr-theme'); } catch (e) {}
486
+ var mode = s === 'light' || s === 'dark' ? s : 'auto';
487
+ document.querySelectorAll('[data-mdxr-theme]').forEach(function (b) {
488
+ if (!(b instanceof HTMLElement)) {
489
+ return;
490
+ }
491
+ b.dataset.mode = mode;
492
+ b.setAttribute('title', 'Theme: ' + mode);
493
+ b.setAttribute('aria-label', 'Switch theme (current: ' + mode + ')');
494
+ });
495
+ });
496
+ `;
497
+ //#endregion
498
+ //#region src/html.ts
499
+ /**
500
+ * JS destined for an inline `<script>` element: neutralize the two byte
501
+ * sequences the HTML parser treats specially (`<\/script` ends the element,
502
+ * `<!--` opens a comment escape). `\u003C` keeps the meaning in strings,
503
+ * comments, and regex literals alike. Applied centrally here so every
504
+ * snippet — authored constants, the client bundle, the hydrate bundle —
505
+ * is safe regardless of its producer.
506
+ */
507
+ const inlineScript = (js) => js.replaceAll(/<\/script/giu, "\\u003C/script").replaceAll("<!--", "\\u003C!--");
508
+ /**
509
+ * Inline `<style>` content: a `</style` byte sequence inside the CSS (e.g. a
510
+ * `content:` string in user theme CSS) would end the element early. `<\/`
511
+ * reads identically inside CSS strings/comments, so it's safe to emit.
512
+ */
513
+ const inlineStyle = (css) => css.replaceAll(/<\/style/giu, "<\\/style");
514
+ const ESCAPES = {
515
+ "\"": "&quot;",
516
+ "&": "&amp;",
517
+ "'": "&#39;",
518
+ "<": "&lt;",
519
+ ">": "&gt;"
520
+ };
521
+ const escapeHtml = (s) => s.replaceAll(/[&<>"']/gu, (c) => ESCAPES[c] ?? c);
522
+ /** Inline SVG from the bundled lucide set (bodies carry stroke attrs). */
523
+ const iconSvg = (name) => {
524
+ const icon = icons.icons[name];
525
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 ${icon?.width ?? 24} ${icon?.height ?? 24}" class="lucide" aria-hidden="true">${icon?.body ?? ""}</svg>`;
526
+ };
527
+ /**
528
+ * Theme toggle button — document chrome fixed to the top-right corner.
529
+ * `data-mode` selects the visible icon (auto → light → dark, cycled by
530
+ * `handleDocEvent`); THEME_JS restores the stored choice before paint.
531
+ */
532
+ const THEME_TOGGLE_HTML = `<button type="button" class="mdxr-theme" data-mdxr-theme data-mode="auto" title="Theme: auto" aria-label="Switch theme (current: auto)"><span class="mdxr-theme-i mdxr-theme-i-auto">${iconSvg("sun-moon")}</span><span class="mdxr-theme-i mdxr-theme-i-light">${iconSvg("sun")}</span><span class="mdxr-theme-i mdxr-theme-i-dark">${iconSvg("moon")}</span></button>`;
533
+ const htmlDocument = (o) => `<!doctype html>
534
+ <html lang="en">
535
+ <head>
536
+ <meta charset="utf-8">
537
+ <meta name="viewport" content="width=device-width, initial-scale=1">
538
+ <meta name="generator" content="mdxr">
539
+ <title>${escapeHtml(o.title)}</title>
540
+ <script>${inlineScript(THEME_JS)}<\/script>
541
+ ${o.needsKatex === true ? `<link rel="stylesheet" href="${KATEX_CDN_URL}">` : ""}
542
+ <style>${inlineStyle(o.css)}</style>
543
+ </head>
544
+ <body class="bg-white text-neutral-900 antialiased dark:bg-neutral-950 dark:text-neutral-100">
545
+ ${THEME_TOGGLE_HTML}
546
+ <main id="mdxr-root" class="prose prose-neutral dark:prose-invert mx-auto max-w-3xl px-6 py-10">${o.body}</main>
547
+ <script>${inlineScript(o.clientJs)}<\/script>
548
+ ${o.needsMermaid ? `<script type="module">${inlineScript(MERMAID_JS)}<\/script>` : ""}
549
+ ${o.liveReload === true ? `<script>${inlineScript(LIVE_RELOAD_JS)}<\/script>` : ""}
550
+ ${o.hydrateJs === void 0 ? "" : `<script>${inlineScript(o.hydrateJs)}<\/script>`}
551
+ </body>
552
+ </html>
553
+ `;
554
+ //#endregion
555
+ //#region src/hydrate/export-index.ts
556
+ /**
557
+ * Export-surface scanning for the hydration bundle: which leaf module
558
+ * provides each public name, and which names each public specifier
559
+ * (`@suzumiyaaoba/mdxr`, `…/components`) actually exports. Derived by
560
+ * scanning the
561
+ * leaf modules under `src/ui/` + `src/components/ui/` rather than parsing
562
+ * barrels: any export form (`export *`, `export const`, `export {X}` lists,
563
+ * re-export chains like ask→ask-question) resolves correctly, and new leaf
564
+ * files self-register.
565
+ */
566
+ let exportIndexCache;
567
+ const DECLARE_RE = /export\s+(?:const|function|class)\s+(?<name>\w+)/gu;
568
+ const REEXPORT_RE = /export\s+(?!type\b)\{(?<names>[^}]*)\}\s*from\s*"(?<mod>\.[^"]+)"/gu;
569
+ /** `export * as name from "x"` — the exported name is `name`. */
570
+ const STAR_AS_RE = /export\s+\*\s+as\s+(?<name>\w+)\s+from\s*"[^"]+"/gu;
571
+ /** `export * from "./x.js"` — the surface walk follows relative targets. */
572
+ const STAR_RE = /export\s+\*\s+from\s*"(?<mod>\.[^"]+)"/gu;
573
+ /** `export { A, B as C }` with no `from` — local re-export list (shadcn style). */
574
+ const LOCAL_EXPORT_RE = /export\s+(?!type\b)\{(?<names>[^}]*)\}(?!\s*from\b)/gu;
575
+ /**
576
+ * `{ A, type B, C as D }` clause → runtime names importers see (`A`, `D`).
577
+ * `type`-marked parts are erased at runtime — emitting `export { B } from "m"`
578
+ * for one would explode the whole bundle with "no matching export".
579
+ */
580
+ const exportedNames = (names) => (names ?? "").split(",").map((part) => part.trim()).filter((part) => part !== "" && !part.startsWith("type ")).map((part) => part.split(/\s+as\s+/u).pop()?.trim() ?? "").filter((name) => name !== "");
581
+ /** `[exportName, target]` pairs a leaf module re-exports (`export {X as Y} from "./z"`). */
582
+ const reexportTargets = (source, dir) => {
583
+ const pairs = [];
584
+ for (const m of source.matchAll(REEXPORT_RE)) {
585
+ const mod = path.join(dir, m.groups?.mod ?? "");
586
+ for (const name of exportedNames(m.groups?.names)) pairs.push([name, mod]);
587
+ }
588
+ return pairs;
589
+ };
590
+ /** A `./x.js` specifier in our sources maps to `./x.ts`/`.tsx` on disk. */
591
+ const sourceFile = (spec, dir) => {
592
+ const stem = path.resolve(dir, spec).replace(/\.js$/u, "");
593
+ for (const ext of [
594
+ ".ts",
595
+ ".tsx",
596
+ ".js"
597
+ ]) {
598
+ const p = `${stem}${ext}`;
599
+ if (existsSync(p)) return p;
600
+ }
601
+ };
602
+ /** Every runtime-exported name in `source` lands in `out`. */
603
+ const collectNames = (source, out) => {
604
+ for (const m of source.matchAll(DECLARE_RE)) {
605
+ const name = m.groups?.name;
606
+ if (name !== void 0) out.add(name);
607
+ }
608
+ for (const m of source.matchAll(STAR_AS_RE)) {
609
+ const name = m.groups?.name;
610
+ if (name !== void 0) out.add(name);
611
+ }
612
+ for (const re of [REEXPORT_RE, LOCAL_EXPORT_RE]) for (const m of source.matchAll(re)) for (const name of exportedNames(m.groups?.names)) out.add(name);
613
+ };
614
+ /**
615
+ * Every name `entry` re-exports, chasing `export *` targets — the surface the
616
+ * real package shows importers. Type-only exports never match the regexes
617
+ * (`export type`, `type X` parts), so the set holds runtime names only.
618
+ */
619
+ const collectSurface = async (entry) => {
620
+ const out = /* @__PURE__ */ new Set();
621
+ const seen = /* @__PURE__ */ new Set();
622
+ const walk = async (file) => {
623
+ if (seen.has(file)) return;
624
+ seen.add(file);
625
+ let source;
626
+ try {
627
+ source = await readFile(file, "utf-8");
628
+ } catch {
629
+ return;
630
+ }
631
+ collectNames(source, out);
632
+ const dir = path.dirname(file);
633
+ const starTargets = [];
634
+ for (const m of source.matchAll(STAR_RE)) {
635
+ const mod = m.groups?.mod;
636
+ if (mod === void 0) continue;
637
+ const target = sourceFile(mod, dir);
638
+ if (target !== void 0) starTargets.push(target);
639
+ }
640
+ await Promise.all(starTargets.map(walk));
641
+ };
642
+ await walk(entry);
643
+ return out;
644
+ };
645
+ /**
646
+ * All `name → module` pairs a leaf file claims — declarations plus every
647
+ * re-export form. Returned rather than written into a shared map: callers
648
+ * merge in sorted filename order, so a duplicated export name resolves
649
+ * deterministically instead of racing whichever `readFile` finished last.
650
+ */
651
+ const scanLeaf = async (filePath) => {
652
+ const source = await readFile(filePath, "utf-8");
653
+ const dir = path.dirname(filePath);
654
+ const pairs = [];
655
+ for (const m of source.matchAll(DECLARE_RE)) {
656
+ const name = m.groups?.name;
657
+ if (name !== void 0) pairs.push([name, filePath]);
658
+ }
659
+ for (const [name, mod] of reexportTargets(source, dir)) pairs.push([name, mod]);
660
+ for (const m of source.matchAll(LOCAL_EXPORT_RE)) for (const name of exportedNames(m.groups?.names)) pairs.push([name, filePath]);
661
+ for (const m of source.matchAll(STAR_AS_RE)) {
662
+ const name = m.groups?.name;
663
+ if (name !== void 0) pairs.push([name, filePath]);
664
+ }
665
+ return pairs;
666
+ };
667
+ /**
668
+ * export name → module specifier for every name the catalog can produce,
669
+ * plus the real export surface of each public specifier.
670
+ */
671
+ const exportIndex = async () => {
672
+ if (exportIndexCache !== void 0) return exportIndexCache;
673
+ const map = /* @__PURE__ */ new Map([["DocContext", path.join(srcDir, "doc-context.js")]]);
674
+ const leafDirs = [path.join(srcDir, "ui"), path.join(srcDir, "components/ui")];
675
+ const scanned = await Promise.all(leafDirs.map(async (dir) => {
676
+ const files = (await readdir(dir)).filter((f) => /\.tsx?$/u.test(f) && f !== "index.ts").toSorted();
677
+ return await Promise.all(files.map(async (f) => await scanLeaf(path.join(dir, f))));
678
+ }));
679
+ for (const pairs of scanned.flat()) for (const [name, mod] of pairs) map.set(name, mod);
680
+ for (const [name, mod] of Object.entries(shadcnModules)) map.set(name, path.join(srcDir, "ui", mod));
681
+ const index = {
682
+ map,
683
+ surfaces: {
684
+ components: await collectSurface(path.join(srcDir, "components.ts")),
685
+ mdxr: await collectSurface(path.join(srcDir, "index.ts"))
686
+ }
687
+ };
688
+ exportIndexCache = index;
689
+ return index;
690
+ };
691
+ //#endregion
692
+ //#region src/hydrate/plugins.ts
693
+ /**
694
+ * esbuild plugins for the hydration bundle: pin shared packages to mdxr's own
695
+ * node_modules (a second React copy would break hooks/context), feed the
696
+ * compiled MDX module in as a virtual import, and shrink bundled icon sets to
697
+ * the names a document actually rendered.
698
+ */
699
+ /**
700
+ * Everything the document bundle shares with mdxr itself — React above all
701
+ * (a second copy would break hooks/context) — is resolved from this package's
702
+ * own node_modules regardless of where the user's components live.
703
+ * (esbuild serializes onResolve filters to Go's RE2: no `u` flag.)
704
+ */
705
+ const SHARED_RE = new RegExp(`^(?:${SHARED_PACKAGES.join("|")})(?:/.*)?$`);
706
+ const pinShared = {
707
+ name: "mdxr:pin-shared",
708
+ setup(b) {
709
+ b.onResolve({ filter: SHARED_RE }, async (args) => {
710
+ if (args.pluginData === pinShared) return null;
711
+ const r = await b.resolve(args.path, {
712
+ kind: args.kind,
713
+ pluginData: pinShared,
714
+ resolveDir: pkgRoot
715
+ });
716
+ return r.errors.length === 0 ? { path: r.path } : null;
717
+ });
718
+ }
719
+ };
720
+ /** The compiled MDX module enters the bundle as a virtual `mdxr:doc` import. */
721
+ const docModule = (code) => ({
722
+ name: "mdxr:doc",
723
+ setup(b) {
724
+ b.onResolve({ filter: /^mdxr:doc$/ }, () => ({
725
+ namespace: "mdxr-doc",
726
+ path: "mdxr:doc"
727
+ }));
728
+ b.onLoad({
729
+ filter: /^mdxr:doc$/,
730
+ namespace: "mdxr-doc"
731
+ }, () => ({
732
+ contents: code,
733
+ loader: "js",
734
+ resolveDir: srcDir
735
+ }));
736
+ }
737
+ });
738
+ const ICON_SETS = {
739
+ "@iconify-json/lucide": icons,
740
+ "@iconify-json/vscode-icons": icons$1
741
+ };
742
+ /**
743
+ * The bundled icon sets are several MB of JSON; a document only ever renders
744
+ * the icons recorded during SSR. Redirecting each `@iconify-json/*` import to
745
+ * a partial collection (used names only, alias parents included) keeps
746
+ * `addCollection` calls working while shipping kilobytes, not megabytes.
747
+ */
748
+ const iconSets = (usedIcons) => ({
749
+ name: "mdxr:icons",
750
+ setup(b) {
751
+ b.onResolve({ filter: /^@iconify-json\// }, (args) => ({
752
+ namespace: "mdxr-icons",
753
+ path: args.path
754
+ }));
755
+ b.onLoad({
756
+ filter: /^@iconify-json\//,
757
+ namespace: "mdxr-icons"
758
+ }, (args) => {
759
+ const set = ICON_SETS[args.path];
760
+ if (set === void 0) return {
761
+ contents: "export const icons = {};",
762
+ loader: "js"
763
+ };
764
+ const icons = {};
765
+ const aliases = {};
766
+ for (const full of usedIcons) {
767
+ const [prefix, name] = full.split(":");
768
+ if (prefix !== set.prefix || name === void 0) continue;
769
+ if (Object.hasOwn(set.icons, name)) icons[name] = set.icons[name];
770
+ const alias = Object.hasOwn(set.aliases ?? {}, name) ? set.aliases?.[name] : void 0;
771
+ if (alias !== void 0) {
772
+ aliases[name] = alias;
773
+ if (Object.hasOwn(set.icons, alias.parent)) icons[alias.parent] = set.icons[alias.parent];
774
+ }
775
+ }
776
+ return {
777
+ contents: `export const icons = ${JSON.stringify({
778
+ ...set,
779
+ aliases,
780
+ icons
781
+ })};`,
782
+ loader: "js"
783
+ };
784
+ });
785
+ }
786
+ });
787
+ //#endregion
788
+ //#region src/hydrate/import-scan.ts
789
+ /**
790
+ * User-module import scanning: which names an importer pulls from
791
+ * `@suzumiyaaoba/mdxr`(/components), and which valibot properties it accesses
792
+ * through
793
+ * an imported `v` alias. The virtual runtime module generated for each
794
+ * importer contains only what it requests — a full-surface module would
795
+ * drag the whole catalog into every hydration bundle.
796
+ */
797
+ /**
798
+ * Properties accessed on `alias` (`v`'s local name) — or `"all"` when the
799
+ * namespace escapes (bare use, `alias[key]`, spread, re-export) and the full
800
+ * valibot namespace must be shipped.
801
+ */
802
+ const scanVProps = (stripped, alias) => {
803
+ const esc = alias.replaceAll(/[$()*+.?[\\\]^{|}]/gu, "\\$&");
804
+ const access = new RegExp(`\\b${esc}\\s*\\?\\.\\s*(\\w+)|\\b${esc}\\s*\\.\\s*(\\w+)`, "gu");
805
+ const props = /* @__PURE__ */ new Set();
806
+ for (const use of stripped.matchAll(access)) {
807
+ const prop = use[1] ?? use[2];
808
+ if (prop !== void 0) props.add(prop);
809
+ }
810
+ const leftover = stripped.replaceAll(access, "");
811
+ return new RegExp(`\\b${esc}\\b`, "u").test(leftover) ? "all" : props;
812
+ };
813
+ /**
814
+ * `/* … *\/` and `// …` are legal inside a multiline import clause, and a
815
+ * name polluted with comment text would silently fail the surface check and
816
+ * kill the hydration build. Clauses never contain string literals, so a
817
+ * regex strip is safe here.
818
+ */
819
+ const stripComments = (s) => s.replaceAll(/\/\*[\s\S]*?\*\//gu, "").replaceAll(/\/\/[^\n]*/gu, "");
820
+ /** Parse `{ a, b as c, type T }` import specifiers into names + `v` aliases. */
821
+ const parseClause = (clause, names, vAliases) => {
822
+ const inner = stripComments(clause);
823
+ const close = inner.lastIndexOf("}");
824
+ const body = close === -1 ? inner.slice(1) : inner.slice(1, close);
825
+ for (const part of body.split(",")) {
826
+ const [imported, local] = part.trim().replace(/^type\s+/u, "").split(/\s+as\s+/u).map((s) => s?.trim());
827
+ if (imported === void 0 || imported === "") continue;
828
+ names.add(imported);
829
+ if (imported === "v") vAliases.push(local ?? "v");
830
+ }
831
+ };
832
+ /** Merge per-alias prop scans — `"all"` once any alias escapes. */
833
+ const mergeVProps = (stripped, vAliases, escapes) => {
834
+ if (escapes) return "all";
835
+ let vProps = null;
836
+ for (const alias of vAliases) {
837
+ const props = scanVProps(stripped, alias);
838
+ if (props === "all") return "all";
839
+ vProps = vProps === null ? props : /* @__PURE__ */ new Set([...vProps, ...props]);
840
+ }
841
+ return vProps;
842
+ };
843
+ /**
844
+ * Which names `importer` pulls from `@suzumiyaaoba/mdxr`(/components),
845
+ * extracted with a
846
+ * regex over its source. `names`/`vProps` are `"all"` for namespace, default,
847
+ * bare, or dynamic imports and for uninspectable importers — the fallback
848
+ * keeps semantics correct at the cost of emitting every catalog re-export.
849
+ */
850
+ const scanMdxrImports = async (importer) => {
851
+ let source;
852
+ try {
853
+ source = await readFile(importer, "utf-8");
854
+ } catch {
855
+ return {
856
+ names: "all",
857
+ vProps: "all"
858
+ };
859
+ }
860
+ const names = /* @__PURE__ */ new Set();
861
+ const vAliases = [];
862
+ const fromRe = /(?:import|export)\s*(?!\s*type\b)(?<clause>(?:\/\*[\s\S]*?\*\/|\/\/[^\n]*|[^;"'])*?)\s*from\s*["']@suzumiyaaoba\/mdxr(?:\/components)?["']/gu;
863
+ let stripped = source;
864
+ let vEscapes = false;
865
+ for (const m of source.matchAll(fromRe)) {
866
+ const clause = m.groups?.clause ?? "";
867
+ stripped = stripped.replace(m[0], "");
868
+ if (!clause.startsWith("{")) return {
869
+ names: "all",
870
+ vProps: "all"
871
+ };
872
+ const found = vAliases.length;
873
+ parseClause(clause, names, vAliases);
874
+ vEscapes ||= m[0].startsWith("export") && vAliases.length > found;
875
+ }
876
+ stripped = stripped.replaceAll(/(?:import|export)\s+type\s[^;]*?(?:;|$)/gmu, "");
877
+ const sep = String.raw`(?:\s|/\*[\s\S]*?\*/|//[^\n]*)*`;
878
+ if (new RegExp(`import${sep}["']@suzumiyaaoba/mdxr(?:/components)?["']`, "u").test(source) || new RegExp(`import${sep}\\(${sep}["']@suzumiyaaoba/mdxr(?:/components)?["']`, "u").test(source)) return {
879
+ names: "all",
880
+ vProps: "all"
881
+ };
882
+ const vProps = mergeVProps(stripped, vAliases, vEscapes);
883
+ return {
884
+ names,
885
+ vProps: vAliases.length === 0 ? null : vProps ?? /* @__PURE__ */ new Set()
886
+ };
887
+ };
888
+ //#endregion
889
+ //#region src/hydrate/runtime-module.ts
890
+ /**
891
+ * The virtual `@suzumiyaaoba/mdxr`(/components) module used inside hydration
892
+ * bundles.
893
+ * A fresh module is generated per importer containing only the re-exports
894
+ * that importer requests: esbuild eagerly resolves every
895
+ * `export { x } from "m"` target, and the leaf modules' top-level
896
+ * `defineComponent(...)` calls count as side effects — so a module exporting
897
+ * the whole catalog would drag the whole catalog into the bundle.
898
+ */
899
+ /**
900
+ * Names that live outside the leaf map: `builtinComponents` is defined in the
901
+ * catalog barrel itself (pulling it is intentional — the whole catalog is the
902
+ * value) and `defineConfig` is part of the `@suzumiyaaoba/mdxr` API surface.
903
+ * They resolve
904
+ * only when a module actually imports them.
905
+ */
906
+ const EXTRA_MODULES = {
907
+ builtinComponents: path.join(srcDir, "ui/index.js"),
908
+ defineConfig: path.join(srcDir, "config.js"),
909
+ mountDocument: path.join(srcDir, "hydrate-runtime.js")
910
+ };
911
+ /**
912
+ * The virtual module's `v` export. `export * as v` would materialize the
913
+ * entire valibot namespace — a plain object built from named imports ships
914
+ * only the schemas actually written. Unknown `v.x` accesses stay absent,
915
+ * matching `undefined` semantics. The `surface` gate keeps
916
+ * `@suzumiyaaoba/mdxr/components` honest: that specifier has no `v` export,
917
+ * so shipping one would diverge from SSR.
918
+ */
919
+ const vExportLines = (imports, surface) => {
920
+ if (!surface.has("v") || imports.vProps === null) return [];
921
+ if (imports.vProps === "all" || imports.names === "all") return ["export * as v from \"valibot\";"];
922
+ const props = [];
923
+ for (const p of imports.vProps) if (p in valibot) props.push(p);
924
+ return [props.length > 0 ? `import { ${props.join(", ")} } from "valibot";\nexport const v = { ${props.join(", ")} };` : "export const v = {};"];
925
+ };
926
+ /**
927
+ * Virtual stand-in for `@suzumiyaaoba/mdxr`(/components) inside the bundle.
928
+ */
929
+ const runtimeModuleContents = (map, imports, surface) => {
930
+ const lines = [`export { defineComponent, textOf } from ${JSON.stringify(path.join(srcDir, "define.js"))};`, ...vExportLines(imports, surface)];
931
+ const wanted = imports.names === "all" ? [...surface] : [...imports.names];
932
+ const byModule = /* @__PURE__ */ new Map();
933
+ for (const name of wanted) {
934
+ if (!surface.has(name)) continue;
935
+ const mod = map.get(name) ?? EXTRA_MODULES[name];
936
+ if (mod === void 0) continue;
937
+ const list = byModule.get(mod) ?? [];
938
+ list.push(name);
939
+ byModule.set(mod, list);
940
+ }
941
+ for (const [mod, mods] of byModule) lines.push(`export { ${mods.join(", ")} } from ${JSON.stringify(mod)};`);
942
+ return lines.join("\n");
943
+ };
944
+ const runtimeModule = (index) => ({
945
+ name: "mdxr:runtime",
946
+ setup(b) {
947
+ const importers = /* @__PURE__ */ new Map();
948
+ let seq = 0;
949
+ b.onResolve({ filter: /^@suzumiyaaoba\/mdxr(?:\/components)?$/ }, (args) => {
950
+ const virtual = `mdxr:runtime:${seq}`;
951
+ seq += 1;
952
+ importers.set(virtual, {
953
+ importer: args.importer,
954
+ surface: args.path === "@suzumiyaaoba/mdxr" ? index.surfaces.mdxr : index.surfaces.components
955
+ });
956
+ return {
957
+ namespace: "mdxr-runtime",
958
+ path: virtual
959
+ };
960
+ });
961
+ b.onLoad({
962
+ filter: /^mdxr:runtime:/,
963
+ namespace: "mdxr-runtime"
964
+ }, async (args) => {
965
+ const v = importers.get(args.path);
966
+ return {
967
+ contents: runtimeModuleContents(index.map, await scanMdxrImports(v?.importer ?? ""), v?.surface ?? index.surfaces.components),
968
+ loader: "js",
969
+ resolveDir: srcDir
970
+ };
971
+ });
972
+ }
973
+ });
974
+ //#endregion
975
+ //#region src/hydrate.ts
976
+ /**
977
+ * Hydration bundle assembly: generates the inlined client script that
978
+ * rebuilds the exact vnode tree SSR produced and `hydrateRoot`s it onto
979
+ * `<main id="mdxr-root">`. Implementation details live in `src/hydrate/`:
980
+ * `export-index` (export surface scanning), `import-scan` (user-module
981
+ * import analysis), `runtime-module` (virtual package module codegen), and
982
+ * `plugins` (esbuild plugins for shared packages, the doc module, icons).
983
+ */
984
+ /**
985
+ * Build the inlined client script: it rebuilds the exact vnode tree SSR
986
+ * produced — `DocContext.Provider` wrapping the Plan header + the compiled
987
+ * MDX module — and `hydrateRoot`s it onto `<main id="mdxr-root">`.
988
+ * `fileLink` is replayed from the recorded SSR answers, so components see
989
+ * identical data without filesystem access.
990
+ */
991
+ const buildHydrateScript = async (spec) => {
992
+ const index = await exportIndex();
993
+ const { map } = index;
994
+ const imports = [];
995
+ const entries = [];
996
+ let seq = 0;
997
+ const bind = (exported) => {
998
+ const mod = map.get(exported) ?? EXTRA_MODULES[exported];
999
+ if (mod === void 0) throw new Error(`hydration: no module provides catalog export "${exported}"`);
1000
+ const local = `__mdxr_c${seq}`;
1001
+ seq += 1;
1002
+ imports.push(`import { ${exported} as ${local} } from ${JSON.stringify(mod)};`);
1003
+ return local;
1004
+ };
1005
+ const mountDocument = bind("mountDocument");
1006
+ const planHeader = spec.header === void 0 ? "undefined" : bind("PlanHeader");
1007
+ for (const name of spec.usedComponents) {
1008
+ if (!Object.hasOwn(builtinComponents, name)) continue;
1009
+ const exported = name === "pre" ? "Pre" : name;
1010
+ entries.push(`${JSON.stringify(name)}: ${bind(exported)}`);
1011
+ }
1012
+ const userImport = spec.componentsPath === void 0 ? "" : `import * as __mdxrUser from ${JSON.stringify(spec.componentsPath)};`;
1013
+ const entry = `${imports.join("\n")}
1014
+ ${userImport}
1015
+ import __mdxrDoc from "mdxr:doc";
1016
+ ${mountDocument}({
1017
+ components: { ${entries.join(", ")} },
1018
+ doc: __mdxrDoc,
1019
+ fileLinks: ${JSON.stringify(spec.fileLinks)},
1020
+ headerProps: ${JSON.stringify(spec.header)},
1021
+ now: ${JSON.stringify(spec.now)},
1022
+ planHeader: ${planHeader},
1023
+ userModule: ${spec.componentsPath === void 0 ? "undefined" : "__mdxrUser"},
1024
+ });
1025
+ `;
1026
+ const result = await build({
1027
+ absWorkingDir: pkgRoot,
1028
+ bundle: true,
1029
+ define: { "process.env.NODE_ENV": "\"production\"" },
1030
+ format: "iife",
1031
+ jsx: "automatic",
1032
+ jsxImportSource: "react",
1033
+ logLevel: "silent",
1034
+ metafile: true,
1035
+ minify: true,
1036
+ platform: "browser",
1037
+ plugins: [
1038
+ pinShared,
1039
+ docModule(spec.code),
1040
+ runtimeModule(index),
1041
+ iconSets(spec.usedIcons)
1042
+ ],
1043
+ stdin: {
1044
+ contents: entry,
1045
+ loader: "js",
1046
+ resolveDir: pkgRoot,
1047
+ sourcefile: "mdxr-hydrate.js"
1048
+ },
1049
+ target: "es2022",
1050
+ write: false
1051
+ });
1052
+ if (process.env.MDXR_BUNDLE_METAFILE !== void 0) {
1053
+ const { writeFile } = await import("node:fs/promises");
1054
+ await writeFile(process.env.MDXR_BUNDLE_METAFILE, JSON.stringify(result.metafile));
1055
+ await writeFile(`${process.env.MDXR_BUNDLE_METAFILE}.entry.js`, entry);
1056
+ }
1057
+ return result.outputFiles[0]?.text ?? "";
1058
+ };
1059
+ //#endregion
1060
+ //#region src/editor.ts
1061
+ /**
1062
+ * Path → URI path segment-by-segment. `encodeURI` would leave `#`, `?`,
1063
+ * `%`, `&` intact and they parse as delimiters inside the editor URL;
1064
+ * `encodeURIComponent` on the whole path would mangle `/`. Backslashes are
1065
+ * normalized so Windows paths survive too (`C:\x` → `C%3A/x`).
1066
+ */
1067
+ const encodePath = (p) => p.replaceAll("\\", "/").split("/").map((seg) => encodeURIComponent(seg)).join("/");
1068
+ /** `scheme://file/{abs}:{line}` — the VS Code URL convention most editors share. */
1069
+ const atLine = (scheme) => (p, l) => `${scheme}://file${encodePath(p)}${nonEmpty(l) ? `:${l}` : ""}`;
1070
+ /** `{scheme}://open?url=file://{abs}&line={n}` — Sublime/TextMate style. */
1071
+ const queryUrl = (scheme) => (p, l) => `${scheme}://open?url=file://${encodePath(p)}${nonEmpty(l) ? `&line=${l}` : ""}`;
1072
+ /** Known editors; unknown names fall back to the `scheme://file` convention. */
1073
+ const EDITORS = {
1074
+ cursor: atLine("cursor"),
1075
+ idea: (p, l) => `idea://open?file=${encodePath(p)}${nonEmpty(l) ? `&line=${l}` : ""}`,
1076
+ sublime: queryUrl("subl"),
1077
+ textmate: queryUrl("txmt"),
1078
+ vscode: atLine("vscode"),
1079
+ "vscode-insiders": atLine("vscode-insiders"),
1080
+ windsurf: atLine("windsurf"),
1081
+ zed: atLine("zed")
1082
+ };
1083
+ /**
1084
+ * Schemes that execute script when an `<a href>` is clicked. `editor` is a
1085
+ * config value, but frontmatter can also supply it — a document must not be
1086
+ * able to mint `javascript:`/`data:` links through file references.
1087
+ */
1088
+ const SCRIPTABLE = /* @__PURE__ */ new Set([
1089
+ "data",
1090
+ "javascript",
1091
+ "vbscript"
1092
+ ]);
1093
+ const build$1 = (editor, absPath, line) => {
1094
+ const e = nonEmpty(editor) ? editor : "vscode";
1095
+ if (e === "none") return;
1096
+ const known = EDITORS[e];
1097
+ if (known !== void 0) return known(absPath, line);
1098
+ if (e.includes("{path}")) return e.replaceAll("{path}", encodePath(absPath)).replaceAll("{line}", line ?? "");
1099
+ if (!/^[a-z][a-z0-9+.-]*$/iu.test(e)) return;
1100
+ return atLine(e)(absPath, line);
1101
+ };
1102
+ /**
1103
+ * Editor URL for an absolute path. `editor` is a known name (see EDITORS),
1104
+ * `"none"` (no link), a URL template with `{path}`/`{line}` placeholders
1105
+ * (`myed://open?f={path}&l={line}`), or a bare name treated as a
1106
+ * `scheme://file` editor. Undefined → vscode. Scriptable schemes are
1107
+ * refused — `editor` can arrive from frontmatter, which is document input.
1108
+ */
1109
+ const editorUrl = (editor, absPath, line) => {
1110
+ const url = build$1(editor, absPath, line);
1111
+ if (url === void 0) return;
1112
+ const scheme = urlScheme(url);
1113
+ return scheme !== void 0 && SCRIPTABLE.has(scheme) ? void 0 : url;
1114
+ };
1115
+ //#endregion
1116
+ //#region src/rehype/shiki.ts
1117
+ /**
1118
+ * Fenced code blocks are syntax-highlighted with shiki at render time.
1119
+ * `defaultColor: false` emits `--shiki-light`/`--shiki-dark` CSS variables on
1120
+ * every token span; BASE_CSS switches between them via prefers-color-scheme.
1121
+ * SKIP_LANGS (langs.ts) are left untouched: mermaid renders as a diagram,
1122
+ * terminal langs as a transcript, plain-text aliases stay cheap.
1123
+ */
1124
+ const THEMES = {
1125
+ dark: "github-dark",
1126
+ light: "github-light"
1127
+ };
1128
+ /**
1129
+ * A middle ground between startup cost and coverage: grammars in this list are
1130
+ * loaded with the highlighter, anything else in shiki's bundle is loaded on
1131
+ * first use, and unknown names degrade to unhighlighted code.
1132
+ */
1133
+ const PRELOADED_LANGS = [
1134
+ "bash",
1135
+ "c",
1136
+ "cpp",
1137
+ "csharp",
1138
+ "css",
1139
+ "diff",
1140
+ "dockerfile",
1141
+ "go",
1142
+ "graphql",
1143
+ "html",
1144
+ "ini",
1145
+ "java",
1146
+ "javascript",
1147
+ "json",
1148
+ "jsonc",
1149
+ "jsx",
1150
+ "kotlin",
1151
+ "lua",
1152
+ "markdown",
1153
+ "php",
1154
+ "python",
1155
+ "ruby",
1156
+ "rust",
1157
+ "scss",
1158
+ "shell",
1159
+ "sql",
1160
+ "swift",
1161
+ "toml",
1162
+ "tsx",
1163
+ "typescript",
1164
+ "xml",
1165
+ "yaml",
1166
+ "zsh"
1167
+ ];
1168
+ /**
1169
+ * Fence-meta tokens that turn line numbers on: ` ```ts ln ` (also accepts
1170
+ * `line-numbers` / `lineNumbers` / `showLineNumbers`).
1171
+ */
1172
+ const LINE_NUMBER_RE = /(?:^|\s)(?:ln|line-numbers|lineNumbers|showLineNumbers)(?:\s|$)/u;
1173
+ /**
1174
+ * Transformers shared by every highlighted block. The notation set strips
1175
+ * `// [!code …]` markers from the output and tags lines/spans; the meta set
1176
+ * reads the fence info string (`{1,3-5}` line ranges, `/word/` matches).
1177
+ */
1178
+ const TRANSFORMERS = [
1179
+ transformerNotationDiff(),
1180
+ transformerNotationErrorLevel(),
1181
+ transformerNotationFocus(),
1182
+ transformerNotationHighlight(),
1183
+ transformerNotationWordHighlight(),
1184
+ transformerRemoveNotationEscape(),
1185
+ transformerMetaHighlight(),
1186
+ transformerMetaWordHighlight(),
1187
+ {
1188
+ name: "mdxr:line-numbers",
1189
+ pre(node) {
1190
+ const raw = this.options.meta?.__raw ?? "";
1191
+ if (LINE_NUMBER_RE.test(raw)) this.addClassToHast(node, "has-line-numbers");
1192
+ }
1193
+ }
1194
+ ];
1195
+ let highlighterPromise;
1196
+ const getHighlighter = async () => {
1197
+ highlighterPromise ??= createHighlighter({
1198
+ langs: [...PRELOADED_LANGS],
1199
+ themes: [THEMES.light, THEMES.dark]
1200
+ });
1201
+ try {
1202
+ return await highlighterPromise;
1203
+ } catch (error) {
1204
+ highlighterPromise = void 0;
1205
+ throw error;
1206
+ }
1207
+ };
1208
+ const DIFF_FENCE_LANGS = /* @__PURE__ */ new Set(["diff", "patch"]);
1209
+ const isElement = (node) => isRecord(node) && node.type === "element" && typeof node.tagName === "string";
1210
+ /** mdast-sourced nodes use `className`; shiki's hast uses `class`. Read both. */
1211
+ const classNames = (el) => {
1212
+ const c = el?.properties?.className ?? el?.properties?.class;
1213
+ if (typeof c === "string") return c.split(/\s+/u).filter(Boolean);
1214
+ return Array.isArray(c) ? c.map(String) : [];
1215
+ };
1216
+ const textContent$1 = (node) => {
1217
+ if (node.type === "text") return node.value;
1218
+ return "children" in node ? node.children.map(textContent$1).join("") : "";
1219
+ };
1220
+ const languageOf = (code) => /language-(?<lang>[^\s]+)/u.exec(classNames(code).join(" "))?.groups?.lang;
1221
+ /** In-flight grammar loads, deduplicated across concurrent code blocks. */
1222
+ const langLoads = /* @__PURE__ */ new Map();
1223
+ /** Lazily loads a grammar; resolves false when shiki doesn't know the name. */
1224
+ const ensureLanguage = async (highlighter, lang) => {
1225
+ if (highlighter.getLoadedLanguages().includes(lang)) return true;
1226
+ const pending = langLoads.get(lang);
1227
+ if (pending !== void 0) return await pending;
1228
+ const load = (async () => {
1229
+ const bundled = highlighter.getBundledLanguages();
1230
+ const loader = own(bundled, lang);
1231
+ if (loader === void 0) return false;
1232
+ await highlighter.loadLanguage(loader);
1233
+ return highlighter.getLoadedLanguages().includes(lang);
1234
+ })();
1235
+ langLoads.set(lang, load);
1236
+ try {
1237
+ return await load;
1238
+ } finally {
1239
+ langLoads.delete(lang);
1240
+ }
1241
+ };
1242
+ /**
1243
+ * The highlighter when `lang` can be handled — undefined for SKIP_LANGS and
1244
+ * names shiki doesn't know, so callers degrade to unhighlighted code.
1245
+ */
1246
+ const readyHighlighter = async (lang) => {
1247
+ if (SKIP_LANGS.has(lang)) return;
1248
+ try {
1249
+ const highlighter = await getHighlighter();
1250
+ return await ensureLanguage(highlighter, lang) ? highlighter : void 0;
1251
+ } catch {
1252
+ return;
1253
+ }
1254
+ };
1255
+ /**
1256
+ * One side of a hunk, highlighted as the file's own language: `ctx`/`add`
1257
+ * rows read from the new side, `ctx`/`del` rows from the old side. Shiki's
1258
+ * output line array aligns 1:1 with the joined row texts (rows never
1259
+ * contain `\n`).
1260
+ */
1261
+ const sideTokens = (highlighter, lang, src) => {
1262
+ if (src === "") return [];
1263
+ const code = highlighter.codeToHast(src, {
1264
+ defaultColor: false,
1265
+ lang,
1266
+ themes: THEMES
1267
+ }).children.find((child) => isElement(child) && child.tagName === "pre")?.children.find((child) => isElement(child) && child.tagName === "code");
1268
+ if (code === void 0) return [];
1269
+ const lines = [];
1270
+ for (const child of code.children) {
1271
+ if (!isElement(child) || child.tagName !== "span" || !classNames(child).includes("line")) continue;
1272
+ lines.push(child.children.flatMap((tok) => {
1273
+ if (tok.type === "text") return tok.value === "" ? [] : [{ t: tok.value }];
1274
+ if (!isElement(tok)) return [];
1275
+ const style = tok.properties?.style;
1276
+ return [{
1277
+ s: typeof style === "string" && style !== "" ? style : void 0,
1278
+ t: textContent$1(tok)
1279
+ }];
1280
+ }));
1281
+ }
1282
+ return lines;
1283
+ };
1284
+ /** Token lines per row of one hunk — `null` for note rows and for rows whose
1285
+ * side had no usable grammar. Add/ctx rows read the new-side highlight, del
1286
+ * rows the old-side one (the sides can carry different languages after a
1287
+ * cross-extension rename). */
1288
+ const hunkTokens = (sides, rows) => {
1289
+ const nt = sides.new === void 0 ? [] : sideTokens(sides.new.highlighter, sides.new.lang, rows.filter((r) => r.kind === "ctx" || r.kind === "add").map((r) => r.text).join("\n"));
1290
+ const ot = sides.old === void 0 ? [] : sideTokens(sides.old.highlighter, sides.old.lang, rows.filter((r) => r.kind === "ctx" || r.kind === "del").map((r) => r.text).join("\n"));
1291
+ let ni = 0;
1292
+ let oi = 0;
1293
+ return rows.map((row) => {
1294
+ if (row.kind === "add") {
1295
+ const t = nt[ni] ?? null;
1296
+ ni += 1;
1297
+ return t;
1298
+ }
1299
+ if (row.kind === "del") {
1300
+ const t = ot[oi] ?? null;
1301
+ oi += 1;
1302
+ return t;
1303
+ }
1304
+ if (row.kind === "ctx") {
1305
+ const t = nt[ni] ?? null;
1306
+ ni += 1;
1307
+ oi += 1;
1308
+ return t;
1309
+ }
1310
+ return null;
1311
+ });
1312
+ };
1313
+ /**
1314
+ * Language-highlighted rows for every file/hunk of a parsed diff. Language
1315
+ * comes from the `lang=` meta override, else the file's own path (the fence's
1316
+ * `title=`/`filename=` names a single-file diff). A file whose language can't
1317
+ * be resolved keeps `null` rows — it renders plain, like today.
1318
+ */
1319
+ const diffHighlight = async (files, meta) => {
1320
+ const override = fenceLang(meta);
1321
+ const named = files.length === 1 ? fenceFilename(meta) : void 0;
1322
+ const langOf = (path) => override ?? (path === void 0 ? void 0 : langForPath(path));
1323
+ const perFile = files.map((file) => ({
1324
+ file,
1325
+ newLang: langOf(file.newPath ?? file.oldPath ?? named),
1326
+ oldLang: langOf(file.oldPath ?? file.newPath ?? named)
1327
+ }));
1328
+ const wanted = new Set(perFile.flatMap((l) => [l.newLang, l.oldLang].filter((x) => x !== void 0)));
1329
+ const ready = /* @__PURE__ */ new Map();
1330
+ await Promise.all([...wanted].map(async (lang) => {
1331
+ const highlighter = await readyHighlighter(lang);
1332
+ if (highlighter !== void 0) ready.set(lang, highlighter);
1333
+ }));
1334
+ const sideOf = (lang) => {
1335
+ if (lang === void 0) return;
1336
+ const highlighter = ready.get(lang);
1337
+ return highlighter === void 0 ? void 0 : {
1338
+ highlighter,
1339
+ lang
1340
+ };
1341
+ };
1342
+ let any = false;
1343
+ const hl = perFile.map(({ file, newLang, oldLang }) => {
1344
+ const newSide = sideOf(newLang);
1345
+ const oldSide = sideOf(oldLang) ?? newSide;
1346
+ return file.hunks.map((h) => {
1347
+ try {
1348
+ const rows = hunkTokens({
1349
+ new: newSide,
1350
+ old: oldSide
1351
+ }, h.rows);
1352
+ any ||= rows.some((r) => r !== null);
1353
+ return rows;
1354
+ } catch {
1355
+ return h.rows.map(() => null);
1356
+ }
1357
+ });
1358
+ });
1359
+ return any ? hl : void 0;
1360
+ };
1361
+ /**
1362
+ * Rehype plugin: replace the text inside `pre > code` with shiki's highlighted
1363
+ * line spans. The `pre`/`code` elements and their properties (language class,
1364
+ * `meta` from remarkCodeMeta) are kept so the `Pre` component's filename
1365
+ * header, copy payload, and mermaid handling keep working. ```diff/```patch
1366
+ * fences skip the generic path: `DiffView` replaces their markup, so instead
1367
+ * each hunk's old/new sides are highlighted in the file's own language and
1368
+ * parked as JSON on `code`'s `data-diffhl` prop — it serializes into the
1369
+ * compiled module, reaching SSR and hydration renders identically.
1370
+ */
1371
+ const rehypeShiki = () => async (tree) => {
1372
+ const targets = [];
1373
+ const diffs = [];
1374
+ visit(tree, "element", (node) => {
1375
+ if (node.tagName !== "pre") return;
1376
+ const code = node.children[0];
1377
+ if (!isElement(code) || code.tagName !== "code") return;
1378
+ const lang = languageOf(code);
1379
+ if (lang === void 0 || SKIP_LANGS.has(lang)) return;
1380
+ const meta = typeof code.properties?.meta === "string" ? code.properties.meta : "";
1381
+ const text = textContent$1(code);
1382
+ if (DIFF_FENCE_LANGS.has(lang)) {
1383
+ const files = parseDiff(text).filter((f) => f.raw.length > 0);
1384
+ if (files.length > 0) {
1385
+ diffs.push({
1386
+ code,
1387
+ files,
1388
+ meta
1389
+ });
1390
+ return;
1391
+ }
1392
+ }
1393
+ targets.push({
1394
+ code,
1395
+ lang,
1396
+ meta,
1397
+ text
1398
+ });
1399
+ });
1400
+ if (targets.length === 0 && diffs.length === 0) return;
1401
+ await Promise.all([...targets.map(async ({ code, lang, meta, text }) => {
1402
+ const highlighter = await readyHighlighter(lang);
1403
+ if (highlighter === void 0) return;
1404
+ let hast;
1405
+ try {
1406
+ hast = highlighter.codeToHast(text.replace(/\n$/u, ""), {
1407
+ defaultColor: false,
1408
+ lang,
1409
+ meta: { __raw: meta },
1410
+ themes: THEMES,
1411
+ transformers: TRANSFORMERS
1412
+ });
1413
+ } catch {
1414
+ return;
1415
+ }
1416
+ const shikiPre = hast.children.find((child) => isElement(child) && child.tagName === "pre");
1417
+ const highlightedCode = shikiPre?.children[0];
1418
+ if (!isElement(highlightedCode) || highlightedCode.tagName !== "code") return;
1419
+ code.children = highlightedCode.children;
1420
+ const lifted = classNames(shikiPre).filter((c) => c.startsWith("has-"));
1421
+ code.properties = {
1422
+ ...code.properties,
1423
+ className: [
1424
+ ...classNames(code),
1425
+ "shiki",
1426
+ ...lifted
1427
+ ]
1428
+ };
1429
+ }), ...diffs.map(async ({ code, files, meta }) => {
1430
+ const hl = await diffHighlight(files, meta);
1431
+ if (hl === void 0) return;
1432
+ code.properties = {
1433
+ ...code.properties,
1434
+ "data-diffhl": JSON.stringify(hl)
1435
+ };
1436
+ })]);
1437
+ };
1438
+ //#endregion
1439
+ //#region src/remark/ast.ts
1440
+ const isParent = (n) => "children" in n && Array.isArray(n.children);
1441
+ const isFlowElement = (n) => n.type === "mdxJsxFlowElement";
1442
+ /** All literal text under `node`, concatenated in tree order — `inlineCode`
1443
+ * carries its code in `value` too, and dropping it would truncate heading
1444
+ * slugs and directive labels at the first backtick span. */
1445
+ const textContent = (node) => {
1446
+ let out = "";
1447
+ visit(node, ["text", "inlineCode"], (n) => {
1448
+ if ("value" in n && typeof n.value === "string") out += n.value;
1449
+ });
1450
+ return out;
1451
+ };
1452
+ /** A JSX attribute's string value, or undefined when absent/empty/non-string. */
1453
+ const jsxAttr = (node, name) => {
1454
+ if (!Array.isArray(node.attributes)) return;
1455
+ for (const a of node.attributes) if (isRecord(a) && a.name === name && typeof a.value === "string" && a.value !== "") return a.value;
1456
+ };
1457
+ /**
1458
+ * `Record` → `mdxJsxAttribute[]`. null/undefined/"" values are dropped —
1459
+ * MDX attributes arrive as strings, so everything is `String()`-coerced.
1460
+ */
1461
+ const jsxAttrs = (attrs) => Object.entries(attrs).filter(([, val]) => val !== null && val !== void 0 && val !== "").map(([key, val]) => ({
1462
+ name: key,
1463
+ type: "mdxJsxAttribute",
1464
+ value: String(val)
1465
+ }));
1466
+ /** Mutate `node` into an `mdxJsxFlowElement`/`mdxJsxTextElement` in place. */
1467
+ const toMdxElement = (node, kind, name, attrs) => {
1468
+ node.type = kind;
1469
+ node.name = name;
1470
+ node.attributes = jsxAttrs(attrs);
1471
+ };
1472
+ /**
1473
+ * Merge `key: value` into `node.data.hProperties` (creating both levels).
1474
+ * The mdast→hast bridge turns hProperties into element props.
1475
+ */
1476
+ const setHProperty = (node, key, value) => {
1477
+ const data = isRecord(node.data) ? node.data : {};
1478
+ node.data = data;
1479
+ data.hProperties = {
1480
+ ...isRecord(data.hProperties) ? data.hProperties : {},
1481
+ [key]: value
1482
+ };
1483
+ };
1484
+ //#endregion
1485
+ //#region src/remark/callouts.ts
1486
+ /** Callout kinds shared by `:::kind` directives and `> [!KIND]` alerts. */
1487
+ const CALLOUT_KINDS = /* @__PURE__ */ new Set([
1488
+ "note",
1489
+ "tip",
1490
+ "important",
1491
+ "warning",
1492
+ "caution",
1493
+ "danger",
1494
+ "decision",
1495
+ "goal",
1496
+ "nongoal",
1497
+ "question",
1498
+ "answer"
1499
+ ]);
1500
+ /** Lowercases and maps the `non-goal`/`nongoal` spellings to `nongoal`. */
1501
+ const normalizeCalloutKind = (kind) => kind.toLowerCase().replace(/^non-goal$/u, "nongoal");
1502
+ /** GitHub alert marker: `> [!WARNING]` — kinds derived from CALLOUT_KINDS. */
1503
+ const ALERT_RE = new RegExp(`^\\[!(?<kind>${[...CALLOUT_KINDS].map((k) => k === "nongoal" ? "non-?goal" : k).join("|")})\\]\\s*`, "iu");
1504
+ //#endregion
1505
+ //#region src/remark/alerts.ts
1506
+ /** A text node with a mutable string `value` — callers edit it in place. */
1507
+ const isTextValue = (n) => n !== void 0 && n.type === "text" && "value" in n && typeof n.value === "string";
1508
+ /**
1509
+ * GitHub-style alerts become Callout components:
1510
+ * > [!WARNING]
1511
+ * > Be careful.
1512
+ * renders identically to <Callout kind="warning">.
1513
+ */
1514
+ const remarkMdxrAlerts = () => (tree) => {
1515
+ visit(tree, "blockquote", (node) => {
1516
+ if (!isParent(node)) return;
1517
+ const [first] = node.children;
1518
+ if (first === void 0 || first.type !== "paragraph" || !isParent(first)) return;
1519
+ const [text] = first.children;
1520
+ if (!isTextValue(text)) return;
1521
+ const m = ALERT_RE.exec(text.value);
1522
+ if (m === null || m.groups === void 0) return;
1523
+ const rest = text.value.slice(m[0].length);
1524
+ text.value = rest;
1525
+ if (rest === "") first.children.shift();
1526
+ if (first.children.length === 0) node.children.shift();
1527
+ toMdxElement(node, "mdxJsxFlowElement", "Callout", { kind: normalizeCalloutKind(m.groups.kind) });
1528
+ });
1529
+ };
1530
+ //#endregion
1531
+ //#region src/remark/code-file.ts
1532
+ /**
1533
+ * `<CodeFile path="src/foo.ts" lines="40-52" lang="ts" />` embeds a real file
1534
+ * from disk as a fenced code block, so code explanations quote the actual
1535
+ * source instead of drifting copies. `path` resolves relative to the
1536
+ * document; `lines` slices a 1-based range; `lang` overrides the extension.
1537
+ * The emitted `code` node carries `title="…"` meta so `remarkCodeMeta` /
1538
+ * `Pre` render the usual filename header — run this before remarkCodeMeta.
1539
+ */
1540
+ const remarkCodeFile = () => (tree, file) => {
1541
+ visit(tree, "mdxJsxFlowElement", (node) => {
1542
+ if (!isFlowElement(node) || node.name !== "CodeFile") return;
1543
+ const rel = jsxAttr(node, "path");
1544
+ if (rel === void 0) file.fail("<CodeFile> requires a `path` attribute", node, "mdxr:code-file");
1545
+ const abs = path.resolve(file.dirname ?? ".", rel);
1546
+ let content;
1547
+ try {
1548
+ content = readFileSync(abs, "utf-8");
1549
+ } catch {
1550
+ file.fail(`<CodeFile> cannot read ${rel}`, node, "mdxr:code-file");
1551
+ }
1552
+ const rangeSpec = jsxAttr(node, "lines");
1553
+ let titleSuffix = "";
1554
+ if (rangeSpec !== void 0) {
1555
+ const range = parseLineRange(rangeSpec);
1556
+ if (range === void 0) file.fail(`<CodeFile> invalid lines range: ${rangeSpec}`, node, "mdxr:code-file");
1557
+ const all = content.replace(/\n$/u, "").split("\n");
1558
+ if (range.start > all.length) file.fail(`<CodeFile> lines ${rangeSpec} out of range (${all.length} lines in ${rel})`, node, "mdxr:code-file");
1559
+ content = all.slice(range.start - 1, range.end).join("\n");
1560
+ titleSuffix = `:${rangeSpec}`;
1561
+ }
1562
+ const lang = jsxAttr(node, "lang") ?? langForPath(abs);
1563
+ const target = node;
1564
+ target.type = "code";
1565
+ delete target.name;
1566
+ delete target.attributes;
1567
+ delete target.children;
1568
+ target.lang = lang;
1569
+ target.meta = `title="${rel.replaceAll("\"", "'")}${titleSuffix}"`;
1570
+ target.value = content;
1571
+ });
1572
+ };
1573
+ //#endregion
1574
+ //#region src/remark/code-meta.ts
1575
+ /**
1576
+ * The mdast→hast conversion drops the code fence info string (`meta`).
1577
+ * Copy it to `hProperties.meta` so it reaches the `pre` element's props.
1578
+ */
1579
+ const remarkCodeMeta = () => (tree) => {
1580
+ visit(tree, "code", (node) => {
1581
+ if (!("meta" in node) || typeof node.meta !== "string" || node.meta === "") return;
1582
+ setHProperty(node, "meta", node.meta);
1583
+ });
1584
+ };
1585
+ //#endregion
1586
+ //#region src/remark/directives.ts
1587
+ const CONTAINER_COMPONENTS = {
1588
+ audit: "Audit",
1589
+ barchart: "BarChart",
1590
+ bench: "Bench",
1591
+ benchmarks: "Benchmarks",
1592
+ board: "Board",
1593
+ bridge: "Bridge",
1594
+ bump: "Bump",
1595
+ bumps: "Bumps",
1596
+ check: "Check",
1597
+ checks: "Checks",
1598
+ comment: "Comment",
1599
+ comments: "Comments",
1600
+ day: "Day",
1601
+ dbfield: "DbField",
1602
+ dbtable: "DbTable",
1603
+ deps: "Deps",
1604
+ endpoints: "Endpoints",
1605
+ entry: "Entry",
1606
+ envvar: "EnvVar",
1607
+ envvars: "EnvVars",
1608
+ files: "Files",
1609
+ finding: "Finding",
1610
+ findings: "Findings",
1611
+ flow: "Flow",
1612
+ funnel: "Funnel",
1613
+ gantt: "Gantt",
1614
+ gauge: "Gauge",
1615
+ gauges: "Gauges",
1616
+ graph: "Graph",
1617
+ hypotheses: "Hypotheses",
1618
+ hypothesis: "Hypothesis",
1619
+ incident: "Incident",
1620
+ linechart: "LineChart",
1621
+ matrix: "Matrix",
1622
+ package: "Package",
1623
+ packages: "Packages",
1624
+ pathway: "Pathway",
1625
+ phase: "Phase",
1626
+ piechart: "PieChart",
1627
+ plan: "Plan",
1628
+ quadrant: "Quadrant",
1629
+ radar: "Radar",
1630
+ release: "Release",
1631
+ review: "Review",
1632
+ sankey: "Sankey",
1633
+ scatter: "Scatter",
1634
+ schema: "Schema",
1635
+ search: "Search",
1636
+ searches: "Searches",
1637
+ service: "Service",
1638
+ statuspage: "StatusPage",
1639
+ steps: "Steps",
1640
+ stop: "Stop",
1641
+ summary: "Summary",
1642
+ terminal: "Terminal",
1643
+ tests: "Tests",
1644
+ timeline: "Timeline",
1645
+ toc: "Toc",
1646
+ trace: "Trace",
1647
+ treemap: "Treemap",
1648
+ uptime: "Uptime",
1649
+ venn: "Venn",
1650
+ verdict: "Verdict",
1651
+ vuln: "Vuln",
1652
+ waterfall: "Waterfall"
1653
+ };
1654
+ const isDirective = (n) => "name" in n && typeof n.name === "string" && "attributes" in n && isRecord(n.attributes) && isParent(n);
1655
+ /**
1656
+ * remark-directive containers become mdxr components:
1657
+ * :::note[Optional label] → <Callout kind="note" title="Optional label">
1658
+ * :::phase{title="X" status="doing"} → <Phase title="X" status="doing">
1659
+ * :::plan / :::steps / :::summary / :::timeline → Plan / Steps / Summary / Timeline
1660
+ * :::flow / :::findings / :::finding / :::files / :::deps
1661
+ * → Flow / Findings / Finding / Files / Deps
1662
+ * :::terminal / :::trace / :::hypotheses / :::hypothesis / :::searches / :::search
1663
+ * → Terminal / Trace / Hypotheses / Hypothesis / Searches / Search
1664
+ * :::barchart / :::linechart / :::piechart / :::scatter / :::radar
1665
+ * → BarChart / LineChart / PieChart / Scatter / Radar
1666
+ * :::funnel / :::quadrant / :::bridge / :::treemap / :::sankey / :::venn
1667
+ * → Funnel / Quadrant / Bridge / Treemap / Sankey / Venn
1668
+ * :::review / :::comment / :::checks / :::check / :::audit / :::vuln
1669
+ * → Review / Comment / Checks / Check / Audit / Vuln
1670
+ * :::bumps / :::bump / :::packages / :::package
1671
+ * → Bumps / Bump / Packages / Package
1672
+ * :::gauges / :::gauge / :::benchmarks / :::bench
1673
+ * → Gauges / Gauge / Benchmarks / Bench
1674
+ * :::schema / :::dbtable / :::dbfield / :::envvars / :::envvar
1675
+ * → Schema / DbTable / DbField / EnvVars / EnvVar
1676
+ * :::release / :::entry / :::pathway / :::stop / :::incident / :::verdict
1677
+ * → Release / Entry / Pathway / Stop / Incident / Verdict
1678
+ * :::statuspage / :::service / :::uptime / :::day
1679
+ * → StatusPage / Service / Uptime / Day
1680
+ * `non-goal` is accepted as an alias of the `nongoal` callout kind.
1681
+ */
1682
+ const remarkMdxrDirectives = () => (tree, file) => {
1683
+ visit(tree, [
1684
+ "containerDirective",
1685
+ "leafDirective",
1686
+ "textDirective"
1687
+ ], (node) => {
1688
+ if (!isDirective(node)) return;
1689
+ const directive = node;
1690
+ const name = normalizeCalloutKind(directive.name);
1691
+ const isContainer = directive.type === "containerDirective";
1692
+ const container = own(CONTAINER_COMPONENTS, name);
1693
+ const component = CALLOUT_KINDS.has(name) ? "Callout" : container;
1694
+ if (component === void 0) {
1695
+ file.message(`Unknown directive ":::${directive.name}"`, directive, "mdxr:directives");
1696
+ return;
1697
+ }
1698
+ if (!isContainer) {
1699
+ file.message(`:::${directive.name} is a container directive — use three colons`, directive, "mdxr:directives");
1700
+ return;
1701
+ }
1702
+ const attrs = { ...directive.attributes };
1703
+ const [first] = directive.children;
1704
+ if (first !== void 0 && isRecord(first.data) && first.data.directiveLabel === true) {
1705
+ attrs.title ??= textContent(first).trim();
1706
+ directive.children.shift();
1707
+ }
1708
+ if (component === "Callout") toMdxElement(directive, "mdxJsxFlowElement", "Callout", {
1709
+ kind: name,
1710
+ ...attrs
1711
+ });
1712
+ else toMdxElement(directive, "mdxJsxFlowElement", component, attrs);
1713
+ });
1714
+ };
1715
+ //#endregion
1716
+ //#region src/remark/file-paths.ts
1717
+ /**
1718
+ * Inline code that names a real file path — `` `src/mdx.ts` `` — becomes a
1719
+ * `<FileRef>` chip, so prose references get the same editor link, icon, and
1720
+ * copy button as an explicit component. The value must contain a `/` (a bare
1721
+ * `file.ts` stays code) and resolve to an existing file relative to the
1722
+ * document; a trailing `:N`/`:N-M` becomes `lines`. Paths inside links are
1723
+ * left alone — a chip can't nest inside <a>. Runs late in the pipeline so
1724
+ * directive labels and heading slugs are already computed.
1725
+ */
1726
+ const remarkFilePaths = () => (tree, file) => {
1727
+ const dir = file.dirname ?? ".";
1728
+ const resolve = (value) => {
1729
+ if (!value.includes("/") || value.includes("://")) return;
1730
+ const target = splitPathLines(value);
1731
+ if (existsSync(path.resolve(dir, target.path))) return target;
1732
+ return target.lines !== void 0 && existsSync(path.resolve(dir, value)) ? { path: value } : void 0;
1733
+ };
1734
+ visitParents(tree, "inlineCode", (node, ancestors) => {
1735
+ if (ancestors.some((a) => a.type === "link" || a.type === "linkReference" || (a.type === "mdxJsxTextElement" || a.type === "mdxJsxFlowElement") && a.name === "a") || !("value" in node) || typeof node.value !== "string") return;
1736
+ const target = resolve(node.value);
1737
+ if (target === void 0) return;
1738
+ const el = node;
1739
+ toMdxElement(el, "mdxJsxTextElement", "FileRef", {
1740
+ lines: target.lines,
1741
+ path: target.path
1742
+ });
1743
+ el.children = [];
1744
+ delete el.value;
1745
+ });
1746
+ };
1747
+ //#endregion
1748
+ //#region src/remark/headings.ts
1749
+ const isHeading = (n) => n.type === "heading";
1750
+ /** GitHub-style slug: lowercase, punctuation stripped, spaces → `-`. */
1751
+ const slugify = (text) => text.toLowerCase().trim().replaceAll(/[^\p{L}\p{N}_\s-]/gu, "").replaceAll(/\s+/gu, "-");
1752
+ /** Positive-int attribute or fallback — "0"/"abc" both become `fallback`. */
1753
+ const depthAttr = (node, name, fallback) => {
1754
+ const raw = jsxAttr(node, name);
1755
+ const n = Number(raw);
1756
+ return raw === void 0 || !Number.isInteger(n) || n < 1 ? fallback : n;
1757
+ };
1758
+ /** Nest a flat heading list by depth (an h3 under the previous h2, …). */
1759
+ const nest = (headings) => {
1760
+ const roots = [];
1761
+ const stack = [];
1762
+ for (const h of headings) {
1763
+ while (stack.length > 0 && (stack.at(-1)?.depth ?? 0) >= h.depth) stack.pop();
1764
+ const parent = stack.at(-1);
1765
+ if (parent === void 0) roots.push(h);
1766
+ else parent.children.push(h);
1767
+ stack.push(h);
1768
+ }
1769
+ return roots;
1770
+ };
1771
+ const toList = (items) => ({
1772
+ children: items.map((item) => ({
1773
+ children: [{
1774
+ children: [{
1775
+ children: [{
1776
+ type: "text",
1777
+ value: item.text
1778
+ }],
1779
+ type: "link",
1780
+ url: `#${item.slug}`
1781
+ }],
1782
+ type: "paragraph"
1783
+ }, ...item.children.length > 0 ? [toList(item.children)] : []],
1784
+ spread: false,
1785
+ type: "listItem"
1786
+ })),
1787
+ ordered: false,
1788
+ spread: false,
1789
+ type: "list"
1790
+ });
1791
+ /**
1792
+ * Two jobs, one pass over the document:
1793
+ * 1. Every heading gets an `id` (GitHub-style slug, deduplicated), enabling
1794
+ * deep links.
1795
+ * 2. `<Toc>` elements (`:::toc` included) get a nested link list injected as
1796
+ * children — `depth`/`min` attributes bound the heading levels included
1797
+ * (defaults: h2–h3, i.e. min=2 depth=3).
1798
+ */
1799
+ const remarkMdxrHeadings = () => (tree) => {
1800
+ const counts = /* @__PURE__ */ new Map();
1801
+ const used = /* @__PURE__ */ new Set();
1802
+ const headings = [];
1803
+ visit(tree, "heading", (node) => {
1804
+ if (!isHeading(node)) return;
1805
+ const text = textContent(node).trim();
1806
+ const base = slugify(text) || "section";
1807
+ let count = counts.get(base) ?? 0;
1808
+ let slug = count === 0 ? base : `${base}-${count}`;
1809
+ while (used.has(slug)) {
1810
+ count += 1;
1811
+ slug = `${base}-${count}`;
1812
+ }
1813
+ counts.set(base, count + 1);
1814
+ used.add(slug);
1815
+ setHProperty(node, "id", slug);
1816
+ headings.push({
1817
+ children: [],
1818
+ depth: node.depth,
1819
+ slug,
1820
+ text
1821
+ });
1822
+ });
1823
+ visit(tree, "mdxJsxFlowElement", (node) => {
1824
+ if (!isFlowElement(node) || node.name !== "Toc") return;
1825
+ const minDepth = depthAttr(node, "min", 2);
1826
+ const maxDepth = depthAttr(node, "depth", 3);
1827
+ const items = headings.filter((h) => h.depth >= minDepth && h.depth <= maxDepth && h.text !== "");
1828
+ node.children = items.length === 0 ? [] : [toList(nest(items))];
1829
+ });
1830
+ };
1831
+ //#endregion
1832
+ //#region src/remark/no-js.ts
1833
+ const JS_NODES = [
1834
+ "mdxjsEsm",
1835
+ "mdxFlowExpression",
1836
+ "mdxTextExpression"
1837
+ ];
1838
+ const JSX_ELEMENTS = ["mdxJsxFlowElement", "mdxJsxTextElement"];
1839
+ const MESSAGE = "JavaScript expressions and import/export statements are not allowed in mdxr documents. Add a component via mdxr.config.ts instead.";
1840
+ /**
1841
+ * mdxr documents are data, not code: reject ESM imports/exports and JS
1842
+ * expressions (`{...}`) — including inside JSX attributes (`prop={x}`,
1843
+ * `{...spread}`), which `visit` never reaches because attributes are not
1844
+ * children. Extensibility happens through mdxr.config.ts, not through
1845
+ * executable markup inside the document.
1846
+ */
1847
+ const remarkNoJs = () => (tree, file) => {
1848
+ visit(tree, JS_NODES, (node) => {
1849
+ file.fail(MESSAGE, node, "mdxr:no-js");
1850
+ });
1851
+ visit(tree, JSX_ELEMENTS, (node) => {
1852
+ if (!("attributes" in node) || !Array.isArray(node.attributes)) return;
1853
+ for (const attr of node.attributes) if (isRecord(attr) && (attr.type !== "mdxJsxAttribute" || attr.value !== null && typeof attr.value !== "string")) file.fail(MESSAGE, node, "mdxr:no-js");
1854
+ });
1855
+ };
1856
+ //#endregion
1857
+ //#region src/mdx.ts
1858
+ /**
1859
+ * Which catalog entries the document references is visible in the compiled
1860
+ * module itself: JSX identifiers become `_missingMdxReference("Name", …)`
1861
+ * checks, markdown element overrides read `_components.name`, and a `wrapper`
1862
+ * entry is picked straight off `props.components`. (Spreading `components`
1863
+ * into `_components` makes runtime tracking see every key — hence static
1864
+ * extraction here instead.)
1865
+ */
1866
+ const extractUsedComponents = (code, components) => {
1867
+ const catalogKeys = new Set(Object.keys(components));
1868
+ const used = /* @__PURE__ */ new Set();
1869
+ for (const m of code.matchAll(/_missingMdxReference\("(?<name>[^"]+)"/gu)) {
1870
+ const name = m.groups?.name;
1871
+ if (name !== void 0 && catalogKeys.has(name)) used.add(name);
1872
+ }
1873
+ for (const m of code.matchAll(/_components\.(?<dot>\w+)|_components\["(?<bracket>[^"]+)"\]/gu)) {
1874
+ const name = m.groups?.dot ?? m.groups?.bracket;
1875
+ if (name !== void 0 && catalogKeys.has(name)) used.add(name);
1876
+ }
1877
+ if (catalogKeys.has("wrapper")) used.add("wrapper");
1878
+ return [...used];
1879
+ };
1880
+ const mdxToHtml = async (source, components, filePath = "document.mdx", opts = {}) => {
1881
+ const file = new VFile({
1882
+ path: filePath,
1883
+ value: source
1884
+ });
1885
+ matter(file);
1886
+ const fmRaw = isRecord(file.data) ? file.data.matter : void 0;
1887
+ const frontmatter = isRecord(fmRaw) ? fmRaw : {};
1888
+ const editor = typeof frontmatter.editor === "string" && frontmatter.editor !== "" ? frontmatter.editor : opts.editor;
1889
+ const dir = file.dirname ?? ".";
1890
+ const fileLinks = /* @__PURE__ */ new Map();
1891
+ const fileLink = (rel, line) => {
1892
+ const abs = path.resolve(dir, rel);
1893
+ const url = existsSync(abs) ? editorUrl(editor, abs, line) : void 0;
1894
+ if (url !== void 0) fileLinks.set(`${rel}\0${line ?? ""}`, url);
1895
+ return url;
1896
+ };
1897
+ const compiled = await compile(file, {
1898
+ baseUrl: import.meta.url,
1899
+ format: "mdx",
1900
+ rehypePlugins: [rehypeKatex, rehypeShiki],
1901
+ remarkPlugins: [
1902
+ remarkFrontmatter,
1903
+ remarkGfm,
1904
+ remarkMath,
1905
+ remarkDirective,
1906
+ remarkMdxrDirectives,
1907
+ remarkMdxrAlerts,
1908
+ remarkNoJs,
1909
+ remarkMdxrHeadings,
1910
+ remarkCodeFile,
1911
+ remarkCodeMeta,
1912
+ remarkFilePaths
1913
+ ]
1914
+ });
1915
+ for (const m of compiled.messages) process.stderr.write(`mdxr: warning: ${formatError(m)}\n`);
1916
+ const code = String(compiled);
1917
+ const mod = await importBundledCode(code, "doc");
1918
+ const used = extractUsedComponents(code, components);
1919
+ const docComponent = mod.default;
1920
+ if (!isComponent(docComponent)) throw new Error("Compiled document has no default export component.");
1921
+ takeUsedIcons();
1922
+ const renderedAt = /* @__PURE__ */ new Date();
1923
+ const context = {
1924
+ fileLink,
1925
+ now: renderedAt
1926
+ };
1927
+ const hydrated = (opts.hydrate ?? true) && used.length > 0;
1928
+ const renderToMarkup = hydrated ? renderToString : renderToStaticMarkup;
1929
+ const renderDocument = (header) => renderToMarkup(createElement(DocContext.Provider, { value: context }, createElement(Fragment, null, header, createElement(docComponent, { components }))));
1930
+ let body;
1931
+ try {
1932
+ body = renderDocument(null);
1933
+ } catch (error) {
1934
+ throw enhanceRenderError(error, Object.keys(components));
1935
+ }
1936
+ return {
1937
+ body,
1938
+ code,
1939
+ context,
1940
+ fileLinks: Object.fromEntries(fileLinks),
1941
+ frontmatter,
1942
+ hydrated,
1943
+ renderWithHeader: (header) => {
1944
+ try {
1945
+ const html = renderDocument(header);
1946
+ return {
1947
+ fileLinks: Object.fromEntries(fileLinks),
1948
+ html,
1949
+ usedIcons: takeUsedIcons()
1950
+ };
1951
+ } catch (error) {
1952
+ throw enhanceRenderError(error, Object.keys(components));
1953
+ }
1954
+ },
1955
+ renderedAt: renderedAt.toISOString(),
1956
+ usedComponents: used,
1957
+ usedIcons: takeUsedIcons()
1958
+ };
1959
+ };
1960
+ //#endregion
1961
+ //#region src/assets/css.ts
1962
+ /** Base CSS appended after Tailwind utilities (covers what utilities can't). */
1963
+ const BASE_CSS = `
1964
+ /* Preflight makes <svg> display:block, which splits inline text around
1965
+ * icons; Iconify svgs (.iconify) and lucide-react svgs (.lucide) stay inline. */
1966
+ .iconify, .lucide { display: inline-block; }
1967
+ .task-list-item { list-style: none; }
1968
+ ul.contains-task-list { padding-left: 1.25rem; }
1969
+ .task-list-item input[type='checkbox'] { margin-right: 0.4em; }
1970
+ /* --- Interaction feedback -------------------------------------------
1971
+ * Every [data-copy] button carries an idle and a done icon
1972
+ * (.mdxr-copy-idle/.mdxr-copy-done); the delegated event handler
1973
+ * (src/client, also bound in the Storybook preview) toggles
1974
+ * .copied/.copy-failed for ~1.6s after each clipboard attempt. Success
1975
+ * swaps the copy icon for an emerald check with a pop; failure shakes and
1976
+ * tints red; pressing the button scales it down briefly. */
1977
+ .mdxr-copy {
1978
+ display: inline-flex; align-items: center; justify-content: center;
1979
+ border-radius: 0.25rem;
1980
+ transition: opacity 0.15s ease, color 0.15s ease, transform 0.1s ease;
1981
+ }
1982
+ .mdxr-copy:hover { opacity: 1; }
1983
+ .mdxr-copy:active { transform: scale(0.82); }
1984
+ .mdxr-copy:focus-visible {
1985
+ outline: 2px solid rgb(14 165 233); outline-offset: 1px; opacity: 1;
1986
+ }
1987
+ .mdxr-copy.copied, .mdxr-copy.copy-failed { opacity: 1; }
1988
+ .mdxr-copy.copy-failed { color: rgb(220 38 38); }
1989
+ .dark .mdxr-copy.copy-failed { color: rgb(248 113 113); }
1990
+ /* Theme toggle: fixed corner chrome injected by htmlDocument. Cycles
1991
+ * auto → light → dark; [data-mode] picks which icon shows. */
1992
+ .mdxr-theme {
1993
+ position: fixed; top: 0.75rem; right: 0.75rem; z-index: 50;
1994
+ display: inline-flex; align-items: center; justify-content: center;
1995
+ height: 2rem; width: 2rem; margin: 0; padding: 0; border-radius: 9999px;
1996
+ border: 1px solid rgb(229 229 229); color: rgb(82 82 82);
1997
+ background: rgb(255 255 255 / 0.85); backdrop-filter: blur(8px);
1998
+ cursor: pointer;
1999
+ transition: color 0.15s ease, background-color 0.15s ease,
2000
+ border-color 0.15s ease, transform 0.1s ease;
2001
+ }
2002
+ .mdxr-theme:hover { color: rgb(23 23 23); background: rgb(255 255 255); }
2003
+ .mdxr-theme:active { transform: scale(0.88); }
2004
+ .mdxr-theme:focus-visible {
2005
+ outline: 2px solid rgb(14 165 233); outline-offset: 2px;
2006
+ }
2007
+ .dark .mdxr-theme {
2008
+ border-color: rgb(64 64 64); color: rgb(163 163 163);
2009
+ background: rgb(23 23 23 / 0.85);
2010
+ }
2011
+ .dark .mdxr-theme:hover { color: rgb(250 250 250); background: rgb(23 23 23); }
2012
+ .mdxr-theme .mdxr-theme-i { display: none; }
2013
+ .mdxr-theme[data-mode="auto"] .mdxr-theme-i-auto,
2014
+ .mdxr-theme[data-mode="light"] .mdxr-theme-i-light,
2015
+ .mdxr-theme[data-mode="dark"] .mdxr-theme-i-dark {
2016
+ display: inline-flex;
2017
+ animation: mdxr-pop 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
2018
+ }
2019
+ .mdxr-theme svg { height: 1rem; width: 1rem; }
2020
+ @media print { .mdxr-theme { display: none; } }
2021
+ .mdxr-copy-done { display: none; }
2022
+ .copied .mdxr-copy-idle { display: none; }
2023
+ .copied .mdxr-copy-done {
2024
+ display: inline-flex;
2025
+ animation: mdxr-pop 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
2026
+ }
2027
+ .copy-failed { animation: mdxr-shake 0.32s ease; }
2028
+ [data-ask-copy].copy-failed, [data-ask-save].copy-failed {
2029
+ border-color: rgb(248 113 113); color: rgb(220 38 38);
2030
+ }
2031
+ .dark [data-ask-copy].copy-failed, .dark [data-ask-save].copy-failed {
2032
+ color: rgb(248 113 113);
2033
+ }
2034
+ .mdxr-ask [data-ask-copy].copied, .mdxr-ask [data-ask-save].copied {
2035
+ border-color: rgb(52 211 153 / 0.6);
2036
+ }
2037
+ /* Board: cards drag between lanes (and reorder within one). .mdxr-drag
2038
+ * marks the card being held; .mdxr-drop-before (on a card) and
2039
+ * .mdxr-drop-end (on the lane's card box) draw the insertion line;
2040
+ * .mdxr-drop-lane outlines the target lane. The ‹ › move buttons stay
2041
+ * visible at low opacity — they're the touch/keyboard path (HTML5 DnD
2042
+ * never reaches touch browsers). */
2043
+ [data-board-card] { cursor: grab; }
2044
+ [data-board-card].mdxr-drag { cursor: grabbing; opacity: 0.4; }
2045
+ [data-board-card].mdxr-drop-before { box-shadow: 0 -2px 0 0 rgb(14 165 233); }
2046
+ [data-board-cards].mdxr-drop-end {
2047
+ box-shadow: inset 0 -2px 0 0 rgb(14 165 233);
2048
+ border-radius: 0.375rem;
2049
+ }
2050
+ [data-board-lane].mdxr-drop-lane {
2051
+ outline: 2px dashed rgb(14 165 233 / 0.5);
2052
+ outline-offset: -2px;
2053
+ }
2054
+ .mdxr-move {
2055
+ display: inline-flex; align-items: center; justify-content: center;
2056
+ height: 1.25rem; width: 1.25rem; border: 0; padding: 0;
2057
+ border-radius: 0.25rem; background: transparent;
2058
+ color: rgb(163 163 163); cursor: pointer; opacity: 0.6;
2059
+ transition: opacity 0.15s ease, color 0.15s ease,
2060
+ background-color 0.15s ease, transform 0.1s ease;
2061
+ }
2062
+ .mdxr-move:not(:disabled):hover {
2063
+ opacity: 1; color: rgb(64 64 64); background: rgb(245 245 245);
2064
+ }
2065
+ .dark .mdxr-move:not(:disabled):hover {
2066
+ color: rgb(212 212 212); background: rgb(38 38 38);
2067
+ }
2068
+ .mdxr-move:not(:disabled):active { transform: scale(0.85); }
2069
+ .mdxr-move:focus-visible {
2070
+ outline: 2px solid rgb(14 165 233); outline-offset: 1px; opacity: 1;
2071
+ }
2072
+ .mdxr-move:disabled { opacity: 0.2; cursor: default; }
2073
+ @media print {
2074
+ .mdxr-card-moves, .mdxr-board-tools, .mdxr-grip { display: none; }
2075
+ }
2076
+ /* <Comments>: a hover "+" on every code/diff row opens the comment form
2077
+ * under that line. The button sits in the code block's left bleed band
2078
+ * (.mdxr-cline — its .line child keeps the -1rem band bleed) or over the
2079
+ * diff row's line-number gutter (.mdxr-drow); invisible until the row is
2080
+ * hovered or the button takes keyboard focus. */
2081
+ .mdxr-cline, .mdxr-drow { position: relative; }
2082
+ .mdxr-add {
2083
+ position: absolute; top: 0; bottom: 0; z-index: 1;
2084
+ display: flex; align-items: center; justify-content: center;
2085
+ width: 1rem; border: 0; padding: 0; background: transparent;
2086
+ color: rgb(163 163 163); cursor: pointer; opacity: 0;
2087
+ transition: opacity 0.12s ease, color 0.12s ease;
2088
+ }
2089
+ .mdxr-cline > .mdxr-add { left: -1rem; }
2090
+ .mdxr-drow > .mdxr-add { left: 0.1rem; }
2091
+ .mdxr-cline:hover > .mdxr-add, .mdxr-drow:hover > .mdxr-add,
2092
+ .mdxr-add:focus-visible { opacity: 1; }
2093
+ .mdxr-add:hover { color: rgb(2 132 199); }
2094
+ .dark .mdxr-add { color: rgb(115 115 115); }
2095
+ .dark .mdxr-add:hover { color: rgb(56 189 248); }
2096
+ @media print {
2097
+ .mdxr-add, .mdxr-thread-tools, [data-comment-form] { display: none; }
2098
+ }
2099
+ @keyframes mdxr-pop {
2100
+ 0% { transform: scale(0.3); opacity: 0; }
2101
+ 70% { transform: scale(1.15); }
2102
+ 100% { transform: scale(1); opacity: 1; }
2103
+ }
2104
+ @keyframes mdxr-shake {
2105
+ 0%, 100% { transform: translateX(0); }
2106
+ 25% { transform: translateX(-2px); }
2107
+ 75% { transform: translateX(2px); }
2108
+ }
2109
+ /* shiki dual-theme: token spans carry --shiki-* variables, not colors.
2110
+ * Scoping to .line descendants keeps spans that share a .shiki ancestor for
2111
+ * layout (e.g. <Comments> thread strips) from losing their own colors to an
2112
+ * undefined --shiki-* var. .mdxr-diff-hl marks the same kind of token span
2113
+ * inside DiffView rows — it is not nested in a .shiki code element, so it
2114
+ * joins the selectors here. */
2115
+ .shiki .line span, .mdxr-diff-hl {
2116
+ color: var(--shiki-light);
2117
+ font-style: var(--shiki-light-font-style, normal);
2118
+ font-weight: var(--shiki-light-font-weight, normal);
2119
+ text-decoration: var(--shiki-light-text-decoration, none);
2120
+ }
2121
+ @media (prefers-color-scheme: dark) {
2122
+ .shiki .line span, .mdxr-diff-hl {
2123
+ color: var(--shiki-dark);
2124
+ font-style: var(--shiki-dark-font-style, normal);
2125
+ font-weight: var(--shiki-dark-font-weight, normal);
2126
+ text-decoration: var(--shiki-dark-text-decoration, none);
2127
+ }
2128
+ }
2129
+ /* The .dark class (set by THEME_JS in documents, the toolbar in Storybook)
2130
+ * wins over the media query so toggling it re-themes code blocks too. */
2131
+ .dark .shiki .line span, .dark .mdxr-diff-hl {
2132
+ color: var(--shiki-dark);
2133
+ font-style: var(--shiki-dark-font-style, normal);
2134
+ font-weight: var(--shiki-dark-font-weight, normal);
2135
+ text-decoration: var(--shiki-dark-text-decoration, none);
2136
+ }
2137
+ html:not(.dark) .shiki .line span, html:not(.dark) .mdxr-diff-hl {
2138
+ color: var(--shiki-light);
2139
+ font-style: var(--shiki-light-font-style, normal);
2140
+ font-weight: var(--shiki-light-font-weight, normal);
2141
+ text-decoration: var(--shiki-light-text-decoration, none);
2142
+ }
2143
+ /* Line-level features: fences carry has-* classes on <code> itself. The code
2144
+ * is a column flex box so .line spans become block-level items (bands/numbers
2145
+ * span the block width) while the raw "\n" text nodes shiki leaves between
2146
+ * them collapse — whitespace-only anonymous flex items never render, so they
2147
+ * can't double the line spacing under the pre's white-space: pre. The pre's
2148
+ * p-4 (1rem) is mirrored here so highlight bands bleed to the block edge;
2149
+ * min-height keeps empty .line spans from collapsing to zero. */
2150
+ .shiki { display: flex; flex-direction: column; }
2151
+ .shiki .line { display: block; min-height: 1lh; margin: 0 -1rem; padding: 0 1rem; }
2152
+ .shiki .line.highlighted { background: rgba(14, 165, 233, 0.1); }
2153
+ .dark .shiki .line.highlighted { background: rgba(14, 165, 233, 0.16); }
2154
+ .shiki .line.diff.add { background: rgba(16, 185, 129, 0.12); }
2155
+ .shiki .line.diff.remove { background: rgba(239, 68, 68, 0.1); }
2156
+ .dark .shiki .line.diff.add { background: rgba(16, 185, 129, 0.18); }
2157
+ .dark .shiki .line.diff.remove { background: rgba(239, 68, 68, 0.16); }
2158
+ .shiki .line.diff.add::before,
2159
+ .shiki .line.diff.remove::before {
2160
+ display: inline-block; width: 1em; margin-right: 0.5em;
2161
+ color: rgba(113, 113, 122, 0.8); user-select: none;
2162
+ }
2163
+ .shiki .line.diff.add::before { content: '+'; }
2164
+ .shiki .line.diff.remove::before { content: '−'; }
2165
+ .shiki .line.error { background: rgba(239, 68, 68, 0.12); }
2166
+ .shiki .line.warning { background: rgba(245, 158, 11, 0.14); }
2167
+ .dark .shiki .line.error { background: rgba(239, 68, 68, 0.2); }
2168
+ .dark .shiki .line.warning { background: rgba(245, 158, 11, 0.18); }
2169
+ .shiki.has-focused .line:not(.focused) { opacity: 0.4; }
2170
+ .shiki .line.focused { opacity: 1; }
2171
+ .shiki span.highlighted-word {
2172
+ background: rgba(245, 158, 11, 0.18); border-radius: 0.25rem;
2173
+ outline: 1px solid rgba(245, 158, 11, 0.35); padding: 0 0.1rem;
2174
+ }
2175
+ .shiki.has-line-numbers { counter-reset: mdxr-line; }
2176
+ .shiki.has-line-numbers .line::before {
2177
+ counter-increment: mdxr-line; content: counter(mdxr-line);
2178
+ display: inline-block; width: 1.8em; margin-right: 1em;
2179
+ text-align: right; color: rgba(113, 113, 122, 0.6); user-select: none;
2180
+ }
2181
+ .shiki.has-line-numbers .line.diff.add::before,
2182
+ .shiki.has-line-numbers .line.diff.remove::before {
2183
+ content: counter(mdxr-line); /* numbers win over the +/− gutter marker */
2184
+ }
2185
+ /* KaTeX display math gets a little breathing room. */
2186
+ .katex-display { margin: 1.25rem 0; }
2187
+ /* Smooth anchor jumps for the ToC and other in-page links. */
2188
+ html { scroll-behavior: smooth; }
2189
+ /* Native <details> used by Toc/Details/Tree/Json: drop the default marker,
2190
+ * rotate the chevron on open. */
2191
+ .mdxr-toc > details > summary::-webkit-details-marker,
2192
+ .mdxr-details > summary::-webkit-details-marker,
2193
+ .mdxr-tree summary::-webkit-details-marker,
2194
+ .mdxr-json summary::-webkit-details-marker { display: none; }
2195
+ .mdxr-toc > details > summary::marker,
2196
+ .mdxr-details > summary::marker,
2197
+ .mdxr-tree summary::marker,
2198
+ .mdxr-json summary::marker { content: ""; }
2199
+ /* Json: the "N keys/items" badge only matters while the node is folded. */
2200
+ .mdxr-json details[open] > summary .mdxr-count { display: none; }
2201
+ .mdxr-chev { transition: transform 0.15s ease; }
2202
+ details[open] > summary .mdxr-chev { transform: rotate(90deg); }
2203
+ /* Smooth expand/collapse for the native <details> blocks (Details, Toc,
2204
+ * Tree folders). Chromium animates block-size via ::details-content +
2205
+ * interpolate-size; engines without the pseudo-element never match these
2206
+ * rules and keep the instant toggle. */
2207
+ :root { interpolate-size: allow-keywords; }
2208
+ .mdxr-details::details-content,
2209
+ .mdxr-toc > details::details-content,
2210
+ .mdxr-tree details::details-content {
2211
+ block-size: 0;
2212
+ overflow-y: clip;
2213
+ transition:
2214
+ content-visibility 0.22s allow-discrete,
2215
+ block-size 0.22s ease;
2216
+ }
2217
+ .mdxr-details[open]::details-content,
2218
+ .mdxr-toc > details[open]::details-content,
2219
+ .mdxr-tree details[open]::details-content {
2220
+ block-size: auto;
2221
+ block-size: calc-size(auto);
2222
+ }
2223
+ /* ToC outline: numbered top-level entries, guide-lined nested lists. */
2224
+ .mdxr-toc-body ul { list-style: none; margin: 0; padding: 0; }
2225
+ .mdxr-toc-body li > p { margin: 0; }
2226
+ .mdxr-toc-body a {
2227
+ display: flex; align-items: baseline; gap: 0.55rem;
2228
+ border-radius: 0.375rem; padding: 0.28rem 0.5rem;
2229
+ font-size: 0.875rem; line-height: 1.45;
2230
+ color: rgb(82 82 82); text-decoration: none;
2231
+ transition: color 0.12s, background 0.12s;
2232
+ }
2233
+ .mdxr-toc-body a:hover { color: rgb(23 23 23); background: rgb(245 245 245); }
2234
+ .mdxr-toc-body a:active { background: rgb(229 229 229); }
2235
+ .dark .mdxr-toc-body a:active { background: rgb(64 64 64); }
2236
+ .dark .mdxr-toc-body a { color: rgb(163 163 163); }
2237
+ .dark .mdxr-toc-body a:hover { color: rgb(250 250 250); background: rgb(38 38 38); }
2238
+ .mdxr-toc-body > ul { counter-reset: mdxr-toc; }
2239
+ .mdxr-toc-body > ul > li { counter-increment: mdxr-toc; }
2240
+ .mdxr-toc-body > ul > li > p > a,
2241
+ .mdxr-toc-body > ul > li > a { font-weight: 500; color: rgb(64 64 64); }
2242
+ .dark .mdxr-toc-body > ul > li > p > a,
2243
+ .dark .mdxr-toc-body > ul > li > a { color: rgb(212 212 212); }
2244
+ .mdxr-toc-body > ul > li > p > a::before,
2245
+ .mdxr-toc-body > ul > li > a::before {
2246
+ content: counter(mdxr-toc); min-width: 1em;
2247
+ font-size: 0.72rem; font-weight: 400; font-variant-numeric: tabular-nums;
2248
+ color: rgb(163 163 163);
2249
+ }
2250
+ .dark .mdxr-toc-body > ul > li > p > a::before,
2251
+ .dark .mdxr-toc-body > ul > li > a::before { color: rgb(115 115 115); }
2252
+ .mdxr-toc-body ul ul {
2253
+ margin: 0.15rem 0 0.3rem 0.95rem; padding-left: 0.6rem;
2254
+ border-left: 1px solid rgb(229 229 229);
2255
+ }
2256
+ .dark .mdxr-toc-body ul ul { border-color: rgb(64 64 64); }
2257
+ .mdxr-toc-body ul ul a { font-size: 0.8125rem; padding: 0.2rem 0.45rem; }
2258
+ /* Ask: native form controls stay interactive without hydration. The real
2259
+ * inputs are visually hidden; state is styled through :checked/~ siblings. */
2260
+ .mdxr-choice {
2261
+ transition: color 0.12s, background-color 0.12s, border-color 0.12s,
2262
+ transform 0.1s ease;
2263
+ }
2264
+ .mdxr-choice:active { transform: scale(0.985); }
2265
+ .mdxr-choice:has(:checked) {
2266
+ border-color: rgb(23 23 23); background: rgb(250 250 250);
2267
+ }
2268
+ .dark .mdxr-choice:has(:checked) {
2269
+ border-color: rgb(163 163 163); background: rgb(38 38 38 / 0.35);
2270
+ }
2271
+ .mdxr-choice:has(:focus-visible),
2272
+ .mdxr-q:has(:focus-visible) input ~ .mdxr-switch {
2273
+ outline: 2px solid rgb(14 165 233); outline-offset: 1px;
2274
+ }
2275
+ .mdxr-mark {
2276
+ display: grid; place-items: center; flex-shrink: 0;
2277
+ height: 1rem; width: 1rem; margin-top: 0.15rem;
2278
+ border: 1px solid rgb(212 212 212); background: rgb(255 255 255);
2279
+ color: transparent; transition: all 0.12s;
2280
+ }
2281
+ .mdxr-mark-box { border-radius: 0.25rem; }
2282
+ .mdxr-mark-radio { border-radius: 9999px; }
2283
+ .mdxr-mark-radio::after {
2284
+ content: ""; width: 0.45rem; height: 0.45rem; border-radius: 9999px;
2285
+ background: rgb(23 23 23); transform: scale(0);
2286
+ transition: transform 0.12s;
2287
+ }
2288
+ .mdxr-choice input:checked ~ .mdxr-mark { border-color: rgb(23 23 23); }
2289
+ .mdxr-choice input:checked ~ .mdxr-mark-box {
2290
+ background: rgb(23 23 23); color: rgb(255 255 255);
2291
+ }
2292
+ /* Checkbox glyph pops in instead of fading. */
2293
+ .mdxr-mark-box svg {
2294
+ transform: scale(0.4);
2295
+ transition: transform 0.16s cubic-bezier(0.34, 1.56, 0.64, 1);
2296
+ }
2297
+ .mdxr-choice input:checked ~ .mdxr-mark-box svg { transform: scale(1); }
2298
+ .mdxr-choice input:checked ~ .mdxr-mark-radio::after { transform: scale(1); }
2299
+ .dark .mdxr-mark { border-color: rgb(82 82 82); background: rgb(23 23 23); }
2300
+ .dark .mdxr-mark-radio::after { background: rgb(250 250 250); }
2301
+ .dark .mdxr-choice input:checked ~ .mdxr-mark { border-color: rgb(250 250 250); }
2302
+ .dark .mdxr-choice input:checked ~ .mdxr-mark-box {
2303
+ background: rgb(250 250 250); color: rgb(23 23 23);
2304
+ }
2305
+ /* Toggle questions: a native checkbox styled as a switch. */
2306
+ .mdxr-switch {
2307
+ position: relative; flex-shrink: 0;
2308
+ height: 1.25rem; width: 2.25rem; border-radius: 9999px;
2309
+ background: rgb(212 212 212); transition: background 0.15s;
2310
+ }
2311
+ .mdxr-switch::after {
2312
+ content: ""; position: absolute; top: 0.125rem; left: 0.125rem;
2313
+ height: 1rem; width: 1rem; border-radius: 9999px;
2314
+ background: rgb(255 255 255); box-shadow: 0 1px 2px rgb(0 0 0 / 0.25);
2315
+ transition: transform 0.15s, width 0.15s;
2316
+ }
2317
+ .mdxr-q input:checked ~ .mdxr-switch { background: rgb(23 23 23); }
2318
+ .mdxr-q input:checked ~ .mdxr-switch::after { transform: translateX(1rem); }
2319
+ /* Thumb stretches while the switch is held (iOS-style press feedback). */
2320
+ .mdxr-q:active .mdxr-switch::after { width: 1.25rem; }
2321
+ .mdxr-q:active input:checked ~ .mdxr-switch::after {
2322
+ transform: translateX(0.875rem);
2323
+ }
2324
+ .dark .mdxr-switch { background: rgb(64 64 64); }
2325
+ .dark .mdxr-q input:checked ~ .mdxr-switch { background: rgb(245 245 245); }
2326
+ .dark .mdxr-q input:checked ~ .mdxr-switch::after { background: rgb(23 23 23); }
2327
+ /* Reduced motion: every animation/transition above becomes instant. */
2328
+ @media (prefers-reduced-motion: reduce) {
2329
+ html { scroll-behavior: auto; }
2330
+ .mdxr-add,
2331
+ .mdxr-copy,
2332
+ .mdxr-chev,
2333
+ .mdxr-choice,
2334
+ .mdxr-mark,
2335
+ .mdxr-mark-box svg,
2336
+ .mdxr-move,
2337
+ .mdxr-switch,
2338
+ .mdxr-switch::after,
2339
+ .mdxr-theme,
2340
+ .mdxr-toc-body a {
2341
+ transition: none;
2342
+ }
2343
+ .mdxr-copy:active { transform: none; }
2344
+ .mdxr-choice:active { transform: none; }
2345
+ .mdxr-move:active { transform: none; }
2346
+ .mdxr-theme:active { transform: none; }
2347
+ .copied .mdxr-copy-done, .copy-failed { animation: none; }
2348
+ .mdxr-theme .mdxr-theme-i { animation: none; }
2349
+ .mdxr-details::details-content,
2350
+ .mdxr-toc > details::details-content,
2351
+ .mdxr-tree details::details-content { transition: none; }
2352
+ }
2353
+ `;
2354
+ //#endregion
2355
+ //#region src/tailwind.ts
2356
+ const require = module.createRequire(import.meta.url);
2357
+ const tailwindDir = path.dirname(require.resolve("tailwindcss/package.json"));
2358
+ /** Resolve `id` against the document dir then this package; false if absent. */
2359
+ const tryResolve = async (id, base) => {
2360
+ try {
2361
+ const resolved = require.resolve(id, { paths: [base, pkgRoot] });
2362
+ await access(resolved);
2363
+ return resolved;
2364
+ } catch {
2365
+ return false;
2366
+ }
2367
+ };
2368
+ const resolveCss = async (id, base) => {
2369
+ if (path.isAbsolute(id)) return id;
2370
+ if (id === "tailwindcss") return path.join(tailwindDir, "index.css");
2371
+ if (id.startsWith("tailwindcss/")) return path.join(tailwindDir, id.slice(12));
2372
+ if (id.startsWith(".")) return path.resolve(base, id);
2373
+ return await tryResolve(id, base);
2374
+ };
2375
+ /**
2376
+ * React escapes `&` `'` `"` `<` `>` inside rendered attributes. The scanner
2377
+ * must see the literal class text, so decode the entities React emits.
2378
+ */
2379
+ const decodeEntities = (html) => html.replaceAll("&#x27;", "'").replaceAll("&quot;", "\"").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
2380
+ /** `@import "<path>"` — quoted, with Windows separators normalized. */
2381
+ const importLine = (absPath) => `@import ${JSON.stringify(absPath.replaceAll("\\", "/"))};`;
2382
+ /**
2383
+ * Compile Tailwind v4 CSS covering every class candidate found in `sources`
2384
+ * (the rendered HTML, the .mdx source, bundled user components, our own dist).
2385
+ * `themePath` is imported by path — not inlined — so `@import "./x.css"`
2386
+ * inside a theme file resolves relative to the theme, and the file lands in
2387
+ * `dependencies` (which `mdxr serve` turns into watch targets).
2388
+ * Returns minified CSS ready to inline into the HTML document.
2389
+ */
2390
+ const buildCss = async (sources, themePath) => {
2391
+ const input = [
2392
+ "@import \"./src/styles/globals.css\";",
2393
+ "@plugin \"@tailwindcss/typography\";",
2394
+ "@plugin \"@iconify/tailwind4\";",
2395
+ BASE_CSS,
2396
+ themePath === void 0 ? "" : importLine(themePath)
2397
+ ].join("\n");
2398
+ const dependencies = /* @__PURE__ */ new Set();
2399
+ const compiler = await compile$1(input, {
2400
+ base: pkgRoot,
2401
+ customCssResolver: resolveCss,
2402
+ customJsResolver: tryResolve,
2403
+ onDependency: (file) => {
2404
+ dependencies.add(file);
2405
+ }
2406
+ });
2407
+ const candidates = new Scanner({}).scanFiles(sources.map((s) => ({
2408
+ content: s.extension === "html" ? decodeEntities(s.content) : s.content,
2409
+ extension: s.extension
2410
+ })));
2411
+ const css = compiler.build(candidates);
2412
+ return {
2413
+ css: optimize(css, { minify: true }).code,
2414
+ dependencies: [...dependencies]
2415
+ };
2416
+ };
2417
+ //#endregion
2418
+ //#region src/render.ts
2419
+ /** Load a user components module (file or directory with an index file). */
2420
+ const loadComponents = async (componentsPath) => {
2421
+ const entry = resolveModuleEntry(componentsPath);
2422
+ const { module: mod, code } = await loadUserModule(entry);
2423
+ return {
2424
+ code,
2425
+ components: mergeUserComponents(mod)
2426
+ };
2427
+ };
2428
+ /** Components named by `config.componentsPath` — an empty map when unset. */
2429
+ const loadUserComponents = async (config) => config.componentsPath === void 0 ? { components: {} } : await loadComponents(config.componentsPath);
2430
+ /** Read all of our own shipped JS so Tailwind can scan built-in classes.
2431
+ * Memoized: package sources don't change within a process (serve rebuilds
2432
+ * would otherwise rescan dist+src on every keystroke). */
2433
+ let ownSourcesCache;
2434
+ const ownSources = async () => {
2435
+ if (ownSourcesCache !== void 0) return ownSourcesCache;
2436
+ const dirs = [path.join(pkgRoot, "dist"), path.join(pkgRoot, "src")];
2437
+ const out = [];
2438
+ const walk = async (d) => {
2439
+ const ents = await readdir(d, { withFileTypes: true });
2440
+ await Promise.all(ents.map(async (e) => {
2441
+ const p = path.join(d, e.name);
2442
+ if (e.isDirectory()) await walk(p);
2443
+ else if (/\.(?:js|ts|tsx)$/u.test(e.name)) out.push({
2444
+ content: await readFile(p, "utf-8"),
2445
+ extension: e.name.split(".").pop() ?? ""
2446
+ });
2447
+ }));
2448
+ };
2449
+ await Promise.all(dirs.filter((d) => existsSync(d)).map(walk));
2450
+ ownSourcesCache = out;
2451
+ return out;
2452
+ };
2453
+ /**
2454
+ * Inline client bundle for hydration. No catalog component was read → nothing
2455
+ * in the document can hydrate, so the bundle is skipped entirely (pure
2456
+ * markdown docs stay lean). A bundle failure degrades to the (correct)
2457
+ * static output with a warning.
2458
+ */
2459
+ const buildHydrateBundle = async (args) => {
2460
+ if (!(args.hydrate ?? true) || args.usedComponents.length === 0) return;
2461
+ try {
2462
+ return await buildHydrateScript({
2463
+ code: args.code,
2464
+ componentsPath: args.config.componentsPath === void 0 ? void 0 : resolveModuleEntry(args.config.componentsPath),
2465
+ fileLinks: args.fileLinks,
2466
+ header: args.headerProps,
2467
+ now: args.now,
2468
+ usedComponents: args.usedComponents,
2469
+ usedIcons: args.usedIcons
2470
+ });
2471
+ } catch (error) {
2472
+ process.stderr.write(`mdxr: hydration bundle skipped: ${formatError(error)}\n`);
2473
+ return;
2474
+ }
2475
+ };
2476
+ /**
2477
+ * Frontmatter `status` is free-form YAML, not a JSX prop — normalize case
2478
+ * ("Doing" → "doing") and warn+drop anything outside STATUSES so a metadata
2479
+ * typo can't take down the whole document render.
2480
+ */
2481
+ const headerStatus = (raw) => {
2482
+ if (raw === void 0) return;
2483
+ const norm = raw.trim().toLowerCase();
2484
+ if (isStatus(norm)) return norm;
2485
+ process.stderr.write(`mdxr: warning: frontmatter status "${raw}" is not one of ${STATUSES.join("|")} — the badge is skipped\n`);
2486
+ };
2487
+ /**
2488
+ * PlanHeader props from frontmatter — present only with a `title` and no
2489
+ * `<Plan>`-style `<article>` root in the body (that would render a second
2490
+ * header). Used for both the SSR header and the hydration payload.
2491
+ */
2492
+ const frontmatterHeader = (fm, body) => {
2493
+ const title = fm("title");
2494
+ if (title === void 0 || title === "" || /<article/u.test(body)) return;
2495
+ return {
2496
+ date: fm("date"),
2497
+ owner: fm("owner"),
2498
+ status: headerStatus(fm("status")),
2499
+ title,
2500
+ updated: fm("updated"),
2501
+ version: fm("version")
2502
+ };
2503
+ };
2504
+ /** Render MDX source text to a standalone HTML document. */
2505
+ const render = async (source, opts = {}) => {
2506
+ const dir = path.resolve(opts.dir ?? process.cwd());
2507
+ const filePath = path.resolve(dir, opts.filePath ?? "document.mdx");
2508
+ const config = await loadConfig(dir);
2509
+ const user = await loadUserComponents(config);
2510
+ const collisions = Object.keys(user.components).filter((k) => Object.hasOwn(builtinComponents, k));
2511
+ for (const k of collisions) process.stderr.write(`mdxr: project component <${k}> overrides the built-in\n`);
2512
+ const components = {
2513
+ ...builtinComponents,
2514
+ ...user.components
2515
+ };
2516
+ const { body, code, fileLinks, frontmatter, renderedAt, renderWithHeader, usedComponents, usedIcons } = await mdxToHtml(source, components, filePath, {
2517
+ editor: config.editor,
2518
+ hydrate: opts.hydrate
2519
+ });
2520
+ const fmStr = (key) => {
2521
+ const val = frontmatter[key];
2522
+ if (typeof val === "string") return val === "" ? void 0 : val;
2523
+ if (val instanceof Date) return val.toISOString().slice(0, 10);
2524
+ return typeof val === "number" ? String(val) : void 0;
2525
+ };
2526
+ const fmTitle = fmStr("title");
2527
+ const h1Text = /<h1[^>]*>(?<text>[\s\S]*?)<\/h1>/u.exec(body)?.groups?.text.replaceAll(/<[^>]*>/gu, "").replaceAll("&#x27;", "'").replaceAll("&quot;", "\"").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&").trim();
2528
+ const title = fmTitle ?? (nonEmpty(h1Text) ? h1Text : void 0) ?? "mdxr document";
2529
+ const headerProps = frontmatterHeader(fmStr, body);
2530
+ let docBody = body;
2531
+ let docFileLinks = fileLinks;
2532
+ let docIcons = usedIcons;
2533
+ if (headerProps !== void 0) {
2534
+ const pass = renderWithHeader(createElement(PlanHeader, headerProps));
2535
+ docBody = pass.html;
2536
+ docFileLinks = pass.fileLinks;
2537
+ docIcons = pass.usedIcons;
2538
+ }
2539
+ const themeCss = config.themePath === void 0 ? void 0 : await readFile(config.themePath, "utf-8");
2540
+ const sources = [
2541
+ {
2542
+ content: docBody,
2543
+ extension: "html"
2544
+ },
2545
+ {
2546
+ content: source,
2547
+ extension: "mdx"
2548
+ },
2549
+ {
2550
+ content: themeCss ?? "",
2551
+ extension: "css"
2552
+ },
2553
+ ...await ownSources(),
2554
+ ...user.code === void 0 ? [] : [{
2555
+ content: user.code,
2556
+ extension: "js"
2557
+ }],
2558
+ ...config.componentsCode === void 0 ? [] : [{
2559
+ content: config.componentsCode,
2560
+ extension: "js"
2561
+ }]
2562
+ ];
2563
+ const { css, dependencies } = await buildCss(sources, config.themePath);
2564
+ opts.onDependencies?.(dependencies);
2565
+ const [js, hydrateJs] = await Promise.all([clientJs(), buildHydrateBundle({
2566
+ code,
2567
+ config,
2568
+ fileLinks: docFileLinks,
2569
+ headerProps,
2570
+ hydrate: opts.hydrate,
2571
+ now: renderedAt,
2572
+ usedComponents,
2573
+ usedIcons: docIcons
2574
+ })]);
2575
+ return htmlDocument({
2576
+ body: docBody,
2577
+ clientJs: js,
2578
+ css,
2579
+ hydrateJs,
2580
+ liveReload: opts.liveReload,
2581
+ needsKatex: /class="[^"]*katex/u.test(docBody),
2582
+ needsMermaid: /class="[^"]*mermaid/u.test(docBody),
2583
+ title
2584
+ });
2585
+ };
2586
+ /** Read `mdxPath` and render it to a standalone HTML document. */
2587
+ const renderFile = async (mdxPath, opts = {}) => {
2588
+ const abs = path.resolve(mdxPath);
2589
+ const source = await readFile(abs, "utf-8");
2590
+ return await render(source, {
2591
+ ...opts,
2592
+ dir: path.dirname(abs),
2593
+ filePath: abs
2594
+ });
2595
+ };
2596
+ //#endregion
2597
+ //#region src/serve.ts
2598
+ const errorPage = (err) => `<!doctype html><meta charset="utf-8"><body style="font-family:monospace;background:#1c1917;color:#fca5a5;padding:2rem"><h1>mdxr render error</h1><pre>${formatError(err).replaceAll("&", "&amp;").replaceAll("<", "&lt;")}</pre></body>`;
2599
+ /** Dependency/build output dirs — never worth a watch fd. */
2600
+ const SKIP_DIRS = /* @__PURE__ */ new Set([
2601
+ ".git",
2602
+ ".mdxr-cache",
2603
+ "dist",
2604
+ "node_modules",
2605
+ "storybook-static"
2606
+ ]);
2607
+ /**
2608
+ * The set of directories being watched. `fs.watch` recursive mode exists only
2609
+ * on darwin/win32 — elsewhere `arm` puts one non-recursive watcher per
2610
+ * directory, which also survives atomic saves (rename-over kills a watch
2611
+ * aimed at the file itself).
2612
+ */
2613
+ const createWatchSet = () => {
2614
+ const watchers = [];
2615
+ const armed = /* @__PURE__ */ new Set();
2616
+ const track = (w, dir) => {
2617
+ watchers.push(w);
2618
+ if (dir !== void 0) {
2619
+ armed.add(dir);
2620
+ w.on("close", () => {
2621
+ armed.delete(dir);
2622
+ });
2623
+ }
2624
+ w.on("error", () => {
2625
+ w.close();
2626
+ });
2627
+ };
2628
+ /** One watcher on `dir` itself (no descent). */
2629
+ const armFlat = (dir, onEvent) => {
2630
+ if (armed.has(dir) || SKIP_DIRS.has(path.basename(dir))) return;
2631
+ try {
2632
+ track(fs.watch(dir, onEvent), dir);
2633
+ } catch {}
2634
+ };
2635
+ /**
2636
+ * Recursive fallback: a watcher on `dir` plus every directory under it.
2637
+ * The `armed` check guards only the watcher install — the descent still
2638
+ * runs on an already-armed root, so directories created mid-session get
2639
+ * picked up on the next rebuild (armDeps re-arms the tree for this).
2640
+ */
2641
+ const arm = (dir, onEvent) => {
2642
+ if (SKIP_DIRS.has(path.basename(dir))) return;
2643
+ if (!armed.has(dir)) try {
2644
+ track(fs.watch(dir, onEvent), dir);
2645
+ } catch {}
2646
+ let ents;
2647
+ try {
2648
+ ents = fs.readdirSync(dir, { withFileTypes: true });
2649
+ } catch {
2650
+ return;
2651
+ }
2652
+ for (const e of ents) if (e.isDirectory()) arm(path.join(dir, e.name), onEvent);
2653
+ };
2654
+ return {
2655
+ arm,
2656
+ armFlat,
2657
+ close() {
2658
+ for (const w of watchers) w.close();
2659
+ },
2660
+ get size() {
2661
+ return watchers.length;
2662
+ },
2663
+ track
2664
+ };
2665
+ };
2666
+ const servePreview = async (target, port) => {
2667
+ let html = "";
2668
+ const clients = /* @__PURE__ */ new Set();
2669
+ const server = http.createServer((req, res) => {
2670
+ if (req.url === "/__mdxr_events") {
2671
+ res.writeHead(200, {
2672
+ "cache-control": "no-cache",
2673
+ connection: "keep-alive",
2674
+ "content-type": "text/event-stream"
2675
+ });
2676
+ res.write("retry: 1000\n\n");
2677
+ clients.add(res);
2678
+ req.on("close", () => {
2679
+ clients.delete(res);
2680
+ });
2681
+ res.on("error", () => {
2682
+ clients.delete(res);
2683
+ });
2684
+ return;
2685
+ }
2686
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2687
+ res.end(html);
2688
+ });
2689
+ const watch = createWatchSet();
2690
+ let recursiveWatch = false;
2691
+ let timer;
2692
+ /**
2693
+ * Dependency files (theme CSS, its nested imports) may live outside the
2694
+ * watched document directory. Inside the watch dir the root watcher covers
2695
+ * them; outside, a flat watcher on the file's own directory suffices —
2696
+ * and avoids descending into a potentially huge ancestor tree. Also
2697
+ * re-arms the tree so directories created mid-session are picked up.
2698
+ */
2699
+ const armDeps = (onEvent) => {
2700
+ if (!recursiveWatch) watch.arm(target.watchDir, onEvent);
2701
+ for (const dep of target.deps?.() ?? []) {
2702
+ const dir = path.dirname(dep);
2703
+ if (dir === target.watchDir || dir.startsWith(`${target.watchDir}${path.sep}`) || dir.split(path.sep).includes("node_modules")) continue;
2704
+ watch.armFlat(dir, onEvent);
2705
+ }
2706
+ };
2707
+ const rebuild = async (onEvent) => {
2708
+ try {
2709
+ html = await target.renderDoc();
2710
+ } catch (error) {
2711
+ html = errorPage(error);
2712
+ }
2713
+ armDeps(onEvent);
2714
+ };
2715
+ let reloading = Promise.resolve();
2716
+ const reload = (onEvent) => {
2717
+ const prev = reloading;
2718
+ reloading = (async () => {
2719
+ await prev;
2720
+ try {
2721
+ await rebuild(onEvent);
2722
+ for (const c of clients) try {
2723
+ c.write("event: reload\ndata: {}\n\n");
2724
+ } catch {
2725
+ clients.delete(c);
2726
+ }
2727
+ } catch {}
2728
+ })();
2729
+ };
2730
+ const notify = () => {
2731
+ clearTimeout(timer);
2732
+ timer = setTimeout(() => {
2733
+ reload(notify);
2734
+ }, 80);
2735
+ };
2736
+ try {
2737
+ watch.track(fs.watch(target.watchDir, { recursive: true }, notify));
2738
+ recursiveWatch = true;
2739
+ } catch {
2740
+ watch.arm(target.watchDir, notify);
2741
+ }
2742
+ if (watch.size === 0 && target.watchFile !== void 0) try {
2743
+ watch.track(fs.watch(target.watchFile, notify));
2744
+ } catch {}
2745
+ server.on("close", () => {
2746
+ clearTimeout(timer);
2747
+ watch.close();
2748
+ });
2749
+ await rebuild(notify);
2750
+ server.listen(port, "127.0.0.1");
2751
+ try {
2752
+ await once(server, "listening");
2753
+ } catch (error) {
2754
+ server.close();
2755
+ throw error;
2756
+ }
2757
+ const address = server.address();
2758
+ const boundPort = typeof address === "object" && address !== null ? address.port : port;
2759
+ console.log(`mdxr: serving ${target.label} at http://localhost:${boundPort}`);
2760
+ console.log("mdxr: watching for changes (Ctrl+C to stop)");
2761
+ return server;
2762
+ };
2763
+ /**
2764
+ * Wraps a render fn with dependency tracking: `deps` reads the paths the
2765
+ * last render reported via `onDependencies` (theme CSS + its imports), so
2766
+ * `armDeps` can watch files outside the document's own directory.
2767
+ */
2768
+ const trackDeps = (renderDoc) => {
2769
+ let deps = [];
2770
+ return {
2771
+ deps: () => deps,
2772
+ renderDoc: async () => {
2773
+ const collected = [];
2774
+ const doc = await renderDoc((d) => {
2775
+ collected.push(...d);
2776
+ });
2777
+ deps = collected;
2778
+ return doc;
2779
+ }
2780
+ };
2781
+ };
2782
+ /** Serve an .mdx file, rebuilding + live-reloading on changes in its directory. */
2783
+ const serve = async (mdxPath, port) => {
2784
+ const abs = path.resolve(mdxPath);
2785
+ return await servePreview({
2786
+ label: mdxPath,
2787
+ ...trackDeps(async (onDeps) => await renderFile(abs, {
2788
+ liveReload: true,
2789
+ onDependencies: onDeps
2790
+ })),
2791
+ watchDir: path.dirname(abs),
2792
+ watchFile: abs
2793
+ }, port);
2794
+ };
2795
+ /** Serve MDX source passed directly (e.g. piped via stdin). */
2796
+ const serveSource = async (source, port, opts = {}) => {
2797
+ const dir = path.resolve(opts.dir ?? process.cwd());
2798
+ return await servePreview({
2799
+ label: opts.filePath ?? "stdin",
2800
+ ...trackDeps(async (onDeps) => await render(source, {
2801
+ dir,
2802
+ filePath: opts.filePath,
2803
+ liveReload: true,
2804
+ onDependencies: onDeps
2805
+ })),
2806
+ watchDir: dir
2807
+ }, port);
2808
+ };
2809
+ //#endregion
2810
+ //#region src/cli.ts
2811
+ const readStdin = async () => {
2812
+ const chunks = [];
2813
+ for await (const chunk of process.stdin) chunks.push(chunk);
2814
+ return Buffer.concat(chunks).toString("utf-8");
2815
+ };
2816
+ const requireStdinSource = () => {
2817
+ if (process.stdin.isTTY) throw new Error("no input: pass an .mdx file or pipe MDX source via stdin");
2818
+ };
2819
+ const fail = (err, json) => {
2820
+ if (json) console.log(JSON.stringify({
2821
+ error: formatError(err),
2822
+ ok: false
2823
+ }));
2824
+ else console.error(`mdxr: error: ${formatError(err)}`);
2825
+ process.exit(1);
2826
+ };
2827
+ const cli = cac("mdxr");
2828
+ cli.command("render [file]", "Render an .mdx document to a standalone HTML file").option("-o, --out <path>", "Output path (default: <file>.html; stdout for stdin input or '-')").option("--format <format>", "Error output: text | json").option("--no-hydrate", "Emit static HTML without the client hydration bundle").action(async (file, opts) => {
2829
+ const json = opts.format === "json";
2830
+ try {
2831
+ parseErrorFormat(opts.format);
2832
+ const fromStdin = file === void 0 || file === "-";
2833
+ if (fromStdin) requireStdinSource();
2834
+ const html = fromStdin ? await render(await readStdin(), {
2835
+ dir: process.cwd(),
2836
+ filePath: "<stdin>",
2837
+ hydrate: opts.hydrate
2838
+ }) : await renderFile(file, { hydrate: opts.hydrate });
2839
+ const out = opts.out === "-" ? void 0 : opts.out ?? (fromStdin ? void 0 : `${file.replace(/\.(?:mdx|md)$/u, "")}.html`);
2840
+ if (out === void 0) {
2841
+ process.stdout.on("error", (err) => {
2842
+ if (err.code === "EPIPE") process.exit(0);
2843
+ throw err;
2844
+ });
2845
+ process.stdout.write(html);
2846
+ return;
2847
+ }
2848
+ await writeFile(path.resolve(out), html);
2849
+ if (json) console.log(JSON.stringify({
2850
+ ok: true,
2851
+ out
2852
+ }));
2853
+ else console.log(`mdxr: wrote ${out}`);
2854
+ } catch (error) {
2855
+ fail(error, json);
2856
+ }
2857
+ });
2858
+ cli.command("serve [file]", "Preview a document in the browser with live reload").option("-p, --port <port>", "Port", { default: 3737 }).action(async (file, opts) => {
2859
+ try {
2860
+ const port = Number(opts.port);
2861
+ if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`invalid --port: ${opts.port}`);
2862
+ if (file === void 0 || file === "-") {
2863
+ requireStdinSource();
2864
+ await serveSource(await readStdin(), port, {
2865
+ dir: process.cwd(),
2866
+ filePath: "<stdin>"
2867
+ });
2868
+ return;
2869
+ }
2870
+ await serve(file, port);
2871
+ } catch (error) {
2872
+ fail(error, false);
2873
+ }
2874
+ });
2875
+ cli.command("catalog", "List available components (built-in + project-defined)").option("--json", "Print machine-readable JSON").option("--dir <dir>", "Project directory to read mdxr.config.ts from").action(async (opts) => {
2876
+ try {
2877
+ const dir = path.resolve(opts.dir ?? process.cwd());
2878
+ const config = await loadConfig(dir);
2879
+ const user = await loadUserComponents(config);
2880
+ const entries = catalogEntries(user.components);
2881
+ if (opts.json === true) console.log(JSON.stringify({
2882
+ builtins: Object.keys(builtinComponents),
2883
+ components: entries,
2884
+ conventions: CONVENTIONS
2885
+ }, null, 2));
2886
+ else console.log(formatCatalog(entries));
2887
+ } catch (error) {
2888
+ fail(error, false);
2889
+ }
2890
+ });
2891
+ cli.command("init", "Install the mdxr agent skill into this project").option("--tool <tool>", "agents | claude | devin | all", { default: "agents" }).option("--global", "Install into your home directory instead of the project").option("--force", "Overwrite an existing skill").action(async (opts) => {
2892
+ try {
2893
+ const paths = await installSkill(opts);
2894
+ for (const p of paths) console.log(`mdxr: installed skill → ${p}`);
2895
+ } catch (error) {
2896
+ fail(error, false);
2897
+ }
2898
+ });
2899
+ cli.help();
2900
+ cli.parse();
2901
+ //#endregion
2902
+ export {};