editorconfig_parser/
lib.rs1use std::path::{Path, PathBuf};
2
3use globset::{Glob, GlobMatcher};
4
5#[derive(Debug, Default, Clone)]
6pub struct EditorConfig {
7 root: bool,
9
10 sections: Vec<EditorConfigSection>,
11
12 cwd: Option<PathBuf>,
14}
15
16impl EditorConfig {
17 pub fn root(&self) -> bool {
18 self.root
19 }
20
21 pub fn sections(&self) -> &[EditorConfigSection] {
22 &self.sections
23 }
24
25 pub fn cwd(&self) -> Option<&Path> {
26 self.cwd.as_deref()
27 }
28
29 pub fn with_cwd<P: AsRef<Path>>(mut self, cwd: P) -> Self {
31 self.cwd = Some(cwd.as_ref().to_path_buf());
32 self
33 }
34}
35
36#[derive(Debug, Default, Clone)]
38pub struct EditorConfigSection {
39 pub name: String,
41
42 pub matcher: Option<GlobMatcher>,
43
44 pub properties: EditorConfigProperties,
45}
46
47#[derive(Debug, Default, Clone, Eq, PartialEq)]
48pub enum EditorConfigProperty<T> {
49 #[default]
50 None,
51 Unset,
52 Value(T),
53}
54
55#[derive(Debug, Default, Clone, Eq, PartialEq)]
56pub struct EditorConfigProperties {
57 pub indent_style: EditorConfigProperty<IndentStyle>,
62
63 pub indent_size: EditorConfigProperty<usize>,
67
68 pub tab_width: EditorConfigProperty<usize>,
71
72 pub end_of_line: EditorConfigProperty<EndOfLine>,
75
76 pub charset: EditorConfigProperty<Charset>,
80
81 pub trim_trailing_whitespace: EditorConfigProperty<bool>,
83
84 pub insert_final_newline: EditorConfigProperty<bool>,
87
88 pub max_line_length: EditorConfigProperty<MaxLineLength>,
92}
93
94#[derive(Debug, Clone, Copy, Eq, PartialEq)]
95pub enum MaxLineLength {
96 Number(usize),
98 Off,
100}
101
102#[derive(Debug, Clone, Copy, Eq, PartialEq)]
103pub enum IndentStyle {
104 Tab,
105 Space,
106}
107
108#[derive(Debug, Clone, Copy, Eq, PartialEq)]
109pub enum EndOfLine {
110 Lf,
111 Cr,
112 Crlf,
113}
114
115#[derive(Debug, Clone, Copy, Eq, PartialEq)]
116pub enum Charset {
117 Latin1,
118 Utf8,
119 Utf8bom,
120 Utf16be,
121 Utf16le,
122}
123
124impl EditorConfig {
125 pub fn parse(source_text: &str) -> Self {
127 let mut root = false;
133 let mut sections = vec![];
134 let mut preamble = true;
135 for line in source_text.lines() {
136 let line = line.trim();
137 if line.is_empty() {
139 continue;
140 }
141 if line.starts_with([';', '#']) {
143 continue;
144 }
145 if preamble
147 && !line.starts_with('[')
148 && let Some((key, value)) = line.split_once('=')
149 && key.trim_end() == "root"
150 && value.trim_start().eq_ignore_ascii_case("true")
151 {
152 root = true;
153 }
154 if let Some(line) = line.strip_prefix('[') {
156 preamble = false;
157 if let Some(line) = line.strip_suffix(']') {
158 let name = line.to_string();
159 let matcher = Glob::new(&name).ok().map(|glob| glob.compile_matcher());
160 sections.push(EditorConfigSection {
161 name,
162 matcher,
163 ..EditorConfigSection::default()
164 });
165 }
166 }
167 if let Some(section) = sections.last_mut()
169 && let Some((key, value)) = line.split_once('=')
170 {
171 let value = value.trim_start();
172 let properties = &mut section.properties;
173 match key.trim_end() {
174 "indent_style" => {
175 properties.indent_style = IndentStyle::parse(value);
176 }
177 "indent_size" => {
178 properties.indent_size = EditorConfigProperty::<usize>::parse(value);
179 }
180 "tab_width" => {
181 properties.tab_width = EditorConfigProperty::<usize>::parse(value);
182 }
183 "end_of_line" => {
184 properties.end_of_line = EditorConfigProperty::<EndOfLine>::parse(value);
185 }
186 "charset" => {
187 properties.charset = EditorConfigProperty::<Charset>::parse(value);
188 }
189 "trim_trailing_whitespace" => {
190 properties.trim_trailing_whitespace =
191 EditorConfigProperty::<bool>::parse(value);
192 }
193 "insert_final_newline" => {
194 properties.insert_final_newline =
195 EditorConfigProperty::<bool>::parse(value);
196 }
197 "max_line_length" => {
198 properties.max_line_length =
199 EditorConfigProperty::<MaxLineLength>::parse(value);
200 }
201 _ => {}
202 }
203 }
204 }
205
206 Self { root, sections, cwd: None }
207 }
208
209 pub fn resolve(&self, path: &Path) -> EditorConfigProperties {
212 let path =
213 if let Some(cwd) = &self.cwd { path.strip_prefix(cwd).unwrap_or(path) } else { path };
214 let mut properties = EditorConfigProperties::default();
215 for section in &self.sections {
216 if section.matcher.as_ref().is_some_and(|matcher| matcher.is_match(path)) {
217 properties.override_with(§ion.properties);
218 }
219 }
220 properties
221 }
222}
223
224impl<T: Copy> EditorConfigProperty<T> {
225 fn override_with(&mut self, other: &Self) {
226 match other {
227 Self::Value(value) => {
228 *self = Self::Value(*value);
229 }
230 Self::Unset => {
231 *self = Self::None;
232 }
233 Self::None => {}
234 }
235 }
236}
237
238impl EditorConfigProperties {
239 fn override_with(&mut self, other: &Self) {
240 self.indent_style.override_with(&other.indent_style);
241 self.indent_size.override_with(&other.indent_size);
242 self.tab_width.override_with(&other.tab_width);
243 self.end_of_line.override_with(&other.end_of_line);
244 self.charset.override_with(&other.charset);
245 self.trim_trailing_whitespace.override_with(&other.trim_trailing_whitespace);
246 self.insert_final_newline.override_with(&other.insert_final_newline);
247 self.max_line_length.override_with(&other.max_line_length);
248 }
249}
250
251impl EditorConfigProperty<usize> {
252 fn parse(s: &str) -> Self {
253 if s.eq_ignore_ascii_case("unset") {
254 Self::Unset
255 } else {
256 s.parse::<usize>().map_or(Self::None, EditorConfigProperty::Value)
257 }
258 }
259}
260
261impl EditorConfigProperty<bool> {
262 fn parse(s: &str) -> Self {
263 if s.eq_ignore_ascii_case("true") {
264 EditorConfigProperty::Value(true)
265 } else if s.eq_ignore_ascii_case("false") {
266 EditorConfigProperty::Value(false)
267 } else if s.eq_ignore_ascii_case("unset") {
268 EditorConfigProperty::Unset
269 } else {
270 EditorConfigProperty::None
271 }
272 }
273}
274
275impl IndentStyle {
276 fn parse(s: &str) -> EditorConfigProperty<Self> {
277 if s.eq_ignore_ascii_case("tab") {
278 EditorConfigProperty::Value(Self::Tab)
279 } else if s.eq_ignore_ascii_case("space") {
280 EditorConfigProperty::Value(Self::Space)
281 } else if s.eq_ignore_ascii_case("unset") {
282 EditorConfigProperty::Unset
283 } else {
284 EditorConfigProperty::None
285 }
286 }
287}
288
289impl EditorConfigProperty<EndOfLine> {
290 fn parse(s: &str) -> Self {
291 if s.eq_ignore_ascii_case("lf") {
292 Self::Value(EndOfLine::Lf)
293 } else if s.eq_ignore_ascii_case("cr") {
294 Self::Value(EndOfLine::Cr)
295 } else if s.eq_ignore_ascii_case("crlf") {
296 Self::Value(EndOfLine::Crlf)
297 } else if s.eq_ignore_ascii_case("unset") {
298 Self::Unset
299 } else {
300 Self::None
301 }
302 }
303}
304
305impl EditorConfigProperty<Charset> {
306 fn parse(s: &str) -> Self {
307 if s.eq_ignore_ascii_case("utf-8") {
308 Self::Value(Charset::Utf8)
309 } else if s.eq_ignore_ascii_case("latin1") {
310 Self::Value(Charset::Latin1)
311 } else if s.eq_ignore_ascii_case("utf-16be") {
312 Self::Value(Charset::Utf16be)
313 } else if s.eq_ignore_ascii_case("utf-16le") {
314 Self::Value(Charset::Utf16le)
315 } else if s.eq_ignore_ascii_case("utf-8-bom") {
316 Self::Value(Charset::Utf8bom)
317 } else if s.eq_ignore_ascii_case("unset") {
318 Self::Unset
319 } else {
320 Self::None
321 }
322 }
323}
324
325impl EditorConfigProperty<MaxLineLength> {
326 fn parse(s: &str) -> Self {
327 if s.eq_ignore_ascii_case("off") {
328 Self::Value(MaxLineLength::Off)
329 } else if s.eq_ignore_ascii_case("unset") {
330 Self::Unset
331 } else if let Ok(n) = s.parse::<usize>() {
332 Self::Value(MaxLineLength::Number(n))
333 } else {
334 Self::None
335 }
336 }
337}