diff --git a/src/compare/diff.rs b/src/compare/diff.rs index c68db815..2a683ff9 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 49d41d79..36945541 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)?;