Skip to main content

dioxus_code/
lib.rs

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, code_str};
19
20/// Compile-time options for the [`code!`] and [`code_str!`] macros.
21///
22/// Both macros read this builder syntactically; pass
23/// [`CodeOptions::builder`] with [`CodeOptions::with_language`] to override the
24/// language that would otherwise be inferred from the file extension. For
25/// [`code_str!`] the language is required since there is no extension to
26/// infer from.
27///
28/// ```rust
29/// use dioxus_code::{CodeOptions, Language, code};
30///
31/// let _source = code!(
32///     "/snippets/demo.rs",
33///     CodeOptions::builder().with_language(Language::Rust)
34/// );
35/// ```
36#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
37pub struct CodeOptions {
38    language: Option<Language>,
39}
40
41impl CodeOptions {
42    /// Create default code options.
43    ///
44    /// ```rust
45    /// use dioxus_code::CodeOptions;
46    /// let _opts = CodeOptions::new();
47    /// ```
48    pub const fn new() -> Self {
49        Self { language: None }
50    }
51
52    /// Create default code options.
53    ///
54    /// Alias for [`Self::new`], matching builder-style asset APIs.
55    ///
56    /// ```rust
57    /// use dioxus_code::{CodeOptions, Language};
58    /// let _opts = CodeOptions::builder().with_language(Language::Rust);
59    /// ```
60    pub const fn builder() -> Self {
61        Self::new()
62    }
63
64    /// Set the language explicitly.
65    ///
66    /// Pass a [`Language`] variant directly, `Some(Language::...)`, or `None`.
67    ///
68    /// ```rust
69    /// use dioxus_code::{CodeOptions, Language};
70    /// let _opts = CodeOptions::new().with_language(Language::Rust);
71    /// ```
72    pub fn with_language(mut self, language: impl Into<Option<Language>>) -> Self {
73        self.language = language.into();
74        self
75    }
76
77    /// The explicit language, if one was configured.
78    pub const fn language(self) -> Option<Language> {
79        self.language
80    }
81}
82
83/// A syntax-highlighting theme.
84///
85/// Themes are exposed as associated constants on [`Theme`] (for example
86/// [`Theme::TOKYO_NIGHT`]) and ship as scoped CSS so multiple themes can
87/// coexist on the same page without leaking styles.
88///
89/// ```rust
90/// use dioxus_code::Theme;
91/// let _theme = Theme::TOKYO_NIGHT;
92/// ```
93#[derive(Debug, Clone, Copy, PartialEq)]
94pub struct Theme {
95    stylesheet: ThemeStylesheet,
96    system_light: ThemeStylesheet,
97    system_dark: ThemeStylesheet,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq)]
101struct ThemeStylesheet {
102    class: &'static str,
103    asset: Asset,
104}
105
106impl Theme {
107    const fn stylesheet(self) -> ThemeStylesheet {
108        self.stylesheet
109    }
110
111    const fn system_light(self) -> ThemeStylesheet {
112        self.system_light
113    }
114
115    const fn system_dark(self) -> ThemeStylesheet {
116        self.system_dark
117    }
118}
119
120impl Default for Theme {
121    fn default() -> Self {
122        Self::RUSTDOC_AYU
123    }
124}
125
126/// Syntax theme selection for [`Code()`].
127///
128/// ```rust
129/// use dioxus_code::{CodeTheme, Theme};
130/// let _theme = CodeTheme::fixed(Theme::TOKYO_NIGHT);
131/// ```
132#[derive(Debug, Clone, Copy, PartialEq)]
133pub struct CodeTheme {
134    selection: CodeThemeSelection,
135}
136
137#[derive(Debug, Clone, Copy, PartialEq)]
138enum CodeThemeChoice<T> {
139    Fixed(T),
140    System { light: T, dark: T },
141}
142
143type CodeThemeSelection = CodeThemeChoice<Theme>;
144type CodeThemeStylesheets = CodeThemeChoice<ThemeStylesheet>;
145
146impl CodeTheme {
147    /// Create a fixed theme selection.
148    ///
149    /// ```rust
150    /// use dioxus_code::{CodeTheme, Theme};
151    /// let _theme = CodeTheme::fixed(Theme::TOKYO_NIGHT);
152    /// ```
153    pub const fn fixed(theme: Theme) -> Self {
154        Self {
155            selection: CodeThemeSelection::Fixed(theme),
156        }
157    }
158
159    /// Create a CSS-only system theme pair.
160    ///
161    /// ```rust
162    /// use dioxus_code::{CodeTheme, Theme};
163    /// let _theme = CodeTheme::system(Theme::GITHUB_LIGHT, Theme::TOKYO_NIGHT);
164    /// ```
165    pub const fn system(light: Theme, dark: Theme) -> Self {
166        Self {
167            selection: CodeThemeSelection::System { light, dark },
168        }
169    }
170
171    /// CSS classes to apply to a code container using this theme selection.
172    ///
173    /// ```rust
174    /// use dioxus_code::{CodeTheme, Theme};
175    /// let classes = CodeTheme::fixed(Theme::TOKYO_NIGHT).classes();
176    /// assert!(classes.contains("dxc-tokyo-night"));
177    /// ```
178    pub fn classes(self) -> String {
179        match self.stylesheets() {
180            CodeThemeStylesheets::Fixed(stylesheet) => stylesheet.class.to_string(),
181            CodeThemeStylesheets::System { light, dark } => {
182                format!("dxc-system {} {}", light.class, dark.class)
183            }
184        }
185    }
186
187    const fn stylesheets(self) -> CodeThemeStylesheets {
188        match self.selection {
189            CodeThemeSelection::Fixed(theme) => CodeThemeStylesheets::Fixed(theme.stylesheet()),
190            CodeThemeSelection::System { light, dark } => CodeThemeStylesheets::System {
191                light: light.system_light(),
192                dark: dark.system_dark(),
193            },
194        }
195    }
196}
197
198impl Default for CodeTheme {
199    fn default() -> Self {
200        Self::fixed(Theme::default())
201    }
202}
203
204impl From<Theme> for CodeTheme {
205    fn from(theme: Theme) -> Self {
206        Self::fixed(theme)
207    }
208}
209
210include!(concat!(env!("OUT_DIR"), "/theme_assets.rs"));
211
212pub mod advanced;
213pub use advanced::{HighlightError, HighlightQueryErrorKind};
214
215/// Source text to highlight at runtime.
216///
217/// Available with the `runtime` feature. Build one with [`SourceCode::new`],
218/// then pass it to [`Code()`].
219///
220/// ```rust
221/// use dioxus_code::{Language, SourceCode};
222/// let _src = SourceCode::new(Language::Rust, "fn main() {}");
223/// ```
224#[cfg(feature = "runtime")]
225#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]
226#[derive(Debug, Clone, PartialEq, Eq)]
227pub struct SourceCode {
228    source: String,
229    language: Language,
230}
231
232#[cfg(feature = "runtime")]
233#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]
234impl SourceCode {
235    /// Wrap a raw source string with an explicit language.
236    ///
237    /// ```rust
238    /// use dioxus_code::{Language, SourceCode};
239    /// let _src = SourceCode::new(Language::Rust, "fn main() {}");
240    /// ```
241    pub fn new(language: Language, source: impl ToString) -> Self {
242        Self {
243            source: source.to_string(),
244            language,
245        }
246    }
247
248    /// Replace the language used to highlight this source.
249    ///
250    /// To set the language from a runtime slug, use [`Language::from_slug`]
251    /// and pass the resulting variant.
252    ///
253    /// ```rust
254    /// use dioxus_code::{Language, SourceCode};
255    /// let _src = SourceCode::new(Language::Rust, "fn main() {}").with_language(Language::Rust);
256    /// ```
257    pub fn with_language(mut self, language: Language) -> Self {
258        self.language = language;
259        self
260    }
261
262    /// Highlight this source, returning typed errors when runtime highlighting fails.
263    ///
264    /// Use `Into<HighlightedSource>` for the lossy rendering path that discards
265    /// the error and renders plaintext.
266    pub fn highlight(self) -> Result<advanced::HighlightedSource, HighlightError> {
267        advanced::Buffer::new(self.language, self.source).map(|buffer| buffer.highlighted())
268    }
269
270    fn highlight_or_plaintext(self) -> advanced::HighlightedSource {
271        let language = self.language;
272        let source = self.source.clone();
273        match self.highlight() {
274            Ok(source) => source,
275            Err(_) => advanced::HighlightedSource::plaintext(source, language),
276        }
277    }
278}
279
280#[cfg(feature = "runtime")]
281pub(crate) struct RawHighlightSpan {
282    pub(crate) start: u32,
283    pub(crate) end: u32,
284    pub(crate) tag: Option<&'static str>,
285    pub(crate) pattern_index: u32,
286}
287
288#[cfg(feature = "runtime")]
289pub(crate) fn normalize_spans(
290    spans: impl IntoIterator<Item = RawHighlightSpan>,
291) -> Vec<advanced::HighlightSpan> {
292    let mut deduped: HashMap<(u32, u32), RawHighlightSpan> = HashMap::new();
293
294    for span in spans.into_iter() {
295        let key = (span.start, span.end);
296        if let Some(existing) = deduped.get(&key) {
297            let should_replace = match (span.tag.is_some(), existing.tag.is_some()) {
298                (true, false) => true,
299                (false, true) => false,
300                _ => span.pattern_index >= existing.pattern_index,
301            };
302            if should_replace {
303                deduped.insert(key, span);
304            }
305        } else {
306            deduped.insert(key, span);
307        }
308    }
309
310    let mut spans: Vec<_> = deduped
311        .into_values()
312        .filter_map(|span| {
313            Some(advanced::HighlightSpan::new(
314                span.start..span.end,
315                span.tag?,
316            ))
317        })
318        .collect();
319
320    spans.sort_by_key(|span| (span.start(), span.end()));
321
322    let mut coalesced: Vec<advanced::HighlightSpan> = Vec::with_capacity(spans.len());
323    for span in spans {
324        if let Some(last) = coalesced.last_mut()
325            && span.tag() == last.tag()
326            && span.start() <= last.end()
327        {
328            last.set_end(last.end().max(span.end()));
329            continue;
330        }
331        coalesced.push(span);
332    }
333
334    coalesced
335}
336
337#[cfg(feature = "runtime")]
338#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]
339impl From<SourceCode> for advanced::HighlightedSource {
340    fn from(code: SourceCode) -> Self {
341        code.highlight_or_plaintext()
342    }
343}
344
345/// Props for [`Code()`].
346///
347/// ```rust
348/// use dioxus_code::{CodeProps, Theme, code};
349/// let _props = CodeProps {
350///     src: code!("/snippets/demo.rs"),
351///     theme: Theme::TOKYO_NIGHT.into(),
352/// };
353/// ```
354#[derive(Props, Clone, PartialEq)]
355pub struct CodeProps {
356    /// Source to render.
357    #[props(into)]
358    pub src: advanced::HighlightedSource,
359    /// Syntax theme. Defaults to [`Theme::RUSTDOC_AYU`].
360    #[props(default, into)]
361    pub theme: CodeTheme,
362}
363
364/// Render syntax-highlighted source code.
365///
366/// Pair the [`code!`] macro for compile-time parsing, or [`SourceCode`] for
367/// runtime parsing with the `runtime` feature. The component injects its own
368/// stylesheet plus the selected theme's stylesheet.
369///
370/// ```rust
371/// use dioxus::prelude::*;
372/// use dioxus_code::{Code, Theme, code};
373///
374/// fn _example() -> Element {
375///     rsx! {
376///         Code { src: code!("/snippets/demo.rs"), theme: Theme::TOKYO_NIGHT }
377///     }
378/// }
379/// ```
380#[component]
381pub fn Code(props: CodeProps) -> Element {
382    let source = &props.src;
383    let segments = source.trimmed_segments();
384    let class = format!("dxc {}", props.theme.classes());
385    let language = source.language().slug();
386
387    rsx! {
388        advanced::CodeThemeStyles { theme: props.theme }
389        document::Stylesheet { href: CODE_CSS }
390        pre {
391            class,
392            "data-language": language,
393            code {
394                for segment in segments {
395                    if let Some(tag) = segment.tag() {
396                        advanced::TokenSpan {
397                            text: segment.text(),
398                            tag,
399                        }
400                    } else {
401                        span {
402                            "{segment.text()}"
403                        }
404                    }
405                }
406            }
407        }
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414
415    #[test]
416    fn system_theme_classes_include_scoped_slots() {
417        assert_eq!(
418            CodeTheme::system(Theme::GITHUB_LIGHT, Theme::TOKYO_NIGHT).classes(),
419            "dxc-system dxc-system-light-github-light dxc-system-dark-tokyo-night",
420        );
421    }
422
423    #[test]
424    fn plaintext_is_escaped() {
425        assert_eq!(
426            advanced::HighlightedSource::from_static_parts(
427                "<script>alert(1)</script>",
428                Language::Rust,
429                &[]
430            )
431            .segments(),
432            vec![advanced::HighlightSegment::new(
433                "<script>alert(1)</script>",
434                None,
435            )]
436        );
437    }
438
439    #[test]
440    fn highlighted_lines_preserve_trailing_empty_line() {
441        let source =
442            advanced::HighlightedSource::from_static_parts("let x = 1;\n", Language::Rust, &[]);
443        let lines = source.lines();
444        assert_eq!(lines.len(), 2);
445        assert_eq!(
446            lines[0],
447            vec![advanced::HighlightSegment::new("let x = 1;", None)]
448        );
449        assert!(lines[1].is_empty());
450    }
451
452    #[test]
453    fn code_options_accepts_language_options() {
454        assert_eq!(
455            CodeOptions::builder()
456                .with_language(Language::Rust)
457                .language(),
458            Some(Language::Rust),
459        );
460        assert_eq!(
461            CodeOptions::builder()
462                .with_language(Some(Language::Rust))
463                .language(),
464            Some(Language::Rust),
465        );
466        assert_eq!(CodeOptions::builder().with_language(None).language(), None);
467    }
468
469    #[cfg(feature = "runtime")]
470    #[test]
471    fn runtime_source_code_highlights() {
472        let tree: advanced::HighlightedSource =
473            SourceCode::new(Language::Rust, "fn main() {}").into();
474        assert_eq!(tree.language(), Language::Rust);
475        assert!(tree.spans().iter().any(|span| {
476            span.tag() == "k" && &tree.source()[span.start() as usize..span.end() as usize] == "fn"
477        }));
478    }
479
480    #[cfg(feature = "macro")]
481    #[test]
482    fn code_str_macro_highlights_inline_source() {
483        const TREE: advanced::HighlightedSource = code_str!(
484            "fn main() {}",
485            CodeOptions::builder().with_language(Language::Rust)
486        );
487        assert_eq!(TREE.language(), Language::Rust);
488        assert_eq!(TREE.source(), "fn main() {}");
489        assert!(TREE.spans().iter().any(|span| {
490            span.tag() == "k" && &TREE.source()[span.start() as usize..span.end() as usize] == "fn"
491        }));
492    }
493}