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

Fix delimiting within <choose> and <layout> for CSL #269

Merged
merged 6 commits into from
Feb 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 25 additions & 0 deletions src/csl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1674,6 +1674,9 @@ pub(crate) struct WritingContext {
cases: NonEmptyStack<Option<TextCase>>,
/// Inheritable name options.
name_options: NonEmptyStack<InheritableNameOptions>,
/// Delimiters from ancestor delimiting elements (e.g., `cs:group`).
/// To be applied within `cs:choose`, but not another delimiting element or a macro.
delimiters: NonEmptyStack<Option<String>>,

// Buffers.
/// The buffer we're writing to. If block-level or formatting changes, we
Expand All @@ -1698,6 +1701,7 @@ impl Default for WritingContext {
format_stack: NonEmptyStack::default(),
cases: NonEmptyStack::default(),
name_options: NonEmptyStack::default(),
delimiters: NonEmptyStack::default(),
buf: CaseFolder::default(),
elem_stack: NonEmptyStack::default(),
}
Expand Down Expand Up @@ -1932,6 +1936,24 @@ impl WritingContext {
.reconfigure((*self.cases.last()).map(Into::into).unwrap_or_default());
}

/// Set the delimiter of the children of a [`citationberg::Group`] or
/// [`citationberg::Layout`]. It is inherited by [`citationberg::Choose`]
/// for its children as well.
fn push_delimiter(&mut self, delimiter: Option<String>) -> DelimiterIdx {
let idx = self.delimiters.len();
self.delimiters.push(delimiter);
DelimiterIdx(idx)
}
/// Clear the delimiter after a [`citationberg::Group`] or
/// [`citationberg::Layout`].
fn pop_delimiter(&mut self, idx: DelimiterIdx) {
if idx.0 == self.delimiters.len() {
return;
}

self.delimiters.drain(idx.0).for_each(drop);
}

/// Push a new suppressed variable if we are suppressing queried variables.
fn maybe_suppress(&self, variable: Variable) {
if self.suppress_queried_variables {
Expand Down Expand Up @@ -2753,6 +2775,9 @@ impl DisplayLoc {
#[must_use = "case stack must be popped"]
struct CaseIdx(NonZeroUsize);

#[must_use = "delimiter stack must be popped"]
struct DelimiterIdx(NonZeroUsize);

impl<T: EntryLike> Write for Context<'_, T> {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
self.push_str(s);
Expand Down
41 changes: 23 additions & 18 deletions src/csl/rendering/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,12 @@ impl RenderCsl for citationberg::Text {
MaybeTyped::String(s) => ctx.push_str(&s.replace('-', "–")),
},
ResolvedTextTarget::Macro(mac) => {
// Delimiters from ancestor delimiting elements are NOT applied within.
let idx = ctx.writing.push_delimiter(None);
for child in &mac.children {
child.render(ctx);
}
ctx.writing.pop_delimiter(idx);
}
ResolvedTextTarget::Term(s) => ctx.push_str(s),
ResolvedTextTarget::Value(val) => ctx.push_str(val),
Expand Down Expand Up @@ -831,7 +834,9 @@ where
impl RenderCsl for citationberg::Choose {
fn render<T: EntryLike>(&self, ctx: &mut Context<T>) {
choose_children(self, ctx, |children, ctx| {
render_with_delimiter(children, self.delimiter.as_deref(), ctx);
// Propagate parent group's delimiter to 'choose' output, as
// required by CSL, by not pushing a delimiter to the stack.
render_with_delimiter(children, ctx);
});
}

Expand Down Expand Up @@ -859,11 +864,16 @@ impl RenderCsl for citationberg::Choose {
}
}

/// Render `children` with the delimiter in `ctx` between them.
///
/// The delimiter can be updated by [`ctx.writing.push_delimiter`][super::WritingContext::push_delimiter]
/// and [`ctx.writing.pop_delimiter`][super::WritingContext::pop_delimiter].
fn render_with_delimiter<T: EntryLike>(
children: &[LayoutRenderingElement],
delimiter: Option<&str>,
ctx: &mut Context<T>,
) {
let delimiter = ctx.writing.delimiters.last().clone();

let mut first = true;
let mut loc = None;

Expand All @@ -874,7 +884,7 @@ fn render_with_delimiter<T: EntryLike>(
}

if !first {
if let Some(delim) = delimiter {
if let Some(delim) = &delimiter {
let prev_loc = std::mem::take(&mut loc);

if let Some(prev_loc) = prev_loc {
Expand All @@ -885,20 +895,10 @@ fn render_with_delimiter<T: EntryLike>(
ctx.push_str(delim);
}
}
first = false;

let pos = ctx.push_elem(citationberg::Formatting::default());

match child {
LayoutRenderingElement::Text(text) => text.render(ctx),
LayoutRenderingElement::Number(num) => num.render(ctx),
LayoutRenderingElement::Label(label) => label.render(ctx),
LayoutRenderingElement::Date(date) => date.render(ctx),
LayoutRenderingElement::Names(names) => names.render(ctx),
LayoutRenderingElement::Choose(choose) => choose.render(ctx),
LayoutRenderingElement::Group(_group) => _group.render(ctx),
}

first = false;
child.render(ctx);
ctx.commit_elem(pos, None, None);
}

Expand Down Expand Up @@ -1147,7 +1147,10 @@ impl RenderCsl for citationberg::Group {
let affix_loc = ctx.apply_prefix(&affixes);

let info = self.will_have_info(ctx).1;
render_with_delimiter(&self.children, self.delimiter.as_deref(), ctx);

let delim_idx = ctx.writing.push_delimiter(self.delimiter.clone());
render_with_delimiter(&self.children, ctx);
ctx.writing.pop_delimiter(delim_idx);

ctx.apply_suffix(&affixes, affix_loc);

Expand Down Expand Up @@ -1247,11 +1250,13 @@ impl RenderCsl for citationberg::LayoutRenderingElement {

impl RenderCsl for citationberg::Layout {
fn render<T: EntryLike>(&self, ctx: &mut Context<T>) {
let fidx = ctx.push_format(self.to_formatting());
let format_idx = ctx.push_format(self.to_formatting());
let delim_idx = ctx.writing.push_delimiter(self.delimiter.clone());
for e in &self.elements {
e.render(ctx);
}
ctx.pop_format(fidx);
ctx.writing.pop_delimiter(delim_idx);
ctx.pop_format(format_idx);
}

fn will_render<T: EntryLike>(&self, ctx: &mut Context<T>, var: Variable) -> bool {
Expand Down
82 changes: 82 additions & 0 deletions tests/local/choose_UseAncestorsDelimiter.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
>>===== MODE =====>>
citation
<<===== MODE =====<<


>>===== DESCRIPTION =====>>
Delimiters from the nearest delimiters from the nearest ancestor delimiting element (e.g., <group>)
are applied within the output of <choose>
(i.e., the output of the matching <if>, <else-if>, or <else>).
https://docs.citationstyles.org/en/v1.0.2/specification.html#choose

But they are not applied within the output of a delimiting element or a <text macro="…"> element.
https://docs.citationstyles.org/en/v1.0.2/specification.html#delimiter
https://docs.citationstyles.org/en/v1.0.2/specification.html#macro

https://github.com/typst/hayagriva/issues/180
<<===== DESCRIPTION =====<<


>>===== RESULT =====>>
Title_Edition_Publisher_Place_TitleEditionPublisherPlace_PublisherPublisherPublisher
<<===== RESULT =====<<


>>===== INPUT =====>>
[
{
"edition": "Edition",
"id": "random",
"publisher": "Publisher",
"publisher-place": "Place",
"title": "Title",
"type": "book"
}
]
<<===== INPUT =====<<


>>===== CSL =====>>
<?xml version="1.0" encoding="utf-8"?>
<style xmlns="http://purl.org/net/xbiblio/csl" version="1.0" class="in-text">
<info>
<id />
<title />
</info>
<macro name="unaffected">
<text variable="title"/>
<text variable="edition"/>
<choose>
<if type="book">
<text variable="publisher"/>
<text variable="publisher-place"/>
</if>
</choose>
</macro>
<citation>
<layout>
<group delimiter="_">
<text variable="title"/>
<text variable="edition"/>
<choose>
<if type="book">
<text variable="publisher"/>
<text variable="publisher-place"/>
</if>
</choose>
<text macro="unaffected"/>
<group>
<text variable="publisher"/>
<text variable="publisher"/>
<text variable="publisher"/>
</group>
</group>
</layout>
</citation>
</style>
<<===== CSL =====<<


>>===== VERSION =====>>
1.0.2
<<===== VERSION =====<<