Skip to content

Commit e14e798

Browse files
committed
wip
1 parent 53019bd commit e14e798

File tree

12 files changed

+303
-1
lines changed

12 files changed

+303
-1
lines changed

astro.config.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import rehypeMermaid from "rehype-mermaid";
1818
import rehypeCodeGroupReact from "./src/lib/plugins/code-group/plugin";
1919
import rehypeReadMoreReact from "./src/lib/plugins/read-more/plugin";
2020
import rehypeBlogListReact from "./src/lib/plugins/blog-list/plugin";
21+
import rehypeBlock from "./src/lib/plugins/parser/plugin";
2122
import {
2223
default as remarkDirective,
2324
default as remarkReadMoreDirective,
@@ -51,6 +52,7 @@ export default defineConfig({
5152
},
5253
remarkPlugins: [remarkDirective, remarkReadMoreDirective],
5354
rehypePlugins: [
55+
rehypeBlock,
5456
rehypeMermaid,
5557
[
5658
rehypeCallouts,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ collection:
99
- code-block
1010
- markdown
1111
- text
12+
- card
1213
---
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
title: Card
3+
description: Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4+
permalink: card
5+
icon: lucide:bell
6+
---
7+
8+
:::card-group{col="2"}
9+
:::card{label="Test", icon="lucide:bell"}
10+
test
11+
:::
12+
:::card{label="Test", icon="lucide:bell"}
13+
d
14+
:::
15+
:::

content/docs/documentation/getting-started/getting-started.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ icon: lucide:info
1010
## Introduction
1111

1212
Explainer provides a rich set of components that can be used directly in your Markdown files. This documentation outlines the various markdown components available for creating beautiful, interactive documentation.
13+
14+
:::card-group{cols=2}
15+
:::
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
const props = Astro.props;
3+
---
4+
5+
<div class="grid grid-cols-2 gap-3">
6+
<slot />
7+
</div>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
const props = Astro.props;
3+
console.log("Card", props);
4+
---
5+
6+
<div class="border p-5 rounded-lg">Card</div>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
const props = Astro.props;
3+
---
4+
5+
<p>
6+
<pre
7+
class="astro-code astro-code-themes github-light catppuccin-frappe has-highlighted"
8+
set:html={props.html}
9+
/>
10+
</p>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
import CardGroup from "@/components/content/card-group/card-group.astro";
3+
import Card from "@/components/content/card-group/card.astro";
4+
import BlockDynamic from "./dynamic-block.astro";
5+
6+
interface ASTNode {
7+
delimiter?: string;
8+
startTag?: string;
9+
attributes?: Record<string, any>;
10+
children?: Array<ASTNode | { type: "text"; value: string }>;
11+
type?: string;
12+
}
13+
14+
const mdx: Record<string, any> = {
15+
"card-group": CardGroup,
16+
card: Card,
17+
};
18+
19+
const { ast } = Astro.props as { ast: string | undefined };
20+
if (!ast) return null;
21+
22+
const node: ASTNode | ASTNode[] = JSON.parse(ast);
23+
24+
const renderNodeData = (
25+
block: ASTNode | { type: "text"; value: string } | undefined,
26+
): any => {
27+
if (!block) return null;
28+
29+
if (Array.isArray(block)) {
30+
return block.flatMap(renderNodeData).filter(Boolean);
31+
}
32+
33+
if (typeof block !== "object") return block;
34+
35+
if ("type" in block && block.type === "text") return block.value;
36+
37+
const Component = mdx[block.startTag || ""];
38+
39+
// Si aucun composant trouvé, on rend les enfants directement (raw / code / span)
40+
const childrenRendered =
41+
block.children?.flatMap(renderNodeData).filter(Boolean) || [];
42+
43+
if (!Component) {
44+
// Rendu direct des enfants
45+
return childrenRendered;
46+
}
47+
48+
return {
49+
component: Component,
50+
props: block.attributes || {},
51+
children: childrenRendered,
52+
};
53+
};
54+
55+
const treeData = Array.isArray(node)
56+
? node.flatMap(renderNodeData).filter(Boolean)
57+
: renderNodeData(node);
58+
---
59+
60+
{
61+
treeData.map((node: any, i: number) => {
62+
if (typeof node === "string") return node;
63+
return (
64+
<BlockDynamic
65+
key={i}
66+
component={node.component}
67+
props={node.props}
68+
children={node.children}
69+
/>
70+
);
71+
})
72+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
import DynamicBlockRenderer from "./dynamic-block.astro";
3+
const { component: Component, props, children } = Astro.props;
4+
5+
const childrenArray = Array.isArray(children)
6+
? children
7+
: [children].filter(Boolean);
8+
---
9+
10+
<Component {...props}>
11+
{
12+
childrenArray.map((child: any) => {
13+
if (typeof child === "string") return child;
14+
return (
15+
<DynamicBlockRenderer
16+
component={child.component}
17+
props={child.props}
18+
children={child.children}
19+
/>
20+
);
21+
})
22+
}
23+
</Component>

src/lib/plugins/parser/plugin.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import type { Element, Root } from "unist";
2+
import { visit } from "unist-util-visit";
3+
4+
// Exemple de mapping startTag -> composant Astro
5+
const mdx: Record<string, string> = {
6+
"card-group": "CardGroup",
7+
card: "Card",
8+
};
9+
10+
interface Block {
11+
delimiter: string;
12+
startTag: string;
13+
attributes: Record<string, any>;
14+
children: Array<Block | { type: "text"; value: string }>;
15+
}
16+
17+
// Parse les attributs de la ligne :::tag key="value"
18+
const parseAttributes = (str: string): Record<string, any> => {
19+
const regex = /(\w+)\s*=\s*(?:"([^"]*)"|(\S+))/g;
20+
const attrs: Record<string, any> = {};
21+
let match: RegExpExecArray | null;
22+
23+
while ((match = regex.exec(str)) !== null) {
24+
const key = match[1];
25+
let value: any;
26+
27+
if (match[2] !== undefined) {
28+
// Valeur entre guillemets
29+
const raw = match[2];
30+
try {
31+
value = JSON.parse(raw); // Essayer de parser JSON
32+
} catch {
33+
value = raw; // fallback string
34+
}
35+
} else if (match[3] !== undefined) {
36+
// Valeur non-quoted (true, false, number, etc.)
37+
const raw = match[3];
38+
if (raw === "true") value = true;
39+
else if (raw === "false") value = false;
40+
else if (!isNaN(Number(raw))) value = Number(raw);
41+
else value = raw;
42+
}
43+
44+
attrs[key] = value;
45+
}
46+
47+
return attrs;
48+
};
49+
50+
// Fonction principale : parse un node et retourne un Block[]
51+
const parseSingleNode = (node: Element): Block[] => {
52+
// On combine le node text + mdxTextExpression éventuel en un seul texte
53+
let combinedText = "";
54+
55+
for (const child of node.children || []) {
56+
if (child.type === "text") {
57+
combinedText += child.value;
58+
} else if ((child as any).type === "mdxTextExpression") {
59+
combinedText += " " + (child as any).value;
60+
}
61+
}
62+
63+
// Fonction récursive interne pour parser un texte complet
64+
const parseBlockText = (text: string): Block[] => {
65+
const blocks: Block[] = [];
66+
const lines = text.split(/\r?\n/);
67+
const stack: Block[] = [];
68+
69+
for (const line of lines) {
70+
const startMatch = line.match(/^:::(\w[\w-]*)\s*(.*)$/);
71+
const endMatch = line.match(/^:::/);
72+
73+
if (startMatch) {
74+
const block: Block = {
75+
delimiter: ":::",
76+
startTag: startMatch[1],
77+
attributes: parseAttributes(startMatch[2] || ""),
78+
children: [],
79+
};
80+
81+
if (stack.length > 0) {
82+
// Ajoute comme enfant du dernier parent
83+
stack[stack.length - 1].children.push(block);
84+
} else {
85+
// Bloc racine
86+
blocks.push(block);
87+
}
88+
89+
stack.push(block); // push pour gérer la fermeture
90+
} else if (endMatch) {
91+
stack.pop(); // on ferme le bloc courant
92+
} else {
93+
if (stack.length > 0) {
94+
stack[stack.length - 1].children.push({ type: "text", value: line });
95+
}
96+
}
97+
}
98+
99+
// Pousser tout bloc restant dans stack (blocs non fermés)
100+
while (stack.length > 0) {
101+
const remaining = stack.pop()!;
102+
if (stack.length > 0) {
103+
stack[stack.length - 1].children.push(remaining);
104+
} else if (!blocks.includes(remaining)) {
105+
blocks.push(remaining);
106+
}
107+
}
108+
109+
return blocks;
110+
};
111+
112+
return parseBlockText(combinedText);
113+
};
114+
115+
// Plugin rehype
116+
export default function rehypeComponents() {
117+
return (tree: Root) => {
118+
visit(tree, "element", (node: Element, index, parent) => {
119+
if (!parent) return;
120+
121+
const parsedBlocks = parseSingleNode(node);
122+
123+
console.log(JSON.stringify(parsedBlocks, null, 2));
124+
125+
parent.children[index] = {
126+
type: "element",
127+
tagName: "BlockRenderer",
128+
properties: {
129+
ast: JSON.stringify(parsedBlocks),
130+
},
131+
children: [],
132+
} as Element;
133+
});
134+
};
135+
}

0 commit comments

Comments
 (0)