Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for subsequent author substitution #228

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
18 changes: 18 additions & 0 deletions src/csl/elem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ElemChild> {
for i in 0..self.0.len() {
Expand Down
204 changes: 203 additions & 1 deletion src/csl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -488,6 +488,12 @@ impl<T: EntryLike + Hash + PartialEq + Eq + Debug> 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,
Expand Down Expand Up @@ -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<ElemChild> {
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<Elem>) {
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 {
Expand Down
2 changes: 2 additions & 0 deletions src/csl/rendering/names.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -273,6 +274,7 @@ impl RenderCsl for Names {
}
}

ctx.commit_elem(depth, self.display, Some(ElemMeta::Names));
ctx.writing.stop_suppressing_queried_variables();
}

Expand Down
5 changes: 5 additions & 0 deletions tests/citeproc-pass.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ bugreports_UnisaHarvardInitialization
bugreports_YearSuffixLingers
bugreports_disambiguate
bugreports_effingBug
bugreports_parenthesis
bugreports_undefinedCrash
collapse_AuthorCollapse
collapse_AuthorCollapseDifferentAuthorsOneWithEtAl
Expand Down Expand Up @@ -244,6 +245,8 @@ name_PeriodAfterInitials
name_QuashOrdinaryVariableRenderedViaSubstitute
name_RomanianTwo
name_SemicolonWithAnd
name_SubsequentAuthorSubstituteMultipleNames
name_SubsequentAuthorSubstituteSingleField
name_SubstituteMacroInheritDecorations
name_SubstituteName
name_SubstituteOnDateGroupSpanFail
Expand All @@ -252,6 +255,7 @@ name_SubstituteOnMacroGroupSpanFail
name_SubstituteOnNamesSingletonGroupSpanFail
name_SubstituteOnNamesSpanNamesSpanFail
name_SubstituteOnNumberGroupSpanFail
name_SubstitutePartialEach
name_WesternArticularLowercase
name_WesternSimple
name_WesternTwoAuthors
Expand Down Expand Up @@ -389,6 +393,7 @@ quotes_PunctuationNasty
sort_BibliographyResortOnUpdate
sort_CaseInsensitiveBibliography
sort_CaseInsensitiveCitation
sort_ChicagoYearSuffix1
sort_Citation
sort_CitationNumberPrimaryAscendingViaMacroBibliography
sort_CitationNumberPrimaryAscendingViaVariableBibliography
Expand Down
92 changes: 92 additions & 0 deletions tests/local/name_SubsequentAuthorSubstitute.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
>>==== MODE ====>>
bibliography
<<==== MODE ====<<

>>==== RESULT ====>>
<div class="csl-bib-body">
<div class="csl-entry">Bumke, Courtly Culture: Literature and Society in the High Middle Ages</div>
<div class="csl-entry">-----, Höfische</div>
</div>
<<==== RESULT ====<<

>>==== CITATION-ITEMS ====>>
[
[
{
"id": "ITEM1"
}
],
[
{
"id": "ITEM2"
}
]
]
<<==== CITATION-ITEMS ====<<

>>==== CSL ====>>
<?xml version="1.0" encoding="utf-8"?>
<style
xmlns="http://purl.org/net/xbiblio/csl"
class="note"
version="1.0">
<info>
<id />
<title />
<updated>2009-08-10T04:49:00+09:00</updated>
</info>
<citation>
<layout>
<text value="Bogus"/>
</layout>
</citation>
<bibliography subsequent-author-substitute="-----">
<layout>
<group delimiter=", ">
<names variable="author">
<name form="short" and="text"/>
</names>
<text variable="title"/>
<names variable="translator">
<name form="short" and="text"/>
<label form="short" prefix=" "/>
</names>
</group>
</layout>
</bibliography>
</style>
<<==== 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 =====<<

Loading