diff --git a/src/csl/mod.rs b/src/csl/mod.rs index f5d8b13..7423af2 100644 --- a/src/csl/mod.rs +++ b/src/csl/mod.rs @@ -1945,11 +1945,6 @@ impl WritingContext { .sum::() } - /// Check if the last subtree is empty. - fn last_is_empty(&self) -> bool { - !self.buf.has_content() && !self.elem_stack.last().has_content() - } - /// Write the [`NameDisambiguationProperties`] that the first `cs:name` /// element used. Do not change if this is not the first `cs:name` element. /// Returns whether this was the first `cs:name` element. diff --git a/src/csl/rendering/mod.rs b/src/csl/rendering/mod.rs index bd71009..effda13 100644 --- a/src/csl/rendering/mod.rs +++ b/src/csl/rendering/mod.rs @@ -28,11 +28,13 @@ pub(crate) trait RenderCsl { fn render(&self, ctx: &mut Context); /// Check whether the element will render a given variable. fn will_render(&self, ctx: &mut Context, var: Variable) -> bool; + /// Check whether the element will be empty. + fn will_be_empty(&self, ctx: &mut Context) -> bool; } impl RenderCsl for citationberg::Text { fn render(&self, ctx: &mut Context) { - let Some(target) = ResolvedTextTarget::compute(self, ctx) else { return }; + let Some(target) = ResolvedTextTarget::compute(self, ctx, false) else { return }; let depth = ctx.push_elem(self.formatting); // Only print affixes if this macro is not used out of its original context. @@ -89,7 +91,7 @@ impl RenderCsl for citationberg::Text { if matches!(var, NumberVariable::Page) => { // TODO: Remove this hack - ctx.push_str(&n.to_str().replace("-", "–")) + ctx.push_str(&n.to_str().replace('-', "–")) } NumberVariableResult::Regular(n) => ctx.push_str(&n.to_str()), NumberVariableResult::Transparent(n) => ctx.push_transparent(n), @@ -136,7 +138,9 @@ impl RenderCsl for citationberg::Text { } fn will_render(&self, ctx: &mut Context, var: Variable) -> bool { - let Some(target) = ResolvedTextTarget::compute(self, ctx) else { return false }; + let Some(target) = ResolvedTextTarget::compute(self, ctx, false) else { + return false; + }; match target { ResolvedTextTarget::StandardVariable(s, _) => var == Variable::Standard(s), ResolvedTextTarget::NumberVariable(n, _) => var == Variable::Number(n), @@ -147,6 +151,39 @@ impl RenderCsl for citationberg::Text { ResolvedTextTarget::Value(_) => false, } } + + fn will_be_empty(&self, ctx: &mut Context) -> bool { + // If suppress_queried_variables is set to true, we need to perform a + // silent lookup, otherwise we need to perform a regular lookup. + let suppressing = ctx.writing.suppress_queried_variables; + if suppressing { + ctx.writing.stop_suppressing_queried_variables(); + } + let lookup_result = ResolvedTextTarget::compute(self, ctx, suppressing); + if suppressing { + ctx.writing.start_suppressing_queried_variables(); + } + + let Some(target) = lookup_result else { + // We found nothing but still queried a variable so the containing + // group must be removed. + if suppressing { + ctx.writing.usage_info.borrow_mut().last_mut().has_vars = true; + } + + return true; + }; + + match target { + ResolvedTextTarget::StandardVariable(_, s) => s.is_empty(), + ResolvedTextTarget::NumberVariable(_, _) => false, + ResolvedTextTarget::Macro(mac) => { + mac.children.iter().all(|c| c.will_be_empty(ctx)) + } + ResolvedTextTarget::Term(_) => false, + ResolvedTextTarget::Value(v) => v.is_empty(), + } + } } enum ResolvedTextTarget<'a, 'b> { @@ -161,6 +198,7 @@ impl<'a, 'b> ResolvedTextTarget<'a, 'b> { fn compute( text: &'b citationberg::Text, ctx: &mut Context<'a, T>, + silent: bool, ) -> Option { match ctx.instance.kind { // If we are supposed to print a variable we need to return if this @@ -192,10 +230,10 @@ impl<'a, 'b> ResolvedTextTarget<'a, 'b> { match &text.target { TextTarget::Variable { var: Variable::Standard(var), form } => ctx - .resolve_standard_variable(*form, *var, false) + .resolve_standard_variable(*form, *var, silent) .map(|s| ResolvedTextTarget::StandardVariable(*var, s)), TextTarget::Variable { var: Variable::Number(var), .. } => ctx - .resolve_number_variable(*var, false) + .resolve_number_variable(*var, silent) .map(|n| ResolvedTextTarget::NumberVariable(*var, n)), TextTarget::Variable { .. } => None, TextTarget::Macro { name } => { @@ -269,6 +307,14 @@ impl RenderCsl for citationberg::Number { var == Variable::Number(self.variable) } + + fn will_be_empty(&self, ctx: &mut Context) -> bool { + if self.will_render(ctx, self.variable.into()) { + ctx.resolve_number_variable(self.variable, true).is_none() + } else { + true + } + } } fn render_typed_num( @@ -309,11 +355,52 @@ fn render_page_range(range: std::ops::Range, ctx: &mut Contex .unwrap(); } +fn label_pluralization( + label: &citationberg::Label, + variable: NumberVariableResult, +) -> bool { + match label.label.plural { + LabelPluralize::Always => true, + LabelPluralize::Never => false, + LabelPluralize::Contextual => match variable { + NumberVariableResult::Regular(MaybeTyped::String(_)) => false, + NumberVariableResult::Regular(MaybeTyped::Typed(n)) => { + n.is_plural(label.variable.is_number_of_variable()) + } + NumberVariableResult::Transparent(_) => false, + }, + } +} + impl RenderCsl for citationberg::Label { fn render(&self, ctx: &mut Context) { + if self.will_be_empty(ctx) { + return; + } + + let Some(variable) = ctx.resolve_number_variable(self.variable, false) else { + return; + }; + + let depth = ctx.push_elem(citationberg::Formatting::default()); + let plural = label_pluralization(self, variable); + + let content = ctx + .term(Term::from(self.variable), self.label.form, plural) + .unwrap_or_default(); + + render_label_with_var(&self.label, ctx, content); + ctx.commit_elem(depth, None, Some(ElemMeta::Label)); + } + + fn will_render(&self, _ctx: &mut Context, _var: Variable) -> bool { + false + } + + fn will_be_empty(&self, ctx: &mut Context) -> bool { match ctx.instance.kind { Some(SpecialForm::VarOnly(Variable::Number(n))) if self.variable != n => { - return + return true; } Some( SpecialForm::VarOnly(_) @@ -321,7 +408,7 @@ impl RenderCsl for citationberg::Label { | SpecialForm::OnlyYearSuffix, ) => { if self.variable != NumberVariable::Locator { - return; + return true; } } _ => {} @@ -336,36 +423,15 @@ impl RenderCsl for citationberg::Label { .locator .map_or(false, |l| l.0 == Locator::Custom) { - return; + return true; } - let Some(variable) = ctx.resolve_number_variable(self.variable, false) else { - return; - }; - - let depth = ctx.push_elem(citationberg::Formatting::default()); - let plural = match self.label.plural { - LabelPluralize::Always => true, - LabelPluralize::Never => false, - LabelPluralize::Contextual => match variable { - NumberVariableResult::Regular(MaybeTyped::String(_)) => false, - NumberVariableResult::Regular(MaybeTyped::Typed(n)) => { - n.is_plural(self.variable.is_number_of_variable()) - } - NumberVariableResult::Transparent(_) => false, - }, - }; - - let content = ctx - .term(Term::from(self.variable), self.label.form, plural) - .unwrap_or_default(); - - render_label_with_var(&self.label, ctx, content); - ctx.commit_elem(depth, None, Some(ElemMeta::Label)); - } - - fn will_render(&self, _ctx: &mut Context, _var: Variable) -> bool { - false + if let Some(num) = ctx.resolve_number_variable(self.variable, false) { + let plural = label_pluralization(self, num); + ctx.term(Term::from(self.variable), self.label.form, plural).is_none() + } else { + true + } } } @@ -512,6 +578,15 @@ impl RenderCsl for citationberg::Date { Some(var) == self.variable.map(Variable::Date) } + + fn will_be_empty(&self, ctx: &mut Context) -> bool { + let Some(variable) = self.variable else { return true }; + if !self.will_render(ctx, variable.into()) { + true + } else { + ctx.resolve_date_variable(variable, false).is_none() + } + } } fn render_date_part( @@ -647,32 +722,45 @@ fn render_year_suffix_implicitly(ctx: &mut Context) { } } -impl RenderCsl for citationberg::Choose { - fn render(&self, ctx: &mut Context) { - for branch in self.branches() { - if branch.match_.test(BranchConditionIter::from_branch(branch, ctx)) { - render_with_delimiter(&branch.children, self.delimiter.as_deref(), ctx); - return; - } +fn choose_children( + choose: &citationberg::Choose, + ctx: &mut Context, + mut f: F, +) -> Option +where + F: FnMut(&[LayoutRenderingElement], &mut Context) -> R, +{ + for branch in choose.branches() { + if branch.match_.test(BranchConditionIter::from_branch(branch, ctx)) { + return Some(f(&branch.children, ctx)); } + } - if let Some(fallthrough) = &self.otherwise { - render_with_delimiter(&fallthrough.children, self.delimiter.as_deref(), ctx); - } + choose + .otherwise + .as_ref() + .map(|fallthrough| f(&fallthrough.children, ctx)) +} + +impl RenderCsl for citationberg::Choose { + fn render(&self, ctx: &mut Context) { + choose_children(self, ctx, |children, ctx| { + render_with_delimiter(children, self.delimiter.as_deref(), ctx); + }); } fn will_render(&self, ctx: &mut Context, var: Variable) -> bool { - for branch in self.branches() { - if branch.match_.test(BranchConditionIter::from_branch(branch, ctx)) { - return branch.children.iter().any(|c| c.will_render(ctx, var)); - } - } + choose_children(self, ctx, |children, ctx| { + children.iter().any(|c| c.will_render(ctx, var)) + }) + .unwrap_or_default() + } - if let Some(fallthrough) = &self.otherwise { - fallthrough.children.iter().any(|c| c.will_render(ctx, var)) - } else { - false - } + fn will_be_empty(&self, ctx: &mut Context) -> bool { + choose_children(self, ctx, |children, ctx| { + children.iter().all(|c| c.will_be_empty(ctx)) + }) + .unwrap_or(true) } } @@ -681,11 +769,15 @@ fn render_with_delimiter( delimiter: Option<&str>, ctx: &mut Context, ) { - let mut last_empty = true; + let mut first = true; let mut loc = None; for child in children { - if !last_empty { + if child.will_be_empty(ctx) { + continue; + } + + if !first { if let Some(delim) = delimiter { let prev_loc = std::mem::take(&mut loc); @@ -710,20 +802,12 @@ fn render_with_delimiter( LayoutRenderingElement::Group(_group) => _group.render(ctx), } - last_empty = ctx.writing.last_is_empty(); - if last_empty { - ctx.discard_elem(pos); - } else { - ctx.commit_elem(pos, None, None); - } + first = false; + ctx.commit_elem(pos, None, None); } if let Some(loc) = loc { - if last_empty { - ctx.discard_elem(loc); - } else { - ctx.commit_elem(loc, None, None); - } + ctx.commit_elem(loc, None, None); } } @@ -992,6 +1076,10 @@ impl RenderCsl for citationberg::Group { fn will_render(&self, ctx: &mut Context, var: Variable) -> bool { self.children.iter().any(|e| e.will_render(ctx, var)) } + + fn will_be_empty(&self, ctx: &mut Context) -> bool { + self.children.iter().all(|e| e.will_be_empty(ctx)) + } } impl RenderCsl for citationberg::LayoutRenderingElement { @@ -1032,6 +1120,26 @@ impl RenderCsl for citationberg::LayoutRenderingElement { } } } + + fn will_be_empty(&self, ctx: &mut Context) -> bool { + match self { + citationberg::LayoutRenderingElement::Text(text) => text.will_be_empty(ctx), + citationberg::LayoutRenderingElement::Number(num) => num.will_be_empty(ctx), + citationberg::LayoutRenderingElement::Label(label) => { + label.will_be_empty(ctx) + } + citationberg::LayoutRenderingElement::Date(date) => date.will_be_empty(ctx), + citationberg::LayoutRenderingElement::Names(names) => { + names.will_be_empty(ctx) + } + citationberg::LayoutRenderingElement::Choose(choose) => { + choose.will_be_empty(ctx) + } + citationberg::LayoutRenderingElement::Group(group) => { + group.will_be_empty(ctx) + } + } + } } impl RenderCsl for citationberg::Layout { @@ -1046,6 +1154,10 @@ impl RenderCsl for citationberg::Layout { fn will_render(&self, ctx: &mut Context, var: Variable) -> bool { self.elements.iter().any(|e| e.will_render(ctx, var)) } + + fn will_be_empty(&self, ctx: &mut Context) -> bool { + self.elements.iter().all(|e| e.will_be_empty(ctx)) + } } impl RenderCsl for citationberg::RenderingElement { @@ -1062,6 +1174,13 @@ impl RenderCsl for citationberg::RenderingElement { citationberg::RenderingElement::Other(o) => o.will_render(ctx, var), } } + + fn will_be_empty(&self, ctx: &mut Context) -> bool { + match self { + citationberg::RenderingElement::Layout(l) => l.will_be_empty(ctx), + citationberg::RenderingElement::Other(o) => o.will_be_empty(ctx), + } + } } impl From for Case { diff --git a/src/csl/rendering/names.rs b/src/csl/rendering/names.rs index a4fae70..c4d5026 100644 --- a/src/csl/rendering/names.rs +++ b/src/csl/rendering/names.rs @@ -170,6 +170,46 @@ impl NameDisambiguationProperties { } } +fn renders_given_special_form( + names: &Names, + ctx: &Context, + is_empty: bool, +) -> bool { + match &ctx.instance.kind { + Some(SpecialForm::VarOnly(Variable::Name(var))) => { + // Skip if none of the variables are the author and the supplement does not contain the author either. + let contains_v = names.variable.iter().any(|v| var == v); + let substitute_will_render_v = is_empty + && names.substitute().map_or(false, |s| { + s.children + .iter() + .filter_map(|c| match c { + LayoutRenderingElement::Names(n) => Some(n.variable.iter()), + _ => None, + }) + .flatten() + .any(|v| var == v) + }); + if !contains_v && !substitute_will_render_v { + return false; + } + } + Some( + SpecialForm::VarOnly(_) + | SpecialForm::OnlyFirstDate + | SpecialForm::OnlyYearSuffix, + ) => return false, + Some(SpecialForm::SuppressAuthor) => { + if names.variable.iter().any(|v| &NameVariable::Author == v) { + return false; + } + } + None => {} + } + + true +} + impl RenderCsl for Names { fn render(&self, ctx: &mut Context) { // The editor and translator variables need to be merged if they are @@ -212,38 +252,9 @@ impl RenderCsl for Names { // Write the substitute if all variables are empty. let is_empty = people.iter().all(|(p, _)| p.is_empty()); // Suppress this variable if we are in a special form. - match &ctx.instance.kind { - Some(SpecialForm::VarOnly(Variable::Name(var))) => { - // Skip if none of the variables are the author and the supplement does not contain the author either. - let contains_v = self.variable.iter().any(|v| var == v); - let substitute_will_render_v = is_empty - && self.substitute().map_or(false, |s| { - s.children - .iter() - .filter_map(|c| match c { - LayoutRenderingElement::Names(n) => { - Some(n.variable.iter()) - } - _ => None, - }) - .flatten() - .any(|v| var == v) - }); - if !contains_v && !substitute_will_render_v { - return; - } - } - Some( - SpecialForm::VarOnly(_) - | SpecialForm::OnlyFirstDate - | SpecialForm::OnlyYearSuffix, - ) => return, - Some(SpecialForm::SuppressAuthor) => { - if self.variable.iter().any(|v| &NameVariable::Author == v) { - return; - } - } - None => {} + if !renders_given_special_form(self, ctx, is_empty) { + ctx.writing.pop_name_options(); + return; } if is_empty { @@ -391,6 +402,20 @@ impl RenderCsl for Names { false } + + fn will_be_empty(&self, ctx: &mut Context) -> bool { + let is_empty = self + .variable + .iter() + .all(|v| ctx.resolve_name_variable(*v, false).is_empty()); + + !renders_given_special_form(self, ctx, is_empty) + || (is_empty + && self + .substitute() + .map(|s| s.children.iter().all(|c| c.will_be_empty(ctx))) + .unwrap_or(true)) + } } #[derive(Debug, Clone, Copy)] diff --git a/tests/citeproc-pass.txt b/tests/citeproc-pass.txt index 0d4e601..def3c20 100644 --- a/tests/citeproc-pass.txt +++ b/tests/citeproc-pass.txt @@ -129,6 +129,7 @@ disambiguate_YearSuffixWithEtAlSubsequent flipflop_OrphanQuote form_TitleTestNoLongFalse fullstyles_APA +group_SuppressValueWithEmptySubgroup integration_CitationSort integration_CitationSortTwice label_CompactNamesAfterFullNames