1use crate::analyze;
2use crate::lint::LintOptions;
3use gram_diagnostics::Diagnostic;
4
5pub struct Snippet {
6 pub source: String,
8 pub fence_start_line: usize,
10}
11
12pub fn extract_snippets(doc_source: &str) -> Vec<Snippet> {
14 let mut snippets = Vec::new();
15 let mut in_fence = false;
16 let mut fence_start = 0usize;
17 let mut buf = String::new();
18
19 for (line_no, line) in doc_source.lines().enumerate() {
20 let trimmed = line.trim();
21 if !in_fence {
22 if is_gram_fence_open(trimmed) {
24 in_fence = true;
25 fence_start = line_no;
26 buf.clear();
27 }
28 } else {
29 if is_fence_close(trimmed) {
31 snippets.push(Snippet { source: buf.clone(), fence_start_line: fence_start });
32 in_fence = false;
33 } else {
34 buf.push_str(line);
35 buf.push('\n');
36 }
37 }
38 }
39 snippets
40}
41
42pub fn lint_markdown(doc_source: &str, _opts: &LintOptions) -> Vec<(Snippet, Vec<Diagnostic>)> {
48 extract_snippets(doc_source)
49 .into_iter()
50 .map(|snippet| {
51 let (_, raw) = analyze::analyze_source(&snippet.source);
52 let diags = raw.iter().map(|d| analyze::to_public(&snippet.source, d)).collect();
53 (snippet, diags)
54 })
55 .collect()
56}
57
58fn is_gram_fence_open(trimmed: &str) -> bool {
59 for prefix in ["```", "~~~"] {
61 if let Some(rest) = trimmed.strip_prefix(prefix) {
62 let lang = rest.trim();
63 if lang.eq_ignore_ascii_case("gram") {
64 return true;
65 }
66 }
67 }
68 false
69}
70
71fn is_fence_close(trimmed: &str) -> bool {
72 (trimmed.starts_with("```") || trimmed.starts_with("~~~"))
73 && trimmed.chars().all(|c| c == '`' || c == '~')
74 && trimmed.len() >= 3
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn extracts_gram_fence() {
83 let md = "# Hello\n\n```gram\n(a)-[:KNOWS]->(b)\n```\n";
84 let snippets = extract_snippets(md);
85 assert_eq!(snippets.len(), 1);
86 assert_eq!(snippets[0].source.trim(), "(a)-[:KNOWS]->(b)");
87 assert_eq!(snippets[0].fence_start_line, 2);
88 }
89
90 #[test]
91 fn ignores_non_gram_fences() {
92 let md = "```cypher\nMATCH (n) RETURN n\n```\n\n```gram\n(a)\n```\n";
93 let snippets = extract_snippets(md);
94 assert_eq!(snippets.len(), 1);
95 assert_eq!(snippets[0].source.trim(), "(a)");
96 }
97
98 #[test]
99 fn multiple_fences() {
100 let md = "```gram\n(a)\n```\n\nSome text.\n\n```gram\n(b)\n```\n";
101 let snippets = extract_snippets(md);
102 assert_eq!(snippets.len(), 2);
103 assert_eq!(snippets[0].source.trim(), "(a)");
104 assert_eq!(snippets[1].source.trim(), "(b)");
105 }
106
107 #[test]
108 fn lint_valid_snippet_no_diagnostics() {
109 let md = "```gram\n(alice)-[:KNOWS]->(bob)\n```\n";
110 let results = lint_markdown(md, &LintOptions { strict: false });
111 assert_eq!(results.len(), 1);
112 assert!(results[0].1.is_empty());
113 }
114
115 #[test]
116 fn lint_invalid_snippet_has_diagnostics() {
117 let md = "```gram\n(((\n```\n";
118 let results = lint_markdown(md, &LintOptions { strict: false });
119 assert_eq!(results.len(), 1);
120 assert!(!results[0].1.is_empty());
121 }
122}