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::many_till; use nom::multi::separated_list1; use nom::sequence::tuple; use super::element::Element; use super::object::Object; use super::org_source::convert_error; use super::org_source::OrgSource; use super::parser_with_context::parser_with_context; use super::source::Source; use super::token::AllTokensIterator; use super::token::Token; use super::util::exit_matcher_parser; use super::util::get_consumed; use super::util::start_of_line; use super::Context; use crate::error::Res; use crate::parser::comment::comment; use crate::parser::element_parser::element; use crate::parser::exiting::ExitClass; use crate::parser::object_parser::standard_set_object; use crate::parser::parser_context::ContextElement; use crate::parser::parser_context::ContextTree; use crate::parser::parser_context::ExitMatcherNode; use crate::parser::planning::planning; use crate::parser::property_drawer::property_drawer; use crate::parser::util::blank_line; use crate::parser::util::maybe_consume_trailing_whitespace_if_not_exiting; #[derive(Debug)] pub struct Document<'s> { pub source: &'s str, pub zeroth_section: Option>, pub children: Vec>, } #[derive(Debug)] pub struct Heading<'s> { pub source: &'s str, pub stars: usize, pub title: Vec>, pub children: Vec>, } #[derive(Debug)] pub struct Section<'s> { pub source: &'s str, pub children: Vec>, } #[derive(Debug)] pub enum DocumentElement<'s> { Heading(Heading<'s>), Section(Section<'s>), } impl<'s> Source<'s> for Document<'s> { fn get_source(&'s self) -> &'s str { self.source } } impl<'s> Source<'s> for DocumentElement<'s> { fn get_source(&'s self) -> &'s str { match self { DocumentElement::Heading(obj) => obj.source, DocumentElement::Section(obj) => obj.source, } } } impl<'s> Source<'s> for Section<'s> { fn get_source(&'s self) -> &'s str { self.source } } impl<'s> Source<'s> for Heading<'s> { fn get_source(&'s self) -> &'s str { self.source } } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[allow(dead_code)] pub fn document(input: &str) -> Res<&str, Document> { let initial_context: ContextTree<'_, '_> = ContextTree::new(); let wrapped_input = OrgSource::new(input); let (remaining, document) = _document(&initial_context, wrapped_input) .map(|(rem, out)| (Into::<&str>::into(rem), out)) .map_err(convert_error)?; { // If there are radio targets in this document then we need to parse the entire document again with the knowledge of the radio targets. let all_radio_targets: Vec<&Vec>> = document .iter_tokens() .filter_map(|tkn| match tkn { Token::Object(obj) => Some(obj), _ => None, }) .filter_map(|obj| match obj { Object::RadioTarget(rt) => Some(rt), _ => None, }) .map(|rt| &rt.children) .collect(); if !all_radio_targets.is_empty() { let initial_context = initial_context .with_additional_node(ContextElement::RadioTarget(all_radio_targets)); let (remaining, document) = _document(&initial_context, wrapped_input) .map(|(rem, out)| (Into::<&str>::into(rem), out)) .map_err(convert_error)?; return Ok((remaining.into(), document)); } } Ok((remaining.into(), document)) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] fn _document<'r, 's>( context: Context<'r, 's>, input: OrgSource<'s>, ) -> Res, Document<'s>> { let zeroth_section_matcher = parser_with_context!(zeroth_section)(context); let heading_matcher = parser_with_context!(heading)(context); let (remaining, _blank_lines) = many0(blank_line)(input)?; let (remaining, zeroth_section) = opt(zeroth_section_matcher)(remaining)?; let (remaining, children) = many0(heading_matcher)(remaining)?; let source = get_consumed(input, remaining); Ok(( remaining, Document { source: source.into(), zeroth_section, children, }, )) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] fn zeroth_section<'r, 's>( context: Context<'r, 's>, input: OrgSource<'s>, ) -> Res, Section<'s>> { // TODO: The zeroth section is specialized so it probably needs its own parser let parser_context = context .with_additional_node(ContextElement::ConsumeTrailingWhitespace(true)) .with_additional_node(ContextElement::Context("section")) .with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode { class: ExitClass::Document, exit_matcher: §ion_end, })); let without_consuming_whitespace_context = parser_context.with_additional_node(ContextElement::ConsumeTrailingWhitespace(false)); let element_matcher = parser_with_context!(element(true))(&parser_context); let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context); let (remaining, comment_and_property_drawer_element) = opt(tuple(( opt(parser_with_context!(comment)( &without_consuming_whitespace_context, )), parser_with_context!(property_drawer)(context), many0(blank_line), )))(input)?; let (remaining, (mut children, _exit_contents)) = verify( many_till(element_matcher, exit_matcher), |(children, _exit_contents)| { !children.is_empty() || comment_and_property_drawer_element.is_some() }, )(remaining)?; comment_and_property_drawer_element.map(|(comment, property_drawer, _ws)| { children.insert(0, Element::PropertyDrawer(property_drawer)); comment .map(Element::Comment) .map(|ele| children.insert(0, ele)); }); let (remaining, _trailing_ws) = maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?; let source = get_consumed(input, remaining); Ok(( remaining, Section { source: source.into(), children, }, )) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] fn section<'r, 's>( context: Context<'r, 's>, mut input: OrgSource<'s>, ) -> Res, Section<'s>> { // TODO: The zeroth section is specialized so it probably needs its own parser let parser_context = context .with_additional_node(ContextElement::ConsumeTrailingWhitespace(true)) .with_additional_node(ContextElement::Context("section")) .with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode { class: ExitClass::Document, exit_matcher: §ion_end, })); let element_matcher = parser_with_context!(element(true))(&parser_context); let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context); let (mut remaining, (planning_element, property_drawer_element)) = tuple(( opt(parser_with_context!(planning)(&parser_context)), opt(parser_with_context!(property_drawer)(&parser_context)), ))(input)?; if planning_element.is_none() && property_drawer_element.is_none() { let (remain, _ws) = many0(blank_line)(remaining)?; remaining = remain; input = remain; } let (remaining, (mut children, _exit_contents)) = verify( many_till(element_matcher, exit_matcher), |(children, _exit_contents)| { !children.is_empty() || property_drawer_element.is_some() || planning_element.is_some() }, )(remaining)?; property_drawer_element .map(Element::PropertyDrawer) .map(|ele| children.insert(0, ele)); planning_element .map(Element::Planning) .map(|ele| children.insert(0, ele)); let (remaining, _trailing_ws) = maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?; let source = get_consumed(input, remaining); Ok(( remaining, Section { source: source.into(), children, }, )) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] fn section_end<'r, 's>( context: Context<'r, 's>, input: OrgSource<'s>, ) -> Res, OrgSource<'s>> { let headline_matcher = parser_with_context!(headline)(context); recognize(headline_matcher)(input) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] fn heading<'r, 's>( context: Context<'r, 's>, input: OrgSource<'s>, ) -> Res, Heading<'s>> { not(|i| context.check_exit_matcher(i))(input)?; let (remaining, (star_count, _ws, title)) = headline(context, input)?; let section_matcher = parser_with_context!(section)(context); let heading_matcher = parser_with_context!(heading)(context); let (remaining, children) = many0(alt(( map( verify(heading_matcher, |h| h.stars > star_count), DocumentElement::Heading, ), map(section_matcher, DocumentElement::Section), )))(remaining)?; let source = get_consumed(input, remaining); Ok(( remaining, Heading { source: source.into(), stars: star_count, title, children, }, )) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] fn headline<'r, 's>( context: Context<'r, 's>, input: OrgSource<'s>, ) -> Res, (usize, OrgSource<'s>, Vec>)> { let parser_context = context.with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode { class: ExitClass::Document, exit_matcher: &headline_title_end, })); let standard_set_object_matcher = parser_with_context!(standard_set_object)(&parser_context); let (remaining, (_sol, star_count, ws, title, maybe_tags, _ws, _line_ending)) = tuple(( start_of_line, many1_count(tag("*")), space1, many1(standard_set_object_matcher), opt(tuple((space0, tags))), space0, alt((line_ending, eof)), ))(input)?; Ok((remaining, (star_count, ws, title))) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] fn headline_title_end<'r, 's>( _context: Context<'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) } impl<'s> Document<'s> { pub fn iter_tokens<'r>(&'r self) -> impl Iterator> { AllTokensIterator::new(Token::Document(self)) } }