use std::collections::BTreeMap;
use kdl::{KdlNode, KdlValue};
use crate::ast::{
style::{Style, StyleBlock, UnknownStyleProp, canonicalize_style_key},
token::{
FilterKind, FilterLiteral, FilterOp, GradientKind, GradientLiteral, GradientStopRef,
MaskLiteral, MaskShape, ShadowLayerRef, ShadowLiteral, Token, TokenBlock, TokenLiteral,
TokenType, TokenValue,
},
value::{Dimension, PropertyValue, Unit},
};
use crate::error::{ParseError, ParseErrorCode};
use super::helpers::{
entry_annotation, entry_to_property_value, kdl_value_to_literal_string, node_span,
optional_bool_prop, optional_dimension_prop, optional_f64_prop, optional_i64_prop,
optional_token_ref_prop, required_string_prop,
};
pub(super) fn transform_tokens(node: &KdlNode) -> Result<TokenBlock, ParseError> {
let format = required_string_prop(node, "format")?.to_owned();
let mut token_list: Vec<Token> = Vec::new();
if let Some(children) = node.children() {
for child in children.nodes() {
if child.name().value() == "token" {
token_list.push(transform_token(child)?);
}
}
}
Ok(TokenBlock {
format,
tokens: token_list,
})
}
fn transform_token(node: &KdlNode) -> Result<Token, ParseError> {
let id = required_string_prop(node, "id")?.to_owned();
let type_str = required_string_prop(node, "type")?;
let token_type = TokenType::from_type_name(type_str);
if token_type == TokenType::Gradient {
let token_value = transform_gradient(node);
let source_span = node_span(node);
return Ok(Token {
id,
token_type,
value: token_value,
source_span,
});
}
if token_type == TokenType::Shadow {
let token_value = transform_shadow(node);
let source_span = node_span(node);
return Ok(Token {
id,
token_type,
value: token_value,
source_span,
});
}
if token_type == TokenType::Filter {
let token_value = transform_filter(node);
let source_span = node_span(node);
return Ok(Token {
id,
token_type,
value: token_value,
source_span,
});
}
if token_type == TokenType::Mask {
let token_value = transform_mask(node);
let source_span = node_span(node);
return Ok(Token {
id,
token_type,
value: token_value,
source_span,
});
}
let value_entry = node.entry("value").ok_or_else(|| {
ParseError::spanless(
ParseErrorCode::InvalidPropertyValue,
format!("token `{id}` is missing required property `value`"),
)
})?;
let token_value = match entry_annotation(value_entry) {
Some("token") => match value_entry.value() {
KdlValue::String(s) => TokenValue::Reference {
token_id: s.clone(),
},
other => {
return Err(ParseError::spanless(
ParseErrorCode::InvalidPropertyValue,
format!("token `{id}` has (token) annotation but non-string value: {other:?}"),
));
}
},
Some(unit_str) => {
let unit = Unit::from_annotation(unit_str);
let numeric = match value_entry.value() {
KdlValue::Integer(n) => *n as f64,
KdlValue::Float(f) => *f,
other => {
return Err(ParseError::spanless(
ParseErrorCode::InvalidPropertyValue,
format!(
"token `{id}` has unit annotation but non-numeric value: {other:?}"
),
));
}
};
TokenValue::Literal(TokenLiteral::Dimension(Dimension {
value: numeric,
unit,
}))
}
None => {
let literal = match value_entry.value() {
KdlValue::String(s) => TokenLiteral::String(s.clone()),
KdlValue::Integer(n) => TokenLiteral::Number(*n as f64),
KdlValue::Float(f) => TokenLiteral::Number(*f),
other => {
return Err(ParseError::spanless(
ParseErrorCode::InvalidPropertyValue,
format!("token `{id}` has unsupported value type: {other:?}"),
));
}
};
TokenValue::Literal(literal)
}
};
let source_span = node_span(node);
Ok(Token {
id,
token_type,
value: token_value,
source_span,
})
}
const DEFAULT_GRADIENT_ANGLE_DEG: f64 = 90.0;
fn transform_gradient(node: &KdlNode) -> TokenValue {
let kind = if optional_bool_prop(node, "radial").unwrap_or(false) {
GradientKind::Radial
} else {
GradientKind::Linear
};
let angle_deg =
optional_dimension_prop(node, "angle").map_or(DEFAULT_GRADIENT_ANGLE_DEG, |d| d.value);
let center_x = optional_f64_prop(node, "center-x");
let center_y = optional_f64_prop(node, "center-y");
let radius = optional_f64_prop(node, "radius");
let mut stops: Vec<GradientStopRef> = Vec::new();
if let Some(children) = node.children() {
for child in children.nodes() {
if child.name().value() != "stop" {
continue;
}
let Some(color_token) = optional_token_ref_prop(child, "color") else {
continue;
};
let offset = optional_f64_prop(child, "offset").unwrap_or(0.0);
stops.push(GradientStopRef {
offset,
color_token,
});
}
}
TokenValue::Literal(TokenLiteral::Gradient(GradientLiteral {
kind,
angle_deg,
center_x,
center_y,
radius,
stops,
}))
}
fn transform_shadow(node: &KdlNode) -> TokenValue {
let mut layers: Vec<ShadowLayerRef> = Vec::new();
if let Some(children) = node.children() {
for child in children.nodes() {
if child.name().value() != "layer" {
continue;
}
let Some(color_token) = optional_token_ref_prop(child, "color") else {
continue;
};
let dx = optional_dimension_prop(child, "dx")
.map(|d| d.value)
.unwrap_or(0.0);
let dy = optional_dimension_prop(child, "dy")
.map(|d| d.value)
.unwrap_or(0.0);
let blur = optional_dimension_prop(child, "blur")
.map(|d| d.value)
.unwrap_or(0.0);
layers.push(ShadowLayerRef {
dx,
dy,
blur,
color_token,
});
}
}
TokenValue::Literal(TokenLiteral::Shadow(ShadowLiteral { layers }))
}
fn transform_filter(node: &KdlNode) -> TokenValue {
let mut ops: Vec<FilterOp> = Vec::new();
if let Some(children) = node.children() {
for child in children.nodes() {
let Some(kind) = FilterKind::from_op_name(child.name().value()) else {
continue;
};
let amount = optional_f64_prop(child, "amount");
let shadow = optional_token_ref_prop(child, "shadow");
let highlight = optional_token_ref_prop(child, "highlight");
let seed = optional_i64_prop(child, "seed");
let scale = optional_f64_prop(child, "scale");
ops.push(FilterOp {
kind,
amount,
shadow,
highlight,
seed,
scale,
});
}
}
TokenValue::Literal(TokenLiteral::Filter(FilterLiteral { ops }))
}
fn transform_mask(node: &KdlNode) -> TokenValue {
let mut shape = MaskShape::Rect;
let mut radius: Option<f64> = None;
let mut feather = 0.0;
let mut invert = false;
if let Some(children) = node.children() {
for child in children.nodes() {
let Some(kind) = MaskShape::from_shape_name(child.name().value()) else {
continue;
};
shape = kind;
radius = optional_f64_prop(child, "radius");
feather = optional_f64_prop(child, "feather").unwrap_or(0.0);
invert = optional_bool_prop(child, "invert").unwrap_or(false);
break;
}
}
TokenValue::Literal(TokenLiteral::Mask(MaskLiteral {
shape,
radius,
feather,
invert,
}))
}
pub(super) fn transform_styles(node: &KdlNode) -> Result<StyleBlock, ParseError> {
let source_span = node_span(node);
let mut style_list: Vec<Style> = Vec::new();
if let Some(children) = node.children() {
for child in children.nodes() {
if child.name().value() == "style" {
let id = required_string_prop(child, "id")?.to_owned();
let style_source_span = node_span(child);
let mut properties: BTreeMap<String, PropertyValue> = BTreeMap::new();
let mut unknown_props: BTreeMap<String, UnknownStyleProp> = BTreeMap::new();
if let Some(prop_nodes) = child.children() {
for prop_node in prop_nodes.nodes() {
let prop_name = prop_node.name().value();
if let Some(canonical) = canonicalize_style_key(prop_name) {
let first_positional =
prop_node.entries().iter().find(|e| e.name().is_none());
if let Some(entry) = first_positional
&& let Ok(pv) = entry_to_property_value(entry)
{
properties.insert(canonical.to_owned(), pv);
}
} else {
let raw = prop_node
.entries()
.iter()
.find(|e| e.name().is_none())
.map(|e| kdl_value_to_literal_string(e.value()))
.unwrap_or_default();
unknown_props.insert(prop_name.to_owned(), UnknownStyleProp { raw });
}
}
}
style_list.push(Style {
id,
properties,
unknown_props,
source_span: style_source_span,
});
}
}
}
Ok(StyleBlock {
styles: style_list,
source_span,
})
}