From d5ea650b9626aaf65bbf6c2d479c4ea40d84aa09 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Fri, 25 Aug 2023 05:36:57 -0400 Subject: [PATCH 1/5] Add a test for dynamic blocks. --- .../greater_element/dynamic_block/simple.org | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 org_mode_samples/greater_element/dynamic_block/simple.org diff --git a/org_mode_samples/greater_element/dynamic_block/simple.org b/org_mode_samples/greater_element/dynamic_block/simple.org new file mode 100644 index 0000000..a21ce8a --- /dev/null +++ b/org_mode_samples/greater_element/dynamic_block/simple.org @@ -0,0 +1,23 @@ +#+BEGIN: clocktable :scope file :maxlevel 2 +#+CAPTION: Clock summary at [2023-08-25 Fri 05:34] +| Headline | Time | +|--------------+--------| +| *Total time* | *0:00* | +#+END: + +#+BEGIN: columnview :hlines 1 :id global +| ITEM | TODO | PRIORITY | TAGS | +|-------+------+----------+------------------------------| +| Foo | | B | | +|-------+------+----------+------------------------------| +| Bar | TODO | B | | +|-------+------+----------+------------------------------| +| Baz | | B | :thisisatag: | +| Lorem | | B | :thisshouldinheritfromabove: | +| Ipsum | | B | | +#+END: +* Foo +* TODO Bar +* Baz :thisisatag: +** Lorem :thisshouldinheritfromabove: +*** Ipsum From 2d4e54845b6e9b035161f329d9860f95dc53f258 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Fri, 25 Aug 2023 06:13:29 -0400 Subject: [PATCH 2/5] Add support for parsing tags in headlines. --- .../greater_element/dynamic_block/simple.org | 6 ++-- src/compare/diff.rs | 2 ++ src/parser/document.rs | 30 ++++++++++++++++--- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/org_mode_samples/greater_element/dynamic_block/simple.org b/org_mode_samples/greater_element/dynamic_block/simple.org index a21ce8a..1034c5e 100644 --- a/org_mode_samples/greater_element/dynamic_block/simple.org +++ b/org_mode_samples/greater_element/dynamic_block/simple.org @@ -14,10 +14,12 @@ |-------+------+----------+------------------------------| | Baz | | B | :thisisatag: | | Lorem | | B | :thisshouldinheritfromabove: | -| Ipsum | | B | | +| Ipsum | | B | :multiple:tags: | #+END: * Foo * TODO Bar * Baz :thisisatag: ** Lorem :thisshouldinheritfromabove: -*** Ipsum +*** Ipsum :multiple:tags: +* Dolar :: +* cat :dog: bat diff --git a/src/compare/diff.rs b/src/compare/diff.rs index 4b18716..c68db81 100644 --- a/src/compare/diff.rs +++ b/src/compare/diff.rs @@ -348,6 +348,8 @@ fn compare_heading<'s>( child_status.push(compare_object(source, emacs_child, rust_child)?); } + // TODO: Compare tags, todo-keyword, level, priority + for (emacs_child, rust_child) in children.iter().skip(2).zip(rust.children.iter()) { match rust_child { DocumentElement::Heading(rust_heading) => { diff --git a/src/parser/document.rs b/src/parser/document.rs index 957fff2..dd4e117 100644 --- a/src/parser/document.rs +++ b/src/parser/document.rs @@ -1,6 +1,8 @@ use nom::branch::alt; use nom::bytes::complete::tag; +use nom::character::complete::anychar; use nom::character::complete::line_ending; +use nom::character::complete::space0; use nom::character::complete::space1; use nom::combinator::eof; use nom::combinator::map; @@ -12,6 +14,7 @@ use nom::multi::many0; use nom::multi::many1; use nom::multi::many1_count; use nom::multi::many_till; +use nom::multi::separated_list1; use nom::sequence::tuple; use super::element::Element; @@ -297,26 +300,45 @@ fn headline<'r, 's>( let parser_context = context.with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode { class: ExitClass::Document, - exit_matcher: &headline_end, + exit_matcher: &headline_title_end, })); let standard_set_object_matcher = parser_with_context!(standard_set_object)(&parser_context); - let (remaining, (_sol, star_count, ws, title, _line_ending)) = tuple(( + let (remaining, (_sol, star_count, ws, title, maybe_tags, _ws, _line_ending)) = tuple(( start_of_line, many1_count(tag("*")), space1, many1(standard_set_object_matcher), + opt(tuple((space0, tags))), + space0, alt((line_ending, eof)), ))(input)?; Ok((remaining, (star_count, ws, title))) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -fn headline_end<'r, 's>( +fn headline_title_end<'r, 's>( _context: Context<'r, 's>, input: OrgSource<'s>, ) -> Res, OrgSource<'s>> { - line_ending(input) + recognize(tuple(( + opt(tuple((space0, tags, space0))), + alt((line_ending, eof)), + )))(input) +} + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +fn tags<'s>(input: OrgSource<'s>) -> Res, Vec>> { + let (remaining, (_open, tags, _close)) = + tuple((tag(":"), separated_list1(tag(":"), single_tag), tag(":")))(input)?; + Ok((remaining, tags)) +} + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +fn single_tag<'r, 's>(input: OrgSource<'s>) -> Res, OrgSource<'s>> { + recognize(many1(verify(anychar, |c| { + c.is_alphanumeric() || "_@#%".contains(*c) + })))(input) } impl<'s> Document<'s> { From be6197e4c7e7d749115d7e639bb848705bb07670 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Fri, 25 Aug 2023 06:20:06 -0400 Subject: [PATCH 3/5] Store the tags in the heading. --- src/parser/document.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/parser/document.rs b/src/parser/document.rs index dd4e117..5112b4e 100644 --- a/src/parser/document.rs +++ b/src/parser/document.rs @@ -54,6 +54,7 @@ pub struct Heading<'s> { pub source: &'s str, pub stars: usize, pub title: Vec>, + pub tags: Vec<&'s str>, pub children: Vec>, } @@ -270,7 +271,7 @@ fn heading<'r, 's>( input: OrgSource<'s>, ) -> Res, Heading<'s>> { not(|i| context.check_exit_matcher(i))(input)?; - let (remaining, (star_count, _ws, title)) = headline(context, input)?; + let (remaining, (star_count, _ws, 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 +288,7 @@ fn heading<'r, 's>( source: source.into(), stars: star_count, title, + tags: heading_tags, children, }, )) @@ -296,7 +298,7 @@ fn heading<'r, 's>( fn headline<'r, 's>( context: Context<'r, 's>, input: OrgSource<'s>, -) -> Res, (usize, OrgSource<'s>, Vec>)> { +) -> Res, (usize, OrgSource<'s>, Vec>, Vec<&'s str>)> { let parser_context = context.with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode { class: ExitClass::Document, @@ -313,7 +315,21 @@ fn headline<'r, 's>( space0, alt((line_ending, eof)), ))(input)?; - Ok((remaining, (star_count, ws, title))) + Ok(( + remaining, + ( + star_count, + ws, + title, + maybe_tags + .map(|(_ws, tags)| { + tags.into_iter() + .map(|single_tag| Into::<&str>::into(single_tag)) + .collect() + }) + .unwrap_or(Vec::new()), + ), + )) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] From 9cc5e63c1b0a30f4907e0f307fcc98878edc5cce Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Fri, 25 Aug 2023 06:46:00 -0400 Subject: [PATCH 4/5] Compare heading tags. --- src/compare/diff.rs | 48 +++++++++++++++++++++++++++-- src/parser/sexp.rs | 74 ++++++++++++++++++++++----------------------- 2 files changed, 81 insertions(+), 41 deletions(-) diff --git a/src/compare/diff.rs b/src/compare/diff.rs index c68db81..2a683ff 100644 --- a/src/compare/diff.rs +++ b/src/compare/diff.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use super::util::assert_bounds; use super::util::assert_name; use crate::parser::sexp::Token; @@ -58,6 +60,7 @@ 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 { @@ -323,6 +326,7 @@ fn compare_heading<'s>( let children = emacs.as_list()?; let mut child_status = Vec::new(); let mut this_status = DiffStatus::Good; + let mut message = None; let emacs_name = "headline"; if assert_name(emacs, emacs_name).is_err() { this_status = DiffStatus::Bad; @@ -347,8 +351,19 @@ 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 tags, todo-keyword, level, priority + // TODO: Compare todo-keyword, level, priority for (emacs_child, rust_child) in children.iter().skip(2).zip(rust.children.iter()) { match rust_child { @@ -364,11 +379,38 @@ fn compare_heading<'s>( Ok(DiffResult { status: this_status, name: emacs_name.to_owned(), - message: None, + message, children: child_status, }) } +fn get_tags_from_heading<'s>( + emacs: &'s Token<'s>, +) -> Result, Box> { + 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 tags = attributes_map + .get(":tags") + .ok_or("Missing :tags attribute.")?; + match tags.as_atom() { + Ok("nil") => { + return Ok(HashSet::new()); + } + Ok(val) => panic!("Unexpected value for tags: {:?}", val), + Err(_) => {} + }; + let tags = { + let tags = tags.as_list()?; + let strings = tags.iter().map(Token::as_atom).collect::, _>>()?; + strings.into_iter().map(unquote).collect::, _>>()? + }; + Ok(tags) +} + fn compare_paragraph<'s>( source: &'s str, emacs: &'s Token<'s>, @@ -1027,7 +1069,7 @@ fn compare_plain_text<'s>( rust.source.len() )); } - let unquoted_text = text.unquote()?; + let unquoted_text = unquote(text.text)?; if unquoted_text != rust.source { this_status = DiffStatus::Bad; message = Some(format!( diff --git a/src/parser/sexp.rs b/src/parser/sexp.rs index 49d41d7..3694554 100644 --- a/src/parser/sexp.rs +++ b/src/parser/sexp.rs @@ -35,44 +35,6 @@ pub struct TextWithProperties<'s> { pub properties: Vec>, } -impl<'s> TextWithProperties<'s> { - pub fn unquote(&self) -> Result> { - let mut out = String::with_capacity(self.text.len()); - if !self.text.starts_with(r#"""#) { - return Err("Quoted text does not start with quote.".into()); - } - if !self.text.ends_with(r#"""#) { - return Err("Quoted text does not end with quote.".into()); - } - let interior_text = &self.text[1..(self.text.len() - 1)]; - let mut state = ParseState::Normal; - for current_char in interior_text.chars().into_iter() { - state = match (state, current_char) { - (ParseState::Normal, '\\') => ParseState::Escape, - (ParseState::Normal, _) => { - out.push(current_char); - ParseState::Normal - } - (ParseState::Escape, 'n') => { - out.push('\n'); - ParseState::Normal - } - (ParseState::Escape, '\\') => { - out.push('\\'); - ParseState::Normal - } - (ParseState::Escape, '"') => { - out.push('"'); - ParseState::Normal - } - _ => todo!(), - }; - } - - Ok(out) - } -} - enum ParseState { Normal, Escape, @@ -133,6 +95,42 @@ impl<'s> Token<'s> { } } +pub fn unquote(text: &str) -> Result> { + let mut out = String::with_capacity(text.len()); + if !text.starts_with(r#"""#) { + return Err("Quoted text does not start with quote.".into()); + } + if !text.ends_with(r#"""#) { + return Err("Quoted text does not end with quote.".into()); + } + let interior_text = &text[1..(text.len() - 1)]; + let mut state = ParseState::Normal; + for current_char in interior_text.chars().into_iter() { + state = match (state, current_char) { + (ParseState::Normal, '\\') => ParseState::Escape, + (ParseState::Normal, _) => { + out.push(current_char); + ParseState::Normal + } + (ParseState::Escape, 'n') => { + out.push('\n'); + ParseState::Normal + } + (ParseState::Escape, '\\') => { + out.push('\\'); + ParseState::Normal + } + (ParseState::Escape, '"') => { + out.push('"'); + ParseState::Normal + } + _ => todo!(), + }; + } + + Ok(out) +} + #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] pub fn sexp_with_padding<'s>(input: &'s str) -> Res<&'s str, Token<'s>> { let (remaining, _) = multispace0(input)?; From 3e143796f7b1573e0b56c53f5f68de64f84f45c2 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Sun, 27 Aug 2023 15:56:08 -0400 Subject: [PATCH 5/5] Compare heading todo keywords. This only handles the default case where the only valid TODO keywords are TODO and DONE. --- src/compare/diff.rs | 65 ++++++++++++++++++++++++++++++++---------- src/parser/document.rs | 31 ++++++++++++++++++-- 2 files changed, 78 insertions(+), 18 deletions(-) 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))