use std::collections::HashSet;
use crate::command::{Argument, Block, Command};
use nom::branch::alt;
use nom::bytes::complete::{escaped_transform, is_not, tag, take, take_while_m_n};
use nom::character::complete::{
alphanumeric1, anychar, char, line_ending, not_line_ending, one_of, space0, space1,
};
use nom::combinator::{consumed, eof, map_res, opt, peek, recognize, value, verify};
use nom::error::ErrorKind;
use nom::multi::{many0, many0_count, many_till, separated_list1};
use nom::sequence::{delimited, pair, preceded, separated_pair, terminated};
use nom::Finish as _;
type Span<'a> = nom_locate::LocatedSpan<&'a str>;
type IResult<'a, O> = nom::IResult<Span<'a>, O>;
type Error<'a> = nom::error::Error<Span<'a>>;
pub(crate) fn parse(input: &str) -> Result<Vec<Block>, Error> {
blocks(Span::new(input)).finish().map(|(_, blocks)| blocks)
}
#[cfg(test)]
pub(crate) fn parse_command(input: &str) -> Result<Command, Error> {
command(Span::new(input)).finish().map(|(_, cmd)| cmd)
}
fn blocks(input: Span) -> IResult<Vec<Block>> {
let (input, (blocks, _)) = many_till(block, eof)(input)?;
Ok((input, blocks))
}
fn block(input: Span) -> IResult<Block> {
let line_number = input.location_line();
let (input, (literal, commands)) = consumed(commands)(input)?;
let block = Block { literal: literal.to_string(), commands, line_number };
if input.is_empty() && block.commands.is_empty() {
return Ok((input, block));
}
let (input, _) = separator(input)?;
let (input, _) = output(input)?;
Ok((input, block))
}
fn commands(mut input: Span) -> IResult<Vec<Command>> {
let mut commands = Vec::new();
loop {
if let (i, Some(_)) = opt(empty_or_comment_line)(input)? {
input = i;
continue;
}
if input.is_empty() {
return Ok((input, commands));
}
if let (_, Some(_)) = peek(opt(separator))(input)? {
if !commands.is_empty() {
return Ok((input, commands));
}
}
let (i, command) = command(input)?;
commands.push(command);
input = i;
}
}
fn command(input: Span) -> IResult<Command> {
let (input, maybe_silent) = opt(terminated(char('('), space0))(input)?;
let silent = maybe_silent.is_some();
let mut tags = HashSet::new();
let (input, prefix) = opt(terminated(string, pair(tag(":"), space0)))(input)?;
let (input, maybe_tags) = opt(delimited(space0, taglist, space0))(input)?;
tags.extend(maybe_tags.unwrap_or_default());
let (input, maybe_fail) = opt(terminated(char('!'), space0))(input)?;
let fail = maybe_fail.is_some();
let (input, maybe_literal) = opt(terminated(tag(">"), space0))(input)?;
if maybe_literal.is_some() {
let line_number = input.location_line();
let (input, name) = line_continuation(input)?;
let args = Vec::new();
return Ok((input, Command { name, args, tags, prefix, silent, fail, line_number }));
}
let line_number = input.location_line();
let (input, name) = string(input)?;
let (input, args) = many0(preceded(space1, argument))(input)?;
let (mut input, maybe_tags) = opt(preceded(space1, taglist))(input)?;
tags.extend(maybe_tags.unwrap_or_default());
if silent {
(input, _) = preceded(space0, char(')'))(input)?;
}
let (input, _) = space0(input)?;
let (input, _) = opt(comment)(input)?;
let (input, _) = line_ending(input)?;
Ok((input, Command { name, args, tags, prefix, silent, fail, line_number }))
}
fn argument(input: Span) -> IResult<Argument> {
if let Ok((input, (key, value))) = separated_pair(string, tag("="), opt(string))(input) {
return Ok((input, Argument { key: Some(key), value: value.unwrap_or_default() }));
}
let (input, value) = string(input)?;
Ok((input, Argument { key: None, value }))
}
fn taglist(input: Span) -> IResult<HashSet<String>> {
let (input, tags) =
delimited(tag("["), separated_list1(one_of(", "), string), tag("]"))(input)?;
Ok((input, HashSet::from_iter(tags)))
}
fn separator(input: Span) -> IResult<()> {
value((), terminated(tag("---"), alt((line_ending, eof))))(input)
}
fn output(input: Span) -> IResult<Span> {
if let (input, Some(output)) = opt(alt((line_ending, eof)))(input)? {
return Ok((input, output));
}
recognize(many_till(anychar, pair(alt((line_ending, eof)), alt((line_ending, eof)))))(input)
}
fn string(input: Span) -> IResult<String> {
alt((unquoted_string, quoted_string('\''), quoted_string('"')))(input)
}
fn unquoted_string(input: Span) -> IResult<String> {
let (input, string) = recognize(pair(
alt((alphanumeric1, tag("_"))),
many0_count(alt((alphanumeric1, tag("_"), tag("-"), tag("."), tag("/"), tag("@")))),
))(input)?;
Ok((input, string.to_string()))
}
fn quoted_string(quote: char) -> impl FnMut(Span) -> IResult<String> {
move |input| {
let q = match quote {
'\'' | '\"' => quote.to_string(),
c => panic!("invalid quote character {c}"),
};
let q = q.as_str();
let (input, maybe_empty) = opt(tag(format!("{q}{q}").as_str()))(input)?;
if maybe_empty.is_some() {
return Ok((input, String::new()));
}
let result = delimited(
tag(q),
escaped_transform(
is_not(format!("\\{q}").as_str()),
'\\',
alt((
value('\'', tag("\'")),
value('\"', tag("\"")),
value('\\', tag("\\")),
value('\0', tag("0")),
value('\n', tag("n")),
value('\r', tag("r")),
value('\t', tag("t")),
map_res(
preceded(tag("x"), take(2usize)),
|input: Span| match u8::from_str_radix(input.fragment(), 16) {
Ok(byte) => Ok(char::from(byte)),
Err(_) => Err(Error::new(input, ErrorKind::HexDigit)),
},
),
map_res(
delimited(
tag("u{"),
take_while_m_n(1, 6, |c: char| c.is_ascii_hexdigit()),
tag("}"),
),
|input: Span| {
let codepoint = u32::from_str_radix(input.fragment(), 16)
.or(Err(Error::new(input, ErrorKind::HexDigit)))?;
char::from_u32(codepoint).ok_or(Error::new(input, ErrorKind::Char))
},
),
)),
),
tag(q),
)(input);
result
}
}
fn empty_or_comment_line(input: Span) -> IResult<Span> {
verify(recognize(delimited(space0, opt(comment), alt((line_ending, eof)))), |line: &Span| {
!line.is_empty()
})(input)
}
fn comment(input: Span) -> IResult<Span> {
recognize(preceded(alt((tag("//"), tag("#"))), not_line_ending))(input)
}
fn line_continuation(mut input: Span) -> IResult<String> {
let mut result = String::new();
loop {
let (i, line) = terminated(not_line_ending, line_ending)(input)?;
input = i;
result.push_str(line.as_ref());
if line.ends_with('\\') {
result.pop();
continue;
}
return Ok((input, result));
}
}