Skip to main content

markup_fmt/
ctx.rs

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, &QUOTES);
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('"', "&quot;"),
116                Quotes::Single => proceeded.replace('\'', "&#x27;"),
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            // Trim original code before sending it to the external formatter.
143            // This makes sure the code will be trimmed
144            // though external formatter isn't available.
145            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
429/// Hints provide some useful additional information to the external formatter.
430pub struct Hints<'s> {
431    pub print_width: usize,
432    /// current indent width = indent width in config * indent level
433    pub indent_level: u16,
434    /// Whether the code is inside attribute.
435    pub attr: bool,
436    /// Fake file extension.
437    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}