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