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 Some(language) = code_language(props.language.as_deref(), props.filename.as_deref()) else {
186 return plain_code_block(&props.code);
187 };
188 let theme = match try_use_context::<CodeThemeOverride>() {
189 Some(CodeThemeOverride(theme)) => theme(),
190 None => CodeTheme::system(Theme::GITHUB_LIGHT, Theme::TOKYO_NIGHT),
191 };
192
193 rsx! {
194 Code {
195 src: SourceCode::new(language, props.code),
196 theme,
197 }
198 }
199}
200
201#[cfg(not(feature = "highlight"))]
208#[component]
209fn HighlightedCode(props: HighlightedCodeProps) -> Element {
210 plain_code_block(&props.code)
211}
212
213pub(crate) fn plain_code_block(code: &str) -> Element {
221 rsx! {
222 pre {
223 class: "dxc",
224 margin: "0",
225 padding: "1rem",
226 overflow: "auto",
227 tab_size: "4",
228 font_family: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", monospace",
229 font_size: "0.9rem",
230 line_height: "1.55",
231 code { "{code}" }
232 }
233 }
234}
235
236#[cfg(feature = "highlight")]
243pub(crate) fn code_language(language: Option<&str>, filename: Option<&str>) -> Option<Language> {
244 language
245 .and_then(language_from_alias)
246 .or_else(|| filename.and_then(Language::detect))
247 .or_else(|| language.and_then(Language::detect))
248}
249
250#[cfg(feature = "highlight")]
251fn language_from_alias(language: &str) -> Option<Language> {
252 let normalized = language.trim().to_ascii_lowercase();
253 match normalized.as_str() {
254 #[cfg(feature = "lang-bash")]
255 "bash" | "sh" | "shell" | "zsh" | "console" | "terminal" => Some(Language::Bash),
256 #[cfg(feature = "lang-cpp")]
257 "c++" | "cc" | "cxx" | "hpp" => Some(Language::Cpp),
258 #[cfg(feature = "lang-c-sharp")]
259 "c#" | "cs" => Some(Language::CSharp),
260 #[cfg(feature = "lang-dockerfile")]
261 "docker" | "dockerfile" | "containerfile" => Some(Language::Dockerfile),
262 #[cfg(feature = "lang-html")]
263 "html" | "htm" => Some(Language::Html),
264 #[cfg(feature = "lang-javascript")]
265 "js" | "javascript" | "jsx" | "mjs" | "cjs" => Some(Language::JavaScript),
266 #[cfg(feature = "lang-json")]
267 "json" | "jsonc" => Some(Language::Json),
268 #[cfg(feature = "lang-markdown")]
269 "markdown" | "md" | "mdx" => Some(Language::Markdown),
270 #[cfg(feature = "lang-python")]
271 "py" | "python" => Some(Language::Python),
272 "rs" | "rust" => Some(Language::Rust),
275 #[cfg(feature = "lang-typescript")]
276 "ts" | "typescript" => Some(Language::TypeScript),
277 #[cfg(feature = "lang-tsx")]
278 "tsx" => Some(Language::Tsx),
279 #[cfg(feature = "lang-toml")]
280 "toml" => Some(Language::Toml),
281 #[cfg(feature = "lang-yaml")]
282 "yaml" | "yml" => Some(Language::Yaml),
283 other => Language::from_slug(other),
284 }
285}
286
287#[derive(Props, Clone, PartialEq)]
289struct CopyButtonProps {
290 code: String,
291 copied: Signal<bool>,
292}
293
294#[component]
296fn CopyButton(props: CopyButtonProps) -> Element {
297 #[allow(unused_mut)]
298 let mut copied = props.copied;
299 let code = props.code.clone();
300
301 rsx! {
302 button {
303 class: "btn btn-ghost btn-xs opacity-60 hover:opacity-100 group-hover:opacity-100 transition-all duration-150 hover:bg-base-content/10",
304 "data-code": "{code}",
305 onclick: move |_| {
306 #[cfg(target_arch = "wasm32")]
308 {
309 use dioxus::prelude::*;
310 let code = code.clone();
311 spawn(async move {
312 let js = format!(
314 "navigator.clipboard.writeText({}).catch(console.error)",
315 serde_json::to_string(&code).unwrap_or_default()
316 );
317 let _ = document::eval(&js);
318 copied.set(true);
319 gloo_timers::future::TimeoutFuture::new(2000).await;
320 copied.set(false);
321 });
322 }
323 },
324 if copied() {
325 Icon { class: "size-4 text-success", icon: LdCheck }
326 } else {
327 Icon { class: "size-4", icon: LdCopy }
328 }
329 }
330 }
331}
332
333#[cfg(all(test, feature = "highlight"))]
334mod tests {
335 use super::*;
336
337 #[test]
338 fn rust_needs_no_lang_feature() {
339 assert_eq!(code_language(Some("rs"), None), Some(Language::Rust));
340 }
341
342 #[test]
343 fn unknown_language_has_no_grammar() {
344 assert_eq!(code_language(Some("brainfuck"), None), None);
345 }
346
347 #[test]
350 fn bare_fence_has_no_grammar() {
351 assert_eq!(code_language(None, None), None);
352 }
353
354 #[test]
359 #[cfg(not(feature = "lang-cpp"))]
360 fn disabled_grammar_falls_back_to_plain_text() {
361 assert_eq!(code_language(Some("c++"), None), None);
362 assert_eq!(code_language(Some("cpp"), None), None);
363 }
364
365 #[test]
366 #[cfg(not(feature = "lang-c-sharp"))]
367 fn disabled_c_sharp_grammar_falls_back_to_plain_text() {
368 assert_eq!(code_language(Some("c#"), None), None);
369 assert_eq!(code_language(Some("cs"), None), None);
370 }
371
372 #[test]
373 #[cfg(feature = "lang-python")]
374 fn enabled_grammar_resolves() {
375 assert_eq!(code_language(Some("py"), None), Some(Language::Python));
376 }
377}