Skip to content

Commit dd4bfd2

Browse files
committed
feat: implement card-group mdx renderer
1 parent e14e798 commit dd4bfd2

File tree

6 files changed

+195
-36
lines changed

6 files changed

+195
-36
lines changed

content/docs/documentation/foundamentals/components/card.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ permalink: card
55
icon: lucide:bell
66
---
77

8-
:::card-group{col="2"}
8+
# Card
9+
10+
:::card-group{cols=4, test="test"}
911
:::card{label="Test", icon="lucide:bell"}
1012
test
1113
:::
Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,25 @@
11
---
2-
const props = Astro.props;
2+
type Props = {
3+
cols?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
4+
};
5+
6+
const { cols } = Astro.props;
7+
const gridCols = {
8+
1: "grid-cols-1",
9+
2: "grid-cols-2",
10+
3: "grid-cols-3",
11+
4: "grid-cols-4",
12+
5: "grid-cols-5",
13+
6: "grid-cols-6",
14+
7: "grid-cols-7",
15+
8: "grid-cols-8",
16+
9: "grid-cols-9",
17+
10: "grid-cols-10",
18+
11: "grid-cols-11",
19+
12: "grid-cols-12",
20+
};
321
---
422

5-
<div class="grid grid-cols-2 gap-3">
23+
<div class:list={["grid gap-5", gridCols[cols ?? 2]]}>
624
<slot />
725
</div>
Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,31 @@
11
---
2+
import { Icon } from "@iconify/react";
3+
4+
type props = {
5+
label: string;
6+
icon?: string;
7+
};
8+
29
const props = Astro.props;
310
console.log("Card", props);
411
---
512

