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    /// The tag, in its `!!` shorthand if it is in the `tag:yaml.org,2002:`
27    /// namespace.
28    pub(crate) fn normalized_tag(&self) -> Option<String> {
29        self.tag().map(|t| crate::tag::normalize(&t).into_owned())
30    }
31
32    /// Get the value part of this tagged scalar (without the tag)
33    pub fn value(&self) -> Option<Scalar> {
34        // Find the nested SCALAR node
35        for child in self.0.children() {
36            if child.kind() == SyntaxKind::SCALAR {
37                return Scalar::cast(child);
38            }
39        }
40        None
41    }
42
43    /// Get the collection this tag is attached to, if the tagged node wraps
44    /// a mapping (e.g. `!custom` on a block mapping).
45    pub fn as_mapping(&self) -> Option<Mapping> {
46        self.0.children().find_map(Mapping::cast)
47    }
48
49    /// Get the collection this tag is attached to, if the tagged node wraps
50    /// a sequence (e.g. `!!seq` on a flow sequence).
51    pub fn as_sequence(&self) -> Option<Sequence> {
52        self.0.children().find_map(Sequence::cast)
53    }
54
55    /// Get the string value of this tagged scalar (just the value part),
56    /// with quotes stripped and escape sequences processed.
57    ///
58    /// Returns `None` if the tag is on a collection or alias.
59    pub fn as_string(&self) -> Option<String> {
60        self.value().map(|scalar| scalar.as_string())
61    }
62
63    /// Extract a set from this tagged node if it has a !!set tag on a
64    /// mapping.
65    pub fn as_set(&self) -> Option<Set> {
66        Set::cast(self.0.clone())
67    }
68
69    /// Extract ordered mapping from this tagged node if it has a !!omap tag
70    /// on a sequence.
71    ///
72    /// Returns the entries of the inner sequence as `MappingEntry` values, each
73    /// holding a single key-value pair with full CST fidelity.
74    pub fn as_ordered_mapping(&self) -> Option<Vec<MappingEntry>> {
75        if self.normalized_tag().as_deref() != Some(crate::tag::OMAP) {
76            return None;
77        }
78        self.extract_mapping_entries()
79    }
80
81    /// Extract pairs from this tagged node if it has a !!pairs tag on a
82    /// sequence.
83    ///
84    /// Returns the entries of the inner sequence as `MappingEntry` values.
85    /// Unlike `!!omap`, duplicate keys are allowed.
86    pub fn as_pairs(&self) -> Option<Vec<MappingEntry>> {
87        if self.normalized_tag().as_deref() != Some(crate::tag::PAIRS) {
88            return None;
89        }
90        self.extract_mapping_entries()
91    }
92
93    /// Shared helper: iterate over the inner sequence and collect the first
94    /// `MappingEntry` from each single-key mapping item.
95    fn extract_mapping_entries(&self) -> Option<Vec<MappingEntry>> {
96        let sequence = self.as_sequence()?;
97        Some(
98            sequence
99                .items()
100                .filter_map(Mapping::cast)
101                .filter_map(|mapping| mapping.entries().next())
102                .collect(),
103        )
104    }
105}
106
107/// The indentation of the first non-blank line after `node`'s first line
108/// break, or `None` if it fits on one line.
109fn continuation_indent(node: &SyntaxNode) -> Option<usize> {
110    let mut tokens = node
111        .descendants_with_tokens()
112        .filter_map(|e| e.into_token())
113        .skip_while(|t| t.kind() != SyntaxKind::NEWLINE);
114    tokens.next()?;
115    let first = tokens.find(|t| t.kind() != SyntaxKind::NEWLINE);
116    Some(
117        first
118            .filter(|t| matches!(t.kind(), SyntaxKind::INDENT | SyntaxKind::WHITESPACE))
119            .map_or(0, |t| t.text().len()),
120    )
121}
122
123impl TaggedNode {
124    /// A copy of this node with its plain content double quoted, if a flow
125    /// collection would end that content at a flow indicator in it.
126    fn flow_quoted(&self) -> Option<SyntaxNode> {
127        let scalar = self.0.children().find_map(Scalar::cast)?;
128        if !scalar.needs_flow_quotes() {
129            return None;
130        }
131        let mut builder = rowan::GreenNodeBuilder::new();
132        builder.start_node(SyntaxKind::ROOT.into());
133        scalar.build_content(&mut builder, 0, true);
134        builder.finish_node();
135        let quoted = builder.finish().children().next()?.into_node()?.to_owned();
136        let green = self
137            .0
138            .green()
139            .replace_child(scalar.syntax().index(), quoted.into());
140        Some(SyntaxNode::new_root(green))
141    }
142}
143
144impl AsYaml for TaggedNode {
145    fn as_node(&self) -> Option<&SyntaxNode> {
146        Some(&self.0)
147    }
148
149    fn kind(&self) -> YamlKind {
150        self.tag().map_or(YamlKind::Scalar, |t| {
151            YamlKind::Tagged(std::borrow::Cow::Owned(t))
152        })
153    }
154
155    /// Content on lines after the tag (a block collection, say) is shifted
156    /// to start at column `indent`, as for an untagged collection. Content
157    /// starting on the tag line (a flow collection) is shifted like an
158    /// untagged flow collection, keeping its continuation lines' offset
159    /// from `indent`.
160    fn build_content(
161        &self,
162        builder: &mut rowan::GreenNodeBuilder,
163        indent: usize,
164        flow_context: bool,
165    ) -> bool {
166        let quoted = flow_context.then(|| self.flow_quoted()).flatten();
167        let node = quoted.as_ref().unwrap_or(&self.0);
168        let content_on_tag_line = self
169            .0
170            .children_with_tokens()
171            .take_while(|c| c.as_node().is_none())
172            .all(|c| c.kind() != SyntaxKind::NEWLINE);
173        let delta = if content_on_tag_line {
174            let delta = indent as isize - crate::column::line_indent(&self.0) as isize;
175            crate::as_yaml::clear_parent_column(&self.0, indent, delta)
176        } else {
177            continuation_indent(&self.0).map_or(0, |base| indent as isize - base as isize)
178        };
179        builder.start_node(SyntaxKind::TAGGED_NODE.into());
180        let ends_with_newline = crate::as_yaml::copy_node_content_reindent(builder, node, delta);
181        builder.finish_node();
182        ends_with_newline
183    }
184
185    fn is_inline(&self) -> bool {
186        crate::yaml::ValueNode::is_inline(self)
187    }
188}