use std::fmt::Write;
use crate::customization;
use crate::scope::{EMPTY_ATOM_NUMBER, MAX_ATOMS_IN_SCOPE, Scope, lock_global_scope_repo};
use crate::themes::compiled::CompiledTheme;
pub fn generate_css(theme: &CompiledTheme, prefix: &str) -> String {
let mut css = String::new();
writeln!(css, "/*").unwrap();
writeln!(css, " * theme \"{}\" generated by zalo", theme.name).unwrap();
writeln!(css, " */").unwrap();
writeln!(css).unwrap();
for rule in &theme.rules {
if rule.style_modifier.has_properties() {
generate_rule_css(&mut css, rule, prefix);
}
}
css
}
fn generate_rule_css(
css: &mut String,
rule: &crate::themes::compiled::CompiledThemeRule,
prefix: &str,
) {
let css_selector = scope_to_css_selector(rule.selector.target_scope, prefix, false);
write!(css, "{css_selector} {{").unwrap();
if let Some(fg) = rule.style_modifier.foreground {
write!(css, " {}", fg.as_css_color_property()).unwrap();
}
if let Some(bg) = rule.style_modifier.background {
write!(css, " {}", bg.as_css_bg_color_property()).unwrap();
}
if let Some(font_style) = rule.style_modifier.font_style {
write!(css, " {}", font_style.css_attributes().join("")).unwrap();
}
writeln!(css, " }}").unwrap();
}
pub fn scope_to_css_selector(scope: Scope, prefix: &str, as_class: bool) -> String {
let css_classes = scope_to_css_classes(scope, prefix);
if as_class {
css_classes.join(" ")
} else {
css_classes
.into_iter()
.map(|class| format!(".{}", class))
.collect::<Vec<String>>()
.join("")
}
}
pub fn scope_to_css_classes(scope: Scope, prefix: &str) -> Vec<String> {
let mut css_classes = Vec::new();
let repo = lock_global_scope_repo();
for i in 0..MAX_ATOMS_IN_SCOPE {
let atom_number = scope.atom_at(i);
match atom_number {
0 => break, n if n == EMPTY_ATOM_NUMBER => {
continue;
}
n => {
let atom_str = repo.atom_number_to_str(n);
let class = escape_css_identifier(atom_str, prefix);
css_classes.push(class);
}
}
}
css_classes.sort();
css_classes.dedup();
css_classes
}
fn escape_css_identifier(identifier: &str, prefix: &str) -> String {
let identifier =
identifier
.chars()
.fold(String::with_capacity(identifier.len()), |mut output, c| {
if c.is_ascii_alphabetic()
|| c == '-'
|| c == '_'
|| (!output.is_empty() && c.is_ascii_digit())
{
output.push(c);
} else {
write!(output, "\\{:x} ", c as u32).unwrap();
}
output
});
customization::shorten_identifier(&identifier, prefix)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scope::Scope;
use crate::themes::RawTheme;
use insta::assert_snapshot;
#[test]
fn test_escape_css_identifier() {
assert_eq!(escape_css_identifier("keyword", "g-"), "k");
assert_eq!(
escape_css_identifier("meta-function", "g-"),
"g-meta-function"
);
assert_eq!(escape_css_identifier("git_gutter", "g-"), "g-git_gutter");
assert_eq!(escape_css_identifier("c++", "g-"), "g-c\\2b \\2b ");
}
#[test]
fn test_scope_to_css_selector() {
let scope = Scope::new("keyword.operator")[0];
let selector = scope_to_css_selector(scope, "g-", false);
assert_eq!(selector, ".k.o");
let simple_scope = Scope::new("comment")[0];
let simple_selector = scope_to_css_selector(simple_scope, "g-", false);
assert_eq!(simple_selector, ".c");
}
#[test]
fn test_scope_to_css_class() {
let scope = Scope::new("keyword.operator")[0];
let class = scope_to_css_selector(scope, "g-", true);
assert_eq!(class, "k o");
let simple_scope = Scope::new("comment")[0];
let simple_class = scope_to_css_selector(simple_scope, "g-", true);
assert_eq!(simple_class, "c");
}
#[test]
fn can_generate_css_for_theme() {
let theme = RawTheme::load_from_file(
"grammars-themes/packages/tm-themes/themes/vitesse-black.json",
)
.unwrap()
.compile()
.unwrap();
assert_snapshot!(generate_css(&theme, "g-"));
}
}