1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3#![warn(missing_docs)]
4
5extern crate self as dioxus_code;
6
7use dioxus::prelude::*;
8#[cfg(feature = "runtime")]
9use std::collections::HashMap;
10
11mod language;
12pub use language::Language;
13
14const CODE_CSS: Asset = asset!("/assets/dioxus-code.css");
15
16#[cfg(feature = "macro")]
17#[cfg_attr(docsrs, doc(cfg(feature = "macro")))]
18pub use dioxus_code_macro::code;
19
20#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
35pub struct CodeOptions {
36 language: Option<Language>,
37}
38
39impl CodeOptions {
40 pub const fn new() -> Self {
47 Self { language: None }
48 }
49
50 pub const fn builder() -> Self {
59 Self::new()
60 }
61
62 pub fn with_language(mut self, language: impl Into<Option<Language>>) -> Self {
71 self.language = language.into();
72 self
73 }
74
75 pub const fn language(self) -> Option<Language> {
77 self.language
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq)]
92pub struct Theme {
93 stylesheet: ThemeStylesheet,
94 system_light: ThemeStylesheet,
95 system_dark: ThemeStylesheet,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq)]
99struct ThemeStylesheet {
100 class: &'static str,
101 asset: Asset,
102}
103
104impl Theme {
105 const fn stylesheet(self) -> ThemeStylesheet {
106 self.stylesheet
107 }
108
109 const fn system_light(self) -> ThemeStylesheet {
110 self.system_light
111 }
112
113 const fn system_dark(self) -> ThemeStylesheet {
114 self.system_dark
115 }
116}
117
118impl Default for Theme {
119 fn default() -> Self {
120 Self::RUSTDOC_AYU
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq)]
131pub struct CodeTheme {
132 selection: CodeThemeSelection,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq)]
136enum CodeThemeChoice<T> {
137 Fixed(T),
138 System { light: T, dark: T },
139}
140
141type CodeThemeSelection = CodeThemeChoice<Theme>;
142type CodeThemeStylesheets = CodeThemeChoice<ThemeStylesheet>;
143
144impl CodeTheme {
145 pub const fn fixed(theme: Theme) -> Self {
152 Self {
153 selection: CodeThemeSelection::Fixed(theme),
154 }
155 }
156
157 pub const fn system(light: Theme, dark: Theme) -> Self {
164 Self {
165 selection: CodeThemeSelection::System { light, dark },
166 }
167 }
168
169 pub fn classes(self) -> String {
177 match self.stylesheets() {
178 CodeThemeStylesheets::Fixed(stylesheet) => stylesheet.class.to_string(),
179 CodeThemeStylesheets::System { light, dark } => {
180 format!("dxc-system {} {}", light.class, dark.class)
181 }
182 }
183 }
184
185 const fn stylesheets(self) -> CodeThemeStylesheets {
186 match self.selection {
187 CodeThemeSelection::Fixed(theme) => CodeThemeStylesheets::Fixed(theme.stylesheet()),
188 CodeThemeSelection::System { light, dark } => CodeThemeStylesheets::System {
189 light: light.system_light(),
190 dark: dark.system_dark(),
191 },
192 }
193 }
194}
195
196impl Default for CodeTheme {
197 fn default() -> Self {
198 Self::fixed(Theme::default())
199 }
200}
201
202impl From<Theme> for CodeTheme {
203 fn from(theme: Theme) -> Self {
204 Self::fixed(theme)
205 }
206}
207
208include!(concat!(env!("OUT_DIR"), "/theme_assets.rs"));
209
210pub mod advanced;
211pub use advanced::{HighlightError, HighlightQueryErrorKind};
212
213#[cfg(feature = "runtime")]
223#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]
224#[derive(Debug, Clone, PartialEq, Eq)]
225pub struct SourceCode {
226 source: String,
227 language: Language,
228}
229
230#[cfg(feature = "runtime")]
231#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]
232impl SourceCode {
233 pub fn new(language: Language, source: impl ToString) -> Self {
240 Self {
241 source: source.to_string(),
242 language,
243 }
244 }
245
246 pub fn with_language(mut self, language: Language) -> Self {
256 self.language = language;
257 self
258 }
259
260 pub fn highlight(self) -> Result<advanced::HighlightedSource, HighlightError> {
265 advanced::Buffer::new(self.language, self.source).map(|buffer| buffer.highlighted())
266 }
267
268 fn highlight_or_plaintext(self) -> advanced::HighlightedSource {
269 let language = self.language;
270 let source = self.source.clone();
271 match self.highlight() {
272 Ok(source) => source,
273 Err(_) => advanced::HighlightedSource::plaintext(source, language),
274 }
275 }
276}
277
278#[cfg(feature = "runtime")]
279pub(crate) struct RawHighlightSpan {
280 pub(crate) start: u32,
281 pub(crate) end: u32,
282 pub(crate) tag: Option<&'static str>,
283 pub(crate) pattern_index: u32,
284}
285
286#[cfg(feature = "runtime")]
287pub(crate) fn normalize_spans(
288 spans: impl IntoIterator<Item = RawHighlightSpan>,
289) -> Vec<advanced::HighlightSpan> {
290 let mut deduped: HashMap<(u32, u32), RawHighlightSpan> = HashMap::new();
291
292 for span in spans.into_iter() {
293 let key = (span.start, span.end);
294 if let Some(existing) = deduped.get(&key) {
295 let should_replace = match (span.tag.is_some(), existing.tag.is_some()) {
296 (true, false) => true,
297 (false, true) => false,
298 _ => span.pattern_index >= existing.pattern_index,
299 };
300 if should_replace {
301 deduped.insert(key, span);
302 }
303 } else {
304 deduped.insert(key, span);
305 }
306 }
307
308 let mut spans: Vec<_> = deduped
309 .into_values()
310 .filter_map(|span| {
311 Some(advanced::HighlightSpan::new(
312 span.start..span.end,
313 span.tag?,
314 ))
315 })
316 .collect();
317
318 spans.sort_by_key(|span| (span.start(), span.end()));
319
320 let mut coalesced: Vec<advanced::HighlightSpan> = Vec::with_capacity(spans.len());
321 for span in spans {
322 if let Some(last) = coalesced.last_mut()
323 && span.tag() == last.tag()
324 && span.start() <= last.end()
325 {
326 last.set_end(last.end().max(span.end()));
327 continue;
328 }
329 coalesced.push(span);
330 }
331
332 coalesced
333}
334
335#[cfg(feature = "runtime")]
336#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]
337impl From<SourceCode> for advanced::HighlightedSource {
338 fn from(code: SourceCode) -> Self {
339 code.highlight_or_plaintext()
340 }
341}
342
343#[derive(Props, Clone, PartialEq)]
353pub struct CodeProps {
354 #[props(into)]
356 pub src: advanced::HighlightedSource,
357 #[props(default, into)]
359 pub theme: CodeTheme,
360}
361
362#[component]
379pub fn Code(props: CodeProps) -> Element {
380 let source = &props.src;
381 let segments = source.trimmed_segments();
382 let class = format!("dxc {}", props.theme.classes());
383 let language = source.language().slug();
384
385 rsx! {
386 advanced::CodeThemeStyles { theme: props.theme }
387 document::Stylesheet { href: CODE_CSS }
388 pre {
389 class,
390 "data-language": language,
391 code {
392 for segment in segments {
393 if let Some(tag) = segment.tag() {
394 advanced::TokenSpan {
395 text: segment.text(),
396 tag,
397 }
398 } else {
399 span {
400 "{segment.text()}"
401 }
402 }
403 }
404 }
405 }
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn system_theme_classes_include_scoped_slots() {
415 assert_eq!(
416 CodeTheme::system(Theme::GITHUB_LIGHT, Theme::TOKYO_NIGHT).classes(),
417 "dxc-system dxc-system-light-github-light dxc-system-dark-tokyo-night",
418 );
419 }
420
421 #[test]
422 fn plaintext_is_escaped() {
423 assert_eq!(
424 advanced::HighlightedSource::from_static_parts(
425 "<script>alert(1)</script>",
426 Language::Rust,
427 &[]
428 )
429 .segments(),
430 vec![advanced::HighlightSegment::new(
431 "<script>alert(1)</script>",
432 None,
433 )]
434 );
435 }
436
437 #[test]
438 fn highlighted_lines_preserve_trailing_empty_line() {
439 let source =
440 advanced::HighlightedSource::from_static_parts("let x = 1;\n", Language::Rust, &[]);
441 let lines = source.lines();
442 assert_eq!(lines.len(), 2);
443 assert_eq!(
444 lines[0],
445 vec![advanced::HighlightSegment::new("let x = 1;", None)]
446 );
447 assert!(lines[1].is_empty());
448 }
449
450 #[test]
451 fn code_options_accepts_language_options() {
452 assert_eq!(
453 CodeOptions::builder()
454 .with_language(Language::Rust)
455 .language(),
456 Some(Language::Rust),
457 );
458 assert_eq!(
459 CodeOptions::builder()
460 .with_language(Some(Language::Rust))
461 .language(),
462 Some(Language::Rust),
463 );
464 assert_eq!(CodeOptions::builder().with_language(None).language(), None);
465 }
466
467 #[cfg(feature = "runtime")]
468 #[test]
469 fn runtime_source_code_highlights() {
470 let tree: advanced::HighlightedSource =
471 SourceCode::new(Language::Rust, "fn main() {}").into();
472 assert_eq!(tree.language(), Language::Rust);
473 assert!(tree.spans().iter().any(|span| {
474 span.tag() == "k" && &tree.source()[span.start() as usize..span.end() as usize] == "fn"
475 }));
476 }
477}