@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.
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +2902 -0
- package/dist/components.d.mts +791 -0
- package/dist/components.mjs +2 -0
- package/dist/config-SI9IyFiC.mjs +184 -0
- package/dist/doc-context-CEqzMYKv.d.mts +568 -0
- package/dist/index.d.mts +20 -0
- package/dist/index.mjs +4 -0
- package/dist/ui-GKFD0mx5.mjs +15986 -0
- package/package.json +131 -0
- package/skill/SKILL.md +49 -0
- package/skill/references/components/charts.md +172 -0
- package/skill/references/components/document.md +98 -0
- package/skill/references/components/forms.md +35 -0
- package/skill/references/components/investigation.md +198 -0
- package/skill/references/components/layout.md +68 -0
- package/skill/references/components/output.md +158 -0
- package/skill/references/components/planning.md +155 -0
- package/skill/references/components/reports.md +252 -0
- package/skill/references/components/shadcn.md +19 -0
- package/skill/references/components.md +186 -0
- package/skill/references/extending.md +48 -0
- package/src/ask-sheet.ts +40 -0
- package/src/assets/css.ts +392 -0
- package/src/assets/scripts.ts +78 -0
- package/src/catalog.ts +287 -0
- package/src/cli.ts +178 -0
- package/src/client/doc-events.ts +1108 -0
- package/src/client/entry.ts +27 -0
- package/src/client-js.ts +47 -0
- package/src/component-map.ts +47 -0
- package/src/components/ui/accordion.tsx +77 -0
- package/src/components/ui/alert-dialog.tsx +185 -0
- package/src/components/ui/alert.tsx +76 -0
- package/src/components/ui/aspect-ratio.tsx +22 -0
- package/src/components/ui/attachment.tsx +208 -0
- package/src/components/ui/avatar.tsx +106 -0
- package/src/components/ui/badge.tsx +52 -0
- package/src/components/ui/breadcrumb.tsx +121 -0
- package/src/components/ui/bubble.tsx +128 -0
- package/src/components/ui/button-group.tsx +88 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/calendar.tsx +226 -0
- package/src/components/ui/card.tsx +102 -0
- package/src/components/ui/carousel.tsx +246 -0
- package/src/components/ui/chart.tsx +379 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/collapsible.tsx +19 -0
- package/src/components/ui/combobox.tsx +298 -0
- package/src/components/ui/command.tsx +193 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/dialog.tsx +159 -0
- package/src/components/ui/direction.tsx +4 -0
- package/src/components/ui/drawer.tsx +227 -0
- package/src/components/ui/dropdown-menu.tsx +269 -0
- package/src/components/ui/empty.tsx +104 -0
- package/src/components/ui/field.tsx +237 -0
- package/src/components/ui/fieldset.tsx +32 -0
- package/src/components/ui/frame.tsx +87 -0
- package/src/components/ui/hover-card.tsx +50 -0
- package/src/components/ui/input-group.tsx +159 -0
- package/src/components/ui/input-otp.tsx +83 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/item.tsx +202 -0
- package/src/components/ui/kbd.tsx +26 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/marker.tsx +71 -0
- package/src/components/ui/menubar.tsx +284 -0
- package/src/components/ui/message-scroller.tsx +128 -0
- package/src/components/ui/message.tsx +91 -0
- package/src/components/ui/meter.tsx +80 -0
- package/src/components/ui/native-select.tsx +64 -0
- package/src/components/ui/navigation-menu.tsx +170 -0
- package/src/components/ui/pagination.tsx +133 -0
- package/src/components/ui/popover.tsx +87 -0
- package/src/components/ui/progress.tsx +82 -0
- package/src/components/ui/questionnaire.tsx +328 -0
- package/src/components/ui/radio-group.tsx +35 -0
- package/src/components/ui/resizable.tsx +49 -0
- package/src/components/ui/scroll-area.tsx +50 -0
- package/src/components/ui/select.tsx +201 -0
- package/src/components/ui/separator.tsx +22 -0
- package/src/components/ui/sheet.tsx +135 -0
- package/src/components/ui/sidebar.tsx +730 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +51 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +113 -0
- package/src/components/ui/tabs.tsx +82 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/toast.tsx +229 -0
- package/src/components/ui/toggle-group.tsx +87 -0
- package/src/components/ui/toggle.tsx +43 -0
- package/src/components/ui/tooltip.tsx +65 -0
- package/src/components.ts +78 -0
- package/src/config.ts +56 -0
- package/src/define.ts +122 -0
- package/src/doc-context.ts +20 -0
- package/src/editor.ts +94 -0
- package/src/format-error.ts +78 -0
- package/src/guards.ts +84 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/html.ts +91 -0
- package/src/hydrate/export-index.ts +232 -0
- package/src/hydrate/import-scan.ts +169 -0
- package/src/hydrate/plugins.ts +118 -0
- package/src/hydrate/runtime-module.ts +145 -0
- package/src/hydrate-runtime.ts +67 -0
- package/src/hydrate.ts +147 -0
- package/src/index.ts +16 -0
- package/src/init.ts +57 -0
- package/src/langs.ts +148 -0
- package/src/lines.ts +53 -0
- package/src/load-user-module.ts +191 -0
- package/src/mdx.ts +248 -0
- package/src/paths.ts +36 -0
- package/src/rehype/shiki.ts +533 -0
- package/src/remark/alerts.ts +53 -0
- package/src/remark/ast.ts +96 -0
- package/src/remark/callouts.ts +25 -0
- package/src/remark/code-file.ts +85 -0
- package/src/remark/code-meta.ts +21 -0
- package/src/remark/directives.ts +170 -0
- package/src/remark/file-paths.ts +64 -0
- package/src/remark/headings.ts +131 -0
- package/src/remark/no-js.ts +40 -0
- package/src/render.ts +337 -0
- package/src/serve.ts +322 -0
- package/src/styles/globals.css +134 -0
- package/src/styles/shadcn.css +641 -0
- package/src/tailwind.ts +119 -0
- package/src/ui/approvals.tsx +76 -0
- package/src/ui/ask-question.tsx +386 -0
- package/src/ui/ask.tsx +206 -0
- package/src/ui/attrs.ts +51 -0
- package/src/ui/audit.tsx +139 -0
- package/src/ui/bar-chart.tsx +334 -0
- package/src/ui/benchmarks.tsx +143 -0
- package/src/ui/bits.tsx +537 -0
- package/src/ui/board.tsx +173 -0
- package/src/ui/bridge.tsx +207 -0
- package/src/ui/bumps.tsx +178 -0
- package/src/ui/callout.tsx +106 -0
- package/src/ui/changes.tsx +89 -0
- package/src/ui/chart-bits.tsx +52 -0
- package/src/ui/chart.ts +577 -0
- package/src/ui/checks.tsx +203 -0
- package/src/ui/child-index.tsx +44 -0
- package/src/ui/children.ts +43 -0
- package/src/ui/chips.tsx +49 -0
- package/src/ui/cmd.tsx +27 -0
- package/src/ui/columns.tsx +31 -0
- package/src/ui/comments.tsx +544 -0
- package/src/ui/compare.tsx +67 -0
- package/src/ui/decision.tsx +79 -0
- package/src/ui/deps.tsx +78 -0
- package/src/ui/details.tsx +43 -0
- package/src/ui/diff-parse.ts +307 -0
- package/src/ui/diff.tsx +458 -0
- package/src/ui/diffstat.tsx +58 -0
- package/src/ui/due.tsx +65 -0
- package/src/ui/effort.tsx +29 -0
- package/src/ui/endpoints.tsx +118 -0
- package/src/ui/envvars.tsx +96 -0
- package/src/ui/figure.tsx +37 -0
- package/src/ui/file-icon.ts +1020 -0
- package/src/ui/file-link.ts +32 -0
- package/src/ui/file-ref.tsx +39 -0
- package/src/ui/files.tsx +112 -0
- package/src/ui/findings.tsx +85 -0
- package/src/ui/flow.tsx +73 -0
- package/src/ui/funnel.tsx +121 -0
- package/src/ui/gantt.tsx +443 -0
- package/src/ui/gauges.tsx +135 -0
- package/src/ui/glossary.tsx +29 -0
- package/src/ui/graph-layout.ts +149 -0
- package/src/ui/graph-specs.tsx +102 -0
- package/src/ui/graph.tsx +278 -0
- package/src/ui/grid.tsx +119 -0
- package/src/ui/hypothesis.tsx +94 -0
- package/src/ui/icon.tsx +80 -0
- package/src/ui/incident.tsx +130 -0
- package/src/ui/index.ts +342 -0
- package/src/ui/ins-del.tsx +41 -0
- package/src/ui/json.tsx +190 -0
- package/src/ui/layout.ts +9 -0
- package/src/ui/line-chart.tsx +251 -0
- package/src/ui/matrix.tsx +208 -0
- package/src/ui/meta.tsx +73 -0
- package/src/ui/option.tsx +64 -0
- package/src/ui/owner.tsx +40 -0
- package/src/ui/packages.tsx +104 -0
- package/src/ui/pathway.tsx +96 -0
- package/src/ui/phase.tsx +41 -0
- package/src/ui/pie-chart.tsx +170 -0
- package/src/ui/plan.tsx +77 -0
- package/src/ui/pre.tsx +175 -0
- package/src/ui/priority.tsx +51 -0
- package/src/ui/props.tsx +77 -0
- package/src/ui/quadrant.tsx +172 -0
- package/src/ui/radar.tsx +200 -0
- package/src/ui/ref.tsx +160 -0
- package/src/ui/release.tsx +178 -0
- package/src/ui/req.tsx +42 -0
- package/src/ui/review.tsx +133 -0
- package/src/ui/risk.tsx +67 -0
- package/src/ui/sankey.tsx +287 -0
- package/src/ui/scatter.tsx +237 -0
- package/src/ui/schema.tsx +113 -0
- package/src/ui/score.tsx +105 -0
- package/src/ui/search.tsx +120 -0
- package/src/ui/series.tsx +38 -0
- package/src/ui/severity.tsx +69 -0
- package/src/ui/shadcn.tsx +216 -0
- package/src/ui/spark.tsx +86 -0
- package/src/ui/stack.tsx +57 -0
- package/src/ui/stats.tsx +63 -0
- package/src/ui/status-badge.tsx +66 -0
- package/src/ui/statuspage.tsx +238 -0
- package/src/ui/steps.tsx +74 -0
- package/src/ui/summary.tsx +41 -0
- package/src/ui/symbol-ref.tsx +73 -0
- package/src/ui/terminal.tsx +117 -0
- package/src/ui/tests.tsx +218 -0
- package/src/ui/timeline.tsx +63 -0
- package/src/ui/toc.tsx +56 -0
- package/src/ui/tones.ts +187 -0
- package/src/ui/trace.tsx +69 -0
- package/src/ui/tree.tsx +281 -0
- package/src/ui/treemap.tsx +128 -0
- package/src/ui/venn.tsx +258 -0
- package/src/ui/verdict.tsx +78 -0
- 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
|
+
};
|