use crate::syntax;
use super::{character::line_ending_or_eof, expr, metadata};
use std::borrow::Cow;
use winnow::{
Parser,
ascii::{space0, space1, till_line_ending},
combinator::{alt, delimited, opt, preceded, repeat, terminated, trace},
error::{FromExternalError, ParserError},
stream::{AsChar, Stream, StreamIsPartial},
token::{literal, take_while},
};
#[inline(always)]
pub(super) fn is_comment_prefix<C: AsChar>(c: C) -> bool {
matches!(c.as_char(), ';' | '#' | '%' | '|' | '*')
}
pub fn account_declaration<'i, I, E>(
input: &mut I,
) -> winnow::Result<syntax::AccountDeclaration<'i>, E>
where
I: Stream<Slice = &'i str>
+ StreamIsPartial
+ StreamIsPartial
+ winnow::stream::Compare<&'static str>
+ winnow::stream::FindSlice<(char, char)>,
<I as Stream>::Token: AsChar + Clone,
E: ParserError<I>,
{
(
delimited(
(literal("account"), space1),
till_line_ending,
line_ending_or_eof,
),
repeat(
0..,
alt((
multiline_text((space1, take_while(1.., is_comment_prefix)))
.map(syntax::AccountDetail::Comment),
multiline_text((space1, literal("note"), space1)).map(syntax::AccountDetail::Note),
delimited(
(space1, literal("alias"), space1),
till_line_ending,
line_ending_or_eof,
)
.map(|a: &str| syntax::AccountDetail::Alias(a.trim_end().into())),
)),
),
)
.map(|(name, details): (&str, _)| syntax::AccountDeclaration {
name: name.trim_end().into(),
details,
})
.parse_next(input)
}
pub fn commodity_declaration<'i, I, E>(input: &mut I) -> Result<syntax::CommodityDeclaration<'i>, E>
where
I: Stream<Slice = &'i str>
+ StreamIsPartial
+ winnow::stream::Compare<&'static str>
+ winnow::stream::FindSlice<(char, char)>,
<I as Stream>::Token: AsChar + Clone,
E: ParserError<I> + FromExternalError<I, pretty_decimal::ParseError>,
{
(
delimited(
(literal("commodity"), space1),
till_line_ending,
line_ending_or_eof,
),
repeat(
0..,
alt((
multiline_text((space1, take_while(1.., is_comment_prefix)))
.map(syntax::CommodityDetail::Comment),
multiline_text((space1, literal("note"), space1))
.map(syntax::CommodityDetail::Note),
delimited(
(space1, literal("alias"), space1),
till_line_ending,
line_ending_or_eof,
)
.map(|a: &str| syntax::CommodityDetail::Alias(a.trim_end().into())),
delimited(
(space1, literal("format"), space1),
expr::amount,
line_ending_or_eof,
)
.map(syntax::CommodityDetail::Format),
)),
),
)
.map(
|(name, details): (&'i str, _)| syntax::CommodityDeclaration {
name: name.trim_end().into(),
details,
},
)
.parse_next(input)
}
pub fn apply_tag<'i, I, E>(input: &mut I) -> winnow::Result<syntax::ApplyTag<'i>, E>
where
I: Stream<Slice = &'i str>
+ StreamIsPartial
+ winnow::stream::Compare<&'static str>
+ winnow::stream::FindSlice<(char, char)>,
<I as Stream>::Token: AsChar + Clone,
E: ParserError<I>,
{
trace(
"directive::apply_tag",
(
preceded(
(literal("apply"), space1, literal("tag"), space1),
metadata::tag_key,
),
delimited(space0, opt(metadata::metadata_value), line_ending_or_eof),
)
.map(|(key, value): (&str, _)| syntax::ApplyTag {
key: key.into(),
value,
}),
)
.parse_next(input)
}
pub fn end_apply_tag<I, E>(input: &mut I) -> winnow::Result<<I as Stream>::Slice, E>
where
I: Stream + StreamIsPartial + winnow::stream::Compare<&'static str>,
<I as Stream>::Token: AsChar,
E: ParserError<I>,
{
trace(
"directive::end_apply_tag",
terminated(
(
literal("end"),
space1,
literal("apply"),
space1,
literal("tag"),
)
.take(),
(space0, line_ending_or_eof),
),
)
.parse_next(input)
}
pub fn include<'i, I, E>(input: &mut I) -> winnow::Result<syntax::IncludeFile<'i>, E>
where
I: Stream<Slice = &'i str>
+ StreamIsPartial
+ winnow::stream::FindSlice<(char, char)>
+ winnow::stream::Compare<&'static str>,
E: ParserError<I>,
<I as Stream>::Token: AsChar + Clone,
{
trace(
"directive::include",
delimited(
(literal("include"), space1),
till_line_ending,
line_ending_or_eof,
)
.map(|x: &str| syntax::IncludeFile(x.trim_end().into())),
)
.parse_next(input)
}
pub fn top_comment<'i, I, E>(input: &mut I) -> winnow::Result<syntax::TopLevelComment<'i>, E>
where
I: Stream<Slice = &'i str>
+ StreamIsPartial
+ winnow::stream::Compare<&'static str>
+ winnow::stream::FindSlice<(char, char)>,
<I as Stream>::Token: AsChar + Clone,
E: ParserError<I>,
{
trace(
"directive::top_comment",
multiline_text(take_while(1.., is_comment_prefix)).map(syntax::TopLevelComment),
)
.parse_next(input)
}
fn multiline_text<'a, I, E, F, O1>(prefix: F) -> impl Parser<I, Cow<'a, str>, E>
where
I: Stream<Slice = &'a str>
+ StreamIsPartial
+ winnow::stream::Compare<&'static str>
+ winnow::stream::FindSlice<(char, char)>,
E: ParserError<I>,
F: Parser<I, O1, E>,
<I as Stream>::Token: AsChar + Clone,
{
trace(
"directive::multiline_text",
repeat(1.., delimited(prefix, till_line_ending, line_ending_or_eof))
.fold(String::new, |mut ret, l| {
ret.push_str(l);
ret.push('\n');
ret
})
.map(Cow::Owned),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse::testing::expect_parse_ok;
use indoc::indoc;
use pretty_assertions::assert_eq;
use winnow::error::{ErrMode, InputError};
#[test]
fn account_declaration_without_details() {
let input = "account Foo:Bar Baz";
assert_eq!(
expect_parse_ok(account_declaration, input),
(
"",
syntax::AccountDeclaration {
name: "Foo:Bar Baz".into(),
details: vec![]
}
)
);
let input = "account Foo:Bar Baz\n2022";
assert_eq!(
expect_parse_ok(account_declaration, input),
(
"2022",
syntax::AccountDeclaration {
name: "Foo:Bar Baz".into(),
details: vec![]
}
)
);
}
#[test]
fn account_declaration_with_details() {
let input = indoc! {"
account Foo:Bar
; comment1
; comment1-cont
note note1
alias alias1
alias Alias 2:
note note2
note note2-cont
2020"};
assert_eq!(
expect_parse_ok(account_declaration, input),
(
"\n2020",
syntax::AccountDeclaration {
name: "Foo:Bar".into(),
details: vec![
syntax::AccountDetail::Comment(" comment1\n comment1-cont\n".into()),
syntax::AccountDetail::Note("note1\n".into()),
syntax::AccountDetail::Alias("alias1".into()),
syntax::AccountDetail::Alias("Alias 2:".into()),
syntax::AccountDetail::Note("note2\nnote2-cont\n".into()),
],
}
)
)
}
#[test]
fn apply_tag_without_value() {
let input = "apply tag foo ";
assert_eq!(
expect_parse_ok(apply_tag, input),
(
"",
syntax::ApplyTag {
key: "foo".into(),
value: None,
}
)
);
let input = "apply tag test@1-2!#[] \napply";
assert_eq!(
expect_parse_ok(apply_tag, input),
(
"apply",
syntax::ApplyTag {
key: "test@1-2!#[]".into(),
value: None,
}
)
);
}
#[test]
fn apply_tag_with_value() {
let input = "apply tag foo:bar\napply tag key";
assert_eq!(
expect_parse_ok(apply_tag, input),
(
"apply tag key",
syntax::ApplyTag {
key: "foo".into(),
value: Some(syntax::MetadataValue::Text("bar".into())),
}
)
);
let input = "apply tag foo: bar ";
assert_eq!(
expect_parse_ok(apply_tag, input),
(
"",
syntax::ApplyTag {
key: "foo".into(),
value: Some(syntax::MetadataValue::Text("bar".into())),
}
)
);
let input = "apply tag test@1-2!#[] :: [2022-3-4] \n";
assert_eq!(
expect_parse_ok(apply_tag, input),
(
"",
syntax::ApplyTag {
key: "test@1-2!#[]".into(),
value: Some(syntax::MetadataValue::Expr("[2022-3-4]".into())),
}
)
);
}
#[test]
fn end_apply_tag_accepts_valid_inputs() {
let input: &str = "end apply tag";
assert_eq!(expect_parse_ok(end_apply_tag, input), ("", "end apply tag"));
let input: &str = "end apply tag \nfoo";
assert_eq!(
expect_parse_ok(end_apply_tag, input),
("foo", "end apply tag")
);
}
#[test]
fn end_apply_tag_rejects_unexpected() {
let input: &str = "end apply tag following";
assert_eq!(
end_apply_tag.parse_peek(input),
Err(ErrMode::Backtrack(InputError::at("following")))
);
let input: &str = "end applytag";
assert_eq!(
end_apply_tag.parse_peek(input),
Err(ErrMode::Backtrack(InputError::at("tag")))
);
let input: &str = "endapply tag";
assert_eq!(
end_apply_tag.parse_peek(input),
Err(ErrMode::Backtrack(InputError::at("apply tag")))
);
}
#[test]
fn include_parses_normal_file() {
assert_eq!(
expect_parse_ok(include, "include foobar.ledger\n"),
("", syntax::IncludeFile("foobar.ledger".into()))
);
}
#[test]
fn include_trims_space_in_end() {
assert_eq!(
expect_parse_ok(include, "include foobar.ledger \n"),
("", syntax::IncludeFile("foobar.ledger".into()))
);
}
#[test]
fn include_keeps_spaces_in_the_middle() {
assert_eq!(
expect_parse_ok(include, "include\t\t /path/to/foo bar.ledger \n"),
("", syntax::IncludeFile("/path/to/foo bar.ledger".into()))
);
}
#[test]
fn top_comment_single_line() {
assert_eq!(
expect_parse_ok(top_comment, ";foo"),
("", syntax::TopLevelComment("foo\n".into()))
);
assert_eq!(
expect_parse_ok(top_comment, ";foo\nbaz"),
("baz", syntax::TopLevelComment("foo\n".into()))
);
}
#[test]
fn top_comment_multi_lines() {
assert_eq!(
expect_parse_ok(top_comment, ";foo\n;bar"),
("", syntax::TopLevelComment("foo\nbar\n".into()))
);
assert_eq!(
expect_parse_ok(top_comment, ";foo\n#bar\nbaz"),
("baz", syntax::TopLevelComment("foo\nbar\n".into()))
);
}
}