use std::fmt::Write;
use crate::customization::IdentifierShortener;
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, shortener: IdentifierShortener) -> 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, shortener);
}
}
css
}
fn generate_rule_css(
css: &mut String,
rule: &crate::themes::compiled::CompiledThemeRule,
prefix: &str,
shortener: IdentifierShortener,
) {
let css_selector = scope_to_css_selector(rule.selector.target_scope, prefix, false, shortener);
let original_len = css.len();
let mut is_empty = true;
write!(css, "{css_selector} {{").unwrap();
if let Some(fg) = rule.style_modifier.foreground {
write!(css, " {}", fg.as_css_color_property()).unwrap();
is_empty = false;
}
if let Some(bg) = rule.style_modifier.background {
write!(css, " {}", bg.as_css_bg_color_property()).unwrap();
is_empty = false;
}
if let Some(font_style) = rule.style_modifier.font_style {
let attrs = font_style.css_attributes();
if !attrs.is_empty() {
write!(css, " {}", attrs.join("")).unwrap();
is_empty = false;
}
}
if is_empty {
css.truncate(original_len);
} else {
writeln!(css, " }}").unwrap();
}
}
pub fn scope_to_css_selector(
scope: Scope,
prefix: &str,
as_class: bool,
shortener: IdentifierShortener,
) -> String {
let css_classes = scope_to_css_classes(scope, prefix, shortener);
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,
shortener: IdentifierShortener,
) -> 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, shortener);
css_classes.push(class);
}
}
}
css_classes.sort();
css_classes.dedup();
css_classes
}
fn escape_css_identifier(identifier: &str, prefix: &str, shortener: IdentifierShortener) -> 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
});
shortener(&identifier, prefix)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::customization::shorten_identifier;
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-", shorten_identifier),
"k"
);
assert_eq!(
escape_css_identifier("meta-function", "g-", shorten_identifier),
"g-meta-function"
);
assert_eq!(
escape_css_identifier("git_gutter", "g-", shorten_identifier),
"g-git_gutter"
);
assert_eq!(
escape_css_identifier("c++", "g-", shorten_identifier),
"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, shorten_identifier);
assert_eq!(selector, ".k.o");
let simple_scope = Scope::new("comment")[0];
let simple_selector = scope_to_css_selector(simple_scope, "g-", false, shorten_identifier);
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, shorten_identifier);
assert_eq!(class, "k o");
let simple_scope = Scope::new("comment")[0];
let simple_class = scope_to_css_selector(simple_scope, "g-", true, shorten_identifier);
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-", shorten_identifier));
}
}