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
//! [`nom`] functions for parsing plaintext [`map`](https://www.gamers.org/dEngine/quake/QDP/qmapspec.html) data.

pub mod primitive;

#[cfg(doc)]
use crate::repr::Map;

pub mod repr;

use nom::{
    branch::alt,
    bytes::complete::{escaped, is_not, tag},
    character::complete::{char, none_of, one_of, space1},
    combinator::{opt, recognize},
    multi::many1,
    sequence::{delimited, pair, preceded, terminated, tuple},
    IResult,
};

/// Recognize an unsigned integer literal.
pub fn parse_integer_unsigned(input: &str) -> IResult<&str, &str> {
    recognize(many1(one_of("0123456789")))(input)
}

/// Recognize a signed integer literal.
pub fn parse_integer_signed(input: &str) -> IResult<&str, &str> {
    recognize(tuple((opt(one_of("+-")), parse_integer_unsigned)))(input)
}

/// Recognize a floating-point literal.
pub fn parse_float(input: &str) -> IResult<&str, &str> {
    alt((
        // Case one: +.42 / -.42 / .42
        recognize(tuple((opt(one_of("+-")), char('.'), parse_integer_unsigned))),
        // Case two: 42e42 and 42.42e42
        recognize(tuple((
            parse_integer_signed,
            opt(preceded(char('.'), parse_integer_unsigned)),
            one_of("eE"),
            opt(one_of("+-")),
            parse_integer_unsigned,
        ))),
        // Case two: 42. and 42.42
        recognize(tuple((parse_integer_signed, char('.'), opt(parse_integer_unsigned)))),
    ))(input)
}

/// Parse a quoted string literal into string slice: `"Foo"` becomes an `&str` containing `Foo`.
pub fn parse_string(input: &str) -> IResult<&str, &str> {
    let esc = escaped(none_of("\"\'"), '\\', one_of("\"\'"));
    let esc_or_empty = alt((esc, tag("")));
    let res = delimited(one_of("\"\'"), esc_or_empty, one_of("\"\'"))(input)?;

    Ok(res)
}

/// Parse a comment beginning with `//` and terminating at end-of-line, not including end-of-line characters.
pub fn parse_eol_comment(input: &str) -> IResult<&str, &str> {
    let (i, (_, o)) = pair(
        recognize(terminated(tag("//"), opt(space1))),
        is_not("\n\r"),
    )(input)?;
    Ok((i, o))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_float() {
        assert_eq!(parse_float("1.0"), Ok(("", "1.0")));
        assert_eq!(parse_float("1.000000"), Ok(("", "1.000000")));
        assert_eq!(parse_float("-1.000000"), Ok(("", "-1.000000")));
        assert_eq!(parse_float("0."), Ok(("", "0.")));
        assert_eq!(parse_float(".0"), Ok(("", ".0")));
    }

    #[test]
    fn test_string() {
        assert_eq!(parse_string("\"Foo\""), Ok(("", "Foo")));
        assert_eq!(parse_string("'Foo'"), Ok(("", "Foo")));
        assert_eq!(parse_string("\"Antigen\nText\nRendering\nTest\n1234...? 5678! 9, 0.\""), Ok(("", "Antigen\nText\nRendering\nTest\n1234...? 5678! 9, 0.")));
    }

    #[test]
    fn test_eol_comment() {
        assert_eq!(parse_eol_comment("// Comment"), Ok(("", "Comment")));
        assert_eq!(parse_eol_comment("// Comment\n"), Ok(("\n", "Comment")));
        assert_eq!(parse_eol_comment("// Comment\r\n"), Ok(("\r\n", "Comment")));
    }
}