diff --git a/src/compare/diff.rs b/src/compare/diff.rs index 0bfd6ba..96757e9 100644 --- a/src/compare/diff.rs +++ b/src/compare/diff.rs @@ -9,6 +9,7 @@ use super::sexp::unquote; use super::sexp::Token; use super::util::compare_standard_properties; use super::util::get_property; +use super::util::get_property_quoted_string; use super::util::get_property_unquoted_atom; use crate::types::AngleLink; use crate::types::BabelCall; @@ -51,6 +52,8 @@ use crate::types::Paragraph; use crate::types::PlainLink; use crate::types::PlainList; use crate::types::PlainListItem; +use crate::types::PlainListItemCounter; +use crate::types::PlainListItemPreBlank; use crate::types::PlainListType; use crate::types::PlainText; use crate::types::Planning; @@ -753,6 +756,7 @@ fn compare_plain_list<'s>( let mut this_status = DiffStatus::Good; let mut message = None; + // Compare type // :type is an unquoted atom of either descriptive, ordered, or unordered let list_type = get_property_unquoted_atom(emacs, ":type")?; match (list_type, &rust.list_type) { @@ -830,7 +834,40 @@ fn compare_plain_list_item<'s>( .collect::, _>>()?; child_status.push(artificial_diff_scope("contents", contents_status)?); - // TODO: Compare :bullet :counter :pre-blank + // Compare bullet + let bullet = get_property_quoted_string(emacs, ":bullet")? + .ok_or("Plain list items must have a :bullet.")?; + if bullet != rust.bullet { + this_status = DiffStatus::Bad; + message = Some(format!( + "Bullet mismatch (emacs != rust) {:?} != {:?}", + bullet, rust.bullet + )); + } + + // Compare counter + let counter = get_property_unquoted_atom(emacs, ":counter")?; + let counter: Option = counter + .map(|val| val.parse()) + .map_or(Ok(None), |r| r.map(Some))?; + match (counter, rust.counter) { + (None, None) => {} + (None, Some(_)) | (Some(_), None) => { + this_status = DiffStatus::Bad; + message = Some(format!( + "Counter mismatch (emacs != rust) {:?} != {:?}", + counter, rust.counter + )); + } + (Some(e), Some(r)) if e != r => { + this_status = DiffStatus::Bad; + message = Some(format!( + "Counter mismatch (emacs != rust) {:?} != {:?}", + counter, rust.counter + )); + } + (Some(_), Some(_)) => {} + }; // Compare checkbox let checkbox = get_property(emacs, ":checkbox")? @@ -851,6 +888,20 @@ fn compare_plain_list_item<'s>( } }; + // Compare pre-blank + // :pre-blank appears to count the line breaks between "::" and the contents in a descriptive list. Oddly enough it does not count the spaces so I'm not quite sure what the value is. + let pre_blank = get_property_unquoted_atom(emacs, ":pre-blank")?; + let pre_blank: Option = pre_blank + .map(|val| val.parse()) + .map_or(Ok(None), |r| r.map(Some))?; + if pre_blank.unwrap_or(0) != rust.pre_blank { + this_status = DiffStatus::Bad; + message = Some(format!( + "Pre-blank mismatch (emacs != rust) {:?} != {:?}", + pre_blank, rust.pre_blank + )); + } + Ok(DiffResult { status: this_status, name: rust.get_elisp_name(), diff --git a/src/compare/util.rs b/src/compare/util.rs index f52ef47..a706804 100644 --- a/src/compare/util.rs +++ b/src/compare/util.rs @@ -1,5 +1,6 @@ use super::elisp_fact::GetElispFact; use super::sexp::Token; +use crate::compare::sexp::unquote; use crate::types::GetStandardProperties; use crate::types::StandardProperties; @@ -202,3 +203,17 @@ pub(crate) fn get_property_unquoted_atom<'s, 'x>( .map(Token::as_atom) .map_or(Ok(None), |r| r.map(Some))?) } + +/// Get a named property containing an quoted string from the emacs token. +/// +/// Returns None if key is not found. +pub(crate) fn get_property_quoted_string<'s, 'x>( + emacs: &'s Token<'s>, + key: &'x str, +) -> Result, Box> { + Ok(get_property(emacs, key)? + .map(Token::as_atom) + .map_or(Ok(None), |r| r.map(Some))? + .map(unquote) + .map_or(Ok(None), |r| r.map(Some))?) +} diff --git a/src/parser/org_source.rs b/src/parser/org_source.rs index 252a5fc..8e3a9a6 100644 --- a/src/parser/org_source.rs +++ b/src/parser/org_source.rs @@ -78,6 +78,12 @@ impl<'s> OrgSource<'s> { self.slice(..(other.start - self.start)) } + pub(crate) fn get_until_end_of(&self, other: OrgSource<'s>) -> OrgSource<'s> { + debug_assert!(other.start >= self.start); + debug_assert!(other.end <= self.end); + self.slice(..(other.end - self.start)) + } + pub(crate) fn get_start_of_line(&self) -> OrgSource<'s> { let skipped_text = self.text_since_line_break(); let mut bracket_depth = self.bracket_depth; diff --git a/src/parser/plain_list.rs b/src/parser/plain_list.rs index 3b10d55..61f8b6b 100644 --- a/src/parser/plain_list.rs +++ b/src/parser/plain_list.rs @@ -44,6 +44,8 @@ use crate::types::IndentationLevel; use crate::types::Object; use crate::types::PlainList; use crate::types::PlainListItem; +use crate::types::PlainListItemCounter; +use crate::types::PlainListItemPreBlank; use crate::types::PlainListType; #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] @@ -54,12 +56,12 @@ pub(crate) fn detect_plain_list<'b, 'g, 'r, 's>( if verify( tuple(( start_of_line, - space0, + parser_with_context!(indentation_level)(context), parser_with_context!(bullet)(context), alt((space1, line_ending, eof)), )), - |(_start, indent, (_bullet_type, bull), _after_whitespace)| { - Into::<&str>::into(bull) != "*" || indent.len() > 0 + |(_start, (indent_level, _), (_bullet_type, bull), _after_whitespace)| { + !Into::<&str>::into(bull).starts_with("*") || *indent_level > 0 }, )(input) .is_ok() @@ -163,11 +165,12 @@ fn plain_list_item<'b, 'g, 'r, 's>( let (remaining, (indent_level, _leading_whitespace)) = indentation_level(context, input)?; let (remaining, (bullet_type, bull)) = verify( parser_with_context!(bullet)(context), - |(_bullet_type, bull)| Into::<&str>::into(bull) != "*" || indent_level > 0, + |(_bullet_type, bull)| !Into::<&str>::into(bull).starts_with("*") || indent_level > 0, )(remaining)?; - let (remaining, _maybe_counter_set) = + let (remaining, maybe_counter_set) = opt(tuple((space1, tag("[@"), counter_set_value, tag("]"))))(remaining)?; + let maybe_counter_set = maybe_counter_set.map(|(_, _, val, _)| val); let (remaining, maybe_checkbox) = opt(tuple((space1, item_checkbox)))(remaining)?; @@ -213,10 +216,12 @@ fn plain_list_item<'b, 'g, 'r, 's>( source: source.into(), indentation: indent_level, bullet: bull.into(), + counter: maybe_counter_set, checkbox: None, tag: maybe_tag .map(|(_ws, item_tag)| item_tag) .unwrap_or(Vec::new()), + pre_blank: 0, children: Vec::new(), }, ), @@ -224,7 +229,11 @@ fn plain_list_item<'b, 'g, 'r, 's>( } Err(_) => {} }; - let (remaining, _ws) = item_tag_post_gap(&parser_context, remaining)?; + let (remaining, pre_blank) = item_tag_post_gap(&parser_context, remaining)?; + let pre_blank = Into::<&str>::into(pre_blank) + .bytes() + .filter(|b| *b == b'\n') + .count(); let (mut remaining, (mut children, _exit_contents)) = many_till( include_input(parser_with_context!(element(true))(&parser_context)), @@ -256,12 +265,15 @@ fn plain_list_item<'b, 'g, 'r, 's>( source: source.into(), indentation: indent_level, bullet: bull.into(), + counter: maybe_counter_set, checkbox: maybe_checkbox.map(|(_, (checkbox_type, source))| { (checkbox_type, Into::<&str>::into(source)) }), tag: maybe_tag .map(|(_ws, item_tag)| item_tag) .unwrap_or(Vec::new()), + pre_blank: PlainListItemPreBlank::try_from(pre_blank) + .expect("pre-blank cannot be larger than 2."), children: children.into_iter().map(|(_start, item)| item).collect(), }, ), @@ -279,18 +291,23 @@ fn bullet<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, (BulletType, OrgSource<'s>)> { - alt(( - map(tag("*"), |bull| (BulletType::Unordered, bull)), - map(tag("-"), |bull| (BulletType::Unordered, bull)), - map(tag("+"), |bull| (BulletType::Unordered, bull)), - map( - recognize(tuple(( - parser_with_context!(counter)(context), - alt((tag("."), tag(")"))), - ))), - |bull| (BulletType::Ordered, bull), - ), - ))(input) + let (remaining, ((bullet_type, _without_space), peek_trailing_space)) = tuple(( + alt(( + map(tag("*"), |bull| (BulletType::Unordered, bull)), + map(tag("-"), |bull| (BulletType::Unordered, bull)), + map(tag("+"), |bull| (BulletType::Unordered, bull)), + map( + recognize(tuple(( + parser_with_context!(counter)(context), + alt((tag("."), tag(")"))), + ))), + |bull| (BulletType::Ordered, bull), + ), + )), + peek(space0), + ))(input)?; + let with_space = input.get_until_end_of(peek_trailing_space); + Ok((remaining, (bullet_type, with_space))) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] @@ -311,12 +328,25 @@ fn counter<'b, 'g, 'r, 's>( } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -fn counter_set_value<'s>(input: OrgSource<'s>) -> Res, OrgSource<'s>> { +fn counter_set_value<'s>(input: OrgSource<'s>) -> Res, PlainListItemCounter> { alt(( - recognize(one_of( - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", - )), - digit1, + map( + one_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"), + |letter| { + let num = match letter { + 'a'..='z' => (letter as u32) - ('a' as u32) + 1, + 'A'..='Z' => (letter as u32) - ('A' as u32) + 1, + _ => unreachable!(), + }; + PlainListItemCounter::try_from(num) + .expect("Counter set value should be between 1 and 26 inclusive.") + }, + ), + map(digit1, |num: OrgSource<'_>| { + Into::<&str>::into(num) + .parse() + .expect("digit1 must parse to a number.") + }), ))(input) } diff --git a/src/types/greater_element.rs b/src/types/greater_element.rs index bfe469a..c63b58c 100644 --- a/src/types/greater_element.rs +++ b/src/types/greater_element.rs @@ -26,11 +26,16 @@ pub struct PlainListItem<'s> { pub source: &'s str, pub indentation: IndentationLevel, pub bullet: &'s str, + pub counter: Option, pub checkbox: Option<(CheckboxType, &'s str)>, pub tag: Vec>, + pub pre_blank: PlainListItemPreBlank, pub children: Vec>, } +pub type PlainListItemCounter = u16; +pub type PlainListItemPreBlank = u8; + #[derive(Debug)] pub enum CheckboxType { On, diff --git a/src/types/mod.rs b/src/types/mod.rs index ca31e24..7976ecd 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -24,6 +24,8 @@ pub use greater_element::IndentationLevel; pub use greater_element::NodeProperty; pub use greater_element::PlainList; pub use greater_element::PlainListItem; +pub use greater_element::PlainListItemCounter; +pub use greater_element::PlainListItemPreBlank; pub use greater_element::PlainListType; pub use greater_element::PropertyDrawer; pub use greater_element::Table;