plumb_core/rules/spacing/
grid_conformance.rs1use indexmap::IndexMap;
10
11use crate::config::Config;
12use crate::report::{Confidence, Fix, FixKind, Severity, Violation, ViolationSink};
13use crate::rules::Rule;
14use crate::rules::spacing::SPACING_PROPERTIES;
15use crate::rules::util::{nearest_multiple, parse_px};
16use crate::snapshot::SnapshotCtx;
17
18const FRACT_TOLERANCE: f64 = 1e-6;
22
23#[derive(Debug, Clone, Copy)]
25pub struct GridConformance;
26
27impl Rule for GridConformance {
28 fn id(&self) -> &'static str {
29 "spacing/grid-conformance"
30 }
31
32 fn default_severity(&self) -> Severity {
33 Severity::Warning
34 }
35
36 fn summary(&self) -> &'static str {
37 "Flags spacing values that aren't multiples of `spacing.base_unit`."
38 }
39
40 fn check(&self, ctx: &SnapshotCtx<'_>, config: &Config, sink: &mut ViolationSink<'_>) {
41 let base_unit = config.spacing.base_unit;
42 if base_unit == 0 {
43 return;
46 }
47 let base_unit_f = f64::from(base_unit);
48
49 for node in ctx.nodes() {
50 for prop in SPACING_PROPERTIES {
51 let Some(raw) = node.computed_styles.get(*prop) else {
52 continue;
53 };
54 let Some(value) = parse_px(raw) else { continue };
55 if (value / base_unit_f).fract().abs() <= FRACT_TOLERANCE {
56 continue;
57 }
58 let suggested = nearest_multiple(value, base_unit);
59 let to = if suggested == 0 {
60 "0".to_owned()
61 } else {
62 format!("{suggested}px")
63 };
64 sink.push(Violation {
65 rule_id: self.id().to_owned(),
66 severity: self.default_severity(),
67 message: format!(
68 "`{selector}` has off-grid {prop} {raw}; expected a multiple of {base_unit}px.",
69 selector = node.selector,
70 ),
71 selector: node.selector.clone(),
72 viewport: ctx.snapshot().viewport.clone(),
73 rect: ctx.rect_for(node.dom_order),
74 dom_order: node.dom_order,
75 fix: Some(Fix {
76 kind: FixKind::CssPropertyReplace {
77 property: (*prop).to_owned(),
78 from: raw.clone(),
79 to: to.clone(),
80 },
81 description: format!(
82 "Snap `{prop}` to the nearest spacing-grid value ({to}).",
83 ),
84 confidence: Confidence::Medium,
85 }),
86 doc_url: "https://plumb.aramhammoudeh.com/rules/spacing-grid-conformance"
87 .to_owned(),
88 metadata: IndexMap::new(),
89 });
90 }
91 }
92 }
93}