1#[path = "yaml/core.rs"]
12mod core;
13#[path = "yaml/model.rs"]
14mod model;
15
16pub use core::{
17 lex_basic_mapping_tokens, parse_basic_entry, parse_basic_entry_tree, parse_basic_mapping_tree,
18 parse_shadow,
19};
20pub use model::{
21 BasicYamlEntry, ShadowYamlOptions, ShadowYamlOutcome, ShadowYamlReport, YamlInputKind,
22 YamlShadowToken, YamlShadowTokenKind,
23};
24
25#[cfg(test)]
26mod tests {
27 use super::*;
28 use crate::syntax::SyntaxKind;
29
30 #[test]
31 fn parses_basic_title_entry() {
32 let parsed = parse_basic_entry("title: My Title");
33 assert_eq!(
34 parsed,
35 Some(BasicYamlEntry {
36 key: "title",
37 value: "My Title"
38 })
39 );
40 }
41
42 #[test]
43 fn parses_single_line_with_multiple_colons() {
44 let parsed = parse_basic_entry("a: b: c: d");
45 assert_eq!(
46 parsed,
47 Some(BasicYamlEntry {
48 key: "a",
49 value: "b: c: d"
50 })
51 );
52 }
53
54 #[test]
55 fn rejects_missing_value() {
56 assert_eq!(parse_basic_entry("title:"), None);
57 }
58
59 #[test]
60 fn rejects_multiline_input() {
61 assert_eq!(parse_basic_entry("title: My Title\nauthor: Me"), None);
62 }
63
64 #[test]
65 fn accepts_single_line_with_crlf_terminator() {
66 let parsed = parse_basic_entry("title: My Title\r");
67 assert_eq!(
68 parsed,
69 Some(BasicYamlEntry {
70 key: "title",
71 value: "My Title"
72 })
73 );
74 }
75
76 #[test]
77 fn builds_basic_rowan_tree() {
78 let tree = parse_basic_entry_tree("title: My Title").expect("tree");
79 assert_eq!(tree.kind(), SyntaxKind::DOCUMENT);
80 assert_eq!(tree.text().to_string(), "title: My Title");
81
82 let content = tree
83 .children()
84 .find(|n| n.kind() == SyntaxKind::YAML_METADATA_CONTENT)
85 .expect("yaml metadata content");
86 assert_eq!(content.text().to_string(), "title: My Title");
87
88 let mapping = content
89 .children()
90 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
91 .expect("yaml block map");
92 let entry = mapping
93 .children()
94 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
95 .expect("yaml block map entry");
96 let key = entry
97 .children()
98 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_KEY)
99 .expect("yaml block map key");
100 let value = entry
101 .children()
102 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_VALUE)
103 .expect("yaml block map value");
104
105 let key_token_kinds: Vec<_> = key
106 .children_with_tokens()
107 .filter_map(|el| el.into_token())
108 .map(|tok| tok.kind())
109 .collect();
110 assert_eq!(
111 key_token_kinds,
112 vec![SyntaxKind::YAML_KEY, SyntaxKind::YAML_COLON,]
113 );
114
115 let value_token_kinds: Vec<_> = value
116 .children_with_tokens()
117 .filter_map(|el| el.into_token())
118 .map(|tok| tok.kind())
119 .collect();
120 assert_eq!(
121 value_token_kinds,
122 vec![SyntaxKind::WHITESPACE, SyntaxKind::YAML_SCALAR,]
123 );
124 }
125
126 #[test]
127 fn builds_basic_rowan_tree_for_multiline_mapping() {
128 let tree = parse_basic_mapping_tree("title: My Title\nauthor: Me\n").expect("tree");
129 assert_eq!(tree.kind(), SyntaxKind::DOCUMENT);
130 assert_eq!(tree.text().to_string(), "title: My Title\nauthor: Me\n");
131
132 let content = tree
133 .children()
134 .find(|n| n.kind() == SyntaxKind::YAML_METADATA_CONTENT)
135 .expect("yaml metadata content");
136 let mapping = content
137 .children()
138 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
139 .expect("yaml block map");
140 let entries: Vec<_> = mapping
141 .children()
142 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
143 .collect();
144 assert_eq!(entries.len(), 2);
145
146 let token_kinds: Vec<_> = mapping
147 .descendants_with_tokens()
148 .filter_map(|el| el.into_token())
149 .map(|tok| tok.kind())
150 .collect();
151 assert_eq!(
152 token_kinds,
153 vec![
154 SyntaxKind::YAML_KEY,
155 SyntaxKind::YAML_COLON,
156 SyntaxKind::WHITESPACE,
157 SyntaxKind::YAML_SCALAR,
158 SyntaxKind::NEWLINE,
159 SyntaxKind::YAML_KEY,
160 SyntaxKind::YAML_COLON,
161 SyntaxKind::WHITESPACE,
162 SyntaxKind::YAML_SCALAR,
163 SyntaxKind::NEWLINE,
164 ]
165 );
166 }
167
168 #[test]
169 fn mapping_nodes_preserve_entry_text_boundaries() {
170 let tree = parse_basic_mapping_tree("title: A\nauthor: B\n").expect("tree");
171 let content = tree
172 .children()
173 .find(|n| n.kind() == SyntaxKind::YAML_METADATA_CONTENT)
174 .expect("yaml metadata content");
175 let mapping = content
176 .children()
177 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
178 .expect("yaml block map");
179
180 let entry_texts: Vec<_> = mapping
181 .children()
182 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
183 .map(|n| n.text().to_string())
184 .collect();
185 assert_eq!(
186 entry_texts,
187 vec!["title: A\n".to_string(), "author: B\n".to_string(),]
188 );
189 }
190
191 #[test]
192 fn splits_mapping_on_colon_outside_quoted_key() {
193 let input = "\"foo:bar\": 23\n'x:y': 24\n";
194 let tree = parse_basic_mapping_tree(input).expect("tree");
195 assert_eq!(tree.text().to_string(), input);
196
197 let keys: Vec<String> = tree
198 .descendants_with_tokens()
199 .filter_map(|el| el.into_token())
200 .filter(|tok| tok.kind() == SyntaxKind::YAML_KEY)
201 .map(|tok| tok.text().to_string())
202 .collect();
203 assert_eq!(keys, vec!["\"foo:bar\"".to_string(), "'x:y'".to_string()]);
204 }
205
206 #[test]
207 fn preserves_explicit_tag_tokens_in_key_and_value() {
208 let input = "!!str a: !!int 42\n";
209 let tree = parse_basic_mapping_tree(input).expect("tree");
210 assert_eq!(tree.text().to_string(), input);
211
212 let tag_tokens: Vec<_> = tree
213 .descendants_with_tokens()
214 .filter_map(|el| el.into_token())
215 .filter(|tok| tok.kind() == SyntaxKind::YAML_TAG)
216 .map(|tok| tok.text().to_string())
217 .collect();
218 assert_eq!(tag_tokens, vec!["!!str".to_string(), "!!int".to_string()]);
219 }
220
221 #[test]
222 fn lexer_emits_tokens_for_quoted_keys_and_inline_comments() {
223 let input = "\"foo:bar\": 23 # note\n'x:y': 'z' # ok\n";
224 let tokens = lex_basic_mapping_tokens(input).expect("tokens");
225 let kinds: Vec<_> = tokens.iter().map(|t| t.kind).collect();
226 assert_eq!(
227 kinds,
228 vec![
229 YamlShadowTokenKind::Key,
230 YamlShadowTokenKind::Colon,
231 YamlShadowTokenKind::Whitespace,
232 YamlShadowTokenKind::Scalar,
233 YamlShadowTokenKind::Whitespace,
234 YamlShadowTokenKind::Comment,
235 YamlShadowTokenKind::Newline,
236 YamlShadowTokenKind::Key,
237 YamlShadowTokenKind::Colon,
238 YamlShadowTokenKind::Whitespace,
239 YamlShadowTokenKind::Scalar,
240 YamlShadowTokenKind::Whitespace,
241 YamlShadowTokenKind::Comment,
242 YamlShadowTokenKind::Newline,
243 ]
244 );
245 let comments: Vec<_> = tokens
246 .iter()
247 .filter(|t| t.kind == YamlShadowTokenKind::Comment)
248 .map(|t| t.text)
249 .collect();
250 assert_eq!(comments, vec!["# note", "# ok"]);
251 }
252
253 #[test]
254 fn lexer_emits_indent_and_dedent_for_indented_entries() {
255 let input = "root: 1\n child: 2\n";
256 let tokens = lex_basic_mapping_tokens(input).expect("tokens");
257 let kinds: Vec<_> = tokens.iter().map(|t| t.kind).collect();
258 assert!(kinds.contains(&YamlShadowTokenKind::Indent));
259 assert!(kinds.contains(&YamlShadowTokenKind::Dedent));
260 }
261
262 #[test]
263 fn parser_builds_nested_block_map_from_indent_tokens() {
264 let input = "root: 1\n child: 2\n";
265 let tree = parse_basic_mapping_tree(input).expect("tree");
266
267 let outer_map = tree
268 .descendants()
269 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
270 .expect("outer map");
271 let outer_entry = outer_map
272 .children()
273 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
274 .expect("outer entry");
275 let outer_value = outer_entry
276 .children()
277 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_VALUE)
278 .expect("outer value");
279
280 let nested_map = outer_value
281 .children()
282 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
283 .expect("nested map");
284 let nested_entry_count = nested_map
285 .children()
286 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
287 .count();
288 assert_eq!(nested_entry_count, 1);
289 }
290
291 #[test]
292 fn rejects_tree_for_invalid_input() {
293 assert!(parse_basic_entry_tree("title:").is_none());
294 }
295
296 #[test]
297 fn shadow_parse_is_disabled_by_default() {
298 let report = parse_shadow("title: My Title", ShadowYamlOptions::default());
299 assert_eq!(report.outcome, ShadowYamlOutcome::SkippedDisabled);
300 assert_eq!(report.shadow_reason, "shadow-disabled");
301 assert_eq!(report.normalized_input, None);
302 }
303
304 #[test]
305 fn shadow_parse_skips_when_disabled_even_for_valid_input() {
306 let report = parse_shadow(
307 "title: My Title",
308 ShadowYamlOptions {
309 enabled: false,
310 input_kind: YamlInputKind::Plain,
311 },
312 );
313 assert_eq!(report.outcome, ShadowYamlOutcome::SkippedDisabled);
314 assert_eq!(report.shadow_reason, "shadow-disabled");
315 }
316
317 #[test]
318 fn shadow_parse_reports_prototype_parsed_when_enabled() {
319 let report = parse_shadow(
320 "title: My Title",
321 ShadowYamlOptions {
322 enabled: true,
323 input_kind: YamlInputKind::Plain,
324 },
325 );
326 assert_eq!(report.outcome, ShadowYamlOutcome::PrototypeParsed);
327 assert_eq!(report.shadow_reason, "prototype-basic-mapping-parsed");
328 assert_eq!(report.normalized_input.as_deref(), Some("title: My Title"));
329 }
330
331 #[test]
332 fn shadow_parse_reports_prototype_rejected_when_enabled() {
333 let report = parse_shadow(
334 "title:",
335 ShadowYamlOptions {
336 enabled: true,
337 input_kind: YamlInputKind::Plain,
338 },
339 );
340 assert_eq!(report.outcome, ShadowYamlOutcome::PrototypeRejected);
341 assert_eq!(report.shadow_reason, "prototype-basic-mapping-rejected");
342 }
343
344 #[test]
345 fn shadow_parse_accepts_hashpipe_mode_but_remains_prototype_scoped() {
346 let report = parse_shadow(
347 "#| title: My Title",
348 ShadowYamlOptions {
349 enabled: true,
350 input_kind: YamlInputKind::Hashpipe,
351 },
352 );
353 assert_eq!(report.outcome, ShadowYamlOutcome::PrototypeParsed);
354 assert_eq!(report.shadow_reason, "prototype-basic-mapping-parsed");
355 assert_eq!(report.normalized_input.as_deref(), Some("title: My Title"));
356 }
357}