Skip to main content

ephemeris_documents/yaml/source/read/
opaque.rs

1//! Mappings kept as raw text, and the block rewrite of flow mappings.
2
3use yaml_rt::{CollectionStyle, NodeId};
4
5use super::fit::Parsed;
6use super::value::Site;
7use super::{Bounds, Mapping, Reader, Text, Tree, Value};
8use crate::error::{DocumentError, YamlError};
9use crate::yaml::source::Opaque;
10
11impl Reader<'_> {
12    /// A mapping the tree cannot edit, kept as raw text. A flow mapping with
13    /// entries also gets the block rewrite that edits use.
14    pub(super) fn opaque(
15        &self,
16        site: Site,
17        node: NodeId,
18        span: Bounds,
19    ) -> Result<Parsed<Value>, DocumentError> {
20        let Parsed { value: leaf, end } = self.leaf(site.before(), node, span)?;
21        let text = leaf.into_text();
22        let opaque = match self.flow_rewrite(site, node, span, end)? {
23            Some(block) => Opaque::flow(text, block),
24            None => Opaque::raw(text),
25        };
26        Ok(Parsed {
27            value: Value::Opaque(opaque),
28            end,
29        })
30    }
31
32    /// Whether `node` is a flow mapping with entries.
33    pub(super) fn is_flow_with_entries(&self, node: NodeId) -> bool {
34        self.mapping_style(node) == Some(CollectionStyle::Flow)
35            && self.document.mapping_entries(node).next().is_some()
36    }
37
38    /// The flow mapping `node` as a block mapping, or `None` when it has a
39    /// comment, a key that is not a scalar, or a value that spans lines.
40    /// `end` is where the line of the closing brace ends.
41    fn flow_rewrite(
42        &self,
43        site: Site,
44        node: NodeId,
45        span: Bounds,
46        end: usize,
47    ) -> Result<Option<Mapping>, DocumentError> {
48        if !self.is_flow_with_entries(node) {
49            return Ok(None);
50        }
51        let text = self.slice(span.start, span.end)?;
52        let Some(brace) = text.find('{').and_then(|open| span.start.checked_add(open)) else {
53            return Ok(None);
54        };
55        if Text::comment_start(text).is_some() {
56            return Ok(None);
57        }
58        let indent = site.flow_indent(self.step);
59        let Some(source) = self.block_source(node, 0)? else {
60            return Ok(None);
61        };
62        let tree = match Tree::parse(&source) {
63            Ok(tree) => tree,
64            Err(error @ DocumentError::Yaml(YamlError::DuplicateKey { path: _, key: _ })) => {
65                return Err(error);
66            }
67            Err(_) => return Ok(None),
68        };
69        let before = self.slice(site.before(), brace)?;
70        let after = Text::from(self.slice(span.end, end)?);
71        let kept = before.trim_end();
72        let owned = indent > 0 || before.starts_with([' ', '\t']) || !kept.is_empty();
73        let mut opening = Text::from(format!("{kept}{after}"));
74        if owned {
75            opening.terminate();
76        } else if after.as_str().trim().is_empty() {
77            opening = Text::default();
78        }
79        Ok(tree
80            .into_mapping()
81            .map(|mapping| mapping.reopened(opening, indent, self.step)))
82    }
83
84    fn push_line(out: &mut String, pad: &str, key: &str, rest: &str) {
85        out.push_str(pad);
86        out.push_str(key);
87        if !rest.is_empty() {
88            out.push(' ');
89            out.push_str(rest);
90        }
91        out.push('\n');
92    }
93
94    /// The entries of the flow mapping `node` written as block source with
95    /// keys at `indent`. A document cannot start indented, so the caller
96    /// moves the result to where the mapping sits.
97    fn block_source(&self, node: NodeId, indent: usize) -> Result<Option<String>, DocumentError> {
98        let pad = " ".repeat(indent);
99        let mut out = String::new();
100        for (key, value) in self.document.mapping_entries(node) {
101            if self.scalar(key).is_none() {
102                return Ok(None);
103            }
104            let key_span = self.range(key)?;
105            let Some(colon_end) = self.colon_end(key_span.end) else {
106                return Ok(None);
107            };
108            let key_text = self.slice(key_span.start, colon_end)?;
109            let span = self.extent(value)?;
110            if key_text.contains('\n') || (!span.is_empty() && span.start < colon_end) {
111                return Ok(None);
112            }
113            let value_text = if span.is_empty() {
114                ""
115            } else {
116                self.slice(colon_end, span.end)?
117            };
118            if self.is_flow_with_entries(value) {
119                let Some(open) = value_text.find('{') else {
120                    return Ok(None);
121                };
122                let properties = value_text.get(..open).unwrap_or_default().trim();
123                let nested = indent.saturating_add(self.step);
124                let Some(inner) = self.block_source(value, nested)? else {
125                    return Ok(None);
126                };
127                Self::push_line(&mut out, &pad, key_text, properties);
128                out.push_str(&inner);
129            } else if value_text.contains('\n') {
130                return Ok(None);
131            } else {
132                Self::push_line(&mut out, &pad, key_text, value_text.trim());
133            }
134        }
135        Ok(Some(out))
136    }
137}