Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5e0c8cd
Add substitution for CompleteAll
DerDrodt Oct 5, 2024
f816f90
Fix remaining rules
DerDrodt Oct 8, 2024
5a16bfe
Clippy
DerDrodt Oct 8, 2024
d31f027
Merge branch 'main' into fix-219
DerDrodt Oct 16, 2024
41ec702
Fix merge errors
DerDrodt Oct 16, 2024
ef2e59a
Handle Bib tests
DerDrodt Oct 16, 2024
0f59ca4
Fix clippy
DerDrodt Oct 16, 2024
16f7985
Improve code
DerDrodt Oct 16, 2024
40de906
Fix name replacement
DerDrodt Oct 17, 2024
49f24d5
Add passing test
DerDrodt Oct 17, 2024
d772c7c
Fix bib layout affixes
DerDrodt Oct 17, 2024
9feff13
Update archives
DerDrodt Oct 17, 2024
4d084c7
Fix bibs with spans passing incorrectly
DerDrodt Oct 18, 2024
6f2adb2
Revert "Update archives"
reknih Feb 5, 2025
777a16b
Revert "Fix clippy"
reknih Feb 5, 2025
42cbb03
Revert "Handle Bib tests"
reknih Feb 5, 2025
54ab520
Merge branch 'main' into pr/228
reknih Feb 5, 2025
766e668
Move `f219` to local test
reknih Feb 5, 2025
210e2f8
Merge branch 'main' into fix-219
DerDrodt Apr 16, 2025
ffca181
Change test output to use formatted_result
DerDrodt Apr 17, 2025
25838e1
Fix punctuation pull logic
DerDrodt Apr 17, 2025
1703998
Document
DerDrodt Apr 17, 2025
0f165f7
Remove bugreports_parseName
DerDrodt Apr 17, 2025
38e3fa6
Merge branch 'main' into fix-219
DerDrodt Apr 24, 2025
1e134f3
Merge branch 'main' into fix-219
DerDrodt Apr 29, 2025
3081778
Merge branch 'main' into fix-219
DerDrodt May 15, 2025
37417f9
Merge branch 'main' into fix-219
DerDrodt May 20, 2025
b3a8bf5
Merge branch 'main' into fix-219
DerDrodt May 26, 2025
591dbe8
Merge branch 'main' into fix-219
DerDrodt Jun 27, 2025
8bc1493
Merge branch 'main' into fix-219
DerDrodt Jul 14, 2025
40c2863
Merge branch 'main' into fix-219
Drodt Sep 10, 2025
d31eedd
Fmt
DerDrodt Sep 10, 2025
d3b1d81
Merge branch 'main' into fix-219
Drodt Nov 14, 2025
2fcdc89
Regenerate passing tests
Drodt Nov 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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