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 13 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ strum = { version = "0.26", features = ["derive"], optional = true }

[dev-dependencies]
heck = "0.5"
html_parser = "0.7"
serde_json = "1"

[[bin]]
Expand Down
2 changes: 1 addition & 1 deletion archive/locales/de-AT.cbor

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion archive/locales/de-CH.cbor

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion archive/locales/de-DE.cbor

Large diffs are not rendered by default.

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
214 changes: 212 additions & 2 deletions 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, ToFormatting,
StyleClass, SubsequentAuthorSubstituteRule, TermForm, ToFormatting,
};
use citationberg::{DateForm, LongShortForm, OrdinalLookup, TextCase};
use indexmap::IndexSet;
Expand Down Expand Up @@ -488,6 +488,12 @@ impl<'a, T: EntryLike + Hash + PartialEq + Eq + Debug> BibliographyDriver<'a, 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 Expand Up @@ -1386,7 +1588,15 @@ impl<'a> StyleContext<'a> {
let mut ctx = self.ctx(entry, props, locale, term_locale, true);
ctx.writing
.push_name_options(&self.csl.bibliography.as_ref()?.name_options);
self.csl.bibliography.as_ref()?.layout.render(&mut ctx);

let layout = &self.csl.bibliography.as_ref()?.layout;
if let Some(prefix) = layout.prefix.as_ref() {
ctx.push_str(prefix);
}
layout.render(&mut ctx);
if let Some(suffix) = layout.suffix.as_ref() {
ctx.push_str(suffix);
}
Some(ctx)
}

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
Loading