1use anyhow::{bail, Ok, Result};
2
3#[derive(Debug, Clone)]
4pub struct EntryLineLexer<'a> {
5 lines: Vec<&'a str>,
6 index: usize,
7}
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub enum EntryLine<'a> {
11 Empty,
12 Comment(&'a str),
13 ProductionMarker,
14 Header(&'a str),
15 Option(&'a str, &'a str),
16 LockedHeader(&'a str),
17 LockedOption(&'a str, &'a str),
18}
19
20impl<'a> EntryLineLexer<'a> {
21 pub fn new(contents: &'a str) -> Self {
22 let lines = contents.lines().collect();
23 Self { lines, index: 0 }
24 }
25
26 pub fn tokenize(&mut self) -> Result<Vec<EntryLine<'a>>> {
27 let mut res = vec![];
28 while let Some(line) = self.next_line() {
29 if line.trim().starts_with('#') {
30 res.push(tokenize_commented(line));
31 } else {
32 res.push(tokenize_uncommented(line)?);
33 }
34 }
35
36 Ok(res)
37 }
38
39 fn next_line(&mut self) -> Option<&'a str> {
40 let res = self.lines.get(self.index).copied();
41 if self.index < self.lines.len() {
42 self.index += 1;
43 }
44 res
45 }
46}
47
48fn tokenize_commented(line: &str) -> EntryLine {
49 let trimmed = line.trim()[1..].trim();
50 if trimmed == "production" {
51 EntryLine::ProductionMarker
52 } else if trimmed.starts_with('[') && trimmed.ends_with(']') {
53 EntryLine::LockedHeader(&trimmed[1..trimmed.len() - 1])
55 } else if trimmed.contains('=') {
56 let [key, value]: [&str; 2] = trimmed
58 .splitn(2, '=')
59 .collect::<Vec<_>>()
60 .try_into()
61 .expect("should always be splitted to two entries");
62 let key = key.trim_end();
63 let value = value.trim_start();
64 EntryLine::LockedOption(key, value)
65 } else {
66 let start = if line.trim().starts_with("# ") { 2 } else { 1 };
68 EntryLine::Comment(&line[start..])
69 }
70}
71
72fn tokenize_uncommented(line: &str) -> Result<EntryLine> {
73 if line.starts_with('[') && line.ends_with(']') {
74 Ok(EntryLine::Header(&line[1..line.len() - 1]))
76 } else if line.contains('=') {
77 let [key, value]: [&str; 2] = line
79 .splitn(2, '=')
80 .collect::<Vec<_>>()
81 .try_into()
82 .expect("should always be splitted to two entries");
83 let key = key.trim_end();
84 let value = value.trim_start();
85 Ok(EntryLine::Option(key, value))
86 } else if line.trim() == "" {
87 Ok(EntryLine::Empty)
88 } else {
89 bail!("unexpected line: {:?}", line);
90 }
91}