diff --git a/src/compare/diff.rs b/src/compare/diff.rs index 2a683ff..5c22d63 100644 --- a/src/compare/diff.rs +++ b/src/compare/diff.rs @@ -2,6 +2,7 @@ use std::collections::HashSet; use super::util::assert_bounds; use super::util::assert_name; +use crate::parser::sexp::unquote; use crate::parser::sexp::Token; use crate::parser::AngleLink; use crate::parser::Bold; @@ -60,7 +61,6 @@ use crate::parser::Timestamp; use crate::parser::Underline; use crate::parser::Verbatim; use crate::parser::VerseBlock; -use crate::parser::sexp::unquote; #[derive(Debug)] pub struct DiffResult { @@ -336,6 +336,45 @@ fn compare_heading<'s>( this_status = DiffStatus::Bad; } + // Compare tags + let emacs_tags = get_tags_from_heading(emacs)?; + let emacs_tags: HashSet<_> = emacs_tags.iter().map(|val| val.as_str()).collect(); + let rust_tags: HashSet<&str> = rust.tags.iter().map(|val| *val).collect(); + let difference: Vec<&str> = emacs_tags + .symmetric_difference(&rust_tags) + .map(|val| *val) + .collect(); + if !difference.is_empty() { + this_status = DiffStatus::Bad; + message = Some(format!("Mismatched tags: {}", difference.join(", "))); + } + + // Compare todo-keyword + let todo_keyword = { + let children = emacs.as_list()?; + let attributes_child = children + .iter() + .nth(1) + .ok_or("Should have an attributes child.")?; + let attributes_map = attributes_child.as_map()?; + let todo_keyword = attributes_map + .get(":todo-keyword") + .ok_or("Missing :todo-keyword attribute."); + todo_keyword?.as_atom()? + }; + match (todo_keyword, rust.todo_keyword, unquote(todo_keyword)) { + ("nil", None, _) => {} + (_, Some(rust_todo), Ok(emacs_todo)) if emacs_todo == rust_todo => {} + (emacs_todo, rust_todo, _) => { + this_status = DiffStatus::Bad; + message = Some(format!( + "(emacs != rust) {:?} != {:?}", + emacs_todo, rust_todo + )); + } + }; + + // Compare title let title = { let children = emacs.as_list()?; let attributes_child = children @@ -351,20 +390,10 @@ fn compare_heading<'s>( for (emacs_child, rust_child) in title.as_list()?.iter().zip(rust.title.iter()) { child_status.push(compare_object(source, emacs_child, rust_child)?); } - let emacs_tags = get_tags_from_heading(emacs)?; - let emacs_tags: HashSet<_> = emacs_tags.iter().map(|val| val.as_str()).collect(); - let rust_tags: HashSet<&str> = rust.tags.iter().map(|val| *val).collect(); - let difference: Vec<&str> = emacs_tags - .symmetric_difference(&rust_tags) - .map(|val| *val) - .collect(); - if !difference.is_empty() { - this_status = DiffStatus::Bad; - message = Some(format!("Mismatched tags: {}", difference.join(", "))); - } - // TODO: Compare todo-keyword, level, priority + // TODO: Compare todo-type, level, priority + // Compare section for (emacs_child, rust_child) in children.iter().skip(2).zip(rust.children.iter()) { match rust_child { DocumentElement::Heading(rust_heading) => { @@ -405,8 +434,14 @@ fn get_tags_from_heading<'s>( }; let tags = { let tags = tags.as_list()?; - let strings = tags.iter().map(Token::as_atom).collect::, _>>()?; - strings.into_iter().map(unquote).collect::, _>>()? + let strings = tags + .iter() + .map(Token::as_atom) + .collect::, _>>()?; + strings + .into_iter() + .map(unquote) + .collect::, _>>()? }; Ok(tags) } diff --git a/src/parser/document.rs b/src/parser/document.rs index 5112b4e..8fd8624 100644 --- a/src/parser/document.rs +++ b/src/parser/document.rs @@ -53,6 +53,8 @@ pub struct Document<'s> { pub struct Heading<'s> { pub source: &'s str, pub stars: usize, + pub todo_keyword: Option<&'s str>, + // TODO: add todo-type enum pub title: Vec>, pub tags: Vec<&'s str>, pub children: Vec>, @@ -271,7 +273,8 @@ fn heading<'r, 's>( input: OrgSource<'s>, ) -> Res, Heading<'s>> { not(|i| context.check_exit_matcher(i))(input)?; - let (remaining, (star_count, _ws, title, heading_tags)) = headline(context, input)?; + let (remaining, (star_count, _ws, maybe_todo_keyword, title, heading_tags)) = + headline(context, input)?; let section_matcher = parser_with_context!(section)(context); let heading_matcher = parser_with_context!(heading)(context); let (remaining, children) = many0(alt(( @@ -287,6 +290,8 @@ fn heading<'r, 's>( Heading { source: source.into(), stars: star_count, + todo_keyword: maybe_todo_keyword + .map(|(todo_keyword, _ws)| Into::<&str>::into(todo_keyword)), title, tags: heading_tags, children, @@ -298,7 +303,16 @@ fn heading<'r, 's>( fn headline<'r, 's>( context: Context<'r, 's>, input: OrgSource<'s>, -) -> Res, (usize, OrgSource<'s>, Vec>, Vec<&'s str>)> { +) -> Res< + OrgSource<'s>, + ( + usize, + OrgSource<'s>, + Option<(OrgSource<'s>, OrgSource<'s>)>, + Vec>, + Vec<&'s str>, + ), +> { let parser_context = context.with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode { class: ExitClass::Document, @@ -306,10 +320,14 @@ fn headline<'r, 's>( })); let standard_set_object_matcher = parser_with_context!(standard_set_object)(&parser_context); - let (remaining, (_sol, star_count, ws, title, maybe_tags, _ws, _line_ending)) = tuple(( + let ( + remaining, + (_sol, star_count, ws, maybe_todo_keyword, title, maybe_tags, _ws, _line_ending), + ) = tuple(( start_of_line, many1_count(tag("*")), space1, + opt(tuple((heading_keyword, space1))), many1(standard_set_object_matcher), opt(tuple((space0, tags))), space0, @@ -320,6 +338,7 @@ fn headline<'r, 's>( ( star_count, ws, + maybe_todo_keyword, title, maybe_tags .map(|(_ws, tags)| { @@ -357,6 +376,12 @@ fn single_tag<'r, 's>(input: OrgSource<'s>) -> Res, OrgSource<'s>> })))(input) } +#[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) +} + impl<'s> Document<'s> { pub fn iter_tokens<'r>(&'r self) -> impl Iterator> { AllTokensIterator::new(Token::Document(self))