use std::collections::BTreeSet;
use crate::ast::node::{FieldNode, FootnoteNode, InstanceNode, PolygonNode, PolylineNode, TocNode};
use crate::diagnostics::Diagnostic;
use super::shared::{
AnchorParentCtx, AnchorProps, check_anchor, check_dimension_geom, 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_polygon(
poly: &PolygonNode,
ctx: WalkCtx,
seen_ids: &mut BTreeSet<String>,
referenced_token_ids: &mut BTreeSet<String>,
diagnostics: &mut Vec<Diagnostic>,
) {
let WalkCtx {
resolved_tokens,
declared_style_ids,
..
} = ctx;
register_id(&poly.id, seen_ids, diagnostics);
check_style_ref(
&poly.id,
poly.style.as_deref(),
declared_style_ids,
poly.source_span,
diagnostics,
);
for (idx, pt) in poly.points.iter().enumerate() {
let x_label = format!("point[{idx}].x");
let y_label = format!("point[{idx}].y");
check_dimension_geom(
&poly.id,
&x_label,
pt.x.as_ref(),
true,
poly.source_span,
diagnostics,
);
check_dimension_geom(
&poly.id,
&y_label,
pt.y.as_ref(),
true,
poly.source_span,
diagnostics,
);
}
if poly.points.len() < 3 {
diagnostics.push(Diagnostic::error(
"shape.insufficient_points",
format!(
"polygon '{}': requires at least 3 points, got {}",
poly.id,
poly.points.len()
),
poly.source_span,
Some(poly.id.clone()),
));
}
check_visual_prop(
&poly.id,
"fill",
poly.fill.as_ref(),
VisualExpect::ColorOrGradient,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
&poly.id,
"stroke",
poly.stroke.as_ref(),
VisualExpect::Color,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
&poly.id,
"stroke-width",
poly.stroke_width.as_ref(),
VisualExpect::Dimension,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
if let Some(fr) = &poly.fill_rule
&& !matches!(fr.as_str(), "nonzero" | "evenodd")
{
diagnostics.push(Diagnostic::warning(
"node.unknown_property",
format!(
"polygon '{}': unrecognized fill-rule '{}' (version-relative; \
allowed values are nonzero, evenodd)",
poly.id, fr
),
poly.source_span,
Some(poly.id.clone()),
));
}
if let Some(sa) = &poly.stroke_alignment
&& !matches!(sa.as_str(), "inside" | "center" | "outside")
{
diagnostics.push(Diagnostic::warning(
"node.unknown_property",
format!(
"polygon '{}': unrecognized stroke-alignment '{}' (version-relative; \
allowed values are inside, center, outside)",
poly.id, sa
),
poly.source_span,
Some(poly.id.clone()),
));
}
check_unknown_props(
"polygon",
&poly.id,
&poly.unknown_props,
poly.source_span,
diagnostics,
);
}
pub(in crate::validate::check) fn check_polyline(
poly: &PolylineNode,
ctx: WalkCtx,
seen_ids: &mut BTreeSet<String>,
referenced_token_ids: &mut BTreeSet<String>,
diagnostics: &mut Vec<Diagnostic>,
) {
let WalkCtx {
resolved_tokens,
declared_style_ids,
..
} = ctx;
register_id(&poly.id, seen_ids, diagnostics);
check_style_ref(
&poly.id,
poly.style.as_deref(),
declared_style_ids,
poly.source_span,
diagnostics,
);
for (idx, pt) in poly.points.iter().enumerate() {
let x_label = format!("point[{idx}].x");
let y_label = format!("point[{idx}].y");
check_dimension_geom(
&poly.id,
&x_label,
pt.x.as_ref(),
true,
poly.source_span,
diagnostics,
);
check_dimension_geom(
&poly.id,
&y_label,
pt.y.as_ref(),
true,
poly.source_span,
diagnostics,
);
}
if poly.points.len() < 2 {
diagnostics.push(Diagnostic::error(
"shape.insufficient_points",
format!(
"polyline '{}': requires at least 2 points, got {}",
poly.id,
poly.points.len()
),
poly.source_span,
Some(poly.id.clone()),
));
}
check_visual_prop(
&poly.id,
"fill",
poly.fill.as_ref(),
VisualExpect::ColorOrGradient,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
&poly.id,
"stroke",
poly.stroke.as_ref(),
VisualExpect::Color,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
&poly.id,
"stroke-width",
poly.stroke_width.as_ref(),
VisualExpect::Dimension,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
if let Some(fr) = &poly.fill_rule
&& !matches!(fr.as_str(), "nonzero" | "evenodd")
{
diagnostics.push(Diagnostic::warning(
"node.unknown_property",
format!(
"polyline '{}': unrecognized fill-rule '{}' (version-relative; \
allowed values are nonzero, evenodd)",
poly.id, fr
),
poly.source_span,
Some(poly.id.clone()),
));
}
check_unknown_props(
"polyline",
&poly.id,
&poly.unknown_props,
poly.source_span,
diagnostics,
);
}
pub(in crate::validate::check) fn check_instance(
inst: &InstanceNode,
ctx: WalkCtx,
seen_ids: &mut BTreeSet<String>,
referenced_token_ids: &mut BTreeSet<String>,
diagnostics: &mut Vec<Diagnostic>,
) {
let WalkCtx {
resolved_tokens,
declared_component_ids,
component_local_ids,
..
} = ctx;
register_id(&inst.id, seen_ids, diagnostics);
let component_known = declared_component_ids.contains(&inst.component);
if !component_known {
diagnostics.push(Diagnostic::error(
"component.unknown_reference",
format!(
"instance '{}': references component '{}' which is not declared in the \
components block",
inst.id, inst.component
),
inst.source_span,
Some(inst.id.clone()),
));
}
let local_ids = component_local_ids.get(&inst.component);
for ov in &inst.overrides {
check_visual_prop(
&inst.id,
"fill",
ov.fill.as_ref(),
VisualExpect::Color,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
if let Some(spans) = &ov.spans {
for span in spans {
check_visual_prop(
&inst.id,
"fill",
span.fill.as_ref(),
VisualExpect::Color,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
}
}
let target_known = local_ids
.map(|ids| ids.contains(&ov.ref_id))
.unwrap_or(false);
if component_known && !target_known {
diagnostics.push(Diagnostic::warning(
"component.unknown_override_target",
format!(
"instance '{}': override ref '{}' matches no descendant id in component '{}'",
inst.id, ov.ref_id, inst.component
),
ov.source_span.or(inst.source_span),
Some(inst.id.clone()),
));
}
}
check_unknown_props(
"instance",
&inst.id,
&inst.unknown_props,
inst.source_span,
diagnostics,
);
}
const KNOWN_FIELD_TYPES: &[&str] = &[
"running-head",
"page-number",
"page-ref",
"page-count",
"section-page-number",
"section-page-count",
"section-name",
];
pub(in crate::validate::check) fn check_field(
field: &FieldNode,
ctx: WalkCtx,
seen_ids: &mut BTreeSet<String>,
referenced_token_ids: &mut BTreeSet<String>,
parent_ctx: AnchorParentCtx,
diagnostics: &mut Vec<Diagnostic>,
) {
let WalkCtx {
resolved_tokens,
declared_style_ids,
all_node_ids,
zone_ids,
..
} = ctx;
register_id(&field.id, seen_ids, diagnostics);
check_style_ref(
&field.id,
field.style.as_deref(),
declared_style_ids,
field.source_span,
diagnostics,
);
check_anchor(
&field.id,
AnchorProps {
anchor: field.anchor.as_deref(),
anchor_zone: field.anchor_zone.as_deref(),
anchor_sibling: field.anchor_sibling.as_deref(),
anchor_parent: field.anchor_parent == Some(true),
anchor_edge: field.anchor_edge.as_deref(),
anchor_gap: field.anchor_gap.as_ref(),
},
parent_ctx,
zone_ids,
field.source_span,
diagnostics,
);
if !KNOWN_FIELD_TYPES.contains(&field.field_type.as_str()) {
diagnostics.push(Diagnostic::warning(
"field.unknown_type",
format!(
"field '{}': unknown type '{}'; expected one of {}",
field.id,
field.field_type,
KNOWN_FIELD_TYPES.join(", ")
),
field.source_span,
Some(field.id.clone()),
));
}
if field.field_type == "page-ref" {
let resolved = field
.target
.as_ref()
.map(|t| all_node_ids.contains(t))
.unwrap_or(false);
if !resolved {
diagnostics.push(Diagnostic::warning(
"field.unresolved_ref",
format!(
"field '{}': page-ref target {} matches no node id in the document",
field.id,
field
.target
.as_deref()
.map(|t| format!("'{t}'"))
.unwrap_or_else(|| "(absent)".to_owned())
),
field.source_span,
Some(field.id.clone()),
));
}
}
check_visual_prop(
&field.id,
"fill",
field.fill.as_ref(),
VisualExpect::Color,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
&field.id,
"font-family",
field.font_family.as_ref(),
VisualExpect::FontFamily,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
&field.id,
"font-size",
field.font_size.as_ref(),
VisualExpect::Dimension,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_unknown_props(
"field",
&field.id,
&field.unknown_props,
field.source_span,
diagnostics,
);
}
pub(in crate::validate::check) fn check_toc(
toc: &TocNode,
ctx: WalkCtx,
seen_ids: &mut BTreeSet<String>,
referenced_token_ids: &mut BTreeSet<String>,
parent_ctx: AnchorParentCtx,
diagnostics: &mut Vec<Diagnostic>,
) {
let WalkCtx {
resolved_tokens,
declared_style_ids,
zone_ids,
..
} = ctx;
register_id(&toc.id, seen_ids, diagnostics);
check_style_ref(
&toc.id,
toc.style.as_deref(),
declared_style_ids,
toc.source_span,
diagnostics,
);
check_anchor(
&toc.id,
AnchorProps {
anchor: toc.anchor.as_deref(),
anchor_zone: toc.anchor_zone.as_deref(),
anchor_sibling: toc.anchor_sibling.as_deref(),
anchor_parent: toc.anchor_parent == Some(true),
anchor_edge: toc.anchor_edge.as_deref(),
anchor_gap: toc.anchor_gap.as_ref(),
},
parent_ctx,
zone_ids,
toc.source_span,
diagnostics,
);
if toc.match_role.is_none() && toc.match_style.is_none() {
diagnostics.push(Diagnostic::warning(
"toc.no_selector",
format!(
"toc '{}' has neither match-role nor match-style; it will collect no entries",
toc.id
),
toc.source_span,
Some(toc.id.clone()),
));
}
check_visual_prop(
&toc.id,
"fill",
toc.fill.as_ref(),
VisualExpect::Color,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
&toc.id,
"font-family",
toc.font_family.as_ref(),
VisualExpect::FontFamily,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
&toc.id,
"font-size",
toc.font_size.as_ref(),
VisualExpect::Dimension,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_unknown_props(
"toc",
&toc.id,
&toc.unknown_props,
toc.source_span,
diagnostics,
);
}
pub(in crate::validate::check) fn check_footnote(
footnote: &FootnoteNode,
ctx: WalkCtx,
seen_ids: &mut BTreeSet<String>,
referenced_token_ids: &mut BTreeSet<String>,
diagnostics: &mut Vec<Diagnostic>,
) {
let WalkCtx {
resolved_tokens,
declared_style_ids,
..
} = ctx;
register_id(&footnote.id, seen_ids, diagnostics);
check_style_ref(
&footnote.id,
footnote.style.as_deref(),
declared_style_ids,
footnote.source_span,
diagnostics,
);
check_visual_prop(
&footnote.id,
"fill",
footnote.fill.as_ref(),
VisualExpect::Color,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
&footnote.id,
"font-family",
footnote.font_family.as_ref(),
VisualExpect::FontFamily,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_visual_prop(
&footnote.id,
"font-size",
footnote.font_size.as_ref(),
VisualExpect::Dimension,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_spans(
&footnote.id,
&footnote.spans,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
check_unknown_props(
"footnote",
&footnote.id,
&footnote.unknown_props,
footnote.source_span,
diagnostics,
);
}