dprint_vue_plugin/
format.rs

1use std::borrow::Cow;
2use std::cmp::Ordering;
3use std::fmt::Write;
4use std::iter::repeat;
5use std::path::Path;
6use std::path::PathBuf;
7
8use anyhow::Result;
9use dprint_core::configuration::ConfigKeyMap;
10
11use crate::configuration::Configuration;
12use vue_sfc::{Block, Section};
13
14fn default_lang(block: &str) -> Option<&'static str> {
15    match block {
16        "template" => Some("html"),
17        "script" => Some("js"),
18        "style" => Some("css"),
19        _ => None,
20    }
21}
22
23pub fn format(
24    _path: &Path,
25    content: &str,
26    config: &Configuration,
27    mut format_with_host: impl FnMut(&Path, String, &ConfigKeyMap) -> Result<Option<String>>,
28) -> Result<String> {
29    let mut buffer = String::new();
30
31    let mut sections = vue_sfc::parse(content)?.into_iter().peekable();
32
33    while let Some(section) = sections.next() {
34        if let Section::Block(block) = section {
35            writeln!(
36                &mut buffer,
37                "{}",
38                format_block(block, config, &mut format_with_host)?
39            )?;
40        } else {
41            writeln!(&mut buffer, "{}", section)?;
42        }
43
44        if sections.peek().is_some() {
45            writeln!(&mut buffer)?;
46        }
47    }
48
49    Ok(buffer)
50}
51
52fn format_block<'a>(
53    block: Block<'a>,
54    config: &Configuration,
55    format_with_host: &mut impl FnMut(&Path, String, &ConfigKeyMap) -> Result<Option<String>>,
56) -> Result<Block<'a>> {
57    let lang = block
58        .attributes
59        .iter()
60        .find_map(|(name, value)| match (name.as_str(), value) {
61            ("lang", Some(value)) => Some(value.as_str()),
62            _ => None,
63        })
64        .or_else(|| default_lang(&block.name));
65
66    if let Some(lang) = lang {
67        let pretty = {
68            let file_path = PathBuf::from(format!("file.vue.{lang}"));
69            let Some(pretty) =
70                format_with_host(&file_path, block.content.to_string(), &ConfigKeyMap::new())?
71            else {
72                return Ok(block);
73            };
74
75            let indent_width = pretty
76                .lines()
77                .filter_map(|line| {
78                    let trimed_line = line.trim_start();
79                    if trimed_line.is_empty() {
80                        None
81                    } else {
82                        Some(line.len() - trimed_line.len())
83                    }
84                })
85                .min()
86                .unwrap_or(0);
87
88            let desired_indent_width =
89                if block.name.as_str() == "template" && config.indent_template {
90                    usize::from(config.indent_width)
91                } else {
92                    0
93                };
94
95            match indent_width.cmp(&desired_indent_width) {
96                Ordering::Equal => pretty,
97                Ordering::Less => {
98                    let delta = desired_indent_width - indent_width;
99
100                    let mut buffer =
101                        String::with_capacity(pretty.len() + pretty.lines().count() * delta);
102
103                    for line in pretty.lines() {
104                        buffer.extend(repeat(if config.use_tabs { '\t' } else { ' ' }).take(delta));
105                        buffer.push_str(line);
106                        buffer.push('\n');
107                    }
108
109                    buffer
110                }
111                Ordering::Greater => {
112                    let delta = indent_width - desired_indent_width;
113
114                    let mut buffer =
115                        String::with_capacity(pretty.len() - pretty.lines().count() * delta);
116
117                    for line in pretty.lines() {
118                        buffer.push_str(&line[delta..]);
119                        buffer.push('\n');
120                    }
121
122                    buffer
123                }
124            }
125        };
126
127        Ok(Block {
128            name: block.name,
129            attributes: block.attributes,
130            content: Cow::Owned(pretty),
131        })
132    } else {
133        Ok(block)
134    }
135}
136
137#[cfg(test)]
138mod test {
139    use std::path::{Path, PathBuf};
140
141    use crate::configuration::Configuration;
142
143    use super::format;
144
145    #[test]
146    fn test_format_with_host() {
147        let config = Configuration {
148            indent_template: true,
149            use_tabs: false,
150            indent_width: 2,
151        };
152
153        let raw = "<template></template><script></script>";
154
155        let mut buffer = Vec::new();
156
157        format(Path::new("file.vue"), raw, &config, |path, content, _| {
158            buffer.push((path.to_owned(), content.clone()));
159            Ok(Some(content))
160        })
161        .unwrap();
162
163        assert_eq!(buffer[0], (PathBuf::from("file.vue.html"), String::new()));
164
165        assert_eq!(buffer[1], (PathBuf::from("file.vue.js"), String::new()));
166    }
167
168    #[test]
169    fn test_indent_template() {
170        let config = Configuration {
171            indent_template: true,
172            use_tabs: false,
173            indent_width: 2,
174        };
175
176        assert_eq!(
177            format(
178                Path::new("file.vue"),
179                "<template><div></div></template>",
180                &config,
181                |_, raw, _| Ok(Some(raw))
182            )
183            .unwrap(),
184            "<template>\n  <div></div>\n</template>"
185        );
186
187        assert_eq!(
188            format(
189                Path::new("file.vue"),
190                "<template>\n  <div></div>\n\n  <div></div>\n</template>",
191                &config,
192                |_, raw, _| Ok(Some(raw))
193            )
194            .unwrap(),
195            "<template>\n  <div></div>\n\n  <div></div>\n</template>"
196        );
197    }
198}