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        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    /// Extracts one semantic node as valid standalone YAML where possible.
286    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/// Failure to parse, validate, or render a YAML fragment.
385#[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}