Skip to main content

ailint_core/parser/
mod.rs

1//! Parsers for each supported guidance format.
2
3pub mod json;
4pub mod markdown;
5pub mod yaml;
6
7use std::path::Path;
8
9use anyhow::Result;
10
11use crate::file_type::FileType;
12
13/// A parsed guidance document — the input to the rule engine.
14#[derive(Debug, Clone)]
15pub struct ParsedDocument {
16    /// Path the document was read from.
17    pub path: std::path::PathBuf,
18    /// Detected guidance file type.
19    pub file_type: FileType,
20    /// Full original file contents.
21    pub raw: String,
22    /// Format-specific parsed representation.
23    pub content: DocumentContent,
24}
25
26/// Format-specific parsed representation for a document.
27#[derive(Debug, Clone)]
28pub enum DocumentContent {
29    /// Parsed Markdown structure.
30    Markdown(markdown::MarkdownDoc),
31    /// Parsed YAML value.
32    Yaml(serde_yaml::Value),
33    /// Parsed JSON value.
34    Json(serde_json::Value),
35    /// The file was recognized as YAML or JSON but failed to parse. The
36    /// stored string is the parser error message so rules like
37    /// `malformed-yaml` (AIL041) can surface it.
38    ParseError(String),
39    /// Plain text with no recognized structure.
40    Text,
41    /// A zero-length or whitespace-only file.
42    Empty,
43}
44
45/// Read and parse a guidance file according to its detected type.
46pub fn parse(path: &Path, file_type: FileType) -> Result<ParsedDocument> {
47    let raw = std::fs::read_to_string(path)?;
48    let content = dispatch(&raw, file_type, path);
49    Ok(ParsedDocument {
50        path: path.to_path_buf(),
51        file_type,
52        raw,
53        content,
54    })
55}
56
57fn dispatch(raw: &str, file_type: FileType, path: &Path) -> DocumentContent {
58    if raw.is_empty() {
59        return DocumentContent::Empty;
60    }
61    let ext = path
62        .extension()
63        .and_then(|s| s.to_str())
64        .map(|s| s.to_ascii_lowercase());
65    let ext_str = ext.as_deref();
66
67    match file_type {
68        FileType::ClaudeMd
69        | FileType::AgentsMd
70        | FileType::CopilotCustomization
71        | FileType::CopilotInstructions
72        | FileType::JunieGuidelines
73        | FileType::AiderConventions
74        | FileType::GitHubSkill
75        | FileType::CustomProjectRules
76        | FileType::GenericMarkdown => DocumentContent::Markdown(markdown::parse(raw)),
77
78        FileType::CursorRules
79        | FileType::WindsurfRules
80        | FileType::ClineRules
81        | FileType::ContinueRules => match ext_str {
82            Some("md") | Some("markdown") | Some("mdc") => {
83                DocumentContent::Markdown(markdown::parse(raw))
84            }
85            Some("yaml") | Some("yml") => match yaml::parse(raw) {
86                Ok(v) => DocumentContent::Yaml(v),
87                Err(e) => DocumentContent::ParseError(e.to_string()),
88            },
89            Some("json") => match json::parse(raw) {
90                Ok(v) => DocumentContent::Json(v),
91                Err(e) => DocumentContent::ParseError(e.to_string()),
92            },
93            _ => DocumentContent::Markdown(markdown::parse(raw)),
94        },
95
96        FileType::GenericSystemPrompt => match ext_str {
97            Some("json") => match json::parse(raw) {
98                Ok(v) => DocumentContent::Json(v),
99                Err(e) => DocumentContent::ParseError(e.to_string()),
100            },
101            Some("yaml") | Some("yml") => match yaml::parse(raw) {
102                Ok(v) => DocumentContent::Yaml(v),
103                Err(e) => DocumentContent::ParseError(e.to_string()),
104            },
105            Some("md") | Some("markdown") => DocumentContent::Markdown(markdown::parse(raw)),
106            _ => DocumentContent::Markdown(markdown::parse(raw)),
107        },
108
109        FileType::GenericYaml => match yaml::parse(raw) {
110            Ok(v) => DocumentContent::Yaml(v),
111            Err(e) => DocumentContent::ParseError(e.to_string()),
112        },
113
114        FileType::Unknown => DocumentContent::Text,
115    }
116}