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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
37pub struct CodeOptions {
38 language: Option<Language>,
39}
40
41impl CodeOptions {
42 pub const fn new() -> Self {
49 Self { language: None }
50 }
51
52 pub const fn builder() -> Self {
61 Self::new()
62 }
63
64 pub fn with_language(mut self, language: impl Into<Option<Language>>) -> Self {
73 self.language = language.into();
74 self
75 }
76
77 pub const fn language(self) -> Option<Language> {
79 self.language
80 }
81}
82
83#[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#[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 pub const fn fixed(theme: Theme) -> Self {
154 Self {
155 selection: CodeThemeSelection::Fixed(theme),
156 }
157 }
158
159 pub const fn system(light: Theme, dark: Theme) -> Self {
166 Self {
167 selection: CodeThemeSelection::System { light, dark },
168 }
169 }
170
171 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#[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 pub fn new(language: Language, source: impl ToString) -> Self {
242 Self {
243 source: source.to_string(),
244 language,
245 }
246 }
247
248 pub fn with_language(mut self, language: Language) -> Self {
258 self.language = language;
259 self
260 }
261
262 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#[derive(Props, Clone, PartialEq)]
355pub struct CodeProps {
356 #[props(into)]
358 pub src: advanced::HighlightedSource,
359 #[props(default, into)]
361 pub theme: CodeTheme,
362}
363
364#[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}