dprint_vue_plugin/
format.rs1use 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}