@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,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
|
|
4
|
+
import { cn } from "cn";
|
|
5
|
+
|
|
6
|
+
function TooltipProvider({
|
|
7
|
+
delay = 0,
|
|
8
|
+
...props
|
|
9
|
+
}: TooltipPrimitive.Provider.Props) {
|
|
10
|
+
return (
|
|
11
|
+
<TooltipPrimitive.Provider
|
|
12
|
+
data-slot="tooltip-provider"
|
|
13
|
+
delay={delay}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
|
|
20
|
+
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
|
|
24
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function TooltipContent({
|
|
28
|
+
className,
|
|
29
|
+
side = "top",
|
|
30
|
+
sideOffset = 4,
|
|
31
|
+
align = "center",
|
|
32
|
+
alignOffset = 0,
|
|
33
|
+
children,
|
|
34
|
+
...props
|
|
35
|
+
}: TooltipPrimitive.Popup.Props &
|
|
36
|
+
Pick<
|
|
37
|
+
TooltipPrimitive.Positioner.Props,
|
|
38
|
+
"align" | "alignOffset" | "side" | "sideOffset"
|
|
39
|
+
>) {
|
|
40
|
+
return (
|
|
41
|
+
<TooltipPrimitive.Portal>
|
|
42
|
+
<TooltipPrimitive.Positioner
|
|
43
|
+
align={align}
|
|
44
|
+
alignOffset={alignOffset}
|
|
45
|
+
side={side}
|
|
46
|
+
sideOffset={sideOffset}
|
|
47
|
+
className="isolate z-50"
|
|
48
|
+
>
|
|
49
|
+
<TooltipPrimitive.Popup
|
|
50
|
+
data-slot="tooltip-content"
|
|
51
|
+
className={cn(
|
|
52
|
+
"bg-foreground text-background data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md px-3 py-1.5 text-xs has-data-[slot=kbd]:pr-1.5 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm",
|
|
53
|
+
className
|
|
54
|
+
)}
|
|
55
|
+
{...props}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
|
|
59
|
+
</TooltipPrimitive.Popup>
|
|
60
|
+
</TooltipPrimitive.Positioner>
|
|
61
|
+
</TooltipPrimitive.Portal>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Public entry: `@suzumiyaaoba/mdxr/components`
|
|
2
|
+
// Re-export the built-in catalog so custom components can compose them.
|
|
3
|
+
// `export *` keeps the surface complete — the hydration bundle never loads
|
|
4
|
+
// this barrel (it resolves each used name straight to its leaf module).
|
|
5
|
+
export * from "./ui/index.js";
|
|
6
|
+
export { DocContext } from "./doc-context.js";
|
|
7
|
+
export {
|
|
8
|
+
defineComponent,
|
|
9
|
+
textOf,
|
|
10
|
+
type ComponentMeta,
|
|
11
|
+
type MdxrComponent,
|
|
12
|
+
} from "./define.js";
|
|
13
|
+
|
|
14
|
+
// shadcn/ui (Base UI) component set — composable from project components.
|
|
15
|
+
export * from "./components/ui/accordion.js";
|
|
16
|
+
export * from "./components/ui/alert.js";
|
|
17
|
+
export * from "./components/ui/alert-dialog.js";
|
|
18
|
+
export * from "./components/ui/aspect-ratio.js";
|
|
19
|
+
export * from "./components/ui/attachment.js";
|
|
20
|
+
export * from "./components/ui/avatar.js";
|
|
21
|
+
export * from "./components/ui/badge.js";
|
|
22
|
+
export * from "./components/ui/breadcrumb.js";
|
|
23
|
+
export * from "./components/ui/bubble.js";
|
|
24
|
+
export * from "./components/ui/button.js";
|
|
25
|
+
export * from "./components/ui/button-group.js";
|
|
26
|
+
export * from "./components/ui/calendar.js";
|
|
27
|
+
export * from "./components/ui/card.js";
|
|
28
|
+
export * from "./components/ui/carousel.js";
|
|
29
|
+
export * from "./components/ui/chart.js";
|
|
30
|
+
export * from "./components/ui/checkbox.js";
|
|
31
|
+
export * from "./components/ui/collapsible.js";
|
|
32
|
+
export * from "./components/ui/combobox.js";
|
|
33
|
+
export * from "./components/ui/command.js";
|
|
34
|
+
export * from "./components/ui/context-menu.js";
|
|
35
|
+
export * from "./components/ui/dialog.js";
|
|
36
|
+
export * from "./components/ui/direction.js";
|
|
37
|
+
export * from "./components/ui/drawer.js";
|
|
38
|
+
export * from "./components/ui/dropdown-menu.js";
|
|
39
|
+
export * from "./components/ui/empty.js";
|
|
40
|
+
export * from "./components/ui/field.js";
|
|
41
|
+
export * from "./components/ui/fieldset.js";
|
|
42
|
+
export * from "./components/ui/frame.js";
|
|
43
|
+
export * from "./components/ui/hover-card.js";
|
|
44
|
+
export * from "./components/ui/input.js";
|
|
45
|
+
export * from "./components/ui/input-group.js";
|
|
46
|
+
export * from "./components/ui/input-otp.js";
|
|
47
|
+
export * from "./components/ui/item.js";
|
|
48
|
+
export * from "./components/ui/kbd.js";
|
|
49
|
+
export * from "./components/ui/label.js";
|
|
50
|
+
export * from "./components/ui/marker.js";
|
|
51
|
+
export * from "./components/ui/menubar.js";
|
|
52
|
+
export * from "./components/ui/message.js";
|
|
53
|
+
export * from "./components/ui/message-scroller.js";
|
|
54
|
+
export * from "./components/ui/meter.js";
|
|
55
|
+
export * from "./components/ui/native-select.js";
|
|
56
|
+
export * from "./components/ui/navigation-menu.js";
|
|
57
|
+
export * from "./components/ui/pagination.js";
|
|
58
|
+
export * from "./components/ui/popover.js";
|
|
59
|
+
export * from "./components/ui/progress.js";
|
|
60
|
+
export * from "./components/ui/questionnaire.js";
|
|
61
|
+
export * from "./components/ui/radio-group.js";
|
|
62
|
+
export * from "./components/ui/resizable.js";
|
|
63
|
+
export * from "./components/ui/scroll-area.js";
|
|
64
|
+
export * from "./components/ui/select.js";
|
|
65
|
+
export * from "./components/ui/separator.js";
|
|
66
|
+
export * from "./components/ui/sheet.js";
|
|
67
|
+
export * from "./components/ui/sidebar.js";
|
|
68
|
+
export * from "./components/ui/skeleton.js";
|
|
69
|
+
export * from "./components/ui/slider.js";
|
|
70
|
+
export * from "./components/ui/spinner.js";
|
|
71
|
+
export * from "./components/ui/switch.js";
|
|
72
|
+
export * from "./components/ui/table.js";
|
|
73
|
+
export * from "./components/ui/tabs.js";
|
|
74
|
+
export * from "./components/ui/textarea.js";
|
|
75
|
+
export * from "./components/ui/toast.js";
|
|
76
|
+
export * from "./components/ui/toggle.js";
|
|
77
|
+
export * from "./components/ui/toggle-group.js";
|
|
78
|
+
export * from "./components/ui/tooltip.js";
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { isRecord, nonEmpty } from "./guards.js";
|
|
5
|
+
import { loadUserModule } from "./load-user-module.js";
|
|
6
|
+
|
|
7
|
+
/** Shape of `mdxr.config.ts` in a project that uses mdxr. */
|
|
8
|
+
export interface MdxrConfig {
|
|
9
|
+
/** Path to a TS/TSX module whose named exports are extra MDX components. */
|
|
10
|
+
components?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Editor used for file links (`vscode` default). A known name (`cursor`,
|
|
13
|
+
* `zed`, `vscode-insiders`, `windsurf`, `sublime`, `textmate`, `idea`), a
|
|
14
|
+
* `{path}`/`{line}` URL template, or `"none"` to disable. Frontmatter
|
|
15
|
+
* `editor:` overrides per document.
|
|
16
|
+
*/
|
|
17
|
+
editor?: string;
|
|
18
|
+
/** Path to a CSS file with `@theme` overrides / extra utilities. */
|
|
19
|
+
theme?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const defineConfig = (config: MdxrConfig): MdxrConfig => config;
|
|
23
|
+
|
|
24
|
+
export interface ResolvedConfig {
|
|
25
|
+
componentsPath?: string;
|
|
26
|
+
themePath?: string;
|
|
27
|
+
/** Bundled mdxr.config code (Tailwind scan source). */
|
|
28
|
+
componentsCode?: string;
|
|
29
|
+
dir: string;
|
|
30
|
+
editor?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const CONFIG_FILES = [
|
|
34
|
+
"mdxr.config.ts",
|
|
35
|
+
"mdxr.config.mts",
|
|
36
|
+
"mdxr.config.js",
|
|
37
|
+
"mdxr.config.mjs",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export const loadConfig = async (dir: string): Promise<ResolvedConfig> => {
|
|
41
|
+
const name = CONFIG_FILES.find((n) => fs.existsSync(path.join(dir, n)));
|
|
42
|
+
if (name === undefined) {
|
|
43
|
+
return { dir };
|
|
44
|
+
}
|
|
45
|
+
const { module: mod, code } = await loadUserModule(path.join(dir, name));
|
|
46
|
+
const raw = isRecord(mod.default) ? mod.default : {};
|
|
47
|
+
return {
|
|
48
|
+
componentsCode: code,
|
|
49
|
+
componentsPath: nonEmpty(raw.components)
|
|
50
|
+
? path.resolve(dir, raw.components)
|
|
51
|
+
: undefined,
|
|
52
|
+
dir,
|
|
53
|
+
editor: nonEmpty(raw.editor) ? raw.editor : undefined,
|
|
54
|
+
themePath: nonEmpty(raw.theme) ? path.resolve(dir, raw.theme) : undefined,
|
|
55
|
+
};
|
|
56
|
+
};
|
package/src/define.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Fragment, isValidElement } from "react";
|
|
2
|
+
import type { ReactElement, ReactNode } from "react";
|
|
3
|
+
import type { GenericSchema, InferOutput } from "valibot";
|
|
4
|
+
import { safeParse } from "valibot";
|
|
5
|
+
|
|
6
|
+
import { isRecord } from "./guards.js";
|
|
7
|
+
|
|
8
|
+
/** Metadata attached to a component for `mdxr catalog` and prop validation. */
|
|
9
|
+
export interface ComponentMeta {
|
|
10
|
+
description?: string;
|
|
11
|
+
/** Valibot schema for props (MDX attributes arrive as strings; `children` is passed through). */
|
|
12
|
+
schema?: GenericSchema;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Props as received from a document: arbitrary attributes plus children. */
|
|
16
|
+
export interface DocProps extends Record<string, unknown> {
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A component usable in an mdxr document. Props are intentionally loose —
|
|
22
|
+
* documents are data, and prop validation happens via `defineComponent`
|
|
23
|
+
* schemas at render time. Only ever invoked by MDX/React.
|
|
24
|
+
*/
|
|
25
|
+
export type AnyComponent = ((props: DocProps) => ReactNode) & {
|
|
26
|
+
__mdxr?: ComponentMeta;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Name → component map passed to MDX's `components` prop. */
|
|
30
|
+
export type ComponentMap = Record<string, AnyComponent>;
|
|
31
|
+
|
|
32
|
+
export type MdxrComponent = AnyComponent;
|
|
33
|
+
|
|
34
|
+
type PropsOf<S> = S extends GenericSchema
|
|
35
|
+
? InferOutput<S>
|
|
36
|
+
: Record<string, unknown>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate `props` against `schema`, throwing the catalog's `Invalid props`
|
|
40
|
+
* error on failure. `tag` names the element in the message
|
|
41
|
+
* (`Invalid props on <Node>`); without it the bare `Invalid props` form is
|
|
42
|
+
* reported — the shape `defineComponent` produces.
|
|
43
|
+
*/
|
|
44
|
+
export const parseProps = <S extends GenericSchema>(
|
|
45
|
+
schema: S,
|
|
46
|
+
props: unknown,
|
|
47
|
+
tag?: string
|
|
48
|
+
): InferOutput<S> => {
|
|
49
|
+
const r = safeParse(schema, props);
|
|
50
|
+
if (!r.success) {
|
|
51
|
+
const detail = r.issues
|
|
52
|
+
.map(
|
|
53
|
+
(i) =>
|
|
54
|
+
`${i.path?.map((p) => String(p.key)).join(".") ?? "props"}: ${i.message}`
|
|
55
|
+
)
|
|
56
|
+
.join("; ");
|
|
57
|
+
throw new Error(
|
|
58
|
+
tag === undefined
|
|
59
|
+
? `Invalid props: ${detail}`
|
|
60
|
+
: `Invalid props on <${tag}>: ${detail}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return r.output;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Wrap a render function with prop validation + catalog metadata.
|
|
68
|
+
* Prop types are inferred from the valibot schema, so the render callback
|
|
69
|
+
* is fully typed. Validation failures surface as document errors so the
|
|
70
|
+
* agent can self-correct.
|
|
71
|
+
*/
|
|
72
|
+
export const defineComponent = <
|
|
73
|
+
S extends GenericSchema | undefined = undefined,
|
|
74
|
+
>(
|
|
75
|
+
meta: { description?: string; schema?: S },
|
|
76
|
+
render: (props: PropsOf<S> & { children?: ReactNode }) => ReactElement | null
|
|
77
|
+
): MdxrComponent => {
|
|
78
|
+
const Comp = (props: DocProps): ReactElement | null => {
|
|
79
|
+
const parsed: unknown =
|
|
80
|
+
meta.schema === undefined ? props : parseProps(meta.schema, props);
|
|
81
|
+
// Validated above by the schema (or intentionally loose without one).
|
|
82
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
83
|
+
return render(parsed as PropsOf<S> & { children?: ReactNode });
|
|
84
|
+
};
|
|
85
|
+
Comp.__mdxr = meta;
|
|
86
|
+
return Comp;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Normalize a `children` prop into a flat array: nested arrays and fragments
|
|
91
|
+
* are unwrapped, and null/undefined/boolean nodes are dropped. Used instead of
|
|
92
|
+
* `React.Children` utilities, which lint rules discourage.
|
|
93
|
+
*/
|
|
94
|
+
export const flattenChildren = (node: ReactNode): ReactNode[] => {
|
|
95
|
+
if (Array.isArray(node)) {
|
|
96
|
+
return node.flatMap(flattenChildren);
|
|
97
|
+
}
|
|
98
|
+
if (node === null || node === undefined || typeof node === "boolean") {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
if (
|
|
102
|
+
isValidElement<{ children?: ReactNode }>(node) &&
|
|
103
|
+
node.type === Fragment
|
|
104
|
+
) {
|
|
105
|
+
return flattenChildren(node.props.children);
|
|
106
|
+
}
|
|
107
|
+
return [node];
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Extract all text from a React node tree (used for copy-to-clipboard payloads). */
|
|
111
|
+
export const textOf = (node: unknown): string => {
|
|
112
|
+
if (typeof node === "string" || typeof node === "number") {
|
|
113
|
+
return String(node);
|
|
114
|
+
}
|
|
115
|
+
if (Array.isArray(node)) {
|
|
116
|
+
return node.map(textOf).join("");
|
|
117
|
+
}
|
|
118
|
+
if (isRecord(node) && isRecord(node.props)) {
|
|
119
|
+
return textOf(node.props.children);
|
|
120
|
+
}
|
|
121
|
+
return "";
|
|
122
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createContext } from "react";
|
|
2
|
+
|
|
3
|
+
export interface DocContextValue {
|
|
4
|
+
/**
|
|
5
|
+
* Build an editor URL (`vscode://file/…`, per the resolved `editor` setting)
|
|
6
|
+
* for a document-relative path, or undefined when file links are disabled or
|
|
7
|
+
* the file does not exist on disk.
|
|
8
|
+
*/
|
|
9
|
+
fileLink?: (relPath: string, line?: string) => string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* The document's render timestamp. Relative-time components (`<Due>`) must
|
|
12
|
+
* read "now" from here: the value is serialized into the hydration payload,
|
|
13
|
+
* so client rendering sees the same instant SSR did — a render crossing
|
|
14
|
+
* midnight can't produce a hydration mismatch.
|
|
15
|
+
*/
|
|
16
|
+
now?: Date;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Render-time document info shared with components. */
|
|
20
|
+
export const DocContext = createContext<DocContextValue>({});
|
package/src/editor.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { nonEmpty, urlScheme } from "./guards.js";
|
|
2
|
+
|
|
3
|
+
type EditorLink = (absPath: string, line?: string) => string;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Path → URI path segment-by-segment. `encodeURI` would leave `#`, `?`,
|
|
7
|
+
* `%`, `&` intact and they parse as delimiters inside the editor URL;
|
|
8
|
+
* `encodeURIComponent` on the whole path would mangle `/`. Backslashes are
|
|
9
|
+
* normalized so Windows paths survive too (`C:\x` → `C%3A/x`).
|
|
10
|
+
*/
|
|
11
|
+
const encodePath = (p: string): string =>
|
|
12
|
+
p
|
|
13
|
+
.replaceAll("\\", "/")
|
|
14
|
+
.split("/")
|
|
15
|
+
.map((seg) => encodeURIComponent(seg))
|
|
16
|
+
.join("/");
|
|
17
|
+
|
|
18
|
+
/** `scheme://file/{abs}:{line}` — the VS Code URL convention most editors share. */
|
|
19
|
+
const atLine =
|
|
20
|
+
(scheme: string): EditorLink =>
|
|
21
|
+
(p, l) =>
|
|
22
|
+
`${scheme}://file${encodePath(p)}${nonEmpty(l) ? `:${l}` : ""}`;
|
|
23
|
+
|
|
24
|
+
/** `{scheme}://open?url=file://{abs}&line={n}` — Sublime/TextMate style. */
|
|
25
|
+
const queryUrl =
|
|
26
|
+
(scheme: string): EditorLink =>
|
|
27
|
+
(p, l) =>
|
|
28
|
+
`${scheme}://open?url=file://${encodePath(p)}${nonEmpty(l) ? `&line=${l}` : ""}`;
|
|
29
|
+
|
|
30
|
+
/** Known editors; unknown names fall back to the `scheme://file` convention. */
|
|
31
|
+
export const EDITORS: Record<string, EditorLink> = {
|
|
32
|
+
cursor: atLine("cursor"),
|
|
33
|
+
idea: (p, l) =>
|
|
34
|
+
`idea://open?file=${encodePath(p)}${nonEmpty(l) ? `&line=${l}` : ""}`,
|
|
35
|
+
sublime: queryUrl("subl"),
|
|
36
|
+
textmate: queryUrl("txmt"),
|
|
37
|
+
vscode: atLine("vscode"),
|
|
38
|
+
"vscode-insiders": atLine("vscode-insiders"),
|
|
39
|
+
windsurf: atLine("windsurf"),
|
|
40
|
+
zed: atLine("zed"),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Schemes that execute script when an `<a href>` is clicked. `editor` is a
|
|
45
|
+
* config value, but frontmatter can also supply it — a document must not be
|
|
46
|
+
* able to mint `javascript:`/`data:` links through file references.
|
|
47
|
+
*/
|
|
48
|
+
const SCRIPTABLE = new Set(["data", "javascript", "vbscript"]);
|
|
49
|
+
|
|
50
|
+
const build = (
|
|
51
|
+
editor: string | undefined,
|
|
52
|
+
absPath: string,
|
|
53
|
+
line?: string
|
|
54
|
+
): string | undefined => {
|
|
55
|
+
const e = nonEmpty(editor) ? editor : "vscode";
|
|
56
|
+
if (e === "none") {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const known = EDITORS[e];
|
|
60
|
+
if (known !== undefined) {
|
|
61
|
+
return known(absPath, line);
|
|
62
|
+
}
|
|
63
|
+
if (e.includes("{path}")) {
|
|
64
|
+
return e
|
|
65
|
+
.replaceAll("{path}", encodePath(absPath))
|
|
66
|
+
.replaceAll("{line}", line ?? "");
|
|
67
|
+
}
|
|
68
|
+
// A bare name becomes a `scheme://file` editor — but only if it could
|
|
69
|
+
// actually be a URI scheme; otherwise the link would be malformed.
|
|
70
|
+
if (!/^[a-z][a-z0-9+.-]*$/iu.test(e)) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
return atLine(e)(absPath, line);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Editor URL for an absolute path. `editor` is a known name (see EDITORS),
|
|
78
|
+
* `"none"` (no link), a URL template with `{path}`/`{line}` placeholders
|
|
79
|
+
* (`myed://open?f={path}&l={line}`), or a bare name treated as a
|
|
80
|
+
* `scheme://file` editor. Undefined → vscode. Scriptable schemes are
|
|
81
|
+
* refused — `editor` can arrive from frontmatter, which is document input.
|
|
82
|
+
*/
|
|
83
|
+
export const editorUrl = (
|
|
84
|
+
editor: string | undefined,
|
|
85
|
+
absPath: string,
|
|
86
|
+
line?: string
|
|
87
|
+
): string | undefined => {
|
|
88
|
+
const url = build(editor, absPath, line);
|
|
89
|
+
if (url === undefined) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const scheme = urlScheme(url);
|
|
93
|
+
return scheme !== undefined && SCRIPTABLE.has(scheme) ? undefined : url;
|
|
94
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate the `--format` flag: omitted and "text" mean human-readable
|
|
3
|
+
* errors, "json" switches failure output to a machine-readable object.
|
|
4
|
+
*/
|
|
5
|
+
export const parseErrorFormat = (format?: string): "text" | "json" => {
|
|
6
|
+
if (format === undefined || format === "text") {
|
|
7
|
+
return "text";
|
|
8
|
+
}
|
|
9
|
+
if (format === "json") {
|
|
10
|
+
return "json";
|
|
11
|
+
}
|
|
12
|
+
throw new Error(`invalid --format "${format}" (expected text|json)`);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const formatError = (err: unknown): string => {
|
|
16
|
+
if (err instanceof Error) {
|
|
17
|
+
// vfile-style messages carry line/column for agent self-repair.
|
|
18
|
+
const e = err as Error & { line?: number; column?: number; file?: string };
|
|
19
|
+
const loc =
|
|
20
|
+
e.line === null || e.line === undefined
|
|
21
|
+
? ""
|
|
22
|
+
: `:${e.line}:${e.column ?? 0}`;
|
|
23
|
+
const file = e.file ?? "";
|
|
24
|
+
return `${file}${loc} ${e.message}`.trim();
|
|
25
|
+
}
|
|
26
|
+
return String(err);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const levenshtein = (a: string, b: string): number => {
|
|
30
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) => [
|
|
31
|
+
i,
|
|
32
|
+
...Array.from({ length: b.length }, () => 0),
|
|
33
|
+
]);
|
|
34
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
35
|
+
dp[0][j] = j;
|
|
36
|
+
}
|
|
37
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
38
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
39
|
+
dp[i][j] = Math.min(
|
|
40
|
+
dp[i - 1][j] + 1,
|
|
41
|
+
dp[i][j - 1] + 1,
|
|
42
|
+
dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return dp[a.length][b.length];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* MDX's "Expected component `<X>`" errors get a friendlier message: the
|
|
51
|
+
* closest catalog name (edit distance ≤ 3) plus the full list, so the agent
|
|
52
|
+
* can self-correct without a `mdxr catalog` round-trip.
|
|
53
|
+
*/
|
|
54
|
+
export const enhanceRenderError = (
|
|
55
|
+
err: unknown,
|
|
56
|
+
componentNames: string[]
|
|
57
|
+
): unknown => {
|
|
58
|
+
if (!(err instanceof Error)) {
|
|
59
|
+
return err;
|
|
60
|
+
}
|
|
61
|
+
const m = /Expected component [`"'](?<name>\w+)[`"']/u.exec(err.message);
|
|
62
|
+
const name = m?.groups?.name;
|
|
63
|
+
if (name === undefined) {
|
|
64
|
+
return err;
|
|
65
|
+
}
|
|
66
|
+
const names = componentNames.filter((n) => /^[A-Z]/u.test(n));
|
|
67
|
+
const [nearest] = names
|
|
68
|
+
.map((n) => ({ d: levenshtein(name.toLowerCase(), n.toLowerCase()), n }))
|
|
69
|
+
.toSorted((x, y) => x.d - y.d);
|
|
70
|
+
const hint =
|
|
71
|
+
nearest !== undefined && nearest.d <= 3
|
|
72
|
+
? ` Did you mean <${nearest.n}>?`
|
|
73
|
+
: "";
|
|
74
|
+
return new Error(
|
|
75
|
+
`Unknown component <${name}>.${hint} Available: ${names.join(", ")}. Add custom components via mdxr.config.ts.`,
|
|
76
|
+
{ cause: err }
|
|
77
|
+
);
|
|
78
|
+
};
|
package/src/guards.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** Runtime type guards shared across AST/config/module boundaries. */
|
|
2
|
+
|
|
3
|
+
import type { AnyComponent } from "./define.js";
|
|
4
|
+
|
|
5
|
+
export const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
6
|
+
typeof v === "object" && v !== null;
|
|
7
|
+
|
|
8
|
+
/** `React.memo`/`forwardRef`/`lazy` results are objects, not functions —
|
|
9
|
+
* but they are still valid element types for the MDX components map. */
|
|
10
|
+
const REACT_COMPONENT_OBJECTS = new Set<symbol>([
|
|
11
|
+
Symbol.for("react.memo"),
|
|
12
|
+
Symbol.for("react.forward_ref"),
|
|
13
|
+
Symbol.for("react.lazy"),
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export const isComponent = (v: unknown): v is AnyComponent =>
|
|
17
|
+
typeof v === "function" ||
|
|
18
|
+
(isRecord(v) &&
|
|
19
|
+
typeof v.$$typeof === "symbol" &&
|
|
20
|
+
REACT_COMPONENT_OBJECTS.has(v.$$typeof));
|
|
21
|
+
|
|
22
|
+
/** Treat "" as absent — document attributes arrive as strings. */
|
|
23
|
+
export const nonEmpty = (v: unknown): v is string =>
|
|
24
|
+
typeof v === "string" && v !== "";
|
|
25
|
+
|
|
26
|
+
/** String-or-undefined narrow — unlike `nonEmpty`, "" passes through. */
|
|
27
|
+
export const asString = (v: unknown): string | undefined =>
|
|
28
|
+
typeof v === "string" ? v : undefined;
|
|
29
|
+
|
|
30
|
+
/** Membership-guard factory: `isOneOf(STATUSES)` narrows to the list's union. */
|
|
31
|
+
export const isOneOf =
|
|
32
|
+
<T extends string>(list: readonly T[]) =>
|
|
33
|
+
(x: unknown): x is T =>
|
|
34
|
+
typeof x === "string" && (list as readonly string[]).includes(x);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Prototype-safe record lookup — `table[key]` only for own properties, so
|
|
38
|
+
* keys like "toString" or "constructor" can't leak members off `Object`'s
|
|
39
|
+
* prototype. Use wherever document-supplied strings index a table.
|
|
40
|
+
*/
|
|
41
|
+
export const own = <T>(
|
|
42
|
+
table: Readonly<Record<string, T>>,
|
|
43
|
+
key: string
|
|
44
|
+
): T | undefined => (Object.hasOwn(table, key) ? table[key] : undefined);
|
|
45
|
+
|
|
46
|
+
const SAFE_SCHEMES = new Set(["http", "https", "mailto", "tel"]);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The scheme a browser would parse out of `v`, lowercased — or undefined for
|
|
50
|
+
* scheme-less values. Browsers strip \t\n\r anywhere in a URL and remove
|
|
51
|
+
* leading/trailing C0 controls and spaces before scheme parsing
|
|
52
|
+
* (" javascript:…", "java\tscript:…", "\x01javascript:…" all execute), so
|
|
53
|
+
* the probe uses the same normalized form.
|
|
54
|
+
*/
|
|
55
|
+
export const urlScheme = (v: string): string | undefined => {
|
|
56
|
+
/* oxlint-disable no-control-regex -- C0 range is the point: the URL parser
|
|
57
|
+
strips it at the edges, so the probe must too */
|
|
58
|
+
const probe = v
|
|
59
|
+
.replaceAll(/[\t\n\r]/gu, "")
|
|
60
|
+
.replace(/^[\u0000-\u0020]+/u, "")
|
|
61
|
+
.replace(/[\u0000-\u0020]+$/u, "");
|
|
62
|
+
/* oxlint-enable no-control-regex */
|
|
63
|
+
return /^(?<scheme>[a-zA-Z][a-zA-Z0-9+.-]*):/u
|
|
64
|
+
.exec(probe)
|
|
65
|
+
?.groups?.scheme.toLowerCase();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Doc-supplied `href` props pass through to `<a href>` unescaped — a
|
|
70
|
+
* `javascript:`/`data:` scheme would execute on click. Allow only web
|
|
71
|
+
* schemes plus scheme-less values (anchors, relative and protocol-relative
|
|
72
|
+
* URLs); anything else renders as no link. Editor URLs (`vscode:` etc.)
|
|
73
|
+
* never take this path — they come from the server-side `fileLink`.
|
|
74
|
+
*/
|
|
75
|
+
export const safeHref = (v: unknown): string | undefined => {
|
|
76
|
+
if (typeof v !== "string" || v === "") {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const scheme = urlScheme(v);
|
|
80
|
+
if (scheme === undefined || SAFE_SCHEMES.has(scheme)) {
|
|
81
|
+
return v;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768;
|
|
4
|
+
|
|
5
|
+
export function useIsMobile() {
|
|
6
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>();
|
|
7
|
+
|
|
8
|
+
React.useEffect(() => {
|
|
9
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
10
|
+
const onChange = () => {
|
|
11
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
12
|
+
};
|
|
13
|
+
mql.addEventListener("change", onChange);
|
|
14
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
15
|
+
return () => {
|
|
16
|
+
mql.removeEventListener("change", onChange);
|
|
17
|
+
};
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
return !!isMobile;
|
|
21
|
+
}
|