#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc = include_str!("../README.md")]
#![warn(missing_docs)]
extern crate self as dioxus_code;
use dioxus::prelude::*;
#[cfg(feature = "runtime")]
use std::collections::HashMap;
mod language;
pub use language::Language;
const CODE_CSS: Asset = asset!("/assets/dioxus-code.css");
#[cfg(feature = "macro")]
#[cfg_attr(docsrs, doc(cfg(feature = "macro")))]
pub use dioxus_code_macro::code;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct CodeOptions {
language: Option<Language>,
}
impl CodeOptions {
pub const fn new() -> Self {
Self { language: None }
}
pub const fn builder() -> Self {
Self::new()
}
pub fn with_language(mut self, language: impl Into<Option<Language>>) -> Self {
self.language = language.into();
self
}
pub const fn language(self) -> Option<Language> {
self.language
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Theme {
stylesheet: ThemeStylesheet,
system_light: ThemeStylesheet,
system_dark: ThemeStylesheet,
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct ThemeStylesheet {
class: &'static str,
asset: Asset,
}
impl Theme {
const fn stylesheet(self) -> ThemeStylesheet {
self.stylesheet
}
const fn system_light(self) -> ThemeStylesheet {
self.system_light
}
const fn system_dark(self) -> ThemeStylesheet {
self.system_dark
}
}
impl Default for Theme {
fn default() -> Self {
Self::RUSTDOC_AYU
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CodeTheme {
selection: CodeThemeSelection,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum CodeThemeChoice<T> {
Fixed(T),
System { light: T, dark: T },
}
type CodeThemeSelection = CodeThemeChoice<Theme>;
type CodeThemeStylesheets = CodeThemeChoice<ThemeStylesheet>;
impl CodeTheme {
pub const fn fixed(theme: Theme) -> Self {
Self {
selection: CodeThemeSelection::Fixed(theme),
}
}
pub const fn system(light: Theme, dark: Theme) -> Self {
Self {
selection: CodeThemeSelection::System { light, dark },
}
}
pub fn classes(self) -> String {
match self.stylesheets() {
CodeThemeStylesheets::Fixed(stylesheet) => stylesheet.class.to_string(),
CodeThemeStylesheets::System { light, dark } => {
format!("dxc-system {} {}", light.class, dark.class)
}
}
}
const fn stylesheets(self) -> CodeThemeStylesheets {
match self.selection {
CodeThemeSelection::Fixed(theme) => CodeThemeStylesheets::Fixed(theme.stylesheet()),
CodeThemeSelection::System { light, dark } => CodeThemeStylesheets::System {
light: light.system_light(),
dark: dark.system_dark(),
},
}
}
}
impl Default for CodeTheme {
fn default() -> Self {
Self::fixed(Theme::default())
}
}
impl From<Theme> for CodeTheme {
fn from(theme: Theme) -> Self {
Self::fixed(theme)
}
}
include!(concat!(env!("OUT_DIR"), "/theme_assets.rs"));
pub mod advanced;
pub use advanced::{HighlightError, HighlightQueryErrorKind};
#[cfg(feature = "runtime")]
#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceCode {
source: String,
language: Language,
}
#[cfg(feature = "runtime")]
#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]
impl SourceCode {
pub fn new(language: Language, source: impl ToString) -> Self {
Self {
source: source.to_string(),
language,
}
}
pub fn with_language(mut self, language: Language) -> Self {
self.language = language;
self
}
pub fn highlight(self) -> Result<advanced::HighlightedSource, HighlightError> {
advanced::Buffer::new(self.language, self.source).map(|buffer| buffer.highlighted())
}
fn highlight_or_plaintext(self) -> advanced::HighlightedSource {
let language = self.language;
let source = self.source.clone();
match self.highlight() {
Ok(source) => source,
Err(_) => advanced::HighlightedSource::plaintext(source, language),
}
}
}
#[cfg(feature = "runtime")]
pub(crate) struct RawHighlightSpan {
pub(crate) start: u32,
pub(crate) end: u32,
pub(crate) tag: Option<&'static str>,
pub(crate) pattern_index: u32,
}
#[cfg(feature = "runtime")]
pub(crate) fn normalize_spans(
spans: impl IntoIterator<Item = RawHighlightSpan>,
) -> Vec<advanced::HighlightSpan> {
let mut deduped: HashMap<(u32, u32), RawHighlightSpan> = HashMap::new();
for span in spans.into_iter() {
let key = (span.start, span.end);
if let Some(existing) = deduped.get(&key) {
let should_replace = match (span.tag.is_some(), existing.tag.is_some()) {
(true, false) => true,
(false, true) => false,
_ => span.pattern_index >= existing.pattern_index,
};
if should_replace {
deduped.insert(key, span);
}
} else {
deduped.insert(key, span);
}
}
let mut spans: Vec<_> = deduped
.into_values()
.filter_map(|span| {
Some(advanced::HighlightSpan::new(
span.start..span.end,
span.tag?,
))
})
.collect();
spans.sort_by_key(|span| (span.start(), span.end()));
let mut coalesced: Vec<advanced::HighlightSpan> = Vec::with_capacity(spans.len());
for span in spans {
if let Some(last) = coalesced.last_mut()
&& span.tag() == last.tag()
&& span.start() <= last.end()
{
last.set_end(last.end().max(span.end()));
continue;
}
coalesced.push(span);
}
coalesced
}
#[cfg(feature = "runtime")]
#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]
impl From<SourceCode> for advanced::HighlightedSource {
fn from(code: SourceCode) -> Self {
code.highlight_or_plaintext()
}
}
#[derive(Props, Clone, PartialEq)]
pub struct CodeProps {
#[props(into)]
pub src: advanced::HighlightedSource,
#[props(default, into)]
pub theme: CodeTheme,
}
#[component]
pub fn Code(props: CodeProps) -> Element {
let source = &props.src;
let segments = source.trimmed_segments();
let class = format!("dxc {}", props.theme.classes());
let language = source.language().slug();
rsx! {
advanced::CodeThemeStyles { theme: props.theme }
document::Stylesheet { href: CODE_CSS }
pre {
class,
"data-language": language,
code {
for segment in segments {
if let Some(tag) = segment.tag() {
advanced::TokenSpan {
text: segment.text(),
tag,
}
} else {
span {
"{segment.text()}"
}
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn system_theme_classes_include_scoped_slots() {
assert_eq!(
CodeTheme::system(Theme::GITHUB_LIGHT, Theme::TOKYO_NIGHT).classes(),
"dxc-system dxc-system-light-github-light dxc-system-dark-tokyo-night",
);
}
#[test]
fn plaintext_is_escaped() {
assert_eq!(
advanced::HighlightedSource::from_static_parts(
"<script>alert(1)</script>",
Language::Rust,
&[]
)
.segments(),
vec![advanced::HighlightSegment::new(
"<script>alert(1)</script>",
None,
)]
);
}
#[test]
fn highlighted_lines_preserve_trailing_empty_line() {
let source =
advanced::HighlightedSource::from_static_parts("let x = 1;\n", Language::Rust, &[]);
let lines = source.lines();
assert_eq!(lines.len(), 2);
assert_eq!(
lines[0],
vec![advanced::HighlightSegment::new("let x = 1;", None)]
);
assert!(lines[1].is_empty());
}
#[test]
fn code_options_accepts_language_options() {
assert_eq!(
CodeOptions::builder()
.with_language(Language::Rust)
.language(),
Some(Language::Rust),
);
assert_eq!(
CodeOptions::builder()
.with_language(Some(Language::Rust))
.language(),
Some(Language::Rust),
);
assert_eq!(CodeOptions::builder().with_language(None).language(), None);
}
#[cfg(feature = "runtime")]
#[test]
fn runtime_source_code_highlights() {
let tree: advanced::HighlightedSource =
SourceCode::new(Language::Rust, "fn main() {}").into();
assert_eq!(tree.language(), Language::Rust);
assert!(tree.spans().iter().any(|span| {
span.tag() == "k" && &tree.source()[span.start() as usize..span.end() as usize] == "fn"
}));
}
}