use std::fmt::{self, Display, Formatter};
use derive_more::Display;
use macro_rules_attribute::{apply, attribute_alias};
use nom::branch::*;
use nom::bytes::complete::*;
use nom::character::complete::*;
use nom::combinator::*;
use nom::error::{context, VerboseError};
use nom::multi::*;
use nom::sequence::*;
use nom::{Finish, Parser};
#[cfg(test)]
use serde::Serialize;
use unicode_ident::{is_xid_continue, is_xid_start};
pub use value::{Char, Int, List, Map, MapItem, NamedField, Str, Struct, Tuple, Value};
type Error<'a> = VerboseError<&'a str>;
type IResult<'a, T> = nom::IResult<&'a str, T, Error<'a>>;
mod value;
attribute_alias! {
#[apply(ast)] =
#[cfg_attr(test, derive(Serialize))]
#[derive(Debug, PartialEq)];
}
#[apply(ast)]
pub struct File<'s> {
pub extentions: Vec<WsLead<'s, Attribute<'s>>>,
pub value: WsLead<'s, Value<'s>>,
pub trailing_ws: Whitespace<'s>,
}
impl Display for File<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.extentions.iter().try_for_each(|e| write!(f, "{e}"))?;
write!(f, "{}{}", self.value, self.trailing_ws)
}
}
impl<'s> TryFrom<&'s str> for File<'s> {
type Error = String;
fn try_from(s: &'s str) -> Result<Self, Self::Error> {
match file(s).finish() {
Ok(("", file)) => Ok(file),
Ok((rem, _)) => Err(format!("unexpected: {rem}")),
Err(e) => Err(e.to_string()),
}
}
}
#[apply(ast)]
#[derive(Display)]
#[display(fmt = "#{after_pound}!\
{after_exclamation}[{after_bracket}enable{after_enable}({extentions}){after_paren}]")]
pub struct Attribute<'s> {
pub after_pound: Whitespace<'s>,
pub after_exclamation: Whitespace<'s>,
pub after_bracket: Whitespace<'s>,
pub after_enable: Whitespace<'s>,
pub extentions: Separated<'s, &'s str>,
pub after_paren: Whitespace<'s>,
}
#[apply(ast)]
pub struct WsLead<'s, T> {
pub leading: Whitespace<'s>,
pub content: T,
}
impl<T: Display> Display for WsLead<'_, T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", self.leading, self.content)
}
}
fn ws_lead<'a, T>(
t: impl Parser<&'a str, T, Error<'a>>,
) -> impl Parser<&'a str, WsLead<'a, T>, Error<'a>> {
map(pair(ws, t), |(leading, content)| WsLead {
leading,
content,
})
}
#[apply(ast)]
#[derive(Default)]
pub struct WsFollowed<'s, T> {
pub content: T,
pub following: Whitespace<'s>,
}
impl<T: Display> Display for WsFollowed<'_, T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", self.content, self.following)
}
}
fn ws_followed<'a, T>(
t: impl Parser<&'a str, T, Error<'a>>,
) -> impl Parser<&'a str, WsFollowed<'a, T>, Error<'a>> {
map(pair(t, ws), |(content, following)| WsFollowed {
content,
following,
})
}
#[apply(ast)]
#[derive(Default)]
pub struct WsWrapped<'s, T> {
pub leading: Whitespace<'s>,
pub content: T,
pub following: Whitespace<'s>,
}
impl<T: Display> Display for WsWrapped<'_, T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}{}{}", self.leading, self.content, self.following)
}
}
fn ws_wrapped<'a, T>(
t: impl Parser<&'a str, T, Error<'a>>,
) -> impl Parser<&'a str, WsWrapped<'a, T>, Error<'a>> {
map(tuple((ws, t, ws)), |(leading, content, following)| {
WsWrapped {
leading,
content,
following,
}
})
}
#[apply(ast)]
pub enum Ws<'s> {
LineComment(&'s str),
Space(&'s str),
BlockComment(&'s str),
}
impl Display for Ws<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Ws::LineComment(c) => write!(f, "//{c}"),
Ws::Space(s) => write!(f, "{s}"),
Ws::BlockComment(c) => write!(f, "/*{c}*/"),
}
}
}
impl Ws<'_> {
pub fn is_comment(&self) -> bool {
!matches!(self, Self::Space(_))
}
pub fn is_line_comment(&self) -> bool {
matches!(self, Self::LineComment(_))
}
}
#[apply(ast)]
#[derive(Default)]
pub struct Whitespace<'s>(pub Vec<Ws<'s>>);
impl Display for Whitespace<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.0.iter().try_for_each(|ws| write!(f, "{ws}"))
}
}
#[apply(ast)]
pub struct Separated<'s, T> {
pub values: Vec<WsWrapped<'s, T>>,
pub trailing_comma: bool,
pub trailing_ws: Whitespace<'s>,
}
impl<T: Display> Display for Separated<'_, T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let Self {
values,
trailing_comma,
trailing_ws,
} = self;
let Some((last, rest)) = values.split_last() else {
return write!(f, "{trailing_ws}")
};
rest.iter().try_for_each(|i| write!(f, "{i},"))?;
write!(f, "{last}")?;
if *trailing_comma {
write!(f, ",")?;
}
write!(f, "{trailing_ws}")
}
}
#[apply(ast)]
pub struct ExtentionIdent {}
fn file(input: &str) -> IResult<File> {
map(
tuple((
context("attributes", many0(ws_lead(extention_attr))),
context("value", ws_lead(value::value)),
ws,
)),
|(extentions, value, trailing_ws)| File {
extentions,
value,
trailing_ws,
},
)(input)
}
#[allow(unused)]
fn dbg_dmp<'a, O>(
context: impl Display,
mut p: impl Parser<&'a str, O, Error<'a>>,
) -> impl FnMut(&'a str) -> IResult<O> {
move |s: &str| match p.parse(s) {
Err(nom::Err::Error(c)) => {
println!("{}: Parsing Error: {} at:\n{:?}", context, c, s);
Err(nom::Err::Error(c))
}
Err(e) => {
println!("{}: Error({}) at:\n{:?}", context, e, s);
Err(e)
}
a => a,
}
}
fn extention_attr(input: &str) -> IResult<Attribute> {
map(
tuple((
char('#'),
ws,
char('!'),
ws,
char('['),
ws,
tag("enable"),
ws,
char('('),
separated(ident),
char(')'),
ws,
char(']'),
)),
|(
_,
after_pound,
_,
after_exclamation,
_,
after_bracket,
_,
after_ident,
_,
extentions,
_,
after_paren,
_,
)| {
Attribute {
after_pound,
after_exclamation,
extentions,
after_bracket,
after_enable: after_ident,
after_paren,
}
},
)(input)
}
fn ident(input: &str) -> IResult<&str> {
alt((
recognize(pair(
tag("r#"),
cut(many1(alt((take_while1(is_xid_continue), is_a("+-."))))),
)),
recognize(pair(
alt((tag("_"), take_while_m_n(1, 1, is_xid_start))),
take_while(is_xid_continue),
)),
))(input)
}
fn block_comment(input: &str) -> IResult<Ws> {
map(
delimited(
tag("/*"),
recognize(many0(alt((
is_not("*/"),
recognize(block_comment),
take_until1("*/"),
)))),
tag("*/"),
),
Ws::BlockComment,
)(input)
}
fn ws(input: &str) -> IResult<Whitespace> {
map(
many0(alt((
map(is_a("\n\t\r "), Ws::Space),
map(
preceded(tag("//"), recognize(pair(is_not("\n"), line_ending))),
Ws::LineComment,
),
block_comment,
))),
Whitespace,
)(input)
}
fn separated<'a, T, P: Parser<&'a str, T, Error<'a>>>(
p: P,
) -> impl FnMut(&'a str) -> IResult<Separated<T>> {
context(
"separated",
map(
tuple((
separated_list0(char(','), ws_wrapped(p)),
opt(char(',')),
ws,
)),
|(values, trailing_comma, trailing_ws)| Separated {
values,
trailing_comma: trailing_comma.is_some(),
trailing_ws,
},
),
)
}
struct FormatOption<'a, T>(&'a Option<T>);
impl<T: Display> Display for FormatOption<'_, T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self.0 {
Some(t) => write!(f, "{t}"),
None => Ok(()),
}
}
}
#[cfg(test)]
mod test {
use include_dir::include_dir;
use super::*;
const INPUTS: include_dir::Dir = include_dir!("$CARGO_MANIFEST_DIR/tests/input");
#[test]
fn whitespace() {
assert_eq!(
block_comment("/* a */ ").finish().unwrap(),
(" ", Ws::BlockComment(" a ".into()))
);
assert_eq!(
block_comment("/* a /* inner */ */ ").finish().unwrap(),
(" ", Ws::BlockComment(" a /* inner */ ".into()))
);
assert_eq!(
ws("/* *//* a /* inner */ */").finish().unwrap(),
(
"",
Whitespace(vec![
Ws::BlockComment(" ".into()),
Ws::BlockComment(" a /* inner */ ".into())
])
)
);
}
#[test]
fn ensure_valid_ron() {
for file in INPUTS.entries() {
let _: ron::Value = ron::de::from_bytes(file.as_file().unwrap().contents()).unwrap();
}
}
#[test]
fn parse() {
for file in INPUTS.entries() {
let contents = file.as_file().unwrap().contents_utf8().unwrap();
let ron: File = contents.try_into().map_err(|e| eprintln!("{e}")).unwrap();
assert_eq!(ron.to_string(), contents);
insta::with_settings!({snapshot_path => "../tests/snapshots"},{
insta::assert_ron_snapshot!(file.path().display().to_string(), ron);
})
}
}
}