From 7f6f22717b25828d087dd65960331f9b36b12466 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 4 Sep 2023 12:31:43 -0400 Subject: [PATCH 01/20] Add comment. --- src/context/global_settings.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/context/global_settings.rs b/src/context/global_settings.rs index ad3640d..6299672 100644 --- a/src/context/global_settings.rs +++ b/src/context/global_settings.rs @@ -1,5 +1,7 @@ use crate::types::Object; +// TODO: Ultimately, I think we'll need most of this: https://orgmode.org/manual/In_002dbuffer-Settings.html + #[derive(Debug, Clone)] pub struct GlobalSettings<'g, 's> { pub radio_targets: Vec<&'g Vec>>, From a8f277efe5834df7143f6aad1d389473637fa455 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 4 Sep 2023 12:48:59 -0400 Subject: [PATCH 02/20] Scan for setupfile at the beginning of a parse. --- src/parser/document.rs | 6 ++++++ src/parser/mod.rs | 1 + src/parser/setup_file.rs | 31 +++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 src/parser/setup_file.rs diff --git a/src/parser/document.rs b/src/parser/document.rs index 3e9cbe4..4e7084d 100644 --- a/src/parser/document.rs +++ b/src/parser/document.rs @@ -19,6 +19,7 @@ use nom::multi::separated_list1; use nom::sequence::tuple; use super::org_source::OrgSource; +use super::setup_file::scan_for_setup_file; use super::token::AllTokensIterator; use super::token::Token; use super::util::exit_matcher_parser; @@ -78,6 +79,11 @@ fn document_org_source<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, Document<'s>> { + let setup_file = scan_for_setup_file(input); + if setup_file.is_ok() { + let (_remaining, setup_file) = setup_file.expect("If-statement proves this is okay."); + println!("TODO: Process setup_file: {}", setup_file); + } let (remaining, document) = _document(context, input).map(|(rem, out)| (Into::<&str>::into(rem), out))?; { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index eae8a04..124fdb6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -33,6 +33,7 @@ mod planning; mod property_drawer; mod radio_link; mod regular_link; +mod setup_file; pub mod sexp; mod statistics_cookie; mod subscript_and_superscript; diff --git a/src/parser/setup_file.rs b/src/parser/setup_file.rs new file mode 100644 index 0000000..e422a38 --- /dev/null +++ b/src/parser/setup_file.rs @@ -0,0 +1,31 @@ +use nom::branch::alt; +use nom::bytes::complete::is_not; +use nom::bytes::complete::tag_no_case; +use nom::character::complete::anychar; +use nom::character::complete::line_ending; +use nom::character::complete::space1; +use nom::combinator::eof; +use nom::multi::many_till; +use nom::sequence::tuple; + +use super::OrgSource; +use crate::error::Res; +use crate::parser::util::start_of_line; + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +pub fn scan_for_setup_file<'s>(input: OrgSource<'s>) -> Res, OrgSource<'s>> { + let (remaining, setup) = many_till(anychar, setup_file)(input) + .map(|(remaining, (_, setup_file))| (remaining, setup_file))?; + Ok((remaining, setup)) +} + +fn setup_file<'s>(input: OrgSource<'s>) -> Res, OrgSource<'s>> { + let (remaining, (_, _, _, setup_file, _)) = tuple(( + start_of_line, + tag_no_case("#+SETUPFILE:"), + space1, + is_not("\r\n"), + alt((line_ending, eof)), + ))(input)?; + Ok((remaining, setup_file)) +} From da1ce2717d481747cf2e65c25d325981a272eaad Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 4 Sep 2023 13:00:41 -0400 Subject: [PATCH 03/20] Introduce a file access interface for reading additional files. --- src/context/file_access_interface.rs | 13 +++++++++++++ src/context/global_settings.rs | 6 +++++- src/context/mod.rs | 3 +++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 src/context/file_access_interface.rs diff --git a/src/context/file_access_interface.rs b/src/context/file_access_interface.rs new file mode 100644 index 0000000..10d1536 --- /dev/null +++ b/src/context/file_access_interface.rs @@ -0,0 +1,13 @@ +use std::path::Path; + +pub trait FileAccessInterface { + fn read_file>(&self, path: P) -> Result>; +} + +pub struct LocalFileAccessInterface; + +impl FileAccessInterface for LocalFileAccessInterface { + fn read_file>(&self, path: P) -> Result> { + Ok(std::fs::read_to_string(path)?) + } +} diff --git a/src/context/global_settings.rs b/src/context/global_settings.rs index 6299672..cc7e644 100644 --- a/src/context/global_settings.rs +++ b/src/context/global_settings.rs @@ -1,16 +1,20 @@ +use super::FileAccessInterface; +use super::LocalFileAccessInterface; use crate::types::Object; // TODO: Ultimately, I think we'll need most of this: https://orgmode.org/manual/In_002dbuffer-Settings.html #[derive(Debug, Clone)] -pub struct GlobalSettings<'g, 's> { +pub struct GlobalSettings<'g, 's, F: FileAccessInterface> { pub radio_targets: Vec<&'g Vec>>, + pub file_access: F, } impl<'g, 's> GlobalSettings<'g, 's> { pub fn new() -> Self { GlobalSettings { radio_targets: Vec::new(), + file_access: LocalFileAccessInterface, } } } diff --git a/src/context/mod.rs b/src/context/mod.rs index 32e04f0..fbf4618 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -2,6 +2,7 @@ use crate::error::Res; use crate::parser::OrgSource; mod exiting; +mod file_access_interface; mod global_settings; mod list; mod parser_context; @@ -18,6 +19,8 @@ pub trait Matcher = for<'s> Fn(OrgSource<'s>) -> Res, OrgSource<'s pub type DynMatcher<'c> = dyn Matcher + 'c; pub use exiting::ExitClass; +pub use file_access_interface::FileAccessInterface; +pub use file_access_interface::LocalFileAccessInterface; pub use global_settings::GlobalSettings; pub use list::List; pub use parser_context::Context; From 08eb59acd36c85879f1797dec53b14a8ff3157fd Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 4 Sep 2023 13:26:11 -0400 Subject: [PATCH 04/20] Rename parser_context to context. --- src/context/{parser_context.rs => context.rs} | 0 src/context/global_settings.rs | 11 ++++++----- src/context/mod.rs | 8 ++++---- 3 files changed, 10 insertions(+), 9 deletions(-) rename src/context/{parser_context.rs => context.rs} (100%) diff --git a/src/context/parser_context.rs b/src/context/context.rs similarity index 100% rename from src/context/parser_context.rs rename to src/context/context.rs diff --git a/src/context/global_settings.rs b/src/context/global_settings.rs index cc7e644..4b68272 100644 --- a/src/context/global_settings.rs +++ b/src/context/global_settings.rs @@ -1,20 +1,21 @@ -use super::FileAccessInterface; -use super::LocalFileAccessInterface; +// use super::FileAccessInterface; +// use super::LocalFileAccessInterface; use crate::types::Object; // TODO: Ultimately, I think we'll need most of this: https://orgmode.org/manual/In_002dbuffer-Settings.html #[derive(Debug, Clone)] -pub struct GlobalSettings<'g, 's, F: FileAccessInterface> { +// , F: FileAccessInterface +pub struct GlobalSettings<'g, 's> { pub radio_targets: Vec<&'g Vec>>, - pub file_access: F, + // pub file_access: F, } impl<'g, 's> GlobalSettings<'g, 's> { pub fn new() -> Self { GlobalSettings { radio_targets: Vec::new(), - file_access: LocalFileAccessInterface, + // file_access: LocalFileAccessInterface, } } } diff --git a/src/context/mod.rs b/src/context/mod.rs index fbf4618..42f63fd 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -1,11 +1,11 @@ use crate::error::Res; use crate::parser::OrgSource; +mod context; mod exiting; mod file_access_interface; mod global_settings; mod list; -mod parser_context; mod parser_with_context; pub type RefContext<'b, 'g, 'r, 's> = &'b Context<'g, 'r, 's>; @@ -18,12 +18,12 @@ pub trait Matcher = for<'s> Fn(OrgSource<'s>) -> Res, OrgSource<'s #[allow(dead_code)] pub type DynMatcher<'c> = dyn Matcher + 'c; +pub use context::Context; +pub use context::ContextElement; +pub use context::ExitMatcherNode; pub use exiting::ExitClass; pub use file_access_interface::FileAccessInterface; pub use file_access_interface::LocalFileAccessInterface; pub use global_settings::GlobalSettings; pub use list::List; -pub use parser_context::Context; -pub use parser_context::ContextElement; -pub use parser_context::ExitMatcherNode; pub(crate) use parser_with_context::parser_with_context; From a7330e38e4edcbadcb25cc2e9a11ca4385bcdd08 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 4 Sep 2023 16:29:41 -0400 Subject: [PATCH 05/20] Enable dynamic access to the file access interface. --- src/context/file_access_interface.rs | 8 +++++--- src/context/global_settings.rs | 13 ++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/context/file_access_interface.rs b/src/context/file_access_interface.rs index 10d1536..ff8d9e3 100644 --- a/src/context/file_access_interface.rs +++ b/src/context/file_access_interface.rs @@ -1,13 +1,15 @@ +use std::fmt::Debug; use std::path::Path; -pub trait FileAccessInterface { - fn read_file>(&self, path: P) -> Result>; +pub trait FileAccessInterface: Debug { + fn read_file(&self, path: &dyn AsRef) -> Result>; } +#[derive(Debug, Clone)] pub struct LocalFileAccessInterface; impl FileAccessInterface for LocalFileAccessInterface { - fn read_file>(&self, path: P) -> Result> { + fn read_file(&self, path: &dyn AsRef) -> Result> { Ok(std::fs::read_to_string(path)?) } } diff --git a/src/context/global_settings.rs b/src/context/global_settings.rs index 4b68272..4601b24 100644 --- a/src/context/global_settings.rs +++ b/src/context/global_settings.rs @@ -1,27 +1,26 @@ -// use super::FileAccessInterface; -// use super::LocalFileAccessInterface; +use super::FileAccessInterface; +use super::LocalFileAccessInterface; use crate::types::Object; // TODO: Ultimately, I think we'll need most of this: https://orgmode.org/manual/In_002dbuffer-Settings.html #[derive(Debug, Clone)] -// , F: FileAccessInterface pub struct GlobalSettings<'g, 's> { pub radio_targets: Vec<&'g Vec>>, - // pub file_access: F, + pub file_access: &'g dyn FileAccessInterface, } impl<'g, 's> GlobalSettings<'g, 's> { - pub fn new() -> Self { + pub fn new() -> GlobalSettings<'g, 's> { GlobalSettings { radio_targets: Vec::new(), - // file_access: LocalFileAccessInterface, + file_access: &LocalFileAccessInterface, } } } impl<'g, 's> Default for GlobalSettings<'g, 's> { - fn default() -> Self { + fn default() -> GlobalSettings<'g, 's> { GlobalSettings::new() } } From ee02e0771791cdc4fcd245542529483b283e4206 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 4 Sep 2023 16:53:02 -0400 Subject: [PATCH 06/20] Read the setup file into memory. --- src/context/file_access_interface.rs | 5 ++--- src/error/error.rs | 11 +++++++++-- src/parser/document.rs | 7 +++++++ src/parser/org_source.rs | 1 + 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/context/file_access_interface.rs b/src/context/file_access_interface.rs index ff8d9e3..a31c020 100644 --- a/src/context/file_access_interface.rs +++ b/src/context/file_access_interface.rs @@ -1,15 +1,14 @@ use std::fmt::Debug; -use std::path::Path; pub trait FileAccessInterface: Debug { - fn read_file(&self, path: &dyn AsRef) -> Result>; + fn read_file(&self, path: &str) -> Result; } #[derive(Debug, Clone)] pub struct LocalFileAccessInterface; impl FileAccessInterface for LocalFileAccessInterface { - fn read_file(&self, path: &dyn AsRef) -> Result> { + fn read_file(&self, path: &str) -> Result { Ok(std::fs::read_to_string(path)?) } } diff --git a/src/error/error.rs b/src/error/error.rs index ef61469..5a620f0 100644 --- a/src/error/error.rs +++ b/src/error/error.rs @@ -5,13 +5,14 @@ use nom::IResult; pub type Res = IResult>; // TODO: MyError probably shouldn't be based on the same type as the input type since it's used exclusively with static strings right now. -#[derive(Debug, PartialEq)] +#[derive(Debug)] pub enum CustomError { MyError(MyError), Nom(I, ErrorKind), + IO(std::io::Error), } -#[derive(Debug, PartialEq)] +#[derive(Debug)] pub struct MyError(pub I); impl ParseError for CustomError { @@ -24,3 +25,9 @@ impl ParseError for CustomError { other } } + +impl From for CustomError { + fn from(value: std::io::Error) -> Self { + CustomError::IO(value) + } +} diff --git a/src/parser/document.rs b/src/parser/document.rs index 4e7084d..6e03cc9 100644 --- a/src/parser/document.rs +++ b/src/parser/document.rs @@ -33,6 +33,7 @@ use crate::context::ExitMatcherNode; use crate::context::GlobalSettings; use crate::context::List; use crate::context::RefContext; +use crate::error::CustomError; use crate::error::Res; use crate::parser::comment::comment; use crate::parser::element_parser::element; @@ -82,6 +83,12 @@ fn document_org_source<'b, 'g, 'r, 's>( let setup_file = scan_for_setup_file(input); if setup_file.is_ok() { let (_remaining, setup_file) = setup_file.expect("If-statement proves this is okay."); + let setup_file_contents = context + .get_global_settings() + .file_access + .read_file(Into::<&str>::into(setup_file)) + .map_err(|err| nom::Err::>>::Failure(err.into()))?; + println!("TODO: Process setup_file: {}", setup_file); } let (remaining, document) = diff --git a/src/parser/org_source.rs b/src/parser/org_source.rs index dc2bf0c..87f93da 100644 --- a/src/parser/org_source.rs +++ b/src/parser/org_source.rs @@ -318,6 +318,7 @@ impl<'s> From>> for CustomError<&'s str> { match value { CustomError::MyError(err) => CustomError::MyError(err.into()), CustomError::Nom(input, error_kind) => CustomError::Nom(input.into(), error_kind), + CustomError::IO(err) => CustomError::IO(err), } } } From c7d5c89a60e93da3f189170283ff249e426ab822 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 4 Sep 2023 17:16:08 -0400 Subject: [PATCH 07/20] Passing the setupfile contents to the document parser. --- src/parser/document.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/parser/document.rs b/src/parser/document.rs index 6e03cc9..aaadfbb 100644 --- a/src/parser/document.rs +++ b/src/parser/document.rs @@ -34,6 +34,7 @@ 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; @@ -88,6 +89,11 @@ fn document_org_source<'b, 'g, 'r, 's>( .file_access .read_file(Into::<&str>::into(setup_file)) .map_err(|err| nom::Err::>>::Failure(err.into()))?; + let parsed_setup_file = _document(context, setup_file_contents.as_str().into()); + if parsed_setup_file.is_err() { + return Err(nom::Err::Error(CustomError::MyError(MyError("Failed to parse the setup file.".into())))); + } + let (_remaining, parsed_setup_file) = parsed_setup_file.expect("The if-statement proves this is ok."); println!("TODO: Process setup_file: {}", setup_file); } From 27cf6c046295eed4c65995f4fad402621943f2af Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 4 Sep 2023 17:29:50 -0400 Subject: [PATCH 08/20] Remove unnecessary map_err from main.rs. --- src/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1d2320a..14ce151 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,7 +68,7 @@ fn run_anonymous_parse>(org_contents: P) -> Result<(), Box>(org_contents: P) -> Result<(), Box>(org_path: P) -> Result<(), Box>(org_path: P) -> Result<(), Box Date: Mon, 4 Sep 2023 17:38:02 -0400 Subject: [PATCH 09/20] Add a parse_with_settings function. --- src/parser/document.rs | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/parser/document.rs b/src/parser/document.rs index aaadfbb..5f0d1c0 100644 --- a/src/parser/document.rs +++ b/src/parser/document.rs @@ -51,6 +51,9 @@ 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. #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[allow(dead_code)] pub fn parse<'s>(input: &'s str) -> Result, String> { @@ -65,6 +68,30 @@ pub fn parse<'s>(input: &'s str) -> Result, String> { ret } +/// 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". +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +#[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. #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[allow(dead_code)] pub fn document<'b, 'g, 'r, 's>( @@ -91,11 +118,14 @@ fn document_org_source<'b, 'g, 'r, 's>( .map_err(|err| nom::Err::>>::Failure(err.into()))?; let parsed_setup_file = _document(context, setup_file_contents.as_str().into()); if parsed_setup_file.is_err() { - return Err(nom::Err::Error(CustomError::MyError(MyError("Failed to parse the setup file.".into())))); + return Err(nom::Err::Error(CustomError::MyError(MyError( + "Failed to parse the setup file.".into(), + )))); } - let (_remaining, parsed_setup_file) = parsed_setup_file.expect("The if-statement proves this is ok."); + let (_remaining, parsed_setup_file) = + parsed_setup_file.expect("The if-statement proves this is ok."); - println!("TODO: Process setup_file: {}", setup_file); + println!("TODO: Process setup_file: {:#?}", parsed_setup_file); } let (remaining, document) = _document(context, input).map(|(rem, out)| (Into::<&str>::into(rem), out))?; From 275b4b53d1b9499015f21a069d5d4244f57333f2 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 4 Sep 2023 19:17:23 -0400 Subject: [PATCH 10/20] Use a single function for finding all keywords. --- src/compare/diff.rs | 2 ++ src/parser/document.rs | 43 +++++++++++++++++------ src/parser/in_buffer_settings.rs | 28 +++++++++++++++ src/parser/keyword.rs | 60 +++++++++++++++++++------------- src/parser/mod.rs | 1 + src/parser/setup_file.rs | 29 ++++++--------- src/types/lesser_element.rs | 2 ++ 7 files changed, 111 insertions(+), 54 deletions(-) create mode 100644 src/parser/in_buffer_settings.rs diff --git a/src/compare/diff.rs b/src/compare/diff.rs index 4402200..704335d 100644 --- a/src/compare/diff.rs +++ b/src/compare/diff.rs @@ -1392,6 +1392,8 @@ fn compare_keyword<'s>( Ok(_) => {} }; + // TODO: Compare key and value + Ok(DiffResult { status: this_status, name: emacs_name.to_owned(), diff --git a/src/parser/document.rs b/src/parser/document.rs index 5f0d1c0..9fd8021 100644 --- a/src/parser/document.rs +++ b/src/parser/document.rs @@ -18,6 +18,7 @@ use nom::multi::many_till; use nom::multi::separated_list1; use nom::sequence::tuple; +use super::in_buffer_settings::scan_for_in_buffer_settings; use super::org_source::OrgSource; use super::setup_file::scan_for_setup_file; use super::token::AllTokensIterator; @@ -92,6 +93,8 @@ pub fn parse_with_settings<'g, 's>( /// 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". #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[allow(dead_code)] pub fn document<'b, 'g, 'r, 's>( @@ -109,24 +112,42 @@ fn document_org_source<'b, 'g, 'r, 's>( input: OrgSource<'s>, ) -> Res, Document<'s>> { let setup_file = scan_for_setup_file(input); - if setup_file.is_ok() { + let setup_file = if setup_file.is_ok() { let (_remaining, setup_file) = setup_file.expect("If-statement proves this is okay."); let setup_file_contents = context .get_global_settings() .file_access .read_file(Into::<&str>::into(setup_file)) .map_err(|err| nom::Err::>>::Failure(err.into()))?; - let parsed_setup_file = _document(context, setup_file_contents.as_str().into()); - if parsed_setup_file.is_err() { - return Err(nom::Err::Error(CustomError::MyError(MyError( - "Failed to parse the setup file.".into(), - )))); - } - let (_remaining, parsed_setup_file) = - parsed_setup_file.expect("The if-statement proves this is ok."); - - println!("TODO: Process setup_file: {:#?}", parsed_setup_file); + Some(setup_file_contents) + } else { + None + }; + let setup_file_settings = setup_file + .as_ref() + .map(|input| input.as_str().into()) + .map(scan_for_in_buffer_settings) + .map_or(Ok(None), |r| r.map(Some)) + .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 (_, document_settings) = scan_for_in_buffer_settings(input)?; + let mut final_settings = Vec::with_capacity( + document_settings.len() + + match setup_file_settings { + Some((_, ref setup_file_settings)) => setup_file_settings.len(), + None => 0, + }, + ); + if let Some((_, setup_file_settings)) = setup_file_settings { + final_settings.extend(setup_file_settings.into_iter()); } + + // 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))?; { diff --git a/src/parser/in_buffer_settings.rs b/src/parser/in_buffer_settings.rs new file mode 100644 index 0000000..e653e2f --- /dev/null +++ b/src/parser/in_buffer_settings.rs @@ -0,0 +1,28 @@ +use nom::branch::alt; +use nom::bytes::complete::tag_no_case; +use nom::character::complete::anychar; +use nom::combinator::map; +use nom::multi::many0; +use nom::multi::many_till; + +use super::keyword::filtered_keyword; +use super::OrgSource; +use crate::error::Res; +use crate::types::Keyword; + +pub fn scan_for_in_buffer_settings<'s>( + input: OrgSource<'s>, +) -> Res, Vec>> { + // TODO: Optimization idea: since this is slicing the OrgSource at each character, it might be more efficient to do a parser that uses a search function like take_until, and wrap it in a function similar to consumed but returning the input along with the normal output, then pass all of that into a verify that confirms we were at the start of a line using the input we just returned. + + let keywords = many0(map( + many_till(anychar, filtered_keyword(in_buffer_settings_key)), + |(_, kw)| kw, + ))(input); + keywords +} + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +fn in_buffer_settings_key<'s>(input: OrgSource<'s>) -> Res, OrgSource<'s>> { + alt((tag_no_case("todo"), tag_no_case("setupfile")))(input) +} diff --git a/src/parser/keyword.rs b/src/parser/keyword.rs index ee7a22d..0d1a11d 100644 --- a/src/parser/keyword.rs +++ b/src/parser/keyword.rs @@ -7,6 +7,7 @@ use nom::character::complete::anychar; use nom::character::complete::line_ending; use nom::character::complete::space0; use nom::character::complete::space1; +use nom::combinator::consumed; use nom::combinator::eof; use nom::combinator::not; use nom::combinator::peek; @@ -16,6 +17,7 @@ use nom::sequence::tuple; use super::org_source::BracketDepth; use super::org_source::OrgSource; +use crate::context::Matcher; use crate::context::RefContext; use crate::error::CustomError; use crate::error::MyError; @@ -29,19 +31,26 @@ const ORG_ELEMENT_AFFILIATED_KEYWORDS: [&'static str; 13] = [ ]; const ORG_ELEMENT_DUAL_KEYWORDS: [&'static str; 2] = ["caption", "results"]; -#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -pub fn keyword<'b, 'g, 'r, 's>( - _context: RefContext<'b, 'g, 'r, 's>, +pub fn filtered_keyword( + key_parser: F, +) -> impl for<'s> Fn(OrgSource<'s>) -> Res, Keyword<'s>> { + move |input| _filtered_keyword(&key_parser, input) +} + +#[cfg_attr( + feature = "tracing", + tracing::instrument(ret, level = "debug", skip(key_parser)) +)] +fn _filtered_keyword<'s, F: Matcher>( + key_parser: F, input: OrgSource<'s>, ) -> Res, Keyword<'s>> { start_of_line(input)?; // TODO: When key is a member of org-element-parsed-keywords, value can contain the standard set objects, excluding footnote references. - let (remaining, rule) = recognize(tuple(( + let (remaining, (consumed_input, (_, _, parsed_key, _, parsed_value, _))) = consumed(tuple(( space0, tag("#+"), - not(peek(tag_no_case("call"))), - not(peek(tag_no_case("begin"))), - is_not(" \t\r\n:"), + key_parser, tag(":"), alt((recognize(tuple((space1, is_not("\r\n")))), space0)), alt((line_ending, eof)), @@ -49,33 +58,36 @@ pub fn keyword<'b, 'g, 'r, 's>( Ok(( remaining, Keyword { - source: rule.into(), + source: consumed_input.into(), + key: parsed_key.into(), + value: parsed_value.into(), }, )) } +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +pub fn keyword<'b, 'g, 'r, 's>( + _context: RefContext<'b, 'g, 'r, 's>, + input: OrgSource<'s>, +) -> Res, Keyword<'s>> { + filtered_keyword(regular_keyword_key)(input) +} + #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] pub fn affiliated_keyword<'b, 'g, 'r, 's>( _context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, Keyword<'s>> { - start_of_line(input)?; + filtered_keyword(affiliated_key)(input) +} - // TODO: When key is a member of org-element-parsed-keywords, value can contain the standard set objects, excluding footnote references. - let (remaining, rule) = recognize(tuple(( - space0, - tag("#+"), - affiliated_key, - tag(":"), - alt((recognize(tuple((space1, is_not("\r\n")))), space0)), - alt((line_ending, eof)), - )))(input)?; - Ok(( - remaining, - Keyword { - source: rule.into(), - }, - )) +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +fn regular_keyword_key<'s>(input: OrgSource<'s>) -> Res, OrgSource<'s>> { + recognize(tuple(( + not(peek(tag_no_case("call"))), + not(peek(tag_no_case("begin"))), + is_not(" \t\r\n:"), + )))(input) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 124fdb6..ebda885 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -15,6 +15,7 @@ mod footnote_definition; mod footnote_reference; mod greater_block; mod horizontal_rule; +mod in_buffer_settings; mod inline_babel_call; mod inline_source_block; mod keyword; diff --git a/src/parser/setup_file.rs b/src/parser/setup_file.rs index e422a38..509ab17 100644 --- a/src/parser/setup_file.rs +++ b/src/parser/setup_file.rs @@ -1,31 +1,22 @@ -use nom::branch::alt; -use nom::bytes::complete::is_not; use nom::bytes::complete::tag_no_case; use nom::character::complete::anychar; -use nom::character::complete::line_ending; -use nom::character::complete::space1; -use nom::combinator::eof; +use nom::combinator::map; use nom::multi::many_till; -use nom::sequence::tuple; +use super::keyword::filtered_keyword; use super::OrgSource; use crate::error::Res; -use crate::parser::util::start_of_line; #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -pub fn scan_for_setup_file<'s>(input: OrgSource<'s>) -> Res, OrgSource<'s>> { - let (remaining, setup) = many_till(anychar, setup_file)(input) - .map(|(remaining, (_, setup_file))| (remaining, setup_file))?; +pub fn scan_for_setup_file<'s>(input: OrgSource<'s>) -> Res, &'s str> { + let (remaining, setup) = map( + many_till(anychar, filtered_keyword(setupfile_key)), + |(_, kw)| kw.value, + )(input)?; Ok((remaining, setup)) } -fn setup_file<'s>(input: OrgSource<'s>) -> Res, OrgSource<'s>> { - let (remaining, (_, _, _, setup_file, _)) = tuple(( - start_of_line, - tag_no_case("#+SETUPFILE:"), - space1, - is_not("\r\n"), - alt((line_ending, eof)), - ))(input)?; - Ok((remaining, setup_file)) +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +fn setupfile_key<'s>(input: OrgSource<'s>) -> Res, OrgSource<'s>> { + tag_no_case("setupfile")(input) } diff --git a/src/types/lesser_element.rs b/src/types/lesser_element.rs index 33f6529..06ae33b 100644 --- a/src/types/lesser_element.rs +++ b/src/types/lesser_element.rs @@ -87,6 +87,8 @@ pub struct HorizontalRule<'s> { #[derive(Debug)] pub struct Keyword<'s> { pub source: &'s str, + pub key: &'s str, + pub value: &'s str, } #[derive(Debug)] From d3c733c5adf1bdc92f9348ec36e308522a91da59 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 4 Sep 2023 21:46:40 -0400 Subject: [PATCH 11/20] Take into account the source directory when parsing org-mode in Organic. Previously only the emacs code was doing this. --- src/context/file_access_interface.rs | 14 ++++++++++++-- src/context/global_settings.rs | 4 +++- src/lib.rs | 3 +++ src/main.rs | 28 ++++++++++++++++++++++++---- src/parser/keyword.rs | 26 ++++++++++++++++++-------- src/parser/mod.rs | 1 + 6 files changed, 61 insertions(+), 15 deletions(-) diff --git a/src/context/file_access_interface.rs b/src/context/file_access_interface.rs index a31c020..87d7b27 100644 --- a/src/context/file_access_interface.rs +++ b/src/context/file_access_interface.rs @@ -1,14 +1,24 @@ use std::fmt::Debug; +use std::path::PathBuf; pub trait FileAccessInterface: Debug { fn read_file(&self, path: &str) -> Result; } #[derive(Debug, Clone)] -pub struct LocalFileAccessInterface; +pub struct LocalFileAccessInterface { + pub working_directory: Option, +} impl FileAccessInterface for LocalFileAccessInterface { fn read_file(&self, path: &str) -> Result { - Ok(std::fs::read_to_string(path)?) + let final_path = self + .working_directory + .as_ref() + .map(PathBuf::as_path) + .map(|pb| pb.join(path)) + .unwrap_or_else(|| PathBuf::from(path)); + eprintln!("Reading file: {}", final_path.display()); + Ok(std::fs::read_to_string(final_path)?) } } diff --git a/src/context/global_settings.rs b/src/context/global_settings.rs index 4601b24..f1b4d1b 100644 --- a/src/context/global_settings.rs +++ b/src/context/global_settings.rs @@ -14,7 +14,9 @@ impl<'g, 's> GlobalSettings<'g, 's> { pub fn new() -> GlobalSettings<'g, 's> { GlobalSettings { radio_targets: Vec::new(), - file_access: &LocalFileAccessInterface, + file_access: &LocalFileAccessInterface { + working_directory: None, + }, } } } diff --git a/src/lib.rs b/src/lib.rs index c481b7d..d8790d6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,3 +19,6 @@ mod context; mod error; pub mod parser; pub mod types; + +pub use context::GlobalSettings; +pub use context::LocalFileAccessInterface; diff --git a/src/main.rs b/src/main.rs index 14ce151..ece63fe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -103,10 +103,18 @@ fn run_parse_on_file>(org_path: P) -> Result<(), Box>(org_path: P) -> Result<(), Box>(org_path: P) -> Result<(), Box> { + use organic::parser::parse_with_settings; + use organic::GlobalSettings; + use organic::LocalFileAccessInterface; + let org_path = org_path.as_ref(); eprintln!( "This program was built with compare disabled. Only parsing with organic, not comparing." ); - // TODO: This should take into account the original file path when parsing + let parent_directory = org_path + .parent() + .ok_or("Should be contained inside a directory.")?; let org_contents = std::fs::read_to_string(org_path)?; let org_contents = org_contents.as_str(); - let rust_parsed = parse(org_contents)?; + let global_settings = GlobalSettings { + radio_targets: Vec::new(), + file_access: &LocalFileAccessInterface { + working_directory: Some(parent_directory.to_path_buf()), + }, + }; + let rust_parsed = parse_with_settings(org_contents, &global_settings)?; println!("{:#?}", rust_parsed); Ok(()) } diff --git a/src/parser/keyword.rs b/src/parser/keyword.rs index 0d1a11d..886ec70 100644 --- a/src/parser/keyword.rs +++ b/src/parser/keyword.rs @@ -47,14 +47,24 @@ fn _filtered_keyword<'s, F: Matcher>( ) -> Res, Keyword<'s>> { start_of_line(input)?; // TODO: When key is a member of org-element-parsed-keywords, value can contain the standard set objects, excluding footnote references. - let (remaining, (consumed_input, (_, _, parsed_key, _, parsed_value, _))) = consumed(tuple(( - space0, - tag("#+"), - key_parser, - tag(":"), - alt((recognize(tuple((space1, is_not("\r\n")))), space0)), - alt((line_ending, eof)), - )))(input)?; + let (remaining, (consumed_input, (_, _, parsed_key, _))) = + consumed(tuple((space0, tag("#+"), key_parser, tag(":"))))(input)?; + match tuple((space0, alt((line_ending, eof))))(remaining) { + Ok((remaining, _)) => { + return Ok(( + remaining, + Keyword { + source: consumed_input.into(), + key: parsed_key.into(), + value: "".into(), + }, + )); + } + err @ Err(_) => err?, + }; + let (remaining, _ws) = space1(remaining)?; + let (remaining, parsed_value) = is_not("\r\n")(remaining)?; + let (remaining, _ws) = tuple((space0, alt((line_ending, eof))))(remaining)?; Ok(( remaining, Keyword { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ebda885..8394ab0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -46,4 +46,5 @@ mod token; mod util; pub use document::document; pub use document::parse; +pub use document::parse_with_settings; pub use org_source::OrgSource; From 9060f9b26dd6462c6a7e8c66fa3d3d9c2fa0d313 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 4 Sep 2023 22:05:43 -0400 Subject: [PATCH 12/20] Only do a single pre-pass on the full document pulling out both setupfile and all other in-buffer settings. Previously we made a separate pass just to find setupfile and then we pulled the in-buffer settings from everything. --- src/parser/document.rs | 57 ++++++++++++++++------------------------ src/parser/mod.rs | 1 - src/parser/setup_file.rs | 22 ---------------- 3 files changed, 23 insertions(+), 57 deletions(-) delete mode 100644 src/parser/setup_file.rs diff --git a/src/parser/document.rs b/src/parser/document.rs index 9fd8021..033199c 100644 --- a/src/parser/document.rs +++ b/src/parser/document.rs @@ -20,7 +20,6 @@ use nom::sequence::tuple; use super::in_buffer_settings::scan_for_in_buffer_settings; use super::org_source::OrgSource; -use super::setup_file::scan_for_setup_file; use super::token::AllTokensIterator; use super::token::Token; use super::util::exit_matcher_parser; @@ -111,41 +110,31 @@ fn document_org_source<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, Document<'s>> { - let setup_file = scan_for_setup_file(input); - let setup_file = if setup_file.is_ok() { - let (_remaining, setup_file) = setup_file.expect("If-statement proves this is okay."); - let setup_file_contents = context - .get_global_settings() - .file_access - .read_file(Into::<&str>::into(setup_file)) - .map_err(|err| nom::Err::>>::Failure(err.into()))?; - Some(setup_file_contents) - } else { - None - }; - let setup_file_settings = setup_file - .as_ref() - .map(|input| input.as_str().into()) - .map(scan_for_in_buffer_settings) - .map_or(Ok(None), |r| r.map(Some)) - .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 mut final_settings = Vec::new(); let (_, document_settings) = scan_for_in_buffer_settings(input)?; - let mut final_settings = Vec::with_capacity( - document_settings.len() - + match setup_file_settings { - Some((_, ref setup_file_settings)) => setup_file_settings.len(), - None => 0, - }, - ); - if let Some((_, setup_file_settings)) = setup_file_settings { - final_settings.extend(setup_file_settings.into_iter()); + 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); // TODO: read the keywords into settings and apply them to the GlobalSettings. let (remaining, document) = diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 8394ab0..72a350c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -34,7 +34,6 @@ mod planning; mod property_drawer; mod radio_link; mod regular_link; -mod setup_file; pub mod sexp; mod statistics_cookie; mod subscript_and_superscript; diff --git a/src/parser/setup_file.rs b/src/parser/setup_file.rs deleted file mode 100644 index 509ab17..0000000 --- a/src/parser/setup_file.rs +++ /dev/null @@ -1,22 +0,0 @@ -use nom::bytes::complete::tag_no_case; -use nom::character::complete::anychar; -use nom::combinator::map; -use nom::multi::many_till; - -use super::keyword::filtered_keyword; -use super::OrgSource; -use crate::error::Res; - -#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -pub fn scan_for_setup_file<'s>(input: OrgSource<'s>) -> Res, &'s str> { - let (remaining, setup) = map( - many_till(anychar, filtered_keyword(setupfile_key)), - |(_, kw)| kw.value, - )(input)?; - Ok((remaining, setup)) -} - -#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -fn setupfile_key<'s>(input: OrgSource<'s>) -> Res, OrgSource<'s>> { - tag_no_case("setupfile")(input) -} From 1c142b68c673843d29f69c6e7727b5810a81d4a4 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 4 Sep 2023 22:11:56 -0400 Subject: [PATCH 13/20] Make the parse entry point call the parse_with_settings entry point. --- src/parser/document.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/parser/document.rs b/src/parser/document.rs index 033199c..a7c57a3 100644 --- a/src/parser/document.rs +++ b/src/parser/document.rs @@ -57,15 +57,7 @@ use crate::types::Section; #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[allow(dead_code)] pub fn parse<'s>(input: &'s str) -> Result, String> { - let global_settings = GlobalSettings::default(); - 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_with_settings(input, &GlobalSettings::default()) } /// Parse a full org-mode document with starting settings. From b0392ad6fb07955b14137e8263dc68605e4a5c8d Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 4 Sep 2023 22:15:43 -0400 Subject: [PATCH 14/20] Trim the trailing space off keywords with values. --- src/main.rs | 7 +++---- src/parser/document.rs | 3 --- src/parser/in_buffer_settings.rs | 1 + src/parser/keyword.rs | 13 ++++++++++--- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/main.rs b/src/main.rs index ece63fe..fd82386 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,8 +14,11 @@ use organic::emacs_parse_file_org_document; use organic::get_emacs_version; #[cfg(feature = "compare")] use organic::get_org_mode_version; +use organic::parser::parse_with_settings; #[cfg(feature = "compare")] use organic::parser::sexp::sexp_with_padding; +use organic::GlobalSettings; +use organic::LocalFileAccessInterface; #[cfg(feature = "tracing")] use crate::init_tracing::init_telemetry; @@ -136,10 +139,6 @@ fn run_parse_on_file>(org_path: P) -> Result<(), Box>(org_path: P) -> Result<(), Box> { - use organic::parser::parse_with_settings; - use organic::GlobalSettings; - use organic::LocalFileAccessInterface; - let org_path = org_path.as_ref(); eprintln!( "This program was built with compare disabled. Only parsing with organic, not comparing." diff --git a/src/parser/document.rs b/src/parser/document.rs index a7c57a3..2ad42a7 100644 --- a/src/parser/document.rs +++ b/src/parser/document.rs @@ -54,7 +54,6 @@ 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. -#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[allow(dead_code)] pub fn parse<'s>(input: &'s str) -> Result, String> { parse_with_settings(input, &GlobalSettings::default()) @@ -65,7 +64,6 @@ pub fn parse<'s>(input: &'s str) -> Result, String> { /// 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". -#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[allow(dead_code)] pub fn parse_with_settings<'g, 's>( input: &'s str, @@ -86,7 +84,6 @@ pub fn parse_with_settings<'g, 's>( /// 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". -#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[allow(dead_code)] pub fn document<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, diff --git a/src/parser/in_buffer_settings.rs b/src/parser/in_buffer_settings.rs index e653e2f..f21dc1a 100644 --- a/src/parser/in_buffer_settings.rs +++ b/src/parser/in_buffer_settings.rs @@ -10,6 +10,7 @@ use super::OrgSource; use crate::error::Res; use crate::types::Keyword; +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] pub fn scan_for_in_buffer_settings<'s>( input: OrgSource<'s>, ) -> Res, Vec>> { diff --git a/src/parser/keyword.rs b/src/parser/keyword.rs index 886ec70..3c853b7 100644 --- a/src/parser/keyword.rs +++ b/src/parser/keyword.rs @@ -49,7 +49,11 @@ fn _filtered_keyword<'s, F: Matcher>( // TODO: When key is a member of org-element-parsed-keywords, value can contain the standard set objects, excluding footnote references. let (remaining, (consumed_input, (_, _, parsed_key, _))) = consumed(tuple((space0, tag("#+"), key_parser, tag(":"))))(input)?; - match tuple((space0, alt((line_ending, eof))))(remaining) { + match tuple(( + space0::, CustomError>>, + alt((line_ending, eof)), + ))(remaining) + { Ok((remaining, _)) => { return Ok(( remaining, @@ -60,10 +64,13 @@ fn _filtered_keyword<'s, F: Matcher>( }, )); } - err @ Err(_) => err?, + Err(_) => {} }; let (remaining, _ws) = space1(remaining)?; - let (remaining, parsed_value) = is_not("\r\n")(remaining)?; + let (remaining, parsed_value) = recognize(many_till( + anychar, + peek(tuple((space0, alt((line_ending, eof))))), + ))(remaining)?; let (remaining, _ws) = tuple((space0, alt((line_ending, eof))))(remaining)?; Ok(( remaining, From bdba495f690869a8dbcaf953d48c70201b41d033 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 4 Sep 2023 22:39:24 -0400 Subject: [PATCH 15/20] Add a parser for the todo keyword's value. --- src/parser/in_buffer_settings.rs | 16 +++++- src/parser/keyword_todo.rs | 90 ++++++++++++++++++++++++++++++++ src/parser/mod.rs | 1 + 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 src/parser/keyword_todo.rs diff --git a/src/parser/in_buffer_settings.rs b/src/parser/in_buffer_settings.rs index f21dc1a..3c533ac 100644 --- a/src/parser/in_buffer_settings.rs +++ b/src/parser/in_buffer_settings.rs @@ -25,5 +25,19 @@ pub fn scan_for_in_buffer_settings<'s>( #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] fn in_buffer_settings_key<'s>(input: OrgSource<'s>) -> Res, OrgSource<'s>> { - alt((tag_no_case("todo"), tag_no_case("setupfile")))(input) + alt(( + tag_no_case("archive"), + tag_no_case("category"), + tag_no_case("columns"), + tag_no_case("filetags"), + tag_no_case("link"), + tag_no_case("priorities"), + tag_no_case("property"), + tag_no_case("seq_todo"), + tag_no_case("setupfile"), + tag_no_case("startup"), + tag_no_case("tags"), + tag_no_case("todo"), + tag_no_case("typ_todo"), + ))(input) } diff --git a/src/parser/keyword_todo.rs b/src/parser/keyword_todo.rs new file mode 100644 index 0000000..44facbb --- /dev/null +++ b/src/parser/keyword_todo.rs @@ -0,0 +1,90 @@ +use nom::branch::alt; +use nom::bytes::complete::tag; +use nom::bytes::complete::take_till; +use nom::character::complete::line_ending; +use nom::character::complete::space0; +use nom::character::complete::space1; +use nom::combinator::eof; +use nom::combinator::opt; +use nom::combinator::verify; +use nom::multi::separated_list0; +use nom::sequence::tuple; + +use crate::error::Res; + +/// Parses the text in the value of a #+TODO keyword. +/// +/// Example input: "foo bar baz | lorem ipsum" +pub fn keyword_todo<'s>(input: &'s str) -> Res<&'s str, (Vec<&'s str>, Vec<&'s str>)> { + let (remaining, mut before_pipe_words) = separated_list0(space1, keyword_word)(input)?; + let (remaining, after_pipe_words) = opt(tuple(( + tuple((space0, tag("|"), space0)), + separated_list0(space1, keyword_word), + )))(remaining)?; + let (remaining, _eol) = alt((line_ending, eof))(remaining)?; + if let Some((_pipe, after_pipe_words)) = after_pipe_words { + Ok((remaining, (before_pipe_words, after_pipe_words))) + } else if !before_pipe_words.is_empty() { + // If there was no pipe, then the last word becomes a completion state instead. + let mut after_pipe_words = Vec::with_capacity(1); + after_pipe_words.push( + before_pipe_words + .pop() + .expect("If-statement proves this is Some."), + ); + Ok((remaining, (before_pipe_words, after_pipe_words))) + } else { + // No words founds + Ok((remaining, (Vec::new(), Vec::new()))) + } +} + +fn keyword_word<'s>(input: &'s str) -> Res<&'s str, &'s str> { + verify(take_till(|c| " \t\r\n|".contains(c)), |result: &str| { + !result.is_empty() + })(input) +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn before_and_after() -> Result<(), Box> { + let input = "foo bar baz | lorem ipsum"; + let (remaining, (before_pipe_words, after_pipe_words)) = keyword_todo(input)?; + assert_eq!(remaining, ""); + assert_eq!(before_pipe_words, vec!["foo", "bar", "baz"]); + assert_eq!(after_pipe_words, vec!["lorem", "ipsum"]); + Ok(()) + } + + #[test] + fn no_pipe() -> Result<(), Box> { + let input = "foo bar baz"; + let (remaining, (before_pipe_words, after_pipe_words)) = keyword_todo(input)?; + assert_eq!(remaining, ""); + assert_eq!(before_pipe_words, vec!["foo", "bar"]); + assert_eq!(after_pipe_words, vec!["baz"]); + Ok(()) + } + + #[test] + fn early_pipe() -> Result<(), Box> { + let input = "| foo bar baz"; + let (remaining, (before_pipe_words, after_pipe_words)) = keyword_todo(input)?; + assert_eq!(remaining, ""); + assert_eq!(before_pipe_words, Vec::<&str>::new()); + assert_eq!(after_pipe_words, vec!["foo", "bar", "baz"]); + Ok(()) + } + + #[test] + fn late_pipe() -> Result<(), Box> { + let input = "foo bar baz |"; + let (remaining, (before_pipe_words, after_pipe_words)) = keyword_todo(input)?; + assert_eq!(remaining, ""); + assert_eq!(before_pipe_words, vec!["foo", "bar", "baz"]); + assert_eq!(after_pipe_words, Vec::<&str>::new()); + Ok(()) + } +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 72a350c..0b95974 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -19,6 +19,7 @@ mod in_buffer_settings; mod inline_babel_call; mod inline_source_block; mod keyword; +mod keyword_todo; mod latex_environment; mod latex_fragment; mod lesser_block; From 70fafd801ec9823acb322548188baae6441042b4 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Wed, 6 Sep 2023 11:00:19 -0400 Subject: [PATCH 16/20] Apply the TODO keyword settings. --- src/context/global_settings.rs | 6 ++++++ src/main.rs | 12 +++++++----- src/parser/document.rs | 10 ++++++++++ src/parser/in_buffer_settings.rs | 26 ++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/context/global_settings.rs b/src/context/global_settings.rs index f1b4d1b..efeccfc 100644 --- a/src/context/global_settings.rs +++ b/src/context/global_settings.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeSet; + use super::FileAccessInterface; use super::LocalFileAccessInterface; use crate::types::Object; @@ -8,6 +10,8 @@ use crate::types::Object; pub struct GlobalSettings<'g, 's> { pub radio_targets: Vec<&'g Vec>>, pub file_access: &'g dyn FileAccessInterface, + pub in_progress_todo_keywords: BTreeSet, + pub complete_todo_keywords: BTreeSet, } impl<'g, 's> GlobalSettings<'g, 's> { @@ -17,6 +21,8 @@ impl<'g, 's> GlobalSettings<'g, 's> { file_access: &LocalFileAccessInterface { working_directory: None, }, + in_progress_todo_keywords: BTreeSet::new(), + complete_todo_keywords: BTreeSet::new() } } } diff --git a/src/main.rs b/src/main.rs index fd82386..e5a19dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -148,11 +148,13 @@ fn run_parse_on_file>(org_path: P) -> Result<(), Box( 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) = diff --git a/src/parser/in_buffer_settings.rs b/src/parser/in_buffer_settings.rs index 3c533ac..f2ff0e7 100644 --- a/src/parser/in_buffer_settings.rs +++ b/src/parser/in_buffer_settings.rs @@ -6,9 +6,11 @@ use nom::multi::many0; use nom::multi::many_till; use super::keyword::filtered_keyword; +use super::keyword_todo::keyword_todo; use super::OrgSource; use crate::error::Res; use crate::types::Keyword; +use crate::GlobalSettings; #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] pub fn scan_for_in_buffer_settings<'s>( @@ -41,3 +43,27 @@ fn in_buffer_settings_key<'s>(input: OrgSource<'s>) -> Res, OrgSou tag_no_case("typ_todo"), ))(input) } + +pub fn apply_in_buffer_settings<'g, 's, 'sf>( + keywords: Vec>, + original_settings: &'g GlobalSettings<'g, 's>, +) -> Result, String> { + let mut new_settings = original_settings.clone(); + + for kw in keywords.iter().filter(|kw| { + kw.key.eq_ignore_ascii_case("todo") + || kw.key.eq_ignore_ascii_case("seq_todo") + || kw.key.eq_ignore_ascii_case("typ_todo") + }) { + let (_, (in_progress_words, complete_words)) = + keyword_todo(kw.value).map_err(|err| err.to_string())?; + new_settings + .in_progress_todo_keywords + .extend(in_progress_words.into_iter().map(str::to_string)); + new_settings + .complete_todo_keywords + .extend(complete_words.into_iter().map(str::to_string)); + } + + Ok(new_settings) +} From 028aeb70aaf6fbb5f446f0f886b9dd3aad96ab40 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Wed, 6 Sep 2023 11:45:35 -0400 Subject: [PATCH 17/20] Use the global settings todo keywords when parsing headlines. --- src/main.rs | 12 +++++--- src/parser/document.rs | 52 ++++++++++++++++++++++++-------- src/parser/in_buffer_settings.rs | 4 +-- src/parser/keyword_todo.rs | 20 +++++++----- 4 files changed, 61 insertions(+), 27 deletions(-) diff --git a/src/main.rs b/src/main.rs index e5a19dc..fc6d4dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -111,11 +111,13 @@ fn run_parse_on_file>(org_path: P) -> Result<(), Box( 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_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; @@ -382,7 +383,6 @@ fn headline<'b, 'g, 'r, 's>( exit_matcher: &headline_title_end, }); let parser_context = context.with_additional_node(&parser_context); - let standard_set_object_matcher = parser_with_context!(standard_set_object)(&parser_context); let ( remaining, @@ -393,8 +393,11 @@ fn headline<'b, 'g, 'r, 's>( *star_count > parent_stars }), space1, - opt(tuple((heading_keyword, space1))), - many1(standard_set_object_matcher), + 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)), @@ -443,9 +446,34 @@ fn single_tag<'r, 's>(input: OrgSource<'s>) -> Res, OrgSource<'s>> } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -fn heading_keyword<'s>(input: OrgSource<'s>) -> Res, OrgSource<'s>> { - // TODO: This should take into account the value of "#+TODO:" ref https://orgmode.org/manual/Per_002dfile-keywords.html and possibly the configurable variable org-todo-keywords ref https://orgmode.org/manual/Workflow-states.html. Case is significant. - alt((tag("TODO"), tag("DONE")))(input) +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> { diff --git a/src/parser/in_buffer_settings.rs b/src/parser/in_buffer_settings.rs index f2ff0e7..59f1143 100644 --- a/src/parser/in_buffer_settings.rs +++ b/src/parser/in_buffer_settings.rs @@ -6,7 +6,7 @@ use nom::multi::many0; use nom::multi::many_till; use super::keyword::filtered_keyword; -use super::keyword_todo::keyword_todo; +use super::keyword_todo::todo_keywords; use super::OrgSource; use crate::error::Res; use crate::types::Keyword; @@ -56,7 +56,7 @@ pub fn apply_in_buffer_settings<'g, 's, 'sf>( || kw.key.eq_ignore_ascii_case("typ_todo") }) { let (_, (in_progress_words, complete_words)) = - keyword_todo(kw.value).map_err(|err| err.to_string())?; + todo_keywords(kw.value).map_err(|err| err.to_string())?; new_settings .in_progress_todo_keywords .extend(in_progress_words.into_iter().map(str::to_string)); diff --git a/src/parser/keyword_todo.rs b/src/parser/keyword_todo.rs index 44facbb..821aa8e 100644 --- a/src/parser/keyword_todo.rs +++ b/src/parser/keyword_todo.rs @@ -12,14 +12,18 @@ use nom::sequence::tuple; use crate::error::Res; +// ref https://orgmode.org/manual/Per_002dfile-keywords.html +// ref https://orgmode.org/manual/Workflow-states.html +// Case is significant. + /// Parses the text in the value of a #+TODO keyword. /// /// Example input: "foo bar baz | lorem ipsum" -pub fn keyword_todo<'s>(input: &'s str) -> Res<&'s str, (Vec<&'s str>, Vec<&'s str>)> { - let (remaining, mut before_pipe_words) = separated_list0(space1, keyword_word)(input)?; +pub fn todo_keywords<'s>(input: &'s str) -> Res<&'s str, (Vec<&'s str>, Vec<&'s str>)> { + let (remaining, mut before_pipe_words) = separated_list0(space1, todo_keyword_word)(input)?; let (remaining, after_pipe_words) = opt(tuple(( tuple((space0, tag("|"), space0)), - separated_list0(space1, keyword_word), + separated_list0(space1, todo_keyword_word), )))(remaining)?; let (remaining, _eol) = alt((line_ending, eof))(remaining)?; if let Some((_pipe, after_pipe_words)) = after_pipe_words { @@ -39,7 +43,7 @@ pub fn keyword_todo<'s>(input: &'s str) -> Res<&'s str, (Vec<&'s str>, Vec<&'s s } } -fn keyword_word<'s>(input: &'s str) -> Res<&'s str, &'s str> { +fn todo_keyword_word<'s>(input: &'s str) -> Res<&'s str, &'s str> { verify(take_till(|c| " \t\r\n|".contains(c)), |result: &str| { !result.is_empty() })(input) @@ -51,7 +55,7 @@ mod tests { #[test] fn before_and_after() -> Result<(), Box> { let input = "foo bar baz | lorem ipsum"; - let (remaining, (before_pipe_words, after_pipe_words)) = keyword_todo(input)?; + let (remaining, (before_pipe_words, after_pipe_words)) = todo_keywords(input)?; assert_eq!(remaining, ""); assert_eq!(before_pipe_words, vec!["foo", "bar", "baz"]); assert_eq!(after_pipe_words, vec!["lorem", "ipsum"]); @@ -61,7 +65,7 @@ mod tests { #[test] fn no_pipe() -> Result<(), Box> { let input = "foo bar baz"; - let (remaining, (before_pipe_words, after_pipe_words)) = keyword_todo(input)?; + let (remaining, (before_pipe_words, after_pipe_words)) = todo_keywords(input)?; assert_eq!(remaining, ""); assert_eq!(before_pipe_words, vec!["foo", "bar"]); assert_eq!(after_pipe_words, vec!["baz"]); @@ -71,7 +75,7 @@ mod tests { #[test] fn early_pipe() -> Result<(), Box> { let input = "| foo bar baz"; - let (remaining, (before_pipe_words, after_pipe_words)) = keyword_todo(input)?; + let (remaining, (before_pipe_words, after_pipe_words)) = todo_keywords(input)?; assert_eq!(remaining, ""); assert_eq!(before_pipe_words, Vec::<&str>::new()); assert_eq!(after_pipe_words, vec!["foo", "bar", "baz"]); @@ -81,7 +85,7 @@ mod tests { #[test] fn late_pipe() -> Result<(), Box> { let input = "foo bar baz |"; - let (remaining, (before_pipe_words, after_pipe_words)) = keyword_todo(input)?; + let (remaining, (before_pipe_words, after_pipe_words)) = todo_keywords(input)?; assert_eq!(remaining, ""); assert_eq!(before_pipe_words, vec!["foo", "bar", "baz"]); assert_eq!(after_pipe_words, Vec::<&str>::new()); From 400f53e440f41df591c09e9dc4633d96dd2ec535 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Wed, 6 Sep 2023 11:48:24 -0400 Subject: [PATCH 18/20] Cleanup. --- src/context/context.rs | 5 ++++- src/context/file_access_interface.rs | 1 - src/context/global_settings.rs | 2 +- src/parser/document.rs | 2 -- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/context/context.rs b/src/context/context.rs index d8f2de3..0f41963 100644 --- a/src/context/context.rs +++ b/src/context/context.rs @@ -90,7 +90,10 @@ impl<'g, 'r, 's> Context<'g, 'r, 's> { self.global_settings } - pub fn with_global_settings<'gg>(&self, new_settings: &'gg GlobalSettings<'gg, 's>) -> Context<'gg, 'r, 's> { + pub fn with_global_settings<'gg>( + &self, + new_settings: &'gg GlobalSettings<'gg, 's>, + ) -> Context<'gg, 'r, 's> { Context { global_settings: new_settings, tree: self.tree.clone(), diff --git a/src/context/file_access_interface.rs b/src/context/file_access_interface.rs index 87d7b27..d54c7cf 100644 --- a/src/context/file_access_interface.rs +++ b/src/context/file_access_interface.rs @@ -18,7 +18,6 @@ impl FileAccessInterface for LocalFileAccessInterface { .map(PathBuf::as_path) .map(|pb| pb.join(path)) .unwrap_or_else(|| PathBuf::from(path)); - eprintln!("Reading file: {}", final_path.display()); Ok(std::fs::read_to_string(final_path)?) } } diff --git a/src/context/global_settings.rs b/src/context/global_settings.rs index efeccfc..4231d42 100644 --- a/src/context/global_settings.rs +++ b/src/context/global_settings.rs @@ -22,7 +22,7 @@ impl<'g, 's> GlobalSettings<'g, 's> { working_directory: None, }, in_progress_todo_keywords: BTreeSet::new(), - complete_todo_keywords: BTreeSet::new() + complete_todo_keywords: BTreeSet::new(), } } } diff --git a/src/parser/document.rs b/src/parser/document.rs index 8da59ef..e9556f3 100644 --- a/src/parser/document.rs +++ b/src/parser/document.rs @@ -135,8 +135,6 @@ fn document_org_source<'b, 'g, 'r, 's>( 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))?; { From 7c471ab32e6189f387fa9695bba6bebe69b00079 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Wed, 6 Sep 2023 12:08:06 -0400 Subject: [PATCH 19/20] Compare keyword keys and values. --- src/compare/diff.rs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/compare/diff.rs b/src/compare/diff.rs index 704335d..a2f4117 100644 --- a/src/compare/diff.rs +++ b/src/compare/diff.rs @@ -1392,7 +1392,30 @@ fn compare_keyword<'s>( Ok(_) => {} }; - // TODO: Compare key and value + let key = unquote( + get_property(emacs, ":key")? + .ok_or("Emacs keywords should have a :key")? + .as_atom()?, + )?; + if key != rust.key.to_uppercase() { + this_status = DiffStatus::Bad; + message = Some(format!( + "Mismatchs keyword keys (emacs != rust) {:?} != {:?}", + key, rust.key + )) + } + let value = unquote( + get_property(emacs, ":value")? + .ok_or("Emacs keywords should have a :value")? + .as_atom()?, + )?; + if value != rust.value { + this_status = DiffStatus::Bad; + message = Some(format!( + "Mismatchs keyword values (emacs != rust) {:?} != {:?}", + value, rust.value + )) + } Ok(DiffResult { status: this_status, From 12cbb898618970e6ff2eb40c5e811ff01da58848 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Wed, 6 Sep 2023 12:39:03 -0400 Subject: [PATCH 20/20] Compare todo-type on headlines. --- src/compare/diff.rs | 25 ++++++++++++++++++++++--- src/parser/document.rs | 31 ++++++++++++++++++++++++------- src/types/document.rs | 8 +++++++- src/types/mod.rs | 1 + 4 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/compare/diff.rs b/src/compare/diff.rs index a2f4117..4b24dc4 100644 --- a/src/compare/diff.rs +++ b/src/compare/diff.rs @@ -60,6 +60,7 @@ use crate::types::TableCell; use crate::types::TableRow; use crate::types::Target; use crate::types::Timestamp; +use crate::types::TodoKeywordType; use crate::types::Underline; use crate::types::Verbatim; use crate::types::VerseBlock; @@ -510,9 +511,9 @@ fn compare_heading<'s>( .map(Token::as_atom) .map_or(Ok(None), |r| r.map(Some))? .unwrap_or("nil"); - match (todo_keyword, rust.todo_keyword, unquote(todo_keyword)) { + match (todo_keyword, &rust.todo_keyword, unquote(todo_keyword)) { ("nil", None, _) => {} - (_, Some(rust_todo), Ok(emacs_todo)) if emacs_todo == rust_todo => {} + (_, Some((_rust_todo_type, rust_todo)), Ok(emacs_todo)) if emacs_todo == *rust_todo => {} (emacs_todo, rust_todo, _) => { this_status = DiffStatus::Bad; message = Some(format!( @@ -521,6 +522,24 @@ fn compare_heading<'s>( )); } }; + // Compare todo-type + let todo_type = get_property(emacs, ":todo-type")? + .map(Token::as_atom) + .map_or(Ok(None), |r| r.map(Some))? + .unwrap_or("nil"); + // todo-type is an unquoted string either todo, done, or nil + match (todo_type, &rust.todo_keyword) { + ("nil", None) => {} + ("todo", Some((TodoKeywordType::Todo, _))) => {} + ("done", Some((TodoKeywordType::Done, _))) => {} + (emacs_todo, rust_todo) => { + this_status = DiffStatus::Bad; + message = Some(format!( + "(emacs != rust) {:?} != {:?}", + emacs_todo, rust_todo + )); + } + }; // Compare title let title = get_property(emacs, ":title")?.ok_or("Missing :title attribute.")?; @@ -532,7 +551,7 @@ fn compare_heading<'s>( .collect::, _>>()?; child_status.push(artificial_diff_scope("title".to_owned(), title_status)?); - // TODO: Compare todo-type, priority, :footnote-section-p, :archivedp, :commentedp + // TODO: Compare priority, :footnote-section-p, :archivedp, :commentedp // Compare section let section_status = children diff --git a/src/parser/document.rs b/src/parser/document.rs index e9556f3..8c50dbd 100644 --- a/src/parser/document.rs +++ b/src/parser/document.rs @@ -51,6 +51,7 @@ use crate::types::Element; use crate::types::Heading; use crate::types::Object; use crate::types::Section; +use crate::types::TodoKeywordType; /// Parse a full org-mode document. /// @@ -346,8 +347,9 @@ fn _heading<'b, 'g, 'r, 's>( Heading { source: source.into(), stars: star_count, - todo_keyword: maybe_todo_keyword - .map(|(todo_keyword, _ws)| Into::<&str>::into(todo_keyword)), + todo_keyword: maybe_todo_keyword.map(|((todo_keyword_type, todo_keyword), _ws)| { + (todo_keyword_type, Into::<&str>::into(todo_keyword)) + }), title, tags: heading_tags, children, @@ -371,7 +373,7 @@ fn headline<'b, 'g, 'r, 's>( ( usize, OrgSource<'s>, - Option<(OrgSource<'s>, OrgSource<'s>)>, + Option<((TodoKeywordType, OrgSource<'s>), OrgSource<'s>)>, Vec>, Vec<&'s str>, ), @@ -447,23 +449,38 @@ fn single_tag<'r, 's>(input: OrgSource<'s>) -> Res, OrgSource<'s>> fn heading_keyword<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, -) -> Res, 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((tag("TODO"), tag("DONE")))(input) + 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() - .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)); + 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(_) => {} } diff --git a/src/types/document.rs b/src/types/document.rs index 957d7b9..654377a 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -13,7 +13,7 @@ pub struct Document<'s> { pub struct Heading<'s> { pub source: &'s str, pub stars: usize, - pub todo_keyword: Option<&'s str>, + pub todo_keyword: Option<(TodoKeywordType, &'s str)>, // TODO: add todo-type enum pub title: Vec>, pub tags: Vec<&'s str>, @@ -32,6 +32,12 @@ pub enum DocumentElement<'s> { Section(Section<'s>), } +#[derive(Debug)] +pub enum TodoKeywordType { + Todo, + Done, +} + impl<'s> Source<'s> for Document<'s> { fn get_source(&'s self) -> &'s str { self.source diff --git a/src/types/mod.rs b/src/types/mod.rs index 4ab8c17..efd1b04 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -8,6 +8,7 @@ pub use document::Document; pub use document::DocumentElement; pub use document::Heading; pub use document::Section; +pub use document::TodoKeywordType; pub use element::Element; pub use greater_element::Drawer; pub use greater_element::DynamicBlock;