Compare commits

...

5 Commits

Author SHA1 Message Date
Tom Alexander
12cbb89861
Compare todo-type on headlines.
All checks were successful
rust-test Build rust-test has succeeded
rust-build Build rust-build has succeeded
2023-09-06 12:39:03 -04:00
Tom Alexander
7c471ab32e
Compare keyword keys and values. 2023-09-06 12:10:57 -04:00
Tom Alexander
400f53e440
Cleanup. 2023-09-06 11:53:21 -04:00
Tom Alexander
028aeb70aa
Use the global settings todo keywords when parsing headlines. 2023-09-06 11:45:35 -04:00
Tom Alexander
70fafd801e
Apply the TODO keyword settings. 2023-09-06 11:07:57 -04:00
10 changed files with 179 additions and 35 deletions

View File

@ -60,6 +60,7 @@ use crate::types::TableCell;
use crate::types::TableRow; use crate::types::TableRow;
use crate::types::Target; use crate::types::Target;
use crate::types::Timestamp; use crate::types::Timestamp;
use crate::types::TodoKeywordType;
use crate::types::Underline; use crate::types::Underline;
use crate::types::Verbatim; use crate::types::Verbatim;
use crate::types::VerseBlock; use crate::types::VerseBlock;
@ -510,9 +511,9 @@ fn compare_heading<'s>(
.map(Token::as_atom) .map(Token::as_atom)
.map_or(Ok(None), |r| r.map(Some))? .map_or(Ok(None), |r| r.map(Some))?
.unwrap_or("nil"); .unwrap_or("nil");
match (todo_keyword, rust.todo_keyword, unquote(todo_keyword)) { match (todo_keyword, &rust.todo_keyword, unquote(todo_keyword)) {
("nil", None, _) => {} ("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, _) => { (emacs_todo, rust_todo, _) => {
this_status = DiffStatus::Bad; this_status = DiffStatus::Bad;
message = Some(format!( 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 // Compare title
let title = get_property(emacs, ":title")?.ok_or("Missing :title attribute.")?; let title = get_property(emacs, ":title")?.ok_or("Missing :title attribute.")?;
@ -532,7 +551,7 @@ fn compare_heading<'s>(
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
child_status.push(artificial_diff_scope("title".to_owned(), title_status)?); 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 // Compare section
let section_status = children let section_status = children
@ -1392,7 +1411,30 @@ fn compare_keyword<'s>(
Ok(_) => {} 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 { Ok(DiffResult {
status: this_status, status: this_status,

View File

@ -90,7 +90,10 @@ impl<'g, 'r, 's> Context<'g, 'r, 's> {
self.global_settings 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 { Context {
global_settings: new_settings, global_settings: new_settings,
tree: self.tree.clone(), tree: self.tree.clone(),

View File

@ -18,7 +18,6 @@ impl FileAccessInterface for LocalFileAccessInterface {
.map(PathBuf::as_path) .map(PathBuf::as_path)
.map(|pb| pb.join(path)) .map(|pb| pb.join(path))
.unwrap_or_else(|| PathBuf::from(path)); .unwrap_or_else(|| PathBuf::from(path));
eprintln!("Reading file: {}", final_path.display());
Ok(std::fs::read_to_string(final_path)?) Ok(std::fs::read_to_string(final_path)?)
} }
} }

View File

@ -1,3 +1,5 @@
use std::collections::BTreeSet;
use super::FileAccessInterface; use super::FileAccessInterface;
use super::LocalFileAccessInterface; use super::LocalFileAccessInterface;
use crate::types::Object; use crate::types::Object;
@ -8,6 +10,8 @@ use crate::types::Object;
pub struct GlobalSettings<'g, 's> { pub struct GlobalSettings<'g, 's> {
pub radio_targets: Vec<&'g Vec<Object<'s>>>, pub radio_targets: Vec<&'g Vec<Object<'s>>>,
pub file_access: &'g dyn FileAccessInterface, pub file_access: &'g dyn FileAccessInterface,
pub in_progress_todo_keywords: BTreeSet<String>,
pub complete_todo_keywords: BTreeSet<String>,
} }
impl<'g, 's> GlobalSettings<'g, 's> { impl<'g, 's> GlobalSettings<'g, 's> {
@ -17,6 +21,8 @@ impl<'g, 's> GlobalSettings<'g, 's> {
file_access: &LocalFileAccessInterface { file_access: &LocalFileAccessInterface {
working_directory: None, working_directory: None,
}, },
in_progress_todo_keywords: BTreeSet::new(),
complete_todo_keywords: BTreeSet::new(),
} }
} }
} }

View File

@ -111,11 +111,13 @@ fn run_parse_on_file<P: AsRef<Path>>(org_path: P) -> Result<(), Box<dyn std::err
.ok_or("Should be contained inside a directory.")?; .ok_or("Should be contained inside a directory.")?;
let org_contents = std::fs::read_to_string(org_path)?; let org_contents = std::fs::read_to_string(org_path)?;
let org_contents = org_contents.as_str(); let org_contents = org_contents.as_str();
let global_settings = GlobalSettings { let file_access_interface = LocalFileAccessInterface {
radio_targets: Vec::new(),
file_access: &LocalFileAccessInterface {
working_directory: Some(parent_directory.to_path_buf()), working_directory: Some(parent_directory.to_path_buf()),
}, };
let global_settings = {
let mut global_settings = GlobalSettings::default();
global_settings.file_access = &file_access_interface;
global_settings
}; };
let rust_parsed = parse_with_settings(org_contents, &global_settings)?; let rust_parsed = parse_with_settings(org_contents, &global_settings)?;
let org_sexp = emacs_parse_file_org_document(org_path)?; let org_sexp = emacs_parse_file_org_document(org_path)?;
@ -148,11 +150,13 @@ fn run_parse_on_file<P: AsRef<Path>>(org_path: P) -> Result<(), Box<dyn std::err
.ok_or("Should be contained inside a directory.")?; .ok_or("Should be contained inside a directory.")?;
let org_contents = std::fs::read_to_string(org_path)?; let org_contents = std::fs::read_to_string(org_path)?;
let org_contents = org_contents.as_str(); let org_contents = org_contents.as_str();
let global_settings = GlobalSettings { let file_access_interface = LocalFileAccessInterface {
radio_targets: Vec::new(),
file_access: &LocalFileAccessInterface {
working_directory: Some(parent_directory.to_path_buf()), working_directory: Some(parent_directory.to_path_buf()),
}, };
let global_settings = {
let mut global_settings = GlobalSettings::default();
global_settings.file_access = &file_access_interface;
global_settings
}; };
let rust_parsed = parse_with_settings(org_contents, &global_settings)?; let rust_parsed = parse_with_settings(org_contents, &global_settings)?;
println!("{:#?}", rust_parsed); println!("{:#?}", rust_parsed);

View File

@ -18,6 +18,7 @@ use nom::multi::many_till;
use nom::multi::separated_list1; use nom::multi::separated_list1;
use nom::sequence::tuple; use nom::sequence::tuple;
use super::in_buffer_settings::apply_in_buffer_settings;
use super::in_buffer_settings::scan_for_in_buffer_settings; use super::in_buffer_settings::scan_for_in_buffer_settings;
use super::org_source::OrgSource; use super::org_source::OrgSource;
use super::token::AllTokensIterator; use super::token::AllTokensIterator;
@ -50,6 +51,7 @@ use crate::types::Element;
use crate::types::Heading; use crate::types::Heading;
use crate::types::Object; use crate::types::Object;
use crate::types::Section; use crate::types::Section;
use crate::types::TodoKeywordType;
/// Parse a full org-mode document. /// Parse a full org-mode document.
/// ///
@ -124,7 +126,15 @@ fn document_org_source<'b, 'g, 'r, 's>(
final_settings.extend(setup_file_settings); final_settings.extend(setup_file_settings);
} }
final_settings.extend(document_settings); final_settings.extend(document_settings);
// TODO: read the keywords into settings and apply them to the GlobalSettings. 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) = let (remaining, document) =
_document(context, input).map(|(rem, out)| (Into::<&str>::into(rem), out))?; _document(context, input).map(|(rem, out)| (Into::<&str>::into(rem), out))?;
@ -337,8 +347,9 @@ fn _heading<'b, 'g, 'r, 's>(
Heading { Heading {
source: source.into(), source: source.into(),
stars: star_count, stars: star_count,
todo_keyword: maybe_todo_keyword todo_keyword: maybe_todo_keyword.map(|((todo_keyword_type, todo_keyword), _ws)| {
.map(|(todo_keyword, _ws)| Into::<&str>::into(todo_keyword)), (todo_keyword_type, Into::<&str>::into(todo_keyword))
}),
title, title,
tags: heading_tags, tags: heading_tags,
children, children,
@ -362,7 +373,7 @@ fn headline<'b, 'g, 'r, 's>(
( (
usize, usize,
OrgSource<'s>, OrgSource<'s>,
Option<(OrgSource<'s>, OrgSource<'s>)>, Option<((TodoKeywordType, OrgSource<'s>), OrgSource<'s>)>,
Vec<Object<'s>>, Vec<Object<'s>>,
Vec<&'s str>, Vec<&'s str>,
), ),
@ -372,7 +383,6 @@ fn headline<'b, 'g, 'r, 's>(
exit_matcher: &headline_title_end, exit_matcher: &headline_title_end,
}); });
let parser_context = context.with_additional_node(&parser_context); let parser_context = context.with_additional_node(&parser_context);
let standard_set_object_matcher = parser_with_context!(standard_set_object)(&parser_context);
let ( let (
remaining, remaining,
@ -383,8 +393,11 @@ fn headline<'b, 'g, 'r, 's>(
*star_count > parent_stars *star_count > parent_stars
}), }),
space1, space1,
opt(tuple((heading_keyword, space1))), opt(tuple((
many1(standard_set_object_matcher), parser_with_context!(heading_keyword)(&parser_context),
space1,
))),
many1(parser_with_context!(standard_set_object)(&parser_context)),
opt(tuple((space0, tags))), opt(tuple((space0, tags))),
space0, space0,
alt((line_ending, eof)), alt((line_ending, eof)),
@ -433,9 +446,49 @@ fn single_tag<'r, 's>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>>
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn heading_keyword<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> { fn heading_keyword<'b, 'g, 'r, '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. context: RefContext<'b, 'g, 'r, 's>,
alt((tag("TODO"), tag("DONE")))(input) input: OrgSource<'s>,
) -> Res<OrgSource<'s>, (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> { impl<'s> Document<'s> {

View File

@ -6,9 +6,11 @@ use nom::multi::many0;
use nom::multi::many_till; use nom::multi::many_till;
use super::keyword::filtered_keyword; use super::keyword::filtered_keyword;
use super::keyword_todo::todo_keywords;
use super::OrgSource; use super::OrgSource;
use crate::error::Res; use crate::error::Res;
use crate::types::Keyword; use crate::types::Keyword;
use crate::GlobalSettings;
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub fn scan_for_in_buffer_settings<'s>( pub fn scan_for_in_buffer_settings<'s>(
@ -41,3 +43,27 @@ fn in_buffer_settings_key<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSou
tag_no_case("typ_todo"), tag_no_case("typ_todo"),
))(input) ))(input)
} }
pub fn apply_in_buffer_settings<'g, 's, 'sf>(
keywords: Vec<Keyword<'sf>>,
original_settings: &'g GlobalSettings<'g, 's>,
) -> Result<GlobalSettings<'g, 's>, 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)
}

