gta_ide_parser/
lib.rs

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
25/// Comments are prefixed with the # symbol.
26pub 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
34/// A file is a set of sections
35pub 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
54/// Sections are lines between section...end
55fn 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
65/// Section name could start with a digit, e.g. 2dfx
66fn section_name(input: &str) -> IResult<&str, &str> {
67    terminated(
68        recognize(many1(alt((alphanumeric1, tag("_"))))),
69        terminated(space0, line_ending),
70    )(input)
71}
72
73/// the end string are case-sensitive.
74fn section_end(input: &str) -> IResult<&str, &str> {
75    tag("end")(input)
76}
77
78/// each line is a set of columns
79fn section_line(input: &str) -> IResult<&str, Vec<&str>> {
80    terminated(preceded(not(peek(section_end)), columns), blank_lines)(input)
81}
82
83/// Data values can be comma- or space-delimited: commas are replaced with spaces when the game reads a line from the file.
84fn 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}