use std::str::FromStr; use super::compare_field::compare_property_list_of_quoted_string; use super::compare_field::compare_property_quoted_string; use super::compare_field::ComparePropertiesResult; use super::diff::DiffEntry; use super::diff::DiffStatus; use super::elisp_fact::GetElispFact; use super::sexp::Token; use crate::compare::diff::compare_ast_node; use crate::compare::sexp::unquote; use crate::types::AffiliatedKeywordValue; use crate::types::AstNode; use crate::types::GetAffiliatedKeywords; use crate::types::GetStandardProperties; use crate::types::StandardProperties; /// Check if the child string slice is a slice of the parent string slice. fn is_slice_of(parent: &str, child: &str) -> bool { let parent_start = parent.as_ptr() as usize; let parent_end = parent_start + parent.len(); let child_start = child.as_ptr() as usize; let child_end = child_start + child.len(); child_start >= parent_start && child_end <= parent_end } /// Get the byte offset into source that the rust object exists at. /// /// These offsets are zero-based unlike the elisp ones. fn get_rust_byte_offsets<'b, 's, S: StandardProperties<'s> + ?Sized>( original_document: &'s str, rust_ast_node: &'b S, ) -> (usize, usize) { let rust_object_source = rust_ast_node.get_source(); debug_assert!(is_slice_of(original_document, rust_object_source)); let offset = rust_object_source.as_ptr() as usize - original_document.as_ptr() as usize; let end = offset + rust_object_source.len(); (offset, end) } pub(crate) fn compare_standard_properties< 'b, 's, S: GetStandardProperties<'s> + GetElispFact<'s> + ?Sized, >( original_document: &'s str, emacs: &'b Token<'s>, rust: &'b S, ) -> Result<(), Box> { assert_name(emacs, rust.get_elisp_fact().get_elisp_name())?; assert_bounds(original_document, emacs, rust.get_standard_properties())?; Ok(()) } pub(crate) fn assert_name<'b, 's, S: AsRef>( emacs: &'b Token<'s>, name: S, ) -> Result<(), Box> { let name = name.as_ref(); let children = emacs.as_list()?; let first_child = children .first() .ok_or("Should have at least one child.")? .as_atom()?; if first_child != name { Err(format!( "AST node name mismatch. Expected a (rust) {expected} cell, but found a (emacs) {found} cell.", expected = name, found = first_child ))?; } Ok(()) } /// Assert that the character ranges defined by upstream org-mode's :standard-properties match the slices in Organic's StandardProperties. /// /// This does **not** handle plain text because plain text is a special case. pub(crate) fn assert_bounds<'b, 's, S: StandardProperties<'s> + ?Sized>( original_document: &'s str, emacs: &'b Token<'s>, rust: &'b S, ) -> Result<(), Box> { let standard_properties = get_emacs_standard_properties(emacs)?; // 1-based let (begin, end) = ( standard_properties .begin .ok_or("Token should have a begin.")?, standard_properties.end.ok_or("Token should have an end.")?, ); let (rust_begin, rust_end) = get_rust_byte_offsets(original_document, rust); // 0-based let rust_begin_char_offset = (&original_document[..rust_begin]).chars().count() + 1; // 1-based let rust_end_char_offset = rust_begin_char_offset + (&original_document[rust_begin..rust_end]).chars().count(); // 1-based if rust_begin_char_offset != begin || rust_end_char_offset != end { Err(format!("Rust bounds (in chars) ({rust_begin}, {rust_end}) do not match emacs bounds ({emacs_begin}, {emacs_end})", rust_begin = rust_begin_char_offset, rust_end = rust_end_char_offset, emacs_begin=begin, emacs_end=end))?; } Ok(()) } struct EmacsStandardProperties { begin: Option, #[allow(dead_code)] post_affiliated: Option, #[allow(dead_code)] contents_begin: Option, #[allow(dead_code)] contents_end: Option, end: Option, #[allow(dead_code)] post_blank: Option, } fn get_emacs_standard_properties<'b, 's>( emacs: &'b Token<'s>, ) -> Result> { 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 standard_properties = attributes_map.get(":standard-properties"); Ok(if standard_properties.is_some() { let mut std_props = standard_properties .expect("if statement proves its Some") .as_vector()? .into_iter(); let begin = maybe_token_to_usize(std_props.next())?; let post_affiliated = maybe_token_to_usize(std_props.next())?; let contents_begin = maybe_token_to_usize(std_props.next())?; let contents_end = maybe_token_to_usize(std_props.next())?; let end = maybe_token_to_usize(std_props.next())?; let post_blank = maybe_token_to_usize(std_props.next())?; EmacsStandardProperties { begin, post_affiliated, contents_begin, contents_end, end, post_blank, } } else { let begin = maybe_token_to_usize(attributes_map.get(":begin").map(|token| *token))?; let end = maybe_token_to_usize(attributes_map.get(":end").map(|token| *token))?; let contents_begin = maybe_token_to_usize(attributes_map.get(":contents-begin").map(|token| *token))?; let contents_end = maybe_token_to_usize(attributes_map.get(":contents-end").map(|token| *token))?; let post_blank = maybe_token_to_usize(attributes_map.get(":post-blank").map(|token| *token))?; let post_affiliated = maybe_token_to_usize(attributes_map.get(":post-affiliated").map(|token| *token))?; EmacsStandardProperties { begin, post_affiliated, contents_begin, contents_end, end, post_blank, } }) } fn maybe_token_to_usize( token: Option<&Token<'_>>, ) -> Result, Box> { Ok(token .map(|token| token.as_atom()) .map_or(Ok(None), |r| r.map(Some))? .map(|val| { if val == "nil" { None } else { Some(val.parse::()) } }) .flatten() // Outer option is whether or not the param exists, inner option is whether or not it is nil .map_or(Ok(None), |r| r.map(Some))?) } /// Get a named property from the emacs token. /// /// Returns Ok(None) if value is nil or absent. pub(crate) fn get_property<'b, 's, 'x>( emacs: &'b Token<'s>, key: &'x str, ) -> 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 prop = attributes_map.get(key).map(|token| *token); match prop.map(|token| token.as_atom()) { Some(Ok("nil")) => return Ok(None), _ => {} }; Ok(prop) } /// Get a named property containing an unquoted atom from the emacs token. /// /// Returns None if key is not found. pub(crate) fn get_property_unquoted_atom<'b, 's, 'x>( emacs: &'b Token<'s>, key: &'x str, ) -> Result, Box> { Ok(get_property(emacs, key)? .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<'b, 's, 'x>( emacs: &'b 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))?) } /// Get a named property containing an unquoted numeric value. /// /// Returns None if key is not found. pub(crate) fn get_property_numeric<'b, 's, 'x, N: FromStr>( emacs: &'b Token<'s>, key: &'x str, ) -> Result, Box> where ::Err: std::error::Error, ::Err: 's, { let unparsed_string = get_property(emacs, key)? .map(Token::as_atom) .map_or(Ok(None), |r| r.map(Some))?; let parsed_number = unparsed_string .map(|val| val.parse::()) .map_or(Ok(None), |r| r.map(Some))?; Ok(parsed_number) } pub(crate) fn compare_children<'b, 's, 'x, RC>( source: &'s str, emacs: &'b Token<'s>, rust_children: &'x Vec, child_status: &mut Vec>, this_status: &mut DiffStatus, message: &mut Option, ) -> Result<(), Box> where AstNode<'b, 's>: From<&'x RC>, { let emacs_children = emacs.as_list()?; let emacs_children_length = emacs_children.len() - 2; if emacs_children_length != rust_children.len() { *this_status = DiffStatus::Bad; *message = Some(format!( "Child length mismatch (emacs != rust) {:?} != {:?}", emacs_children_length, rust_children.len() )); } for (emacs_child, rust_child) in emacs_children.iter().skip(2).zip(rust_children.iter()) { child_status.push(compare_ast_node(source, emacs_child, rust_child.into())?); } Ok(()) } pub(crate) fn compare_children_iter<'b, 's, RC, RI: Iterator + ExactSizeIterator>( source: &'s str, emacs: &'b Token<'s>, rust_children: RI, child_status: &mut Vec>, this_status: &mut DiffStatus, message: &mut Option, ) -> Result<(), Box> where AstNode<'b, 's>: From, { let emacs_children = emacs.as_list()?; let emacs_children_length = emacs_children.len() - 2; if emacs_children_length != rust_children.len() { *this_status = DiffStatus::Bad; *message = Some(format!( "Child length mismatch (emacs != rust) {:?} != {:?}", emacs_children_length, rust_children.len() )); } for (emacs_child, rust_child) in emacs_children.iter().skip(2).zip(rust_children) { child_status.push(compare_ast_node(source, emacs_child, rust_child.into())?); } Ok(()) } pub(crate) fn assert_no_children<'b, 's>( emacs: &'b Token<'s>, this_status: &mut DiffStatus, message: &mut Option, ) -> Result<(), Box> { let emacs_children_length = emacs.as_list()?.len(); // 2, one for the name of the node and one for the properties. Children would come after that. if emacs_children_length != 2 { *this_status = DiffStatus::Bad; *message = Some(format!( "Should have no children but emacs has {:?} children.", emacs_children_length - 2, )); } Ok(()) } pub(crate) fn compare_additional_properties<'b, 's, RK, RV, RI>( emacs: &'b Token<'s>, rust_children: RI, ) -> Result, Box> where RK: AsRef, RV: AsRef, RI: Iterator + ExactSizeIterator, { for (rust_key, rust_value) in rust_children { let rust_key = rust_key.as_ref(); let rust_value = rust_value.as_ref(); let emacs_value = get_property_quoted_string(emacs, rust_key)?; if Some(rust_value) != emacs_value.as_ref().map(String::as_str) { let this_status = DiffStatus::Bad; let message = Some(format!( "{} mismatch (emacs != rust) {:?} != {:?}", rust_key, emacs_value, rust_value )); return Ok(ComparePropertiesResult::SelfChange(this_status, message)); } } Ok(ComparePropertiesResult::NoChange) } pub(crate) fn compare_affiliated_keywords<'b, 's, GAK>( source: &'s str, emacs: &'b Token<'s>, rust: &'b GAK, ) -> Result>, Box> where GAK: GetAffiliatedKeywords<'s>, { let mut ret = Vec::new(); let affiliated_keywords = rust.get_affiliated_keywords(); for (rust_name, rust_value) in affiliated_keywords.keywords.iter() { let emacs_property_name = format!(":{}", rust_name); match rust_value { AffiliatedKeywordValue::SingleString(rust_value) => { let diff = compare_property_quoted_string( source, emacs, rust, emacs_property_name.as_str(), |_| Some(*rust_value), )?; ret.push(diff); } AffiliatedKeywordValue::ListOfStrings(rust_value) => { let diff = compare_property_list_of_quoted_string( source, emacs, rust, emacs_property_name.as_str(), |_| Some(rust_value.iter()), )?; ret.push(diff); } AffiliatedKeywordValue::ListOfListsOfObjects(rust_value) => { // foo } }; } Ok(ret) } pub(crate) fn affiliated_keywords_names<'s, GAK>(rust: &'s GAK) -> impl Iterator + 's where GAK: GetAffiliatedKeywords<'s>, { rust.get_affiliated_keywords() .keywords .keys() .map(|k| format!(":{}", k)) }