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