View File

@ -12,14 +12,18 @@ use nom::sequence::tuple;
use crate::error::Res; 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. /// Parses the text in the value of a #+TODO keyword.
/// ///
/// Example input: "foo bar baz | lorem ipsum" /// Example input: "foo bar baz | lorem ipsum"
pub fn keyword_todo<'s>(input: &'s str) -> Res<&'s str, (Vec<&'s str>, Vec<&'s str>)> { 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, keyword_word)(input)?; let (remaining, mut before_pipe_words) = separated_list0(space1, todo_keyword_word)(input)?;
let (remaining, after_pipe_words) = opt(tuple(( let (remaining, after_pipe_words) = opt(tuple((
tuple((space0, tag("|"), space0)), tuple((space0, tag("|"), space0)),
separated_list0(space1, keyword_word), separated_list0(space1, todo_keyword_word),
)))(remaining)?; )))(remaining)?;
let (remaining, _eol) = alt((line_ending, eof))(remaining)?; let (remaining, _eol) = alt((line_ending, eof))(remaining)?;
if let Some((_pipe, after_pipe_words)) = after_pipe_words { 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| { verify(take_till(|c| " \t\r\n|".contains(c)), |result: &str| {
!result.is_empty() !result.is_empty()
})(input) })(input)
@ -51,7 +55,7 @@ mod tests {
#[test] #[test]
fn before_and_after() -> Result<(), Box<dyn std::error::Error>> { fn before_and_after() -> Result<(), Box<dyn std::error::Error>> {
let input = "foo bar baz | lorem ipsum"; 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!(remaining, "");
assert_eq!(before_pipe_words, vec!["foo", "bar", "baz"]); assert_eq!(before_pipe_words, vec!["foo", "bar", "baz"]);
assert_eq!(after_pipe_words, vec!["lorem", "ipsum"]); assert_eq!(after_pipe_words, vec!["lorem", "ipsum"]);
@ -61,7 +65,7 @@ mod tests {
#[test] #[test]
fn no_pipe() -> Result<(), Box<dyn std::error::Error>> { fn no_pipe() -> Result<(), Box<dyn std::error::Error>> {
let input = "foo bar baz"; 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!(remaining, "");
assert_eq!(before_pipe_words, vec!["foo", "bar"]); assert_eq!(before_pipe_words, vec!["foo", "bar"]);
assert_eq!(after_pipe_words, vec!["baz"]); assert_eq!(after_pipe_words, vec!["baz"]);
@ -71,7 +75,7 @@ mod tests {
#[test] #[test]
fn early_pipe() -> Result<(), Box<dyn std::error::Error>> { fn early_pipe() -> Result<(), Box<dyn std::error::Error>> {
let input = "| foo bar baz"; 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!(remaining, "");
assert_eq!(before_pipe_words, Vec::<&str>::new()); assert_eq!(before_pipe_words, Vec::<&str>::new());
assert_eq!(after_pipe_words, vec!["foo", "bar", "baz"]); assert_eq!(after_pipe_words, vec!["foo", "bar", "baz"]);
@ -81,7 +85,7 @@ mod tests {
#[test] #[test]
fn late_pipe() -> Result<(), Box<dyn std::error::Error>> { fn late_pipe() -> Result<(), Box<dyn std::error::Error>> {
let input = "foo bar baz |"; 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!(remaining, "");
assert_eq!(before_pipe_words, vec!["foo", "bar", "baz"]); assert_eq!(before_pipe_words, vec!["foo", "bar", "baz"]);
assert_eq!(after_pipe_words, Vec::<&str>::new()); assert_eq!(after_pipe_words, Vec::<&str>::new());

View File

@ -13,7 +13,7 @@ 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>, pub todo_keyword: Option<(TodoKeywordType, &'s str)>,
// TODO: add todo-type enum // 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>,
@ -32,6 +32,12 @@ pub enum DocumentElement<'s> {
Section(Section<'s>), Section(Section<'s>),
} }
#[derive(Debug)]
pub enum TodoKeywordType {
Todo,
Done,
}
impl<'s> Source<'s> for Document<'s> { impl<'s> Source<'s> for Document<'s> {
fn get_source(&'s self) -> &'s str { fn get_source(&'s self) -> &'s str {
self.source self.source

View File

@ -8,6 +8,7 @@ pub use document::Document;
pub use document::DocumentElement; pub use document::DocumentElement;
pub use document::Heading; pub use document::Heading;
pub use document::Section; pub use document::Section;
pub use document::TodoKeywordType;
pub use element::Element; pub use element::Element;
pub use greater_element::Drawer; pub use greater_element::Drawer;
pub use greater_element::DynamicBlock; pub use greater_element::DynamicBlock;