mod parse_state;
use std::{borrow::Cow, fmt};
use nom::{
branch::alt,
bytes::complete::{tag, take, take_until, take_while1},
character::complete::{hex_digit1, i32, multispace1, not_line_ending, u32},
combinator::{eof, map, map_parser, map_res, opt, peek, value},
error::{context, convert_error, make_error, ContextError, ParseError, VerboseError},
multi::{many0, separated_list0},
sequence::{delimited, pair, preceded, terminated},
Finish, IResult, Parser,
};
use crate::bundle::{Int28, Key, Resource, ResourceBundle, Table};
use self::parse_state::ParseState;
macro_rules! type_id {
($(#[$meta:meta])* $name:ident, $tag:literal) => {
$(#[$meta])*
fn $name<'a, E>(input: ParseState<'a>) -> IResult<ParseState<'a>, ParseState<'a>, E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>
{
context(stringify!($name), preceded(tag(":"),
tag($tag)
))(input)
}
};
($(#[$meta:meta])* $name:ident, $( $tag:literal ),+) => {
$(#[$meta])*
fn $name<'a, E>(input: ParseState<'a>) -> IResult<ParseState<'a>, ParseState<'a>, E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>
{
context(stringify!($name), preceded(tag(":"), alt((
$( tag($tag) ),+
))))(input)
}
};
}
type_id!(
root_table_type,
"table(nofallback)",
"table"
);
type_id!(
string_type,
"string"
);
type_id!(
array_type,
"array"
);
type_id!(
table_type,
"table"
);
type_id!(
binary_type,
"binary",
"bin"
);
type_id!(
integer_type,
"integer",
"int"
);
type_id!(
int_vector_type,
"intvector"
);
type_id!(
import_type,
"import"
);
type_id!(
include_type,
"include"
);
type_id!(
alias_type,
"alias"
);
fn type_id_no_root<'a, E>(input: ParseState<'a>) -> IResult<ParseState<'a>, ParseState<'a>, E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
context(
"type_id",
alt((
string_type,
array_type,
table_type,
binary_type,
integer_type,
int_vector_type,
import_type,
include_type,
alias_type,
)),
)(input)
}
fn invariant_chars<'a, E>(input: ParseState<'a>) -> IResult<ParseState<'a>, ParseState<'a>, E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
context(
"invariant_chars",
take_while1(|c: char| c.is_alphanumeric() || c == '-'),
)(input)
}
fn eol_comment<'a, E>(input: ParseState<'a>) -> IResult<ParseState<'a>, ParseState<'a>, E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
context("eol_comment", preceded(tag("//"), not_line_ending))(input)
}
fn delimited_comment<'a, E>(input: ParseState<'a>) -> IResult<ParseState<'a>, ParseState<'a>, E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
context(
"delimited_comment",
delimited(tag("/*"), take_until("*/"), tag("*/")),
)(input)
}
fn comment<'a, E>(input: ParseState<'a>) -> IResult<ParseState<'a>, ParseState<'a>, E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
context("comment", alt((eol_comment, delimited_comment)))(input)
}
fn string<'a, E>(input: ParseState<'a>) -> IResult<ParseState<'a>, &'a str, E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
context(
"string",
map(
token(alt((
delimited(tag("\""), take_until("\""), tag("\"")),
invariant_chars,
))),
|value| value.input(),
),
)(input)
}
fn integer<'a, E>(input: ParseState<'a>) -> IResult<ParseState<'a>, u32, E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
context("integer", token(alt((u32, map(i32, |value| value as u32)))))(input)
}
fn discardable<'a, E>(input: ParseState<'a>) -> IResult<ParseState<'a>, (), E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
context("discardable", value((), many0(alt((comment, multispace1)))))(input)
}
fn token<'a, F, O, E>(mut parser: F) -> impl FnMut(ParseState<'a>) -> IResult<ParseState<'a>, O, E>
where
F: Parser<ParseState<'a>, O, E>,
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
context("token", move |input: ParseState<'a>| {
let (rest, _) = discardable.parse(input)?;
let (rest, token) = parser.parse(rest)?;
let (rest, _) = discardable.parse(rest)?;
Ok((rest, token))
})
}
macro_rules! simple_token {
($name:ident, $str:expr) => {
fn $name<'a, E>(input: ParseState<'a>) -> IResult<ParseState<'a>, ParseState<'a>, E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
context(stringify!($name), token(tag($str)))(input)
}
};
}
simple_token!(left_brace, "{");
simple_token!(right_brace, "}");
simple_token!(comma, ",");
fn key<'a, E>(state: ParseState<'a>) -> IResult<ParseState<'a>, Key<'a>, E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
let input = state.clone();
context(
"key",
map(
terminated(
string,
peek(alt((type_id_no_root, left_brace))),
),
|key| {
let key = Key::from(key);
state.encounter_key(key.clone());
key
},
),
)(input)
}
fn table_entry<'a, E>(input: ParseState<'a>) -> IResult<ParseState<'a>, (Key<'a>, Resource<'a>), E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
context("table_entry", pair(key, resource))(input)
}
macro_rules! resource_body {
($body_parser:expr) => {
delimited(left_brace, $body_parser, right_brace)
};
}
macro_rules! resource {
($type:ident, $body_parser:expr) => {
preceded($type, resource_body!($body_parser))
};
}
macro_rules! resource_opt_tag {
($type:ident, $body_parser:expr) => {
preceded(opt($type), resource_body!($body_parser))
};
}
fn table_body<'a, E>(state: ParseState<'a>) -> IResult<ParseState<'a>, Table<'a>, E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
let (state, entries) = context("table_body", many0(table_entry)).parse(state)?;
let mut table = Table::new();
for (k, v) in entries {
table.insert(k, v);
}
Ok((state, table))
}
fn table_resource<'a, E>(input: ParseState<'a>) -> IResult<ParseState<'a>, Resource<'a>, E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
context(
"table_resource",
map(resource_opt_tag!(table_type, table_body), Resource::Table),
)(input)
}
fn root_table_resource<'a, E>(
input: ParseState<'a>,
) -> IResult<ParseState<'a>, (bool, Resource<'a>), E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
let (input, is_fallback_enabled) = opt(root_table_type).parse(input)?;
let is_fallback_enabled =
is_fallback_enabled.is_none_or(|type_id| type_id.input() != "table(nofallback)");
let (input, resource) = map(resource_body!(table_body), Resource::Table).parse(input)?;
Ok((input, (is_fallback_enabled, resource)))
}
fn array_resource<'a, E>(input: ParseState<'a>) -> IResult<ParseState<'a>, Resource<'a>, E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
context(
"array_resource",
map(
resource_opt_tag!(array_type, many0(terminated(resource, opt(comma)))),
Resource::Array,
),
)(input)
}
fn int_resource<'a, E>(input: ParseState<'a>) -> IResult<ParseState<'a>, Resource<'a>, E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
context(
"int_resource",
map(resource!(integer_type, integer), |value| {
Resource::Integer(Int28::from(value))
}),
)(input)
}
fn int_vector_resource<'a, E>(input: ParseState<'a>) -> IResult<ParseState<'a>, Resource<'a>, E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
context(
"int_vector_resource",
map(
resource!(
int_vector_type,
terminated(separated_list0(comma, integer), opt(comma))
),
Resource::IntVector,
),
)(input)
}
fn binary_byte(input: &str) -> IResult<&str, u8> {
context(
"binary_byte",
map_res(map_parser(take(2usize), hex_digit1), |word| {
u8::from_str_radix(word, 16)
}),
)(input)
}
fn binary_resource<'a, E>(input: ParseState<'a>) -> IResult<ParseState<'a>, Resource<'a>, E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
let (rest, string) = resource!(binary_type, string).parse(input.clone())?;
let (_, elements) = match many0(binary_byte).parse(string) {
Ok(elements) => elements,
Err(err) => {
println!("{err}");
return Err(nom::Err::Error(make_error(
input,
nom::error::ErrorKind::HexDigit,
)));
}
};
Ok((rest, Resource::Binary(Cow::from(elements))))
}
fn string_resource<'a, E>(input: ParseState<'a>) -> IResult<ParseState<'a>, Resource<'a>, E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
context(
"string_resource",
map(
alt((resource_opt_tag!(string_type, string), string)),
|value| Resource::String(Cow::from(value)),
),
)(input)
}
fn resource<'a, E>(input: ParseState<'a>) -> IResult<ParseState<'a>, Resource<'a>, E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
context(
"resource",
alt((
int_resource,
int_vector_resource,
string_resource,
binary_resource,
table_resource,
array_resource,
)),
)(input)
}
fn bundle<'a, E>(input: ParseState<'a>) -> IResult<ParseState<'a>, ResourceBundle<'a>, E>
where
E: ParseError<ParseState<'a>> + ContextError<ParseState<'a>>,
{
context(
"bundle",
map(
terminated(
pair(
string,
root_table_resource,
),
eof,
),
|(name, (is_locale_fallback_enabled, root))| {
ResourceBundle::new(Cow::from(name), root, is_locale_fallback_enabled)
},
),
)(input)
}
#[derive(Debug)]
#[non_exhaustive]
pub struct Reader;
impl Reader {
pub fn read(input: &str) -> Result<(ResourceBundle<'_>, Vec<Key<'_>>), TextParserError> {
let input = ParseState::new(input);
let (final_state, bundle) = bundle::<VerboseError<ParseState>>(input.clone())
.finish()
.map_err(|err| TextParserError {
trace: convert_error(input, err),
})?;
let keys_in_discovery_order = final_state.take_keys().into_iter().collect();
Ok((bundle, keys_in_discovery_order))
}
}
#[derive(Debug)]
pub struct TextParserError {
trace: String,
}
impl fmt::Display for TextParserError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_fmt(format_args!(
"Parse error while reading text bundle:\n{}",
self.trace
))
}
}