1use crate::{
2 Language,
3 config::{LanguageOptions, Quotes, WhitespaceSensitivity},
4 helpers,
5 state::State,
6};
7use memchr::memchr;
8use std::borrow::Cow;
9
10const QUOTES: [&str; 3] = ["\"", "\"", "'"];
11
12pub(crate) struct Ctx<'b, E, F>
13where
14 F: for<'a> FnMut(&'a str, Hints<'b>) -> Result<Cow<'a, str>, E>,
15{
16 pub(crate) source: &'b str,
17 pub(crate) language: Language,
18 pub(crate) indent_width: usize,
19 pub(crate) print_width: usize,
20 pub(crate) options: &'b LanguageOptions,
21 pub(crate) external_formatter: F,
22 pub(crate) external_formatter_errors: Vec<E>,
23}
24
25impl<'b, E, F> Ctx<'b, E, F>
26where
27 F: for<'a> FnMut(&'a str, Hints<'b>) -> Result<Cow<'a, str>, E>,
28{
29 pub(crate) fn script_indent(&self) -> bool {
30 match self.language {
31 Language::Html
32 | Language::Jinja
33 | Language::Vento
34 | Language::Angular
35 | Language::Mustache => self
36 .options
37 .html_script_indent
38 .unwrap_or(self.options.script_indent),
39 Language::Vue => self
40 .options
41 .vue_script_indent
42 .unwrap_or(self.options.script_indent),
43 Language::Svelte => self
44 .options
45 .svelte_script_indent
46 .unwrap_or(self.options.script_indent),
47 Language::Astro => self
48 .options
49 .astro_script_indent
50 .unwrap_or(self.options.script_indent),
51 Language::Xml => false,
52 }
53 }
54
55 pub(crate) fn style_indent(&self) -> bool {
56 match self.language {
57 Language::Html
58 | Language::Jinja
59 | Language::Vento
60 | Language::Angular
61 | Language::Mustache => self
62 .options
63 .html_style_indent
64 .unwrap_or(self.options.style_indent),
65 Language::Vue => self
66 .options
67 .vue_style_indent
68 .unwrap_or(self.options.style_indent),
69 Language::Svelte => self
70 .options
71 .svelte_style_indent
72 .unwrap_or(self.options.style_indent),
73 Language::Astro => self
74 .options
75 .astro_style_indent
76 .unwrap_or(self.options.style_indent),
77 Language::Xml => false,
78 }
79 }
80
81 pub(crate) fn is_whitespace_sensitive(&self, tag_name: &str) -> bool {
82 match self.language {
83 Language::Vue | Language::Svelte | Language::Astro | Language::Angular
84 if helpers::is_component(tag_name) =>
85 {
86 matches!(
87 self.options
88 .component_whitespace_sensitivity
89 .unwrap_or(self.options.whitespace_sensitivity),
90 WhitespaceSensitivity::Css | WhitespaceSensitivity::Strict
91 )
92 }
93 Language::Xml => false,
94 _ => match self.options.whitespace_sensitivity {
95 WhitespaceSensitivity::Css => {
96 helpers::is_whitespace_sensitive_tag(tag_name, self.language)
97 }
98 WhitespaceSensitivity::Strict => true,
99 WhitespaceSensitivity::Ignore => false,
100 },
101 }
102 }
103
104 pub(crate) fn with_escaping_quotes(
105 &mut self,
106 s: &str,
107 mut processer: impl FnMut(String, &mut Self) -> String,
108 ) -> String {
109 let escaped = helpers::UNESCAPING_AC.replace_all(s, "ES);
110 let proceeded = processer(escaped, self);
111 if memchr(b'\'', proceeded.as_bytes()).is_some()
112 && memchr(b'"', proceeded.as_bytes()).is_some()
113 {
114 match self.options.quotes {
115 Quotes::Double => proceeded.replace('"', """),
116 Quotes::Single => proceeded.replace('\'', "'"),
117 }
118 } else {
119 proceeded
120 }
121 }
122
123 pub(crate) fn format_expr(&mut self, code: &str, attr: bool, start: usize) -> String {
124 match self.try_format_expr(code, attr, start) {
125 Ok(formatted) => formatted,
126 Err(e) => {
127 self.external_formatter_errors.push(e);
128 code.to_owned()
129 }
130 }
131 }
132
133 pub(crate) fn try_format_expr(
134 &mut self,
135 code: &str,
136 attr: bool,
137 start: usize,
138 ) -> Result<String, E> {
139 if code.trim().is_empty() {
140 Ok(String::new())
141 } else {
142 let preprocessed = code.trim_start();
146 let will_add_brackets =
147 preprocessed.starts_with('{') || preprocessed.starts_with("...");
148 let wrapped = if will_add_brackets {
149 self.source
150 .get(0..start.saturating_sub(1))
151 .unwrap_or_default()
152 .replace(|c: char| !c.is_ascii_whitespace(), " ")
153 + "["
154 + code.trim()
155 + "]"
156 } else {
157 self.source
158 .get(0..start)
159 .unwrap_or_default()
160 .replace(|c: char| !c.is_ascii_whitespace(), " ")
161 + code
162 };
163 let formatted = self.try_format_with_external_formatter(
164 wrapped,
165 Hints {
166 print_width: self.print_width,
167 indent_level: 0,
168 attr,
169 ext: "tsx",
170 },
171 )?;
172 let mut formatted =
173 formatted.trim_matches(|c: char| c.is_ascii_whitespace() || c == ';');
174 formatted = trim_delim(preprocessed, formatted, '[', ']');
175 formatted = trim_delim(preprocessed, formatted, '(', ')');
176 if will_add_brackets {
177 formatted = formatted.trim_ascii_end().trim_end_matches(',');
178 }
179 Ok(formatted.trim_ascii().to_owned())
180 }
181 }
182
183 pub(crate) fn format_binding(&mut self, code: &str, start: usize) -> String {
184 if code.trim().is_empty() {
185 String::new()
186 } else {
187 let wrapped = self
188 .source
189 .get(0..start.saturating_sub(4))
190 .unwrap_or_default()
191 .replace(|c: char| !c.is_ascii_whitespace(), " ")
192 + "let "
193 + code.trim()
194 + " = 0";
195 let formatted = self.format_with_external_formatter(
196 wrapped,
197 Hints {
198 print_width: self.print_width,
199 indent_level: 0,
200 attr: false,
201 ext: "ts",
202 },
203 );
204 let formatted = formatted.trim_matches(|c: char| c.is_ascii_whitespace() || c == ';');
205 formatted
206 .strip_prefix("let ")
207 .and_then(|s| s.strip_suffix(" = 0"))
208 .unwrap_or(formatted)
209 .to_owned()
210 }
211 }
212
213 pub(crate) fn format_type_params(&mut self, code: &str, start: usize) -> String {
214 if code.trim().is_empty() {
215 String::new()
216 } else {
217 let wrapped = self
218 .source
219 .get(0..start.saturating_sub(7))
220 .unwrap_or_default()
221 .replace(|c: char| !c.is_ascii_whitespace(), " ")
222 + "type T<"
223 + code.trim()
224 + "> = 0";
225 let formatted = self.format_with_external_formatter(
226 wrapped,
227 Hints {
228 print_width: self.print_width,
229 indent_level: 0,
230 attr: true,
231 ext: "ts",
232 },
233 );
234 let formatted = formatted.trim_matches(|c: char| c.is_ascii_whitespace() || c == ';');
235 formatted
236 .strip_prefix("type T<")
237 .and_then(|s| s.strip_suffix("> = 0"))
238 .map(|s| s.trim())
239 .map(|s| s.strip_suffix(',').unwrap_or(s))
240 .unwrap_or(formatted)
241 .to_owned()
242 }
243 }
244
245 pub(crate) fn format_stmt_header(&mut self, keyword: &str, code: &str) -> String {
246 if code.trim().is_empty() {
247 String::new()
248 } else {
249 let wrapped = format!("{keyword} ({code}) {{}}");
250 let formatted = self.format_with_external_formatter(
251 wrapped,
252 Hints {
253 print_width: self.print_width,
254 indent_level: 0,
255 attr: false,
256 ext: "js",
257 },
258 );
259 formatted
260 .strip_prefix(keyword)
261 .map(|s| s.trim_start())
262 .and_then(|s| s.strip_prefix('('))
263 .and_then(|s| s.trim_end().strip_suffix('}'))
264 .and_then(|s| s.trim_end().strip_suffix('{'))
265 .and_then(|s| s.trim_end().strip_suffix(')'))
266 .unwrap_or(code)
267 .to_owned()
268 }
269 }
270
271 pub(crate) fn format_script<'a>(
272 &mut self,
273 code: &'a str,
274 lang: &'b str,
275 start: usize,
276 state: &State,
277 ) -> Cow<'a, str> {
278 self.format_with_external_formatter(
279 self.source
280 .get(0..start)
281 .unwrap_or_default()
282 .replace(|c: char| !c.is_ascii_whitespace(), " ")
283 + code,
284 Hints {
285 print_width: self.print_width,
286 indent_level: state.indent_level,
287 attr: false,
288 ext: lang,
289 },
290 )
291 }
292
293 pub(crate) fn format_style<'a>(
294 &mut self,
295 code: &'a str,
296 lang: &'b str,
297 start: usize,
298 state: &State,
299 ) -> Cow<'a, str> {
300 self.format_with_external_formatter(
301 "\n".repeat(
302 self.source
303 .get(0..start)
304 .unwrap_or_default()
305 .lines()
306 .count()
307 .saturating_sub(1),
308 ) + code,
309 Hints {
310 print_width: self
311 .print_width
312 .saturating_sub((state.indent_level as usize) * self.indent_width)
313 .saturating_sub(if self.style_indent() {
314 self.indent_width
315 } else {
316 0
317 }),
318 indent_level: state.indent_level,
319 attr: false,
320 ext: if lang == "postcss" { "css" } else { lang },
321 },
322 )
323 }
324
325 pub(crate) fn format_style_attr(&mut self, code: &str, start: usize, state: &State) -> String {
326 self.format_with_external_formatter(
327 self.source
328 .get(0..start)
329 .unwrap_or_default()
330 .replace(|c: char| !c.is_ascii_whitespace(), " ")
331 + code,
332 Hints {
333 print_width: u16::MAX as usize,
334 indent_level: state.indent_level,
335 attr: true,
336 ext: "css",
337 },
338 )
339 .trim()
340 .to_owned()
341 }
342
343 pub(crate) fn format_json<'a>(
344 &mut self,
345 code: &'a str,
346 start: usize,
347 state: &State,
348 ) -> Cow<'a, str> {
349 self.format_with_external_formatter(
350 self.source
351 .get(0..start)
352 .unwrap_or_default()
353 .replace(|c: char| !c.is_ascii_whitespace(), " ")
354 + code,
355 Hints {
356 print_width: self
357 .print_width
358 .saturating_sub((state.indent_level as usize) * self.indent_width)
359 .saturating_sub(if self.script_indent() {
360 self.indent_width
361 } else {
362 0
363 }),
364 indent_level: state.indent_level,
365 attr: false,
366 ext: "json",
367 },
368 )
369 }
370
371 pub(crate) fn format_jinja(
372 &mut self,
373 code: &str,
374 start: usize,
375 expr: bool,
376 state: &State,
377 ) -> String {
378 self.format_with_external_formatter(
379 self.source
380 .get(0..start)
381 .unwrap_or_default()
382 .replace(|c: char| !c.is_ascii_whitespace(), " ")
383 + code,
384 Hints {
385 print_width: self
386 .print_width
387 .saturating_sub((state.indent_level as usize) * self.indent_width),
388 indent_level: state.indent_level,
389 attr: false,
390 ext: if expr {
391 "markup-fmt-jinja-expr"
392 } else {
393 "markup-fmt-jinja-stmt"
394 },
395 },
396 )
397 .trim_ascii()
398 .to_owned()
399 }
400
401 fn format_with_external_formatter<'a>(
402 &mut self,
403 code: String,
404 hints: Hints<'b>,
405 ) -> Cow<'a, str> {
406 match (self.external_formatter)(&code, hints) {
407 Ok(Cow::Owned(formatted)) => Cow::from(formatted),
408 Ok(Cow::Borrowed(..)) => Cow::from(code),
409 Err(e) => {
410 self.external_formatter_errors.push(e);
411 code.into()
412 }
413 }
414 }
415
416 fn try_format_with_external_formatter<'a>(
417 &mut self,
418 code: String,
419 hints: Hints<'b>,
420 ) -> Result<Cow<'a, str>, E> {
421 match (self.external_formatter)(&code, hints) {
422 Ok(Cow::Owned(formatted)) => Ok(Cow::from(formatted)),
423 Ok(Cow::Borrowed(..)) => Ok(Cow::from(code)),
424 Err(e) => Err(e),
425 }
426 }
427}
428
429pub struct Hints<'s> {
431 pub print_width: usize,
432 pub indent_level: u16,
434 pub attr: bool,
436 pub ext: &'s str,
438}
439
440fn trim_delim<'a>(user_input: &str, formatted: &'a str, start: char, end: char) -> &'a str {
441 if user_input
442 .trim_start()
443 .chars()
444 .take_while(|c| *c == start)
445 .count()
446 < formatted.chars().take_while(|c| *c == start).count()
447 && user_input
448 .trim_end()
449 .chars()
450 .rev()
451 .take_while(|c| *c == end)
452 .count()
453 < formatted.chars().rev().take_while(|c| *c == end).count()
454 {
455 formatted
456 .trim_ascii()
457 .strip_prefix(start)
458 .and_then(|s| s.strip_suffix(end))
459 .unwrap_or(formatted)
460 } else {
461 formatted
462 }
463}