1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
use std::collections::HashMap;
use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::bytes::complete::take_till1;
use nom::character::complete::alpha1;
use nom::character::complete::alphanumeric1;
use nom::character::complete::char;
use nom::character::complete::line_ending;
use nom::character::complete::multispace1;
use nom::character::complete::not_line_ending;
use nom::character::complete::space0;
use nom::character::complete::space1;
use nom::combinator::all_consuming;
use nom::combinator::map;
use nom::combinator::not;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::multi::many0;
use nom::sequence::delimited;
use nom::sequence::pair;
use nom::sequence::preceded;
use nom::sequence::terminated;
use nom::sequence::tuple;
use nom::IResult;

/// Comments are prefixed with the # symbol.
pub fn comment(input: &str) -> IResult<&str, &str> {
    preceded(char('#'), not_line_ending)(input)
}

pub fn blank_lines(input: &str) -> IResult<&str, &str> {
    recognize(many0(alt((multispace1, comment))))(input)
}

fn identifier(input: &str) -> IResult<&str, &str> {
    recognize(pair(
        alt((alpha1, tag("_"))),
        many0(alt((alphanumeric1, tag("_")))),
    ))(input)
}

/// A file is a set of sections
pub fn parse_file(input: &str) -> IResult<&str, std::collections::HashMap<String, Vec<Vec<&str>>>> {
    map(all_consuming(many0(section)), |sections: Vec<_>| {
        let mut m = HashMap::new();
        for (name, lines) in sections {
            m.insert(name.to_string(), lines);
        }
        m
    })(input)
}

/// Sections are lines between section...end
fn section(input: &str) -> IResult<&str, (&str, Vec<Vec<&str>>)> {
    terminated(
        tuple((section_name, many0(section_line))),
        terminated(section_end, blank_lines),
    )(input)
}

fn section_name(input: &str) -> IResult<&str, &str> {
    delimited(blank_lines, identifier, terminated(space0, line_ending))(input)
}

/// the end string are case-sensitive.
fn section_end(input: &str) -> IResult<&str, &str> {
    tag("end")(input)
}

/// each line is a set of columns
fn section_line(input: &str) -> IResult<&str, Vec<&str>> {
    delimited(
        blank_lines,
        preceded(not(peek(section_end)), columns),
        blank_lines,
    )(input)
}

/// Data values can be comma- or space-delimited: commas are replaced with spaces when the game reads a line from the file.
fn columns(input: &str) -> IResult<&str, Vec<&str>> {
    many0(delimited(column_space, column, column_space))(input)
}

fn column(input: &str) -> IResult<&str, &str> {
    take_till1(|c: char| !c.is_ascii_graphic() || c == '#' || c == ',')(input)
}

pub fn column_space(input: &str) -> IResult<&str, &str> {
    recognize(many0(alt((space1, comment, tag(",")))))(input)
}

pub fn parse(input: &str) -> Option<std::collections::HashMap<String, Vec<Vec<&str>>>> {
    match parse_file(input) {
        Ok((_, v)) => Some(v),
        Err(_) => None,
    }
}