Skip to main content

dioxus_mdx/components/
code.rs

1//! Code block components for documentation.
2//!
3//! Features syntax highlighting for common programming languages.
4
5use dioxus::prelude::*;
6use dioxus_free_icons::{Icon, icons::ld_icons::*};
7
8#[cfg(feature = "mermaid")]
9use super::mermaid::MermaidDiagram;
10use crate::parser::{CodeBlockNode, CodeGroupNode, highlight_code};
11
12/// Props for DocCodeBlock component.
13#[derive(Props, Clone, PartialEq)]
14pub struct DocCodeBlockProps {
15    /// Code block data.
16    pub block: CodeBlockNode,
17}
18
19/// Single code block with syntax highlighting and copy button.
20#[component]
21pub fn DocCodeBlock(props: DocCodeBlockProps) -> Element {
22    // Mermaid blocks are rendered as diagrams, not syntax-highlighted code
23    #[cfg(feature = "mermaid")]
24    if props.block.language.as_deref() == Some("mermaid") {
25        return rsx! { MermaidDiagram { code: props.block.code.clone() } };
26    }
27
28    let copied = use_signal(|| false);
29    let code = props.block.code.clone();
30    let code_for_copy = code.clone();
31
32    // Apply syntax highlighting
33    let highlighted = highlight_code(&code, props.block.language.as_deref());
34
35    rsx! {
36        div { class: "my-6 relative group rounded-lg border border-base-content/10 overflow-hidden",
37            // Language label and filename - refined header
38            if props.block.language.is_some() || props.block.filename.is_some() {
39                div { class: "flex items-center justify-between bg-base-200/80 px-4 py-2.5 border-b border-base-content/10 text-sm",
40                    span { class: "text-base-content/60 font-mono text-xs tracking-wide",
41                        if let Some(filename) = &props.block.filename {
42                            "{filename}"
43                        } else if let Some(lang) = &props.block.language {
44                            "{lang}"
45                        }
46                    }
47                    // Copy button - always visible with subtle opacity
48                    CopyButton {
49                        code: code_for_copy.clone(),
50                        copied: copied,
51                    }
52                }
53            }
54
55            // Code content with syntax highlighting
56            // Note: mt-0 overrides prose typography margins
57            pre {
58                class: if props.block.language.is_some() || props.block.filename.is_some() {
59                    "bg-base-300/50 px-4 py-4 overflow-x-auto syntax-highlight mt-0"
60                } else {
61                    "bg-base-300/50 p-4 overflow-x-auto relative syntax-highlight"
62                },
63                code {
64                    class: "text-sm font-mono leading-relaxed",
65                    dangerous_inner_html: "{highlighted}",
66                }
67                // Copy button for blocks without header
68                if props.block.language.is_none() && props.block.filename.is_none() {
69                    div { class: "absolute top-3 right-3",
70                        CopyButton {
71                            code: code_for_copy,
72                            copied: copied,
73                        }
74                    }
75                }
76            }
77        }
78    }
79}
80
81/// Props for DocCodeGroup component.
82#[derive(Props, Clone, PartialEq)]
83pub struct DocCodeGroupProps {
84    /// Code group data.
85    pub group: CodeGroupNode,
86}
87
88/// Code group with multiple language variants in tabs.
89#[component]
90pub fn DocCodeGroup(props: DocCodeGroupProps) -> Element {
91    let mut active_tab = use_signal(|| 0usize);
92
93    rsx! {
94        div { class: "my-6 rounded-lg border border-base-content/10 overflow-hidden",
95            // Tab headers - refined styling with subtle shadows
96            div { class: "flex items-center bg-base-200/80 border-b border-base-content/10",
97                for (i, block) in props.group.blocks.iter().enumerate() {
98                    button {
99                        key: "{i}",
100                        class: if active_tab() == i {
101                            "px-4 py-2.5 text-sm font-medium text-primary border-b-2 border-primary -mb-px bg-base-300/30 transition-colors"
102                        } else {
103                            "px-4 py-2.5 text-sm font-medium text-base-content/60 hover:text-base-content hover:bg-base-300/20 transition-colors"
104                        },
105                        onclick: move |_| active_tab.set(i),
106                        if let Some(filename) = &block.filename {
107                            "{filename}"
108                        } else if let Some(lang) = &block.language {
109                            "{lang}"
110                        } else {
111                            "Code"
112                        }
113                    }
114                }
115            }
116
117            // Active code block
118            if let Some(block) = props.group.blocks.get(active_tab()) {
119                CodeGroupBlock { block: block.clone() }
120            }
121        }
122    }
123}
124
125/// Props for CodeGroupBlock.
126#[derive(Props, Clone, PartialEq)]
127struct CodeGroupBlockProps {
128    block: CodeBlockNode,
129}
130
131/// Code block within a code group (no top border radius).
132#[component]
133fn CodeGroupBlock(props: CodeGroupBlockProps) -> Element {
134    let copied = use_signal(|| false);
135    let code = props.block.code.clone();
136
137    // Apply syntax highlighting
138    let highlighted = highlight_code(&code, props.block.language.as_deref());
139
140    rsx! {
141        div { class: "relative group",
142            // mt-0 overrides prose typography margins
143            pre { class: "bg-base-300/50 px-4 py-4 overflow-x-auto syntax-highlight mt-0",
144                code {
145                    class: "text-sm font-mono leading-relaxed",
146                    dangerous_inner_html: "{highlighted}",
147                }
148            }
149            div { class: "absolute top-3 right-3",
150                CopyButton {
151                    code: code.clone(),
152                    copied: copied,
153                }
154            }
155        }
156    }
157}
158
159/// Props for CopyButton.
160#[derive(Props, Clone, PartialEq)]
161struct CopyButtonProps {
162    code: String,
163    copied: Signal<bool>,
164}
165
166/// Copy to clipboard button.
167#[component]
168fn CopyButton(props: CopyButtonProps) -> Element {
169    #[allow(unused_mut)]
170    let mut copied = props.copied;
171    let code = props.code.clone();
172
173    rsx! {
174        button {
175            class: "btn btn-ghost btn-xs opacity-60 hover:opacity-100 group-hover:opacity-100 transition-all duration-150 hover:bg-base-content/10",
176            "data-code": "{code}",
177            onclick: move |_| {
178                // Use JavaScript for clipboard (client-side only)
179                #[cfg(target_arch = "wasm32")]
180                {
181                    use dioxus::prelude::*;
182                    let code = code.clone();
183                    spawn(async move {
184                        // Use eval to copy to clipboard
185                        let js = format!(
186                            "navigator.clipboard.writeText({}).catch(console.error)",
187                            serde_json::to_string(&code).unwrap_or_default()
188                        );
189                        let _ = document::eval(&js);
190                        copied.set(true);
191                        gloo_timers::future::TimeoutFuture::new(2000).await;
192                        copied.set(false);
193                    });
194                }
195            },
196            if copied() {
197                Icon { class: "size-4 text-success", icon: LdCheck }
198            } else {
199                Icon { class: "size-4", icon: LdCopy }
200            }
201        }
202    }
203}