use super::{Formatter, HtmlElement};
use crate::formatter::html_inline::HighlightLines;
use crate::highlight;
use crate::languages::Language;
use crate::themes::Theme;
use derive_builder::Builder;
use lumis_core::formatter::Formatter as _;
use std::collections::HashMap;
use std::io::{self, Write};
use std::str::FromStr;
#[derive(Clone, Debug)]
pub enum DefaultTheme {
Theme(String),
LightDark,
}
impl FromStr for DefaultTheme {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"light-dark()" => DefaultTheme::LightDark,
theme_name => DefaultTheme::Theme(theme_name.to_string()),
})
}
}
#[derive(Builder, Clone, Debug)]
#[builder(default, build_fn(skip))]
pub struct HtmlMultiThemes {
lang: Language,
themes: HashMap<String, Theme>,
#[builder(setter(custom))]
default_theme: Option<DefaultTheme>,
#[builder(setter(into))]
css_variable_prefix: String,
pre_class: Option<String>,
italic: bool,
include_highlights: bool,
highlight_lines: Option<HighlightLines>,
header: Option<HtmlElement>,
}
impl HtmlMultiThemesBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn default_theme<T: Into<DefaultThemeArg>>(&mut self, value: T) -> &mut Self {
self.default_theme = Some(value.into().into_enum());
self
}
pub fn build(&mut self) -> Result<HtmlMultiThemes, String> {
let result = HtmlMultiThemes {
lang: self.lang.take().unwrap_or(Language::PlainText),
themes: self.themes.take().unwrap_or_default(),
default_theme: self.default_theme.take().flatten(),
css_variable_prefix: self
.css_variable_prefix
.take()
.unwrap_or_else(|| "--lumis".to_string()),
pre_class: self.pre_class.take().flatten(),
italic: self.italic.take().unwrap_or(false),
include_highlights: self.include_highlights.take().unwrap_or(false),
highlight_lines: self.highlight_lines.take().flatten(),
header: self.header.take().flatten(),
};
if result.themes.is_empty() {
return Err("At least one theme is required".to_string());
}
match &result.default_theme {
Some(DefaultTheme::Theme(name)) if !result.themes.contains_key(name) => {
return Err(format!("Default theme '{}' not found in themes map", name));
}
Some(DefaultTheme::LightDark)
if !result.themes.contains_key("light") || !result.themes.contains_key("dark") =>
{
return Err("LightDark mode requires themes named 'light' and 'dark'".to_string());
}
None => {
}
_ => {}
}
Ok(result)
}
}
#[doc(hidden)]
pub enum DefaultThemeArg {
String(String),
Bool(bool),
}
impl DefaultThemeArg {
fn into_enum(self) -> Option<DefaultTheme> {
match self {
DefaultThemeArg::String(s) => Some(s.parse().unwrap()),
DefaultThemeArg::Bool(false) => None,
DefaultThemeArg::Bool(true) => Some(DefaultTheme::Theme("light".to_string())),
}
}
}
impl From<&str> for DefaultThemeArg {
fn from(s: &str) -> Self {
DefaultThemeArg::String(s.to_string())
}
}
impl From<String> for DefaultThemeArg {
fn from(s: String) -> Self {
DefaultThemeArg::String(s)
}
}
impl From<bool> for DefaultThemeArg {
fn from(b: bool) -> Self {
DefaultThemeArg::Bool(b)
}
}
impl Default for HtmlMultiThemes {
fn default() -> Self {
Self {
lang: Language::PlainText,
themes: HashMap::new(),
default_theme: None,
css_variable_prefix: "--lumis".to_string(),
pre_class: None,
italic: false,
include_highlights: false,
highlight_lines: None,
header: None,
}
}
}
impl Formatter for HtmlMultiThemes {
fn format(&self, source: &str, output: &mut dyn Write) -> io::Result<()> {
let events = highlight::highlight_events(source, self.lang).map_err(io::Error::other)?;
let core_formatter = lumis_core::formatter::html_multi_themes::HtmlMultiThemes::new(
self.lang,
self.themes.clone(),
self.default_theme.clone().map(map_default_theme),
self.css_variable_prefix.clone(),
self.pre_class.clone(),
self.italic,
self.include_highlights,
self.highlight_lines.clone().map(map_highlight_lines),
self.header.clone(),
);
core_formatter.render(source, &events, output)
}
}
fn map_default_theme(
default_theme: DefaultTheme,
) -> lumis_core::formatter::html_multi_themes::DefaultTheme {
match default_theme {
DefaultTheme::Theme(name) => {
lumis_core::formatter::html_multi_themes::DefaultTheme::Theme(name)
}
DefaultTheme::LightDark => {
lumis_core::formatter::html_multi_themes::DefaultTheme::LightDark
}
}
}
fn map_highlight_lines(
highlight_lines: HighlightLines,
) -> lumis_core::formatter::html_inline::HighlightLines {
lumis_core::formatter::html_inline::HighlightLines {
lines: highlight_lines.lines,
style: highlight_lines.style.map(|style| match style {
crate::formatter::html_inline::HighlightLinesStyle::Theme => {
lumis_core::formatter::html_inline::HighlightLinesStyle::Theme
}
crate::formatter::html_inline::HighlightLinesStyle::Style(style) => {
lumis_core::formatter::html_inline::HighlightLinesStyle::Style(style)
}
}),
class: highlight_lines.class,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_decoration() {
use crate::formatter::html::text_decoration;
use crate::themes::{TextDecoration, UnderlineStyle};
let none = TextDecoration::default();
assert_eq!(text_decoration(&none), "none");
let underline = TextDecoration {
underline: UnderlineStyle::Solid,
strikethrough: false,
};
assert_eq!(text_decoration(&underline), "underline");
let wavy = TextDecoration {
underline: UnderlineStyle::Wavy,
strikethrough: false,
};
assert_eq!(text_decoration(&wavy), "underline wavy");
let strike = TextDecoration {
underline: UnderlineStyle::None,
strikethrough: true,
};
assert_eq!(text_decoration(&strike), "line-through");
let both = TextDecoration {
underline: UnderlineStyle::Solid,
strikethrough: true,
};
assert_eq!(text_decoration(&both), "underline line-through");
let wavy_strike = TextDecoration {
underline: UnderlineStyle::Wavy,
strikethrough: true,
};
assert_eq!(text_decoration(&wavy_strike), "underline wavy line-through");
}
#[test]
fn test_theme_mode_generates_font_css_variables() {
let mut themes = HashMap::new();
themes.insert(
"light".to_string(),
crate::themes::get("github_light").unwrap(),
);
themes.insert(
"dark".to_string(),
crate::themes::get("github_dark").unwrap(),
);
let formatter = HtmlMultiThemesBuilder::new()
.lang(Language::Rust)
.themes(themes)
.default_theme("light")
.italic(true)
.build()
.unwrap();
let source = "fn main() {}";
let mut output = Vec::new();
formatter.format(source, &mut output).unwrap();
let html = String::from_utf8(output).unwrap();
assert!(html.contains("--lumis-light-font-style:"));
assert!(html.contains("--lumis-dark-font-style:"));
assert!(html.contains("--lumis-light-font-weight:"));
assert!(html.contains("--lumis-dark-font-weight:"));
assert!(html.contains("--lumis-light-text-decoration:"));
assert!(html.contains("--lumis-dark-text-decoration:"));
}
#[test]
fn test_lightdark_mode_includes_text_decoration() {
let mut themes = HashMap::new();
themes.insert(
"light".to_string(),
crate::themes::get("github_light").unwrap(),
);
themes.insert(
"dark".to_string(),
crate::themes::get("github_dark").unwrap(),
);
let formatter = HtmlMultiThemesBuilder::new()
.lang(Language::Rust)
.themes(themes)
.default_theme("light-dark()")
.italic(true)
.build()
.unwrap();
let source = "fn main() {}";
let mut output = Vec::new();
formatter.format(source, &mut output).unwrap();
let html = String::from_utf8(output).unwrap();
assert!(html.contains("font-weight: light-dark("));
assert!(html.contains("font-style: light-dark("));
assert!(html.contains("text-decoration: light-dark("));
}
#[test]
fn test_lightdark_mode_always_outputs_font_weight() {
let mut themes = HashMap::new();
themes.insert(
"light".to_string(),
crate::themes::get("github_light").unwrap(),
);
themes.insert(
"dark".to_string(),
crate::themes::get("github_dark").unwrap(),
);
let formatter = HtmlMultiThemesBuilder::new()
.lang(Language::Rust)
.themes(themes)
.default_theme("light-dark()")
.build()
.unwrap();
let source = "// comment";
let mut output = Vec::new();
formatter.format(source, &mut output).unwrap();
let html = String::from_utf8(output).unwrap();
assert!(html.contains("font-weight: light-dark(normal, normal)"));
}
#[test]
fn test_none_mode_generates_font_css_variables() {
let mut themes = HashMap::new();
themes.insert(
"light".to_string(),
crate::themes::get("github_light").unwrap(),
);
themes.insert(
"dark".to_string(),
crate::themes::get("github_dark").unwrap(),
);
let formatter = HtmlMultiThemesBuilder::new()
.lang(Language::Rust)
.themes(themes)
.build()
.unwrap();
let source = "fn main() {}";
let mut output = Vec::new();
formatter.format(source, &mut output).unwrap();
let html = String::from_utf8(output).unwrap();
assert!(html.contains("--lumis-light-font-style:"));
assert!(html.contains("--lumis-dark-font-style:"));
assert!(html.contains("--lumis-light-font-weight:"));
assert!(html.contains("--lumis-dark-font-weight:"));
assert!(html.contains("--lumis-light-text-decoration:"));
assert!(html.contains("--lumis-dark-text-decoration:"));
assert!(!html.contains("font-style:italic;"));
assert!(!html.contains("font-weight:bold;"));
}
#[test]
fn test_font_style_values_are_correct() {
let mut themes = HashMap::new();
themes.insert(
"light".to_string(),
crate::themes::get("github_light").unwrap(),
);
themes.insert(
"dark".to_string(),
crate::themes::get("github_dark").unwrap(),
);
let formatter = HtmlMultiThemesBuilder::new()
.lang(Language::Rust)
.themes(themes)
.default_theme("light")
.italic(true)
.build()
.unwrap();
let source = "fn main() {}";
let mut output = Vec::new();
formatter.format(source, &mut output).unwrap();
let html = String::from_utf8(output).unwrap();
assert!(
html.contains("--lumis-light-font-style:normal")
|| html.contains("--lumis-dark-font-style:normal")
);
assert!(
html.contains("--lumis-light-font-weight:normal")
|| html.contains("--lumis-dark-font-weight:normal")
);
assert!(
html.contains("--lumis-light-text-decoration:none")
|| html.contains("--lumis-dark-text-decoration:none")
);
}
#[test]
fn test_italic_flag_respects_lightdark_mode() {
let mut themes = HashMap::new();
themes.insert(
"light".to_string(),
crate::themes::get("github_light").unwrap(),
);
themes.insert(
"dark".to_string(),
crate::themes::get("github_dark").unwrap(),
);
let formatter = HtmlMultiThemesBuilder::new()
.lang(Language::Rust)
.themes(themes.clone())
.default_theme("light-dark()")
.italic(false)
.build()
.unwrap();
let source = "// comment";
let mut output = Vec::new();
formatter.format(source, &mut output).unwrap();
let html = String::from_utf8(output).unwrap();
assert!(!html.contains("font-style: light-dark("));
let formatter = HtmlMultiThemesBuilder::new()
.lang(Language::Rust)
.themes(themes)
.default_theme("light-dark()")
.italic(true)
.build()
.unwrap();
let mut output = Vec::new();
formatter.format(source, &mut output).unwrap();
let html = String::from_utf8(output).unwrap();
assert!(html.contains("font-style: light-dark("));
}
}