1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3
4use crate::{NodeId, SemanticKind, YamlDoc, YamlError, YamlScalarStyle};
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct YamlFragment {
9 doc: YamlDoc,
10 root: NodeId,
11}
12
13impl YamlFragment {
14 pub fn parse_owned(input: String) -> Result<Self, FragmentError> {
16 let doc = YamlDoc::parse_owned(input).map_err(FragmentError::from)?;
17 if doc.document_count() != 1 {
18 return Err(FragmentError::new(format!(
19 "a YAML value must contain exactly one document, found {}",
20 doc.document_count()
21 )));
22 }
23 let root = doc
24 .document_root(0)
25 .map_err(FragmentError::from)?
26 .ok_or_else(|| FragmentError::new("a YAML value must contain one root node"))?;
27 let fragment = Self { doc, root };
28 fragment.validate_alias_scope()?;
29 Ok(fragment)
30 }
31
32 pub fn parse(input: &str) -> Result<Self, FragmentError> {
34 Self::parse_owned(input.to_owned())
35 }
36
37 #[must_use]
39 pub fn document(&self) -> &YamlDoc {
40 &self.doc
41 }
42
43 #[must_use]
45 pub const fn root(&self) -> NodeId {
46 self.root
47 }
48
49 pub fn to_yaml(&self) -> Result<String, FragmentError> {
51 self.doc
52 .extract_node(self.root)
53 .map_err(FragmentError::from)
54 }
55
56 pub(crate) fn contains_anchor(&self) -> bool {
57 self.subtree_nodes()
58 .any(|node| self.doc.anchor(node).is_some())
59 }
60
61 pub(crate) fn from_document_node(doc: &YamlDoc, root: NodeId) -> Result<Self, FragmentError> {
62 doc.node(root)
63 .ok_or_else(|| FragmentError::new("fragment source node is missing"))?;
64 Ok(Self {
65 doc: doc.clone(),
66 root,
67 })
68 }
69
70 pub(crate) fn prepared(&self, target: &YamlDoc) -> Result<Self, FragmentError> {
71 self.prepared_with_anchor_exemption(target, None)
72 }
73
74 pub(crate) fn prepared_for_replacement(
75 &self,
76 target: &YamlDoc,
77 replaced: NodeId,
78 ) -> Result<Self, FragmentError> {
79 self.prepared_with_anchor_exemption(target, target.anchor(replaced))
80 }
81
82 fn prepared_with_anchor_exemption(
83 &self,
84 target: &YamlDoc,
85 exempt_anchor: Option<&str>,
86 ) -> Result<Self, FragmentError> {
87 let mut used = target.anchor_names();
88 if let Some(anchor) = exempt_anchor {
89 used.remove(anchor);
90 }
91 let mut renamed = BTreeMap::new();
92 for node in self.subtree_nodes() {
93 let Some(name) = self.doc.anchor(node) else {
94 continue;
95 };
96 if used.insert(name.to_owned()) {
97 continue;
98 }
99 let mut suffix = 1_u64;
100 let replacement = loop {
101 let candidate = format!("{name}_{suffix}");
102 if used.insert(candidate.clone()) {
103 break candidate;
104 }
105 suffix = suffix.saturating_add(1);
106 };
107 renamed.insert(name.to_owned(), replacement);
108 }
109 if renamed.is_empty() {
110 return Ok(self.clone());
111 }
112
113 let mut doc = self.doc.clone();
114 let nodes = self.subtree_nodes().collect::<Vec<_>>();
115 for node in nodes {
116 let Some(properties) = doc.semantics.properties(node) else {
117 continue;
118 };
119 if let Some(span) = properties.anchor {
120 let old = doc.source.slice(span);
121 if let Some(new) = renamed.get(old) {
122 doc.queue_edit(span, new.clone())
123 .map_err(FragmentError::from)?;
124 }
125 }
126 if let Some(span) = properties.alias {
127 let old = doc.source.slice(span);
128 if let Some(new) = renamed.get(old) {
129 doc.queue_edit(span, new.clone())
130 .map_err(FragmentError::from)?;
131 }
132 }
133 }
134 doc.commit_edits().map_err(FragmentError::from)?;
135 let root = doc
136 .document_root(0)
137 .map_err(FragmentError::from)?
138 .ok_or_else(|| FragmentError::new("prepared fragment lost its root node"))?;
139 Ok(Self { doc, root })
140 }
141
142 pub(crate) fn render_flow(&self, target: &YamlDoc) -> Result<String, FragmentError> {
143 let prepared = self.prepared(target)?;
144 prepared.render_node_flow(prepared.root, 0)
145 }
146
147 pub(crate) fn render_flow_for_replacement(
148 &self,
149 target: &YamlDoc,
150 replaced: NodeId,
151 ) -> Result<String, FragmentError> {
152 let prepared = self.prepared_for_replacement(target, replaced)?;
153 prepared.render_node_flow(prepared.root, 0)
154 }
155
156 fn render_node_flow(&self, node: NodeId, depth: usize) -> Result<String, FragmentError> {
157 if depth > 1024 {
158 return Err(FragmentError::new(
159 "fragment rendering recursion limit exceeded",
160 ));
161 }
162 match self.doc.semantic_kind(node) {
163 Some(SemanticKind::Alias) => Ok(format!(
164 "*{}",
165 self.doc.alias_name(node).unwrap_or_default()
166 )),
167 Some(SemanticKind::Scalar { style }) => {
168 let prefix = self.property_prefix(node);
169 if matches!(style, YamlScalarStyle::Literal | YamlScalarStyle::Folded)
170 || self
171 .doc
172 .node(node)
173 .is_some_and(|node| node.span().is_empty())
174 {
175 let value = self.doc.scalar_value(node).map_err(FragmentError::from)?;
176 Ok(format!("{prefix}{}", quote_string(&value)))
177 } else {
178 let source = self.doc.extract_node(node).map_err(FragmentError::from)?;
179 if source.contains(['\n', '\r'])
180 || style == YamlScalarStyle::Plain
181 && source.contains(['[', ']', '{', '}', ','])
182 {
183 let value = self.doc.scalar_value(node).map_err(FragmentError::from)?;
184 Ok(format!("{prefix}{}", quote_string(&value)))
185 } else {
186 Ok(source)
187 }
188 }
189 }
190 Some(SemanticKind::Sequence { .. }) => {
191 let mut output = self.property_prefix(node);
192 output.push('[');
193 for (index, item) in self.doc.sequence_items(node).enumerate() {
194 if index > 0 {
195 output.push_str(", ");
196 }
197 output.push_str(&self.render_node_flow(item, depth + 1)?);
198 }
199 output.push(']');
200 Ok(output)
201 }
202 Some(SemanticKind::Mapping { .. }) => {
203 let mut output = self.property_prefix(node);
204 output.push('{');
205 for (index, (key, value)) in self.doc.mapping_entries(node).enumerate() {
206 if index > 0 {
207 output.push_str(", ");
208 }
209 output.push_str(&self.render_node_flow(key, depth + 1)?);
210 output.push_str(": ");
211 output.push_str(&self.render_node_flow(value, depth + 1)?);
212 }
213 output.push('}');
214 Ok(output)
215 }
216 Some(SemanticKind::Document) | None => {
217 Err(FragmentError::new("cannot render unknown YAML node"))
218 }
219 }
220 }
221
222 fn property_prefix(&self, node: NodeId) -> String {
223 let mut prefix = String::new();
224 if let Some(tag) = self.doc.raw_tag(node) {
225 prefix.push_str(tag);
226 prefix.push(' ');
227 }
228 if let Some(anchor) = self.doc.anchor(node) {
229 prefix.push('&');
230 prefix.push_str(anchor);
231 prefix.push(' ');
232 }
233 prefix
234 }
235
236 fn subtree_nodes(&self) -> impl Iterator<Item = NodeId> + '_ {
237 let span = self.doc.node(self.root).map(|node| node.span());
238 self.doc
239 .nodes
240 .iter()
241 .enumerate()
242 .map(|(index, _)| NodeId::from_usize(index))
243 .filter(move |node| {
244 let Some(root_span) = span else {
245 return false;
246 };
247 self.doc.node(*node).is_some_and(|node| {
248 node.span().start >= root_span.start && node.span().end <= root_span.end
249 })
250 })
251 }
252
253 fn validate_alias_scope(&self) -> Result<(), FragmentError> {
254 let root_span = self
255 .doc
256 .node(self.root)
257 .map(|node| node.span())
258 .ok_or_else(|| FragmentError::new("fragment root node is missing"))?;
259 for node in self.subtree_nodes() {
260 if !matches!(self.doc.semantic_kind(node), Some(SemanticKind::Alias)) {
261 continue;
262 }
263 let target = self.doc.resolve_alias(node).ok_or_else(|| {
264 FragmentError::new(format!(
265 "unresolved value alias `*{}`",
266 self.doc.alias_name(node).unwrap_or_default()
267 ))
268 })?;
269 let target_span = self
270 .doc
271 .node(target)
272 .map(|node| node.span())
273 .ok_or_else(|| FragmentError::new("alias target is missing"))?;
274 if target_span.start < root_span.start || target_span.end > root_span.end {
275 return Err(FragmentError::new(
276 "a value alias cannot reference a node outside the value root",
277 ));
278 }
279 }
280 Ok(())
281 }
282}
283
284impl YamlDoc {
285 pub fn extract_node(&self, node: NodeId) -> Result<String, YamlError> {
287 let node = self.expect_node(node)?;
288 let source = self.source.slice(node.span);
289 let line_start = self.source.as_str()[..node.span.start as usize]
290 .rfind(['\n', '\r'])
291 .map_or(0, |index| index + 1);
292 let base_indent = self.source.as_str()[line_start..node.span.start as usize]
293 .bytes()
294 .take_while(|byte| *byte == b' ')
295 .count();
296 Ok(deindent_continuation_lines(source, base_indent))
297 }
298
299 pub(crate) fn anchor_names(&self) -> BTreeSet<String> {
300 self.nodes
301 .iter()
302 .enumerate()
303 .filter_map(|(index, _)| self.anchor(NodeId::from_usize(index)).map(str::to_owned))
304 .collect()
305 }
306}
307
308fn deindent_continuation_lines(source: &str, indent: usize) -> String {
309 if indent == 0 || !source.contains(['\n', '\r']) {
310 return source.to_owned();
311 }
312 let bytes = source.as_bytes();
313 let mut output = String::with_capacity(source.len());
314 let mut position = 0;
315 let mut first = true;
316 while position < bytes.len() {
317 if !first {
318 let mut removed = 0;
319 while removed < indent && bytes.get(position) == Some(&b' ') {
320 position += 1;
321 removed += 1;
322 }
323 }
324 first = false;
325 let line_end = source[position..]
326 .find(['\n', '\r'])
327 .map_or(source.len(), |offset| position + offset);
328 output.push_str(&source[position..line_end]);
329 position = line_end;
330 if bytes.get(position) == Some(&b'\r') {
331 output.push('\r');
332 position += 1;
333 if bytes.get(position) == Some(&b'\n') {
334 output.push('\n');
335 position += 1;
336 }
337 } else if bytes.get(position) == Some(&b'\n') {
338 output.push('\n');
339 position += 1;
340 }
341 }
342 output
343}
344
345pub(crate) fn indent_text(source: &str, indent: usize) -> String {
346 if indent == 0 || source.is_empty() {
347 return source.to_owned();
348 }
349 let prefix = " ".repeat(indent);
350 let mut output = String::with_capacity(source.len() + prefix.len());
351 let mut at_line_start = true;
352 for character in source.chars() {
353 if at_line_start && !matches!(character, '\r' | '\n') {
354 output.push_str(&prefix);
355 at_line_start = false;
356 }
357 output.push(character);
358 if character == '\n' || character == '\r' {
359 at_line_start = true;
360 }
361 }
362 output
363}
364
365pub(crate) fn quote_string(value: &str) -> String {
366 let mut output = String::from("\"");
367 for character in value.chars() {
368 match character {
369 '"' => output.push_str("\\\""),
370 '\\' => output.push_str("\\\\"),
371 '\n' => output.push_str("\\n"),
372 '\r' => output.push_str("\\r"),
373 '\t' => output.push_str("\\t"),
374 character if character.is_control() => {
375 output.push_str(&format!("\\u{:04X}", u32::from(character)));
376 }
377 character => output.push(character),
378 }
379 }
380 output.push('"');
381 output
382}
383
384#[derive(Debug, Clone, PartialEq, Eq)]
386pub struct FragmentError {
387 message: String,
388}
389
390impl FragmentError {
391 pub(crate) fn new(message: impl Into<String>) -> Self {
392 Self {
393 message: message.into(),
394 }
395 }
396}
397
398impl fmt::Display for FragmentError {
399 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
400 formatter.write_str(&self.message)
401 }
402}
403
404impl std::error::Error for FragmentError {}
405
406impl From<YamlError> for FragmentError {
407 fn from(error: YamlError) -> Self {
408 Self::new(error.to_string())
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 #[test]
417 fn fragment_requires_one_nonempty_document() {
418 assert!(YamlFragment::parse("").is_err());
419 assert!(YamlFragment::parse("--- a\n--- b\n").is_err());
420 assert!(YamlFragment::parse("[a, b]").is_ok());
421 }
422
423 #[test]
424 fn extraction_deindents_nested_block_nodes() {
425 let doc = YamlDoc::parse("outer:\n one: 1\n two:\n - a\n - b\n").unwrap();
426 let node = doc
427 .get_mapping_value(doc.document_root_mapping(0).unwrap(), "outer")
428 .unwrap()
429 .unwrap();
430 assert_eq!(
431 doc.extract_node(node).unwrap(),
432 "one: 1\ntwo:\n - a\n - b"
433 );
434 }
435
436 #[test]
437 fn rejects_unresolved_value_aliases() {
438 assert!(YamlFragment::parse("*outside").is_err());
439 }
440
441 #[test]
442 fn flow_rendering_normalizes_block_collections_only() {
443 let fragment = YamlFragment::parse("one:\n - a\n - b\n").unwrap();
444 let target = YamlDoc::parse("target: []\n").unwrap();
445 assert_eq!(fragment.render_flow(&target).unwrap(), "{one: [a, b]}");
446 }
447}