use crate::lang::{
elements::{Located, MathBlock},
parsers::{
utils::{
any_line, beginning_of_line, capture, context, cow_str,
end_of_line_or_input, locate, take_line_until1,
},
IResult, Span,
},
};
use nom::{
bytes::complete::tag,
character::complete::{char, line_ending, space0},
combinator::{map_parser, not, opt},
multi::many0,
sequence::{delimited, preceded},
};
use std::borrow::Cow;
pub fn math_block<'a>(input: Span<'a>) -> IResult<Located<MathBlock<'a>>> {
fn inner(input: Span) -> IResult<MathBlock> {
let (input, environment) = beginning_of_math_block(input)?;
let (input, lines) = many0(preceded(
not(end_of_math_block),
map_parser(any_line, cow_str),
))(input)?;
let (input, _) = end_of_math_block(input)?;
let math_block = MathBlock::new(lines, environment);
Ok((input, math_block))
}
context("Math Block", locate(capture(inner)))(input)
}
fn beginning_of_math_block<'a>(
input: Span<'a>,
) -> IResult<Option<Cow<'a, str>>> {
let environment_parser =
delimited(char('%'), take_line_until1("%"), char('%'));
let (input, _) = beginning_of_line(input)?;
let (input, _) = space0(input)?;
let (input, _) = tag("{{$")(input)?;
let (input, environment) =
opt(map_parser(environment_parser, cow_str))(input)?;
let (input, _) = space0(input)?;
let (input, _) = line_ending(input)?;
Ok((input, environment))
}
fn end_of_math_block(input: Span) -> IResult<()> {
let (input, _) = beginning_of_line(input)?;
let (input, _) = space0(input)?;
let (input, _) = tag("}}$")(input)?;
let (input, _) = space0(input)?;
let (input, _) = end_of_line_or_input(input)?;
Ok((input, ()))
}
#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
#[test]
fn math_block_should_fail_if_input_empty() {
let input = Span::from("");
assert!(math_block(input).is_err());
}
#[test]
fn math_block_should_fail_if_does_not_start_with_dedicated_line() {
let input = Span::from(indoc! {r"
\sum_i a_i^2
}}$
"});
assert!(math_block(input).is_err());
}
#[test]
fn math_block_should_fail_if_does_not_end_with_dedicated_line() {
let input = Span::from(indoc! {r"
{{$
\sum_i a_i^2
"});
assert!(math_block(input).is_err());
}
#[test]
fn math_block_should_support_zero_lines_between_as_formula() {
let input = Span::from(indoc! {r"
{{$
}}$
"});
let (input, m) = math_block(input).unwrap();
assert!(input.is_empty(), "Did not consume math block");
assert!(m.lines.is_empty(), "Has lines unexpectedly");
assert_eq!(m.environment, None);
}
#[test]
fn math_block_should_consume_all_lines_between_as_formula() {
let input = Span::from(indoc! {r"
{{$
\sum_i a_i^2
=
1
}}$
"});
let (input, m) = math_block(input).unwrap();
assert!(input.is_empty(), "Did not consume math block");
assert_eq!(
m.lines.iter().map(AsRef::as_ref).collect::<Vec<&str>>(),
vec![r"\sum_i a_i^2", "=", "1"]
);
assert_eq!(m.environment, None);
}
#[test]
fn math_block_should_fail_if_environment_delimiters_not_used_correctly() {
let input = Span::from(indoc! {r"
{{$%align
\sum_i a_i^2
=
1
}}$
"});
assert!(math_block(input).is_err());
let input = Span::from(indoc! {r"
{{$align%
\sum_i a_i^2
=
1
}}$
"});
assert!(math_block(input).is_err());
let input = Span::from(indoc! {r"
{{$%%
\sum_i a_i^2
=
1
}}$
"});
assert!(math_block(input).is_err());
}
#[test]
fn math_block_should_accept_optional_environment_specifier() {
let input = Span::from(indoc! {r"
{{$%align%
\sum_i a_i^2 &= 1 + 1 \\
&= 2.
}}$
"});
let (input, m) = math_block(input).unwrap();
assert!(input.is_empty(), "Did not consume math block");
assert_eq!(
m.lines.iter().map(AsRef::as_ref).collect::<Vec<&str>>(),
vec![r"\sum_i a_i^2 &= 1 + 1 \\", r"&= 2."]
);
assert_eq!(m.environment.as_deref(), Some("align"));
}
}