Skip to main content

ephemeris_documents/yaml/source/
read.rs

1//! Reading the tree from `yaml-rt` spans.
2
3mod bounds;
4mod fit;
5mod opaque;
6mod value;
7
8use std::borrow::Cow;
9
10use yaml_rt::{CollectionStyle, NodeId, SemanticKind, Span, YamlDoc, YamlScalarStyle};
11
12use self::bounds::Bounds;
13use self::fit::{Collected, Parsed};
14use self::value::Site;
15use super::leaf::Leaf;
16use super::mapping::{Entry, Mapping};
17use super::opaque::Opaque;
18use super::text::Text;
19use super::{Tree, Value};
20use crate::error::{DocumentError, InternalError};
21use crate::yaml::CANONICAL_STEP;
22
23/// Reads a document by the byte spans `yaml-rt` reports.
24///
25/// A mapping that does not fit the tree, such as one with explicit or
26/// complex keys, is kept as raw text instead.
27pub(super) struct Reader<'a> {
28    text: &'a str,
29    document: &'a YamlDoc,
30    step: usize,
31}
32
33/// The entries of a block mapping.
34struct Block {
35    entries: Vec<Entry>,
36    indent: usize,
37}
38
39impl<'a> Reader<'a> {
40    pub(super) fn new(text: &'a str, document: &'a YamlDoc) -> Self {
41        Self {
42            text,
43            document,
44            step: CANONICAL_STEP,
45        }
46        .with_detected_step()
47    }
48
49    pub(super) fn tree(&self) -> Result<Tree, DocumentError> {
50        let root = match self.document.document_count() {
51            0 => None,
52            _ => self
53                .document
54                .document_root(0)
55                .map_err(DocumentError::yaml_parse)?,
56        };
57        let Some(root) = root else {
58            let empty = Mapping::block(Text::from(self.text), Vec::new(), 0, self.step);
59            return Ok(Tree::new(
60                Text::default(),
61                Value::Mapping(empty),
62                Text::default(),
63            ));
64        };
65        let span = self.extent(root)?;
66        let line = self.line_start(span.start);
67        let style = self.mapping_style(root);
68        let site = Site::Root { line };
69        let editable = self.mapping(root, span, site)?;
70        if self.is_flow_with_entries(root) {
71            let Parsed { value, end } = self.opaque(site, root, span)?;
72            return Ok(Tree::new(
73                Text::from(self.slice(0, line)?),
74                value,
75                Text::from(self.slice(end, self.text.len())?),
76            ));
77        }
78        if let (
79            Some(Parsed {
80                value: mut mapping,
81                end,
82            }),
83            Some(style),
84        ) = (editable, style)
85        {
86            let header = match style {
87                CollectionStyle::Block => mapping.take_header(),
88                CollectionStyle::Flow => Text::from(self.slice(0, line)?),
89            };
90            let footer = Text::from(self.slice(end, self.text.len())?);
91            return Ok(Tree::new(header, Value::Mapping(mapping), footer));
92        }
93        let text = Text::from(self.slice(line, self.text.len())?);
94        let node = if style.is_some() {
95            Value::Opaque(Opaque::raw(text))
96        } else {
97            Value::Leaf(Leaf::raw(text))
98        };
99        Ok(Tree::new(
100            Text::from(self.slice(0, line)?),
101            node,
102            Text::default(),
103        ))
104    }
105
106    /// The entries of a block mapping, with the first one's leading text
107    /// starting at `from`. A mapping that does not fit the tree stays raw.
108    /// A key that appears twice is an error.
109    fn block(&self, mapping: NodeId, from: usize) -> Result<Option<Parsed<Block>>, DocumentError> {
110        let mut entries = Collected::default();
111        let mut indent = None;
112        let mut cursor = from;
113        for (key, value) in self.document.mapping_entries(mapping) {
114            let Some(parsed) = self.entry(key, value, cursor)? else {
115                return Ok(None);
116            };
117            let column = parsed.value.indent();
118            if *indent.get_or_insert(column) != column {
119                return Ok(None);
120            }
121            entries.reject_duplicate(parsed.value.key())?;
122            cursor = parsed.end;
123            entries.push(parsed.value);
124        }
125        Ok(indent.map(|indent| Parsed {
126            value: Block {
127                entries: entries.into_vec(),
128                indent,
129            },
130            end: cursor,
131        }))
132    }
133
134    fn entry(
135        &self,
136        key: NodeId,
137        value: NodeId,
138        cursor: usize,
139    ) -> Result<Option<Parsed<Entry>>, DocumentError> {
140        let Some(name) = self.scalar(key) else {
141            return Ok(None);
142        };
143        let key_span = self.range(key)?;
144        let line = self.line_start(key_span.start);
145        if line < cursor
146            || self
147                .slice(line, key_span.start)?
148                .bytes()
149                .any(|byte| byte != b' ')
150        {
151            return Ok(None);
152        }
153        let Some(colon_end) = self.colon_end(key_span.end) else {
154            return Ok(None);
155        };
156        let indent = key_span.start.saturating_sub(line);
157        let span = self.extent(value)?;
158        let Parsed { value, end } = self.value(&name, value, span, colon_end, indent)?;
159        Ok(Some(Parsed {
160            value: Entry::new(
161                Text::from(self.slice(cursor, line)?),
162                name,
163                Text::from(self.slice(line, colon_end)?),
164                value,
165            ),
166            end,
167        }))
168    }
169
170    /// A value kept as raw text. What follows it on its last line, such as
171    /// a comment, goes into the tail.
172    fn leaf(
173        &self,
174        colon_end: usize,
175        value: NodeId,
176        span: Bounds,
177    ) -> Result<Parsed<Leaf>, DocumentError> {
178        let anchor = if span.is_empty() {
179            colon_end
180        } else {
181            span.end.max(colon_end)
182        };
183        let end = self.line_end(anchor);
184        let split = self.comment_start(value, span, anchor);
185        let leaf = Leaf::new(
186            Text::from(self.slice(colon_end, split)?),
187            Text::from(self.slice(split, end)?),
188            self.scalar(value).filter(|_| !span.is_empty()),
189        );
190        Ok(Parsed { value: leaf, end })
191    }
192
193    /// The value of a scalar node, with quotes removed and escapes resolved.
194    fn scalar(&self, node: NodeId) -> Option<String> {
195        matches!(
196            self.document.semantic_kind(node),
197            Some(SemanticKind::Scalar { style: _ })
198        )
199        .then(|| self.document.scalar_value(node).ok())
200        .flatten()
201        .map(Cow::into_owned)
202    }
203
204    /// Where the whitespace before a trailing comment starts, or `fallback`
205    /// when there is no comment inside the value's span.
206    ///
207    /// `yaml-rt` extends the span of a plain scalar over a comment that
208    /// follows it. Other scalars end before it.
209    fn comment_start(&self, value: NodeId, span: Bounds, fallback: usize) -> usize {
210        let plain = matches!(
211            self.document.semantic_kind(value),
212            Some(SemanticKind::Scalar {
213                style: YamlScalarStyle::Plain
214            })
215        );
216        let Some(text) = self.text.get(span.start..span.end).filter(|_| plain) else {
217            return fallback;
218        };
219        Text::comment_start(text).map_or(fallback, |offset| span.start.saturating_add(offset))
220    }
221
222    /// The offset of the `{` of a flow mapping with nothing but whitespace
223    /// between its braces, which ends its node.
224    fn empty_braces(&self, node: NodeId, span: Bounds) -> Option<usize> {
225        if self.document.mapping_entries(node).next().is_some() {
226            return None;
227        }
228        let inner = self.text.get(span.start..span.end)?.strip_suffix('}')?;
229        let open = inner.rfind('{')?;
230        inner
231            .get(open.checked_add(1)?..)?
232            .chars()
233            .all(char::is_whitespace)
234            .then_some(span.start.checked_add(open)?)
235    }
236
237    /// The offset after the colon that follows a key.
238    fn colon_end(&self, key_end: usize) -> Option<usize> {
239        let rest = self.text.get(key_end..)?;
240        let spaces = rest
241            .len()
242            .saturating_sub(rest.trim_start_matches([' ', '\t']).len());
243        let colon = key_end.checked_add(spaces)?;
244        (self.text.get(colon..)?.starts_with(':')).then_some(colon.checked_add(1)?)
245    }
246
247    /// The span of a node, widened to cover its descendants.
248    ///
249    /// `yaml-rt` ends a collection's span before the content of a block
250    /// scalar that closes it.
251    fn extent(&self, node: NodeId) -> Result<Bounds, DocumentError> {
252        self.document
253            .children(node)
254            .try_fold(self.range(node)?, |covered, child| {
255                Ok(covered.cover(self.extent(child)?))
256            })
257    }
258
259    fn range(&self, node: NodeId) -> Result<Bounds, DocumentError> {
260        let Span { start, end } = self
261            .document
262            .node(node)
263            .map(yaml_rt::Node::span)
264            .ok_or(InternalError::YamlNodeMissing)?;
265        Ok(Bounds::new(
266            usize::try_from(start).unwrap_or(usize::MAX),
267            usize::try_from(end).unwrap_or(usize::MAX),
268        ))
269    }
270
271    fn slice(&self, start: usize, end: usize) -> Result<&'a str, DocumentError> {
272        self.text
273            .get(start..end)
274            .ok_or_else(|| InternalError::YamlSpanInvalid { start, end }.into())
275    }
276
277    /// The offset of the start of the line that holds `offset`.
278    fn line_start(&self, offset: usize) -> usize {
279        self.text
280            .get(..offset)
281            .and_then(|before| before.rfind('\n'))
282            .map_or(0, |newline| newline.saturating_add(1))
283    }
284
285    /// The offset after the line break that ends the line holding `offset`.
286    /// An offset that already follows a line break is its own answer.
287    fn line_end(&self, offset: usize) -> usize {
288        let after_break = offset
289            .checked_sub(1)
290            .is_some_and(|previous| self.text.get(previous..offset) == Some("\n"));
291        if after_break {
292            return offset;
293        }
294        self.text
295            .get(offset..)
296            .and_then(|rest| rest.find('\n'))
297            .map_or(self.text.len(), |newline| {
298                offset.saturating_add(newline).saturating_add(1)
299            })
300    }
301
302    fn with_detected_step(mut self) -> Self {
303        if let Ok(root) = self.document.root_mapping() {
304            self.step = self.detect_step(root).unwrap_or(CANONICAL_STEP);
305        }
306        self
307    }
308
309    /// The indent of the first nested block mapping below its key.
310    fn detect_step(&self, mapping: NodeId) -> Option<usize> {
311        self.document
312            .mapping_entries(mapping)
313            .find_map(|(key, value)| {
314                let is_block = matches!(
315                    self.document.semantic_kind(value),
316                    Some(SemanticKind::Mapping {
317                        style: CollectionStyle::Block
318                    })
319                );
320                if !is_block {
321                    return None;
322                }
323                let (child, _) = self.document.mapping_entries(value).next()?;
324                let key_start = self.range(key).ok()?.start;
325                let child_start = self.range(child).ok()?.start;
326                let parent = key_start.saturating_sub(self.line_start(key_start));
327                let inner = child_start.saturating_sub(self.line_start(child_start));
328                inner
329                    .checked_sub(parent)
330                    .filter(|step| *step > 0)
331                    .or_else(|| self.detect_step(value))
332            })
333    }
334}