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            pre {
57                class: if props.block.language.is_some() || props.block.filename.is_some() {
58                    "bg-base-200 px-4 py-4 overflow-x-auto syntax-highlight mt-0"
59                } else {
60                    "bg-base-200 p-4 overflow-x-auto relative syntax-highlight"
61                },
62                code {
63                    class: "text-sm font-mono leading-relaxed",
64                    dangerous_inner_html: "{highlighted}",
65                }
66                // Copy button for blocks without header
67                if props.block.language.is_none() && props.block.filename.is_none() {
68                    div { class: "absolute top-3 right-3",
69                        CopyButton {
70                            code: code_for_copy,
71                            copied: copied,
72                        }
73                    }
74                }
75            }
76        }
77    }
78}
79
80/// Props for DocCodeGroup component.
81#[derive(Props, Clone, PartialEq)]
82pub struct DocCodeGroupProps {
83    /// Code group data.
84    pub group: CodeGroupNode,
85}
86
87/// Code group with multiple language variants in tabs.
88#[component]
89pub fn DocCodeGroup(props: DocCodeGroupProps) -> Element {
90    let mut active_tab = use_signal(|| 0usize);
91
92    rsx! {
93        div { class: "my-6 rounded-lg border border-base-content/10 overflow-hidden",
94            // Tab headers - refined styling with subtle shadows
95            div { class: "flex items-center bg-base-200/80 border-b border-base-content/10",
96                for (i, block) in props.group.blocks.iter().enumerate() {
97                    button {
98                        key: "{i}",
99                        class: if active_tab() == i {
100                            "px-4 py-2.5 text-sm font-medium text-primary border-b-2 border-primary -mb-px bg-base-200/60 transition-colors"
101                        } else {
102                            "px-4 py-2.5 text-sm font-medium text-base-content/60 hover:text-base-content hover:bg-base-300/20 transition-colors"
103                        },
104                        onclick: move |_| active_tab.set(i),
105                        if let Some(filename) = &block.filename {
106                            "{filename}"
107                        } else if let Some(lang) = &block.language {
108                            "{lang}"
109                        } else {
110                            "Code"
111                        }
112                    }
113                }
114            }
115
116            // Active code block
117            if let Some(block) = props.group.blocks.get(active_tab()) {
118                CodeGroupBlock { block: block.clone() }
119            }
120        }
121    }
122}
123
124/// Props for CodeGroupBlock.
125#[derive(Props, Clone, PartialEq)]
126struct CodeGroupBlockProps {
127    block: CodeBlockNode,
128}
129
130/// Code block within a code group (no top border radius).
131#[component]
132fn CodeGroupBlock(props: CodeGroupBlockProps) -> Element {
133    let copied = use_signal(|| false);
134    let code = props.block.code.clone();
135
136    // Apply syntax highlighting
137    let highlighted = highlight_code(&code, props.block.language.as_deref());
138
139    rsx! {
140        div { class: "relative group",
141            // mt-0 overrides prose typography margins
142            pre {
143                class: "bg-base-200 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}