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}