Compare heading todo keywords.
All checks were successful
rust-test Build rust-test has succeeded
rust-build Build rust-build has succeeded

This only handles the default case where the only valid TODO keywords are TODO and DONE.
This commit is contained in:
Tom Alexander 2023-08-27 15:56:08 -04:00
parent 9cc5e63c1b
commit 3e143796f7
Signed by: talexander
GPG Key ID: D3A179C9A53C0EDE
2 changed files with 78 additions and 18 deletions

View File

@ -2,6 +2,7 @@ use std::collections::HashSet;
use super::util::assert_bounds; use super::util::assert_bounds;
use super::util::assert_name; use super::util::assert_name;
use crate::parser::sexp::unquote;
use crate::parser::sexp::Token; use crate::parser::sexp::Token;
use crate::parser::AngleLink; use crate::parser::AngleLink;
use crate::parser::Bold; use crate::parser::Bold;
@ -60,7 +61,6 @@ use crate::parser::Timestamp;
use crate::parser::Underline; use crate::parser::Underline;
use crate::parser::Verbatim; use crate::parser::Verbatim;
use crate::parser::VerseBlock; use crate::parser::VerseBlock;
use crate::parser::sexp::unquote;
#[derive(Debug)] #[derive(Debug)]
pub struct DiffResult { pub struct DiffResult {
@ -336,6 +336,45 @@ fn compare_heading<'s>(
this_status = DiffStatus::Bad; 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 title = {
let children = emacs.as_list()?; let children = emacs.as_list()?;
let attributes_child = children 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()) { for (emacs_child, rust_child) in title.as_list()?.iter().zip(rust.title.iter()) {
child_status.push(compare_object(source, emacs_child, rust_child)?); 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()) { for (emacs_child, rust_child) in children.iter().skip(2).zip(rust.children.iter()) {
match rust_child { match rust_child {
DocumentElement::Heading(rust_heading) => { DocumentElement::Heading(rust_heading) => {
@ -405,8 +434,14 @@ fn get_tags_from_heading<'s>(
}; };
let tags = { let tags = {
let tags = tags.as_list()?; let tags = tags.as_list()?;
let strings = tags.iter().map(Token::as_atom).collect::<Result<Vec<&str>, _>>()?; let strings = tags
strings.into_iter().map(unquote).collect::<Result<HashSet<String>, _>>()? .iter()
.map(Token::as_atom)
.collect::<Result<Vec<&str>, _>>()?;
strings
.into_iter()
.map(unquote)
.collect::<Result<HashSet<String>, _>>()?
}; };
Ok(tags) Ok(tags)
} }

View File

@ -53,6 +53,8 @@ pub struct Document<'s> {
pub struct Heading<'s> { pub struct Heading<'s> {
pub source: &'s str, pub source: &'s str,
pub stars: usize, pub stars: usize,
pub todo_keyword: Option<&'s str>,
// TODO: add todo-type enum
pub title: Vec<Object<'s>>, pub title: Vec<Object<'s>>,
pub tags: Vec<&'s str>, pub tags: Vec<&'s str>,
pub children: Vec<DocumentElement<'s>>, pub children: Vec<DocumentElement<'s>>,
@ -271,7 +273,8 @@ fn heading<'r, 's>(
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Heading<'s>> { ) -> Res<OrgSource<'s>, Heading<'s>> {
not(|i| context.check_exit_matcher(i))(input)?; 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 section_matcher = parser_with_context!(section)(context);
let heading_matcher = parser_with_context!(heading)(context); let heading_matcher = parser_with_context!(heading)(context);
let (remaining, children) = many0(alt(( let (remaining, children) = many0(alt((
@ -287,6 +290,8 @@ fn heading<'r, 's>(
Heading { Heading {
source: source.into(), source: source.into(),
stars: star_count, stars: star_count,
todo_keyword: maybe_todo_keyword
.map(|(todo_keyword, _ws)| Into::<&str>::into(todo_keyword)),
title, title,
tags: heading_tags, tags: heading_tags,
children, children,
@ -298,7 +303,16 @@ fn heading<'r, 's>(
fn headline<'r, 's>( fn headline<'r, 's>(
context: Context<'r, 's>, context: Context<'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, (usize, OrgSource<'s>, Vec<Object<'s>>, Vec<&'s str>)> { ) -> Res<
OrgSource<'s>,
(
usize,
OrgSource<'s>,
Option<(OrgSource<'s>, OrgSource<'s>)>,
Vec<Object<'s>>,
Vec<&'s str>,
),
> {
let parser_context = let parser_context =
context.with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode { context.with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Document, 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 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, start_of_line,
many1_count(tag("*")), many1_count(tag("*")),
space1, space1,
opt(tuple((heading_keyword, space1))),
many1(standard_set_object_matcher), many1(standard_set_object_matcher),
opt(tuple((space0, tags))), opt(tuple((space0, tags))),
space0, space0,
@ -320,6 +338,7 @@ fn headline<'r, 's>(
( (
star_count, star_count,
ws, ws,
maybe_todo_keyword,
title, title,
maybe_tags maybe_tags
.map(|(_ws, tags)| { .map(|(_ws, tags)| {
@ -357,6 +376,12 @@ fn single_tag<'r, 's>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>>
})))(input) })))(input)
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn heading_keyword<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, 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> { impl<'s> Document<'s> {
pub fn iter_tokens<'r>(&'r self) -> impl Iterator<Item = Token<'r, 's>> { pub fn iter_tokens<'r>(&'r self) -> impl Iterator<Item = Token<'r, 's>> {
AllTokensIterator::new(Token::Document(self)) AllTokensIterator::new(Token::Document(self))