Skip to main content

dioxus_mdx/components/
renderer.rs

1//! Main documentation renderer component.
2
3use dioxus::prelude::*;
4
5use super::slugify;
6use crate::components::{
7    DocAccordionGroup, DocCallout, DocCardGroup, DocCodeBlock, DocCodeGroup, DocExpandable,
8    DocParamField, DocRequestExample, DocResponseExample, DocResponseField, DocSteps, DocTabs,
9    DocUpdate, OpenApiViewer,
10};
11use crate::parser::{CardGroupNode, DocNode, parse_mdx};
12
13/// Inject `id` attributes into heading tags so TOC anchor links work.
14fn inject_heading_ids(html: &str) -> String {
15    let re = regex::Regex::new(r"<(h[2-4])>(.*?)</h[2-4]>").unwrap();
16    re.replace_all(html, |caps: &regex::Captures| {
17        let tag = &caps[1];
18        let inner = &caps[2];
19        // Strip any inner HTML tags to get plain text for the slug
20        let plain = regex::Regex::new(r"<[^>]+>")
21            .unwrap()
22            .replace_all(inner, "");
23        let id = slugify(&plain);
24        format!("<{tag} id=\"{id}\">{inner}</{tag}>")
25    })
26    .into_owned()
27}
28
29/// Props for DocNodeRenderer component.
30#[derive(Props, Clone, PartialEq)]
31pub struct DocNodeRendererProps {
32    /// The DocNode to render.
33    pub node: DocNode,
34}
35
36/// Render a single DocNode.
37#[component]
38pub fn DocNodeRenderer(props: DocNodeRendererProps) -> Element {
39    match &props.node {
40        DocNode::Markdown(md) => {
41            let html = markdown::to_html_with_options(md, &markdown::Options::gfm())
42                .unwrap_or_else(|_| md.clone());
43            let html = inject_heading_ids(&html);
44            rsx! {
45                div {
46                    class: "prose-content",
47                    dangerous_inner_html: html,
48                }
49            }
50        }
51        DocNode::Callout(callout) => {
52            rsx! {
53                DocCallout {
54                    callout_type: callout.callout_type,
55                    content: callout.content.clone(),
56                }
57            }
58        }
59        DocNode::Card(card) => {
60            // Wrap single card in a group
61            rsx! {
62                DocCardGroup {
63                    group: CardGroupNode {
64                        cols: 1,
65                        cards: vec![card.clone()],
66                    }
67                }
68            }
69        }
70        DocNode::CardGroup(group) => {
71            rsx! {
72                DocCardGroup { group: group.clone() }
73            }
74        }
75        DocNode::Tabs(tabs) => {
76            rsx! {
77                DocTabs { tabs: tabs.clone() }
78            }
79        }
80        DocNode::Steps(steps) => {
81            rsx! {
82                DocSteps { steps: steps.clone() }
83            }
84        }
85        DocNode::AccordionGroup(group) => {
86            rsx! {
87                DocAccordionGroup { group: group.clone() }
88            }
89        }
90        DocNode::CodeBlock(block) => {
91            rsx! {
92                DocCodeBlock { block: block.clone() }
93            }
94        }
95        DocNode::CodeGroup(group) => {
96            rsx! {
97                DocCodeGroup { group: group.clone() }
98            }
99        }
100        DocNode::ParamField(field) => {
101            rsx! {
102                DocParamField { field: field.clone() }
103            }
104        }
105        DocNode::ResponseField(field) => {
106            rsx! {
107                DocResponseField { field: field.clone() }
108            }
109        }
110        DocNode::Expandable(expandable) => {
111            rsx! {
112                DocExpandable { expandable: expandable.clone() }
113            }
114        }
115        DocNode::RequestExample(example) => {
116            rsx! {
117                DocRequestExample { example: example.clone() }
118            }
119        }
120        DocNode::ResponseExample(example) => {
121            rsx! {
122                DocResponseExample { example: example.clone() }
123            }
124        }
125        DocNode::Update(update) => {
126            rsx! {
127                DocUpdate { update: update.clone() }
128            }
129        }
130        DocNode::OpenApi(openapi) => {
131            rsx! {
132                OpenApiViewer {
133                    spec: openapi.spec.clone(),
134                    tags: openapi.tags.clone(),
135                    show_schemas: openapi.show_schemas,
136                }
137            }
138        }
139    }
140}
141
142/// Props for DocContent component.
143#[derive(Props, Clone, PartialEq)]
144pub struct DocContentProps {
145    /// Parsed documentation nodes.
146    pub nodes: Vec<DocNode>,
147}
148
149/// Render a list of DocNodes.
150#[component]
151pub fn DocContent(props: DocContentProps) -> Element {
152    rsx! {
153        div { class: "doc-content",
154            for (i, node) in props.nodes.iter().enumerate() {
155                DocNodeRenderer { key: "{i}", node: node.clone() }
156            }
157        }
158    }
159}
160
161/// Props for MdxContent component.
162#[derive(Props, Clone, PartialEq)]
163pub struct MdxContentProps {
164    /// Raw MDX content to parse and render.
165    pub content: String,
166}
167
168/// Parse and render MDX content.
169///
170/// This is the main entry point for rendering MDX in Dioxus applications.
171///
172/// # Example
173///
174/// ```rust,ignore
175/// use dioxus::prelude::*;
176/// use dioxus_mdx::MdxContent;
177///
178/// #[component]
179/// fn DocsPage(content: String) -> Element {
180///     rsx! {
181///         MdxContent { content }
182///     }
183/// }
184/// ```
185#[component]
186pub fn MdxContent(props: MdxContentProps) -> Element {
187    let nodes = parse_mdx(&props.content);
188
189    rsx! {
190        DocContent { nodes: nodes }
191    }
192}
193
194/// Parse and render MDX content (legacy alias).
195#[component]
196pub fn MdxRenderer(content: String) -> Element {
197    let nodes = parse_mdx(&content);
198
199    rsx! {
200        DocContent { nodes: nodes }
201    }
202}