use super::Expander;
use super::env::Env;
use crate::breakpoint::{BREAKPOINTS, rank};
use crate::cel::{Error, Value};
use crate::diagnostic::{Code, Location};
use crate::expanded;
use crate::tree::{DeclaredType, Param, ParamValue, Point, Signature, StringPart};
use crate::validate::params::value_at;
#[derive(Clone, Debug)]
pub(super) struct Out {
pub(super) key: String,
pub(super) breakpoint: Option<String>,
pub(super) value: Value,
pub(super) input: Option<String>,
}
impl Out {
fn into_param(self) -> expanded::Param {
expanded::Param {
key: self.key,
breakpoint: self.breakpoint,
value: self.value.to_json(),
input: self.input,
}
}
}
pub(super) fn key_of(param: &Param) -> &str {
match param.value {
ParamValue::Class(_) => "class",
ParamValue::Id(_) => "id",
_ => param.key.as_deref().unwrap_or(""),
}
}
impl<'r> Expander<'r> {
pub(super) fn param_value(
&mut self,
param: &Param,
declared: Option<&DeclaredType>,
env: &Env<'r>,
) -> Option<Value> {
Some(match ¶m.value {
ParamValue::Bare(text) => Value::bare(text, declared),
ParamValue::String(parts) => Value::string(&self.quoted(parts, param, env)),
ParamValue::Json(text) => Value::from_json(&serde_json::from_str(text).ok()?),
ParamValue::Expr(source) => match self.evaluate(source, env) {
Ok(value) => value,
Err(error) => {
self.failed(&error, source, param, env);
return None;
}
},
ParamValue::Flag => Value::Bool(true),
ParamValue::Class(text) | ParamValue::Id(text) => Value::string(text),
})
}
fn failed(&mut self, error: &Error, source: &str, param: &Param, env: &Env) {
let at = value_at(param);
let at = Location::new(at.line, at.column + 1 + error.at());
self.report(
Code::F024,
env,
at,
format!("`{source}`: {}", error.message()),
);
}
fn quoted(&mut self, parts: &[StringPart], param: &Param, env: &Env<'r>) -> String {
let at = value_at(param);
let start = Point {
line: at.line,
column: at.column,
offset: 0,
};
let mut places = self.lines[&env.document].paths_from(start).into_iter();
let mut text = String::new();
for part in parts {
match part {
StringPart::Text(literal) => text.push_str(&literal.value),
StringPart::Interpolation(interpolation) => {
let at = places.next().unwrap_or(at);
text.push_str(
&self
.path_text(&interpolation.path, at, env)
.unwrap_or_default(),
);
}
}
}
text
}
pub(super) fn evaluated(
&mut self,
params: &[&'r Param],
signature: Option<&Signature>,
env: &Env<'r>,
) -> Vec<Out> {
let mut out = Vec::new();
for param in params {
let key = key_of(param).to_string();
let declared = signature
.and_then(|signature| signature.params.iter().find(|d| d.name == key))
.map(|declaration| &declaration.declared);
if let (Some(DeclaredType::Input { .. }), ParamValue::Bare(input)) =
(declared, ¶m.value)
{
out.push(Out {
value: env.input(input),
key,
breakpoint: param.breakpoint.clone(),
input: Some(input.clone()),
});
continue;
}
let Some(value) = self.param_value(param, declared, env) else {
continue;
};
let mut previous = value.to_json();
out.push(Out {
key: key.clone(),
breakpoint: param.breakpoint.clone(),
value,
input: None,
});
let responsive =
matches!(param.value, ParamValue::Expr(_)) && param.breakpoint.is_none();
let Some(values) = env.breakpoints.clone().filter(|_| responsive) else {
continue;
};
for (values, breakpoint) in values.iter().zip(BREAKPOINTS) {
let at = Env {
values: values.clone(),
..env.clone()
};
let Some(value) = self.param_value(param, declared, &at) else {
continue;
};
let written = value.to_json();
if written != previous {
previous = written;
out.push(Out {
key: key.clone(),
breakpoint: Some(breakpoint.to_string()),
value,
input: None,
});
}
}
}
out.retain(|out| !matches!(out.value, Value::Null));
out
}
}
pub(super) fn boxed(fence: Vec<Out>, caller: Vec<Out>) -> Vec<expanded::Param> {
let mut params = Vec::new();
for out in fence {
under(out, &caller, &mut params);
}
params.extend(caller);
let mut classes: Vec<String> = Vec::new();
for out in params.iter().filter(|out| out.key == "class") {
let names = out.value.written().ok().flatten().unwrap_or_default();
for name in names.split_whitespace() {
if !classes.iter().any(|known| known == name) {
classes.push(name.to_string());
}
}
}
params.retain(|out| out.key != "class");
if !classes.is_empty() {
params.push(Out {
key: "class".into(),
breakpoint: None,
value: Value::string(&classes.join(" ")),
input: None,
});
}
params.into_iter().map(Out::into_param).collect()
}
fn under(fence: Out, caller: &[Out], kept: &mut Vec<Out>) {
if fence.key == "class" {
kept.push(fence);
return;
}
let from_here =
|out: &&Out| rank(out.breakpoint.as_deref()) <= rank(fence.breakpoint.as_deref());
if caller
.iter()
.filter(from_here)
.any(|out| covers(&out.key, &fence.key))
{
return;
}
let in_part = caller
.iter()
.filter(from_here)
.any(|out| out.key != fence.key && covers(&fence.key, &out.key));
match longhands(&fence).filter(|_| in_part) {
Some(parts) => {
for part in parts {
under(part, caller, kept);
}
}
None => kept.push(fence),
}
}
fn parts(key: &str) -> &'static [&'static str] {
match key {
"padding" => &["padding-block", "padding-inline"],
"padding-block" => &["padding-block-start", "padding-block-end"],
"padding-inline" => &["padding-inline-start", "padding-inline-end"],
_ => &[],
}
}
fn covers(key: &str, other: &str) -> bool {
key == other || parts(key).iter().any(|part| covers(part, other))
}
fn longhands(out: &Out) -> Option<Vec<Out>> {
let names = parts(&out.key);
let text = out.value.written().ok().flatten()?;
let values: Vec<&str> = text.split_whitespace().collect();
let (first, second) = match values.as_slice() {
[one] => (*one, *one),
[first, second] => (*first, *second),
_ => return None,
};
let [one, two] = names else {
return None;
};
Some(
[(one, first), (two, second)]
.into_iter()
.map(|(name, value)| Out {
key: (*name).to_string(),
breakpoint: out.breakpoint.clone(),
value: Value::string(value),
input: None,
})
.collect(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn out(key: &str, breakpoint: Option<&str>, value: &str) -> Out {
Out {
key: key.into(),
breakpoint: breakpoint.map(Into::into),
value: Value::string(value),
input: None,
}
}
fn written(params: &[expanded::Param]) -> Vec<String> {
params
.iter()
.map(|param| match ¶m.breakpoint {
Some(breakpoint) => format!("{breakpoint}:{}={}", param.key, param.value),
None => format!("{}={}", param.key, param.value),
})
.collect()
}
#[test]
fn the_callers_params_cover_the_fences_from_their_breakpoint_up() {
let fence = || {
vec![
out("class", None, "card"),
out("padding", None, "s"),
out("border", None, "subtle"),
out("padding", Some("md"), "m"),
]
};
let wide = boxed(
fence(),
vec![out("padding", None, "l"), out("class", None, "wide card")],
);
assert_eq!(
written(&wide),
["border=\"subtle\"", "padding=\"l\"", "class=\"card wide\""]
);
let from_md = boxed(fence(), vec![out("padding", Some("md"), "l")]);
assert_eq!(
written(&from_md),
[
"padding=\"s\"",
"border=\"subtle\"",
"md:padding=\"l\"",
"class=\"card\""
]
);
}
#[test]
fn a_shorthand_covered_in_part_is_split() {
let split = boxed(
vec![out("padding", None, "s l")],
vec![out("padding-inline-start", None, "m")],
);
assert_eq!(
written(&split),
[
"padding-block=\"s\"",
"padding-inline-end=\"l\"",
"padding-inline-start=\"m\""
]
);
let covered = boxed(
vec![out("padding-inline", None, "l")],
vec![out("padding", None, "s")],
);
assert_eq!(written(&covered), ["padding=\"s\""]);
let odd = boxed(
vec![out("padding", None, "s m l")],
vec![out("padding-block", None, "m")],
);
assert_eq!(written(&odd), ["padding=\"s m l\"", "padding-block=\"m\""]);
assert!(longhands(&out("border", None, "s")).is_none());
}
#[test]
fn whole_numbers_are_written_without_a_fraction() {
let number = Out {
key: "n".into(),
breakpoint: None,
value: Value::Double(5.0),
input: Some("limit".into()),
};
let param = number.into_param();
assert_eq!(serde_json::to_value(¶m).unwrap()["value"], json!(5));
}
}