6-
<div class="border p-5 rounded-lg">Card</div>
13+
<div
14+
class="flex flex-col p-6 border rounded-xl group hover:bg-secondary w-full"
15+
>
16+
<div
17+
class="flex items-center justify-center w-8 h-8 rounded-full bg-secondary border group-hover:bg-primary/10 group-hover:border-primary/60 duration-150"
18+
>
19+
<Icon
20+
client:load
21+
icon={props.icon}
22+
className="group-hover:text-primary duration-150"
23+
/>
24+
</div>
25+
<div class="mt-4">
26+
<p class="font-semibold text-gray-900 !mt-0 !p-0">{props.label}</p>
27+
<p class="text-sm text-gray-500 !mt-0 !p-0">
28+
<slot />
29+
</p>
30+
</div>
31+
</div>
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import type { Root } from "unist";
2+
import { visit } from "unist-util-visit";
3+
import { CodeBlockSelector } from "./selector";
4+
5+
// Exemple de mapping startTag -> composant Astro
6+
const mdx: Record<string, any> = {
7+
"card-group": "CardGroup",
8+
card: "Card",
9+
codeblock: "Codeblock",
10+
};
11+
12+
interface Block {
13+
delimiter: string;
14+
startTag: string;
15+
attributes: Record<string, string>;
16+
children: Array<Block | { type: "text"; value: string }>;
17+
}
18+
19+
const parseAttributes = (str: string): Record<string, any> => {
20+
const regex = /(\w+)\s*=\s*(?:"([^"]*)"|(\S+))/g;
21+
const attrs: Record<string, any> = {};
22+
let match;
23+
24+
while ((match = regex.exec(str)) !== null) {
25+
const key = match[1];
26+
let value: any;
27+
28+
// Valeur entre guillemets
29+
if (match[2] !== undefined) {
30+
const raw = match[2];
31+
32+
// Essayer de parser JSON (array, object, number, boolean)
33+
try {
34+
value = JSON.parse(raw);
35+
} catch {
36+
value = raw; // fallback string
37+
}
38+
} else if (match[3] !== undefined) {
39+
// Valeur non-quoted (true, false, number, etc.)
40+
const raw = match[3];
41+
if (raw === "true") value = true;
42+
else if (raw === "false") value = false;
43+
else if (!isNaN(Number(raw))) value = Number(raw);
44+
else value = raw;
45+
}
46+
47+
attrs[key] = value;
48+
}
49+
50+
return attrs;
51+
};
52+
53+
const parseSingleNode = (node: { type: string; children?: any[] }): Block[] => {
54+
const parseChildren = (children: any[]): Block[] => {
55+
const blocks: Block[] = [];
56+
const stack: Block[] = [];
57+
58+
for (const child of children) {
59+
if (child.type === "text" || child.type === "mdxTextExpression") {
60+
const lines = child.value.split(/\r?\n/);
61+
for (const line of lines) {
62+
const startMatch = line.match(/^:::(\w[\w-]*)\s*(.*)$/);
63+
const endMatch = line.match(/^:::/);
64+
65+
if (startMatch) {
66+
const block: Block = {
67+
delimiter: ":::",
68+
startTag: startMatch[1],
69+
attributes: parseAttributes(startMatch[2] || ""),
70+
children: [],
71+
};
72+
stack.push(block);
73+
} else if (endMatch) {
74+
const finished = stack.pop();
75+
if (!finished) continue;
76+
if (stack.length > 0) {
77+
stack[stack.length - 1].children.push(finished);
78+
} else {
79+
blocks.push(finished);
80+
}
81+
} else {
82+
if (stack.length > 0) {
83+
stack[stack.length - 1].children.push({
84+
type: "text",
85+
value: line,
86+
});
87+
}
88+
}
89+
}
90+
} else if (child.type === "element") {
91+
const availableBlock = [CodeBlockSelector];
92+
const selector = availableBlock.find((selector) =>
93+
selector.filter(child),
94+
);
95+
96+
if (selector) {
97+
if (stack.length > 0) {
98+
stack[stack.length - 1].children.push(selector.render(child));
99+
} else {
100+
blocks.push(selector.render(child));
101+
}
102+
}
103+
}
104+
}
105+
106+
return blocks;
107+
};
108+
109+
return parseChildren(node.children || []);
110+
};
111+
112+
// Usage dans rehypeComponents
113+
export default function rehypeComponents(): Plugin<[], Root> {
114+
return (tree: Root) => {
115+
visit(tree, "element", (node, index, parent) => {
116+
const parsedBlocks = parseSingleNode(node);
117+
118+
parent.children[index] = {
119+
type: "element",
120+
tagName: "BlockRenderer",
121+
properties: { ast: JSON.stringify(parsedBlocks) },
122+
children: [], // ou des enfants si nécessaire
123+
};
124+
});
125+
};
126+
}

