use crate::theme::Theme;
pub fn generate_css_vars(theme: &Theme) -> String {
let mut lines = Vec::new();
lines.push(":root {".to_string());
let mut names = theme.token_names();
names.sort_unstable();
for name in names {
let color = theme.color(name);
let var_name = css_ident(name);
lines.push(format!(" --opaline-{var_name}: {};", color.to_hex()));
}
#[cfg(feature = "gradients")]
{
let mut grad_names = theme.gradient_names();
grad_names.sort_unstable();
for name in grad_names {
if let Some(gradient) = theme.get_gradient(name) {
let stops: Vec<String> = gradient.stops().iter().map(|c| c.to_hex()).collect();
let var_name = css_ident(name);
lines.push(format!(
" --opaline-gradient-{var_name}: linear-gradient(to right, {});",
stops.join(", ")
));
}
}
}
lines.push("}".to_string());
lines.join("\n")
}
pub fn generate_css_classes(theme: &Theme) -> String {
let mut blocks = Vec::new();
let mut names = theme.style_names();
names.sort_unstable();
for name in names {
let style = theme.style(name);
let class_name = css_ident(name);
let mut props = Vec::new();
if let Some(fg) = style.fg {
props.push(format!(" color: {};", fg.to_hex()));
}
if let Some(bg) = style.bg {
props.push(format!(" background-color: {};", bg.to_hex()));
}
if style.bold {
props.push(" font-weight: bold;".to_string());
}
if style.dim {
props.push(" opacity: 0.7;".to_string());
}
if style.italic {
props.push(" font-style: italic;".to_string());
}
let mut decorations = Vec::new();
if style.underline {
decorations.push("underline");
}
if style.crossed_out {
decorations.push("line-through");
}
if !decorations.is_empty() {
props.push(format!(" text-decoration: {};", decorations.join(" ")));
}
if style.hidden {
props.push(" visibility: hidden;".to_string());
}
if !props.is_empty() {
blocks.push(format!(
".opaline-{class_name} {{\n{}\n}}",
props.join("\n")
));
}
}
blocks.join("\n\n")
}
pub fn generate_stylesheet(theme: &Theme) -> String {
let vars = generate_css_vars(theme);
let classes = generate_css_classes(theme);
if classes.is_empty() {
vars
} else {
format!("{vars}\n\n{classes}")
}
}
fn css_ident(name: &str) -> String {
let normalized = name.replace(['.', '_'], "-");
escape_css_ident_fragment(&normalized)
}
fn escape_css_ident_fragment(name: &str) -> String {
if name.is_empty() {
return "_".to_string();
}
let chars: Vec<char> = name.chars().collect();
let mut escaped = String::with_capacity(name.len());
for (idx, ch) in chars.iter().copied().enumerate() {
let next_is_digit = chars.get(idx + 1).is_some_and(char::is_ascii_digit);
let safe_ascii = ch.is_ascii_alphabetic() || ch == '_' || ch == '-';
let safe_digit = idx > 0 && ch.is_ascii_digit();
let needs_escape = !(safe_ascii || safe_digit)
|| (idx == 0 && ch.is_ascii_digit())
|| (idx == 0 && ch == '-' && next_is_digit);
if needs_escape {
push_css_escape(&mut escaped, ch);
} else {
escaped.push(ch);
}
}
escaped
}
fn push_css_escape(output: &mut String, ch: char) {
use std::fmt::Write as _;
write!(output, "\\{:x} ", u32::from(ch)).expect("write to string");
}