Skip to main content

ephemeris_documents/json/cst/
trivia.rs

1//! Whitespace, comments and indentation of the JSON syntax tree.
2
3use std::fmt;
4use std::iter::repeat_n;
5
6use crate::json::Dialect;
7use crate::text::{Comment, CommentSyntax};
8
9/// The canonical indent of one level.
10const CANONICAL_INDENT: &str = "  ";
11
12/// Whitespace and comments, kept as source text.
13#[derive(Debug, Clone, Default, PartialEq, Eq)]
14pub(crate) struct Trivia(String);
15
16impl Trivia {
17    pub(in crate::json) const fn new(text: String) -> Self {
18        Self(text)
19    }
20
21    pub(in crate::json) fn as_str(&self) -> &str {
22        &self.0
23    }
24
25    /// The blanks that start the last line of the text, or nothing when the
26    /// text has no line break.
27    pub(super) fn indent(&self) -> &str {
28        let Some((_, line)) = self.0.rsplit_once('\n') else {
29            return "";
30        };
31        let end = line
32            .find(|character| character != ' ' && character != '\t')
33            .unwrap_or(line.len());
34        line.get(..end).unwrap_or_default()
35    }
36
37    pub(super) fn is_blank(&self) -> bool {
38        self.0.chars().all(char::is_whitespace)
39    }
40
41    pub(super) fn has_newline(&self) -> bool {
42        self.0.contains('\n')
43    }
44
45    /// Replace the indent `from` at the start of every line with `to`.
46    pub(super) fn reindent_from(&mut self, from: &str, to: &str) {
47        if from == to || !self.has_newline() {
48            return;
49        }
50        if !self
51            .0
52            .split('\n')
53            .skip(1)
54            .any(|line| !line.is_empty() && line.starts_with(from))
55        {
56            return;
57        }
58        let mut lines = self.0.split('\n');
59        let mut out = String::from(lines.next().unwrap_or_default());
60        for line in lines {
61            out.push('\n');
62            match line.strip_prefix(from) {
63                Some(rest) if !line.is_empty() => {
64                    out.push_str(to);
65                    out.push_str(rest);
66                }
67                _ => out.push_str(line),
68            }
69        }
70        self.0 = out;
71    }
72
73    /// Convert the indent at the start of every line from the canonical
74    /// two spaces per level to `unit` per level.
75    pub(super) fn reindent_unit(&mut self, unit: &Indent) {
76        if unit.is_canonical() || !self.has_newline() {
77            return;
78        }
79        let mut lines = self.0.split('\n');
80        let mut out = String::from(lines.next().unwrap_or_default());
81        for line in lines {
82            out.push('\n');
83            let rest = line.trim_start_matches(' ');
84            let width = line.len().saturating_sub(rest.len());
85            out.extend(repeat_n(
86                unit.as_str(),
87                width.div_euclid(CANONICAL_INDENT.len()),
88            ));
89            out.extend(repeat_n(" ", width.rem_euclid(CANONICAL_INDENT.len())));
90            out.push_str(rest);
91        }
92        self.0 = out;
93    }
94
95    /// The comment as `//` lines after `indent`. JSON has no comments.
96    pub(super) fn write_comment(
97        out: &mut String,
98        comment: Option<&Comment>,
99        indent: &str,
100        dialect: Dialect,
101    ) {
102        if let (Dialect::Json5, Some(comment)) = (dialect, comment) {
103            // Writing to a String cannot fail.
104            let _infallible = CommentSyntax::DoubleSlash.write_lines(out, comment, indent);
105        }
106    }
107
108    /// The comment lines of the document root, and a blank line after them.
109    pub(super) fn root(comment: Option<&Comment>, dialect: Dialect) -> Self {
110        let mut text = String::new();
111        Self::write_comment(&mut text, comment, "", dialect);
112        if !text.is_empty() {
113            text.push('\n');
114        }
115        Self(text)
116    }
117
118    /// A line break, the comment lines, and the indent of a member at `depth`.
119    pub(super) fn above(comment: Option<&Comment>, depth: usize, dialect: Dialect) -> Self {
120        let width = CANONICAL_INDENT.len().saturating_mul(depth);
121        let mut text = String::with_capacity(width.saturating_add(1));
122        text.push('\n');
123        if comment.is_some() {
124            let indent = CANONICAL_INDENT.repeat(depth);
125            Self::write_comment(&mut text, comment, &indent, dialect);
126        }
127        text.extend(repeat_n(CANONICAL_INDENT, depth));
128        Self(text)
129    }
130
131    /// A line break and the indent that closes a container at `depth`.
132    pub(super) fn closing(depth: usize) -> Self {
133        let mut text = String::with_capacity(
134            CANONICAL_INDENT
135                .len()
136                .saturating_mul(depth)
137                .saturating_add(1),
138        );
139        text.push('\n');
140        text.extend(repeat_n(CANONICAL_INDENT, depth));
141        Self(text)
142    }
143}
144
145impl fmt::Display for Trivia {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        f.write_str(&self.0)
148    }
149}
150
151/// One level of indentation: a non-empty run of spaces or tabs.
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub(super) struct Indent(String);
154
155impl Default for Indent {
156    fn default() -> Self {
157        Self(CANONICAL_INDENT.to_owned())
158    }
159}
160
161impl Indent {
162    /// A unit of indentation from a run of blanks, or `None` when the run is
163    /// empty.
164    pub(super) fn from_blanks(blanks: &str) -> Option<Self> {
165        (!blanks.is_empty()).then(|| Self(blanks.to_owned()))
166    }
167
168    pub(super) fn as_str(&self) -> &str {
169        &self.0
170    }
171
172    fn is_canonical(&self) -> bool {
173        self.0 == CANONICAL_INDENT
174    }
175}