lintel_validate/parsers/
yaml.rs1use miette::NamedSource;
2use serde_json::Value;
3
4use crate::diagnostics::ParseDiagnostic;
5
6use super::Parser;
7
8pub struct YamlParser;
9
10impl Parser for YamlParser {
11 fn parse(&self, content: &str, file_name: &str) -> Result<Value, ParseDiagnostic> {
12 let clean: alloc::borrow::Cow<'_, str> = if content.contains('\u{FEFF}') {
16 content.replace('\u{FEFF}', "").into()
17 } else {
18 content.into()
19 };
20 serde_yaml::from_str(&clean).map_err(|e| {
21 let offset = e.location().map_or(0, |loc| loc.index());
22 ParseDiagnostic {
23 src: NamedSource::new(file_name, content.to_string()),
24 span: offset.into(),
25 message: e.to_string(),
26 }
27 })
28 }
29
30 fn annotate(&self, content: &str, schema_url: &str) -> Option<String> {
31 Some(format!(
32 "# yaml-language-server: $schema={schema_url}\n{content}"
33 ))
34 }
35
36 fn strip_annotation(&self, content: &str) -> String {
37 strip_yaml_modeline(content)
38 }
39
40 fn extract_schema_uri(&self, content: &str, value: &Value) -> Option<String> {
41 if let Some(uri) = extract_yaml_modeline_schema(content) {
43 return Some(uri);
44 }
45 value
47 .get("$schema")
48 .and_then(Value::as_str)
49 .map(String::from)
50 }
51}
52
53fn strip_yaml_modeline(content: &str) -> String {
55 let mut offset = 0;
56 for line in content.lines() {
57 let trimmed = line.trim();
58 if trimmed.is_empty() {
59 offset += line.len() + 1;
60 continue;
61 }
62 if !trimmed.starts_with('#') {
63 break;
64 }
65 if let Some(after_hash) = trimmed.strip_prefix('#') {
66 let after_hash = after_hash.trim();
67 if let Some(rest) = after_hash.strip_prefix("yaml-language-server:")
68 && rest.trim().starts_with("$schema=")
69 {
70 let line_end = offset + line.len();
71 let remove_end = if content.as_bytes().get(line_end) == Some(&b'\n') {
72 line_end + 1
73 } else {
74 line_end
75 };
76 return format!("{}{}", &content[..offset], &content[remove_end..]);
77 }
78 }
79 offset += line.len() + 1;
80 }
81 content.to_string()
82}
83
84fn extract_yaml_modeline_schema(content: &str) -> Option<String> {
86 for line in content.lines() {
87 let trimmed = line.trim();
88 if trimmed.is_empty() {
89 continue;
90 }
91 if !trimmed.starts_with('#') {
92 break;
93 }
94 let after_hash = trimmed.strip_prefix('#')?.trim();
95 if let Some(rest) = after_hash.strip_prefix("yaml-language-server:") {
96 let rest = rest.trim();
97 if let Some(uri) = rest.strip_prefix("$schema=") {
98 let uri = uri.trim();
99 if !uri.is_empty() {
100 return Some(uri.to_string());
101 }
102 }
103 }
104 }
105 None
106}