diff --git a/src/compare/diff.rs b/src/compare/diff.rs index 4402200..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 @@ -1392,6 +1411,31 @@ fn compare_keyword<'s>( Ok(_) => {} }; + 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, name: emacs_name.to_owned(), diff --git a/src/context/parser_context.rs b/src/context/context.rs similarity index 97% rename from src/context/parser_context.rs rename to src/context/context.rs index d8f2de3..0f41963 100644 --- a/src/context/parser_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 new file mode 100644 index 0000000..d54c7cf --- /dev/null +++ b/src/context/file_access_interface.rs @@ -0,0 +1,23 @@ +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 working_directory: Option, +} + +impl FileAccessInterface for LocalFileAccessInterface { + fn read_file(&self, path: &str) -> Result { + let final_path = self + .working_directory + .as_ref() + .map(PathBuf::as_path) + .map(|pb| pb.join(path)) + .unwrap_or_else(|| PathBuf::from(path)); + Ok(std::fs::read_to_string(final_path)?) + } +} diff --git a/src/context/global_settings.rs b/src/context/global_settings.rs index ad3640d..4231d42 100644 --- a/src/context/global_settings.rs +++ b/src/context/global_settings.rs @@ -1,20 +1,34 @@ +use std::collections::BTreeSet; + +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 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> { - pub fn new() -> Self { + pub fn new() -> GlobalSettings<'g, 's> { GlobalSettings { radio_targets: Vec::new(), + file_access: &LocalFileAccessInterface { + working_directory: None, + }, + in_progress_todo_keywords: BTreeSet::new(), + complete_todo_keywords: BTreeSet::new(), } } } impl<'g, 's> Default for GlobalSettings<'g, 's> { - fn default() -> Self { + fn default() -> GlobalSettings<'g, 's> { GlobalSettings::new() } } diff --git a/src/context/mod.rs b/src/context/mod.rs index 32e04f0..42f63fd 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -1,10 +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>; @@ -17,10 +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; 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/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 1d2320a..fc6d4dd 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; @@ -68,7 +71,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(input: &'s str) -> Result, String> { - let global_settings = GlobalSettings::default(); + 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 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) @@ -62,7 +81,11 @@ pub fn parse<'s>(input: &'s str) -> Result, String> { ret } -#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +/// 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>, @@ -78,6 +101,41 @@ 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, document) = _document(context, input).map(|(rem, out)| (Into::<&str>::into(rem), out))?; { @@ -289,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, @@ -314,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>, ), @@ -324,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, @@ -335,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)), @@ -385,9 +446,49 @@ 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, (TodoKeywordType, OrgSource<'s>)> { + let global_settings = context.get_global_settings(); + if global_settings.in_progress_todo_keywords.is_empty() + && global_settings.complete_todo_keywords.is_empty() + { + alt(( + map(tag("TODO"), |capture| (TodoKeywordType::Todo, capture)), + map(tag("DONE"), |capture| (TodoKeywordType::Done, capture)), + ))(input) + } else { + for todo_keyword in global_settings + .in_progress_todo_keywords + .iter() + .map(String::as_str) + { + let result = tag::<_, _, CustomError<_>>(todo_keyword)(input); + match result { + Ok((remaining, ent)) => { + return Ok((remaining, (TodoKeywordType::Todo, ent))); + } + Err(_) => {} + } + } + for todo_keyword in global_settings + .complete_todo_keywords + .iter() + .map(String::as_str) + { + let result = tag::<_, _, CustomError<_>>(todo_keyword)(input); + match result { + Ok((remaining, ent)) => { + return Ok((remaining, (TodoKeywordType::Done, ent))); + } + Err(_) => {} + } + } + Err(nom::Err::Error(CustomError::MyError(MyError( + "NoTodoKeyword".into(), + )))) + } } impl<'s> Document<'s> { diff --git a/src/parser/in_buffer_settings.rs b/src/parser/in_buffer_settings.rs new file mode 100644 index 0000000..59f1143 --- /dev/null +++ b/src/parser/in_buffer_settings.rs @@ -0,0 +1,69 @@ +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::keyword_todo::todo_keywords; +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>( + 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("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) +} + +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)) = + 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)); + new_settings + .complete_todo_keywords + .extend(complete_words.into_iter().map(str::to_string)); + } + + Ok(new_settings) +} diff --git a/src/parser/keyword.rs b/src/parser/keyword.rs index ee7a22d..3c853b7 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,29 +31,63 @@ const ORG_ELEMENT_AFFILIATED_KEYWORDS: [&'static str; 13] = [ ]; const ORG_ELEMENT_DUAL_KEYWORDS: [&'static str; 2] = ["caption", "results"]; +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, (consumed_input, (_, _, parsed_key, _))) = + consumed(tuple((space0, tag("#+"), key_parser, tag(":"))))(input)?; + match tuple(( + space0::, CustomError>>, + alt((line_ending, eof)), + ))(remaining) + { + Ok((remaining, _)) => { + return Ok(( + remaining, + Keyword { + source: consumed_input.into(), + key: parsed_key.into(), + value: "".into(), + }, + )); + } + Err(_) => {} + }; + let (remaining, _ws) = space1(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, + Keyword { + 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>> { - 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(( - space0, - tag("#+"), - not(peek(tag_no_case("call"))), - not(peek(tag_no_case("begin"))), - is_not(" \t\r\n:"), - tag(":"), - alt((recognize(tuple((space1, is_not("\r\n")))), space0)), - alt((line_ending, eof)), - )))(input)?; - Ok(( - remaining, - Keyword { - source: rule.into(), - }, - )) + filtered_keyword(regular_keyword_key)(input) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] @@ -59,23 +95,16 @@ 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/keyword_todo.rs b/src/parser/keyword_todo.rs new file mode 100644 index 0000000..821aa8e --- /dev/null +++ b/src/parser/keyword_todo.rs @@ -0,0 +1,94 @@ +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; + +// 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 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, todo_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 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) +} +#[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)) = todo_keywords(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)) = todo_keywords(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)) = todo_keywords(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)) = todo_keywords(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 eae8a04..0b95974 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -15,9 +15,11 @@ 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; +mod keyword_todo; mod latex_environment; mod latex_fragment; mod lesser_block; @@ -44,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; 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), } } } 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/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)] 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;