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