You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
While re-typesetting a ~280-page economics report in Typst, I compared the bibliography against
a rendering of the same CSL style and the same source data produced by Word/Zotero
(citeproc-js). Five reproducible divergences turned up. Three are already tracked; two appear to
be unreported. All five are in the MWE below.
The one I would like to highlight is not a rendering difference at all but a diagnostics gap:
a published, unmodified style from citation-style-language/styles contains conditions that can
never be true. hayagriva evaluates them per spec and therefore drops a field for every entry in
the document — with no warning, no error, and output that looks perfectly well-formed. In my
case that was every journal title in the entire bibliography. I only found it by diffing against
another processor.
Environment
Typst
0.15.1 (9dfd3a08), Windows 11
hayagriva
0.10.1 (per Cargo.lock at tag v0.15.1) — current release
biblatex
0.12.0
Compared against
Word + Zotero (citeproc-js), same .csl, same data
Minimal reproduction
Three files, typst compile mwe.typ. Every construct is exercised by a literal marker so the
output can be read without a reference.
mwe.csl
<?xml version="1.0" encoding="utf-8"?>
<stylexmlns="http://purl.org/net/xbiblio/csl"version="1.0"class="in-text"default-locale="en-US">
<info>
<title>MWE</title>
<id>http://example.org/mwe</id>
<updated>2026-01-01T00:00:00+00:00</updated>
</info>
<citation>
<layout>
<textvariable="citation-number"/>
</layout>
</citation>
<bibliography>
<layoutsuffix=".">
<groupdelimiter=" | ">
<!-- D: BibTeX "and others" -->
<namesvariable="author"et-al-min="4"et-al-use-first="1">
<nameand="text"delimiter=", "/>
<et-alterm="et-al"/>
</names>
<!-- A: condition that can never be true (an item has exactly one type) -->
<choose>
<iftype="article-journal article-magazine"match="all">
<textvalue="A-REACHED:"/>
<textvariable="container-title"/>
</if>
<else>
<textvalue="A-MISSED"/>
</else>
</choose>
<!-- B: group delimiter around a date that renders nothing -->
<groupdelimiter=", ">
<textvariable="publisher-place"/>
<datevariable="issued">
<date-partname="day"suffix=". "/>
<date-partname="month"/>
</date>
</group>
<!-- C: suffix period after a term that already ends in a period -->
<namesvariable="editor">
<name/>
<labelprefix=" ("form="short"suffix=".)"/>
</names>
<!-- E: serial identifier -->
<textvariable="number"/>
</group>
</layout>
</bibliography>
</style>
mwe.bib
@article{a_condition,
author = {Ann Author},
title = {A paper},
journal = {Journal of Examples},
year = {2020},
}
@techreport{b_delimiter,
author = {Ben Bookman},
title = {A report},
address = {London},
year = {2021},
}
@book{c_punctuation,
editor = {Cora Curator},
title = {A collection},
publisher = {Some Press},
year = {2022},
}
@article{d_others,
author = {Dana Dean and Eli East and Finn Frost and Gale Gray and others},
title = {A many-author paper},
journal = {Journal of Examples},
year = {2023},
}
@article{d2_others_few,
author = {Dana Dean and others},
title = {A two-token author list},
journal = {Journal of Examples},
year = {2023},
}
@article{d3_others_exact,
author = {Dana Dean and Eli East and Finn Frost and others},
title = {Three named authors plus others},
journal = {Journal of Examples},
year = {2023},
}
@techreport{e_serial,
author = {Hana Hill},
title = {A numbered release},
number = {USDL-26-0599},
year = {2024},
}
Ann Author | A-MISSED |.
Ben Bookman | A-MISSED | London, .
A-MISSED | Cora Curator (ed..).
Dana Dean et al. | A-MISSED |.
Dana Dean and others | A-MISSED |.
Dana Dean et al. | A-MISSED |.
Hana Hill | A-MISSED | USDL-26–599.
Expected output
Ann Author | A-REACHED:Journal of Examples.
Ben Bookman | A-MISSED | London.
A-MISSED | Cora Curator (ed.).
Dana Dean et al. | A-REACHED:Journal of Examples.
Dana Dean et al. | A-REACHED:Journal of Examples.
Dana Dean, Eli East, and Finn Frost … | A-REACHED:Journal of Examples. <- see D
Hana Hill | A-MISSED | USDL-26-0599.
The name list on line 6 is the one case where the correct output is a design decision rather than
a given; see D. Every other line above is what citeproc-js produces from the same inputs.
A — type with several values and match="all": silent, document-wide content loss
A-MISSED appears on all seven entries, four of which arearticle-journal. The branch is
unreachable.
This is the mechanism of #258, which was closed as csl-upstream on the reasoning that match="all" over a multi-valued attribute requires every value to hold, so an item — which has
exactly one type — can never satisfy it. I am not disputing that reading; the spec text
supports it:
"all" – (default), element only tests "true" when all conditions test "true" for all given
test values
The problem is what the user experiences. Two data points:
The pattern is live in the official style repository.german-council-of-economic-experts.csl
on citation-style-language/styles@master currently contains, unmodified:
Rendered by citeproc-js, both branches produce output. Rendered by hayagriva, the first
removes every journal title in the bibliography and the second removes every day/month
date. My 280-page document had zero journal titles and I did not notice for a long time,
because nothing is malformed — the information is simply absent.
Fixing styles one at a time upstream does not protect Typst users. The upstream issue
opened out of Only first key in multiple key if-clause is parsed #258 (citation-style-language/styles#7360) concerned a different style; this
one still carries the pattern. Anyone who picks a published style has no way to know their
bibliography is lossy.
What I would ask for — a diagnostic, not a semantic change. When an <if>/<else-if> carries
a type attribute with two or more values and an effective match="all", the condition is statically unsatisfiable: unlike variable="author editor", where match="all" is meaningful,
an item has exactly one type. A warning at style-parse time would have saved me days and cannot
produce false positives:
warning: condition can never be true: `type` lists 3 values with match="all"
--> german-council-of-economic-experts.csl:212
help: an item has exactly one type; did you mean match="any"?
If you would rather match citeproc-js, treating a multi-valued type as a disjunction is safe
for the same reason — match="all" on type can only ever be an authoring mistake. But the
warning alone would close the gap that actually hurt.
B — cs:group inserts its delimiter around a <date> that renders nothing
Ben Bookman | … | London, — the entry has year = {2021}, and the <date> requests only day and month, so it renders nothing. The group's ", " is inserted anyway.
Isolated probe, same mwe.bib, a_condition (year-only date, no publisher-place):
P2 and P3 are correct — the only variable called is empty, so the whole group is suppressed per
spec. P1 shows the defect in isolation: issued is not empty, so the group is correctly kept,
but the <date> produces no text and is nonetheless counted as content when the delimiter is
placed. A rendering element that emits nothing should not attract a delimiter.
This looks like the already-tracked group_SuppressTermWhenNoOutputFromPartialDate in #327;
the probe above may be a usable regression test. It is also the most damaging of the three
tracked ones in practice, because the stray separator lands in front of the layout suffix and
produces …, London,. — a comma before a full stop, which is never correct in any style, so
there is no configuration that makes it acceptable.
C — suffix adds a period after a term that already ends in one
Cora Curator (ed..) from <label form="short" suffix=".)"/>, because the short editor term
is already ed.. citeproc-js collapses this; the official suite covers it as bugreports_DuplicateTerminalPunctuationInBibliography, listed as failing in #327, and #411 discusses the existing apply_suffix de-duplication that evidently does not reach this
path (the suffix here is .), not ., so a plain "already ends with the suffix" test misses it).
Reported only to record that it occurs with a published style: the same construct appears twice
in german-council-of-economic-experts.csl and produced 18 occurrences of (Hrsg..) in my
document, against 0 in the citeproc-js rendering.
D — BibTeX and others becomes a person named "others"
and others at the end of a name list is the standard BibTeX/biblatex spelling of "et al.", and
it is what reference managers emit when a source has more authors than were entered. hayagriva
parses it into a person whose family name is others, which causes two distinct harms.
1. It is rendered literally. This one is unambiguous:
input: author = {Dana Dean and others}
actual: Dana Dean and others
expected: Dana Dean et al.
2. It is counted as a name, so et-al-min fires one author early.
input: author = {Dana Dean and Eli East and Finn Frost and others} with et-al-min="4"
actual: Dana Dean et al.
Three named authors against a threshold of four should not have collapsed at all. The output looks correct but was produced by the wrong mechanism — the phantom fourth name crossed the
threshold. The same off-by-one presumably reaches sorting and disambiguation, where it is
invisible.
What the right output is here is a design decision I would not want to prejudge: biblatex treats others as forcing "et al." irrespective of maxnames, whereas CSL-JSON has no marker for a
truncated list at all, so there is no obvious mapping. The defensible minimum is that others
must not become a person: it should neither print as a name nor count towards et-al-min.
I could not find an issue for this; the closest is the sample output in #326, which shows R. Aaij others.
E — a serial number containing hyphens is parsed as a numeric range
input: number = {USDL-26-0599}
actual: USDL-26–599
expected: USDL-26-0599
Two separate corruptions: the second hyphen becomes an en dash, and the leading zero of 0599
is dropped. USDL-26-0599 is a real identifier (US Bureau of Labor Statistics news releases),
so the output is not merely ugly, it is wrong.
Not the BibTeX importer. The same corruption occurs from a native hayagriva YAML source:
e_serial_yaml:
type: reporttitle: A numbered releaseauthor: Hana Hillserial-number: USDL-26-0599date: 2024
actual: Hana Hill | … | USDL-26–599.
This survives #445 ("Fix parsing of atypical page numbers and serial numbers", 0.10.0). Escaping
does not help: {{USDL-26-0599}} and a non-breaking hyphen both render the same way. The only
workaround I found was to abandon the field and fold the identifier into a text field.
Relationship to existing issues
Item
Status
Reference
A — unsatisfiable type + match="all"
known, closed as csl-upstream
#258, styles#7360. New here: the pattern is live in a published style, and there is no diagnostic.
B — delimiter around empty <date>
known, tracked as a failing test
#327 (group_SuppressTermWhenNoOutputFromPartialDate). New here: isolated 12-line probe.
Happy to split this into separate issues — I filed it as one because the five were found together
in a single document and the common thread is what I think matters most: each one fails silently
and produces plausible-looking output, so the only way to find them is to render the same style in
a second processor.
I am filing against typst/hayagriva since all five are in the bibliography engine rather than in
Typst itself; say the word if you would prefer them in typst/typst.
Summary
While re-typesetting a ~280-page economics report in Typst, I compared the bibliography against
a rendering of the same CSL style and the same source data produced by Word/Zotero
(citeproc-js). Five reproducible divergences turned up. Three are already tracked; two appear to
be unreported. All five are in the MWE below.
The one I would like to highlight is not a rendering difference at all but a diagnostics gap:
a published, unmodified style from
citation-style-language/stylescontains conditions that cannever be true. hayagriva evaluates them per spec and therefore drops a field for every entry in
the document — with no warning, no error, and output that looks perfectly well-formed. In my
case that was every journal title in the entire bibliography. I only found it by diffing against
another processor.
Environment
Cargo.lockat tagv0.15.1) — current release.csl, same dataMinimal reproduction
Three files,
typst compile mwe.typ. Every construct is exercised by a literal marker so theoutput can be read without a reference.
mwe.cslmwe.bibmwe.typActual output
Expected output
The name list on line 6 is the one case where the correct output is a design decision rather than
a given; see D. Every other line above is what citeproc-js produces from the same inputs.
A —
typewith several values andmatch="all": silent, document-wide content lossA-MISSEDappears on all seven entries, four of which arearticle-journal. The branch isunreachable.
This is the mechanism of #258, which was closed as
csl-upstreamon the reasoning thatmatch="all"over a multi-valued attribute requires every value to hold, so an item — which hasexactly one type — can never satisfy it. I am not disputing that reading; the spec text
supports it:
The problem is what the user experiences. Two data points:
The pattern is live in the official style repository.
german-council-of-economic-experts.cslon
citation-style-language/styles@mastercurrently contains, unmodified:Rendered by citeproc-js, both branches produce output. Rendered by hayagriva, the first
removes every journal title in the bibliography and the second removes every day/month
date. My 280-page document had zero journal titles and I did not notice for a long time,
because nothing is malformed — the information is simply absent.
Fixing styles one at a time upstream does not protect Typst users. The upstream issue
opened out of Only first key in multiple key if-clause is parsed #258 (
citation-style-language/styles#7360) concerned a different style; thisone still carries the pattern. Anyone who picks a published style has no way to know their
bibliography is lossy.
What I would ask for — a diagnostic, not a semantic change. When an
<if>/<else-if>carriesa
typeattribute with two or more values and an effectivematch="all", the condition isstatically unsatisfiable: unlike
variable="author editor", wherematch="all"is meaningful,an item has exactly one type. A warning at style-parse time would have saved me days and cannot
produce false positives:
If you would rather match citeproc-js, treating a multi-valued
typeas a disjunction is safefor the same reason —
match="all"ontypecan only ever be an authoring mistake. But thewarning alone would close the gap that actually hurt.
B —
cs:groupinserts its delimiter around a<date>that renders nothingBen Bookman | … | London,— the entry hasyear = {2021}, and the<date>requests onlydayandmonth, so it renders nothing. The group's", "is inserted anyway.Isolated probe, same
mwe.bib,a_condition(year-only date, nopublisher-place):P2 and P3 are correct — the only variable called is empty, so the whole group is suppressed per
spec. P1 shows the defect in isolation:
issuedis not empty, so the group is correctly kept,but the
<date>produces no text and is nonetheless counted as content when the delimiter isplaced. A rendering element that emits nothing should not attract a delimiter.
This looks like the already-tracked
group_SuppressTermWhenNoOutputFromPartialDatein #327;the probe above may be a usable regression test. It is also the most damaging of the three
tracked ones in practice, because the stray separator lands in front of the layout suffix and
produces
…, London,.— a comma before a full stop, which is never correct in any style, sothere is no configuration that makes it acceptable.
C —
suffixadds a period after a term that already ends in oneCora Curator (ed..)from<label form="short" suffix=".)"/>, because the shorteditortermis already
ed.. citeproc-js collapses this; the official suite covers it asbugreports_DuplicateTerminalPunctuationInBibliography, listed as failing in #327, and#411 discusses the existing
apply_suffixde-duplication that evidently does not reach thispath (the suffix here is
.), not., so a plain "already ends with the suffix" test misses it).Reported only to record that it occurs with a published style: the same construct appears twice
in
german-council-of-economic-experts.csland produced 18 occurrences of(Hrsg..)in mydocument, against 0 in the citeproc-js rendering.
D — BibTeX
and othersbecomes a person named "others"and othersat the end of a name list is the standard BibTeX/biblatex spelling of "et al.", andit is what reference managers emit when a source has more authors than were entered. hayagriva
parses it into a person whose family name is
others, which causes two distinct harms.1. It is rendered literally. This one is unambiguous:
2. It is counted as a name, so
et-al-minfires one author early.Three named authors against a threshold of four should not have collapsed at all. The output
looks correct but was produced by the wrong mechanism — the phantom fourth name crossed the
threshold. The same off-by-one presumably reaches sorting and disambiguation, where it is
invisible.
What the right output is here is a design decision I would not want to prejudge: biblatex treats
othersas forcing "et al." irrespective ofmaxnames, whereas CSL-JSON has no marker for atruncated list at all, so there is no obvious mapping. The defensible minimum is that
othersmust not become a person: it should neither print as a name nor count towards
et-al-min.I could not find an issue for this; the closest is the sample output in #326, which shows
R. Aaij others.E — a serial number containing hyphens is parsed as a numeric range
Two separate corruptions: the second hyphen becomes an en dash, and the leading zero of
0599is dropped.
USDL-26-0599is a real identifier (US Bureau of Labor Statistics news releases),so the output is not merely ugly, it is wrong.
Not the BibTeX importer. The same corruption occurs from a native hayagriva YAML source:
This survives #445 ("Fix parsing of atypical page numbers and serial numbers", 0.10.0). Escaping
does not help:
{{USDL-26-0599}}and a non-breaking hyphen both render the same way. The onlyworkaround I found was to abandon the field and fold the identifier into a text field.
Relationship to existing issues
type+match="all"csl-upstreamstyles#7360. New here: the pattern is live in a published style, and there is no diagnostic.<date>group_SuppressTermWhenNoOutputFromPartialDate). New here: isolated 12-line probe.bugreports_DuplicateTerminalPunctuationInBibliography), #411and othersas a literal nameHappy to split this into separate issues — I filed it as one because the five were found together
in a single document and the common thread is what I think matters most: each one fails silently
and produces plausible-looking output, so the only way to find them is to render the same style in
a second processor.
I am filing against
typst/hayagrivasince all five are in the bibliography engine rather than inTypst itself; say the word if you would prefer them in
typst/typst.