Skip to main content

yaml_edit/nodes/
tagged_node.rs

1use super::{Lang, SyntaxNode};
2use crate::as_yaml::{AsYaml, YamlKind};
3use crate::lex::SyntaxKind;
4use crate::yaml::{Mapping, MappingEntry, Scalar, Sequence, Set};
5use rowan::ast::AstNode;
6
7ast_node!(
8    TaggedNode,
9    TAGGED_NODE,
10    "A YAML tagged scalar (tag + value)"
11);
12impl TaggedNode {
13    /// Get the tag part of this tagged scalar (e.g., "!custom" from "!custom value")
14    pub fn tag(&self) -> Option<String> {
15        // Find the tag token in the children
16        for child in self.0.children_with_tokens() {
17            if let rowan::NodeOrToken::Token(token) = child {
18                if token.kind() == SyntaxKind::TAG {
19                    return Some(token.text().to_string());
20                }
21            }
22        }
23        None
24    }
25
26    /// Get the value part of this tagged scalar (without the tag)
27    pub fn value(&self) -> Option<Scalar> {
28        // Find the nested SCALAR node
29        for child in self.0.children() {
30            if child.kind() == SyntaxKind::SCALAR {
31                return Scalar::cast(child);
32            }
33        }
34        None
35    }
36
37    /// Get the string value of this tagged scalar (just the value part),
38    /// with quotes stripped and escape sequences processed.
39    pub fn as_string(&self) -> Option<String> {
40        if let Some(scalar) = self.value() {
41            Some(scalar.as_string())
42        } else {
43            // Handle cases where the value might be nested deeper
44            self.extract_deepest_string_value()
45        }
46    }
47
48    /// Extract the deepest string value, handling nested tag structures
49    fn extract_deepest_string_value(&self) -> Option<String> {
50        Self::find_string_token_recursive(&self.0)
51    }
52
53    /// Recursively search for the first STRING token in the tree
54    fn find_string_token_recursive(node: &rowan::SyntaxNode<crate::yaml::Lang>) -> Option<String> {
55        // Check tokens first
56        for child in node.children_with_tokens() {
57            if let rowan::NodeOrToken::Token(token) = child {
58                if token.kind() == SyntaxKind::STRING {
59                    return Some(token.text().to_string());
60                }
61            }
62        }
63
64        // Then check child nodes recursively
65        for child in node.children() {
66            if let Some(result) = Self::find_string_token_recursive(&child) {
67                return Some(result);
68            }
69        }
70
71        None
72    }
73
74    /// Extract a set from this tagged scalar if it has a !!set tag
75    pub fn as_set(&self) -> Option<Set> {
76        Set::cast(self.0.clone())
77    }
78
79    /// Extract ordered mapping from this tagged scalar if it has a !!omap tag.
80    ///
81    /// Returns the entries of the inner sequence as `MappingEntry` values, each
82    /// holding a single key-value pair with full CST fidelity.
83    pub fn as_ordered_mapping(&self) -> Option<Vec<MappingEntry>> {
84        if self.tag().as_deref() != Some("!!omap") {
85            return None;
86        }
87        Some(self.extract_mapping_entries())
88    }
89
90    /// Extract pairs from this tagged scalar if it has a !!pairs tag.
91    ///
92    /// Returns the entries of the inner sequence as `MappingEntry` values.
93    /// Unlike `!!omap`, duplicate keys are allowed.
94    pub fn as_pairs(&self) -> Option<Vec<MappingEntry>> {
95        if self.tag().as_deref() != Some("!!pairs") {
96            return None;
97        }
98        Some(self.extract_mapping_entries())
99    }
100
101    /// Shared helper: iterate over the inner sequence and collect the first
102    /// `MappingEntry` from each single-key mapping item.
103    fn extract_mapping_entries(&self) -> Vec<MappingEntry> {
104        let mut entries = Vec::new();
105        for child in self.0.children() {
106            if let Some(sequence) = Sequence::cast(child) {
107                for item in sequence.items() {
108                    if let Some(mapping) = Mapping::cast(item) {
109                        if let Some(entry) = mapping.entries().next() {
110                            entries.push(entry);
111                        }
112                    }
113                }
114                break;
115            }
116        }
117        entries
118    }
119}
120
121impl AsYaml for TaggedNode {
122    fn as_node(&self) -> Option<&SyntaxNode> {
123        Some(&self.0)
124    }
125
126    fn kind(&self) -> YamlKind {
127        self.tag()
128            .map(|t| YamlKind::Tagged(std::borrow::Cow::Owned(t)))
129            .unwrap_or(YamlKind::Scalar)
130    }
131
132    fn build_content(
133        &self,
134        builder: &mut rowan::GreenNodeBuilder,
135        _indent: usize,
136        _flow_context: bool,
137    ) -> bool {
138        crate::as_yaml::copy_node_content(builder, &self.0);
139        false
140    }
141
142    fn is_inline(&self) -> bool {
143        // Tagged scalars are always inline (they appear on the same line as their key)
144        true
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use std::str::FromStr;
151
152    use rowan::ast::AstNode;
153
154    use crate::YamlFile;
155
156    #[test]
157    fn test_tagged_node_as_string_plain() {
158        let yaml = YamlFile::from_str("key: !custom hello").unwrap();
159        let doc = yaml.documents().next().unwrap();
160        let mapping = doc.as_mapping().unwrap();
161        let val = mapping.get_node("key").unwrap();
162        let tagged = crate::yaml::TaggedNode::cast(val).unwrap();
163        assert_eq!(tagged.as_string(), Some("hello".to_string()));
164    }
165
166    #[test]
167    fn test_tagged_node_as_string_double_quoted() {
168        // Without the fix, .value() returned the raw text including quotes.
169        let yaml = YamlFile::from_str(r#"key: !custom "hello world""#).unwrap();
170        let doc = yaml.documents().next().unwrap();
171        let mapping = doc.as_mapping().unwrap();
172        let val = mapping.get_node("key").unwrap();
173        let tagged = crate::yaml::TaggedNode::cast(val).unwrap();
174        assert_eq!(tagged.as_string(), Some("hello world".to_string()));
175    }
176}