src/lib/plugins/parser/block-renderer.astro

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ if (!ast) return null;
2121
2222
const node: ASTNode | ASTNode[] = JSON.parse(ast);
2323
24-
const renderNodeData = (
25-
block: ASTNode | { type: "text"; value: string } | undefined,
26-
): any => {
24+
const renderNodeData = (block: any): any => {
2725
if (!block) return null;
2826
2927
if (Array.isArray(block)) {
@@ -34,14 +32,12 @@ const renderNodeData = (
3432
3533
if ("type" in block && block.type === "text") return block.value;
3634
37-
const Component = mdx[block.startTag || ""];
35+
const Component = mdx[block.startTag ?? ""];
3836
39-
// Si aucun composant trouvé, on rend les enfants directement (raw / code / span)
4037
const childrenRendered =
4138
block.children?.flatMap(renderNodeData).filter(Boolean) || [];
4239
4340
if (!Component) {
44-
// Rendu direct des enfants
4541
return childrenRendered;
4642
}
4743

src/lib/plugins/parser/plugin.ts

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { Element, Root } from "unist";
22
import { visit } from "unist-util-visit";
33

4-
// Exemple de mapping startTag -> composant Astro
54
const mdx: Record<string, string> = {
65
"card-group": "CardGroup",
76
card: "Card",
@@ -14,27 +13,24 @@ interface Block {
1413
children: Array<Block | { type: "text"; value: string }>;
1514
}
1615

17-
// Parse les attributs de la ligne :::tag key="value"
1816
const parseAttributes = (str: string): Record<string, any> => {
19-
const regex = /(\w+)\s*=\s*(?:"([^"]*)"|(\S+))/g;
17+
const regex = /(\w+)\s*=\s*(?:"([^"]*)"|([^,\s]+))/g;
2018
const attrs: Record<string, any> = {};
2119
let match: RegExpExecArray | null;
2220

2321
while ((match = regex.exec(str)) !== null) {
2422
const key = match[1];
2523
let value: any;
26-
2724
if (match[2] !== undefined) {
28-
// Valeur entre guillemets
2925
const raw = match[2];
3026
try {
31-
value = JSON.parse(raw); // Essayer de parser JSON
27+
value = JSON.parse(raw);
3228
} catch {
33-
value = raw; // fallback string
29+
value = raw;
3430
}
3531
} else if (match[3] !== undefined) {
36-
// Valeur non-quoted (true, false, number, etc.)
3732
const raw = match[3];
33+
3834
if (raw === "true") value = true;
3935
else if (raw === "false") value = false;
4036
else if (!isNaN(Number(raw))) value = Number(raw);
@@ -47,9 +43,7 @@ const parseAttributes = (str: string): Record<string, any> => {
4743
return attrs;
4844
};
4945

50-
// Fonction principale : parse un node et retourne un Block[]
5146
const parseSingleNode = (node: Element): Block[] => {
52-
// On combine le node text + mdxTextExpression éventuel en un seul texte
5347
let combinedText = "";
5448

5549
for (const child of node.children || []) {
@@ -60,7 +54,6 @@ const parseSingleNode = (node: Element): Block[] => {
6054
}
6155
}
6256

63-
// Fonction récursive interne pour parser un texte complet
6457
const parseBlockText = (text: string): Block[] => {
6558
const blocks: Block[] = [];
6659
const lines = text.split(/\r?\n/);
@@ -79,24 +72,21 @@ const parseSingleNode = (node: Element): Block[] => {
7972
};
8073

8174
if (stack.length > 0) {
82-
// Ajoute comme enfant du dernier parent
8375
stack[stack.length - 1].children.push(block);
8476
} else {
85-
// Bloc racine
8677
blocks.push(block);
8778
}
8879

89-
stack.push(block); // push pour gérer la fermeture
80+
stack.push(block);
9081
} else if (endMatch) {
91-
stack.pop(); // on ferme le bloc courant
82+
stack.pop();
9283
} else {
9384
if (stack.length > 0) {
9485
stack[stack.length - 1].children.push({ type: "text", value: line });
9586
}
9687
}
9788
}
9889

99-
// Pousser tout bloc restant dans stack (blocs non fermés)
10090
while (stack.length > 0) {
10191
const remaining = stack.pop()!;
10292
if (stack.length > 0) {
@@ -112,24 +102,26 @@ const parseSingleNode = (node: Element): Block[] => {
112102
return parseBlockText(combinedText);
113103
};
114104

115-
// Plugin rehype
116105
export default function rehypeComponents() {
117106
return (tree: Root) => {
118107
visit(tree, "element", (node: Element, index, parent) => {
119108
if (!parent) return;
120109

121110
const parsedBlocks = parseSingleNode(node);
111+
const hasBlock = parsedBlocks.some((block) => block.startTag in mdx);
122112

123-
console.log(JSON.stringify(parsedBlocks, null, 2));
113+
if (hasBlock) {
114+
console.log(JSON.stringify(parsedBlocks, null, 2));
124115

125-
parent.children[index] = {
126-
type: "element",
127-
tagName: "BlockRenderer",
128-
properties: {
129-
ast: JSON.stringify(parsedBlocks),
130-
},
131-
children: [],
132-
} as Element;
116+
parent.children[index] = {
117+
type: "element",
118+
tagName: "BlockRenderer",
119+
properties: {
120+
ast: JSON.stringify(parsedBlocks),
121+
},
122+
children: [],
123+
} as Element;
124+
}
133125
});
134126
};
135127
}

0 commit comments

Comments
 (0)