1#![doc = include_str!("../README.md")]
2#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
3#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
4
5use crate::writer::*;
6use dioxus_rsx::{BodyNode, CallBody};
7use proc_macro2::{LineColumn, Span};
8use syn::parse::Parser;
9
10mod buffer;
11mod collect_macros;
12mod indent;
13mod prettier_please;
14mod writer;
15
16pub use indent::{IndentOptions, IndentType};
17
18#[derive(serde::Deserialize, serde::Serialize, Clone, Debug, PartialEq, Eq, Hash)]
28pub struct FormattedBlock {
29 pub formatted: String,
31
32 pub start: usize,
34
35 pub end: usize,
37}
38
39#[deprecated(note = "Use try_fmt_file instead - this function panics on error.")]
43pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
44 let parsed =
45 syn::parse_file(contents).expect("fmt_file should only be called on valid syn::File files");
46 try_fmt_file(contents, &parsed, indent).expect("Failed to format file")
47}
48
49pub fn try_fmt_file(
64 contents: &str,
65 parsed: &syn::File,
66 indent: IndentOptions,
67) -> syn::Result<Vec<FormattedBlock>> {
68 let mut formatted_blocks = Vec::new();
69
70 let macros = collect_macros::collect_from_file(parsed);
71
72 if macros.is_empty() {
74 return Ok(formatted_blocks);
75 }
76
77 let mut writer = Writer::new(contents, indent);
78
79 let mut end_span = LineColumn { column: 0, line: 0 };
81 for item in macros {
82 let macro_path = &item.path.segments[0].ident;
83
84 if macro_path.span().start() < end_span {
86 continue;
87 }
88
89 let body = item.parse_body_with(CallBody::parse_strict)?;
90
91 let rsx_start = macro_path.span().start();
92
93 writer.out.indent_level = writer
94 .out
95 .indent
96 .count_indents(writer.src.get(rsx_start.line - 1).unwrap_or(&""));
97
98 if writer.write_rsx_call(&body).is_err() {
101 let span = writer.invalid_exprs.pop().unwrap_or_else(Span::call_site);
102 return Err(syn::Error::new(span, "Failed emit valid rsx - likely due to partially complete expressions in the rsx! macro"));
103 }
104
105 if writer.out.buf.contains('\n') {
107 _ = writer.out.new_line();
108 _ = writer.out.tab();
109 }
110
111 let span = item.delimiter.span().join();
112 let mut formatted = writer.out.buf.split_off(0);
113
114 let start = collect_macros::byte_offset(contents, span.start()) + 1;
115 let end = collect_macros::byte_offset(contents, span.end()) - 1;
116
117 let body_is_solo_expr = body.body.roots.len() == 1
119 && matches!(body.body.roots[0], BodyNode::RawExpr(_) | BodyNode::Text(_));
120
121 if formatted.len() <= 80
123 && !formatted.contains('\n')
124 && !body_is_solo_expr
125 && !formatted.trim().is_empty()
126 {
127 formatted = format!(" {formatted} ");
128 }
129
130 end_span = span.end();
131
132 if contents[start..end] == formatted {
133 continue;
134 }
135
136 formatted_blocks.push(FormattedBlock {
137 formatted,
138 start,
139 end,
140 });
141 }
142
143 Ok(formatted_blocks)
144}
145
146pub fn write_block_out(body: &CallBody) -> Option<String> {
151 let mut buf = Writer::new("", IndentOptions::default());
152 buf.write_rsx_call(body).ok()?;
153 buf.consume()
154}
155
156pub fn fmt_block(block: &str, indent_level: usize, indent: IndentOptions) -> Option<String> {
157 let body = CallBody::parse_strict.parse_str(block).unwrap();
158
159 let mut buf = Writer::new(block, indent);
160 buf.out.indent_level = indent_level;
161 buf.write_rsx_call(&body).ok()?;
162
163 if buf.out.buf.contains('\n') {
165 buf.out.new_line().unwrap();
166 }
167
168 buf.consume()
169}
170
171pub fn apply_formats(input: &str, blocks: Vec<FormattedBlock>) -> String {
173 let mut out = String::new();
174
175 let mut last = 0;
176
177 for FormattedBlock {
178 formatted,
179 start,
180 end,
181 } in blocks
182 {
183 let prefix = &input[last..start];
184 out.push_str(prefix);
185 out.push_str(&formatted);
186 last = end;
187 }
188
189 let suffix = &input[last..];
190 out.push_str(suffix);
191
192 out
193}