@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
@@ -0,0 +1,85 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import type { Node } from "unist";
5
+ import { visit } from "unist-util-visit";
6
+ import type { VFile } from "vfile";
7
+
8
+ import { langForPath } from "../langs.js";
9
+ import { parseLineRange } from "../lines.js";
10
+ import type { MdxTarget } from "./ast.js";
11
+ import { isFlowElement, jsxAttr } from "./ast.js";
12
+
13
+ /** A flow element that becomes a `code` node after mutation. */
14
+ interface CodeTarget extends MdxTarget {
15
+ lang?: string;
16
+ meta?: string;
17
+ }
18
+
19
+ /**
20
+ * `<CodeFile path="src/foo.ts" lines="40-52" lang="ts" />` embeds a real file
21
+ * from disk as a fenced code block, so code explanations quote the actual
22
+ * source instead of drifting copies. `path` resolves relative to the
23
+ * document; `lines` slices a 1-based range; `lang` overrides the extension.
24
+ * The emitted `code` node carries `title="…"` meta so `remarkCodeMeta` /
25
+ * `Pre` render the usual filename header — run this before remarkCodeMeta.
26
+ */
27
+ export const remarkCodeFile = () => (tree: Node, file: VFile) => {
28
+ visit(tree, "mdxJsxFlowElement", (node: Node) => {
29
+ if (!isFlowElement(node) || node.name !== "CodeFile") {
30
+ return;
31
+ }
32
+ const rel = jsxAttr(node, "path");
33
+ if (rel === undefined) {
34
+ file.fail(
35
+ "<CodeFile> requires a `path` attribute",
36
+ node,
37
+ "mdxr:code-file"
38
+ );
39
+ }
40
+ const abs = path.resolve(file.dirname ?? ".", rel);
41
+ let content: string;
42
+ try {
43
+ content = readFileSync(abs, "utf-8");
44
+ } catch {
45
+ file.fail(`<CodeFile> cannot read ${rel}`, node, "mdxr:code-file");
46
+ }
47
+
48
+ const rangeSpec = jsxAttr(node, "lines");
49
+ let titleSuffix = "";
50
+ if (rangeSpec !== undefined) {
51
+ const range = parseLineRange(rangeSpec);
52
+ if (range === undefined) {
53
+ file.fail(
54
+ `<CodeFile> invalid lines range: ${rangeSpec}`,
55
+ node,
56
+ "mdxr:code-file"
57
+ );
58
+ }
59
+ const all = content.replace(/\n$/u, "").split("\n");
60
+ if (range.start > all.length) {
61
+ file.fail(
62
+ `<CodeFile> lines ${rangeSpec} out of range (${all.length} lines in ${rel})`,
63
+ node,
64
+ "mdxr:code-file"
65
+ );
66
+ }
67
+ content = all.slice(range.start - 1, range.end).join("\n");
68
+ titleSuffix = `:${rangeSpec}`;
69
+ }
70
+
71
+ // langForPath, not just extname: basename tables cover Makefile,
72
+ // Dockerfile, .gitignore — extensionless files still get a grammar.
73
+ const lang = jsxAttr(node, "lang") ?? langForPath(abs);
74
+ const target: CodeTarget = node;
75
+ target.type = "code";
76
+ delete target.name;
77
+ delete target.attributes;
78
+ delete target.children;
79
+ target.lang = lang;
80
+ // `"` inside a path would close the title="…" meta early — quote it as `'`.
81
+ const title = rel.replaceAll('"', "'");
82
+ target.meta = `title="${title}${titleSuffix}"`;
83
+ target.value = content;
84
+ });
85
+ };
@@ -0,0 +1,21 @@
1
+ import type { Node } from "unist";
2
+ import { visit } from "unist-util-visit";
3
+
4
+ import { setHProperty } from "./ast.js";
5
+
6
+ /**
7
+ * The mdast→hast conversion drops the code fence info string (`meta`).
8
+ * Copy it to `hProperties.meta` so it reaches the `pre` element's props.
9
+ */
10
+ export const remarkCodeMeta = () => (tree: Node) => {
11
+ visit(tree, "code", (node: Node) => {
12
+ if (
13
+ !("meta" in node) ||
14
+ typeof node.meta !== "string" ||
15
+ node.meta === ""
16
+ ) {
17
+ return;
18
+ }
19
+ setHProperty(node, "meta", node.meta);
20
+ });
21
+ };
@@ -0,0 +1,170 @@
1
+ import type { Node, Parent } from "unist";
2
+ import { visit } from "unist-util-visit";
3
+ import type { VFile } from "vfile";
4
+
5
+ import { isRecord, own } from "../guards.js";
6
+ import { isParent, textContent, toMdxElement } from "./ast.js";
7
+ import { CALLOUT_KINDS, normalizeCalloutKind } from "./callouts.js";
8
+
9
+ const CONTAINER_COMPONENTS: Record<string, string> = {
10
+ audit: "Audit",
11
+ barchart: "BarChart",
12
+ bench: "Bench",
13
+ benchmarks: "Benchmarks",
14
+ board: "Board",
15
+ bridge: "Bridge",
16
+ bump: "Bump",
17
+ bumps: "Bumps",
18
+ check: "Check",
19
+ checks: "Checks",
20
+ comment: "Comment",
21
+ comments: "Comments",
22
+ day: "Day",
23
+ dbfield: "DbField",
24
+ dbtable: "DbTable",
25
+ deps: "Deps",
26
+ endpoints: "Endpoints",
27
+ entry: "Entry",
28
+ envvar: "EnvVar",
29
+ envvars: "EnvVars",
30
+ files: "Files",
31
+ finding: "Finding",
32
+ findings: "Findings",
33
+ flow: "Flow",
34
+ funnel: "Funnel",
35
+ gantt: "Gantt",
36
+ gauge: "Gauge",
37
+ gauges: "Gauges",
38
+ graph: "Graph",
39
+ hypotheses: "Hypotheses",
40
+ hypothesis: "Hypothesis",
41
+ incident: "Incident",
42
+ linechart: "LineChart",
43
+ matrix: "Matrix",
44
+ package: "Package",
45
+ packages: "Packages",
46
+ pathway: "Pathway",
47
+ phase: "Phase",
48
+ piechart: "PieChart",
49
+ plan: "Plan",
50
+ quadrant: "Quadrant",
51
+ radar: "Radar",
52
+ release: "Release",
53
+ review: "Review",
54
+ sankey: "Sankey",
55
+ scatter: "Scatter",
56
+ schema: "Schema",
57
+ search: "Search",
58
+ searches: "Searches",
59
+ service: "Service",
60
+ statuspage: "StatusPage",
61
+ steps: "Steps",
62
+ stop: "Stop",
63
+ summary: "Summary",
64
+ terminal: "Terminal",
65
+ tests: "Tests",
66
+ timeline: "Timeline",
67
+ toc: "Toc",
68
+ trace: "Trace",
69
+ treemap: "Treemap",
70
+ uptime: "Uptime",
71
+ venn: "Venn",
72
+ verdict: "Verdict",
73
+ vuln: "Vuln",
74
+ waterfall: "Waterfall",
75
+ };
76
+
77
+ interface DirectiveNode extends Parent {
78
+ name: string;
79
+ attributes: Record<string, unknown>;
80
+ }
81
+
82
+ const isDirective = (n: Node): n is DirectiveNode =>
83
+ "name" in n &&
84
+ typeof n.name === "string" &&
85
+ "attributes" in n &&
86
+ isRecord(n.attributes) &&
87
+ isParent(n);
88
+
89
+ /**
90
+ * remark-directive containers become mdxr components:
91
+ * :::note[Optional label] → <Callout kind="note" title="Optional label">
92
+ * :::phase{title="X" status="doing"} → <Phase title="X" status="doing">
93
+ * :::plan / :::steps / :::summary / :::timeline → Plan / Steps / Summary / Timeline
94
+ * :::flow / :::findings / :::finding / :::files / :::deps
95
+ * → Flow / Findings / Finding / Files / Deps
96
+ * :::terminal / :::trace / :::hypotheses / :::hypothesis / :::searches / :::search
97
+ * → Terminal / Trace / Hypotheses / Hypothesis / Searches / Search
98
+ * :::barchart / :::linechart / :::piechart / :::scatter / :::radar
99
+ * → BarChart / LineChart / PieChart / Scatter / Radar
100
+ * :::funnel / :::quadrant / :::bridge / :::treemap / :::sankey / :::venn
101
+ * → Funnel / Quadrant / Bridge / Treemap / Sankey / Venn
102
+ * :::review / :::comment / :::checks / :::check / :::audit / :::vuln
103
+ * → Review / Comment / Checks / Check / Audit / Vuln
104
+ * :::bumps / :::bump / :::packages / :::package
105
+ * → Bumps / Bump / Packages / Package
106
+ * :::gauges / :::gauge / :::benchmarks / :::bench
107
+ * → Gauges / Gauge / Benchmarks / Bench
108
+ * :::schema / :::dbtable / :::dbfield / :::envvars / :::envvar
109
+ * → Schema / DbTable / DbField / EnvVars / EnvVar
110
+ * :::release / :::entry / :::pathway / :::stop / :::incident / :::verdict
111
+ * → Release / Entry / Pathway / Stop / Incident / Verdict
112
+ * :::statuspage / :::service / :::uptime / :::day
113
+ * → StatusPage / Service / Uptime / Day
114
+ * `non-goal` is accepted as an alias of the `nongoal` callout kind.
115
+ */
116
+ export const remarkMdxrDirectives = () => (tree: Node, file: VFile) => {
117
+ visit(
118
+ tree,
119
+ ["containerDirective", "leafDirective", "textDirective"],
120
+ (node: Node) => {
121
+ if (!isDirective(node)) {
122
+ return;
123
+ }
124
+ const directive = node;
125
+ const name = normalizeCalloutKind(directive.name);
126
+ const isContainer = directive.type === "containerDirective";
127
+ // own-property lookup: `:::toString` would otherwise resolve to
128
+ // Object.prototype.toString and be "rendered" as a component name.
129
+ const container = own(CONTAINER_COMPONENTS, name);
130
+ const component = CALLOUT_KINDS.has(name) ? "Callout" : container;
131
+ if (component === undefined) {
132
+ file.message(
133
+ `Unknown directive ":::${directive.name}"`,
134
+ directive,
135
+ "mdxr:directives"
136
+ );
137
+ return;
138
+ }
139
+ if (!isContainer) {
140
+ file.message(
141
+ `:::${directive.name} is a container directive — use three colons`,
142
+ directive,
143
+ "mdxr:directives"
144
+ );
145
+ return;
146
+ }
147
+
148
+ const attrs: Record<string, unknown> = { ...directive.attributes };
149
+
150
+ const [first] = directive.children;
151
+ const isLabel =
152
+ first !== undefined &&
153
+ isRecord(first.data) &&
154
+ first.data.directiveLabel === true;
155
+ if (isLabel) {
156
+ attrs.title ??= textContent(first).trim();
157
+ directive.children.shift();
158
+ }
159
+
160
+ if (component === "Callout") {
161
+ toMdxElement(directive, "mdxJsxFlowElement", "Callout", {
162
+ kind: name,
163
+ ...attrs,
164
+ });
165
+ } else {
166
+ toMdxElement(directive, "mdxJsxFlowElement", component, attrs);
167
+ }
168
+ }
169
+ );
170
+ };
@@ -0,0 +1,64 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import type { Node } from "unist";
5
+ import { visitParents } from "unist-util-visit-parents";
6
+ import type { VFile } from "vfile";
7
+
8
+ import { splitPathLines } from "../lines.js";
9
+ import type { MdxTarget } from "./ast.js";
10
+ import { toMdxElement } from "./ast.js";
11
+
12
+ /**
13
+ * Inline code that names a real file path — `` `src/mdx.ts` `` — becomes a
14
+ * `<FileRef>` chip, so prose references get the same editor link, icon, and
15
+ * copy button as an explicit component. The value must contain a `/` (a bare
16
+ * `file.ts` stays code) and resolve to an existing file relative to the
17
+ * document; a trailing `:N`/`:N-M` becomes `lines`. Paths inside links are
18
+ * left alone — a chip can't nest inside <a>. Runs late in the pipeline so
19
+ * directive labels and heading slugs are already computed.
20
+ */
21
+ export const remarkFilePaths = () => (tree: Node, file: VFile) => {
22
+ const dir = file.dirname ?? ".";
23
+ const resolve = (
24
+ value: string
25
+ ): { lines?: string; path: string } | undefined => {
26
+ if (!value.includes("/") || value.includes("://")) {
27
+ return undefined;
28
+ }
29
+ const target = splitPathLines(value);
30
+ if (existsSync(path.resolve(dir, target.path))) {
31
+ return target;
32
+ }
33
+ // A file literally named "x.ts:2" beats the lines interpretation.
34
+ return target.lines !== undefined && existsSync(path.resolve(dir, value))
35
+ ? { path: value }
36
+ : undefined;
37
+ };
38
+ visitParents(tree, "inlineCode", (node: Node, ancestors: Node[]) => {
39
+ // Links keep plain code — FileRef renders its own <a>, and nested
40
+ // anchors break in the HTML parser. Raw JSX <a> wrappers count too:
41
+ // they're mdxJsx elements, not "link" nodes.
42
+ const inLink = ancestors.some(
43
+ (a) =>
44
+ a.type === "link" ||
45
+ a.type === "linkReference" ||
46
+ ((a.type === "mdxJsxTextElement" || a.type === "mdxJsxFlowElement") &&
47
+ (a as MdxTarget).name === "a")
48
+ );
49
+ if (inLink || !("value" in node) || typeof node.value !== "string") {
50
+ return;
51
+ }
52
+ const target = resolve(node.value);
53
+ if (target === undefined) {
54
+ return;
55
+ }
56
+ const el: MdxTarget = node;
57
+ toMdxElement(el, "mdxJsxTextElement", "FileRef", {
58
+ lines: target.lines,
59
+ path: target.path,
60
+ });
61
+ el.children = [];
62
+ delete el.value;
63
+ });
64
+ };
@@ -0,0 +1,131 @@
1
+ import type { Node, Parent } from "unist";
2
+ import { visit } from "unist-util-visit";
3
+
4
+ import { isFlowElement, jsxAttr, setHProperty, textContent } from "./ast.js";
5
+
6
+ interface HeadingNode extends Parent {
7
+ depth: number;
8
+ type: "heading";
9
+ }
10
+
11
+ const isHeading = (n: Node): n is HeadingNode => n.type === "heading";
12
+
13
+ /** GitHub-style slug: lowercase, punctuation stripped, spaces → `-`. */
14
+ const slugify = (text: string): string =>
15
+ text
16
+ .toLowerCase()
17
+ .trim()
18
+ .replaceAll(/[^\p{L}\p{N}_\s-]/gu, "")
19
+ .replaceAll(/\s+/gu, "-");
20
+
21
+ /** Positive-int attribute or fallback — "0"/"abc" both become `fallback`. */
22
+ const depthAttr = (
23
+ node: Parameters<typeof jsxAttr>[0],
24
+ name: string,
25
+ fallback: number
26
+ ): number => {
27
+ const raw = jsxAttr(node, name);
28
+ const n = Number(raw);
29
+ return raw === undefined || !Number.isInteger(n) || n < 1 ? fallback : n;
30
+ };
31
+
32
+ interface TocItem {
33
+ children: TocItem[];
34
+ depth: number;
35
+ slug: string;
36
+ text: string;
37
+ }
38
+
39
+ /** Nest a flat heading list by depth (an h3 under the previous h2, …). */
40
+ const nest = (headings: TocItem[]): TocItem[] => {
41
+ const roots: TocItem[] = [];
42
+ const stack: TocItem[] = [];
43
+ for (const h of headings) {
44
+ while (stack.length > 0 && (stack.at(-1)?.depth ?? 0) >= h.depth) {
45
+ stack.pop();
46
+ }
47
+ const parent = stack.at(-1);
48
+ if (parent === undefined) {
49
+ roots.push(h);
50
+ } else {
51
+ parent.children.push(h);
52
+ }
53
+ stack.push(h);
54
+ }
55
+ return roots;
56
+ };
57
+
58
+ interface ListNode extends Parent {
59
+ ordered?: boolean;
60
+ spread?: boolean;
61
+ }
62
+
63
+ const toList = (items: TocItem[]): ListNode => ({
64
+ children: items.map((item) => ({
65
+ children: [
66
+ {
67
+ children: [
68
+ {
69
+ children: [{ type: "text", value: item.text }],
70
+ type: "link",
71
+ url: `#${item.slug}`,
72
+ },
73
+ ],
74
+ type: "paragraph",
75
+ },
76
+ ...(item.children.length > 0 ? [toList(item.children)] : []),
77
+ ],
78
+ spread: false,
79
+ type: "listItem",
80
+ })),
81
+ ordered: false,
82
+ spread: false,
83
+ type: "list",
84
+ });
85
+
86
+ /**
87
+ * Two jobs, one pass over the document:
88
+ * 1. Every heading gets an `id` (GitHub-style slug, deduplicated), enabling
89
+ * deep links.
90
+ * 2. `<Toc>` elements (`:::toc` included) get a nested link list injected as
91
+ * children — `depth`/`min` attributes bound the heading levels included
92
+ * (defaults: h2–h3, i.e. min=2 depth=3).
93
+ */
94
+ export const remarkMdxrHeadings = () => (tree: Node) => {
95
+ const counts = new Map<string, number>();
96
+ const used = new Set<string>();
97
+ const headings: TocItem[] = [];
98
+
99
+ visit(tree, "heading", (node: Node) => {
100
+ if (!isHeading(node)) {
101
+ return;
102
+ }
103
+ const text = textContent(node).trim();
104
+ const base = slugify(text) || "section";
105
+ let count = counts.get(base) ?? 0;
106
+ let slug = count === 0 ? base : `${base}-${count}`;
107
+ // A suffixed slug can still collide with a literal heading ("a-1" after
108
+ // "a","a" generates a-1 first) — keep bumping until the id is unused.
109
+ while (used.has(slug)) {
110
+ count += 1;
111
+ slug = `${base}-${count}`;
112
+ }
113
+ counts.set(base, count + 1);
114
+ used.add(slug);
115
+
116
+ setHProperty(node, "id", slug);
117
+ headings.push({ children: [], depth: node.depth, slug, text });
118
+ });
119
+
120
+ visit(tree, "mdxJsxFlowElement", (node: Node) => {
121
+ if (!isFlowElement(node) || node.name !== "Toc") {
122
+ return;
123
+ }
124
+ const minDepth = depthAttr(node, "min", 2);
125
+ const maxDepth = depthAttr(node, "depth", 3);
126
+ const items = headings.filter(
127
+ (h) => h.depth >= minDepth && h.depth <= maxDepth && h.text !== ""
128
+ );
129
+ node.children = items.length === 0 ? [] : [toList(nest(items))];
130
+ });
131
+ };
@@ -0,0 +1,40 @@
1
+ import type { Node } from "unist";
2
+ import { visit } from "unist-util-visit";
3
+ import type { VFile } from "vfile";
4
+
5
+ import { isRecord } from "../guards.js";
6
+
7
+ const JS_NODES = ["mdxjsEsm", "mdxFlowExpression", "mdxTextExpression"];
8
+ const JSX_ELEMENTS = ["mdxJsxFlowElement", "mdxJsxTextElement"];
9
+
10
+ const MESSAGE =
11
+ "JavaScript expressions and import/export statements are not allowed in mdxr documents. Add a component via mdxr.config.ts instead.";
12
+
13
+ /**
14
+ * mdxr documents are data, not code: reject ESM imports/exports and JS
15
+ * expressions (`{...}`) — including inside JSX attributes (`prop={x}`,
16
+ * `{...spread}`), which `visit` never reaches because attributes are not
17
+ * children. Extensibility happens through mdxr.config.ts, not through
18
+ * executable markup inside the document.
19
+ */
20
+ export const remarkNoJs = () => (tree: Node, file: VFile) => {
21
+ visit(tree, JS_NODES, (node: Node) => {
22
+ file.fail(MESSAGE, node, "mdxr:no-js");
23
+ });
24
+ visit(tree, JSX_ELEMENTS, (node: Node) => {
25
+ if (!("attributes" in node) || !Array.isArray(node.attributes)) {
26
+ return;
27
+ }
28
+ for (const attr of node.attributes) {
29
+ // String-valued attributes are data; `{expr}` values and `{...x}`
30
+ // spreads are live JavaScript evaluated at render time.
31
+ if (
32
+ isRecord(attr) &&
33
+ (attr.type !== "mdxJsxAttribute" ||
34
+ (attr.value !== null && typeof attr.value !== "string"))
35
+ ) {
36
+ file.fail(MESSAGE, node, "mdxr:no-js");
37
+ }
38
+ }
39
+ });
40
+ };