plumb_core/rules/shadow/
scale_conformance.rs1use crate::config::Config;
5use crate::report::{Confidence, Fix, FixKind, Severity, Violation, ViolationSink};
6use crate::rules::Rule;
7use crate::snapshot::SnapshotCtx;
8
9const BOX_SHADOW: &str = "box-shadow";
11
12#[derive(Debug, Clone, Copy)]
14pub struct ScaleConformance;
15
16impl Rule for ScaleConformance {
17 fn id(&self) -> &'static str {
18 "shadow/scale-conformance"
19 }
20
21 fn default_severity(&self) -> Severity {
22 Severity::Warning
23 }
24
25 fn summary(&self) -> &'static str {
26 "Flags `box-shadow` values that aren't in `shadow.scale`."
27 }
28
29 fn check(&self, ctx: &SnapshotCtx<'_>, config: &Config, sink: &mut ViolationSink<'_>) {
30 let scale = &config.shadow.scale;
31 if scale.is_empty() {
32 return;
33 }
34
35 for node in ctx.nodes() {
36 let Some(raw) = node.computed_styles.get(BOX_SHADOW) else {
37 continue;
38 };
39 let trimmed = raw.trim();
40 if trimmed.eq_ignore_ascii_case("none") {
41 continue;
42 }
43
44 let matches = scale.iter().any(|s| s.trim() == trimmed);
45 if matches {
46 continue;
47 }
48
49 sink.push(Violation {
50 rule_id: self.id().to_owned(),
51 severity: self.default_severity(),
52 message: format!(
53 "`{selector}` has off-scale box-shadow `{trimmed}`; expected a value from shadow.scale.",
54 selector = node.selector,
55 ),
56 selector: node.selector.clone(),
57 viewport: ctx.snapshot().viewport.clone(),
58 rect: ctx.rect_for(node.dom_order),
59 dom_order: node.dom_order,
60 fix: Some(Fix {
61 kind: FixKind::Description {
62 text: format!(
63 "The box-shadow value `{trimmed}` is not in the allowed shadow scale.",
64 ),
65 },
66 description: "Replace `box-shadow` with one of the allowed shadow tokens."
67 .to_owned(),
68 confidence: Confidence::Medium,
69 }),
70 doc_url: "https://plumb.aramhammoudeh.com/rules/shadow-scale-conformance"
71 .to_owned(),
72 metadata: indexmap::IndexMap::new(),
73 });
74 }
75 }
76}