198 lines
6.8 KiB
Rust
198 lines
6.8 KiB
Rust
use nom::branch::alt;
|
|
use nom::bytes::complete::tag;
|
|
use nom::bytes::complete::tag_no_case;
|
|
use nom::bytes::complete::take_while;
|
|
use nom::character::complete::digit1;
|
|
use nom::character::complete::space0;
|
|
use nom::combinator::opt;
|
|
use nom::combinator::recognize;
|
|
use nom::combinator::verify;
|
|
use nom::multi::many0;
|
|
use nom::multi::many1;
|
|
use nom::multi::many_till;
|
|
use nom::sequence::tuple;
|
|
|
|
use super::org_source::OrgSource;
|
|
use super::util::include_input;
|
|
use super::util::WORD_CONSTITUENT_CHARACTERS;
|
|
use crate::context::parser_with_context;
|
|
use crate::context::ContextElement;
|
|
use crate::context::ExitClass;
|
|
use crate::context::ExitMatcherNode;
|
|
use crate::context::RefContext;
|
|
use crate::error::CustomError;
|
|
use crate::error::MyError;
|
|
use crate::error::Res;
|
|
use crate::parser::element_parser::element;
|
|
use crate::parser::util::blank_line;
|
|
use crate::parser::util::exit_matcher_parser;
|
|
use crate::parser::util::get_consumed;
|
|
use crate::parser::util::immediate_in_section;
|
|
use crate::parser::util::maybe_consume_trailing_whitespace;
|
|
use crate::parser::util::start_of_line;
|
|
use crate::types::FootnoteDefinition;
|
|
|
|
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
|
fn footnote_definition<'b, 'g, 'r, 's>(
|
|
context: RefContext<'b, 'g, 'r, 's>,
|
|
input: OrgSource<'s>,
|
|
) -> Res<OrgSource<'s>, FootnoteDefinition<'s>> {
|
|
if immediate_in_section(context, "footnote definition") {
|
|
return Err(nom::Err::Error(CustomError::MyError(MyError(
|
|
"Cannot nest objects of the same element".into(),
|
|
))));
|
|
}
|
|
start_of_line(input)?;
|
|
// Cannot be indented.
|
|
let (remaining, (_, lbl, _, _, _)) = tuple((
|
|
tag_no_case("[fn:"),
|
|
label,
|
|
tag("]"),
|
|
space0,
|
|
opt(verify(many0(blank_line), |lines: &Vec<OrgSource<'_>>| {
|
|
lines.len() <= 2
|
|
})),
|
|
))(input)?;
|
|
let contexts = [
|
|
ContextElement::ConsumeTrailingWhitespace(true),
|
|
ContextElement::Context("footnote definition"),
|
|
ContextElement::ExitMatcherNode(ExitMatcherNode {
|
|
class: ExitClass::Alpha,
|
|
exit_matcher: &footnote_definition_end,
|
|
}),
|
|
];
|
|
let parser_context = context.with_additional_node(&contexts[0]);
|
|
let parser_context = parser_context.with_additional_node(&contexts[1]);
|
|
let parser_context = parser_context.with_additional_node(&contexts[2]);
|
|
let element_matcher = parser_with_context!(element(true))(&parser_context);
|
|
let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context);
|
|
let (mut remaining, (mut children, _exit_contents)) =
|
|
many_till(include_input(element_matcher), exit_matcher)(remaining)?;
|
|
|
|
// Re-parse the last element of the footnote definition with consume trailing whitespace off because the trailing whitespace needs to belong to the footnote definition, not the contents.
|
|
if context.should_consume_trailing_whitespace() {
|
|
if let Some((final_item_input, _)) = children.pop() {
|
|
let final_item_context = ContextElement::ConsumeTrailingWhitespace(false);
|
|
let final_item_context = parser_context.with_additional_node(&final_item_context);
|
|
let (remain, reparsed_final_item) =
|
|
parser_with_context!(element(true))(&final_item_context)(final_item_input)?;
|
|
children.push((final_item_input, reparsed_final_item));
|
|
remaining = remain;
|
|
}
|
|
}
|
|
|
|
let source = get_consumed(input, remaining);
|
|
Ok((
|
|
remaining,
|
|
FootnoteDefinition {
|
|
source: source.into(),
|
|
label: lbl.into(),
|
|
children: children.into_iter().map(|(_, item)| item).collect(),
|
|
},
|
|
))
|
|
}
|
|
|
|
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
|
fn label<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
|
alt((
|
|
digit1,
|
|
take_while(|c| WORD_CONSTITUENT_CHARACTERS.contains(c) || "-_".contains(c)),
|
|
))(input)
|
|
}
|
|
|
|
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
|
fn footnote_definition_end<'b, 'g, 'r, 's>(
|
|
context: RefContext<'b, 'g, 'r, 's>,
|
|
input: OrgSource<'s>,
|
|
) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
|
let (remaining, source) = alt((
|
|
recognize(tuple((
|
|
parser_with_context!(maybe_consume_trailing_whitespace)(context),
|
|
detect_footnote_definition,
|
|
))),
|
|
recognize(tuple((
|
|
start_of_line,
|
|
verify(many1(blank_line), |lines: &Vec<OrgSource<'_>>| {
|
|
lines.len() >= 2
|
|
}),
|
|
))),
|
|
))(input)?;
|
|
|
|
Ok((remaining, source))
|
|
}
|
|
|
|
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
|
fn detect_footnote_definition<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, ()> {
|
|
tuple((start_of_line, tag_no_case("[fn:"), label, tag("]")))(input)?;
|
|
Ok((input, ()))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::context::Context;
|
|
use crate::context::GlobalSettings;
|
|
use crate::context::List;
|
|
use crate::types::Source;
|
|
|
|
#[test]
|
|
fn two_paragraphs() {
|
|
let input = OrgSource::new(
|
|
"[fn:1] A footnote.
|
|
|
|
[fn:2] A multi-
|
|
|
|
line footnote.",
|
|
);
|
|
let global_settings = GlobalSettings::default();
|
|
let initial_context = ContextElement::document_context();
|
|
let initial_context = Context::new(&global_settings, List::new(&initial_context));
|
|
let footnote_definition_matcher = parser_with_context!(element(true))(&initial_context);
|
|
let (remaining, first_footnote_definition) =
|
|
footnote_definition_matcher(input).expect("Parse first footnote_definition");
|
|
let (remaining, second_footnote_definition) =
|
|
footnote_definition_matcher(remaining).expect("Parse second footnote_definition.");
|
|
assert_eq!(Into::<&str>::into(remaining), "");
|
|
assert_eq!(
|
|
first_footnote_definition.get_source(),
|
|
"[fn:1] A footnote.
|
|
|
|
"
|
|
);
|
|
assert_eq!(
|
|
second_footnote_definition.get_source(),
|
|
"[fn:2] A multi-
|
|
|
|
line footnote."
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn multiline_break() {
|
|
let input = OrgSource::new(
|
|
"[fn:2] A multi-
|
|
|
|
line footnote.
|
|
|
|
|
|
not in the footnote.",
|
|
);
|
|
let global_settings = GlobalSettings::default();
|
|
let initial_context = ContextElement::document_context();
|
|
let initial_context = Context::new(&global_settings, List::new(&initial_context));
|
|
let footnote_definition_matcher = parser_with_context!(element(true))(&initial_context);
|
|
let (remaining, first_footnote_definition) =
|
|
footnote_definition_matcher(input).expect("Parse first footnote_definition");
|
|
assert_eq!(Into::<&str>::into(remaining), "not in the footnote.");
|
|
assert_eq!(
|
|
first_footnote_definition.get_source(),
|
|
"[fn:2] A multi-
|
|
|
|
line footnote.
|
|
|
|
|
|
"
|
|
);
|
|
}
|
|
}
|