use std::collections::{BTreeMap, BTreeSet};
use crate::ast::block_style::BlockStyle;
use crate::ast::token::TokenType;
use crate::ast::value::PropertyValue;
use crate::diagnostics::Diagnostic;
use crate::tokens::{ResolvedToken, ResolvedValue};
#[derive(Debug, Clone, Copy)]
pub(super) enum VisualExpect {
Color,
ColorOrGradient,
Dimension,
FontFamily,
FontWeight,
Shadow,
Filter,
Mask,
}
pub(super) fn check_visual_prop(
node_id: &str,
prop_name: &str,
value: Option<&PropertyValue>,
expect: VisualExpect,
referenced_token_ids: &mut BTreeSet<String>,
resolved_tokens: &BTreeMap<String, ResolvedToken>,
diagnostics: &mut Vec<Diagnostic>,
) {
let Some(pv) = value else {
return;
};
match pv {
PropertyValue::TokenRef(token_id) => {
referenced_token_ids.insert(token_id.clone());
let Some(resolved) = resolved_tokens.get(token_id.as_str()) else {
diagnostics.push(Diagnostic::error(
"token.unknown_reference",
format!(
"node '{}': property '{}' references token '{}' which \
does not exist or failed resolution \
— check the spelling and that the token is declared in the `tokens` block",
node_id, prop_name, token_id
),
None,
Some(node_id.to_owned()),
));
return;
};
if let ResolvedValue::Gradient(g) = &resolved.value {
for (_, color_id) in &g.stops {
referenced_token_ids.insert(color_id.clone());
}
}
if let ResolvedValue::Shadow(s) = &resolved.value {
for layer in &s.layers {
referenced_token_ids.insert(layer.color_token.clone());
}
}
if let ResolvedValue::Filter(f) = &resolved.value {
for op in &f.ops {
if let Some(c) = &op.shadow {
referenced_token_ids.insert(c.clone());
}
if let Some(c) = &op.highlight {
referenced_token_ids.insert(c.clone());
}
}
}
let type_ok = match expect {
VisualExpect::Color => {
matches!(resolved.token_type, TokenType::Color)
}
VisualExpect::ColorOrGradient => {
matches!(resolved.token_type, TokenType::Color | TokenType::Gradient)
}
VisualExpect::Dimension => {
matches!(resolved.token_type, TokenType::Dimension)
}
VisualExpect::FontFamily => {
matches!(resolved.token_type, TokenType::FontFamily)
}
VisualExpect::FontWeight => {
matches!(resolved.token_type, TokenType::FontWeight)
}
VisualExpect::Shadow => {
matches!(resolved.token_type, TokenType::Shadow)
}
VisualExpect::Filter => {
matches!(resolved.token_type, TokenType::Filter)
}
VisualExpect::Mask => {
matches!(resolved.token_type, TokenType::Mask)
}
};
if !type_ok {
diagnostics.push(Diagnostic::error(
"token.incompatible_property",
format!(
"node '{}': property '{}' expects a {} token but \
'{}' is of type '{}'",
node_id,
prop_name,
visual_expect_name(expect),
token_id,
token_type_name(&resolved.token_type),
),
None,
Some(node_id.to_owned()),
));
}
}
PropertyValue::Literal(_) | PropertyValue::Dimension(_) => {
diagnostics.push(Diagnostic::error(
"token.raw_visual_literal",
format!(
"node '{}': visual property '{}' has a raw literal value; \
visual properties must reference design tokens \
— define a token and reference it as `(token)\"token-id\"`",
node_id, prop_name
),
None,
Some(node_id.to_owned()),
));
}
PropertyValue::DataRef(_) => {}
}
}
pub(super) fn check_block_styles(
scope_id: &str,
block_styles: &[BlockStyle],
referenced_token_ids: &mut BTreeSet<String>,
resolved_tokens: &BTreeMap<String, ResolvedToken>,
diagnostics: &mut Vec<Diagnostic>,
) {
for bs in block_styles {
let label = format!("{scope_id}[block role=\"{}\"]", bs.role);
check_visual_prop(
&label,
"font-family",
bs.font_family.as_ref(),
VisualExpect::FontFamily,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
&label,
"font-size",
bs.font_size.as_ref(),
VisualExpect::Dimension,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
&label,
"font-weight",
bs.font_weight.as_ref(),
VisualExpect::FontWeight,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
&label,
"fill",
bs.fill.as_ref(),
VisualExpect::Color,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
}
}
fn visual_expect_name(e: VisualExpect) -> &'static str {
match e {
VisualExpect::Color => "color",
VisualExpect::ColorOrGradient => "color or gradient",
VisualExpect::Dimension => "dimension",
VisualExpect::FontFamily => "fontFamily",
VisualExpect::FontWeight => "fontWeight",
VisualExpect::Shadow => "shadow",
VisualExpect::Filter => "filter",
VisualExpect::Mask => "mask",
}
}
fn token_type_name(t: &TokenType) -> &str {
match t {
TokenType::Color => "color",
TokenType::Dimension => "dimension",
TokenType::Number => "number",
TokenType::FontFamily => "fontFamily",
TokenType::FontWeight => "fontWeight",
TokenType::Gradient => "gradient",
TokenType::Shadow => "shadow",
TokenType::Filter => "filter",
TokenType::Mask => "mask",
TokenType::Unknown(s) => s.as_str(),
}
}