use std::collections::BTreeSet;
use crate::ast::node::{ConnectorNode, ShapeNode, UnknownNode};
use crate::diagnostics::Diagnostic;
use super::shared::{
AnchorParentCtx, AnchorProps, TokenEnv, check_anchor, check_optional_dim, check_spans,
check_style_ref,
};
use super::suggest::check_unknown_props;
use crate::validate::check::nodes::WalkCtx;
use crate::validate::check::register_id;
use crate::validate::check::visual::{VisualExpect, check_visual_prop};
pub(in crate::validate::check) fn check_shape(
s: &ShapeNode,
ctx: WalkCtx,
seen_ids: &mut BTreeSet<String>,
referenced_token_ids: &mut BTreeSet<String>,
geom_required: bool,
parent_ctx: AnchorParentCtx,
diagnostics: &mut Vec<Diagnostic>,
) {
let WalkCtx {
resolved_tokens,
declared_style_ids,
zone_ids,
..
} = ctx;
register_id(&s.id, seen_ids, diagnostics);
check_style_ref(
&s.id,
s.style.as_deref(),
declared_style_ids,
s.source_span,
diagnostics,
);
check_style_ref(
&s.id,
s.text_style.as_deref(),
declared_style_ids,
s.source_span,
diagnostics,
);
let anchor_active = check_anchor(
&s.id,
AnchorProps {
anchor: s.anchor.as_deref(),
anchor_zone: s.anchor_zone.as_deref(),
anchor_sibling: s.anchor_sibling.as_deref(),
anchor_parent: s.anchor_parent == Some(true),
anchor_edge: s.anchor_edge.as_deref(),
anchor_gap: s.anchor_gap.as_ref(),
},
parent_ctx,
zone_ids,
s.source_span,
diagnostics,
);
let xy_required = geom_required && !anchor_active;
{
let mut tokens = TokenEnv {
referenced: referenced_token_ids,
resolved: resolved_tokens,
};
check_optional_dim(
&s.id,
"x",
s.x.as_ref(),
xy_required,
s.source_span,
&mut tokens,
diagnostics,
);
check_optional_dim(
&s.id,
"y",
s.y.as_ref(),
xy_required,
s.source_span,
&mut tokens,
diagnostics,
);
check_optional_dim(
&s.id,
"w",
s.w.as_ref(),
geom_required,
s.source_span,
&mut tokens,
diagnostics,
);
check_optional_dim(
&s.id,
"h",
s.h.as_ref(),
geom_required,
s.source_span,
&mut tokens,
diagnostics,
);
}
check_visual_prop(
&s.id,
"fill",
s.fill.as_ref(),
VisualExpect::Color,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
&s.id,
"stroke",
s.stroke.as_ref(),
VisualExpect::Color,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
&s.id,
"stroke-width",
s.stroke_width.as_ref(),
VisualExpect::Dimension,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
&s.id,
"radius",
s.radius.as_ref(),
VisualExpect::Dimension,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
&s.id,
"padding",
s.padding.as_ref(),
VisualExpect::Dimension,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
if let Some(k) = s.kind.as_deref()
&& !matches!(k, "process" | "decision" | "terminator" | "ellipse")
{
diagnostics.push(Diagnostic::warning(
"shape.unknown_kind",
format!(
"shape '{}': kind '{k}' is not one of \
process/decision/terminator/ellipse",
s.id
),
s.source_span,
Some(s.id.clone()),
));
}
if let Some(sa) = s.stroke_alignment.as_deref()
&& !matches!(sa, "inside" | "center" | "outside")
{
diagnostics.push(Diagnostic::warning(
"shape.invalid_stroke_alignment",
format!(
"shape '{}': stroke-alignment '{sa}' is not one of \
inside/center/outside",
s.id
),
s.source_span,
Some(s.id.clone()),
));
}
if let Some(ha) = s.h_align.as_deref()
&& !matches!(ha, "start" | "center" | "end")
{
diagnostics.push(Diagnostic::warning(
"shape.invalid_h_align",
format!(
"shape '{}': h-align '{ha}' is not one of start/center/end",
s.id
),
s.source_span,
Some(s.id.clone()),
));
}
if let Some(va) = s.v_align.as_deref()
&& !matches!(va, "top" | "middle" | "bottom")
{
diagnostics.push(Diagnostic::warning(
"shape.invalid_v_align",
format!(
"shape '{}': v-align '{va}' is not one of top/middle/bottom",
s.id
),
s.source_span,
Some(s.id.clone()),
));
}
check_spans(
&s.id,
&s.spans,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_unknown_props("shape", &s.id, &s.unknown_props, s.source_span, diagnostics);
}
pub(in crate::validate::check) fn check_connector(
c: &ConnectorNode,
ctx: WalkCtx,
seen_ids: &mut BTreeSet<String>,
referenced_token_ids: &mut BTreeSet<String>,
diagnostics: &mut Vec<Diagnostic>,
) {
let WalkCtx {
resolved_tokens,
declared_style_ids,
all_node_ids,
..
} = ctx;
register_id(&c.id, seen_ids, diagnostics);
check_style_ref(
&c.id,
c.style.as_deref(),
declared_style_ids,
c.source_span,
diagnostics,
);
check_visual_prop(
&c.id,
"stroke",
c.stroke.as_ref(),
VisualExpect::Color,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
&c.id,
"stroke-width",
c.stroke_width.as_ref(),
VisualExpect::Dimension,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
if let Some(target) = &c.from
&& !all_node_ids.contains(target)
{
diagnostics.push(Diagnostic::warning(
"connector.unknown_target",
format!(
"connector '{}': from '{}' matches no node id in the document",
c.id, target
),
c.source_span,
Some(c.id.clone()),
));
}
if let Some(target) = &c.to
&& !all_node_ids.contains(target)
{
diagnostics.push(Diagnostic::warning(
"connector.unknown_target",
format!(
"connector '{}': to '{}' matches no node id in the document",
c.id, target
),
c.source_span,
Some(c.id.clone()),
));
}
if c.from.is_none() || c.to.is_none() {
diagnostics.push(Diagnostic::warning(
"connector.missing_target",
format!(
"connector '{}': both 'from' and 'to' are required to route",
c.id
),
c.source_span,
Some(c.id.clone()),
));
}
if let Some(r) = c.route.as_deref()
&& !matches!(r, "straight" | "orthogonal" | "avoid")
{
diagnostics.push(Diagnostic::warning(
"connector.invalid_route",
format!(
"connector '{}': route '{r}' is not one of straight/orthogonal/avoid",
c.id
),
c.source_span,
Some(c.id.clone()),
));
}
for (label, marker) in [
("marker-start", c.marker_start.as_deref()),
("marker-end", c.marker_end.as_deref()),
] {
if let Some(m) = marker
&& !matches!(m, "none" | "arrow")
{
diagnostics.push(Diagnostic::warning(
"connector.invalid_marker",
format!(
"connector '{}': {label} '{m}' is not one of none/arrow",
c.id
),
c.source_span,
Some(c.id.clone()),
));
}
}
fn is_valid_anchor(a: &str) -> bool {
if a == "auto" {
return true;
}
let mut recognized = false;
for part in a.split('-') {
match part {
"top" | "bottom" | "left" | "right" | "center" | "centre" | "mid" | "middle" => {
recognized = true;
}
_ => return false,
}
}
recognized
}
for (label, anchor) in [
("from-anchor", c.from_anchor.as_deref()),
("to-anchor", c.to_anchor.as_deref()),
] {
if let Some(a) = anchor
&& !is_valid_anchor(a)
{
diagnostics.push(Diagnostic::warning(
"connector.invalid_anchor",
format!(
"connector '{}': {label} '{a}' is not 'auto' or a nine-point anchor \
(top/center/bottom × left/center/right, e.g. bottom-right)",
c.id
),
c.source_span,
Some(c.id.clone()),
));
}
}
check_unknown_props(
"connector",
&c.id,
&c.unknown_props,
c.source_span,
diagnostics,
);
}
pub(in crate::validate::check) fn check_unknown(
u: &UnknownNode,
seen_ids: &mut BTreeSet<String>,
diagnostics: &mut Vec<Diagnostic>,
) {
diagnostics.push(Diagnostic::warning(
"node.unknown_kind",
format!(
"unknown node kind '{}' (forward-compatibility; \
this kind may be valid in a later schema version)",
u.kind
),
u.source_span,
u.id.clone(),
));
if let Some(id) = &u.id {
register_id(id, seen_ids, diagnostics);
}
}