organic/src/parser/headline.rs

296 lines
9.7 KiB
Rust
Raw Normal View History

2023-09-08 19:05:42 +00:00
use nom::branch::alt;
use nom::bytes::complete::is_a;
2023-09-08 19:05:42 +00:00
use nom::bytes::complete::tag;
use nom::character::complete::anychar;
use nom::character::complete::space0;
2023-09-08 19:05:42 +00:00
use nom::character::complete::space1;
2023-10-02 14:48:34 +00:00
use nom::combinator::consumed;
2023-09-08 19:05:42 +00:00
use nom::combinator::map;
use nom::combinator::not;
use nom::combinator::opt;
use nom::combinator::peek;
2023-09-08 19:05:42 +00:00
use nom::combinator::recognize;
use nom::combinator::verify;
use nom::multi::many0;
use nom::multi::many1;
use nom::multi::separated_list1;
use nom::sequence::tuple;
use super::org_source::OrgSource;
use super::section::section;
2023-09-08 19:05:42 +00:00
use super::util::get_consumed;
use super::util::org_line_ending;
use super::util::org_space;
use super::util::org_space_or_line_ending;
2023-09-08 19:05:42 +00:00
use super::util::start_of_line;
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::object_parser::standard_set_object;
use crate::parser::util::blank_line;
use crate::types::DocumentElement;
use crate::types::Heading;
use crate::types::HeadlineLevel;
2023-09-08 19:05:42 +00:00
use crate::types::Object;
use crate::types::PriorityCookie;
2023-09-08 19:05:42 +00:00
use crate::types::TodoKeywordType;
2023-09-11 17:13:28 +00:00
pub(crate) const fn heading(
parent_level: HeadlineLevel,
2023-09-08 19:05:42 +00:00
) -> impl for<'b, 'g, 'r, 's> Fn(
RefContext<'b, 'g, 'r, 's>,
OrgSource<'s>,
) -> Res<OrgSource<'s>, Heading<'s>> {
move |context, input: OrgSource<'_>| _heading(context, input, parent_level)
2023-09-08 19:05:42 +00:00
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn _heading<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
parent_star_count: HeadlineLevel,
2023-09-08 19:05:42 +00:00
) -> Res<OrgSource<'s>, Heading<'s>> {
not(|i| context.check_exit_matcher(i))(input)?;
let (
remaining,
(
headline_level,
star_count,
maybe_todo_keyword,
maybe_priority,
maybe_comment,
title,
heading_tags,
),
) = headline(context, input, parent_star_count)?;
2023-09-08 19:05:42 +00:00
let section_matcher = parser_with_context!(section)(context);
let heading_matcher = parser_with_context!(heading(star_count))(context);
2023-09-08 19:05:42 +00:00
let (remaining, maybe_section) =
opt(map(section_matcher, DocumentElement::Section))(remaining)?;
let (remaining, _ws) = opt(tuple((start_of_line, many0(blank_line))))(remaining)?;
let (remaining, mut children) =
many0(map(heading_matcher, DocumentElement::Heading))(remaining)?;
if let Some(section) = maybe_section {
children.insert(0, section);
}
let remaining = if children.is_empty() {
// Support empty headings
let (remain, _ws) = many0(blank_line)(remaining)?;
remain
} else {
remaining
};
let is_archived = heading_tags.contains(&"ARCHIVE");
2023-09-08 19:05:42 +00:00
let source = get_consumed(input, remaining);
Ok((
remaining,
Heading {
source: source.into(),
level: headline_level,
todo_keyword: maybe_todo_keyword.map(|(todo_keyword_type, todo_keyword)| {
2023-09-08 19:05:42 +00:00
(todo_keyword_type, Into::<&str>::into(todo_keyword))
}),
priority_cookie: maybe_priority.map(|(_, priority)| priority),
2023-09-08 19:05:42 +00:00
title,
tags: heading_tags,
children,
is_comment: maybe_comment.is_some(),
is_archived,
2023-10-02 14:48:34 +00:00
is_footnote_section: false, // TODO
2023-09-08 19:05:42 +00:00
},
))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
2023-09-11 17:13:28 +00:00
pub(crate) fn detect_headline<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, ()> {
2023-09-08 19:05:42 +00:00
tuple((start_of_line, many1(tag("*")), space1))(input)?;
Ok((input, ()))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn headline<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
parent_star_count: HeadlineLevel,
2023-09-08 19:05:42 +00:00
) -> Res<
OrgSource<'s>,
(
HeadlineLevel,
HeadlineLevel,
Option<(TodoKeywordType, OrgSource<'s>)>,
Option<(OrgSource<'s>, PriorityCookie)>,
Option<OrgSource<'s>>,
2023-09-08 19:05:42 +00:00
Vec<Object<'s>>,
Vec<&'s str>,
),
> {
let parser_context = ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Document,
exit_matcher: &headline_title_end,
});
let parser_context = context.with_additional_node(&parser_context);
let (remaining, (_, (headline_level, star_count, _), _)) = tuple((
2023-09-08 19:05:42 +00:00
start_of_line,
verify(
parser_with_context!(headline_level)(&parser_context),
|(_, count, _)| *count > parent_star_count,
),
peek(org_space),
2023-09-08 19:05:42 +00:00
))(input)?;
let (remaining, maybe_todo_keyword) = opt(tuple((
space1,
parser_with_context!(heading_keyword)(&parser_context),
peek(org_space_or_line_ending),
)))(remaining)?;
let (remaining, maybe_priority) = opt(tuple((space1, priority_cookie)))(remaining)?;
let (remaining, maybe_comment) = opt(tuple((
space1,
tag("COMMENT"),
peek(org_space_or_line_ending),
)))(remaining)?;
let (remaining, maybe_title) = opt(tuple((
space1,
2023-10-02 14:48:34 +00:00
consumed(many1(parser_with_context!(standard_set_object)(
&parser_context,
))),
)))(remaining)?;
let (remaining, maybe_tags) = opt(tuple((space0, tags)))(remaining)?;
let (remaining, _) = tuple((space0, org_line_ending))(remaining)?;
2023-09-08 19:05:42 +00:00
Ok((
remaining,
(
headline_level,
2023-09-08 19:05:42 +00:00
star_count,
maybe_todo_keyword.map(|(_, todo, _)| todo),
maybe_priority,
maybe_comment.map(|(_, comment, _)| comment),
2023-10-02 14:48:34 +00:00
maybe_title
.map(|(_, (_, title))| title)
.unwrap_or(Vec::new()),
2023-09-08 19:05:42 +00:00
maybe_tags
.map(|(_ws, tags)| {
tags.into_iter()
.map(|single_tag| Into::<&str>::into(single_tag))
.collect()
})
.unwrap_or(Vec::new()),
),
))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn headline_title_end<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
recognize(tuple((space0, opt(tuple((tags, space0))), org_line_ending)))(input)
2023-09-08 19:05:42 +00:00
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn tags<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, Vec<OrgSource<'s>>> {
let (remaining, (_open, tags, _close)) =
tuple((tag(":"), separated_list1(tag(":"), single_tag), tag(":")))(input)?;
Ok((remaining, tags))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn single_tag<'r, 's>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
recognize(many1(verify(anychar, |c| {
c.is_alphanumeric() || "_@#%".contains(*c)
})))(input)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn heading_keyword<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, (TodoKeywordType, OrgSource<'s>)> {
let global_settings = context.get_global_settings();
if global_settings.in_progress_todo_keywords.is_empty()
&& global_settings.complete_todo_keywords.is_empty()
{
alt((
map(tag("TODO"), |capture| (TodoKeywordType::Todo, capture)),
map(tag("DONE"), |capture| (TodoKeywordType::Done, capture)),
))(input)
} else {
for todo_keyword in global_settings
.in_progress_todo_keywords
.iter()
.map(String::as_str)
{
let result = tag::<_, _, CustomError<_>>(todo_keyword)(input);
match result {
Ok((remaining, ent)) => {
return Ok((remaining, (TodoKeywordType::Todo, ent)));
}
Err(_) => {}
}
}
for todo_keyword in global_settings
.complete_todo_keywords
.iter()
.map(String::as_str)
{
let result = tag::<_, _, CustomError<_>>(todo_keyword)(input);
match result {
Ok((remaining, ent)) => {
return Ok((remaining, (TodoKeywordType::Done, ent)));
}
Err(_) => {}
}
}
Err(nom::Err::Error(CustomError::MyError(MyError(
"NoTodoKeyword".into(),
))))
}
}
fn priority_cookie<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, PriorityCookie> {
let (remaining, (_, priority_character, _)) = tuple((
tag("[#"),
verify(anychar, |c| c.is_alphanumeric()),
tag("]"),
))(input)?;
let cookie = PriorityCookie::try_from(priority_character).map_err(|_| {
nom::Err::Error(CustomError::MyError(MyError(
"Failed to cast priority cookie to number.".into(),
)))
})?;
Ok((remaining, cookie))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn headline_level<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, (HeadlineLevel, HeadlineLevel, OrgSource<'s>)> {
let (remaining, stars) = is_a("*")(input)?;
let count = stars.len().try_into().unwrap();
let level = match context.get_global_settings().odd_levels_only {
crate::context::HeadlineLevelFilter::Odd => {
if count % 2 == 0 {
(count + 2) / 2
} else {
(count + 1) / 2
}
}
crate::context::HeadlineLevelFilter::OddEven => count,
};
Ok((remaining, (level, count, stars)))
}