1#![warn(clippy::pedantic, missing_docs)]
3use std::fmt::{Display, Write};
4use std::ops::Range;
5
6use rsn::tokenizer::{self, Balanced, Token, TokenKind, Tokenizer};
7use thiserror::Error;
8
9pub mod config;
11pub use config::Config;
12mod utils;
13#[allow(clippy::wildcard_imports)]
14use utils::*;
15
16type Result<T, E = Error> = std::result::Result<T, E>;
17
18#[derive(Error, Debug)]
19pub enum Error {
21 #[error("tokenizer error: {_0:?}")]
23 Tokenizer(#[from] tokenizer::Error),
24 #[error("missmatched delimiter at {_0:?}")]
26 MissmatchedDelimiter(Range<usize>),
27}
28
29macro_rules! w {
32 ($($tt:tt)*) => {
33 { write!($($tt)*).unwrap(); }
34 };
35}
36
37struct Indent {
38 level: usize,
39 hard_tab: bool,
40 width: usize,
41}
42
43impl Indent {
44 fn inc(&mut self) {
45 self.level += 1;
46 }
47
48 fn dec(&mut self) {
49 self.level -= 1;
50 }
51}
52
53impl Display for Indent {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 if self.hard_tab {
56 write!(f, "{:1$\t}", "", self.level)
57 } else {
58 write!(f, "{:1$}", "", self.width * self.level)
59 }
60 }
61}
62
63pub fn format_str(source: &str, config: &Config) -> Result<String> {
66 let mut tokenizer = Tokenizer::full(source);
67 let mut f = String::new();
68 let mut indent = config.indent();
69 let mut opened = Vec::new();
70 let mut nled = false;
71 let mut spaced = false;
72 let nl = config.line_ending(source);
73 while let Some(token) = tokenizer.next() {
74 let Token { location, kind } = token?;
75 match kind {
76 TokenKind::Integer(_)
77 | TokenKind::Float(_)
78 | TokenKind::Bool(_)
79 | TokenKind::Character(_)
80 | TokenKind::Byte(_)
81 | TokenKind::String(_) | TokenKind::Bytes(_) | TokenKind::Identifier(_)
84 | TokenKind::Comment(_) => {
86 if nled {
87 w!(f, "{indent}{}", &source[location]);
88 } else {
89 if spaced {
90 w!(f, " ");
91 }
92 w!(f, "{}", &source[location]);
93 }
94 }
95 TokenKind::Colon => w!(f, ":"),
96 TokenKind::Comma => w!(f, ",{nl}"),
97 TokenKind::Open(delimiter) => {
98 let tmp = tokenizer.clone();
99 match format_single_line(source, &mut tokenizer, delimiter, config)? {
100 Some(single_line) if f.lines().last().unwrap_or_default().len() + single_line.len() < config.max_width => {
101 if spaced || delimiter == Balanced::Brace {
102 w!(f, " ");
103 }
104 w!(f, "{single_line}");
105 }
106 _ => {
107 if nled {
108 w!(f, "{indent}{}{nl}", delimiter.open());
109 } else if spaced || delimiter.is_brace() {
110 w!(f, " {}{nl}", delimiter.open());
111 } else {
112 w!(f, "{}{nl}", delimiter.open());
113 }
114 opened.push(delimiter);
115 indent.inc();
116 tokenizer = tmp;
117 }
118 }
119 }
120 TokenKind::Close(delimiter) => {
121 indent.dec();
122 if nled {
123 w!(f, "{indent}{}", delimiter.close());
124 } else {
125 w!(f, "{nl}{indent}{}", delimiter.close());
126 }
127 if opened.is_empty() || delimiter != opened.pop().expect("opened is not empty") {
128 return Err(Error::MissmatchedDelimiter(location));
129 }
130 }
131 TokenKind::Whitespace(ws) => {
132 match config.preserve_empty_lines {
133 config::PreserveEmptyLines::One => {
134 if ws.chars().filter(|c|*c=='\n').count() > 1 {
135 if !nled {
136 w!(f, "{nl}");
137 }
138 w!(f, "{nl}");
139 nled = true;
140 spaced = false;
141 }
142 },
143 config::PreserveEmptyLines::All => for _ in 0..ws.chars().filter(|c|*c=='\n').count().saturating_sub(usize::from(nled)) {
144 w!(f, "{nl}");
145 nled = true;
146 spaced = false;
147 },
148 config::PreserveEmptyLines::None => {},
149 }
150 }
151 }
152 if !matches!(kind, TokenKind::Whitespace(_)) {
153 nled = matches!(kind, TokenKind::Comma | TokenKind::Open(_));
154 spaced = kind == TokenKind::Colon;
155 }
156 }
157 Ok(f)
158}
159
160fn format_single_line(
161 source: &str,
162 tokenizer: &mut Tokenizer<true>,
163 delimiter: Balanced,
164 config: &Config,
165) -> Result<Option<String>> {
166 let mut f = String::new();
167 let mut opened = vec![delimiter];
168 let mut spaced = delimiter == Balanced::Brace;
169 let mut comma = false;
170 let mut empty = true;
171 let mut unspaced = true;
172 w!(f, "{}", delimiter.open());
173 for token in tokenizer {
174 let Token { location, kind } = token?;
175 if comma {
176 match kind {
177 TokenKind::Integer(_)
178 | TokenKind::Float(_)
179 | TokenKind::Bool(_)
180 | TokenKind::Character(_)
181 | TokenKind::Byte(_)
182 | TokenKind::String(_)
183 | TokenKind::Bytes(_)
184 | TokenKind::Identifier(_)
185 | TokenKind::Open(_) => {
186 w!(f, ",");
187 comma = false;
188 }
189 TokenKind::Close(_) => comma = false,
190 _ => {}
191 }
192 }
193 match kind {
194 TokenKind::Byte(_) | TokenKind::String(_)
195 if source[location.clone()].contains('\n') =>
196 {
197 return Ok(None);
198 }
199 TokenKind::Open(_) if opened.len() > config.max_inline_level => {
200 return Ok(None);
201 }
202 TokenKind::Close(_) if !empty && opened.len() > config.max_inline_level => {
203 return Ok(None);
204 }
205 TokenKind::Integer(_)
206 | TokenKind::Float(_)
207 | TokenKind::Bool(_)
208 | TokenKind::Character(_)
209 | TokenKind::Byte(_)
210 | TokenKind::String(_)
211 | TokenKind::Bytes(_)
212 | TokenKind::Identifier(_) => {
213 if spaced {
214 w!(f, " ");
215 }
216 w!(f, "{}", &source[location]);
217 }
218 TokenKind::Comment(_) => {
219 return Ok(None);
221 }
222 TokenKind::Colon => {
223 w!(f, ":");
224 }
225 TokenKind::Comma => comma = true,
226 TokenKind::Open(delimiter) => {
227 opened.push(delimiter);
228 if (spaced || delimiter == Balanced::Brace) && !unspaced {
229 w!(f, " ");
230 }
231 w!(f, "{}", delimiter.open());
232 }
233 TokenKind::Close(delimiter) => {
234 if delimiter == Balanced::Brace {
235 w!(f, " {}", delimiter.close());
236 } else {
237 w!(f, "{}", delimiter.close());
238 }
239 if opened.is_empty() || delimiter != opened.pop().expect("opened is not empty") {
240 return Err(Error::MissmatchedDelimiter(location));
241 }
242 if opened.is_empty() {
243 return Ok(Some(f));
244 }
245 }
246 TokenKind::Whitespace(ws) => {
247 if ws.chars().filter(|c| *c == '\n').count() > 1
248 && !config.preserve_empty_lines.is_none()
249 {
250 return Ok(None);
251 }
252 }
253 }
254 if !kind.is_white_space() {
255 spaced = matches!(
256 kind,
257 TokenKind::Colon | TokenKind::Comma | TokenKind::Open(Balanced::Brace)
258 );
259 unspaced = kind.is_open();
260 }
261 empty |= !(kind.is_value() || kind.is_comment() || kind.is_close());
262 }
263 Ok(Some(f))
264}