1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct YamlKeyValue {
7 pub key: String,
8 pub value: String,
9 pub line: usize,
10 pub indent: usize,
11}
12
13pub fn looks_like_yaml(input: &str) -> bool {
15 input.lines().any(|line| {
16 let trimmed = line.trim();
17 !trimmed.is_empty()
18 && !trimmed.starts_with('#')
19 && (is_yaml_document_start(trimmed)
20 || is_yaml_document_end(trimmed)
21 || is_yaml_list_item(line)
22 || split_yaml_key_value(line).is_some())
23 })
24}
25
26pub fn is_yaml_document_start(line: &str) -> bool {
28 line.trim() == "---"
29}
30
31pub fn is_yaml_document_end(line: &str) -> bool {
33 line.trim() == "..."
34}
35
36pub fn strip_yaml_document_markers(input: &str) -> &str {
38 let mut start = 0;
39
40 if !input.is_empty() {
41 let first_line_end = input.find('\n').unwrap_or(input.len());
42 let first_line = input[..first_line_end].trim_end_matches('\r');
43 if is_yaml_document_start(first_line) {
44 start = first_line_end.min(input.len());
45 if start < input.len() && input.as_bytes()[start] == b'\n' {
46 start += 1;
47 }
48 }
49 }
50
51 let remainder = &input[start..];
52 let mut body_end = remainder.len();
53
54 while body_end > 0 && matches!(remainder.as_bytes()[body_end - 1], b'\n' | b'\r') {
55 body_end -= 1;
56 }
57
58 if body_end == 0 {
59 return "";
60 }
61
62 let last_line_start = remainder[..body_end]
63 .rfind('\n')
64 .map_or(0, |index| index + 1);
65 let last_line = remainder[last_line_start..body_end].trim_end_matches('\r');
66
67 let end = if is_yaml_document_end(last_line) {
68 let mut marker_start = last_line_start;
69 if marker_start > 0 && remainder.as_bytes()[marker_start - 1] == b'\n' {
70 marker_start -= 1;
71 }
72 if marker_start > 0 && remainder.as_bytes()[marker_start - 1] == b'\r' {
73 marker_start -= 1;
74 }
75 marker_start
76 } else {
77 remainder.len()
78 };
79
80 &remainder[..end]
81}
82
83pub fn is_yaml_list_item(line: &str) -> bool {
85 let trimmed = line.trim_start();
86 trimmed == "-" || trimmed.starts_with("- ")
87}
88
89pub fn yaml_indent(line: &str) -> usize {
91 line.chars().take_while(|ch| *ch == ' ').count()
92}
93
94pub fn split_yaml_key_value(line: &str) -> Option<(String, String)> {
96 let content = strip_yaml_comment(line).trim();
97 if content.is_empty() || is_yaml_document_start(content) || is_yaml_document_end(content) {
98 return None;
99 }
100
101 if is_yaml_list_item(content) {
102 return None;
103 }
104
105 let mut in_single = false;
106 let mut in_double = false;
107 let mut escaped = false;
108 let mut chars = content.char_indices().peekable();
109
110 while let Some((index, ch)) = chars.next() {
111 if in_single {
112 if ch == '\'' {
113 in_single = false;
114 }
115 continue;
116 }
117
118 if in_double {
119 if escaped {
120 escaped = false;
121 } else if ch == '\\' {
122 escaped = true;
123 } else if ch == '"' {
124 in_double = false;
125 }
126 continue;
127 }
128
129 match ch {
130 '\'' => in_single = true,
131 '"' => in_double = true,
132 ':' => {
133 let next_is_value = chars
134 .peek()
135 .map(|(_, next)| next.is_whitespace())
136 .unwrap_or(true);
137 if !next_is_value {
138 continue;
139 }
140
141 let key = content[..index].trim();
142 if key.is_empty() {
143 return None;
144 }
145
146 let value = content[index + 1..].trim().to_string();
147 return Some((key.to_string(), value));
148 }
149 _ => {}
150 }
151 }
152
153 None
154}
155
156pub fn extract_yaml_key_values(input: &str) -> Vec<YamlKeyValue> {
158 let mut pairs = Vec::new();
159
160 for (line_index, line) in input.lines().enumerate() {
161 if let Some((key, value)) = split_yaml_key_value(line) {
162 pairs.push(YamlKeyValue {
163 key,
164 value,
165 line: line_index + 1,
166 indent: yaml_indent(line),
167 });
168 }
169 }
170
171 pairs
172}
173
174pub fn quote_yaml_string(input: &str) -> String {
176 format!("'{}'", input.replace('\'', "''"))
177}
178
179fn strip_yaml_comment(line: &str) -> &str {
180 let mut in_single = false;
181 let mut in_double = false;
182 let mut escaped = false;
183
184 for (index, ch) in line.char_indices() {
185 if in_single {
186 if ch == '\'' {
187 in_single = false;
188 }
189 continue;
190 }
191
192 if in_double {
193 if escaped {
194 escaped = false;
195 } else if ch == '\\' {
196 escaped = true;
197 } else if ch == '"' {
198 in_double = false;
199 }
200 continue;
201 }
202
203 match ch {
204 '\'' => in_single = true,
205 '"' => in_double = true,
206 '#' => return &line[..index],
207 _ => {}
208 }
209 }
210
211 line
212}