editorconfig_parser/
lib.rs

1#[derive(Debug, Default, Clone)]
2pub struct EditorConfig<'a> {
3    /// Set to true to tell the core not to check any higher directory for EditorConfig settings for on the current filename.
4    root: bool,
5
6    sections: Vec<EditorConfigSection<'a>>,
7}
8
9impl<'a> EditorConfig<'a> {
10    pub fn root(&self) -> bool {
11        self.root
12    }
13
14    pub fn sections(&self) -> &[EditorConfigSection<'a>] {
15        &self.sections
16    }
17}
18
19/// <https://spec.editorconfig.org/index.html>
20#[derive(Debug, Default, Clone, Eq, PartialEq)]
21pub struct EditorConfigSection<'a> {
22    /// Section Name: the string between the beginning `[` and the ending `]`.
23    pub name: &'a str,
24
25    /// Set to tab or space to use tabs or spaces for indentation, respectively.
26    /// Option tab implies that an indentation is to be filled by as many hard tabs as possible, with the rest of the indentation filled by spaces.
27    /// A non-normative explanation can be found in the indentation_ section.
28    /// The values are case-insensitive.
29    pub indent_style: Option<IdentStyle>,
30
31    /// Set to a whole number defining the number of columns used for each indentation level and the width of soft tabs (when supported).
32    /// If this equals tab, the indent_size shall be set to the tab size, which should be tab_width (if specified); else, the tab size set by the editor.
33    /// The values are case-insensitive.
34    pub indent_size: Option<usize>,
35
36    /// Set to a whole number defining the number of columns used to represent a tab character.
37    /// This defaults to the value of indent_size and should not usually need to be specified.
38    pub tab_width: Option<usize>,
39
40    /// Set to lf, cr, or crlf to control how line breaks are represented.
41    /// The values are case-insensitive.
42    pub end_of_line: Option<EndOfLine>,
43
44    /// Set to latin1, utf-8, utf-8-bom, utf-16be or utf-16le to control the character set.
45    /// Use of utf-8-bom is discouraged.
46    /// The values are case-insensitive.
47    pub charset: Option<Charset>,
48
49    /// Set to true to remove all whitespace characters preceding newline characters in the file and false to ensure it doesn’t.
50    pub trim_trailing_whitespace: Option<bool>,
51
52    /// Set to true ensure file ends with a newline when saving and false to ensure it doesn’t.
53    /// Editors must not insert newlines in empty files when saving those files, even if insert_final_newline = true.
54    pub insert_final_newline: Option<bool>,
55
56    /// Prettier print width.
57    /// Not part of spec <https://github.com/editorconfig/editorconfig-vscode/issues/53#issuecomment-462432616>
58    /// But documented in <https://prettier.io/docs/next/configuration#editorconfig>
59    pub max_line_length: Option<usize>,
60}
61
62#[derive(Debug, Clone, Copy, Eq, PartialEq)]
63pub enum IdentStyle {
64    Tab,
65    Space,
66}
67
68impl IdentStyle {
69    fn parse(s: &str) -> Option<Self> {
70        if s.eq_ignore_ascii_case("tab") {
71            Some(Self::Tab)
72        } else if s.eq_ignore_ascii_case("space") {
73            Some(Self::Space)
74        } else {
75            None
76        }
77    }
78}
79
80#[derive(Debug, Clone, Copy, Eq, PartialEq)]
81pub enum EndOfLine {
82    Lf,
83    Cr,
84    Crlf,
85}
86
87impl EndOfLine {
88    fn parse(s: &str) -> Option<Self> {
89        if s.eq_ignore_ascii_case("lf") {
90            Some(Self::Lf)
91        } else if s.eq_ignore_ascii_case("cr") {
92            Some(Self::Cr)
93        } else if s.eq_ignore_ascii_case("crlf") {
94            Some(Self::Crlf)
95        } else {
96            None
97        }
98    }
99}
100
101#[derive(Debug, Clone, Copy, Eq, PartialEq)]
102pub enum Charset {
103    Latin1,
104    Utf8,
105    Utf8bom,
106    Utf16be,
107    Utf16le,
108}
109
110impl Charset {
111    fn parse(s: &str) -> Option<Self> {
112        if s.eq_ignore_ascii_case("utf-8") {
113            Some(Self::Utf8)
114        } else if s.eq_ignore_ascii_case("latin1") {
115            Some(Self::Latin1)
116        } else if s.eq_ignore_ascii_case("utf-16be") {
117            Some(Self::Utf16be)
118        } else if s.eq_ignore_ascii_case("utf-16le") {
119            Some(Self::Utf16le)
120        } else if s.eq_ignore_ascii_case("utf-8-bom") {
121            Some(Self::Utf8bom)
122        } else {
123            None
124        }
125    }
126}
127
128impl<'a> EditorConfig<'a> {
129    /// <https://spec.editorconfig.org/index.html#id6>
130    pub fn parse(source_text: &'a str) -> Self {
131        // EditorConfig files are in an INI-like file format.
132        // To read an EditorConfig file, take one line at a time, from beginning to end.
133        // For each line:
134        // 1. Remove all leading and trailing whitespace.
135        // 2. Process the remaining text as specified for its type below.
136        let mut root = false;
137        let mut sections = vec![];
138        let mut preamble = true;
139        for line in source_text.lines() {
140            let line = line.trim();
141            // Blank: Contains nothing. Blank lines are ignored.
142            if line.is_empty() {
143                continue;
144            }
145            // Comment: starts with a ; or a #. Comment lines are ignored.
146            if line.starts_with([';', '#']) {
147                continue;
148            }
149            // Parse `root`. Must be specified in the preamble. The value is case-insensitive.
150            if preamble
151                && !line.starts_with('[')
152                && let Some((key, value)) = line.split_once('=')
153                && key.trim_end() == "root"
154                && value.trim_start().eq_ignore_ascii_case("true")
155            {
156                root = true;
157            }
158            // Section Header: starts with a [ and ends with a ]. These lines define globs;
159            if let Some(line) = line.strip_prefix('[') {
160                preamble = false;
161                if let Some(line) = line.strip_suffix(']') {
162                    let name = line;
163                    sections.push(EditorConfigSection { name, ..EditorConfigSection::default() });
164                }
165            }
166            // Key-Value Pair (or Pair): contains a key and a value, separated by an `=`.
167            if let Some(section) = sections.last_mut()
168                && let Some((key, value)) = line.split_once('=')
169            {
170                let value = value.trim_start();
171                match key.trim_end() {
172                    "indent_style" => {
173                        section.indent_style = IdentStyle::parse(value);
174                    }
175                    "indent_size" => {
176                        section.indent_size = parse_number(value);
177                    }
178                    "tab_width" => {
179                        section.indent_size = parse_number(value);
180                    }
181                    "end_of_line" => {
182                        section.end_of_line = EndOfLine::parse(value);
183                    }
184                    "charset" => {
185                        section.charset = Charset::parse(value);
186                    }
187                    "trim_trailing_whitespace" => {
188                        section.trim_trailing_whitespace = parse_bool(value);
189                    }
190                    "insert_final_newline" => {
191                        section.insert_final_newline = parse_bool(value);
192                    }
193                    "max_line_length" => {
194                        section.max_line_length = parse_number(value);
195                    }
196                    _ => {}
197                }
198            }
199        }
200
201        Self { root, sections }
202    }
203}
204
205fn parse_number(s: &str) -> Option<usize> {
206    s.parse::<usize>().ok()
207}
208
209fn parse_bool(s: &str) -> Option<bool> {
210    if s.eq_ignore_ascii_case("true") {
211        Some(true)
212    } else if s.eq_ignore_ascii_case("false") {
213        Some(false)
214    } else {
215        None
216    }
217}