1use dioxus::prelude::*;
6#[cfg(feature = "highlight")]
7use dioxus_code::{Code, CodeTheme, Language, SourceCode, Theme};
8use dioxus_free_icons::{Icon, icons::ld_icons::*};
9
10#[cfg(feature = "mermaid")]
11use super::mermaid::MermaidDiagram;
12use crate::parser::{CodeBlockNode, CodeGroupNode};
13
14#[cfg(feature = "highlight")]
26#[derive(Clone, Copy)]
27pub struct CodeThemeOverride(pub ReadSignal<CodeTheme>);
28
29#[derive(Props, Clone, PartialEq)]
31pub struct DocCodeBlockProps {
32 pub block: CodeBlockNode,
34}
35
36#[component]
38pub fn DocCodeBlock(props: DocCodeBlockProps) -> Element {
39 #[cfg(feature = "mermaid")]
41 if props.block.language.as_deref() == Some("mermaid") {
42 return rsx! { MermaidDiagram { code: props.block.code.clone() } };
43 }
44
45 let copied = use_signal(|| false);
46 let code = props.block.code.clone();
47 let code_for_copy = code.clone();
48
49 rsx! {
50 div { class: "dk-code-block not-prose my-6 relative group inline-block max-w-full rounded-lg border border-base-content/10 overflow-hidden",
55 if props.block.language.is_some() || props.block.filename.is_some() {
57 div { class: "flex items-center justify-between bg-base-200/80 px-4 py-2.5 border-b border-base-content/10 text-sm",
58 span { class: "text-base-content/60 font-mono text-xs tracking-wide",
59 if let Some(filename) = &props.block.filename {
60 "{filename}"
61 } else if let Some(lang) = &props.block.language {
62 "{lang}"
63 }
64 }
65 CopyButton {
67 code: code_for_copy.clone(),
68 copied: copied,
69 }
70 }
71 }
72
73 div {
75 class: if props.block.language.is_some() || props.block.filename.is_some() {
76 "dk-code-block-body bg-base-200"
77 } else {
78 "dk-code-block-body dk-code-block-body--bare bg-base-200 relative"
79 },
80 HighlightedCode {
81 code: code.clone(),
82 language: props.block.language.clone(),
83 filename: props.block.filename.clone(),
84 }
85 if props.block.language.is_none() && props.block.filename.is_none() {
87 div { class: "absolute top-3 right-3",
88 CopyButton {
89 code: code_for_copy,
90 copied: copied,
91 }
92 }
93 }
94 }
95 }
96 }
97}
98
99#[derive(Props, Clone, PartialEq)]
101pub struct DocCodeGroupProps {
102 pub group: CodeGroupNode,
104}
105
106#[component]
108pub fn DocCodeGroup(props: DocCodeGroupProps) -> Element {
109 let mut active_tab = use_signal(|| 0usize);
110
111 rsx! {
112 div { class: "dk-code-block not-prose my-6 inline-block max-w-full rounded-lg border border-base-content/10 overflow-hidden",
113 div { class: "flex items-center bg-base-200/80 border-b border-base-content/10",
115 for (i, block) in props.group.blocks.iter().enumerate() {
116 button {
117 key: "{i}",
118 class: if active_tab() == i {
119 "px-4 py-2.5 text-sm font-medium text-primary border-b-2 border-primary -mb-px bg-base-200/60 transition-colors"
120 } else {
121 "px-4 py-2.5 text-sm font-medium text-base-content/60 hover:text-base-content hover:bg-base-300/20 transition-colors"
122 },
123 onclick: move |_| active_tab.set(i),
124 if let Some(filename) = &block.filename {
125 "{filename}"
126 } else if let Some(lang) = &block.language {
127 "{lang}"
128 } else {
129 "Code"
130 }
131 }
132 }
133 }
134
135 if let Some(block) = props.group.blocks.get(active_tab()) {
137 CodeGroupBlock { block: block.clone() }
138 }
139 }
140 }
141}
142
143#[derive(Props, Clone, PartialEq)]
145struct CodeGroupBlockProps {
146 block: CodeBlockNode,
147}
148
149#[component]
151fn CodeGroupBlock(props: CodeGroupBlockProps) -> Element {
152 let copied = use_signal(|| false);
153 let code = props.block.code.clone();
154
155 rsx! {
156 div { class: "dk-code-group-block bg-base-200 relative group",
157 HighlightedCode {
158 code: code.clone(),
159 language: props.block.language.clone(),
160 filename: props.block.filename.clone(),
161 }
162 div { class: "absolute top-3 right-3",
163 CopyButton {
164 code: code.clone(),
165 copied: copied,
166 }
167 }
168 }
169 }
170}
171
172#[derive(Props, Clone, PartialEq)]
173struct HighlightedCodeProps {
174 code: String,
175 language: Option<String>,
176 filename: Option<String>,
177}
178
179#[cfg(feature = "highlight")]
180#[component]
181fn HighlightedCode(props: HighlightedCodeProps) -> Element {
182 let language = code_language(props.language.as_deref(), props.filename.as_deref());
183 let theme = match try_use_context::<CodeThemeOverride>() {
184 Some(CodeThemeOverride(theme)) => theme(),
185 None => CodeTheme::system(Theme::GITHUB_LIGHT, Theme::TOKYO_NIGHT),
186 };
187
188 rsx! {
189 Code {
190 src: SourceCode::new(language, props.code),
191 theme,
192 }
193 }
194}
195
196#[cfg(not(feature = "highlight"))]
203#[component]
204fn HighlightedCode(props: HighlightedCodeProps) -> Element {
205 plain_code_block(&props.code)
206}
207
208#[cfg(not(feature = "highlight"))]
215pub(crate) fn plain_code_block(code: &str) -> Element {
216 rsx! {
217 pre {
218 class: "dxc",
219 margin: "0",
220 padding: "1rem",
221 overflow: "auto",
222 tab_size: "4",
223 font_family: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", monospace",
224 font_size: "0.9rem",
225 line_height: "1.55",
226 code { "{code}" }
227 }
228 }
229}
230
231#[cfg(feature = "highlight")]
232pub(crate) fn code_language(language: Option<&str>, filename: Option<&str>) -> Language {
233 language
234 .and_then(language_from_alias)
235 .or_else(|| filename.and_then(Language::detect))
236 .or_else(|| language.and_then(Language::detect))
237 .unwrap_or(Language::Markdown)
238}
239
240#[cfg(feature = "highlight")]
241fn language_from_alias(language: &str) -> Option<Language> {
242 let normalized = language.trim().to_ascii_lowercase();
243 match normalized.as_str() {
244 "bash" | "sh" | "shell" | "zsh" | "console" | "terminal" => Some(Language::Bash),
245 "c++" | "cc" | "cxx" | "hpp" => Some(Language::Cpp),
246 "c#" | "cs" => Some(Language::CSharp),
247 "docker" | "dockerfile" | "containerfile" => Some(Language::Dockerfile),
248 "html" | "htm" => Some(Language::Html),
249 "js" | "javascript" | "jsx" | "mjs" | "cjs" => Some(Language::JavaScript),
250 "json" | "jsonc" => Some(Language::Json),
251 "markdown" | "md" | "mdx" => Some(Language::Markdown),
252 "py" | "python" => Some(Language::Python),
253 "rs" | "rust" => Some(Language::Rust),
254 "ts" | "typescript" => Some(Language::TypeScript),
255 "tsx" => Some(Language::Tsx),
256 "toml" => Some(Language::Toml),
257 "yaml" | "yml" => Some(Language::Yaml),
258 other => Language::from_slug(other),
259 }
260}
261
262#[derive(Props, Clone, PartialEq)]
264struct CopyButtonProps {
265 code: String,
266 copied: Signal<bool>,
267}
268
269#[component]
271fn CopyButton(props: CopyButtonProps) -> Element {
272 #[allow(unused_mut)]
273 let mut copied = props.copied;
274 let code = props.code.clone();
275
276 rsx! {
277 button {
278 class: "btn btn-ghost btn-xs opacity-60 hover:opacity-100 group-hover:opacity-100 transition-all duration-150 hover:bg-base-content/10",
279 "data-code": "{code}",
280 onclick: move |_| {
281 #[cfg(target_arch = "wasm32")]
283 {
284 use dioxus::prelude::*;
285 let code = code.clone();
286 spawn(async move {
287 let js = format!(
289 "navigator.clipboard.writeText({}).catch(console.error)",
290 serde_json::to_string(&code).unwrap_or_default()
291 );
292 let _ = document::eval(&js);
293 copied.set(true);
294 gloo_timers::future::TimeoutFuture::new(2000).await;
295 copied.set(false);
296 });
297 }
298 },
299 if copied() {
300 Icon { class: "size-4 text-success", icon: LdCheck }
301 } else {
302 Icon { class: "size-4", icon: LdCopy }
303 }
304 }
305 }
306}