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