dioxus_autofmt/
prettier_please.rs1use dioxus_rsx::CallBody;
2use syn::{parse::Parser, visit_mut::VisitMut, Expr, File, Item, MacroDelimiter};
3
4use crate::{IndentOptions, Writer};
5
6impl Writer<'_> {
7 pub fn unparse_expr(&mut self, expr: &Expr) -> String {
8 unparse_expr(expr, self.raw_src, &self.out.indent)
9 }
10}
11
12const MARKER: &str = "𝕣𝕤𝕩";
14const MARKER_REPLACE: &str = "𝕣𝕤𝕩! {}";
15
16pub fn unparse_expr(expr: &Expr, src: &str, cfg: &IndentOptions) -> String {
17 struct ReplaceMacros<'a> {
18 src: &'a str,
19 formatted_stack: Vec<String>,
20 cfg: &'a IndentOptions,
21 }
22
23 impl VisitMut for ReplaceMacros<'_> {
24 fn visit_macro_mut(&mut self, i: &mut syn::Macro) {
25 if let Some("rsx" | "render") = i
27 .path
28 .segments
29 .last()
30 .map(|i| i.ident.to_string())
31 .as_deref()
32 {
33 let body = CallBody::parse_strict.parse2(i.tokens.clone()).unwrap();
38 let multiline = !Writer::is_short_rsx_call(&body.body.roots);
39 let mut formatted = {
40 let mut writer = Writer::new(self.src, self.cfg.clone());
41 _ = writer.write_body_nodes(&body.body.roots).ok();
42 writer.consume()
43 }
44 .unwrap();
45
46 i.path = syn::parse_str(MARKER).unwrap();
47 i.tokens = Default::default();
48
49 i.delimiter = MacroDelimiter::Brace(Default::default());
53
54 if multiline || formatted.contains('\n') {
56 formatted = formatted
57 .lines()
58 .map(|line| {
59 if line.is_empty() {
61 String::new()
62 } else {
63 format!("{}{line}", self.cfg.indent_str())
64 }
65 })
66 .collect::<Vec<_>>()
67 .join("\n");
68 }
69
70 self.formatted_stack.push(formatted)
72 }
73
74 syn::visit_mut::visit_macro_mut(self, i);
75 }
76 }
77
78 let mut replacer = ReplaceMacros {
80 src,
81 cfg,
82 formatted_stack: vec![],
83 };
84
85 let mut modified_expr = expr.clone();
87 replacer.visit_expr_mut(&mut modified_expr);
88
89 let mut unparsed = unparse_inner(&modified_expr);
91
92 for fmted in replacer.formatted_stack.drain(..) {
94 let is_multiline = fmted.ends_with('}') || fmted.contains('\n');
95 let is_empty = fmted.trim().is_empty();
96
97 let mut out_fmt = String::from("rsx! {");
98 if is_multiline {
99 out_fmt.push('\n');
100 } else if !is_empty {
101 out_fmt.push(' ');
102 }
103
104 let mut whitespace = 0;
105
106 for line in unparsed.lines() {
107 if line.contains(MARKER) {
108 whitespace = line.matches(cfg.indent_str()).count();
109 break;
110 }
111 }
112
113 let mut lines = fmted.lines().enumerate().peekable();
114
115 while let Some((_idx, fmt_line)) = lines.next() {
116 if is_multiline && !fmt_line.is_empty() {
118 out_fmt.push_str(&cfg.indent_str().repeat(whitespace));
119 }
120
121 out_fmt.push_str(fmt_line);
123
124 if lines.peek().is_some() {
126 out_fmt.push('\n');
127 }
128 }
129
130 if is_multiline {
131 out_fmt.push('\n');
132 out_fmt.push_str(&cfg.indent_str().repeat(whitespace));
133 } else if !is_empty {
134 out_fmt.push(' ');
135 }
136
137 out_fmt.push('}');
139
140 unparsed = unparsed.replacen(MARKER_REPLACE, &out_fmt, 1);
141 continue;
142 }
143
144 if unparsed.starts_with("{ ") && unparsed.ends_with(" }") {
146 let mut out_fmt = String::new();
147 out_fmt.push('{');
148 out_fmt.push_str(&unparsed[2..unparsed.len() - 2]);
149 out_fmt.push('}');
150 out_fmt
151 } else {
152 unparsed
153 }
154}
155
156pub fn unparse_inner(expr: &Expr) -> String {
161 let file = wrapped(expr);
162 let wrapped = prettyplease::unparse(&file);
163 unwrapped(wrapped)
164}
165
166fn unwrapped(raw: String) -> String {
168 let mut o = raw
169 .strip_prefix("fn main() {\n")
170 .unwrap()
171 .strip_suffix("}\n")
172 .unwrap()
173 .lines()
174 .map(|line| line.strip_prefix(" ").unwrap_or_default()) .collect::<Vec<_>>()
176 .join("\n");
177
178 if o.ends_with(';') {
180 o.pop();
181 }
182
183 o
184}
185
186fn wrapped(expr: &Expr) -> File {
187 File {
188 shebang: None,
189 attrs: vec![],
190 items: vec![
191 Item::Verbatim(quote::quote! {
193 fn main() {
194 #expr;
195 }
196 }),
197 ],
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use proc_macro2::TokenStream;
205
206 fn fmt_block_from_expr(raw: &str, tokens: TokenStream, cfg: IndentOptions) -> Option<String> {
207 let body = CallBody::parse_strict.parse2(tokens).unwrap();
208 let mut writer = Writer::new(raw, cfg);
209 writer.write_body_nodes(&body.body.roots).ok()?;
210 writer.consume()
211 }
212
213 #[test]
214 fn unparses_raw() {
215 let expr = syn::parse_str("1 + 1").expect("Failed to parse");
216 let unparsed = prettyplease::unparse(&wrapped(&expr));
217 assert_eq!(unparsed, "fn main() {\n 1 + 1;\n}\n");
218 }
219
220 #[test]
221 fn weird_ifcase() {
222 let contents = r##"
223 fn main() {
224 move |_| timer.with_mut(|t| if t.started_at.is_none() { Some(Instant::now()) } else { None })
225 }
226 "##;
227
228 let expr: File = syn::parse_file(contents).unwrap();
229 let out = prettyplease::unparse(&expr);
230 println!("{}", out);
231 }
232
233 #[test]
234 fn multiline_madness() {
235 let contents = r##"
236 {
237 {children.is_some().then(|| rsx! {
238 span {
239 class: "inline-block ml-auto hover:bg-gray-500",
240 onclick: move |evt| {
241 evt.cancel_bubble();
242 },
243 icons::icon_5 {}
244 {rsx! {
245 icons::icon_6 {}
246 }}
247 }
248 })}
249 {children.is_some().then(|| rsx! {
250 span {
251 class: "inline-block ml-auto hover:bg-gray-500",
252 onclick: move |evt| {
253 evt.cancel_bubble();
254 },
255 icons::icon_10 {}
256 }
257 })}
258
259 }
260
261 "##;
262
263 let expr: Expr = syn::parse_str(contents).unwrap();
264 let out = unparse_expr(&expr, contents, &IndentOptions::default());
265 println!("{}", out);
266 }
267
268 #[test]
269 fn write_body_no_indent() {
270 let src = r##"
271 span {
272 class: "inline-block ml-auto hover:bg-gray-500",
273 onclick: move |evt| {
274 evt.cancel_bubble();
275 },
276 icons::icon_10 {}
277 icons::icon_10 {}
278 icons::icon_10 {}
279 icons::icon_10 {}
280 div { "hi" }
281 div { div {} }
282 div { div {} div {} div {} }
283 {children}
284 {
285 some_big_long()
286 .some_big_long()
287 .some_big_long()
288 .some_big_long()
289 .some_big_long()
290 .some_big_long()
291 }
292 div { class: "px-4", {is_current.then(|| rsx! { {children} })} }
293 Thing {
294 field: rsx! {
295 div { "hi" }
296 Component {
297 onrender: rsx! {
298 div { "hi" }
299 Component {
300 onclick: move |_| {
301 another_macro! {
302 div { class: "max-w-lg lg:max-w-2xl mx-auto mb-16 text-center",
303 "gomg"
304 "hi!!"
305 "womh"
306 }
307 };
308 rsx! {
309 div { class: "max-w-lg lg:max-w-2xl mx-auto mb-16 text-center",
310 "gomg"
311 "hi!!"
312 "womh"
313 }
314 };
315 println!("hi")
316 },
317 onrender: move |_| {
318 let _ = 12;
319 let r = rsx! {
320 div { "hi" }
321 };
322 rsx! {
323 div { "hi" }
324 }
325 }
326 }
327 {
328 rsx! {
329 BarChart {
330 id: "bar-plot".to_string(),
331 x: value,
332 y: label
333 }
334 }
335 }
336 }
337 }
338 }
339 }
340 }
341 "##;
342
343 let tokens: TokenStream = syn::parse_str(src).unwrap();
344 let out = fmt_block_from_expr(src, tokens, IndentOptions::default()).unwrap();
345 println!("{}", out);
346 }
347
348 #[test]
349 fn write_component_body() {
350 let src = r##"
351 div { class: "px-4", {is_current.then(|| rsx! { {children} })} }
352 "##;
353
354 let tokens: TokenStream = syn::parse_str(src).unwrap();
355 let out = fmt_block_from_expr(src, tokens, IndentOptions::default()).unwrap();
356 println!("{}", out);
357 }
358
359 #[test]
360 fn weird_macro() {
361 let contents = r##"
362 fn main() {
363 move |_| {
364 drop_macro_semi! {
365 "something_very_long_something_very_long_something_very_long_something_very_long"
366 };
367 let _ = drop_macro_semi! {
368 "something_very_long_something_very_long_something_very_long_something_very_long"
369 };
370 drop_macro_semi! {
371 "something_very_long_something_very_long_something_very_long_something_very_long"
372 };
373 };
374 }
375 "##;
376
377 let expr: File = syn::parse_file(contents).unwrap();
378 let out = prettyplease::unparse(&expr);
379 println!("{}", out);
380 }
381
382 #[test]
383 fn comments_on_nodes() {
384 let src = r##"// hiasdasds
385 div {
386 attr: "value", // comment
387 div {}
388 "hi" // hello!
389 "hi" // hello!
390 "hi" // hello!
391 // hi!
392 }
393 "##;
394
395 let tokens: TokenStream = syn::parse_str(src).unwrap();
396 let out = fmt_block_from_expr(src, tokens, IndentOptions::default()).unwrap();
397 println!("{}", out);
398 }
399}