1use nom::branch::alt;
2use nom::bytes::complete::tag;
3use nom::bytes::complete::take_till1;
4use nom::character::complete::alphanumeric1;
5use nom::character::complete::char;
6use nom::character::complete::line_ending;
7use nom::character::complete::multispace1;
8use nom::character::complete::not_line_ending;
9use nom::character::complete::space0;
10use nom::character::complete::space1;
11use nom::combinator::all_consuming;
12use nom::combinator::map;
13use nom::combinator::not;
14use nom::combinator::peek;
15use nom::combinator::recognize;
16use nom::multi::many0;
17use nom::multi::many1;
18use nom::sequence::delimited;
19use nom::sequence::preceded;
20use nom::sequence::terminated;
21use nom::sequence::tuple;
22use nom::IResult;
23use std::collections::HashMap;
24
25pub fn comment(input: &str) -> IResult<&str, &str> {
27 preceded(char('#'), not_line_ending)(input)
28}
29
30pub fn blank_lines(input: &str) -> IResult<&str, &str> {
31 recognize(many0(alt((multispace1, comment))))(input)
32}
33
34pub fn parse_file(input: &str) -> IResult<&str, HashMap<String, Vec<Vec<&str>>>> {
36 use std::collections::hash_map::Entry;
37
38 map(all_consuming(many0(section)), |sections: Vec<_>| {
39 let mut map: HashMap<String, Vec<Vec<&str>>> = HashMap::new();
40 for (name, lines) in sections {
41 match map.entry(name.to_string()) {
42 Entry::Occupied(mut x) => {
43 x.get_mut().extend(lines);
44 }
45 Entry::Vacant(x) => {
46 x.insert(lines);
47 }
48 }
49 }
50 map
51 })(input)
52}
53
54fn section(input: &str) -> IResult<&str, (&str, Vec<Vec<&str>>)> {
56 terminated(
57 tuple((
58 delimited(blank_lines, section_name, blank_lines),
59 many0(section_line),
60 )),
61 terminated(section_end, blank_lines),
62 )(input)
63}
64
65fn section_name(input: &str) -> IResult<&str, &str> {
67 terminated(
68 recognize(many1(alt((alphanumeric1, tag("_"))))),
69 terminated(space0, line_ending),
70 )(input)
71}
72
73fn section_end(input: &str) -> IResult<&str, &str> {
75 tag("end")(input)
76}
77
78fn section_line(input: &str) -> IResult<&str, Vec<&str>> {
80 terminated(preceded(not(peek(section_end)), columns), blank_lines)(input)
81}
82
83fn columns(input: &str) -> IResult<&str, Vec<&str>> {
85 many0(delimited(column_space, column, column_space))(input)
86}
87
88fn column(input: &str) -> IResult<&str, &str> {
89 take_till1(|c: char| !c.is_ascii_graphic() || c == '#' || c == ',')(input)
90}
91
92pub fn column_space(input: &str) -> IResult<&str, &str> {
93 recognize(many0(alt((space1, comment, tag(",")))))(input)
94}
95
96pub fn parse(
97 input: &str,
98) -> Result<std::collections::HashMap<String, Vec<Vec<&str>>>, nom::Err<nom::error::Error<&str>>> {
99 parse_file(input).map(|(_, x)| x)
100}
101
102#[cfg(test)]
103mod tests {
104 use crate::parse;
105
106 #[test]
107 fn test1() {
108 let parsed = parse(
109 r#"
110
111 objs
112 1
113 #test
114 end
115 objs
116 2
117 end
118
119 #emptysection
120 2dfx
121 end
122
123 cars
124 test#comment
125 end
126
127 objs
128 3 4 5
129 end
130
131 "#,
132 );
133
134 assert_eq!(parsed.is_ok(), true);
135
136 let content = parsed.unwrap();
137
138 let objs = content.get("objs").unwrap();
139 assert_eq!(objs.len(), 3);
140
141 assert_eq!(objs[0][0], "1");
142 assert_eq!(objs[1][0], "2");
143 assert_eq!(objs[2][0], "3");
144 assert_eq!(objs[2][1], "4");
145 assert_eq!(objs[2][2], "5");
146
147 let cars = content.get("cars").unwrap();
148 assert_eq!(cars.len(), 1);
149 assert_eq!(cars[0][0], "test");
150 }
151}