Skip to content

Five bibliography rendering defects, one self-contained MWE (hayagriva 0.10.1 / Typst 0.15.1) #505

Description

@Braintelligence

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/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"?>
<style xmlns="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>
      <text variable="citation-number"/>
    </layout>
  </citation>
  <bibliography>
    <layout suffix=".">
      <group delimiter=" | ">
        <!-- D: BibTeX "and others" -->
        <names variable="author" et-al-min="4" et-al-use-first="1">
          <name and="text" delimiter=", "/>
          <et-al term="et-al"/>
        </names>

        <!-- A: condition that can never be true (an item has exactly one type) -->
        <choose>
          <if type="article-journal article-magazine" match="all">
            <text value="A-REACHED:"/>
            <text variable="container-title"/>
          </if>
          <else>
            <text value="A-MISSED"/>
          </else>
        </choose>

        <!-- B: group delimiter around a date that renders nothing -->
        <group delimiter=", ">
          <text variable="publisher-place"/>
          <date variable="issued">
            <date-part name="day" suffix=". "/>
            <date-part name="month"/>
          </date>
        </group>

        <!-- C: suffix period after a term that already ends in a period -->
        <names variable="editor">
          <name/>
          <label prefix=" (" form="short" suffix=".)"/>
        </names>

        <!-- E: serial identifier -->
        <text variable="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},
}
mwe.typ
#set page(width: 16cm, height: auto, margin: 1cm)
#set text(size: 9pt, lang: "en")

#cite(<a_condition>, form: none)
#cite(<b_delimiter>, form: none)
#cite(<c_punctuation>, form: none)
#cite(<d_others>, form: none)
#cite(<d2_others_few>, form: none)
#cite(<d3_others_exact>, form: none)
#cite(<e_serial>, form: none)

#bibliography("mwe.bib", style: "mwe.csl", title: none)

Actual output

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 are article-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:

  1. The pattern is live in the official style repository. german-council-of-economic-experts.csl
    on citation-style-language/styles@master currently contains, unmodified:

    <else-if type="article-journal article-magazine article-newspaper" match="all">
      <text variable="container-title"/>
    <if type="article-newspaper manuscript speech legal_case legislation interview
              paper-conference pamphlet article bill report personal_communication" match="all">

    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.

  2. 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):

<text value="P1["/>
<group delimiter="+">
  <text value="A"/>
  <date variable="issued"><date-part name="day"/></date>
  <text value="B"/>
</group>
<text value="] P2["/>
<group delimiter="+">
  <text value="A"/>
  <group delimiter=","><text variable="publisher-place"/></group>
  <text value="B"/>
</group>
<text value="] P3["/>
<group delimiter="+">
  <text value="A"/>
  <text variable="publisher-place"/>
  <text value="B"/>
</group>
<text value="]"/>
actual:   P1[A++B] P2[] P3[]
expected: P1[A+B]  P2[] P3[]

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: report
  title: A numbered release
  author: Hana Hill
  serial-number: USDL-26-0599
  date: 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.
C — doubled terminal punctuation known, tracked as a failing test #327 (bugreports_DuplicateTerminalPunctuationInBibliography), #411
D — and others as a literal name no issue found cf. sample output in #326
E — serial number parsed as a range no issue found; not fixed by #445 cf. #445, #170, #463

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions