1use rowan::{TextRange, TextSize};
30
31use super::ast::{AstChildren, AstNode, support};
32use super::{PanacheLanguage, SyntaxKind, SyntaxNode, SyntaxToken};
33use crate::parser::yaml::{ScalarStyle, cook, parse_yaml_tree};
34
35pub fn parse_yaml_document(input: &str) -> Option<YamlDocument> {
41 first_document(&parse_yaml_tree(input)?)
42}
43
44pub fn parse_yaml_documents(input: &str) -> Vec<YamlDocument> {
48 let Some(tree) = parse_yaml_tree(input) else {
49 return Vec::new();
50 };
51 stream_container(&tree)
52 .map(|stream| stream.children().filter_map(YamlDocument::cast).collect())
53 .unwrap_or_default()
54}
55
56pub(crate) fn is_stream_equivalent(kind: SyntaxKind) -> bool {
62 matches!(
63 kind,
64 SyntaxKind::YAML_STREAM
65 | SyntaxKind::YAML_METADATA_CONTENT
66 | SyntaxKind::HASHPIPE_YAML_CONTENT
67 )
68}
69
70fn stream_container(tree: &SyntaxNode) -> Option<SyntaxNode> {
71 tree.descendants().find(|n| is_stream_equivalent(n.kind()))
72}
73
74fn first_document(tree: &SyntaxNode) -> Option<YamlDocument> {
75 stream_container(tree)?
76 .children()
77 .find_map(YamlDocument::cast)
78}
79
80#[derive(Debug, Clone)]
83pub enum YamlNode {
84 BlockMap(YamlBlockMap),
85 BlockSequence(YamlBlockSequence),
86 FlowMap(YamlFlowMap),
87 FlowSequence(YamlFlowSequence),
88 Scalar(YamlScalar),
89}
90
91fn node_child(parent: &SyntaxNode) -> Option<YamlNode> {
96 for child in parent.children() {
97 match child.kind() {
98 SyntaxKind::YAML_BLOCK_MAP => return YamlBlockMap::cast(child).map(YamlNode::BlockMap),
99 SyntaxKind::YAML_BLOCK_SEQUENCE => {
100 return YamlBlockSequence::cast(child).map(YamlNode::BlockSequence);
101 }
102 SyntaxKind::YAML_FLOW_MAP => return YamlFlowMap::cast(child).map(YamlNode::FlowMap),
103 SyntaxKind::YAML_FLOW_SEQUENCE => {
104 return YamlFlowSequence::cast(child).map(YamlNode::FlowSequence);
105 }
106 _ => {}
107 }
108 }
109 scalar_token(parent).map(YamlNode::Scalar)
110}
111
112fn scalar_token(parent: &SyntaxNode) -> Option<YamlScalar> {
117 parent
118 .children()
119 .find(|n| n.kind() == SyntaxKind::YAML_SCALAR)
120 .and_then(YamlScalar::cast)
121}
122
123fn token_of(parent: &SyntaxNode, kind: SyntaxKind) -> Option<SyntaxToken> {
124 parent
125 .children_with_tokens()
126 .filter_map(|el| el.into_token())
127 .find(|t| t.kind() == kind)
128}
129
130fn content_text_range(node: &SyntaxNode) -> TextRange {
137 let range = node.text_range();
138 let content_len = node.text().to_string().trim_end().len();
139 let end = range.start() + TextSize::from(content_len as u32);
140 TextRange::new(range.start(), end)
141}
142
143macro_rules! node_projections {
146 () => {
147 pub fn as_node(&self) -> Option<YamlNode> {
149 node_child(&self.0)
150 }
151
152 pub fn as_scalar(&self) -> Option<YamlScalar> {
154 match self.as_node()? {
155 YamlNode::Scalar(s) => Some(s),
156 _ => None,
157 }
158 }
159
160 pub fn as_block_map(&self) -> Option<YamlBlockMap> {
161 match self.as_node()? {
162 YamlNode::BlockMap(m) => Some(m),
163 _ => None,
164 }
165 }
166
167 pub fn as_block_sequence(&self) -> Option<YamlBlockSequence> {
168 match self.as_node()? {
169 YamlNode::BlockSequence(s) => Some(s),
170 _ => None,
171 }
172 }
173
174 pub fn as_flow_map(&self) -> Option<YamlFlowMap> {
175 match self.as_node()? {
176 YamlNode::FlowMap(m) => Some(m),
177 _ => None,
178 }
179 }
180
181 pub fn as_flow_sequence(&self) -> Option<YamlFlowSequence> {
182 match self.as_node()? {
183 YamlNode::FlowSequence(s) => Some(s),
184 _ => None,
185 }
186 }
187
188 pub fn is_empty(&self) -> bool {
190 self.as_node().is_none()
191 }
192
193 pub fn tag(&self) -> Option<SyntaxToken> {
196 token_of(&self.0, SyntaxKind::YAML_TAG)
197 }
198 };
199}
200
201macro_rules! ast_node {
204 ($(#[$meta:meta])* $name:ident, $kind:ident) => {
205 $(#[$meta])*
206 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
207 pub struct $name(SyntaxNode);
208
209 impl AstNode for $name {
210 type Language = PanacheLanguage;
211
212 fn can_cast(kind: SyntaxKind) -> bool {
213 kind == SyntaxKind::$kind
214 }
215
216 fn cast(syntax: SyntaxNode) -> Option<Self> {
217 Self::can_cast(syntax.kind()).then_some(Self(syntax))
218 }
219
220 fn syntax(&self) -> &SyntaxNode {
221 &self.0
222 }
223 }
224 };
225}
226
227ast_node!(
228 YamlDocument, YAML_DOCUMENT
230);
231
232impl YamlDocument {
233 pub fn block_map(&self) -> Option<YamlBlockMap> {
234 support::child(&self.0)
235 }
236
237 pub fn block_sequence(&self) -> Option<YamlBlockSequence> {
238 support::child(&self.0)
239 }
240
241 pub fn flow_map(&self) -> Option<YamlFlowMap> {
242 support::child(&self.0)
243 }
244
245 pub fn flow_sequence(&self) -> Option<YamlFlowSequence> {
246 support::child(&self.0)
247 }
248
249 pub fn scalar(&self) -> Option<YamlScalar> {
251 scalar_token(&self.0)
252 }
253
254 pub fn as_node(&self) -> Option<YamlNode> {
255 node_child(&self.0)
256 }
257}
258
259ast_node!(
260 YamlBlockMap, YAML_BLOCK_MAP
262);
263
264impl YamlBlockMap {
265 pub fn entries(&self) -> AstChildren<YamlBlockMapEntry> {
266 support::children(&self.0)
267 }
268
269 pub fn content_range(&self) -> TextRange {
271 content_text_range(&self.0)
272 }
273
274 pub fn entry(&self, key: &str) -> Option<YamlBlockMapEntry> {
276 self.entries()
277 .find(|entry| entry.key_text().as_deref() == Some(key))
278 }
279
280 pub fn value_of(&self, key: &str) -> Option<YamlBlockMapValue> {
281 self.entry(key)?.value()
282 }
283}
284
285ast_node!(
286 YamlBlockMapEntry, YAML_BLOCK_MAP_ENTRY
288);
289
290impl YamlBlockMapEntry {
291 pub fn key(&self) -> Option<YamlBlockMapKey> {
292 support::child(&self.0)
293 }
294
295 pub fn key_text(&self) -> Option<String> {
298 self.key()?.scalar().map(|s| s.value())
299 }
300
301 pub fn value(&self) -> Option<YamlBlockMapValue> {
302 support::child(&self.0)
303 }
304}
305
306ast_node!(
307 YamlBlockMapKey, YAML_BLOCK_MAP_KEY
310);
311
312impl YamlBlockMapKey {
313 pub fn scalar(&self) -> Option<YamlScalar> {
315 scalar_token(&self.0)
316 }
317}
318
319ast_node!(
320 YamlBlockMapValue, YAML_BLOCK_MAP_VALUE
323);
324
325impl YamlBlockMapValue {
326 node_projections!();
327}
328
329ast_node!(
330 YamlBlockSequence, YAML_BLOCK_SEQUENCE
332);
333
334impl YamlBlockSequence {
335 pub fn items(&self) -> AstChildren<YamlBlockSequenceItem> {
336 support::children(&self.0)
337 }
338
339 pub fn content_range(&self) -> TextRange {
341 content_text_range(&self.0)
342 }
343}
344
345ast_node!(
346 YamlBlockSequenceItem, YAML_BLOCK_SEQUENCE_ITEM
349);
350
351impl YamlBlockSequenceItem {
352 node_projections!();
353}
354
355ast_node!(
356 YamlFlowSequence, YAML_FLOW_SEQUENCE
358);
359
360impl YamlFlowSequence {
361 pub fn items(&self) -> AstChildren<YamlFlowSequenceItem> {
362 support::children(&self.0)
363 }
364
365 pub fn content_range(&self) -> TextRange {
367 content_text_range(&self.0)
368 }
369}
370
371ast_node!(
372 YamlFlowSequenceItem, YAML_FLOW_SEQUENCE_ITEM
374);
375
376impl YamlFlowSequenceItem {
377 node_projections!();
378}
379
380ast_node!(
381 YamlFlowMap, YAML_FLOW_MAP
383);
384
385impl YamlFlowMap {
386 pub fn entries(&self) -> AstChildren<YamlFlowMapEntry> {
387 support::children(&self.0)
388 }
389
390 pub fn content_range(&self) -> TextRange {
392 content_text_range(&self.0)
393 }
394
395 pub fn entry(&self, key: &str) -> Option<YamlFlowMapEntry> {
396 self.entries()
397 .find(|entry| entry.key_text().as_deref() == Some(key))
398 }
399
400 pub fn value_of(&self, key: &str) -> Option<YamlFlowMapValue> {
401 self.entry(key)?.value()
402 }
403}
404
405ast_node!(
406 YamlFlowMapEntry, YAML_FLOW_MAP_ENTRY
408);
409
410impl YamlFlowMapEntry {
411 pub fn key(&self) -> Option<YamlFlowMapKey> {
412 support::child(&self.0)
413 }
414
415 pub fn key_text(&self) -> Option<String> {
416 self.key()?.scalar().map(|s| s.value())
417 }
418
419 pub fn value(&self) -> Option<YamlFlowMapValue> {
420 support::child(&self.0)
421 }
422}
423
424ast_node!(
425 YamlFlowMapKey, YAML_FLOW_MAP_KEY
427);
428
429impl YamlFlowMapKey {
430 pub fn scalar(&self) -> Option<YamlScalar> {
431 scalar_token(&self.0)
432 }
433}
434
435ast_node!(
436 YamlFlowMapValue, YAML_FLOW_MAP_VALUE
438);
439
440impl YamlFlowMapValue {
441 node_projections!();
442}
443
444#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
447pub enum YamlScalarStyle {
448 Plain,
449 SingleQuoted,
450 DoubleQuoted,
451 Literal,
452 Folded,
453}
454
455impl YamlScalarStyle {
456 fn to_cook_style(self) -> ScalarStyle {
457 match self {
458 YamlScalarStyle::Plain => ScalarStyle::Plain,
459 YamlScalarStyle::SingleQuoted => ScalarStyle::SingleQuoted,
460 YamlScalarStyle::DoubleQuoted => ScalarStyle::DoubleQuoted,
461 YamlScalarStyle::Literal => ScalarStyle::Literal,
462 YamlScalarStyle::Folded => ScalarStyle::Folded,
463 }
464 }
465}
466
467fn detect_style(raw: &str) -> YamlScalarStyle {
468 match raw.trim_start().as_bytes().first() {
469 Some(b'\'') => YamlScalarStyle::SingleQuoted,
470 Some(b'"') => YamlScalarStyle::DoubleQuoted,
471 Some(b'|') => YamlScalarStyle::Literal,
472 Some(b'>') => YamlScalarStyle::Folded,
473 _ => YamlScalarStyle::Plain,
474 }
475}
476
477#[derive(Debug, Clone, PartialEq, Eq, Hash)]
481pub struct YamlScalar(SyntaxNode);
482
483impl YamlScalar {
484 pub fn cast(node: SyntaxNode) -> Option<Self> {
485 (node.kind() == SyntaxKind::YAML_SCALAR).then_some(Self(node))
486 }
487
488 pub fn raw(&self) -> String {
491 self.0.text().to_string()
492 }
493
494 pub fn style(&self) -> YamlScalarStyle {
495 detect_style(&self.raw())
496 }
497
498 pub fn value(&self) -> String {
502 let source = self.prefix_stripped_source();
503 cook(detect_style(&source).to_cook_style(), &source)
504 }
505
506 fn prefix_stripped_source(&self) -> String {
512 self.0
513 .children_with_tokens()
514 .filter_map(|el| el.into_token())
515 .filter(|t| t.kind() != SyntaxKind::YAML_LINE_PREFIX)
516 .map(|t| t.text().to_string())
517 .collect()
518 }
519
520 pub fn text_range(&self) -> TextRange {
521 self.0.text_range()
522 }
523
524 pub fn syntax(&self) -> &SyntaxNode {
525 &self.0
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532
533 #[test]
534 fn parse_yaml_document_descends_envelope() {
535 let doc = parse_yaml_document("title: x\n").expect("document");
536 let map = doc.block_map().expect("block map");
537 assert_eq!(map.entries().count(), 1);
538 }
539
540 #[test]
541 fn content_range_excludes_trailing_trivia() {
542 let input = "outer:\n a: 1\n b: 2\nsibling: x\n";
546 let doc = parse_yaml_document(input).expect("document");
547 let inner = doc.block_map().unwrap().value_of("outer").unwrap();
548 let map = inner.as_block_map().unwrap();
549 let slice = |r: TextRange| &input[usize::from(r.start())..usize::from(r.end())];
550 assert!(
551 slice(map.syntax().text_range()).ends_with('\n'),
552 "text_range keeps the trailing newline (lossless)"
553 );
554 assert_eq!(slice(map.content_range()), "a: 1\n b: 2");
555 }
556
557 #[test]
558 fn key_text_strips_colon() {
559 let doc = parse_yaml_document("key: value\n").expect("document");
560 let entry = doc.block_map().unwrap().entries().next().unwrap();
561 assert_eq!(entry.key_text().as_deref(), Some("key"));
562 }
563
564 #[test]
565 fn value_is_cooked() {
566 let doc = parse_yaml_document("k: 'it''s'\n").expect("document");
567 let value = doc.block_map().unwrap().value_of("k").unwrap();
568 assert_eq!(value.as_scalar().unwrap().value(), "it's");
569
570 let doc = parse_yaml_document("k: \"a\\nb\"\n").expect("document");
571 let value = doc.block_map().unwrap().value_of("k").unwrap();
572 assert_eq!(value.as_scalar().unwrap().value(), "a\nb");
573 }
574
575 #[test]
576 fn raw_preserves_quotes() {
577 let doc = parse_yaml_document("k: 'it''s'\n").expect("document");
578 let scalar = doc
579 .block_map()
580 .unwrap()
581 .value_of("k")
582 .unwrap()
583 .as_scalar()
584 .unwrap();
585 assert_eq!(scalar.raw(), "'it''s'");
586 assert_eq!(scalar.style(), YamlScalarStyle::SingleQuoted);
587 }
588
589 #[test]
590 fn value_skips_embedded_line_prefix() {
591 use crate::parser::yaml::parse_stream_with_prefix;
592
593 let tree = parse_stream_with_prefix("#| key: \"foo\n#| bar\"\n", "#|");
597 let scalar = first_document(&tree)
598 .and_then(|d| d.block_map())
599 .and_then(|m| m.value_of("key"))
600 .and_then(|v| v.as_scalar())
601 .expect("scalar value");
602 let value = scalar.value();
603 assert!(!value.contains("#|"), "prefix leaked into value: {value:?}");
604 assert_eq!(value, "foo bar");
605 assert!(
607 scalar.raw().contains("#|"),
608 "raw() must retain the prefix leaf: {:?}",
609 scalar.raw()
610 );
611 }
612
613 #[test]
614 fn scalar_text_range_is_content_relative() {
615 let input = "k: value\n";
616 let doc = parse_yaml_document(input).expect("document");
617 let scalar = doc
618 .block_map()
619 .unwrap()
620 .value_of("k")
621 .unwrap()
622 .as_scalar()
623 .unwrap();
624 let range = scalar.text_range();
625 let start: usize = range.start().into();
626 let end: usize = range.end().into();
627 assert_eq!(&input[start..end], "value");
628 }
629
630 #[test]
631 fn empty_value_has_no_scalar() {
632 let doc = parse_yaml_document("k:\n").expect("document");
633 let value = doc.block_map().unwrap().value_of("k").unwrap();
634 assert!(value.is_empty());
635 assert!(value.as_scalar().is_none());
636 }
637
638 #[test]
639 fn block_sequence_items_yield_scalars() {
640 let doc = parse_yaml_document("k:\n - a\n - b\n").expect("document");
641 let seq = doc
642 .block_map()
643 .unwrap()
644 .value_of("k")
645 .unwrap()
646 .as_block_sequence()
647 .expect("block sequence");
648 let items: Vec<String> = seq
649 .items()
650 .filter_map(|item| item.as_scalar().map(|s| s.value()))
651 .collect();
652 assert_eq!(items, vec!["a".to_string(), "b".to_string()]);
653 }
654
655 #[test]
656 fn flow_sequence_items_yield_scalars() {
657 let doc = parse_yaml_document("k: [a, b]\n").expect("document");
658 let seq = doc
659 .block_map()
660 .unwrap()
661 .value_of("k")
662 .unwrap()
663 .as_flow_sequence()
664 .expect("flow sequence");
665 let items: Vec<String> = seq
666 .items()
667 .filter_map(|item| item.as_scalar().map(|s| s.value()))
668 .collect();
669 assert_eq!(items, vec!["a".to_string(), "b".to_string()]);
670 }
671
672 #[test]
673 fn tag_token_is_exposed_and_scalar_ignores_it() {
674 let doc = parse_yaml_document("k: !expr foo\n").expect("document");
675 let value = doc.block_map().unwrap().value_of("k").unwrap();
676 assert_eq!(
677 value.tag().map(|t| t.text().to_string()),
678 Some("!expr".to_string())
679 );
680 assert_eq!(value.as_scalar().unwrap().raw(), "foo");
681 }
682
683 #[test]
684 fn quoted_key_with_colon_round_trips() {
685 let doc = parse_yaml_document("\"foo:bar\": 1\n").expect("document");
686 let entry = doc.block_map().unwrap().entries().next().unwrap();
687 assert_eq!(entry.key_text().as_deref(), Some("foo:bar"));
688 assert_eq!(entry.key().unwrap().scalar().unwrap().raw(), "\"foo:bar\"");
689 }
690
691 #[test]
692 fn parse_yaml_documents_returns_all_documents() {
693 let docs = parse_yaml_documents("a: 1\n---\nb: 2\n");
694 assert_eq!(docs.len(), 2);
695 }
696
697 #[test]
698 fn invalid_yaml_yields_no_document() {
699 assert!(parse_yaml_document("k: [\n").is_none());
700 }
701}