Skip to main content

yaml_rt_core/
fragment.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3
4use crate::{NodeId, SemanticKind, YamlDoc, YamlError, YamlScalarStyle};
5
6/// A parsed YAML value document containing exactly one root node.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct YamlFragment {
9    doc: YamlDoc,
10    root: NodeId,
11}
12
13impl YamlFragment {
14    /// Parses an owned YAML value.
15    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    /// Parses a borrowed YAML value.
33    pub fn parse(input: &str) -> Result<Self, FragmentError> {
34        Self::parse_owned(input.to_owned())
35    }
36
37    /// Returns the fragment's parsed document.
38    #[must_use]
39    pub fn document(&self) -> &YamlDoc {
40        &self.doc
41    }
42
43    /// Returns the fragment root node.
44    #[must_use]
45    pub const fn root(&self) -> NodeId {
46        self.root
47    }
48
49    /// Returns the root as minimally de-indented standalone YAML.
50    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    /// Extracts one semantic node as valid standalone YAML where possible.
255    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/// Failure to parse, validate, or render a YAML fragment.
354#[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}