@terrafirmagreg-team/mdx 1.2.0 → 1.2.2
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/package.json +3 -3
- package/src/components/docs-image.tsx +62 -62
- package/src/components/mdx-components.tsx +158 -158
- package/src/components/story-fields.tsx +318 -318
- package/src/components/story-panel.tsx +181 -181
- package/src/lib/md-plugins.ts +1 -0
- package/src/lib/mdx/directive-utils.ts +118 -118
- package/src/lib/pack-assets.ts +46 -46
- package/src/lib/story-remark.ts +468 -468
- package/src/lib/story.ts +11 -11
|
@@ -1,181 +1,181 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
StoryProvider,
|
|
5
|
-
useStory,
|
|
6
|
-
useStoryArgs,
|
|
7
|
-
type WithControlProps,
|
|
8
|
-
} from '@fumadocs/story';
|
|
9
|
-
import { StoryFields } from './story-fields';
|
|
10
|
-
import { remarkFor, tweakControls } from '../lib/story-remark';
|
|
11
|
-
import { highlightRemark } from '../lib/remark-highlight';
|
|
12
|
-
import { useCopyButton } from '@terrafirmagreg-team/ui/hooks/use-copy-button';
|
|
13
|
-
import { Check, Clipboard } from 'lucide-react';
|
|
14
|
-
import {
|
|
15
|
-
Select,
|
|
16
|
-
SelectContent,
|
|
17
|
-
SelectItem,
|
|
18
|
-
SelectTrigger,
|
|
19
|
-
SelectValue,
|
|
20
|
-
} from '@terrafirmagreg-team/ui/components/ui/select';
|
|
21
|
-
import {
|
|
22
|
-
Component,
|
|
23
|
-
createElement,
|
|
24
|
-
useMemo,
|
|
25
|
-
type ErrorInfo,
|
|
26
|
-
type ReactNode,
|
|
27
|
-
} from 'react';
|
|
28
|
-
import { CircleAlert, RotateCcw } from 'lucide-react';
|
|
29
|
-
|
|
30
|
-
function RemarkHint() {
|
|
31
|
-
const { displayName, preset, variant } = useStory();
|
|
32
|
-
const args = useStoryArgs();
|
|
33
|
-
const text = useMemo(
|
|
34
|
-
() => remarkFor(displayName ?? 'component', args, preset?.controls, variant),
|
|
35
|
-
[displayName, args, preset, variant],
|
|
36
|
-
);
|
|
37
|
-
const tokens = useMemo(() => highlightRemark(text), [text]);
|
|
38
|
-
const { checked, onClick } = useCopyButton(() =>
|
|
39
|
-
navigator.clipboard.writeText(text),
|
|
40
|
-
);
|
|
41
|
-
return (
|
|
42
|
-
<div className="relative border-t">
|
|
43
|
-
<pre className="overflow-x-auto px-4 py-2.5 pe-9 font-mono text-xs leading-relaxed text-fd-muted-foreground whitespace-pre-wrap">
|
|
44
|
-
{tokens.map((t, i) => (
|
|
45
|
-
<span key={i} className={t.cls}>
|
|
46
|
-
{t.text}
|
|
47
|
-
</span>
|
|
48
|
-
))}
|
|
49
|
-
</pre>
|
|
50
|
-
<button
|
|
51
|
-
type="button"
|
|
52
|
-
aria-label="Copy markdown"
|
|
53
|
-
title="Copy markdown"
|
|
54
|
-
onClick={onClick}
|
|
55
|
-
className="absolute top-1.5 right-1.5 rounded-md p-1.5 text-fd-muted-foreground hover:bg-fd-accent hover:text-fd-foreground"
|
|
56
|
-
>
|
|
57
|
-
{checked ? (
|
|
58
|
-
<Check className="size-3.5" />
|
|
59
|
-
) : (
|
|
60
|
-
<Clipboard className="size-3.5" />
|
|
61
|
-
)}
|
|
62
|
-
</button>
|
|
63
|
-
</div>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
class PreviewBoundary extends Component<
|
|
68
|
-
{ children: ReactNode; resetKey: unknown },
|
|
69
|
-
{ error: Error | null }
|
|
70
|
-
> {
|
|
71
|
-
state = { error: null as Error | null };
|
|
72
|
-
static getDerivedStateFromError(error: Error) {
|
|
73
|
-
return { error };
|
|
74
|
-
}
|
|
75
|
-
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
76
|
-
console.error(error, info);
|
|
77
|
-
}
|
|
78
|
-
componentDidUpdate(prev: { resetKey: unknown }) {
|
|
79
|
-
if (prev.resetKey !== this.props.resetKey && this.state.error)
|
|
80
|
-
this.setState({ error: null });
|
|
81
|
-
}
|
|
82
|
-
render() {
|
|
83
|
-
if (this.state.error)
|
|
84
|
-
return (
|
|
85
|
-
<div role="alert" className="flex items-center gap-2 text-sm">
|
|
86
|
-
<CircleAlert className="size-3.5 shrink-0 text-fd-error" />
|
|
87
|
-
<span className="font-mono text-xs break-all text-fd-muted-foreground">
|
|
88
|
-
{this.state.error.message}
|
|
89
|
-
</span>
|
|
90
|
-
</div>
|
|
91
|
-
);
|
|
92
|
-
return this.props.children;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function VariantSelect() {
|
|
97
|
-
const { presets, variant, setVariant } = useStory();
|
|
98
|
-
return (
|
|
99
|
-
<Select value={variant} onValueChange={(v) => v && setVariant(v)}>
|
|
100
|
-
<SelectTrigger className="h-7 w-auto max-w-64 gap-1.5 border-0 bg-transparent px-2 text-xs hover:bg-fd-accent focus:ring-0 focus-visible:ring-2">
|
|
101
|
-
<span className="text-fd-muted-foreground">Variant</span>
|
|
102
|
-
<SelectValue className="truncate font-medium" />
|
|
103
|
-
</SelectTrigger>
|
|
104
|
-
<SelectContent align="end">
|
|
105
|
-
{presets.map((item) => (
|
|
106
|
-
<SelectItem
|
|
107
|
-
key={item.variant}
|
|
108
|
-
value={item.variant}
|
|
109
|
-
className="text-xs"
|
|
110
|
-
>
|
|
111
|
-
{item.variant}
|
|
112
|
-
</SelectItem>
|
|
113
|
-
))}
|
|
114
|
-
</SelectContent>
|
|
115
|
-
</Select>
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function Content() {
|
|
120
|
-
const { presets, displayName, preset, variant, setVariant, Component } =
|
|
121
|
-
useStory();
|
|
122
|
-
const args = useStoryArgs();
|
|
123
|
-
const reset = () => setVariant(variant);
|
|
124
|
-
return (
|
|
125
|
-
<div className="not-prose overflow-hidden rounded-xl border bg-fd-card text-fd-card-foreground">
|
|
126
|
-
{(displayName || presets.length > 1) && (
|
|
127
|
-
<div className="flex h-10 items-center justify-end gap-1 border-b ps-4 pe-1.5">
|
|
128
|
-
{displayName && (
|
|
129
|
-
<p className="me-auto truncate font-mono text-[0.8125rem] font-medium">
|
|
130
|
-
{displayName}
|
|
131
|
-
</p>
|
|
132
|
-
)}
|
|
133
|
-
<button
|
|
134
|
-
type="button"
|
|
135
|
-
aria-label="Reset"
|
|
136
|
-
title="Reset"
|
|
137
|
-
onClick={reset}
|
|
138
|
-
className="rounded-md p-1.5 text-fd-muted-foreground hover:bg-fd-accent"
|
|
139
|
-
>
|
|
140
|
-
<RotateCcw className="size-3.5" />
|
|
141
|
-
</button>
|
|
142
|
-
{presets.length > 1 && <VariantSelect />}
|
|
143
|
-
</div>
|
|
144
|
-
)}
|
|
145
|
-
<div className="bg-fd-background p-4 not-last:border-b">
|
|
146
|
-
<PreviewBoundary resetKey={args}>
|
|
147
|
-
{createElement(Component, args)}
|
|
148
|
-
</PreviewBoundary>
|
|
149
|
-
</div>
|
|
150
|
-
{preset && (
|
|
151
|
-
<StoryFields
|
|
152
|
-
key={variant}
|
|
153
|
-
node={preset.controls}
|
|
154
|
-
storyKey={displayName ?? ''}
|
|
155
|
-
/>
|
|
156
|
-
)}
|
|
157
|
-
<RemarkHint />
|
|
158
|
-
</div>
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function DocsWithControl({ presets, ...rest }: WithControlProps) {
|
|
163
|
-
const tweaked = useMemo(
|
|
164
|
-
() =>
|
|
165
|
-
presets.map((preset) => ({
|
|
166
|
-
...preset,
|
|
167
|
-
controls: tweakControls(
|
|
168
|
-
rest.displayName ?? '',
|
|
169
|
-
preset.controls,
|
|
170
|
-
preset.defaultValues,
|
|
171
|
-
),
|
|
172
|
-
})),
|
|
173
|
-
[presets, rest.displayName],
|
|
174
|
-
);
|
|
175
|
-
return (
|
|
176
|
-
<StoryProvider {...rest} presets={tweaked}>
|
|
177
|
-
<Content />
|
|
178
|
-
</StoryProvider>
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
StoryProvider,
|
|
5
|
+
useStory,
|
|
6
|
+
useStoryArgs,
|
|
7
|
+
type WithControlProps,
|
|
8
|
+
} from '@fumadocs/story';
|
|
9
|
+
import { StoryFields } from './story-fields';
|
|
10
|
+
import { remarkFor, tweakControls } from '../lib/story-remark';
|
|
11
|
+
import { highlightRemark } from '../lib/remark-highlight';
|
|
12
|
+
import { useCopyButton } from '@terrafirmagreg-team/ui/hooks/use-copy-button';
|
|
13
|
+
import { Check, Clipboard } from 'lucide-react';
|
|
14
|
+
import {
|
|
15
|
+
Select,
|
|
16
|
+
SelectContent,
|
|
17
|
+
SelectItem,
|
|
18
|
+
SelectTrigger,
|
|
19
|
+
SelectValue,
|
|
20
|
+
} from '@terrafirmagreg-team/ui/components/ui/select';
|
|
21
|
+
import {
|
|
22
|
+
Component,
|
|
23
|
+
createElement,
|
|
24
|
+
useMemo,
|
|
25
|
+
type ErrorInfo,
|
|
26
|
+
type ReactNode,
|
|
27
|
+
} from 'react';
|
|
28
|
+
import { CircleAlert, RotateCcw } from 'lucide-react';
|
|
29
|
+
|
|
30
|
+
function RemarkHint() {
|
|
31
|
+
const { displayName, preset, variant } = useStory();
|
|
32
|
+
const args = useStoryArgs();
|
|
33
|
+
const text = useMemo(
|
|
34
|
+
() => remarkFor(displayName ?? 'component', args, preset?.controls, variant),
|
|
35
|
+
[displayName, args, preset, variant],
|
|
36
|
+
);
|
|
37
|
+
const tokens = useMemo(() => highlightRemark(text), [text]);
|
|
38
|
+
const { checked, onClick } = useCopyButton(() =>
|
|
39
|
+
navigator.clipboard.writeText(text),
|
|
40
|
+
);
|
|
41
|
+
return (
|
|
42
|
+
<div className="relative border-t">
|
|
43
|
+
<pre className="overflow-x-auto px-4 py-2.5 pe-9 font-mono text-xs leading-relaxed text-fd-muted-foreground whitespace-pre-wrap">
|
|
44
|
+
{tokens.map((t, i) => (
|
|
45
|
+
<span key={i} className={t.cls}>
|
|
46
|
+
{t.text}
|
|
47
|
+
</span>
|
|
48
|
+
))}
|
|
49
|
+
</pre>
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
aria-label="Copy markdown"
|
|
53
|
+
title="Copy markdown"
|
|
54
|
+
onClick={onClick}
|
|
55
|
+
className="absolute top-1.5 right-1.5 rounded-md p-1.5 text-fd-muted-foreground hover:bg-fd-accent hover:text-fd-foreground"
|
|
56
|
+
>
|
|
57
|
+
{checked ? (
|
|
58
|
+
<Check className="size-3.5" />
|
|
59
|
+
) : (
|
|
60
|
+
<Clipboard className="size-3.5" />
|
|
61
|
+
)}
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class PreviewBoundary extends Component<
|
|
68
|
+
{ children: ReactNode; resetKey: unknown },
|
|
69
|
+
{ error: Error | null }
|
|
70
|
+
> {
|
|
71
|
+
state = { error: null as Error | null };
|
|
72
|
+
static getDerivedStateFromError(error: Error) {
|
|
73
|
+
return { error };
|
|
74
|
+
}
|
|
75
|
+
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
76
|
+
console.error(error, info);
|
|
77
|
+
}
|
|
78
|
+
componentDidUpdate(prev: { resetKey: unknown }) {
|
|
79
|
+
if (prev.resetKey !== this.props.resetKey && this.state.error)
|
|
80
|
+
this.setState({ error: null });
|
|
81
|
+
}
|
|
82
|
+
render() {
|
|
83
|
+
if (this.state.error)
|
|
84
|
+
return (
|
|
85
|
+
<div role="alert" className="flex items-center gap-2 text-sm">
|
|
86
|
+
<CircleAlert className="size-3.5 shrink-0 text-fd-error" />
|
|
87
|
+
<span className="font-mono text-xs break-all text-fd-muted-foreground">
|
|
88
|
+
{this.state.error.message}
|
|
89
|
+
</span>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
return this.props.children;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function VariantSelect() {
|
|
97
|
+
const { presets, variant, setVariant } = useStory();
|
|
98
|
+
return (
|
|
99
|
+
<Select value={variant} onValueChange={(v) => v && setVariant(v)}>
|
|
100
|
+
<SelectTrigger className="h-7 w-auto max-w-64 gap-1.5 border-0 bg-transparent px-2 text-xs hover:bg-fd-accent focus:ring-0 focus-visible:ring-2">
|
|
101
|
+
<span className="text-fd-muted-foreground">Variant</span>
|
|
102
|
+
<SelectValue className="truncate font-medium" />
|
|
103
|
+
</SelectTrigger>
|
|
104
|
+
<SelectContent align="end">
|
|
105
|
+
{presets.map((item) => (
|
|
106
|
+
<SelectItem
|
|
107
|
+
key={item.variant}
|
|
108
|
+
value={item.variant}
|
|
109
|
+
className="text-xs"
|
|
110
|
+
>
|
|
111
|
+
{item.variant}
|
|
112
|
+
</SelectItem>
|
|
113
|
+
))}
|
|
114
|
+
</SelectContent>
|
|
115
|
+
</Select>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function Content() {
|
|
120
|
+
const { presets, displayName, preset, variant, setVariant, Component } =
|
|
121
|
+
useStory();
|
|
122
|
+
const args = useStoryArgs();
|
|
123
|
+
const reset = () => setVariant(variant);
|
|
124
|
+
return (
|
|
125
|
+
<div className="not-prose overflow-hidden rounded-xl border bg-fd-card text-fd-card-foreground">
|
|
126
|
+
{(displayName || presets.length > 1) && (
|
|
127
|
+
<div className="flex h-10 items-center justify-end gap-1 border-b ps-4 pe-1.5">
|
|
128
|
+
{displayName && (
|
|
129
|
+
<p className="me-auto truncate font-mono text-[0.8125rem] font-medium">
|
|
130
|
+
{displayName}
|
|
131
|
+
</p>
|
|
132
|
+
)}
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
aria-label="Reset"
|
|
136
|
+
title="Reset"
|
|
137
|
+
onClick={reset}
|
|
138
|
+
className="rounded-md p-1.5 text-fd-muted-foreground hover:bg-fd-accent"
|
|
139
|
+
>
|
|
140
|
+
<RotateCcw className="size-3.5" />
|
|
141
|
+
</button>
|
|
142
|
+
{presets.length > 1 && <VariantSelect />}
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
<div className="bg-fd-background p-4 not-last:border-b">
|
|
146
|
+
<PreviewBoundary resetKey={args}>
|
|
147
|
+
{createElement(Component, args)}
|
|
148
|
+
</PreviewBoundary>
|
|
149
|
+
</div>
|
|
150
|
+
{preset && (
|
|
151
|
+
<StoryFields
|
|
152
|
+
key={variant}
|
|
153
|
+
node={preset.controls}
|
|
154
|
+
storyKey={displayName ?? ''}
|
|
155
|
+
/>
|
|
156
|
+
)}
|
|
157
|
+
<RemarkHint />
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function DocsWithControl({ presets, ...rest }: WithControlProps) {
|
|
163
|
+
const tweaked = useMemo(
|
|
164
|
+
() =>
|
|
165
|
+
presets.map((preset) => ({
|
|
166
|
+
...preset,
|
|
167
|
+
controls: tweakControls(
|
|
168
|
+
rest.displayName ?? '',
|
|
169
|
+
preset.controls,
|
|
170
|
+
preset.defaultValues,
|
|
171
|
+
),
|
|
172
|
+
})),
|
|
173
|
+
[presets, rest.displayName],
|
|
174
|
+
);
|
|
175
|
+
return (
|
|
176
|
+
<StoryProvider {...rest} presets={tweaked}>
|
|
177
|
+
<Content />
|
|
178
|
+
</StoryProvider>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
package/src/lib/md-plugins.ts
CHANGED
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
DirectiveNode,
|
|
3
|
-
JsxAttribute,
|
|
4
|
-
JsxAttributeValue,
|
|
5
|
-
MdastNode,
|
|
6
|
-
} from '../../types/mdx';
|
|
7
|
-
|
|
8
|
-
export type {
|
|
9
|
-
DirectiveNode,
|
|
10
|
-
JsxAttribute,
|
|
11
|
-
JsxAttributeValue,
|
|
12
|
-
JsxElement,
|
|
13
|
-
MdastNode,
|
|
14
|
-
} from '../../types/mdx';
|
|
15
|
-
|
|
16
|
-
import { visit, SKIP, CONTINUE } from 'unist-util-visit';
|
|
17
|
-
|
|
18
|
-
export function walkContainers(
|
|
19
|
-
tree: MdastNode,
|
|
20
|
-
fn: (node: DirectiveNode) => boolean | void,
|
|
21
|
-
): void {
|
|
22
|
-
visit(tree as never, 'containerDirective', (node) =>
|
|
23
|
-
fn(node as DirectiveNode) === false ? SKIP : CONTINUE,
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const DIRECTIVE_TYPES = [
|
|
28
|
-
'containerDirective',
|
|
29
|
-
'leafDirective',
|
|
30
|
-
'textDirective',
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
export function walkDirectives(
|
|
34
|
-
tree: MdastNode,
|
|
35
|
-
fn: (node: DirectiveNode) => boolean | void,
|
|
36
|
-
): void {
|
|
37
|
-
visit(tree as never, DIRECTIVE_TYPES, (node) =>
|
|
38
|
-
fn(node as DirectiveNode) === false ? SKIP : CONTINUE,
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function toJsxAttributes(node: DirectiveNode): JsxAttribute[] {
|
|
43
|
-
return Object.entries(node.attributes ?? {}).map(([name, value]) => ({
|
|
44
|
-
type: 'mdxJsxAttribute',
|
|
45
|
-
name,
|
|
46
|
-
value: value == null || value === '' ? jsxExpression(estreeBoolean(true)) : value,
|
|
47
|
-
}));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function labelText(node: MdastNode): string {
|
|
51
|
-
const t = node.type;
|
|
52
|
-
const v = (node as { value?: unknown }).value;
|
|
53
|
-
if (typeof v === 'string') return v;
|
|
54
|
-
if (t === 'textDirective' || t === 'leafDirective') {
|
|
55
|
-
const d = node as DirectiveNode;
|
|
56
|
-
const inner = (d.children ?? []).map(labelText).join('');
|
|
57
|
-
const attrs = Object.entries(d.attributes ?? {})
|
|
58
|
-
.map(([k, v]) => (v == null ? k : v === true ? k : `${k}="${v}"`))
|
|
59
|
-
.join(' ');
|
|
60
|
-
const prefix = t === 'leafDirective' ? '::' : ':';
|
|
61
|
-
return `${prefix}${d.name ?? ''}${inner ? `[${inner}]` : ''}${attrs ? `{${attrs}}` : ''}`;
|
|
62
|
-
}
|
|
63
|
-
return (node.children ?? []).map(labelText).join('');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function extractDirectiveLabel(node: DirectiveNode): string {
|
|
67
|
-
for (const child of node.children ?? []) {
|
|
68
|
-
if (child.type === 'paragraph' && child.data?.directiveLabel) {
|
|
69
|
-
return (child.children ?? []).map(labelText).join('');
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return '';
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function extractLeafLabel(node: MdastNode): string {
|
|
76
|
-
return (node.children ?? []).map(labelText).join('');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function contentChildren(node: DirectiveNode): MdastNode[] {
|
|
80
|
-
return (node.children ?? []).filter(
|
|
81
|
-
(child) =>
|
|
82
|
-
!(child.type === 'paragraph' && child.data?.directiveLabel),
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function jsxAttr(name: string, value: string | boolean): JsxAttribute {
|
|
87
|
-
return { type: 'mdxJsxAttribute', name, value };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function jsxExpression(expression: unknown): JsxAttributeValue {
|
|
91
|
-
return {
|
|
92
|
-
type: 'mdxJsxAttributeValueExpression',
|
|
93
|
-
value: '',
|
|
94
|
-
data: {
|
|
95
|
-
estree: {
|
|
96
|
-
type: 'Program',
|
|
97
|
-
body: [{ type: 'ExpressionStatement', expression }],
|
|
98
|
-
sourceType: 'script',
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function estreeString(value: string): unknown {
|
|
105
|
-
return { type: 'Literal', value, raw: JSON.stringify(value) };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function estreeStringArray(labels: string[]): unknown {
|
|
109
|
-
return {
|
|
110
|
-
type: 'ArrayExpression',
|
|
111
|
-
elements: labels.map(estreeString),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function estreeBoolean(value: boolean): unknown {
|
|
116
|
-
return { type: 'Literal', value, raw: String(value) };
|
|
117
|
-
}
|
|
118
|
-
|
|
1
|
+
import type {
|
|
2
|
+
DirectiveNode,
|
|
3
|
+
JsxAttribute,
|
|
4
|
+
JsxAttributeValue,
|
|
5
|
+
MdastNode,
|
|
6
|
+
} from '../../types/mdx';
|
|
7
|
+
|
|
8
|
+
export type {
|
|
9
|
+
DirectiveNode,
|
|
10
|
+
JsxAttribute,
|
|
11
|
+
JsxAttributeValue,
|
|
12
|
+
JsxElement,
|
|
13
|
+
MdastNode,
|
|
14
|
+
} from '../../types/mdx';
|
|
15
|
+
|
|
16
|
+
import { visit, SKIP, CONTINUE } from 'unist-util-visit';
|
|
17
|
+
|
|
18
|
+
export function walkContainers(
|
|
19
|
+
tree: MdastNode,
|
|
20
|
+
fn: (node: DirectiveNode) => boolean | void,
|
|
21
|
+
): void {
|
|
22
|
+
visit(tree as never, 'containerDirective', (node) =>
|
|
23
|
+
fn(node as DirectiveNode) === false ? SKIP : CONTINUE,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const DIRECTIVE_TYPES = [
|
|
28
|
+
'containerDirective',
|
|
29
|
+
'leafDirective',
|
|
30
|
+
'textDirective',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export function walkDirectives(
|
|
34
|
+
tree: MdastNode,
|
|
35
|
+
fn: (node: DirectiveNode) => boolean | void,
|
|
36
|
+
): void {
|
|
37
|
+
visit(tree as never, DIRECTIVE_TYPES, (node) =>
|
|
38
|
+
fn(node as DirectiveNode) === false ? SKIP : CONTINUE,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function toJsxAttributes(node: DirectiveNode): JsxAttribute[] {
|
|
43
|
+
return Object.entries(node.attributes ?? {}).map(([name, value]) => ({
|
|
44
|
+
type: 'mdxJsxAttribute',
|
|
45
|
+
name,
|
|
46
|
+
value: value == null || value === '' ? jsxExpression(estreeBoolean(true)) : value,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function labelText(node: MdastNode): string {
|
|
51
|
+
const t = node.type;
|
|
52
|
+
const v = (node as { value?: unknown }).value;
|
|
53
|
+
if (typeof v === 'string') return v;
|
|
54
|
+
if (t === 'textDirective' || t === 'leafDirective') {
|
|
55
|
+
const d = node as DirectiveNode;
|
|
56
|
+
const inner = (d.children ?? []).map(labelText).join('');
|
|
57
|
+
const attrs = Object.entries(d.attributes ?? {})
|
|
58
|
+
.map(([k, v]) => (v == null ? k : v === true ? k : `${k}="${v}"`))
|
|
59
|
+
.join(' ');
|
|
60
|
+
const prefix = t === 'leafDirective' ? '::' : ':';
|
|
61
|
+
return `${prefix}${d.name ?? ''}${inner ? `[${inner}]` : ''}${attrs ? `{${attrs}}` : ''}`;
|
|
62
|
+
}
|
|
63
|
+
return (node.children ?? []).map(labelText).join('');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function extractDirectiveLabel(node: DirectiveNode): string {
|
|
67
|
+
for (const child of node.children ?? []) {
|
|
68
|
+
if (child.type === 'paragraph' && child.data?.directiveLabel) {
|
|
69
|
+
return (child.children ?? []).map(labelText).join('');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function extractLeafLabel(node: MdastNode): string {
|
|
76
|
+
return (node.children ?? []).map(labelText).join('');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function contentChildren(node: DirectiveNode): MdastNode[] {
|
|
80
|
+
return (node.children ?? []).filter(
|
|
81
|
+
(child) =>
|
|
82
|
+
!(child.type === 'paragraph' && child.data?.directiveLabel),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function jsxAttr(name: string, value: string | boolean): JsxAttribute {
|
|
87
|
+
return { type: 'mdxJsxAttribute', name, value };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function jsxExpression(expression: unknown): JsxAttributeValue {
|
|
91
|
+
return {
|
|
92
|
+
type: 'mdxJsxAttributeValueExpression',
|
|
93
|
+
value: '',
|
|
94
|
+
data: {
|
|
95
|
+
estree: {
|
|
96
|
+
type: 'Program',
|
|
97
|
+
body: [{ type: 'ExpressionStatement', expression }],
|
|
98
|
+
sourceType: 'script',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function estreeString(value: string): unknown {
|
|
105
|
+
return { type: 'Literal', value, raw: JSON.stringify(value) };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function estreeStringArray(labels: string[]): unknown {
|
|
109
|
+
return {
|
|
110
|
+
type: 'ArrayExpression',
|
|
111
|
+
elements: labels.map(estreeString),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function estreeBoolean(value: boolean): unknown {
|
|
116
|
+
return { type: 'Literal', value, raw: String(value) };
|
|
117
|
+
}
|
|
118
|
+
|