use std::path::Path; use nom::combinator::all_consuming; use nom::combinator::opt; use nom::multi::many0; use super::headline::heading; use super::in_buffer_settings::apply_in_buffer_settings; use super::in_buffer_settings::apply_post_parse_in_buffer_settings; use super::in_buffer_settings::scan_for_in_buffer_settings; use super::org_source::OrgSource; use super::section::zeroth_section; use super::util::get_consumed; use crate::context::parser_with_context; use crate::context::Context; use crate::context::ContextElement; 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::types::AstNode; use crate::parser::org_source::convert_error; use crate::parser::util::blank_line; use crate::types::Document; use crate::types::Object; /// Parse a full org-mode document. /// /// This is a main entry point for Organic. It will parse the full contents of the input string as an org-mode document without an underlying file attached. #[allow(dead_code)] pub fn parse<'s>(input: &'s str) -> Result, Box> { parse_file_with_settings::<&Path>(input, &GlobalSettings::default(), None) } /// Parse a full org-mode document. /// /// This is a main entry point for Organic. It will parse the full contents of the input string as an org-mode document at the file_path. /// /// file_path is not used for reading the file contents. It is only used for determining the document category and filling in the path attribute on the Document. #[allow(dead_code)] pub fn parse_file<'s, P: AsRef>( input: &'s str, file_path: Option

, ) -> Result, Box> { parse_file_with_settings(input, &GlobalSettings::default(), file_path) } /// Parse a full org-mode document with starting settings. /// /// This is a 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 without an underlying file attached. /// /// 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, Box> { parse_file_with_settings::<&Path>(input, global_settings, None) } /// 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 at the file_path starting with the settings you supplied. /// /// This will not prevent additional settings from being learned during parsing, for example when encountering a "#+TODO". /// /// file_path is not used for reading the file contents. It is only used for determining the document category and filling in the path attribute on the Document. #[allow(dead_code)] pub fn parse_file_with_settings<'g, 's, P: AsRef>( input: &'s str, global_settings: &'g GlobalSettings<'g, 's>, file_path: Option

, ) -> Result, Box> { let initial_context = ContextElement::document_context(); let initial_context = Context::new(global_settings, List::new(&initial_context)); let wrapped_input = OrgSource::new(input); let mut doc = all_consuming(parser_with_context!(document_org_source)(&initial_context))(wrapped_input) .map_err(|err| err.to_string()) .map(|(_remaining, parsed_document)| parsed_document)?; if let Some(file_path) = file_path { let full_path = file_path.as_ref().canonicalize()?; if doc.category.is_none() { let category = full_path .file_stem() .expect("File should have a name.") .to_str() .expect("File name should be valid utf-8."); doc.category = Some(category.to_owned()); } doc.path = Some(full_path); } Ok(doc) } /// 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)] 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; let (remaining, mut 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>> = Into::::into(&document) .into_iter() .filter_map(|ast_node| { if let AstNode::RadioTarget(ast_node) = ast_node { Some(ast_node) } else { 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, mut document) = _document(&parser_context, input) .map(|(rem, out)| (Into::<&str>::into(rem), out))?; apply_post_parse_in_buffer_settings(&mut document) .map_err(|err| nom::Err::>>::Failure(err.into()))?; return Ok((remaining.into(), document)); } } // Find final in-buffer settings that do not impact parsing apply_post_parse_in_buffer_settings(&mut document) .map_err(|err| nom::Err::>>::Failure(err.into()))?; 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(), category: None, path: None, zeroth_section, children, }, )) }