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::all_consuming; 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::in_buffer_settings::apply_in_buffer_settings; use super::in_buffer_settings::scan_for_in_buffer_settings; use super::org_source::OrgSource; 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 crate::context::parser_with_context; use crate::context::Context; use crate::context::ContextElement; use crate::context::ExitClass; use crate::context::ExitMatcherNode; use crate::context::GlobalSettings; use crate::context::List; use crate::context::RefContext; use crate::error::CustomError; use crate::error::MyError; use crate::error::Res; use crate::parser::comment::comment; use crate::parser::element_parser::element; use crate::parser::object_parser::standard_set_object; use crate::parser::org_source::convert_error; 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; use crate::types::Document; use crate::types::DocumentElement; use crate::types::Element; use crate::types::Heading; use crate::types::Object; use crate::types::Section; /// Parse a full org-mode document. /// /// This is the main entry point for Organic. It will parse the full contents of the input string as an org-mode document. #[allow(dead_code)] pub fn parse<'s>(input: &'s str) -> Result, String> { parse_with_settings(input, &GlobalSettings::default()) } /// Parse a full org-mode document with starting settings. /// /// This is the secondary entry point for Organic. It will parse the full contents of the input string as an org-mode document starting with the settings you supplied. /// /// This will not prevent additional settings from being learned during parsing, for example when encountering a "#+TODO". #[allow(dead_code)] pub fn parse_with_settings<'g, 's>( input: &'s str, global_settings: &'g GlobalSettings<'g, 's>, ) -> Result, String> { let initial_context = ContextElement::document_context(); let initial_context = Context::new(global_settings, List::new(&initial_context)); let wrapped_input = OrgSource::new(input); let ret = all_consuming(parser_with_context!(document_org_source)(&initial_context))(wrapped_input) .map_err(|err| err.to_string()) .map(|(_remaining, parsed_document)| parsed_document); ret } /// Parse a full org-mode document. /// /// Use this entry point when you want to have direct control over the starting context or if you want to use this integrated with other nom parsers. For general-purpose usage, the `parse` and `parse_with_settings` functions are a lot simpler. /// /// This will not prevent additional settings from being learned during parsing, for example when encountering a "#+TODO". #[allow(dead_code)] pub fn document<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: &'s str, ) -> Res<&'s str, Document<'s>> { let (remaining, doc) = document_org_source(context, input.into()).map_err(convert_error)?; Ok((Into::<&str>::into(remaining), doc)) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[allow(dead_code)] fn document_org_source<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, Document<'s>> { let mut final_settings = Vec::new(); let (_, document_settings) = scan_for_in_buffer_settings(input)?; let setup_files: Vec = document_settings .iter() .filter(|kw| kw.key.eq_ignore_ascii_case("setupfile")) .map(|kw| kw.value) .map(|setup_file| { context .get_global_settings() .file_access .read_file(setup_file) .map_err(|err| nom::Err::>>::Failure(err.into())) }) .collect::, _>>()?; for setup_file in setup_files.iter().map(String::as_str) { let (_, setup_file_settings) = scan_for_in_buffer_settings(setup_file.into()).map_err(|_err| { nom::Err::Error(CustomError::MyError(MyError( "TODO: make this take an owned string so I can dump err.to_string() into it." .into(), ))) })?; final_settings.extend(setup_file_settings); } final_settings.extend(document_settings); let new_settings = apply_in_buffer_settings(final_settings, context.get_global_settings()) .map_err(|_err| { nom::Err::Error(CustomError::MyError(MyError( "TODO: make this take an owned string so I can dump err.to_string() into it." .into(), ))) })?; let new_context = context.with_global_settings(&new_settings); let context = &new_context; // TODO: read the keywords into settings and apply them to the GlobalSettings. let (remaining, document) = _document(context, input).map(|(rem, out)| (Into::<&str>::into(rem), out))?; { // 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 mut new_global_settings = context.get_global_settings().clone(); new_global_settings.radio_targets = all_radio_targets; let parser_context = context.with_global_settings(&new_global_settings); let (remaining, document) = _document(&parser_context, input) .map(|(rem, out)| (Into::<&str>::into(rem), out))?; return Ok((remaining.into(), document)); } } Ok((remaining.into(), document)) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] fn _document<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, '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(0))(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<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, Section<'s>> { // TODO: The zeroth section is specialized so it probably needs its own parser let contexts = [ ContextElement::ConsumeTrailingWhitespace(true), ContextElement::Context("section"), ContextElement::ExitMatcherNode(ExitMatcherNode { class: ExitClass::Document, exit_matcher: §ion_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 without_consuming_whitespace_context = ContextElement::ConsumeTrailingWhitespace(false); let without_consuming_whitespace_context = parser_context.with_additional_node(&without_consuming_whitespace_context); 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<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, mut input: OrgSource<'s>, ) -> Res, Section<'s>> { // TODO: The zeroth section is specialized so it probably needs its own parser let contexts = [ ContextElement::ConsumeTrailingWhitespace(true), ContextElement::Context("section"), ContextElement::ExitMatcherNode(ExitMatcherNode { class: ExitClass::Document, exit_matcher: §ion_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, (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<'b, 'g, 'r, 's>( _context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, OrgSource<'s>> { recognize(detect_headline)(input) } 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, _ws, maybe_todo_keyword, 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, 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 source = get_consumed(input, remaining); Ok(( remaining, Heading { source: source.into(), stars: star_count, todo_keyword: maybe_todo_keyword .map(|(todo_keyword, _ws)| Into::<&str>::into(todo_keyword)), title, tags: heading_tags, children, }, )) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] 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, 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, (_sol, star_count, ws, maybe_todo_keyword, title, maybe_tags, _ws, _line_ending), ) = 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, ))), many1(parser_with_context!(standard_set_object)(&parser_context)), opt(tuple((space0, tags))), space0, alt((line_ending, eof)), ))(input)?; Ok(( remaining, ( star_count, ws, maybe_todo_keyword, 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, 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((tag("TODO"), tag("DONE")))(input) } else { for todo_keyword in global_settings .in_progress_todo_keywords .iter() .chain(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, ent)); } Err(_) => {} } } Err(nom::Err::Error(CustomError::MyError(MyError( "NoTodoKeyword".into(), )))) } } impl<'s> Document<'s> { pub fn iter_tokens<'r>(&'r self) -> impl Iterator> { AllTokensIterator::new(Token::Document(self)) } }