use nom::{
IResult, Parser,
branch::alt,
bytes::complete::{is_not, tag, take_while, take_while_m_n, take_while1},
character::complete::{char, digit1, multispace1},
combinator::{cut, map, map_opt, map_res, opt, recognize, value, verify},
error::{ContextError, FromExternalError, ParseError, context},
multi::{fold_many0, many0, many1, separated_list1},
sequence::{delimited, pair, preceded, separated_pair},
};
use std::str::FromStr;
use crate::command::{Command, CompositeValue, Parameter, Value};
fn parse_escaped_char<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, char, E> {
preceded(
char('\\'),
alt((
value('\n', char('n')),
value('\r', char('r')),
value('\t', char('t')),
value('\x0B', char('v')), value('\x07', char('a')), value('\x08', char('b')), value('\x0C', char('f')), value('\\', char('\\')),
value('"', char('"')),
value('\'', char('\'')),
preceded(
char('x'),
map_opt(
take_while_m_n(2, 2, |c: char| c.is_ascii_hexdigit()),
|hex: &str| u32::from_str_radix(hex, 16).ok().and_then(char::from_u32),
),
),
preceded(
char('u'),
map_opt(
take_while_m_n(4, 4, |c: char| c.is_ascii_hexdigit()),
|hex: &str| u32::from_str_radix(hex, 16).ok().and_then(char::from_u32),
),
),
preceded(
char('U'),
map_opt(
take_while_m_n(8, 8, |c: char| c.is_ascii_hexdigit()),
|hex: &str| u32::from_str_radix(hex, 16).ok().and_then(char::from_u32),
),
),
map_opt(
take_while_m_n(1, 3, |c: char| c.is_digit(8)),
|octal: &str| u32::from_str_radix(octal, 8).ok().and_then(char::from_u32),
),
)),
)
.parse(input)
}
fn parse_string_literal<'a, E: ParseError<&'a str>>(
input: &'a str,
) -> IResult<&'a str, &'a str, E> {
let not_quote_slash = is_not("\"\\");
verify(not_quote_slash, |s: &str| !s.is_empty()).parse(input)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum StringFragment<'a> {
Literal(&'a str),
EscapedChar(char),
LineContinuation,
}
fn parse_line_continuation<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, (), E> {
map((char('\\'), char('\n')), |_| ()).parse(input)
}
fn parse_string_fragment<'a, E: ParseError<&'a str>>(
input: &'a str,
) -> IResult<&'a str, StringFragment<'a>, E> {
alt((
map(parse_string_literal, StringFragment::Literal),
map(parse_line_continuation, |_| {
StringFragment::LineContinuation
}),
map(parse_escaped_char, StringFragment::EscapedChar),
))
.parse(input)
}
fn parse_string<'a, E: ParseError<&'a str> + ContextError<&'a str>>(
input: &'a str,
) -> IResult<&'a str, Value, E> {
let build_string = fold_many0(
parse_string_fragment,
String::new,
|mut string, fragment| {
match fragment {
StringFragment::Literal(s) => string.push_str(s),
StringFragment::EscapedChar(c) => string.push(c),
StringFragment::LineContinuation => {
}
}
string
},
);
context(
"string",
delimited(char('"'), map(build_string, Value::String), char('"')),
)
.parse(input)
}
fn parse_decimal_int<
'a,
E: ParseError<&'a str> + FromExternalError<&'a str, std::num::ParseIntError>,
>(
input: &'a str,
) -> IResult<&'a str, i64, E> {
map_res(recognize(pair(opt(char('-')), digit1)), |s: &str| {
i64::from_str(s)
})
.parse(input)
}
fn parse_hex_int<
'a,
E: ParseError<&'a str> + FromExternalError<&'a str, std::num::ParseIntError>,
>(
input: &'a str,
) -> IResult<&'a str, i64, E> {
preceded(
tag("0x"),
map_res(take_while1(|c: char| c.is_ascii_hexdigit()), |s: &str| {
i64::from_str_radix(s, 16)
}),
)
.parse(input)
}
fn parse_bin_int<
'a,
E: ParseError<&'a str> + FromExternalError<&'a str, std::num::ParseIntError>,
>(
input: &'a str,
) -> IResult<&'a str, i64, E> {
preceded(
tag("0b"),
map_res(take_while1(|c: char| c == '0' || c == '1'), |s: &str| {
i64::from_str_radix(s, 2)
}),
)
.parse(input)
}
fn parse_oct_int<
'a,
E: ParseError<&'a str> + FromExternalError<&'a str, std::num::ParseIntError>,
>(
input: &'a str,
) -> IResult<&'a str, i64, E> {
preceded(
tag("0o"),
map_res(take_while1(|c: char| c.is_ascii_digit()), |s: &str| {
i64::from_str_radix(s, 8)
}),
)
.parse(input)
}
fn parse_integer<
'a,
E: ParseError<&'a str>
+ ContextError<&'a str>
+ FromExternalError<&'a str, std::num::ParseIntError>,
>(
input: &'a str,
) -> IResult<&'a str, Value, E> {
context(
"integer",
alt((
map(parse_hex_int, Value::Int),
map(parse_bin_int, Value::Int),
map(parse_oct_int, Value::Int),
map(parse_decimal_int, Value::Int),
)),
)
.parse(input)
}
fn parse_float<
'a,
E: ParseError<&'a str>
+ ContextError<&'a str>
+ FromExternalError<&'a str, std::num::ParseFloatError>,
>(
input: &'a str,
) -> IResult<&'a str, Value, E> {
context(
"float",
map_res(
recognize((
opt(char('-')),
alt((
recognize((digit1, char('.'), digit0, opt(float_exp))),
recognize((char('.'), digit1, opt(float_exp))),
recognize((digit1, float_exp)),
)),
)),
|s: &str| f64::from_str(s).map(Value::Float),
),
)
.parse(input)
}
fn digit0<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, &'a str, E> {
take_while(|c: char| c.is_ascii_digit())(input)
}
fn float_exp<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, &'a str, E> {
recognize((
alt((char('e'), char('E'))),
opt(alt((char('+'), char('-')))),
digit1,
))
.parse(input)
}
fn parse_literal_str<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, &'a str, E> {
recognize(pair(
take_while1(|c: char| c.is_ascii_alphabetic() || c == '_'),
take_while(|c: char| c.is_ascii_alphanumeric() || c == '_'),
))
.parse(input)
}
fn parse_literal<'a, E: ParseError<&'a str> + ContextError<&'a str>>(
input: &'a str,
) -> IResult<&'a str, Value, E> {
context(
"literal",
map(parse_literal_str, |s: &str| Value::String(s.to_string())),
)
.parse(input)
}
fn parse_bool<'a, E: ParseError<&'a str> + ContextError<&'a str>>(
input: &'a str,
) -> IResult<&'a str, Value, E> {
context(
"boolean",
alt((
map(tag("true"), |_| Value::Bool(true)),
map(tag("false"), |_| Value::Bool(false)),
)),
)
.parse(input)
}
fn parse_basic_value<
'a,
E: ParseError<&'a str>
+ ContextError<&'a str>
+ FromExternalError<&'a str, std::num::ParseIntError>
+ FromExternalError<&'a str, std::num::ParseFloatError>,
>(
input: &'a str,
) -> IResult<&'a str, Value, E> {
context(
"basic_value",
alt((
parse_string, parse_bool,
parse_float,
parse_integer,
parse_literal,
)),
)
.parse(input)
}
fn parse_single_param<
'a,
E: ParseError<&'a str>
+ ContextError<&'a str>
+ FromExternalError<&'a str, std::num::ParseIntError>
+ FromExternalError<&'a str, std::num::ParseFloatError>,
>(
input: &'a str,
) -> IResult<&'a str, Parameter, E> {
map(parse_basic_value, Parameter::Basic).parse(input)
}
fn parse_value_list<
'a,
E: ParseError<&'a str>
+ ContextError<&'a str>
+ FromExternalError<&'a str, std::num::ParseIntError>
+ FromExternalError<&'a str, std::num::ParseFloatError>,
>(
input: &'a str,
) -> IResult<&'a str, Vec<Value>, E> {
context(
"list",
separated_list1(
preceded(parse_whitespace_with_continuation, char(',')),
preceded(parse_whitespace_with_continuation, parse_basic_value),
),
)
.parse(input)
}
fn parse_dict<
'a,
E: ParseError<&'a str>
+ ContextError<&'a str>
+ FromExternalError<&'a str, std::num::ParseIntError>
+ FromExternalError<&'a str, std::num::ParseFloatError>,
>(
input: &'a str,
) -> IResult<&'a str, Vec<(String, Value)>, E> {
context(
"dictionary",
separated_list1(
preceded(parse_whitespace_with_continuation, char(',')),
preceded(
parse_whitespace_with_continuation,
separated_pair(
map(parse_literal_str, |v| v.to_string()),
preceded(parse_whitespace_with_continuation, char(':')),
preceded(parse_whitespace_with_continuation, cut(parse_basic_value)),
),
),
),
)
.parse(input)
}
fn parse_composite_param<
'a,
E: ParseError<&'a str>
+ ContextError<&'a str>
+ FromExternalError<&'a str, std::num::ParseIntError>
+ FromExternalError<&'a str, std::num::ParseFloatError>,
>(
input: &'a str,
) -> IResult<&'a str, Parameter, E> {
context(
"composite_parameter",
(
parse_literal_str,
delimited(
(char('('), parse_whitespace_with_continuation),
cut(alt((
map(parse_dict, CompositeValue::Dict),
map(parse_value_list, |values| {
if values.len() == 1 {
CompositeValue::Single(values[0].clone())
} else {
CompositeValue::List(values)
}
}),
))),
cut((parse_whitespace_with_continuation, char(')'))),
),
),
)
.parse(input)
.map(|(remaining, (key, composite))| {
(remaining, Parameter::Composite(key.to_string(), composite))
})
}
fn parse_parameter<
'a,
E: ParseError<&'a str>
+ ContextError<&'a str>
+ FromExternalError<&'a str, std::num::ParseIntError>
+ FromExternalError<&'a str, std::num::ParseFloatError>,
>(
input: &'a str,
) -> IResult<&'a str, Parameter, E> {
context(
"parameter",
alt((parse_composite_param, parse_single_param)),
)
.parse(input)
}
fn parse_command_name<
'a,
E: ParseError<&'a str>
+ ContextError<&'a str>
+ FromExternalError<&'a str, std::num::ParseIntError>,
>(
input: &'a str,
) -> IResult<&'a str, String, E> {
context(
"command_name",
cut(alt((
map(parse_literal_str, |v| v.to_string()),
map(parse_decimal_int, |n| n.to_string()),
))),
)
.parse(input)
}
fn parse_whitespace_with_continuation<'a, E: ParseError<&'a str>>(
input: &'a str,
) -> IResult<&'a str, &'a str, E> {
recognize(many0(alt((multispace1, tag("\\\n"))))).parse(input)
}
fn parse_whitespace_with_continuation1<'a, E: ParseError<&'a str>>(
input: &'a str,
) -> IResult<&'a str, &'a str, E> {
recognize(many1(alt((multispace1, tag("\\\n"))))).parse(input)
}
pub fn parse_command_line<
'a,
E: ParseError<&'a str>
+ ContextError<&'a str>
+ FromExternalError<&'a str, std::num::ParseIntError>
+ FromExternalError<&'a str, std::num::ParseFloatError>,
>(
input: &'a str,
) -> IResult<&'a str, Command, E> {
(
parse_command_name,
many0(preceded(
parse_whitespace_with_continuation1,
cut(parse_parameter),
)),
)
.parse(input)
.map(|(remaining, (name, params))| (remaining, Command::new(name, params)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_integer() {
assert_eq!(
parse_integer::<nom::error::Error<&str>>("123"),
Ok(("", Value::Int(123)))
);
assert_eq!(
parse_integer::<nom::error::Error<&str>>("-456"),
Ok(("", Value::Int(-456)))
);
assert_eq!(
parse_integer::<nom::error::Error<&str>>("0x1A"),
Ok(("", Value::Int(26)))
);
assert_eq!(
parse_integer::<nom::error::Error<&str>>("0b101"),
Ok(("", Value::Int(5)))
);
}
#[test]
fn test_parse_float() {
assert_eq!(
parse_float::<nom::error::Error<&str>>("1.23"),
Ok(("", Value::Float(1.23)))
);
assert_eq!(
parse_float::<nom::error::Error<&str>>("-4.56"),
Ok(("", Value::Float(-4.56)))
);
assert_eq!(
parse_float::<nom::error::Error<&str>>("1e-2"),
Ok(("", Value::Float(0.01)))
);
}
#[test]
fn test_parse_literal() {
assert_eq!(
parse_literal::<nom::error::Error<&str>>("hello"),
Ok(("", Value::String("hello".to_string())))
);
assert_eq!(
parse_literal::<nom::error::Error<&str>>("_test_123"),
Ok(("", Value::String("_test_123".to_string())))
);
}
#[test]
fn test_parse_command_simple() {
let result = parse_command_line::<nom::error::Error<&str>>("command");
assert!(result.is_ok());
let (remaining, cmd) = result.unwrap();
assert_eq!(remaining, "");
assert_eq!(cmd.name(), "command");
assert_eq!(cmd.params().len(), 0);
}
#[test]
fn test_parse_command_with_params() {
let result = parse_command_line::<nom::error::Error<&str>>("draw Line 2");
println!("Result: {:?}", result);
assert!(result.is_ok());
let (remaining, cmd) = result.unwrap();
assert_eq!(remaining, "");
assert_eq!(cmd.name(), "draw");
assert_eq!(cmd.params().len(), 2);
assert_eq!(cmd.params()[0], Value::String("Line".to_string()).into());
assert_eq!(cmd.params()[1], Value::from(2).into());
}
#[test]
fn test_parse_string_parameter() {
let basic_result = parse_basic_value::<nom::error::Error<&str>>("\"Hello World\"");
println!("Basic value parse result: {:?}", basic_result);
assert!(basic_result.is_ok());
let result = parse_command_line::<nom::error::Error<&str>>("say \"Hello World\"");
println!("String parse result: {:?}", result);
assert!(result.is_ok());
let (remaining, cmd) = result.unwrap();
assert_eq!(remaining, "");
assert_eq!(cmd.name(), "say");
assert_eq!(cmd.params().len(), 1);
assert_eq!(cmd.params()[0], Parameter::from("Hello World"));
let escape_result = parse_basic_value::<nom::error::Error<&str>>("\"Hello\\nWorld\"");
println!("Escape parse result: {:?}", escape_result);
assert!(escape_result.is_ok());
if let Ok((_, Value::String(s))) = escape_result {
assert_eq!(s, "Hello\nWorld");
}
let unicode_result = parse_basic_value::<nom::error::Error<&str>>("\"Emoji: \\U0001F602\"");
println!("Unicode parse result: {:?}", unicode_result);
assert!(unicode_result.is_ok());
if let Ok((_, Value::String(s))) = unicode_result {
assert_eq!(s, "Emoji: 😂");
}
let hex_result = parse_basic_value::<nom::error::Error<&str>>("\"Hex: \\x41\"");
println!("Hex parse result: {:?}", hex_result);
assert!(hex_result.is_ok());
if let Ok((_, Value::String(s))) = hex_result {
assert_eq!(s, "Hex: A");
}
let octal_result = parse_basic_value::<nom::error::Error<&str>>("\"Octal: \\101\"");
println!("Octal parse result: {:?}", octal_result);
assert!(octal_result.is_ok());
if let Ok((_, Value::String(s))) = octal_result {
assert_eq!(s, "Octal: A");
}
}
#[test]
fn test_parse_composite_single() {
let result = parse_command_line::<nom::error::Error<&str>>("draw thickness(2)");
assert!(result.is_ok());
let (remaining, cmd) = result.unwrap();
assert_eq!(remaining, "");
assert_eq!(cmd.name(), "draw");
assert_eq!(cmd.params().len(), 1);
match &cmd.params()[0] {
Parameter::Composite(name, _) => assert_eq!(name, "thickness"),
_ => panic!("Expected composite parameter"),
}
}
#[test]
fn test_parse_composite_list() {
let result = parse_command_line::<nom::error::Error<&str>>("draw color(255, 255, 255)");
assert!(result.is_ok());
let (remaining, cmd) = result.unwrap();
assert_eq!(remaining, "");
assert_eq!(cmd.name(), "draw");
assert_eq!(cmd.params().len(), 1);
}
#[test]
fn test_parse_composite_dict() {
let result = parse_command_line::<nom::error::Error<&str>>("draw pos(x: 10, y: 20)");
assert!(result.is_ok());
let (remaining, cmd) = result.unwrap();
assert_eq!(remaining, "");
assert_eq!(cmd.name(), "draw");
assert_eq!(cmd.params().len(), 1);
}
#[test]
fn test_parse_mixed_parameters() {
let result = parse_command_line::<nom::error::Error<&str>>(
"draw Line 2 pos(x: 0, y: 0) thickness(2) color(255, 255, 255)",
);
assert!(result.is_ok());
let (remaining, cmd) = result.unwrap();
assert_eq!(remaining, "");
assert_eq!(cmd.name(), "draw");
assert_eq!(cmd.params().len(), 5);
assert_eq!(cmd.params()[0], Value::String("Line".to_string()).into());
assert_eq!(cmd.params()[1], Value::from(2).into());
assert_eq!(
cmd.params()[2],
Parameter::Composite(
"pos".to_string(),
CompositeValue::Dict(vec![
("x".to_string(), Value::from(0).into()),
("y".to_string(), Value::from(0).into())
])
)
);
assert_eq!(
cmd.params()[3],
Parameter::Composite("thickness".to_string(), Value::from(2).into())
);
assert_eq!(
cmd.params()[4],
Parameter::Composite(
"color".to_string(),
CompositeValue::List(vec![
Value::from(255).into(),
Value::from(255).into(),
Value::from(255).into()
])
)
);
}
#[test]
fn test_parse_number_command() {
let result = parse_command_line::<nom::error::Error<&str>>("114 arg1 arg2");
assert!(result.is_ok());
let (remaining, cmd) = result.unwrap();
assert_eq!(remaining, "");
assert_eq!(cmd.name(), "114");
assert_eq!(cmd.params().len(), 2);
}
#[test]
fn test_parse_complex_example() {
let result = parse_command_line::<nom::error::Error<&str>>(
"draw Line 2 pos0(x: 0, y: 0) pos1(x: 16, y: 16) thickness(2) color(255, 255, 255)",
);
assert!(result.is_ok());
let (remaining, cmd) = result.unwrap();
assert_eq!(remaining, "");
assert_eq!(cmd.name(), "draw");
assert_eq!(cmd.params().len(), 6);
}
#[test]
fn test_escapes_newline() {
let result = parse_basic_value::<nom::error::Error<&str>>("\"Hello\\\nWorld\"");
println!("Escape parse result: {:?}", result);
assert!(result.is_ok());
if let Ok((_, Value::String(s))) = result {
assert_eq!(s, "HelloWorld");
}
let result = parse_command_line::<nom::error::Error<&str>>("draw Line\\\n2");
println!("Command parse result: {:?}", result);
assert!(result.is_ok());
let (remaining, cmd) = result.unwrap();
assert_eq!(remaining, "");
assert_eq!(cmd.name(), "draw");
assert_eq!(cmd.params().len(), 2);
assert_eq!(cmd.params()[0], Value::String("Line".to_string()).into());
assert_eq!(cmd.params()[1], Value::from(2).into());
}
#[test]
fn test_line_continuation_in_composite_params() {
let result = parse_command_line::<nom::error::Error<&str>>("draw pos(x: 10,\\\ny: 20)");
println!("Composite dict with line continuation: {:?}", result);
assert!(result.is_ok());
let (remaining, cmd) = result.unwrap();
assert_eq!(remaining, "");
assert_eq!(cmd.name(), "draw");
assert_eq!(cmd.params().len(), 1);
let result =
parse_command_line::<nom::error::Error<&str>>("draw color(255,\\\n255,\\\n255)");
println!("Composite list with line continuation: {:?}", result);
assert!(result.is_ok());
let (remaining, cmd) = result.unwrap();
assert_eq!(remaining, "");
assert_eq!(cmd.name(), "draw");
assert_eq!(cmd.params().len(), 1);
let result = parse_command_line::<nom::error::Error<&str>>("draw thickness(\\\n2)");
println!("Composite single with line continuation: {:?}", result);
assert!(result.is_ok());
let (remaining, cmd) = result.unwrap();
assert_eq!(remaining, "");
assert_eq!(cmd.name(), "draw");
assert_eq!(cmd.params().len(), 1);
}
#[test]
fn test_multiple_line_continuations() {
let result = parse_command_line::<nom::error::Error<&str>>(
"draw \\\nLine \\\n2 \\\npos(x: 0,\\\ny: 0)",
);
println!("Multiple line continuations: {:?}", result);
assert!(result.is_ok());
let (remaining, cmd) = result.unwrap();
assert_eq!(remaining, "");
assert_eq!(cmd.name(), "draw");
assert_eq!(cmd.params().len(), 3);
}
}