use std::collections::BTreeMap; use nom::bytes::complete::tag; use nom::bytes::complete::take_until; use nom::character::complete::anychar; use nom::combinator::all_consuming; use nom::combinator::eof; use nom::combinator::map; use nom::combinator::map_parser; use nom::combinator::opt; use nom::combinator::peek; use nom::combinator::recognize; use nom::multi::many0; use nom::multi::many_till; use nom::sequence::tuple; use super::object_parser::standard_set_object; use super::util::confine_context; use crate::context::parser_with_context; use crate::context::Context; use crate::context::ContextElement; use crate::context::GlobalSettings; use crate::context::List; use crate::types::AffiliatedKeywordValue; use crate::types::AffiliatedKeywords; use crate::types::Keyword; pub(crate) fn parse_affiliated_keywords<'g, 's, AK>( global_settings: &'g GlobalSettings<'g, 's>, input: AK, ) -> AffiliatedKeywords<'s> where AK: IntoIterator>, { let mut ret = BTreeMap::new(); for kw in input { let translated_name = translate_name(global_settings, kw.key); let keyword_type = identify_keyword_type(global_settings, translated_name.as_str()); match keyword_type { AffiliatedKeywordType::SingleString => { ret.insert( translated_name, AffiliatedKeywordValue::SingleString(kw.value), ); } AffiliatedKeywordType::ListOfStrings => { 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."), } } AffiliatedKeywordType::OptionalPair => { let (_remaining, optional_string) = opt(all_consuming(map( tuple(( take_until::<_, &str, nom::error::Error<_>>("["), tag("["), recognize(many_till(anychar, peek(tuple((tag("]"), eof))))), tag("]"), eof, )), |(_, _, objects, _, _)| objects, )))(kw.key.into()) .expect("Parser should always succeed."); ret.insert( translated_name, AffiliatedKeywordValue::OptionalPair { optval: optional_string, val: kw.value, }, ); } AffiliatedKeywordType::ObjectTree => { let initial_context = ContextElement::document_context(); let initial_context = Context::new(global_settings, List::new(&initial_context)); let (_remaining, optional_objects) = opt(all_consuming(map( tuple(( take_until("["), tag("["), map_parser( recognize(many_till(anychar, peek(tuple((tag("]"), eof))))), confine_context(|i| { all_consuming(many0(parser_with_context!(standard_set_object)( &initial_context, )))(i) }), ), tag("]"), eof, )), |(_, _, objects, _, _)| objects, )))(kw.key.into()) .expect("Object parser should always succeed."); // TODO: This should be omitting footnote references let (_remaining, objects) = all_consuming(many0(parser_with_context!(standard_set_object)( &initial_context, )))(kw.value.into()) .expect("Object parser should always succeed."); let entry_per_keyword_list = ret .entry(translated_name) .or_insert_with(|| AffiliatedKeywordValue::ObjectTree(Vec::with_capacity(1))); match entry_per_keyword_list { AffiliatedKeywordValue::ObjectTree(entry_per_keyword_list) => { entry_per_keyword_list.push((optional_objects, objects)); } _ => panic!("Invalid AffiliatedKeywordValue type."), } } }; } AffiliatedKeywords { keywords: ret } } fn translate_name<'g, 's>(global_settings: &'g GlobalSettings<'g, 's>, name: &'s str) -> String { let name_until_optval = name .split_once("[") .map(|(before, _after)| before) .unwrap_or(name); for (src, dst) in global_settings.element_keyword_translation_alist { if name_until_optval.eq_ignore_ascii_case(src) { return dst.to_lowercase(); } } name_until_optval.to_lowercase() } enum AffiliatedKeywordType { SingleString, ListOfStrings, OptionalPair, ObjectTree, } fn identify_keyword_type<'g, 's>( global_settings: &'g GlobalSettings<'g, 's>, name: &'s str, ) -> AffiliatedKeywordType { let is_multiple = ["CAPTION", "HEADER"] .into_iter() .any(|candidate| name.eq_ignore_ascii_case(candidate)); let is_parsed = global_settings .element_parsed_keywords .iter() .any(|candidate| name.eq_ignore_ascii_case(candidate)); let can_have_optval = global_settings .element_dual_keywords .iter() .any(|candidate| name.eq_ignore_ascii_case(candidate)); match (is_multiple, is_parsed, can_have_optval) { (true, true, true) => AffiliatedKeywordType::ObjectTree, (true, true, false) => unreachable!("Nothing like this exists in upstream org-mode."), (true, false, true) => unreachable!("Nothing like this exists in upstream org-mode."), (true, false, false) => AffiliatedKeywordType::ListOfStrings, (false, true, true) => unreachable!("Nothing like this exists in upstream org-mode."), (false, true, false) => unreachable!("Nothing like this exists in upstream org-mode."), (false, false, true) => AffiliatedKeywordType::OptionalPair, (false, false, false) => AffiliatedKeywordType::SingleString, } }