diff --git a/astro.config.mjs b/astro.config.mjs index e063390..ff1d61c 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -18,6 +18,7 @@ import rehypeMermaid from "rehype-mermaid"; import rehypeCodeGroupReact from "./src/lib/plugins/code-group/plugin"; import rehypeReadMoreReact from "./src/lib/plugins/read-more/plugin"; import rehypeBlogListReact from "./src/lib/plugins/blog-list/plugin"; +import rehypeBlock from "./src/lib/plugins/parser/plugin"; import { default as remarkDirective, default as remarkReadMoreDirective, @@ -51,6 +52,7 @@ export default defineConfig({ }, remarkPlugins: [remarkDirective, remarkReadMoreDirective], rehypePlugins: [ + rehypeBlock, rehypeMermaid, [ rehypeCallouts, diff --git a/content/docs/documentation/foundamentals/components/_default.mdx b/content/docs/documentation/foundamentals/components/_default.mdx index 5fe6643..ef423f2 100644 --- a/content/docs/documentation/foundamentals/components/_default.mdx +++ b/content/docs/documentation/foundamentals/components/_default.mdx @@ -9,4 +9,5 @@ collection: - code-block - markdown - text + - card --- diff --git a/content/docs/documentation/foundamentals/components/card.mdx b/content/docs/documentation/foundamentals/components/card.mdx new file mode 100644 index 0000000..5fe8a14 --- /dev/null +++ b/content/docs/documentation/foundamentals/components/card.mdx @@ -0,0 +1,128 @@ +--- +title: Card +description: Lorem ipsum dolor sit amet, consectetur adipiscing elit. +permalink: card +icon: lucide:bell +--- + +# Cards + +This component allows you to group text elements while providing visual impact. + +``` +:::card-group + :::card {label="Test", icon="lucide:bell"} + Lorem ipsum dolor sit amet, consectetur adipiscing elit, + sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + ::: + :::card {label="Test", icon="lucide:bell"} + Lorem ipsum dolor sit amet, consectetur adipiscing elit, + sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + ::: +::: +``` + +:::card-group {cols=2} +:::card {label="Test", icon="lucide:bell"} +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +::: +:::card {label="Test", icon="lucide:bell"} +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +::: +::: + +--- + +## Configuration + +### Card per line + +You can customize the number of elements per line by setting the `cols` prop. + +> [!note] +> Default card per line was fixed to 2 + +``` +:::card-group {cols=4} + :::card {label="Test", icon="lucide:bell"} + Lorem ipsum dolor sit amet, consectetur adipiscing elit, + sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + ::: + :::card {label="Test", icon="lucide:bell"} + Lorem ipsum dolor sit amet, consectetur adipiscing elit, + sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + ::: +::: +``` + +:::card-group {cols=4} +:::card {label="Test", icon="lucide:bell"} +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +::: +:::card {label="Test", icon="lucide:bell"} +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +::: +::: + +If the elements cannot be aligned horizontally, they will be moved to the next line. + +``` +:::card-group {cols=2} + :::card {label="Test", icon="lucide:bell"} + Lorem ipsum dolor sit amet, consectetur adipiscing elit, + sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + ::: + :::card {label="Test", icon="lucide:bell"} + Lorem ipsum dolor sit amet, consectetur adipiscing elit, + sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + ::: + :::card {label="Test", icon="lucide:bell"} + Lorem ipsum dolor sit amet, consectetur adipiscing elit, + sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + ::: +::: +``` + +:::card-group {cols=2} +:::card {label="Test", icon="lucide:bell"} +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +::: +:::card {label="Test", icon="lucide:bell"} +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +::: +:::card {label="Test", icon="lucide:bell"} +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +::: +::: + +### Card item + +Vous pouvez personaliser chacune de vos cartes en utilisant les props suivantes : + +- `label`: Le texte à afficher en haut de la carte. (required) +- `icon`: L'icône à afficher en haut de la carte. + +> [!note] +> The cards use the [`iconify`](https://icon-sets.iconify.design/) library to display icons. + +``` +:::card-group + :::card {label="Test", icon="lucide:bell"} + Lorem ipsum dolor sit amet, consectetur adipiscing elit, + sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + ::: + :::card {label="Test", icon="lucide:user"} + Lorem ipsum dolor sit amet, consectetur adipiscing elit, + sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + ::: +::: +``` + +:::card-group {cols=2} +:::card {label="Test", icon="lucide:bell"} +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +::: +:::card {label="Test", icon="lucide:user"} +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +::: +::: diff --git a/content/docs/documentation/getting-started/getting-started.mdx b/content/docs/documentation/getting-started/getting-started.mdx index 3895dcd..af3f875 100644 --- a/content/docs/documentation/getting-started/getting-started.mdx +++ b/content/docs/documentation/getting-started/getting-started.mdx @@ -10,3 +10,6 @@ icon: lucide:info ## Introduction 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. + +:::card-group{cols=2} +::: diff --git a/src/lib/components/content/card-group/card-group.astro b/src/lib/components/content/card-group/card-group.astro new file mode 100644 index 0000000..8cff351 --- /dev/null +++ b/src/lib/components/content/card-group/card-group.astro @@ -0,0 +1,25 @@ +--- +type Props = { + cols?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; +}; + +const { cols } = Astro.props; +const gridCols = { + 1: "grid-cols-1", + 2: "grid-cols-2", + 3: "grid-cols-3", + 4: "grid-cols-4", + 5: "grid-cols-5", + 6: "grid-cols-6", + 7: "grid-cols-7", + 8: "grid-cols-8", + 9: "grid-cols-9", + 10: "grid-cols-10", + 11: "grid-cols-11", + 12: "grid-cols-12", +}; +--- + +
+ +
diff --git a/src/lib/components/content/card-group/card.astro b/src/lib/components/content/card-group/card.astro new file mode 100644 index 0000000..2f453d2 --- /dev/null +++ b/src/lib/components/content/card-group/card.astro @@ -0,0 +1,30 @@ +--- +import { Icon } from "@iconify/react"; + +type props = { + label: string; + icon?: string; +}; + +const props = Astro.props; +--- + +
+
+ +
+
+

{props.label}

+

+ +

+
+
diff --git a/src/lib/components/content/codeblock.astro b/src/lib/components/content/codeblock.astro new file mode 100644 index 0000000..b36dac6 --- /dev/null +++ b/src/lib/components/content/codeblock.astro @@ -0,0 +1,10 @@ +--- +const props = Astro.props; +--- + +

+

+

diff --git a/src/lib/plugins/card-group/plugin.ts b/src/lib/plugins/card-group/plugin.ts new file mode 100644 index 0000000..b472f15 --- /dev/null +++ b/src/lib/plugins/card-group/plugin.ts @@ -0,0 +1,126 @@ +import type { Root } from "unist"; +import { visit } from "unist-util-visit"; +import { CodeBlockSelector } from "./selector"; + +// Exemple de mapping startTag -> composant Astro +const mdx: Record = { + "card-group": "CardGroup", + card: "Card", + codeblock: "Codeblock", +}; + +interface Block { + delimiter: string; + startTag: string; + attributes: Record; + children: Array; +} + +const parseAttributes = (str: string): Record => { + const regex = /(\w+)\s*=\s*(?:"([^"]*)"|(\S+))/g; + const attrs: Record = {}; + let match; + + while ((match = regex.exec(str)) !== null) { + const key = match[1]; + let value: any; + + // Valeur entre guillemets + if (match[2] !== undefined) { + const raw = match[2]; + + // Essayer de parser JSON (array, object, number, boolean) + try { + value = JSON.parse(raw); + } catch { + value = raw; // fallback string + } + } else if (match[3] !== undefined) { + // Valeur non-quoted (true, false, number, etc.) + const raw = match[3]; + if (raw === "true") value = true; + else if (raw === "false") value = false; + else if (!isNaN(Number(raw))) value = Number(raw); + else value = raw; + } + + attrs[key] = value; + } + + return attrs; +}; + +const parseSingleNode = (node: { type: string; children?: any[] }): Block[] => { + const parseChildren = (children: any[]): Block[] => { + const blocks: Block[] = []; + const stack: Block[] = []; + + for (const child of children) { + if (child.type === "text" || child.type === "mdxTextExpression") { + const lines = child.value.split(/\r?\n/); + for (const line of lines) { + const startMatch = line.match(/^:::(\w[\w-]*)\s*(.*)$/); + const endMatch = line.match(/^:::/); + + if (startMatch) { + const block: Block = { + delimiter: ":::", + startTag: startMatch[1], + attributes: parseAttributes(startMatch[2] || ""), + children: [], + }; + stack.push(block); + } else if (endMatch) { + const finished = stack.pop(); + if (!finished) continue; + if (stack.length > 0) { + stack[stack.length - 1].children.push(finished); + } else { + blocks.push(finished); + } + } else { + if (stack.length > 0) { + stack[stack.length - 1].children.push({ + type: "text", + value: line, + }); + } + } + } + } else if (child.type === "element") { + const availableBlock = [CodeBlockSelector]; + const selector = availableBlock.find((selector) => + selector.filter(child), + ); + + if (selector) { + if (stack.length > 0) { + stack[stack.length - 1].children.push(selector.render(child)); + } else { + blocks.push(selector.render(child)); + } + } + } + } + + return blocks; + }; + + return parseChildren(node.children || []); +}; + +// Usage dans rehypeComponents +export default function rehypeComponents(): Plugin<[], Root> { + return (tree: Root) => { + visit(tree, "element", (node, index, parent) => { + const parsedBlocks = parseSingleNode(node); + + parent.children[index] = { + type: "element", + tagName: "BlockRenderer", + properties: { ast: JSON.stringify(parsedBlocks) }, + children: [], // ou des enfants si nécessaire + }; + }); + }; +} diff --git a/src/lib/plugins/parser/block-renderer.astro b/src/lib/plugins/parser/block-renderer.astro new file mode 100644 index 0000000..3259e8a --- /dev/null +++ b/src/lib/plugins/parser/block-renderer.astro @@ -0,0 +1,68 @@ +--- +import CardGroup from "@/components/content/card-group/card-group.astro"; +import Card from "@/components/content/card-group/card.astro"; +import BlockDynamic from "./dynamic-block.astro"; + +interface ASTNode { + delimiter?: string; + startTag?: string; + attributes?: Record; + children?: Array; + type?: string; +} + +const mdx: Record = { + "card-group": CardGroup, + card: Card, +}; + +const { ast } = Astro.props as { ast: string | undefined }; +if (!ast) return null; + +const node: ASTNode | ASTNode[] = JSON.parse(ast); + +const renderNodeData = (block: any): any => { + if (!block) return null; + + if (Array.isArray(block)) { + return block.flatMap(renderNodeData).filter(Boolean); + } + + if (typeof block !== "object") return block; + + if ("type" in block && block.type === "text") return block.value; + + const Component = mdx[block.startTag ?? ""]; + + const childrenRendered = + block.children?.flatMap(renderNodeData).filter(Boolean) || []; + + if (!Component) { + return childrenRendered; + } + + return { + component: Component, + props: block.attributes || {}, + children: childrenRendered, + }; +}; + +const treeData = Array.isArray(node) + ? node.flatMap(renderNodeData).filter(Boolean) + : renderNodeData(node); +--- + +{ + treeData.map((node: any, i: number) => { + if (typeof node === "string") return node; + return ( + + ); + }) +} diff --git a/src/lib/plugins/parser/dynamic-block.astro b/src/lib/plugins/parser/dynamic-block.astro new file mode 100644 index 0000000..1ef7a83 --- /dev/null +++ b/src/lib/plugins/parser/dynamic-block.astro @@ -0,0 +1,23 @@ +--- +import DynamicBlockRenderer from "./dynamic-block.astro"; +const { component: Component, props, children } = Astro.props; + +const childrenArray = Array.isArray(children) + ? children + : [children].filter(Boolean); +--- + + + { + childrenArray.map((child: any) => { + if (typeof child === "string") return child; + return ( + + ); + }) + } + diff --git a/src/lib/plugins/parser/plugin.ts b/src/lib/plugins/parser/plugin.ts new file mode 100644 index 0000000..946e728 --- /dev/null +++ b/src/lib/plugins/parser/plugin.ts @@ -0,0 +1,125 @@ +import type { Element, Root } from "unist"; +import { visit } from "unist-util-visit"; + +const mdx: Record = { + "card-group": "CardGroup", + card: "Card", +}; + +interface Block { + delimiter: string; + startTag: string; + attributes: Record; + children: Array; +} + +const parseAttributes = (str: string): Record => { + const regex = /(\w+)\s*=\s*(?:"([^"]*)"|([^,\s]+))/g; + const attrs: Record = {}; + let match: RegExpExecArray | null; + + while ((match = regex.exec(str)) !== null) { + const key = match[1]; + let value: any; + if (match[2] !== undefined) { + const raw = match[2]; + try { + value = JSON.parse(raw); + } catch { + value = raw; + } + } else if (match[3] !== undefined) { + const raw = match[3]; + + if (raw === "true") value = true; + else if (raw === "false") value = false; + else if (!isNaN(Number(raw))) value = Number(raw); + else value = raw; + } + + attrs[key] = value; + } + + return attrs; +}; + +const parseSingleNode = (node: Element): Block[] => { + let combinedText = ""; + + for (const child of node.children || []) { + if (child.type === "text") { + combinedText += child.value; + } else if ((child as any).type === "mdxTextExpression") { + combinedText += " " + (child as any).value; + } + } + + const parseBlockText = (text: string): Block[] => { + const blocks: Block[] = []; + const lines = text.split(/\r?\n/); + const stack: Block[] = []; + + for (const line of lines) { + const startMatch = line.match(/^:::(\w[\w-]*)\s*(.*)$/); + const endMatch = line.match(/^:::/); + + if (startMatch) { + const block: Block = { + delimiter: ":::", + startTag: startMatch[1], + attributes: parseAttributes(startMatch[2] || ""), + children: [], + }; + + if (stack.length > 0) { + stack[stack.length - 1].children.push(block); + } else { + blocks.push(block); + } + + stack.push(block); + } else if (endMatch) { + stack.pop(); + } else { + if (stack.length > 0) { + stack[stack.length - 1].children.push({ type: "text", value: line }); + } + } + } + + while (stack.length > 0) { + const remaining = stack.pop()!; + if (stack.length > 0) { + stack[stack.length - 1].children.push(remaining); + } else if (!blocks.includes(remaining)) { + blocks.push(remaining); + } + } + + return blocks; + }; + + return parseBlockText(combinedText); +}; + +export default function rehypeComponents() { + return (tree: Root) => { + visit(tree, "element", (node: Element, index, parent) => { + if (!parent) return; + + const parsedBlocks = parseSingleNode(node); + const hasBlock = parsedBlocks.some((block) => block.startTag in mdx); + + if (parent.type === "root" && hasBlock) { + parent.children[index] = { + type: "element", + tagName: "BlockRenderer", + properties: { + ast: JSON.stringify(parsedBlocks), + }, + children: [], + } as Element; + } + }); + }; +} diff --git a/src/lib/plugins/parser/selectors.ts b/src/lib/plugins/parser/selectors.ts new file mode 100644 index 0000000..960aba7 --- /dev/null +++ b/src/lib/plugins/parser/selectors.ts @@ -0,0 +1,25 @@ +import { toHtml } from "hast-util-to-html"; + +export const codeblockSelector = { + filter: (child: any) => { + return child.tagName === "code" && child.children[0].type !== "text"; + }, + metadata: (child: any) => ({ + delimiter: "", + startTag: "codeblock", + attributes: { html: toHtml(child) }, + children: [], + }), +}; + +export const cardGroupSelector = { + filter: (child: any) => { + return child.tagName === "card-group"; + }, + metadata: (child: any) => ({ + delimiter: "", + startTag: "card-group", + attributes: child.attributes, + children: child.children, + }), +}; diff --git a/src/pages/docs/[...slug].astro b/src/pages/docs/[...slug].astro index 8daab4a..de5a81e 100644 --- a/src/pages/docs/[...slug].astro +++ b/src/pages/docs/[...slug].astro @@ -16,6 +16,7 @@ import * as astroContent from "astro:content"; import DocNavigationWrapper from "@/components/elements/DocNavigationWrapper.astro"; import { Icon } from "@iconify/react"; import BlogListWrapper from "@/plugins/blog-list/BlogListWrapper.astro"; +import BlockRenderer from "@/plugins/parser/block-renderer.astro"; interface Props { element: CollectionEntry<"blog">; } @@ -97,7 +98,9 @@ function pascalCase(str: string) {
- +