diff --git a/src/csl/elem.rs b/src/csl/elem.rs index 458f50df..3a1b89ca 100644 --- a/src/csl/elem.rs +++ b/src/csl/elem.rs @@ -178,6 +178,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 3040cdca..7e3be642 100644 --- a/src/csl/mod.rs +++ b/src/csl/mod.rs @@ -17,7 +17,8 @@ use citationberg::{ Affixes, BaseLanguage, Citation, CitationFormat, Collapse, CslMacro, Display, GrammarGender, IndependentStyle, InheritableNameOptions, Layout, LayoutRenderingElement, Locale, LocaleCode, Names, SecondFieldAlign, StyleCategory, - StyleClass, TermForm, ToAffixes, ToFormatting, taxonomy as csl_taxonomy, + StyleClass, SubsequentAuthorSubstituteRule, TermForm, ToAffixes, ToFormatting, + taxonomy as csl_taxonomy, }; use citationberg::{DateForm, LongShortForm, OrdinalLookup, TextCase}; use indexmap::IndexSet; @@ -491,6 +492,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, @@ -1032,6 +1039,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 { @@ -2420,28 +2623,15 @@ impl<'a, T: EntryLike> Context<'a, T> { let mut used_buf = false; let buf = if self.writing.buf.is_empty() { - match self - .writing + self.writing .elem_stack .last_mut_predicate(|p| !p.is_empty()) .and_then(|p| p.0.last_mut()) - { - Some(ElemChild::Text(f)) => &mut f.text, - // Get the text element if it is contained in an `Elem`. - Some(ElemChild::Elem(Elem { children, .. })) - if children.0.len() == 1 - && matches!(children.0[0], ElemChild::Text(_)) => - { - match &mut children.0[0] { - ElemChild::Text(f) => &mut f.text, - _ => unreachable!(), - } - } - _ => { + .and_then(get_last_text) + .unwrap_or_else(|| { used_buf = true; self.writing.buf.as_string_mut() - } - } + }) } else { used_buf = true; self.writing.buf.as_string_mut() @@ -2971,6 +3161,19 @@ enum SpecialForm { SuppressAuthor, } +/// Recursively iterates over `child`. If we reach text, we return a mutable +/// reference to its content. If we reach an element with one child, we +/// recursively call the funtion with this child. Otherwise, we return [None]. +fn get_last_text(child: &mut ElemChild) -> Option<&mut String> { + match child { + ElemChild::Text(formatted) => Some(&mut formatted.text), + ElemChild::Elem(Elem { children, .. }) if children.0.len() == 1 => { + get_last_text(&mut children.0[0]) + } + _ => None, + } +} + #[cfg(test)] mod tests { use std::{fs, path::Path}; diff --git a/src/csl/rendering/names.rs b/src/csl/rendering/names.rs index bda48ceb..00e205f5 100644 --- a/src/csl/rendering/names.rs +++ b/src/csl/rendering/names.rs @@ -257,6 +257,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 { @@ -269,6 +270,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 e7b60830..400089a7 100644 --- a/tests/citeproc-pass.txt +++ b/tests/citeproc-pass.txt @@ -239,6 +239,7 @@ magic_SecondFieldAlign magic_StripPeriodsExcludeAffixes magic_StripPeriodsFalse magic_StripPeriodsTrue +magic_SubsequentAuthorSubstituteOfTitleField magic_SuppressDuplicateVariableRendering magic_TextRangeEnglish magic_TextRangeFrench @@ -293,6 +294,8 @@ name_PeriodAfterInitials name_QuashOrdinaryVariableRenderedViaSubstitute name_RomanianTwo name_SemicolonWithAnd +name_SubsequentAuthorSubstituteMultipleNames +name_SubsequentAuthorSubstituteSingleField name_SubstituteMacroInheritDecorations name_SubstituteName name_SubstituteOnDateGroupSpanFail @@ -301,6 +304,7 @@ name_SubstituteOnMacroGroupSpanFail name_SubstituteOnNamesSingletonGroupSpanFail name_SubstituteOnNamesSpanNamesSpanFail name_SubstituteOnNumberGroupSpanFail +name_SubstitutePartialEach name_WesternArticularLowercase name_WesternSimple name_WesternTwoAuthors @@ -442,6 +446,7 @@ sort_AguStyle sort_BibliographyResortOnUpdate sort_CaseInsensitiveBibliography sort_CaseInsensitiveCitation +sort_ChicagoYearSuffix1 sort_Citation sort_CitationNumberPrimaryAscendingViaMacroBibliography sort_CitationNumberPrimaryAscendingViaVariableBibliography diff --git a/tests/citeproc.rs b/tests/citeproc.rs index 20146a92..630961fb 100644 --- a/tests/citeproc.rs +++ b/tests/citeproc.rs @@ -612,8 +612,8 @@ where true } else { eprintln!("Test {} failed", display()); - eprintln!("Expected:\n{}", case.result); - eprintln!("Got:\n{output}"); + eprintln!("Expected:\n{}", formatted_result); + eprintln!("Got:\n{}", output); false } } 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 =====<< +