Skip to main content

ailint_core/rules/structural/
frontmatter_schema.rs

1//! AIL001 `no-frontmatter-schema-error` — YAML frontmatter fails to parse.
2//!
3//! See: `docs/rules/structural/AIL001.md`
4
5use crate::parser::{DocumentContent, ParsedDocument};
6use crate::rules::structural::AIL001;
7use crate::rules::{Rule, RuleContext, RuleId, Severity, Violation};
8
9/// AIL001 no-frontmatter-schema-error: frontmatter must match the file type's schema.
10#[derive(Debug, Default)]
11pub struct FrontmatterSchemaRule;
12
13impl Rule for FrontmatterSchemaRule {
14    fn id(&self) -> RuleId {
15        AIL001
16    }
17
18    fn default_severity(&self) -> Severity {
19        Severity::Error
20    }
21
22    fn description(&self) -> &'static str {
23        "YAML frontmatter fails to parse."
24    }
25
26    fn fix_hint(&self) -> &'static str {
27        "Check indentation, quote strings containing colons, and align list items."
28    }
29
30    fn run(&self, doc: &ParsedDocument, _ctx: &RuleContext<'_>) -> Vec<Violation> {
31        let DocumentContent::Markdown(md) = &doc.content else {
32            return Vec::new();
33        };
34        let Some(fm) = md.frontmatter.as_ref() else {
35            return Vec::new();
36        };
37        let Err(err) = serde_yaml::from_str::<serde_yaml::Value>(&fm.raw) else {
38            return Vec::new();
39        };
40        let line = line_at_offset(&doc.raw, fm.byte_range.start);
41        let v = Violation::new(
42            AIL001,
43            self.default_severity(),
44            doc.path.clone(),
45            "invalid YAML frontmatter",
46        )
47        .at(line, 1)
48        .with_detail(err.to_string());
49        vec![v]
50    }
51}
52
53fn line_at_offset(raw: &str, offset: usize) -> usize {
54    let end = offset.min(raw.len());
55    raw.as_bytes()[..end]
56        .iter()
57        .filter(|&&b| b == b'\n')
58        .count()
59        + 1
60}