char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somfjson: remove members without leaving spurious whitespace75a6060

main
13.5 KiB398 linesraw
1use serde_json::Value as JsonValue;
2
3use crate::syntax::{self, Value, ValueKind};
4use crate::{Error, Options, Span, format};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum PathSegment<'a> {
8    Key(&'a str),
9    Index(usize),
10}
11
12impl<'a> From<&'a str> for PathSegment<'a> {
13    fn from(value: &'a str) -> Self {
14        Self::Key(value)
15    }
16}
17
18impl From<usize> for PathSegment<'_> {
19    fn from(value: usize) -> Self {
20        Self::Index(value)
21    }
22}
23
24#[derive(Debug, Clone)]
25pub struct Document {
26    source: String,
27    root: Value,
28    options: Options,
29}
30
31impl Document {
32    pub fn parse(source: impl Into<String>) -> Result<Self, Error> {
33        Self::parse_with(source, &Options::default())
34    }
35
36    pub fn parse_with(source: impl Into<String>, options: &Options) -> Result<Self, Error> {
37        let source = source.into();
38        let root = syntax::parse_with(&source, options)?;
39        Ok(Self { source, root, options: *options })
40    }
41
42    pub fn parse_object(source: impl Into<String>) -> Result<Self, Error> {
43        Self::parse_object_with(source, &Options::default())
44    }
45
46    pub fn parse_object_with(source: impl Into<String>, options: &Options) -> Result<Self, Error> {
47        let source = source.into();
48        let root = syntax::parse_object_with(&source, options)?;
49        Ok(Self { source, root, options: *options })
50    }
51
52    pub fn options(&self) -> Options {
53        self.options
54    }
55
56    pub fn source(&self) -> &str {
57        &self.source
58    }
59
60    pub fn into_source(self) -> String {
61        self.source
62    }
63
64    pub fn root(&self) -> &Value {
65        &self.root
66    }
67
68    pub fn to_value(&self) -> Result<JsonValue, Error> {
69        self.root.to_value()
70    }
71
72    /// Missing object keys are inserted; an array index equal to its length appends.
73    /// Parent containers must exist. An empty path replaces the root.
74    pub fn set(&mut self, path: &[PathSegment<'_>], value: &JsonValue) -> Result<(), Error> {
75        let edit = if let Some((last, parent)) = path.split_last() {
76            let container = self.locate(parent)?;
77            if let Some(existing) = child(container, *last)? {
78                self.replacement(existing, value)?
79            } else {
80                let prefix = match (&container.kind, last) {
81                    (ValueKind::Object(_), PathSegment::Key(key)) => {
82                        format!("{}: ", format::key(key))
83                    }
84                    (ValueKind::Array(items), PathSegment::Index(index))
85                        if *index == items.len() =>
86                    {
87                        String::new()
88                    }
89                    _ => {
90                        return Err(syntax::invalid(
91                            container.span.clone(),
92                            "Array index out of bounds",
93                        ));
94                    }
95                };
96                Some(vec![self.insertion(container, &prefix, value)?])
97            }
98        } else {
99            self.replacement(&self.root, value)?
100        };
101        if let Some(edits) = edit {
102            self.apply(edits)?;
103        }
104        Ok(())
105    }
106
107    /// Comments outside the value survive, including those between a key and its value.
108    /// Removing the root is not supported; replace it with an empty object or null instead.
109    pub fn remove(&mut self, path: &[PathSegment<'_>]) -> Result<bool, Error> {
110        let Some((last, parent)) = path.split_last() else {
111            return Err(syntax::invalid(
112                self.root.span.clone(),
113                "Cannot remove the root",
114            ));
115        };
116        let container = self.locate(parent)?;
117        let Some(value) = child(container, *last)? else {
118            return Ok(false);
119        };
120        let mut edits = vec![(value.span.clone(), String::new())];
121        let mut member_start = value.span.start;
122        let mut colon_at = None;
123        let mut key_end = None;
124        let next = match (&container.kind, last) {
125            (ValueKind::Object(members), PathSegment::Key(key)) => {
126                let index = members
127                    .iter()
128                    .position(|member| member.key == *key)
129                    .unwrap();
130                let member = &members[index];
131                member_start = member.key_span.start;
132                key_end = Some(member.key_span.end);
133                edits.push((member.key_span.clone(), String::new()));
134                let colon =
135                    punctuation(&self.source, member.key_span.end..value.span.start, b':').unwrap();
136                colon_at = Some(colon);
137                edits.push((colon..colon + 1, String::new()));
138                members.get(index + 1).map(|member| member.key_span.start)
139            }
140            (ValueKind::Array(items), PathSegment::Index(index)) => {
141                items.get(index + 1).map(|value| value.span.start)
142            }
143            _ => unreachable!(),
144        };
145        let comma = punctuation(
146            &self.source,
147            value.span.end..next.unwrap_or_else(|| self.container_end(container)),
148            b',',
149        );
150        if let Some(comma) = comma {
151            edits.push((comma..comma + 1, String::new()));
152        }
153        // A member alone on its line takes its indentation and line break
154        // with it, rather than leaving a whitespace-only line behind.
155        // Comments or other content on the line block both extensions.
156        let start_of_line = line_start(&self.source, member_start);
157        if start_of_line < member_start && is_blank(&self.source, start_of_line..member_start) {
158            edits.push((start_of_line..member_start, String::new()));
159        }
160        let member_end = comma.map_or(value.span.end, |comma| comma + 1);
161        let bytes = self.source.as_bytes();
162        let mut end = member_end;
163        while end < bytes.len() && matches!(bytes[end], b' ' | b'\t') {
164            end += 1;
165        }
166        if self.source[end..].starts_with("\r\n") {
167            end += 2;
168        } else if self.source[end..].starts_with(['\r', '\n']) {
169            end += 1;
170        } else {
171            end = member_end;
172        }
173        if end > member_end {
174            edits.push((member_end..end, String::new()));
175        }
176        // Blank gaps around the colon and comma (e.g. the space in `a: 1`)
177        // are not part of any span above and would otherwise survive on the
178        // collapsed line. Gaps containing comments are not blank and stay.
179        if let (Some(colon), Some(key_end)) = (colon_at, key_end) {
180            let mut gaps = vec![key_end..colon, colon + 1..value.span.start];
181            if let Some(comma) = comma {
182                gaps.push(value.span.end..comma);
183            }
184            for gap in gaps {
185                if gap.start < gap.end && is_blank(&self.source, gap.clone()) {
186                    edits.push((gap, String::new()));
187                }
188            }
189        }
190        self.apply(edits)?;
191        Ok(true)
192    }
193
194    fn locate(&self, path: &[PathSegment<'_>]) -> Result<&Value, Error> {
195        let mut value = &self.root;
196        for segment in path {
197            value = child(value, *segment)?
198                .ok_or_else(|| syntax::invalid(value.span.clone(), "Path does not exist"))?;
199        }
200        Ok(value)
201    }
202
203    fn replacement(
204        &self,
205        existing: &Value,
206        value: &JsonValue,
207    ) -> Result<Option<Vec<(Span, String)>>, Error> {
208        if existing.to_value().ok().as_ref() == Some(value) {
209            return Ok(None);
210        }
211        let implicit = matches!(existing.kind, ValueKind::Object(_))
212            && !self.source[existing.span.start..].starts_with('{');
213        let indent = line_indent(&self.source, existing.span.start);
214        let mut text = if let JsonValue::String(text) = value
215            && self.source[existing.span.start..].starts_with('\'')
216        {
217            let json = serde_json::to_string(text).unwrap();
218            format!("'{}'", json[1..json.len() - 1].replace('\'', "\\'"))
219        } else {
220            formatted(
221                value,
222                implicit,
223                indent,
224                newline(&self.source),
225                &self.options,
226            )?
227        };
228        if existing.span.is_empty()
229            && existing.span.start > 0
230            && !self.source[..existing.span.start].ends_with(['\r', '\n'])
231        {
232            text = format!("{}{indent}{text}", newline(&self.source));
233        }
234        let mut edits = vec![(existing.span.clone(), text)];
235        if implicit
236            && let Some(comma) =
237                punctuation(&self.source, existing.span.end..self.source.len(), b',')
238        {
239            edits.push((comma..comma + 1, String::new()));
240        }
241        Ok(Some(edits))
242    }
243
244    fn container_end(&self, container: &Value) -> usize {
245        if matches!(container.kind, ValueKind::Object(_))
246            && !self.source[container.span.start..].starts_with('{')
247        {
248            self.source.len()
249        } else {
250            container.span.end
251        }
252    }
253
254    fn insertion(
255        &self,
256        container: &Value,
257        prefix: &str,
258        value: &JsonValue,
259    ) -> Result<(Span, String), Error> {
260        let braced = self.source[container.span.start..].starts_with(['{', '[']);
261        let end = self.container_end(container) - usize::from(braced);
262        let base = line_indent(&self.source, container.span.start);
263        let first = match &container.kind {
264            ValueKind::Object(members) => members.first().map(|member| member.key_span.start),
265            ValueKind::Array(items) => items.first().map(|value| value.span.start),
266            _ => unreachable!(),
267        };
268        let indent = first
269            .filter(|start| {
270                self.source[line_start(&self.source, *start)..*start]
271                    .chars()
272                    .all(|ch| ch == ' ' || ch == '\t')
273            })
274            .map(|start| line_indent(&self.source, start).to_owned())
275            .unwrap_or_else(|| {
276                if braced {
277                    format!("{base}  ")
278                } else {
279                    base.to_owned()
280                }
281            });
282        let line = line_start(&self.source, end);
283        let at_line_start = self.source[line..end]
284            .chars()
285            .all(|ch| ch == ' ' || ch == '\t');
286        let at = if braced && at_line_start { line } else { end };
287        let newline = newline(&self.source);
288        let mut text = String::new();
289        if at > 0 && !self.source[..at].ends_with(['\r', '\n']) {
290            text.push_str(newline);
291        }
292        text.push_str(&indent);
293        text.push_str(prefix);
294        text.push_str(&formatted(value, false, &indent, newline, &self.options)?);
295        if braced {
296            text.push_str(newline);
297            if !at_line_start {
298                text.push_str(base);
299            }
300        } else if self.source.ends_with(['\r', '\n']) {
301            text.push_str(newline);
302        }
303        Ok((at..at, text))
304    }
305
306    fn apply(&mut self, mut edits: Vec<(Span, String)>) -> Result<(), Error> {
307        let mut source = self.source.clone();
308        edits.sort_by_key(|(span, _)| std::cmp::Reverse(span.start));
309        for (span, text) in edits {
310            source.replace_range(span, &text);
311        }
312        let root = syntax::parse_with(&source, &self.options).map_err(|error| Error {
313            kind: error.kind,
314            span: self.root.span.clone(),
315            message: format!("Edit would produce invalid fJSON: {}", error.message),
316            previous: None,
317        })?;
318        self.source = source;
319        self.root = root;
320        Ok(())
321    }
322}
323
324fn child<'a>(value: &'a Value, segment: PathSegment<'_>) -> Result<Option<&'a Value>, Error> {
325    match (&value.kind, segment) {
326        (ValueKind::Object(members), PathSegment::Key(key)) => Ok(members
327            .iter()
328            .find(|member| member.key == key)
329            .map(|member| &member.value)),
330        (ValueKind::Array(items), PathSegment::Index(index)) => Ok(items.get(index)),
331        _ => Err(syntax::invalid(
332            value.span.clone(),
333            "Path segment does not match its container",
334        )),
335    }
336}
337
338fn formatted(
339    value: &JsonValue,
340    implicit: bool,
341    indent: &str,
342    newline: &str,
343    options: &Options,
344) -> Result<String, Error> {
345    Ok(format::print(value, implicit, options)?
346        .split('\n')
347        .collect::<Vec<_>>()
348        .join(&format!("{newline}{indent}")))
349}
350
351fn is_blank(source: &str, span: Span) -> bool {
352    source[span]
353        .bytes()
354        .all(|byte| matches!(byte, b' ' | b'\t'))
355}
356
357fn line_start(source: &str, at: usize) -> usize {
358    source[..at]
359        .rfind(['\r', '\n'])
360        .map_or(0, |index| index + 1)
361}
362
363fn line_indent(source: &str, at: usize) -> &str {
364    let start = line_start(source, at);
365    let end = source[start..at]
366        .find(|ch| ch != ' ' && ch != '\t')
367        .unwrap_or(at - start);
368    &source[start..start + end]
369}
370
371fn newline(source: &str) -> &'static str {
372    if source.contains("\r\n") {
373        "\r\n"
374    } else if source.contains('\r') {
375        "\r"
376    } else {
377        "\n"
378    }
379}
380
381fn punctuation(source: &str, span: Span, target: u8) -> Option<usize> {
382    let bytes = source.as_bytes();
383    let mut at = span.start;
384    while at < span.end {
385        if source[at..].starts_with("//") {
386            at += source[at..span.end]
387                .find(['\r', '\n'])
388                .unwrap_or(span.end - at);
389        } else if source[at..].starts_with("/*") {
390            at += source[at..span.end].find("*/").unwrap() + 2;
391        } else if bytes[at] == target {
392            return Some(at);
393        } else {
394            at += 1;
395        }
396    }
397    None
398}