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