1use super::model::{DuplicateBlockKey, DuplicateFieldKey, Entry, Field, Library, StringTable};
45use std::collections::HashSet;
46
47pub fn parse(source: &str) -> Library {
48 let chars: Vec<char> = source.chars().collect();
49 let parser = Parser { chars: &chars };
50 parser.run()
51}
52
53struct Parser<'a> {
54 chars: &'a [char],
55}
56
57impl<'a> Parser<'a> {
58 fn len(&self) -> usize {
59 self.chars.len()
60 }
61
62 fn run(&self) -> Library {
63 let mut library = Library::default();
64 let mut strings: StringTable = StringTable::new();
65 let mut seen_keys: HashSet<String> = HashSet::new();
66 let n = self.len();
67 let mut pos = 0usize;
68
69 while pos < n {
70 let Some(at) = self.find_from(pos, '@') else {
71 break;
72 };
73 let (directive_type, after_type) = self.read_word(at + 1);
74 let open = self.skip_ws(after_type);
75 let Some((open_delim, close_delim)) = self.opener_at(open) else {
76 pos = at + 1;
77 continue;
78 };
79 let block_start = open + 1;
80 let Some(block_end) = self.find_matching_brace(block_start, close_delim) else {
81 break;
83 };
84
85 let type_lower = directive_type.to_ascii_lowercase();
86 match type_lower.as_str() {
87 "string" => self.parse_string_directive(block_start, block_end, &mut strings),
88 "comment" | "preamble" => {} _ => {
90 let entry = self.parse_entry_body(
99 type_lower.clone(),
100 at,
101 block_start,
102 block_end,
103 &strings,
104 );
105 self.commit_entry(entry, &mut library, &mut seen_keys);
106 }
107 }
108
109 pos = block_end + 1;
110 let _ = open_delim;
111 }
112
113 library
114 }
115
116 fn commit_entry(
117 &self,
118 entry: EntryParse,
119 library: &mut Library,
120 seen_keys: &mut HashSet<String>,
121 ) {
122 let key_taken = !entry.entry.key.is_empty() && !seen_keys.insert(entry.entry.key.clone());
123 if key_taken {
124 library.duplicate_block_keys.push(DuplicateBlockKey {
125 key: entry.entry.key.clone(),
126 start_line: entry.entry.start_line,
127 entry: entry.entry,
128 });
129 return;
130 }
131 if !entry.duplicate_field_names.is_empty() {
132 library.duplicate_field_keys.push(DuplicateFieldKey {
133 start_line: entry.entry.start_line,
134 duplicate_keys: entry.duplicate_field_names.clone(),
135 entry: entry.entry.clone(),
136 });
137 }
138 library.entries.push(entry.entry);
139 }
140
141 fn parse_string_directive(&self, start: usize, end: usize, strings: &mut StringTable) {
142 let mut pos = self.skip_ws(start);
143 let (name, after_name) = self.read_ident(pos);
144 if name.is_empty() {
145 return;
146 }
147 pos = self.skip_ws(after_name);
148 if pos >= end || self.chars[pos] != '=' {
149 return;
150 }
151 pos = self.skip_ws(pos + 1);
152 let (value, _next) = self.parse_value(pos, end, strings);
153 strings.insert(name.to_ascii_lowercase(), value);
154 }
155
156 fn parse_entry_body(
157 &self,
158 entry_type: String,
159 at_pos: usize,
160 start: usize,
161 end: usize,
162 strings: &StringTable,
163 ) -> EntryParse {
164 let line_index = self.line_index_of(at_pos);
165 let mut pos = self.skip_ws(start);
166 let (key, after_key) = self.read_key(pos, end);
167 pos = self.skip_ws_or_comma(after_key, end);
168
169 let mut fields: Vec<Field> = Vec::new();
170 let mut seen_field_names: HashSet<String> = HashSet::new();
171 let mut duplicate_field_names: Vec<String> = Vec::new();
172
173 while pos < end {
174 let (field_name, after_name) = self.read_ident(pos);
175 if field_name.is_empty() {
176 break;
177 }
178 pos = self.skip_ws(after_name);
179 if pos >= end || self.chars[pos] != '=' {
180 break;
181 }
182 pos = self.skip_ws(pos + 1);
183 let (value, after_value) = self.parse_value(pos, end, strings);
184 pos = self.skip_ws(after_value);
185
186 let lower = field_name.to_ascii_lowercase();
187 if !seen_field_names.insert(lower.clone()) {
188 if !duplicate_field_names.contains(&lower) {
189 duplicate_field_names.push(lower);
190 }
191 fields.retain(|f| !f.key.eq_ignore_ascii_case(&field_name));
193 }
194 fields.push(Field {
195 key: field_name,
196 value,
197 });
198
199 pos = self.skip_ws(pos);
200 if pos < end && self.chars[pos] == ',' {
201 pos = self.skip_ws(pos + 1);
202 } else {
203 break;
204 }
205 }
206
207 duplicate_field_names.sort();
208 EntryParse {
209 entry: Entry {
210 key,
211 entry_type,
212 start_line: line_index,
213 fields,
214 },
215 duplicate_field_names,
216 }
217 }
218
219 fn parse_value(&self, mut pos: usize, end: usize, strings: &StringTable) -> (String, usize) {
223 let mut out = String::new();
224 loop {
225 pos = self.skip_ws(pos);
226 if pos >= end {
227 break;
228 }
229 match self.chars[pos] {
230 '{' => {
231 let Some(close) = self.find_matching_brace(pos + 1, '}') else {
232 break;
233 };
234 out.push_str(&self.unescape_quotes(pos + 1, close));
235 pos = close + 1;
236 }
237 '"' => {
238 let Some(close) = self.find_unescaped_quote(pos + 1, end) else {
239 break;
240 };
241 out.push_str(&self.unescape_quotes(pos + 1, close));
242 pos = close + 1;
243 }
244 _ => {
245 let (tok, after) = self.read_ident_or_number(pos);
246 if tok.is_empty() {
247 break;
248 }
249 if let Some(resolved) = strings.get(&tok.to_ascii_lowercase()) {
250 out.push_str(resolved);
251 } else {
252 out.push_str(&tok);
253 }
254 pos = after;
255 }
256 }
257 let after_ws = self.skip_ws(pos);
258 if after_ws < end && self.chars[after_ws] == '#' {
259 pos = after_ws + 1;
260 continue;
261 }
262 break;
263 }
264 (out.trim().to_string(), pos)
265 }
266
267 fn unescape_quotes(&self, start: usize, end: usize) -> String {
270 let mut out = String::with_capacity(end - start);
271 let mut i = start;
272 while i < end {
273 if self.chars[i] == '\\' && i + 1 < end && self.chars[i + 1] == '"' {
274 out.push('"');
275 i += 2;
276 } else {
277 out.push(self.chars[i]);
278 i += 1;
279 }
280 }
281 out
282 }
283
284 fn read_key(&self, start: usize, end: usize) -> (String, usize) {
287 let mut i = start;
288 while i < end && self.chars[i] != ',' {
289 i += 1;
290 }
291 (
292 self.chars[start..i]
293 .iter()
294 .collect::<String>()
295 .trim()
296 .to_string(),
297 i,
298 )
299 }
300
301 fn skip_ws_or_comma(&self, mut pos: usize, end: usize) -> usize {
302 if pos < end && self.chars[pos] == ',' {
303 pos += 1;
304 }
305 self.skip_ws(pos)
306 }
307
308 fn skip_ws(&self, mut pos: usize) -> usize {
309 while pos < self.len() && self.chars[pos].is_whitespace() {
310 pos += 1;
311 }
312 pos
313 }
314
315 fn find_from(&self, start: usize, needle: char) -> Option<usize> {
316 (start..self.len()).find(|&i| self.chars[i] == needle)
317 }
318
319 fn read_word(&self, start: usize) -> (String, usize) {
322 let n = self.len();
323 let mut i = start;
324 while i < n && self.chars[i].is_ascii_alphabetic() {
325 i += 1;
326 }
327 (self.chars[start..i].iter().collect(), i)
328 }
329
330 fn read_ident(&self, start: usize) -> (String, usize) {
332 let n = self.len();
333 let mut i = start;
334 while i < n
335 && (self.chars[i].is_ascii_alphanumeric()
336 || matches!(self.chars[i], '-' | '_' | ':' | '.'))
337 {
338 i += 1;
339 }
340 (self.chars[start..i].iter().collect(), i)
341 }
342
343 fn read_ident_or_number(&self, start: usize) -> (String, usize) {
344 self.read_ident(start)
345 }
346
347 fn opener_at(&self, pos: usize) -> Option<(char, char)> {
350 if pos >= self.len() {
351 return None;
352 }
353 match self.chars[pos] {
354 '{' => Some(('{', '}')),
355 '(' => Some(('(', ')')),
356 _ => None,
357 }
358 }
359
360 fn find_matching_brace(&self, start: usize, closer: char) -> Option<usize> {
366 let opener = if closer == '}' { '{' } else { '(' };
367 let n = self.len();
368 let mut depth = 1i32;
369 let mut i = start;
370 while i < n {
371 if self.chars[i] == opener {
372 depth += 1;
373 } else if self.chars[i] == closer {
374 depth -= 1;
375 if depth == 0 {
376 return Some(i);
377 }
378 }
379 i += 1;
380 }
381 None
382 }
383
384 fn find_unescaped_quote(&self, start: usize, end: usize) -> Option<usize> {
385 let mut i = start;
386 while i < end {
387 if self.chars[i] == '"' && (i == start || self.chars[i - 1] != '\\') {
388 return Some(i);
389 }
390 i += 1;
391 }
392 None
393 }
394
395 fn line_index_of(&self, pos: usize) -> u32 {
396 self.chars[..pos].iter().filter(|&&c| c == '\n').count() as u32
397 }
398}
399
400struct EntryParse {
401 entry: Entry,
402 duplicate_field_names: Vec<String>,
403}