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