use std::collections::{HashMap, HashSet};
use thiserror::Error;
use super::cascade::ComputedStyles;
use super::parser::ComponentValue;
use super::tokenizer::Token;
#[derive(Debug, Error, PartialEq)]
pub enum VarError {
#[error("undefined custom property: --{0}")]
Undefined(String),
#[error("cycle in custom property resolution involving --{0}")]
Cycle(String),
#[error("malformed var() invocation")]
Malformed,
}
pub fn substitute<'i>(
value: &[ComponentValue<'i>],
styles: &ComputedStyles<'i>,
) -> Result<Vec<ComponentValue<'i>>, VarError> {
let mut visited = HashSet::new();
substitute_inner(value, styles, &mut visited)
}
fn substitute_inner<'i>(
value: &[ComponentValue<'i>],
styles: &ComputedStyles<'i>,
visiting: &mut HashSet<String>,
) -> Result<Vec<ComponentValue<'i>>, VarError> {
let mut out: Vec<ComponentValue<'i>> = Vec::with_capacity(value.len());
for cv in value {
match cv {
ComponentValue::Function { name, body } if name.eq_ignore_ascii_case("var") => {
let (var_name, fallback) = parse_var_args(body)?;
if !visiting.insert(var_name.clone()) {
return Err(VarError::Cycle(var_name));
}
let resolved = match styles.get(&var_name) {
Some(rv) => substitute_inner(&rv.value, styles, visiting)?,
None => match fallback {
Some(fb) => substitute_inner(&fb, styles, visiting)?,
None => {
visiting.remove(&var_name);
return Err(VarError::Undefined(var_name));
},
},
};
visiting.remove(&var_name);
out.extend(resolved);
},
ComponentValue::Function { name, body } => {
let inner = substitute_inner(body, styles, visiting)?;
out.push(ComponentValue::Function {
name: name.clone(),
body: inner,
});
},
ComponentValue::Parens(body) => {
out.push(ComponentValue::Parens(substitute_inner(body, styles, visiting)?));
},
ComponentValue::Square(body) => {
out.push(ComponentValue::Square(substitute_inner(body, styles, visiting)?));
},
ComponentValue::Curly(body) => {
out.push(ComponentValue::Curly(substitute_inner(body, styles, visiting)?));
},
other => out.push(other.clone()),
}
}
Ok(out)
}
fn parse_var_args<'i>(
body: &[ComponentValue<'i>],
) -> Result<(String, Option<Vec<ComponentValue<'i>>>), VarError> {
let mut iter = body.iter().enumerate();
let (i, name_cv) = loop {
let (i, cv) = iter.next().ok_or(VarError::Malformed)?;
if !matches!(cv, ComponentValue::Token(Token::Whitespace)) {
break (i, cv);
}
};
let name = match name_cv {
ComponentValue::Token(Token::Ident(s)) if s.starts_with("--") => s.to_string(),
_ => return Err(VarError::Malformed),
};
let mut j = i + 1;
while j < body.len() && matches!(body[j], ComponentValue::Token(Token::Whitespace)) {
j += 1;
}
if j >= body.len() {
return Ok((name, None));
}
if !matches!(body[j], ComponentValue::Token(Token::Comma)) {
return Err(VarError::Malformed);
}
j += 1;
while j < body.len() && matches!(body[j], ComponentValue::Token(Token::Whitespace)) {
j += 1;
}
let fallback = if j < body.len() {
Some(body[j..].to_vec())
} else {
Some(Vec::new())
};
Ok((name, fallback))
}
pub fn resolve_custom_properties<'i>(
styles: &ComputedStyles<'i>,
) -> HashMap<String, Vec<ComponentValue<'i>>> {
let mut out = HashMap::new();
for (name, rv) in styles.iter() {
if !name.starts_with("--") {
continue;
}
let mut visiting = HashSet::new();
if let Ok(resolved) = substitute_inner(&rv.value, styles, &mut visiting) {
out.insert(name.to_string(), resolved);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::html_css::css::cascade::cascade;
use crate::html_css::css::matcher::Element;
use crate::html_css::css::parser::parse_stylesheet;
struct Node;
#[derive(Clone, Copy)]
struct E;
impl Element for E {
fn local_name(&self) -> &str {
"div"
}
fn id(&self) -> Option<&str> {
None
}
fn has_class(&self, _: &str) -> bool {
false
}
fn attribute(&self, _: &str) -> Option<&str> {
None
}
fn has_attribute(&self, _: &str) -> bool {
false
}
fn parent(&self) -> Option<Self> {
None
}
fn prev_element_sibling(&self) -> Option<Self> {
None
}
fn next_element_sibling(&self) -> Option<Self> {
None
}
fn is_empty(&self) -> bool {
true
}
fn first_element_child(&self) -> Option<Self> {
None
}
}
fn cascade_for(css: &'static str) -> ComputedStyles<'static> {
let ss: &'static _ = Box::leak(Box::new(parse_stylesheet(css).unwrap()));
cascade(ss, E, None)
}
fn render_to_string(values: &[ComponentValue<'_>]) -> String {
let mut out = String::new();
for cv in values {
match cv {
ComponentValue::Token(Token::Ident(s)) => {
if !out.is_empty() {
out.push(' ');
}
out.push_str(s);
},
ComponentValue::Token(Token::Number(n)) => {
if !out.is_empty() {
out.push(' ');
}
out.push_str(&format!("{}", n.value));
},
ComponentValue::Token(Token::Dimension { value, unit }) => {
if !out.is_empty() {
out.push(' ');
}
out.push_str(&format!("{}{}", value.value, unit));
},
ComponentValue::Token(Token::Whitespace) => {},
_ => {},
}
}
out
}
#[test]
fn simple_substitution() {
let styles = cascade_for("div { --c: red; color: var(--c); }");
let color = styles.get("color").unwrap();
let resolved = substitute(&color.value, &styles).unwrap();
assert_eq!(render_to_string(&resolved), "red");
}
#[test]
fn fallback_when_undefined() {
let styles = cascade_for("div { color: var(--missing, blue); }");
let color = styles.get("color").unwrap();
let resolved = substitute(&color.value, &styles).unwrap();
assert_eq!(render_to_string(&resolved), "blue");
}
#[test]
fn undefined_without_fallback_errors() {
let styles = cascade_for("div { color: var(--missing); }");
let color = styles.get("color").unwrap();
let res = substitute(&color.value, &styles);
assert!(matches!(res, Err(VarError::Undefined(s)) if s == "--missing"));
}
#[test]
fn nested_var_substitution() {
let styles =
cascade_for("div { --base: 12px; --bigger: var(--base); width: var(--bigger); }");
let width = styles.get("width").unwrap();
let resolved = substitute(&width.value, &styles).unwrap();
assert_eq!(render_to_string(&resolved), "12px");
}
#[test]
fn cycle_two_step_detected() {
let styles = cascade_for("div { --a: var(--b); --b: var(--a); color: var(--a); }");
let color = styles.get("color").unwrap();
let res = substitute(&color.value, &styles);
assert!(matches!(res, Err(VarError::Cycle(_))));
}
#[test]
fn cycle_self_reference_detected() {
let styles = cascade_for("div { --x: var(--x); color: var(--x); }");
let color = styles.get("color").unwrap();
let res = substitute(&color.value, &styles);
assert!(matches!(res, Err(VarError::Cycle(_))));
}
#[test]
fn fallback_can_use_var() {
let styles = cascade_for("div { --known: green; color: var(--missing, var(--known)); }");
let color = styles.get("color").unwrap();
let resolved = substitute(&color.value, &styles).unwrap();
assert_eq!(render_to_string(&resolved), "green");
}
#[test]
fn substitution_inside_function() {
let styles = cascade_for("div { --pad: 10px; width: calc(100% - var(--pad)); }");
let width = styles.get("width").unwrap();
let resolved = substitute(&width.value, &styles).unwrap();
let s = format!("{:?}", resolved);
assert!(s.contains("Dimension"));
assert!(!s.contains("\"var\""));
}
#[test]
fn empty_fallback_substitutes_to_empty() {
let styles = cascade_for("div { color: var(--missing,); }");
let color = styles.get("color").unwrap();
let resolved = substitute(&color.value, &styles).unwrap();
assert!(render_to_string(&resolved).is_empty());
}
#[test]
fn resolve_custom_properties_collects_all() {
let styles = cascade_for("div { --a: red; --b: 12px; --c: var(--a); color: green; }");
let resolved = resolve_custom_properties(&styles);
assert!(resolved.contains_key("--a"));
assert!(resolved.contains_key("--b"));
assert!(resolved.contains_key("--c"));
assert!(!resolved.contains_key("color"));
}
}