diff --git a/src/csl/elem.rs b/src/csl/elem.rs index 36b9d38a..7c0fbd07 100644 --- a/src/csl/elem.rs +++ b/src/csl/elem.rs @@ -182,6 +182,24 @@ impl ElemChildren { }) } + /// Retrieve a mutable reference to the first child with a matching meta by + /// DFS. + pub fn find_meta_mut(&mut self, meta: ElemMeta) -> Option<&mut Elem> { + self.0 + .iter_mut() + .filter_map(|c| match c { + ElemChild::Elem(e) => { + if e.meta == Some(meta) { + Some(e) + } else { + e.children.find_meta_mut(meta) + } + } + _ => None, + }) + .next() + } + /// Remove the first child with any meta by DFS. pub(super) fn remove_any_meta(&mut self) -> Option { for i in 0..self.0.len() { diff --git a/src/csl/mod.rs b/src/csl/mod.rs index 2d587f97..2fcba5ee 100644 --- a/src/csl/mod.rs +++ b/src/csl/mod.rs @@ -15,7 +15,7 @@ use citationberg::{ taxonomy as csl_taxonomy, Affixes, BaseLanguage, Citation, CitationFormat, Collapse, CslMacro, Display, GrammarGender, IndependentStyle, InheritableNameOptions, Layout, LayoutRenderingElement, Locale, LocaleCode, Names, SecondFieldAlign, StyleCategory, - StyleClass, TermForm, ToAffixes, ToFormatting, + StyleClass, SubsequentAuthorSubstituteRule, TermForm, ToAffixes, ToFormatting, }; use citationberg::{DateForm, LongShortForm, OrdinalLookup, TextCase}; use indexmap::IndexSet; @@ -488,6 +488,12 @@ impl BibliographyDriver<'_, T> { )) } + substitute_subsequent_authors( + bibliography.subsequent_author_substitute.as_ref(), + bibliography.subsequent_author_substitute_rule, + &mut items, + ); + Some(RenderedBibliography { hanging_indent: bibliography.hanging_indent, second_field_align: bibliography.second_field_align, @@ -998,6 +1004,202 @@ fn collapse_items<'a, T: EntryLike>(cite: &mut SpeculativeCiteRender<'a, '_, T>) } } +fn substitute_subsequent_authors( + subs: Option<&String>, + mut rule: SubsequentAuthorSubstituteRule, + items: &mut [(ElemChildren, String)], +) { + if let Some(subs) = subs { + let subs = Formatting::default().add_text(subs.clone()); + + fn replace_all(names: &mut Elem, is_empty: bool, subs: &Formatted) { + fn remove_name(mut child: Elem) -> Option { + if matches!(child.meta, Some(ElemMeta::Name(_, _))) { + return None; + } + child.children.0 = child + .children + .0 + .into_iter() + .filter_map(|e| match e { + ElemChild::Elem(e) => remove_name(e), + _ => Some(e), + }) + .collect(); + Some(ElemChild::Elem(child)) + } + let old_children = std::mem::replace( + &mut names.children, + ElemChildren(vec![ElemChild::Text(subs.clone())]), + ); + if !is_empty { + for child in old_children.0 { + match child { + ElemChild::Elem(e) => { + if let Some(c) = remove_name(e) { + names.children.0.push(c); + } + } + _ => names.children.0.push(child), + } + } + } + } + + fn replace_name(e: Elem, subs: &Formatted) -> (ElemChild, bool) { + if matches!(e.meta, Some(ElemMeta::Name(_, _))) { + return ( + ElemChild::Elem(Elem { + children: ElemChildren(vec![ElemChild::Text(subs.clone())]), + display: e.display, + meta: e.meta, + }), + true, + ); + } + + let len = e.children.0.len(); + let mut iter = e.children.0.into_iter(); + let mut children = Vec::with_capacity(len); + let mut changed = false; + for c in iter.by_ref() { + match c { + ElemChild::Elem(ec) => { + let (nc, ch) = replace_name(ec, subs); + children.push(nc); + if ch { + changed = true; + break; + } + } + _ => children.push(c), + } + } + children.extend(iter); + ( + ElemChild::Elem(Elem { + display: e.display, + meta: e.meta, + children: ElemChildren(children), + }), + changed, + ) + } + + fn replace_each(names: &mut Elem, subs: &Formatted) { + let old_children = std::mem::replace( + &mut names.children, + ElemChildren(vec![ElemChild::Text(subs.clone())]), + ); + for child in old_children.0 { + match child { + ElemChild::Elem(e) => { + names.children.0.push(replace_name(e, subs).0); + } + _ => names.children.0.push(child), + } + } + } + + fn get_names(elem: &Elem, names: &mut Vec) { + if matches!(elem.meta, Some(ElemMeta::Name(_, _))) { + names.push(elem.clone()); + } else { + for c in &elem.children.0 { + if let ElemChild::Elem(e) = c { + get_names(e, names); + } + } + } + } + + fn replace_first_n(mut num: usize, names: &mut Elem, subs: &Formatted) { + let old_children = std::mem::replace( + &mut names.children, + ElemChildren(vec![ElemChild::Text(subs.clone())]), + ); + for child in old_children.0.into_iter() { + if num == 0 { + break; + } + match child { + ElemChild::Elem(e) => { + let (c, changed) = replace_name(e, subs); + names.children.0.push(c); + if changed { + num -= 1; + } + } + _ => names.children.0.push(child), + } + } + } + + fn num_of_matches(ns1: &[Elem], ns2: &[Elem]) -> usize { + ns1.iter().zip(ns2.iter()).take_while(|(a, b)| a == b).count() + } + + let mut last_names = None; + + for item in items.iter_mut() { + let ec = &mut item.0; + let Some(names_elem) = ec.find_meta(ElemMeta::Names) else { + continue; + }; + let mut xnames = Vec::new(); + get_names(names_elem, &mut xnames); + let (lnames_elem, lnames) = if let Some(ns) = &last_names { + ns + } else { + // No previous name; nothing to replace. Save and skip + last_names = Some((names_elem.clone(), xnames)); + continue; + }; + if xnames.is_empty() { + rule = SubsequentAuthorSubstituteRule::CompleteAll; + } + match rule { + SubsequentAuthorSubstituteRule::CompleteAll => { + if lnames == &xnames + && (!xnames.is_empty() || names_elem == lnames_elem) + { + let names = ec.find_meta_mut(ElemMeta::Names).unwrap(); + replace_all(names, xnames.is_empty(), &subs); + } else { + last_names = Some((names_elem.clone(), xnames.clone())); + } + } + SubsequentAuthorSubstituteRule::CompleteEach => { + if lnames == &xnames { + let names = ec.find_meta_mut(ElemMeta::Names).unwrap(); + replace_each(names, &subs); + } else { + last_names = Some((names_elem.clone(), xnames.clone())); + } + } + SubsequentAuthorSubstituteRule::PartialEach => { + let nom = num_of_matches(&xnames, lnames); + if nom > 0 { + let names = ec.find_meta_mut(ElemMeta::Names).unwrap(); + replace_first_n(nom, names, &subs); + } else { + last_names = Some((names_elem.clone(), xnames.clone())); + } + } + SubsequentAuthorSubstituteRule::PartialFirst => { + let nom = num_of_matches(&xnames, lnames); + if nom > 0 { + let names = ec.find_meta_mut(ElemMeta::Names).unwrap(); + replace_first_n(1, names, &subs); + } else { + last_names = Some((names_elem.clone(), xnames.clone())); + } + } + } + } + } +} + /// What we have decided for rerendering this item. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] enum CollapseVerdict { diff --git a/src/csl/rendering/names.rs b/src/csl/rendering/names.rs index 3c936750..b8019866 100644 --- a/src/csl/rendering/names.rs +++ b/src/csl/rendering/names.rs @@ -261,6 +261,7 @@ impl RenderCsl for Names { if let Some(substitute) = &self.substitute() { ctx.writing.start_suppressing_queried_variables(); + let depth = ctx.push_elem(self.to_formatting()); for child in &substitute.children { let len = ctx.writing.len(); if let LayoutRenderingElement::Names(names_child) = child { @@ -273,6 +274,7 @@ impl RenderCsl for Names { } } + ctx.commit_elem(depth, self.display, Some(ElemMeta::Names)); ctx.writing.stop_suppressing_queried_variables(); } diff --git a/tests/citeproc-pass.txt b/tests/citeproc-pass.txt index 43469317..32df5257 100644 --- a/tests/citeproc-pass.txt +++ b/tests/citeproc-pass.txt @@ -30,6 +30,7 @@ bugreports_UnisaHarvardInitialization bugreports_YearSuffixLingers bugreports_disambiguate bugreports_effingBug +bugreports_parenthesis bugreports_undefinedCrash collapse_AuthorCollapse collapse_AuthorCollapseDifferentAuthorsOneWithEtAl @@ -244,6 +245,8 @@ name_PeriodAfterInitials name_QuashOrdinaryVariableRenderedViaSubstitute name_RomanianTwo name_SemicolonWithAnd +name_SubsequentAuthorSubstituteMultipleNames +name_SubsequentAuthorSubstituteSingleField name_SubstituteMacroInheritDecorations name_SubstituteName name_SubstituteOnDateGroupSpanFail @@ -252,6 +255,7 @@ name_SubstituteOnMacroGroupSpanFail name_SubstituteOnNamesSingletonGroupSpanFail name_SubstituteOnNamesSpanNamesSpanFail name_SubstituteOnNumberGroupSpanFail +name_SubstitutePartialEach name_WesternArticularLowercase name_WesternSimple name_WesternTwoAuthors @@ -389,6 +393,7 @@ quotes_PunctuationNasty sort_BibliographyResortOnUpdate sort_CaseInsensitiveBibliography sort_CaseInsensitiveCitation +sort_ChicagoYearSuffix1 sort_Citation sort_CitationNumberPrimaryAscendingViaMacroBibliography sort_CitationNumberPrimaryAscendingViaVariableBibliography diff --git a/tests/local/name_SubsequentAuthorSubstitute.txt b/tests/local/name_SubsequentAuthorSubstitute.txt new file mode 100644 index 00000000..f6b55c56 --- /dev/null +++ b/tests/local/name_SubsequentAuthorSubstitute.txt @@ -0,0 +1,92 @@ +>>==== MODE ====>> +bibliography +<<==== MODE ====<< + +>>==== RESULT ====>> +
+
Bumke, Courtly Culture: Literature and Society in the High Middle Ages
+
-----, Höfische
+
+<<==== RESULT ====<< + +>>==== CITATION-ITEMS ====>> +[ + [ + { + "id": "ITEM1" + } + ], + [ + { + "id": "ITEM2" + } + ] +] +<<==== CITATION-ITEMS ====<< + +>>==== CSL ====>> + + +<<==== CSL ====<< + +>>==== INPUT ====>> +[ + { + "id": "ITEM1", + "author": [ + { + "family": "Bumke", + "given": "Joachim" + } + ], + "title": "Courtly Culture: Literature and Society in the High Middle Ages", + "type": "book" + }, + { + "id": "ITEM2", + "author": [ + { + "family": "Bumke", + "given": "Joachim" + } + ], + "title": "Höfische", + "type": "book" + } +] +<<==== INPUT ====<< + + + +>>===== VERSION =====>> +1.0 +<<===== VERSION =====<< +