use nom::branch::alt; use nom::bytes::complete::tag; use nom::character::complete::anychar; use nom::character::complete::line_ending; use nom::character::complete::space0; use nom::character::complete::space1; use nom::combinator::eof; use nom::combinator::map; use nom::combinator::not; use nom::combinator::opt; use nom::combinator::recognize; use nom::combinator::verify; use nom::multi::many0; use nom::multi::many1; use nom::multi::many1_count; use nom::multi::separated_list1; use nom::sequence::tuple; use super::org_source::OrgSource; use super::section::section; use super::util::get_consumed; 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::Object; use crate::types::PriorityCookie; use crate::types::TodoKeywordType; pub(crate) const fn heading( parent_stars: usize, ) -> impl for<'b, 'g, 'r, 's> Fn( RefContext<'b, 'g, 'r, 's>, OrgSource<'s>, ) -> Res, Heading<'s>> { move |context, input: OrgSource<'_>| _heading(context, input, parent_stars) } #[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_stars: usize, ) -> Res, Heading<'s>> { not(|i| context.check_exit_matcher(i))(input)?; let ( remaining, (star_count, maybe_todo_keyword, maybe_priority, maybe_comment, title, heading_tags), ) = headline(context, input, parent_stars)?; let section_matcher = parser_with_context!(section)(context); let heading_matcher = parser_with_context!(heading(star_count))(context); 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"); let source = get_consumed(input, remaining); Ok(( remaining, Heading { source: source.into(), stars: star_count, todo_keyword: maybe_todo_keyword.map(|((todo_keyword_type, todo_keyword), _ws)| { (todo_keyword_type, Into::<&str>::into(todo_keyword)) }), priority_cookie: maybe_priority.map(|(priority, _)| priority), title, tags: heading_tags, children, is_comment: maybe_comment.is_some(), is_archived, }, )) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] pub(crate) fn detect_headline<'s>(input: OrgSource<'s>) -> Res, ()> { 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_stars: usize, ) -> Res< OrgSource<'s>, ( usize, Option<((TodoKeywordType, OrgSource<'s>), OrgSource<'s>)>, Option<(PriorityCookie, OrgSource<'s>)>, Option<(OrgSource<'s>, OrgSource<'s>)>, Vec>, 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, ( _, star_count, _, maybe_todo_keyword, maybe_priority, maybe_comment, title, maybe_tags, _, _, ), ) = tuple(( start_of_line, verify(many1_count(tag("*")), |star_count| { *star_count > parent_stars }), space1, opt(tuple(( parser_with_context!(heading_keyword)(&parser_context), space1, ))), opt(tuple((priority_cookie, space1))), opt(tuple((tag("COMMENT"), space1))), many1(parser_with_context!(standard_set_object)(&parser_context)), opt(tuple((space0, tags))), space0, alt((line_ending, eof)), ))(input)?; Ok(( remaining, ( star_count, maybe_todo_keyword, maybe_priority, maybe_comment, title, 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>> { recognize(tuple(( opt(tuple((space0, tags, space0))), alt((line_ending, eof)), )))(input) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] fn tags<'s>(input: OrgSource<'s>) -> Res, Vec>> { 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>> { 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, (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, 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)) }