diff --git a/src/compare/compare_field.rs b/src/compare/compare_field.rs index 3d0daad..a4612b7 100644 --- a/src/compare/compare_field.rs +++ b/src/compare/compare_field.rs @@ -15,6 +15,7 @@ use super::util::get_property_unquoted_atom; use crate::types::AstNode; use crate::types::CharOffsetInLine; use crate::types::LineNumber; +use crate::types::Object; use crate::types::RetainLabels; use crate::types::SwitchNumberLines; @@ -410,6 +411,80 @@ where Ok(ComparePropertiesResult::NoChange) } +/// Special compare used for affiliate keywords that are parsed as objects. +/// +/// Org-mode seems to store these as a 3-deep list: +/// - Outer list with 1 element per #+caption keyword (or other parsed keyword). +/// - Middle list that seems to always have 1 element. +/// - Inner list of the objects from each #+caption keyword (or other parsed keyword). +pub(crate) fn compare_property_list_of_list_of_list_of_ast_nodes< + 'b, + 's, + 'x: 'b + 's, + R, + RG: Fn(R) -> Option<&'b Vec>>>, +>( + source: &'s str, + emacs: &'b Token<'s>, + rust_node: R, + emacs_field: &'x str, + rust_value_getter: RG, +) -> Result, Box> { + let rust_value = rust_value_getter(rust_node); + let value = get_property(emacs, emacs_field)? + .map(Token::as_list) + .map_or(Ok(None), |r| r.map(Some))?; + let (value, rust_value) = match (value, rust_value) { + (None, None) => { + return Ok(ComparePropertiesResult::NoChange); + } + (None, Some(_)) | (Some(_), None) => { + let this_status = DiffStatus::Bad; + let message = Some(format!( + "{} mismatch (emacs != rust) {:?} != {:?}", + emacs_field, value, rust_value + )); + return Ok(ComparePropertiesResult::SelfChange(this_status, message)); + } + (Some(value), Some(rust_value)) if value.len() != rust_value.len() => { + let this_status = DiffStatus::Bad; + let message = Some(format!( + "{} mismatch (emacs != rust) {:?} != {:?}", + emacs_field, value, rust_value + )); + return Ok(ComparePropertiesResult::SelfChange(this_status, message)); + } + (Some(value), Some(rust_value)) => (value, rust_value), + }; + + // Iterate the outer lists + for (value, rust_value) in value.iter().zip(rust_value.iter()) { + // Assert the middle list is a length of 1 because I've never seen it any other way. + let value = value.as_list()?; + if value.len() != 1 { + let this_status = DiffStatus::Bad; + let message = Some(format!( + "{} mismatch (emacs != rust) {:?} != {:?}", + emacs_field, value, rust_value + )); + return Ok(ComparePropertiesResult::SelfChange(this_status, message)); + } + // Drill past the middle list to the inner list. + let value = value + .first() + .expect("The above if-statement asserts this exists."); + let value = value.as_list()?; + // Compare inner lists + let mut child_status: Vec> = Vec::with_capacity(rust_value.len()); + for (e, r) in value.iter().zip(rust_value) { + child_status.push(compare_ast_node(source, e, r.into())?); + } + let diff_scope = artificial_diff_scope(emacs_field, child_status)?; + return Ok(ComparePropertiesResult::DiffEntry(diff_scope)); + } + Ok(ComparePropertiesResult::NoChange) +} + pub(crate) fn compare_property_number_lines< 'b, 's, diff --git a/src/compare/util.rs b/src/compare/util.rs index 53aa653..5dcc0ed 100644 --- a/src/compare/util.rs +++ b/src/compare/util.rs @@ -1,5 +1,6 @@ use std::str::FromStr; +use super::compare_field::compare_property_list_of_list_of_list_of_ast_nodes; use super::compare_field::compare_property_list_of_quoted_string; use super::compare_field::compare_property_quoted_string; use super::compare_field::ComparePropertiesResult; @@ -376,7 +377,14 @@ where ret.push(diff); } AffiliatedKeywordValue::ListOfListsOfObjects(rust_value) => { - // foo + let diff = compare_property_list_of_list_of_list_of_ast_nodes( + source, + emacs, + rust, + emacs_property_name.as_str(), + |_| Some(rust_value), + )?; + ret.push(diff); } }; } diff --git a/src/parser/affiliated_keyword.rs b/src/parser/affiliated_keyword.rs index b096116..22cb940 100644 --- a/src/parser/affiliated_keyword.rs +++ b/src/parser/affiliated_keyword.rs @@ -25,6 +25,22 @@ pub(crate) fn parse_affiliated_keywords<'g, 's>( translated_name, AffiliatedKeywordValue::SingleString(kw.value), ); + } else if is_list_of_single_string_keyword(global_settings, translated_name.as_str()) { + let list_of_strings = ret + .entry(translated_name) + .or_insert_with(|| AffiliatedKeywordValue::ListOfStrings(Vec::with_capacity(1))); + match list_of_strings { + AffiliatedKeywordValue::ListOfStrings(list_of_strings) + if list_of_strings.is_empty() => + { + list_of_strings.push(kw.value); + } + AffiliatedKeywordValue::ListOfStrings(list_of_strings) => { + list_of_strings.clear(); + list_of_strings.push(kw.value); + } + _ => panic!("Invalid AffiliatedKeywordValue type."), + } } else if is_list_of_objects_keyword(global_settings, translated_name.as_str()) { let initial_context = ContextElement::document_context(); let initial_context = Context::new(global_settings, List::new(&initial_context)); @@ -71,8 +87,25 @@ fn is_single_string_keyword<'g, 's>( _global_settings: &'g GlobalSettings<'g, 's>, name: &'s str, ) -> bool { - // TODO: Is this defined by an elisp variable? I'm only seeing this done for plot. - name.eq_ignore_ascii_case("plot") + // TODO: Is this defined by an elisp variable? + for single_string_name in ["plot", "name"] { + if name.eq_ignore_ascii_case(single_string_name) { + return true; + } + } + false +} + +fn is_list_of_single_string_keyword<'g, 's>( + global_settings: &'g GlobalSettings<'g, 's>, + name: &'s str, +) -> bool { + for single_string_name in ["results"] { + if name.eq_ignore_ascii_case(single_string_name) { + return true; + } + } + false } fn is_list_of_objects_keyword<'g, 's>(