Skip to main content

yog_data/text/
comments.rs

1//! Text with its comments kept as data — what a file becomes in the binary
2//! form, which has no comments: a comment above an object (or above the
3//! entry holding it) becomes its `comment`; one above any other entry
4//! becomes the map's `$<key>`; one above a list's item that isn't an
5//! object joins the list's nearest map's `comment`.
6
7use super::{Spans, SyntaxError, parse};
8use crate::schema::{COMMENT_KEY, META_PREFIX};
9use crate::value::{Map, Value};
10
11/// `text`'s value, its comments in it (see the module docs).
12pub fn with_comments(text: &str) -> Result<Value, SyntaxError> {
13    let doc = parse(text)?;
14    let mut value = doc.value;
15    let lead = comments(&text[..doc.spans.span.start as usize]);
16    let mut stray = Vec::new();
17    attach(&mut value, &doc.spans, text, &mut stray);
18    let mut notes: Vec<String> = lead.into_iter().collect();
19    notes.extend(stray);
20    if let (Value::Map(m), false) = (&mut value, notes.is_empty()) {
21        note(m, COMMENT_KEY, &notes.join("\n"));
22    }
23    Ok(value)
24}
25
26/// Moves the comments inside `v` (spanned by `s`) into it; the ones that
27/// have no object to go to are added to `stray`.
28fn attach(v: &mut Value, s: &Spans, text: &str, stray: &mut Vec<String>) {
29    let inner = s.span.start as usize + 1;
30    match v {
31        Value::Map(m) => {
32            let mut before = inner;
33            let mut entries: Vec<(String, Value)> = std::mem::take(m).into_iter().collect();
34            let mut out = Map::with_capacity(entries.len());
35            let mut own = Vec::new();
36            for ((key, child), cs) in entries.iter_mut().zip(&s.children) {
37                let start = cs.key.unwrap_or(cs.span).start as usize;
38                let said = comments(&text[before..start]);
39                before = cs.span.end as usize;
40                attach(child, cs, text, &mut own);
41                match (said, &mut *child) {
42                    (Some(c), Value::Map(cm)) => note(cm, COMMENT_KEY, &c),
43                    (Some(c), _) => {
44                        out.insert(format!("{META_PREFIX}{key}"), Value::Str(c));
45                    }
46                    (None, _) => {}
47                }
48                out.insert(key.clone(), std::mem::take(child));
49            }
50            own.extend(comments(
51                &text[before..(s.span.end as usize).saturating_sub(1).max(before)],
52            ));
53            *m = out;
54            if !own.is_empty() {
55                note(m, COMMENT_KEY, &own.join("\n"));
56            }
57        }
58        Value::List(items) => {
59            let mut before = inner;
60            for (item, cs) in items.iter_mut().zip(&s.children) {
61                let said = comments(&text[before..cs.span.start as usize]);
62                before = cs.span.end as usize;
63                attach(item, cs, text, stray);
64                match (said, &mut *item) {
65                    (Some(c), Value::Map(im)) => note(im, COMMENT_KEY, &c),
66                    (Some(c), _) => stray.push(c),
67                    (None, _) => {}
68                }
69            }
70        }
71        _ => {}
72    }
73}
74
75/// Adds `text` to `m`'s note `key` (first, before what it said already).
76fn note(m: &mut Map, key: &str, text: &str) {
77    let joined = match m.get(key).and_then(Value::as_str) {
78        Some(had) => format!("{text}\n{had}"),
79        None => text.to_string(),
80    };
81    let rest: Vec<(String, Value)> = std::mem::take(m)
82        .into_iter()
83        .filter(|(k, _)| k != key)
84        .collect();
85    m.insert(key, Value::Str(joined));
86    for (k, v) in rest {
87        m.insert(k, v);
88    }
89}
90
91/// The comments in `gap` (text between values), their markers and
92/// indentation gone, joined by lines; `None` if there are none.
93fn comments(gap: &str) -> Option<String> {
94    let mut lines = Vec::new();
95    let mut rest = gap;
96    while let Some(at) = rest.find('/') {
97        let after = &rest[at + 1..];
98        if let Some(line) = after.strip_prefix('/') {
99            let end = line.find('\n').unwrap_or(line.len());
100            lines.push(line[..end].trim().to_string());
101            rest = &line[end..];
102        } else if let Some(block) = after.strip_prefix('*') {
103            let end = block.find("*/").unwrap_or(block.len());
104            lines.extend(
105                block[..end]
106                    .lines()
107                    .map(|l| l.trim().trim_start_matches('*').trim().to_string())
108                    .filter(|l| !l.is_empty()),
109            );
110            rest = block.get(end + 2..).unwrap_or_default();
111        } else {
112            rest = after;
113        }
114    }
115    (!lines.is_empty()).then(|| lines.join("\n"))
116}
117
118#[cfg(test)]
119mod tests;