ephemeris_documents/json/cst/
trivia.rs1use std::fmt;
4use std::iter::repeat_n;
5
6use crate::json::Dialect;
7use crate::text::{Comment, CommentSyntax};
8
9const CANONICAL_INDENT: &str = " ";
11
12#[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 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 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 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 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 let _infallible = CommentSyntax::DoubleSlash.write_lines(out, comment, indent);
105 }
106 }
107
108 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 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 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#[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 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}