Skip to main content

dioxus_mdx/components/
renderer.rs

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