use std::collections::{BTreeMap, BTreeSet};
use crate::ast::node::{Node, TextSpan, parse_anchor, parse_anchor_edge};
use crate::ast::value::{Dimension, PropertyValue, Unit, dim_to_px};
use crate::diagnostics::Diagnostic;
use crate::tokens::ResolvedToken;
use crate::validate::check::visual::{VisualExpect, check_visual_prop};
pub(in crate::validate::check) fn resolve_axis(dim: &Dimension, basis: f64) -> Option<f64> {
if dim.unit == Unit::Pct {
Some(dim.value / 100.0 * basis)
} else {
dim_to_px(dim.value, &dim.unit)
}
}
pub(super) fn is_valid_blend_mode(s: &str) -> bool {
matches!(
s,
"normal"
| "multiply"
| "screen"
| "overlay"
| "darken"
| "lighten"
| "color-dodge"
| "color-burn"
| "hard-light"
| "soft-light"
| "difference"
| "exclusion"
)
}
pub(in crate::validate::check) fn node_bbox(
node: &Node,
page_w: f64,
page_h: f64,
) -> Option<(f64, f64, f64, f64)> {
match node {
Node::Rect(n) => {
let x = resolve_axis(pv_to_dim(n.x.as_ref())?, page_w)?;
let y = resolve_axis(pv_to_dim(n.y.as_ref())?, page_h)?;
let w = resolve_axis(pv_to_dim(n.w.as_ref())?, page_w)?;
let h = resolve_axis(pv_to_dim(n.h.as_ref())?, page_h)?;
Some((x, y, w, h))
}
Node::Ellipse(n) => {
let x = resolve_axis(pv_to_dim(n.x.as_ref())?, page_w)?;
let y = resolve_axis(pv_to_dim(n.y.as_ref())?, page_h)?;
let w = resolve_axis(pv_to_dim(n.w.as_ref())?, page_w)?;
let h = resolve_axis(pv_to_dim(n.h.as_ref())?, page_h)?;
Some((x, y, w, h))
}
Node::Image(n) => {
let x = resolve_axis(pv_to_dim(n.x.as_ref())?, page_w)?;
let y = resolve_axis(pv_to_dim(n.y.as_ref())?, page_h)?;
let w = resolve_axis(pv_to_dim(n.w.as_ref())?, page_w)?;
let h = resolve_axis(pv_to_dim(n.h.as_ref())?, page_h)?;
Some((x, y, w, h))
}
Node::Frame(n) => {
let x = resolve_axis(pv_to_dim(n.x.as_ref())?, page_w)?;
let y = resolve_axis(pv_to_dim(n.y.as_ref())?, page_h)?;
let w = resolve_axis(pv_to_dim(n.w.as_ref())?, page_w)?;
let h = resolve_axis(pv_to_dim(n.h.as_ref())?, page_h)?;
Some((x, y, w, h))
}
Node::Text(n) => {
let x = resolve_axis(pv_to_dim(n.x.as_ref())?, page_w)?;
let y = resolve_axis(pv_to_dim(n.y.as_ref())?, page_h)?;
let w = resolve_axis(pv_to_dim(n.w.as_ref())?, page_w)?;
let h = resolve_axis(pv_to_dim(n.h.as_ref())?, page_h)?;
Some((x, y, w, h))
}
Node::Code(n) => {
let x = resolve_axis(pv_to_dim(n.x.as_ref())?, page_w)?;
let y = resolve_axis(pv_to_dim(n.y.as_ref())?, page_h)?;
let w = resolve_axis(pv_to_dim(n.w.as_ref())?, page_w)?;
let h = resolve_axis(pv_to_dim(n.h.as_ref())?, page_h)?;
Some((x, y, w, h))
}
Node::Line(n) => {
let x1 = resolve_axis(n.x1.as_ref()?, page_w)?;
let y1 = resolve_axis(n.y1.as_ref()?, page_h)?;
let x2 = resolve_axis(n.x2.as_ref()?, page_w)?;
let y2 = resolve_axis(n.y2.as_ref()?, page_h)?;
let bx = x1.min(x2);
let by = y1.min(y2);
let bw = (x2 - x1).abs();
let bh = (y2 - y1).abs();
Some((bx, by, bw, bh))
}
Node::Polygon(n) => points_bbox(&n.points, page_w, page_h),
Node::Polyline(n) => points_bbox(&n.points, page_w, page_h),
Node::Table(n) => {
let x = resolve_axis(pv_to_dim(n.x.as_ref())?, page_w)?;
let y = resolve_axis(pv_to_dim(n.y.as_ref())?, page_h)?;
let w = resolve_axis(pv_to_dim(n.w.as_ref())?, page_w)?;
let h = resolve_axis(pv_to_dim(n.h.as_ref())?, page_h)?;
Some((x, y, w, h))
}
Node::Shape(n) => {
let x = resolve_axis(pv_to_dim(n.x.as_ref())?, page_w)?;
let y = resolve_axis(pv_to_dim(n.y.as_ref())?, page_h)?;
let w = resolve_axis(pv_to_dim(n.w.as_ref())?, page_w)?;
let h = resolve_axis(pv_to_dim(n.h.as_ref())?, page_h)?;
Some((x, y, w, h))
}
Node::Pattern(n) => {
let x = resolve_axis(pv_to_dim(n.x.as_ref())?, page_w)?;
let y = resolve_axis(pv_to_dim(n.y.as_ref())?, page_h)?;
let w = resolve_axis(pv_to_dim(n.w.as_ref())?, page_w)?;
let h = resolve_axis(pv_to_dim(n.h.as_ref())?, page_h)?;
Some((x, y, w, h))
}
Node::Chart(n) => {
let x = resolve_axis(pv_to_dim(n.x.as_ref())?, page_w)?;
let y = resolve_axis(pv_to_dim(n.y.as_ref())?, page_h)?;
let w = resolve_axis(pv_to_dim(n.w.as_ref())?, page_w)?;
let h = resolve_axis(pv_to_dim(n.h.as_ref())?, page_h)?;
Some((x, y, w, h))
}
Node::Group(_)
| Node::Instance(_)
| Node::Field(_)
| Node::Toc(_)
| Node::Footnote(_)
| Node::Connector(_)
| Node::Unknown(_) => None,
}
}
fn points_bbox(
points: &[crate::ast::node::Point],
page_w: f64,
page_h: f64,
) -> Option<(f64, f64, f64, f64)> {
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
let mut any = false;
for pt in points {
if let (Some(px_val), Some(py_val)) = (
pt.x.as_ref().and_then(|d| resolve_axis(d, page_w)),
pt.y.as_ref().and_then(|d| resolve_axis(d, page_h)),
) {
min_x = min_x.min(px_val);
min_y = min_y.min(py_val);
max_x = max_x.max(px_val);
max_y = max_y.max(py_val);
any = true;
}
}
if any {
Some((min_x, min_y, max_x - min_x, max_y - min_y))
} else {
None
}
}
pub(in crate::validate::check) fn node_rotate_deg(node: &Node) -> Option<f64> {
let dim = match node {
Node::Rect(n) => n.rotate.as_ref(),
Node::Ellipse(n) => n.rotate.as_ref(),
Node::Frame(n) => n.rotate.as_ref(),
Node::Image(n) => n.rotate.as_ref(),
Node::Text(n) => n.rotate.as_ref(),
Node::Code(n) => n.rotate.as_ref(),
Node::Group(n) => n.rotate.as_ref(),
Node::Polygon(n) => n.rotate.as_ref(),
Node::Polyline(n) => n.rotate.as_ref(),
Node::Table(n) => n.rotate.as_ref(),
Node::Shape(n) => n.rotate.as_ref(),
Node::Connector(n) => n.rotate.as_ref(),
Node::Pattern(n) => n.rotate.as_ref(),
Node::Chart(n) => n.rotate.as_ref(),
Node::Line(_)
| Node::Instance(_)
| Node::Field(_)
| Node::Toc(_)
| Node::Footnote(_)
| Node::Unknown(_) => None,
}?;
(dim.unit == Unit::Deg).then_some(dim.value)
}
pub(in crate::validate::check) fn node_role(node: &Node) -> Option<&str> {
match node {
Node::Rect(n) => n.role.as_deref(),
Node::Ellipse(n) => n.role.as_deref(),
Node::Line(n) => n.role.as_deref(),
Node::Text(n) => n.role.as_deref(),
Node::Code(n) => n.role.as_deref(),
Node::Frame(n) => n.role.as_deref(),
Node::Group(n) => n.role.as_deref(),
Node::Image(n) => n.role.as_deref(),
Node::Polygon(n) => n.role.as_deref(),
Node::Polyline(n) => n.role.as_deref(),
Node::Instance(n) => n.role.as_deref(),
Node::Field(n) => n.role.as_deref(),
Node::Toc(n) => n.role.as_deref(),
Node::Footnote(n) => n.role.as_deref(),
Node::Table(n) => n.role.as_deref(),
Node::Shape(n) => n.role.as_deref(),
Node::Connector(n) => n.role.as_deref(),
Node::Pattern(n) => n.role.as_deref(),
Node::Chart(n) => n.role.as_deref(),
Node::Unknown(_) => None,
}
}
pub(in crate::validate::check) fn node_id_and_span(
node: &Node,
) -> (&str, Option<crate::ast::Span>) {
match node {
Node::Rect(n) => (&n.id, n.source_span),
Node::Ellipse(n) => (&n.id, n.source_span),
Node::Line(n) => (&n.id, n.source_span),
Node::Text(n) => (&n.id, n.source_span),
Node::Code(n) => (&n.id, n.source_span),
Node::Frame(n) => (&n.id, n.source_span),
Node::Group(n) => (&n.id, n.source_span),
Node::Image(n) => (&n.id, n.source_span),
Node::Polygon(n) => (&n.id, n.source_span),
Node::Polyline(n) => (&n.id, n.source_span),
Node::Instance(n) => (&n.id, n.source_span),
Node::Field(n) => (&n.id, n.source_span),
Node::Toc(n) => (&n.id, n.source_span),
Node::Footnote(n) => (&n.id, n.source_span),
Node::Table(n) => (&n.id, n.source_span),
Node::Shape(n) => (&n.id, n.source_span),
Node::Connector(n) => (&n.id, n.source_span),
Node::Pattern(n) => (&n.id, n.source_span),
Node::Chart(n) => (&n.id, n.source_span),
Node::Unknown(n) => (n.id.as_deref().unwrap_or(&n.kind), n.source_span),
}
}
#[derive(Clone, Copy)]
pub(in crate::validate::check) struct AnchorParentCtx {
pub(in crate::validate::check) in_container: bool,
pub(in crate::validate::check) parent_box_known: bool,
}
#[derive(Clone, Copy)]
pub(in crate::validate::check) struct AnchorProps<'a> {
pub(in crate::validate::check) anchor: Option<&'a str>,
pub(in crate::validate::check) anchor_zone: Option<&'a str>,
pub(in crate::validate::check) anchor_sibling: Option<&'a str>,
pub(in crate::validate::check) anchor_parent: bool,
pub(in crate::validate::check) anchor_edge: Option<&'a str>,
pub(in crate::validate::check) anchor_gap: Option<&'a Dimension>,
}
pub(super) fn check_anchor(
node_id: &str,
props: AnchorProps,
parent_ctx: AnchorParentCtx,
zone_ids: &BTreeSet<&str>,
span: Option<crate::ast::Span>,
diagnostics: &mut Vec<Diagnostic>,
) -> bool {
let AnchorProps {
anchor,
anchor_zone,
anchor_sibling,
anchor_parent,
anchor_edge,
anchor_gap,
} = props;
if anchor_zone.is_some() && anchor.is_none() {
diagnostics.push(Diagnostic::warning(
"anchor.zone_without_anchor",
format!(
"node '{}': anchor-zone is set but anchor is absent; \
anchor-zone has no effect without an anchor value",
node_id
),
span,
Some(node_id.to_owned()),
));
}
if anchor_sibling.is_some() && anchor.is_none() && anchor_edge.is_none() {
diagnostics.push(Diagnostic::warning(
"anchor.sibling_without_anchor",
format!(
"node '{}': anchor-sibling is set but anchor is absent; \
anchor-sibling has no effect without an anchor value \
(unless anchor-edge is set)",
node_id
),
span,
Some(node_id.to_owned()),
));
}
if anchor_parent && anchor.is_none() {
diagnostics.push(Diagnostic::warning(
"anchor.parent_without_anchor",
format!(
"node '{}': anchor-parent is set but anchor is absent; \
anchor-parent has no effect without an anchor value",
node_id
),
span,
Some(node_id.to_owned()),
));
}
if anchor_parent
&& anchor_zone.is_none()
&& (!parent_ctx.in_container || !parent_ctx.parent_box_known)
{
diagnostics.push(Diagnostic::error(
"anchor.unresolvable_parent",
format!(
"node '{}': anchor-parent is set but the node is not inside a \
frame/group container with a usable box",
node_id
),
span,
Some(node_id.to_owned()),
));
}
if anchor_edge.is_some() && anchor_sibling.is_none() {
diagnostics.push(Diagnostic::warning(
"anchor.edge_without_sibling",
format!(
"node '{}': anchor-edge is set but anchor-sibling is absent; \
it has no effect without an anchor-sibling target",
node_id
),
span,
Some(node_id.to_owned()),
));
}
if let Some(edge) = anchor_edge
&& parse_anchor_edge(edge).is_none()
{
diagnostics.push(Diagnostic::error(
"anchor.unknown_edge",
format!(
"node '{}': anchor-edge value '{}' is not recognized; \
valid values are above, below, before, after",
node_id, edge
),
span,
Some(node_id.to_owned()),
));
}
if let Some(gap) = anchor_gap
&& dim_to_px(gap.value, &gap.unit).is_none()
{
diagnostics.push(Diagnostic::warning(
"anchor.gap_invalid_unit",
format!(
"node '{}': anchor-gap unit '{}' cannot be resolved to px; \
gap must resolve to px",
node_id,
gap.unit.as_annotation()
),
span,
Some(node_id.to_owned()),
));
}
let anchor_active = match anchor {
None => false,
Some(s) => {
if parse_anchor(s).is_some() {
true
} else {
diagnostics.push(Diagnostic::error(
"anchor.unknown_value",
format!(
"node '{}': anchor value '{}' is not recognized; \
valid values are top-left, top-center, top-right, \
center-left, center, center-right, \
bottom-left, bottom-center, bottom-right",
node_id, s
),
span,
Some(node_id.to_owned()),
));
false
}
}
};
if let Some(zone_id) = anchor_zone
&& !zone_ids.contains(zone_id)
{
diagnostics.push(Diagnostic::error(
"anchor.unresolved_zone",
format!(
"node '{}': anchor-zone '{}' does not name a safe-zone on this page",
node_id, zone_id
),
span,
Some(node_id.to_owned()),
));
}
anchor_active || (anchor_sibling.is_some() && anchor_edge.is_some())
}
fn node_sibling_fields(node: &Node) -> Option<(&str, Option<&str>, Option<crate::ast::Span>)> {
let f = match node {
Node::Rect(n) => (n.id.as_str(), n.anchor_sibling.as_deref(), n.source_span),
Node::Ellipse(n) => (n.id.as_str(), n.anchor_sibling.as_deref(), n.source_span),
Node::Text(n) => (n.id.as_str(), n.anchor_sibling.as_deref(), n.source_span),
Node::Code(n) => (n.id.as_str(), n.anchor_sibling.as_deref(), n.source_span),
Node::Image(n) => (n.id.as_str(), n.anchor_sibling.as_deref(), n.source_span),
Node::Frame(n) => (n.id.as_str(), n.anchor_sibling.as_deref(), n.source_span),
Node::Group(n) => (n.id.as_str(), n.anchor_sibling.as_deref(), n.source_span),
Node::Shape(n) => (n.id.as_str(), n.anchor_sibling.as_deref(), n.source_span),
Node::Table(n) => (n.id.as_str(), n.anchor_sibling.as_deref(), n.source_span),
Node::Field(n) => (n.id.as_str(), n.anchor_sibling.as_deref(), n.source_span),
Node::Toc(n) => (n.id.as_str(), n.anchor_sibling.as_deref(), n.source_span),
Node::Pattern(n) => (n.id.as_str(), n.anchor_sibling.as_deref(), n.source_span),
Node::Chart(n) => (n.id.as_str(), n.anchor_sibling.as_deref(), n.source_span),
Node::Line(_)
| Node::Connector(_)
| Node::Polygon(_)
| Node::Polyline(_)
| Node::Footnote(_)
| Node::Instance(_)
| Node::Unknown(_) => return None,
};
Some(f)
}
pub(in crate::validate::check) fn check_sibling_anchors(
children: &[Node],
diagnostics: &mut Vec<Diagnostic>,
) {
let mut in_scope: BTreeSet<&str> = BTreeSet::new();
for child in children {
if let Some((id, _, _)) = node_sibling_fields(child) {
in_scope.insert(id);
}
}
let mut edges: BTreeMap<&str, &str> = BTreeMap::new();
for child in children {
let Some((id, anchor_sibling, span)) = node_sibling_fields(child) else {
continue;
};
if let Some(target) = anchor_sibling {
if in_scope.contains(target) {
edges.insert(id, target);
} else {
diagnostics.push(Diagnostic::error(
"anchor.unresolved_sibling",
format!(
"node '{}': anchor-sibling '{}' does not name a sibling \
node in the same container",
id, target
),
span,
Some(id.to_owned()),
));
}
}
}
let mut reported: BTreeSet<&str> = BTreeSet::new();
let span_of: BTreeMap<&str, Option<crate::ast::Span>> = children
.iter()
.filter_map(|c| node_sibling_fields(c).map(|(id, _, span)| (id, span)))
.collect();
for &start in edges.keys() {
if reported.contains(start) {
continue;
}
let mut visited: BTreeSet<&str> = BTreeSet::new();
let mut current = start;
visited.insert(current);
while let Some(&next) = edges.get(current) {
if visited.contains(next) {
if reported.insert(start) {
diagnostics.push(Diagnostic::error(
"anchor.cycle",
format!(
"node '{}': anchor-sibling chain reaches a cycle \
(at '{}'); its position cannot be resolved",
start, next
),
span_of.get(start).copied().flatten(),
Some(start.to_owned()),
));
}
break;
}
visited.insert(next);
current = next;
}
}
}
pub(super) struct TokenEnv<'a> {
pub(super) referenced: &'a mut BTreeSet<String>,
pub(super) resolved: &'a BTreeMap<String, ResolvedToken>,
}
pub(super) fn check_optional_dim(
node_id: &str,
prop: &str,
value: Option<&PropertyValue>,
required: bool,
span: Option<crate::ast::Span>,
tokens: &mut TokenEnv<'_>,
diagnostics: &mut Vec<Diagnostic>,
) {
match value {
None if required => {
diagnostics.push(Diagnostic::error(
"node.missing_geometry",
format!(
"node '{}': required geometry property '{}' is missing",
node_id, prop
),
span,
Some(node_id.to_owned()),
));
}
None => {
}
Some(PropertyValue::Dimension(d)) if matches!(d.unit, Unit::Unknown(_)) => {
diagnostics.push(Diagnostic::error(
"node.invalid_geometry",
format!(
"node '{}': geometry property '{}' has an unrecognized unit; \
allowed units are px, pt, pct, deg",
node_id, prop
),
span,
Some(node_id.to_owned()),
));
}
Some(PropertyValue::Dimension(_)) => {
}
Some(pv @ PropertyValue::TokenRef(_)) => {
check_visual_prop(
node_id,
prop,
Some(pv),
VisualExpect::Dimension,
&mut *tokens.referenced,
tokens.resolved,
diagnostics,
);
}
Some(PropertyValue::Literal(_) | PropertyValue::DataRef(_)) => {
diagnostics.push(Diagnostic::error(
"node.invalid_geometry",
format!(
"node '{}': geometry property '{}' must be a dimension literal \
(e.g. (px)100) or a dimension token ref; a bare string or \
data ref is not allowed",
node_id, prop
),
span,
Some(node_id.to_owned()),
));
}
}
}
pub(in crate::validate::check) fn pv_to_dim(pv: Option<&PropertyValue>) -> Option<&Dimension> {
match pv? {
PropertyValue::Dimension(d) => Some(d),
PropertyValue::TokenRef(_) | PropertyValue::Literal(_) | PropertyValue::DataRef(_) => None,
}
}
pub(super) fn check_dimension_geom(
node_id: &str,
prop: &str,
dim: Option<&Dimension>,
required: bool,
span: Option<crate::ast::Span>,
diagnostics: &mut Vec<Diagnostic>,
) {
match dim {
None if required => {
diagnostics.push(Diagnostic::error(
"node.missing_geometry",
format!(
"node '{}': required geometry property '{}' is missing",
node_id, prop
),
span,
Some(node_id.to_owned()),
));
}
None => {}
Some(d) if matches!(d.unit, Unit::Unknown(_)) => {
diagnostics.push(Diagnostic::error(
"node.invalid_geometry",
format!(
"node '{}': geometry property '{}' has an unrecognized unit; \
allowed units are px, pt, pct, deg",
node_id, prop
),
span,
Some(node_id.to_owned()),
));
}
Some(_) => {}
}
}
pub(super) fn check_spans(
node_id: &str,
spans: &[TextSpan],
referenced_token_ids: &mut BTreeSet<String>,
resolved_tokens: &BTreeMap<String, ResolvedToken>,
diagnostics: &mut Vec<Diagnostic>,
) {
for span in spans {
check_visual_prop(
node_id,
"fill",
span.fill.as_ref(),
VisualExpect::Color,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
node_id,
"font-weight",
span.font_weight.as_ref(),
VisualExpect::FontWeight,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
node_id,
"highlight",
span.highlight.as_ref(),
VisualExpect::Color,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
}
}
pub(super) fn check_style_ref(
node_id: &str,
style_opt: Option<&str>,
declared_style_ids: &BTreeSet<String>,
span: Option<crate::ast::Span>,
diagnostics: &mut Vec<Diagnostic>,
) {
if let Some(sid) = style_opt
&& !declared_style_ids.contains(sid)
{
diagnostics.push(Diagnostic::error(
"style.unknown_reference",
format!(
"node '{}': references style '{}' which is not declared in the styles block",
node_id, sid
),
span,
Some(node_id.to_owned()),
));
}
}
#[derive(Clone, Copy)]
pub(super) struct VisualProps<'a> {
pub(super) fill: Option<&'a PropertyValue>,
pub(super) stroke: Option<&'a PropertyValue>,
pub(super) stroke_width: Option<&'a PropertyValue>,
pub(super) stroke_dash: Option<&'a PropertyValue>,
pub(super) stroke_gap: Option<&'a PropertyValue>,
pub(super) stroke_linecap: Option<&'a str>,
pub(super) border_top: Option<&'a PropertyValue>,
pub(super) border_bottom: Option<&'a PropertyValue>,
pub(super) border_left: Option<&'a PropertyValue>,
pub(super) border_right: Option<&'a PropertyValue>,
pub(super) stroke_outer: Option<&'a PropertyValue>,
pub(super) border_width: Option<&'a PropertyValue>,
pub(super) stroke_outer_width: Option<&'a PropertyValue>,
pub(super) blend_mode: Option<&'a str>,
pub(super) radius: Option<&'a PropertyValue>,
pub(super) radius_tl: Option<&'a PropertyValue>,
pub(super) radius_tr: Option<&'a PropertyValue>,
pub(super) radius_br: Option<&'a PropertyValue>,
pub(super) radius_bl: Option<&'a PropertyValue>,
pub(super) shadow: Option<&'a PropertyValue>,
pub(super) filter: Option<&'a PropertyValue>,
pub(super) mask: Option<&'a PropertyValue>,
pub(super) blur: Option<&'a Dimension>,
}
pub(super) fn check_visual_props(
kind: &str,
id: &str,
source_span: Option<crate::ast::Span>,
props: VisualProps<'_>,
referenced_token_ids: &mut BTreeSet<String>,
resolved_tokens: &BTreeMap<String, ResolvedToken>,
diagnostics: &mut Vec<Diagnostic>,
) {
check_visual_prop(
id,
"fill",
props.fill,
VisualExpect::ColorOrGradient,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
id,
"stroke",
props.stroke,
VisualExpect::Color,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
id,
"stroke-width",
props.stroke_width,
VisualExpect::Dimension,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
id,
"stroke-dash",
props.stroke_dash,
VisualExpect::Dimension,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
if let Some(PropertyValue::Dimension(d)) = props.stroke_dash
&& d.value < 0.0
{
diagnostics.push(Diagnostic::error(
"node.invalid_geometry",
format!("{kind} '{id}': stroke-dash must be >= 0"),
source_span,
Some(id.to_owned()),
));
}
check_visual_prop(
id,
"stroke-gap",
props.stroke_gap,
VisualExpect::Dimension,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
if let Some(PropertyValue::Dimension(d)) = props.stroke_gap
&& d.value < 0.0
{
diagnostics.push(Diagnostic::error(
"node.invalid_geometry",
format!("{kind} '{id}': stroke-gap must be >= 0"),
source_span,
Some(id.to_owned()),
));
}
if let Some(lc) = props.stroke_linecap
&& !matches!(lc, "butt" | "round" | "square")
{
diagnostics.push(Diagnostic::warning(
"node.unknown_property",
format!("{kind} '{id}': stroke-linecap '{lc}' is not one of butt/round/square"),
source_span,
Some(id.to_owned()),
));
}
for (prop_name, prop_val) in [
("border-top", props.border_top),
("border-bottom", props.border_bottom),
("border-left", props.border_left),
("border-right", props.border_right),
("stroke-outer", props.stroke_outer),
] {
check_visual_prop(
id,
prop_name,
prop_val,
VisualExpect::Color,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
}
for (prop_name, prop_val) in [
("border-width", props.border_width),
("stroke-outer-width", props.stroke_outer_width),
] {
check_visual_prop(
id,
prop_name,
prop_val,
VisualExpect::Dimension,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
}
if let Some(bm) = props.blend_mode
&& !is_valid_blend_mode(bm)
{
diagnostics.push(Diagnostic::warning(
"node.unknown_property",
format!("{kind} '{id}': blend-mode '{bm}' is not a recognized value"),
source_span,
Some(id.to_owned()),
));
}
check_visual_prop(
id,
"radius",
props.radius,
VisualExpect::Dimension,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
for (prop_name, prop_val) in [
("radius-tl", props.radius_tl),
("radius-tr", props.radius_tr),
("radius-br", props.radius_br),
("radius-bl", props.radius_bl),
] {
check_visual_prop(
id,
prop_name,
prop_val,
VisualExpect::Dimension,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
if let Some(PropertyValue::Dimension(d)) = prop_val
&& d.value < 0.0
{
diagnostics.push(Diagnostic::error(
"node.invalid_geometry",
format!("{kind} '{id}': {prop_name} must be >= 0"),
source_span,
Some(id.to_owned()),
));
}
}
check_visual_prop(
id,
"shadow",
props.shadow,
VisualExpect::Shadow,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
id,
"filter",
props.filter,
VisualExpect::Filter,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
id,
"mask",
props.mask,
VisualExpect::Mask,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
if let Some(d) = props.blur
&& d.value < 0.0
{
diagnostics.push(Diagnostic::error(
"node.invalid_geometry",
format!("{kind} '{id}': blur must be >= 0"),
source_span,
Some(id.to_owned()),
));
}
}