use std::collections::{BTreeMap, BTreeSet};
use omena_value_lattice::{canonicalize_css_value, css_values_canonically_equal};
use crate::{
AbstractCssTypedValueV0, AbstractCssValueV0, AbstractPropertyValueCandidateV0,
AbstractPropertyValueNarrowingV0, AbstractPropertyValueV0, abstract_css_typed_value_from_text,
join_abstract_css_typed_values,
};
pub fn abstract_css_value_from_text(value: &str) -> AbstractCssValueV0 {
let value = value.trim();
if value.is_empty() {
return AbstractCssValueV0::Bottom;
}
match canonical_css_value_text(value) {
Some(value) => AbstractCssValueV0::Exact {
typed: abstract_css_typed_value_from_text(value.as_str()).map(Box::new),
value,
},
None => AbstractCssValueV0::Raw {
value: value.to_string(),
},
}
}
pub fn canonical_css_value_text(value: &str) -> Option<String> {
canonicalize_css_value(value).map(|value| value.serialized)
}
pub fn abstract_css_values_canonically_equal(left: &str, right: &str) -> bool {
css_values_canonically_equal(left, right)
}
pub fn join_abstract_css_values(
left: &AbstractCssValueV0,
right: &AbstractCssValueV0,
) -> AbstractCssValueV0 {
match (left, right) {
(AbstractCssValueV0::Bottom, value) | (value, AbstractCssValueV0::Bottom) => value.clone(),
(AbstractCssValueV0::Top, _) | (_, AbstractCssValueV0::Top) => AbstractCssValueV0::Top,
(
AbstractCssValueV0::Exact {
value: left,
typed: left_typed,
},
AbstractCssValueV0::Exact {
value: right,
typed: right_typed,
},
) if left == right => AbstractCssValueV0::Exact {
value: left.clone(),
typed: join_abstract_css_typed_values(left_typed.as_deref(), right_typed.as_deref())
.map(Box::new),
},
(AbstractCssValueV0::Raw { value: left }, AbstractCssValueV0::Raw { value: right })
if left == right =>
{
AbstractCssValueV0::Raw {
value: left.clone(),
}
}
_ => finite_css_value_set([left, right]),
}
}
pub fn narrow_abstract_property_value_for_pseudo_state(
property_name: &str,
requested_pseudo_state: Option<&str>,
candidates: &[AbstractPropertyValueCandidateV0],
) -> AbstractPropertyValueNarrowingV0 {
narrow_abstract_property_value_for_cascade_branch(
property_name,
requested_pseudo_state,
&[],
None,
None,
false,
candidates,
)
}
pub fn narrow_abstract_property_value_for_cascade_branch(
property_name: &str,
requested_pseudo_state: Option<&str>,
requested_condition_context: &[String],
requested_layer_name: Option<&str>,
requested_layer_order: Option<i32>,
exact_layer: bool,
candidates: &[AbstractPropertyValueCandidateV0],
) -> AbstractPropertyValueNarrowingV0 {
let matched = candidates
.iter()
.filter(|candidate| candidate.property_name == property_name)
.filter(|candidate| {
candidate.pseudo_state.as_deref().is_none()
|| candidate.pseudo_state.as_deref() == requested_pseudo_state
})
.filter(|candidate| candidate.condition_context == requested_condition_context)
.filter(|candidate| {
!exact_layer
|| (candidate.layer_name.as_deref() == requested_layer_name
&& candidate.layer_order == requested_layer_order)
})
.collect::<Vec<_>>();
let value = abstract_property_value_from_matched_candidates(
property_name,
requested_pseudo_state,
&matched,
exact_layer,
);
let (display_value, display_values) =
display_property_values_from_matched_candidates(&value, &matched, exact_layer);
AbstractPropertyValueNarrowingV0 {
schema_version: "0",
product: "omena-abstract-value.property-value-narrowing",
stylesheet_scope: "singleStylesheet",
property_name: property_name.to_string(),
requested_pseudo_state: requested_pseudo_state.map(str::to_string),
requested_condition_context: requested_condition_context.to_vec(),
requested_layer_name: requested_layer_name.map(str::to_string),
requested_layer_order,
requested_layer_scope: if exact_layer {
"exactLayer"
} else {
"anyLayer"
},
candidate_count: candidates.len(),
matched_candidate_count: matched.len(),
display_value,
display_values,
value,
}
}
fn abstract_property_value_from_matched_candidates(
property_name: &str,
requested_pseudo_state: Option<&str>,
matched: &[&AbstractPropertyValueCandidateV0],
exact_layer: bool,
) -> AbstractPropertyValueV0 {
if exact_layer
&& matched.len() > 1
&& matched
.iter()
.all(|candidate| candidate.same_selector_ordering && candidate.source_order.is_some())
{
let winner = matched
.iter()
.max_by_key(|candidate| (candidate.important, candidate.source_order.unwrap_or(0)))
.copied();
if let Some(winner) = winner {
return abstract_property_value_from_single_candidate(
property_name,
requested_pseudo_state,
winner.value.trim(),
);
}
}
let mut values = matched
.iter()
.map(|candidate| canonical_property_value_text(candidate.value.trim()))
.filter(|value| !value.is_empty())
.collect::<BTreeSet<_>>();
if values.is_empty() {
return AbstractPropertyValueV0::Bottom {
property_name: property_name.to_string(),
};
}
if values.len() == 1 {
let value = values.pop_first().unwrap_or_default();
return abstract_property_value_from_single_candidate(
property_name,
requested_pseudo_state,
value.as_str(),
);
}
let pseudo_states = matched
.iter()
.filter_map(|candidate| candidate.pseudo_state.as_deref())
.map(str::to_string)
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
AbstractPropertyValueV0::FiniteSet {
property_name: property_name.to_string(),
values: values.into_iter().collect(),
pseudo_states,
}
}
fn abstract_property_value_from_single_candidate(
property_name: &str,
requested_pseudo_state: Option<&str>,
value: &str,
) -> AbstractPropertyValueV0 {
if let Some(custom_property_name) = referenced_custom_property_name(value) {
return AbstractPropertyValueV0::CustomPropertyReference {
property_name: property_name.to_string(),
custom_property_name,
pseudo_state: requested_pseudo_state.map(str::to_string),
};
}
let value = canonical_property_value_text(value);
AbstractPropertyValueV0::Exact {
property_name: property_name.to_string(),
value,
pseudo_state: requested_pseudo_state.map(str::to_string),
}
}
fn display_property_values_from_matched_candidates(
value: &AbstractPropertyValueV0,
matched: &[&AbstractPropertyValueCandidateV0],
exact_layer: bool,
) -> (Option<String>, Vec<String>) {
match value {
AbstractPropertyValueV0::Bottom { .. } | AbstractPropertyValueV0::Top { .. } => {
(None, Vec::new())
}
AbstractPropertyValueV0::Exact { value, .. } => (
display_value_for_exact_canonical_value(value, matched, exact_layer),
Vec::new(),
),
AbstractPropertyValueV0::CustomPropertyReference {
custom_property_name,
..
} => (
display_value_for_custom_property_reference(custom_property_name, matched),
Vec::new(),
),
AbstractPropertyValueV0::FiniteSet { values, .. } => {
let display_values = display_values_for_canonical_values(values, matched);
(None, display_values)
}
}
}
fn display_value_for_exact_canonical_value(
canonical_value: &str,
matched: &[&AbstractPropertyValueCandidateV0],
exact_layer: bool,
) -> Option<String> {
if exact_layer
&& matched.len() > 1
&& matched
.iter()
.all(|candidate| candidate.same_selector_ordering && candidate.source_order.is_some())
{
return matched
.iter()
.max_by_key(|candidate| (candidate.important, candidate.source_order.unwrap_or(0)))
.and_then(|candidate| display_candidate_value(candidate));
}
matched
.iter()
.find(|candidate| canonical_property_value_text(candidate.value.trim()) == canonical_value)
.and_then(|candidate| display_candidate_value(candidate))
.or_else(|| Some(canonical_value.to_string()))
}
fn display_value_for_custom_property_reference(
custom_property_name: &str,
matched: &[&AbstractPropertyValueCandidateV0],
) -> Option<String> {
matched
.iter()
.find(|candidate| {
referenced_custom_property_name(candidate.value.trim()).as_deref()
== Some(custom_property_name)
})
.and_then(|candidate| display_candidate_value(candidate))
.or_else(|| Some(format!("var({custom_property_name})")))
}
fn display_values_for_canonical_values(
canonical_values: &[String],
matched: &[&AbstractPropertyValueCandidateV0],
) -> Vec<String> {
let mut display_by_canonical_value = BTreeMap::new();
for candidate in matched {
let canonical_value = canonical_property_value_text(candidate.value.trim());
if canonical_values.contains(&canonical_value)
&& let Some(display_value) = display_candidate_value(candidate)
{
display_by_canonical_value
.entry(canonical_value)
.or_insert(display_value);
}
}
canonical_values
.iter()
.map(|canonical_value| {
display_by_canonical_value
.get(canonical_value)
.cloned()
.unwrap_or_else(|| canonical_value.clone())
})
.collect()
}
fn display_candidate_value(candidate: &AbstractPropertyValueCandidateV0) -> Option<String> {
let value = candidate.value.trim();
(!value.is_empty()).then(|| value.to_string())
}
fn canonical_property_value_text(value: &str) -> String {
canonical_css_value_text(value).unwrap_or_else(|| value.trim().to_string())
}
fn finite_css_value_set<const N: usize>(values: [&AbstractCssValueV0; N]) -> AbstractCssValueV0 {
let values = values
.into_iter()
.flat_map(flatten_css_value_set)
.collect::<BTreeSet<_>>();
if values.is_empty() {
AbstractCssValueV0::Bottom
} else if values.len() == 1 {
let value = values.into_iter().next().unwrap_or_default();
AbstractCssValueV0::Exact {
typed: abstract_css_typed_value_from_text(value.as_str()).map(Box::new),
value,
}
} else {
let values = values.into_iter().collect::<Vec<_>>();
AbstractCssValueV0::FiniteSet {
typed: finite_css_typed_value_from_strings(values.as_slice()),
values,
}
}
}
fn flatten_css_value_set(value: &AbstractCssValueV0) -> Vec<String> {
match value {
AbstractCssValueV0::Bottom => Vec::new(),
AbstractCssValueV0::Exact { value, .. } | AbstractCssValueV0::Raw { value } => {
vec![value.clone()]
}
AbstractCssValueV0::FiniteSet { values, .. } => values.clone(),
AbstractCssValueV0::Top => vec!["<top>".to_string()],
}
}
fn finite_css_typed_value_from_strings(values: &[String]) -> Option<Box<AbstractCssTypedValueV0>> {
values
.iter()
.filter_map(|value| canonical_css_value_text(value.as_str()))
.filter_map(|value| abstract_css_typed_value_from_text(value.as_str()))
.reduce(
|left, right| match join_abstract_css_typed_values(Some(&left), Some(&right)) {
Some(joined) => joined,
None => left,
},
)
.map(Box::new)
}
fn referenced_custom_property_name(value: &str) -> Option<String> {
let value = value.trim();
let name_start = value.find("var(")? + "var(".len();
let tail = value.get(name_start..)?.trim_start();
if !tail.starts_with("--") {
return None;
}
let name_end = tail
.find(|character: char| character == ')' || character == ',' || character.is_whitespace())
.unwrap_or(tail.len());
let name = tail.get(..name_end)?.trim();
(!name.is_empty()).then(|| name.to_string())
}