feat: generate css from theme

This commit is contained in:
Jan-Bulthuis 2026-07-19 18:35:45 +02:00
parent 2b0c852e99
commit 2bb0a5c099
2 changed files with 93 additions and 3 deletions

View File

@ -8,7 +8,10 @@ use std::{
use toml::{Table, Value}; use toml::{Table, Value};
use tracing::{info, warn}; use tracing::{info, warn};
use typst::foundations::{Content, Func, NativeFunc, func}; use typst::{
foundations::{Content, Func, NativeElement, NativeFunc, func},
text::TextElem,
};
use crate::AppConfig; use crate::AppConfig;
@ -90,7 +93,7 @@ impl ThemeRegistry {
} }
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Color(String); struct Color(String);
impl Color { impl Color {
@ -111,7 +114,7 @@ impl Color {
} }
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Style { struct Style {
fg: Option<Color>, fg: Option<Color>,
bold: bool, 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::<Vec<_>>()
.join(",");
format!("{}{{{}}}", selectors, style_declarations(style))
})
.collect::<Vec<_>>()
.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}}}")
}

View File

@ -30,6 +30,12 @@ impl TypstContext {
Value::Func(crate::syntax::highlight_func()), 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() let library = Library::builder()
.with_inputs(inputs) .with_inputs(inputs)
.with_features([typst::Feature::Html].into_iter().collect()) .with_features([typst::Feature::Html].into_iter().collect())