diff --git a/src/themes.rs b/src/themes.rs index ae0a748..1baee4c 100644 --- a/src/themes.rs +++ b/src/themes.rs @@ -8,7 +8,10 @@ use std::{ use toml::{Table, Value}; use tracing::{info, warn}; -use typst::foundations::{Content, Func, NativeFunc, func}; +use typst::{ + foundations::{Content, Func, NativeElement, NativeFunc, func}, + text::TextElem, +}; use crate::AppConfig; @@ -90,7 +93,7 @@ impl ThemeRegistry { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] struct Color(String); impl Color { @@ -111,7 +114,7 @@ impl Color { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] struct Style { fg: Option, bold: bool, @@ -214,3 +217,84 @@ pub fn init(config: &AppConfig) { } } } + +pub fn theme_css_func() -> Func { + theme_css::func() +} + +fn scope_to_selector(scope: &str) -> String { + format!(".hl-{}", scope.replace('.', "-")) +} + +fn style_declarations(style: &Style) -> String { + let mut parts = Vec::new(); + if let Some(Color(hex)) = &style.fg { + parts.push(format!("color:{hex}")); + } + parts.push(format!( + "font-weight:{}", + if style.bold { "bold" } else { "normal" } + )); + parts.push(format!( + "font-style:{}", + if style.italic { "italic" } else { "normal" } + )); + parts.push(format!( + "text-decoration:{}", + if style.underlined { + "underline" + } else { + "none" + } + )); + parts.join(";") +} + +fn generate_css(scopes: &[&str], theme: &Theme) -> String { + let mut groups: HashMap<&Style, Vec<&str>> = HashMap::new(); + for &scope in scopes { + if let Some(style) = theme.styles.get(scope) { + groups.entry(style).or_default().push(scope); + } + } + groups + .iter() + .map(|(style, scopes)| { + let selectors = scopes + .iter() + .map(|s| scope_to_selector(s)) + .collect::>() + .join(","); + format!("{}{{{}}}", selectors, style_declarations(style)) + }) + .collect::>() + .join("\n") +} + +#[func] +fn theme_css(light: String, dark: String) -> String { + let Some(registry) = REGISTRY.get() else { + return String::new(); + }; + let Some(light_theme) = registry.themes.get(&light) else { + warn!("Unknown light theme: {light}"); + return String::new(); + }; + let Some(dark_theme) = registry.themes.get(&dark) else { + warn!("Unknown dark theme: {dark}"); + return String::new(); + }; + + let mut shared: Vec<&str> = light_theme + .styles + .keys() + .filter(|k| dark_theme.styles.contains_key(*k)) + .map(String::as_str) + .collect(); + shared.sort(); + + let light_css = generate_css(&shared, light_theme); + let dark_css = generate_css(&shared, dark_theme); + + format!("{light_css}\n@media (prefers-color-scheme:dark){{{dark_css}}}") +} diff --git a/src/typst.rs b/src/typst.rs index be3074e..33cc119 100644 --- a/src/typst.rs +++ b/src/typst.rs @@ -30,6 +30,12 @@ impl TypstContext { Value::Func(crate::syntax::highlight_func()), ); } + if config.themes_root.is_some() { + inputs.insert( + "theme_css".into(), + Value::Func(crate::themes::theme_css_func()), + ); + } let library = Library::builder() .with_inputs(inputs) .with_features([typst::Feature::Html].into_iter().collect())