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;
19
20/// Compile-time options for the [`code!`] macro.
21///
22/// The [`code!`] macro reads 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.
25///
26/// ```rust
27/// use dioxus_code::{CodeOptions, Language, code};
28///
29/// let _source = code!(
30///     "/snippets/demo.rs",
31///     CodeOptions::builder().with_language(Language::Rust)
32/// );
33/// ```
34#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
35pub struct CodeOptions {
36    language: Option<Language>,
37}
38
39impl CodeOptions {
40    /// Create default code options.
41    ///
42    /// ```rust
43    /// use dioxus_code::CodeOptions;
44    /// let _opts = CodeOptions::new();
45    /// ```
46    pub const fn new() -> Self {
47        Self { language: None }
48    }
49
50    /// Create default code options.
51    ///
52    /// Alias for [`Self::new`], matching builder-style asset APIs.
53    ///
54    /// ```rust
55    /// use dioxus_code::{CodeOptions, Language};
56    /// let _opts = CodeOptions::builder().with_language(Language::Rust);
57    /// ```
58    pub const fn builder() -> Self {
59        Self::new()
60    }
61
62    /// Set the language explicitly.
63    ///
64    /// Pass a [`Language`] variant directly, `Some(Language::...)`, or `None`.
65    ///
66    /// ```rust
67    /// use dioxus_code::{CodeOptions, Language};
68    /// let _opts = CodeOptions::new().with_language(Language::Rust);
69    /// ```
70    pub fn with_language(mut self, language: impl Into<Option<Language>>) -> Self {
71        self.language = language.into();
72        self
73    }
74
75    /// The explicit language, if one was configured.
76    pub const fn language(self) -> Option<Language> {
77        self.language
78    }
79}
80
81/// A syntax-highlighting theme.
82///
83/// Themes are exposed as associated constants on [`Theme`] (for example
84/// [`Theme::TOKYO_NIGHT`]) and ship as scoped CSS so multiple themes can
85/// coexist on the same page without leaking styles.
86///
87/// ```rust
88/// use dioxus_code::Theme;
89/// let _theme = Theme::TOKYO_NIGHT;
90/// ```
91#[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/// Syntax theme selection for [`Code()`].
125///
126/// ```rust
127/// use dioxus_code::{CodeTheme, Theme};
128/// let _theme = CodeTheme::fixed(Theme::TOKYO_NIGHT);
129/// ```
130#[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    /// Create a fixed theme selection.
146    ///
147    /// ```rust
148    /// use dioxus_code::{CodeTheme, Theme};
149    /// let _theme = CodeTheme::fixed(Theme::TOKYO_NIGHT);
150    /// ```
151    pub const fn fixed(theme: Theme) -> Self {
152        Self {
153            selection: CodeThemeSelection::Fixed(theme),
154        }
155    }
156
157    /// Create a CSS-only system theme pair.
158    ///
159    /// ```rust
160    /// use dioxus_code::{CodeTheme, Theme};
161    /// let _theme = CodeTheme::system(Theme::GITHUB_LIGHT, Theme::TOKYO_NIGHT);
162    /// ```
163    pub const fn system(light: Theme, dark: Theme) -> Self {
164        Self {
165            selection: CodeThemeSelection::System { light, dark },
166        }
167    }
168
169    /// CSS classes to apply to a code container using this theme selection.
170    ///
171    /// ```rust
172    /// use dioxus_code::{CodeTheme, Theme};
173    /// let classes = CodeTheme::fixed(Theme::TOKYO_NIGHT).classes();
174    /// assert!(classes.contains("dxc-tokyo-night"));
175    /// ```
176    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/// Source text to highlight at runtime.
214///
215/// Available with the `runtime` feature. Build one with [`SourceCode::new`],
216/// then pass it to [`Code()`].
217///
218/// ```rust
219/// use dioxus_code::{Language, SourceCode};
220/// let _src = SourceCode::new(Language::Rust, "fn main() {}");
221/// ```
222#[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    /// Wrap a raw source string with an explicit language.
234    ///
235    /// ```rust
236    /// use dioxus_code::{Language, SourceCode};
237    /// let _src = SourceCode::new(Language::Rust, "fn main() {}");
238    /// ```
239    pub fn new(language: Language, source: impl ToString) -> Self {
240        Self {
241            source: source.to_string(),
242            language,
243        }
244    }
245
246    /// Replace the language used to highlight this source.
247    ///
248    /// To set the language from a runtime slug, use [`Language::from_slug`]
249    /// and pass the resulting variant.
250    ///
251    /// ```rust
252    /// use dioxus_code::{Language, SourceCode};
253    /// let _src = SourceCode::new(Language::Rust, "fn main() {}").with_language(Language::Rust);
254    /// ```
255    pub fn with_language(mut self, language: Language) -> Self {
256        self.language = language;
257        self
258    }
259
260    /// Highlight this source, returning typed errors when runtime highlighting fails.
261    ///
262    /// Use `Into<HighlightedSource>` for the lossy rendering path that discards
263    /// the error and renders plaintext.
264    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/// Props for [`Code()`].
344///
345/// ```rust
346/// use dioxus_code::{CodeProps, Theme, code};
347/// let _props = CodeProps {
348///     src: code!("/snippets/demo.rs"),
349///     theme: Theme::TOKYO_NIGHT.into(),
350/// };
351/// ```
352#[derive(Props, Clone, PartialEq)]
353pub struct CodeProps {
354    /// Source to render.
355    #[props(into)]
356    pub src: advanced::HighlightedSource,
357    /// Syntax theme. Defaults to [`Theme::RUSTDOC_AYU`].
358    #[props(default, into)]
359    pub theme: CodeTheme,
360}
361
362/// Render syntax-highlighted source code.
363///
364/// Pair the [`code!`] macro for compile-time parsing, or [`SourceCode`] for
365/// runtime parsing with the `runtime` feature. The component injects its own
366/// stylesheet plus the selected theme's stylesheet.
367///
368/// ```rust
369/// use dioxus::prelude::*;
370/// use dioxus_code::{Code, Theme, code};
371///
372/// fn _example() -> Element {
373///     rsx! {
374///         Code { src: code!("/snippets/demo.rs"), theme: Theme::TOKYO_NIGHT }
375///     }
376/// }
377/// ```
378#[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}