Reverse-chronological numbering for publication lists in \(\LaTeX\) and with citeproc.el

Published: April 06, 2025

Intro

PhD Comics #1995

One thing that always bothered me about my CV and publications page was that although I managed to list my publications in reverse chronological order, i.e., with the newest publications at the top, the numbering of the list of publications was still ascending. So, until now, my publication list was something like:

1. 2025 publication
2. 2024 publication
3. 2022 publication

I found this annoying because as soon as a new publication is added to the list, the number labels of all other publications had to change. The correct way to do this is to use reverse-chronological sorting and descending numbering, i.e., what we want is:

3. 2025 publication
2. 2024 publication
1. 2022 publication

I finally got around to fixing this, but I don't particularly love my implementation. I'll blog about it regardless.

\(\LaTeX\) CV

The fix for \(\LaTeX\) is relatively straightforward and basically an application of this and this TeX.SX answer. However, instead of fixing the alignment issue described in the first SO answer by computing the labelnumberwidth on a per-refsection basis, I want the same labelnumberwidth for all refsections. I thus just hardcode a number that looks good and will accommodate lists with lengths of up to three digits (ambitious, I know…)

% publications
\usepackage[backend=biber,style=ieee,sorting=ydnt]{biblatex}
\defbibheading{noheading}[References]{}

% force the same label width on all label numbers
\DeclareFieldFormat{labelnumberwidth}{\makebox[16pt][l]{#1\adddot}}
% \DeclareFieldFormat{labelnumberwidth}{#1\adddot}
\DeclareFieldFormat{apacase}{#1}
\DeclareFieldFormat[misc]{title}{\mkbibquote{#1}}

% Count total number of entries in each refsection
\AtDataInput{%
  \csnumgdef{entrycount:\therefsection}{%
    \csuse{entrycount:\therefsection}+1}}

% Print the labelnumber as the total number of entries in the
% current refsection, minus the actual labelnumber, plus one
\DeclareFieldFormat{labelnumber}{\mkbibdesc{#1}}
\newrobustcmd*{\mkbibdesc}[1]{%
  \number\numexpr\csuse{entrycount:\therefsection}+1-#1\relax}

% the annotation in the above files will highlight my name whenenver it is
% printed
\renewcommand*{\mkbibnamegiven}[1]{%
  \ifitemannotation{highlight}
    {\textbf{#1}}
    {#1}}

\renewcommand*{\mkbibnamefamily}[1]{%
  \ifitemannotation{highlight}
    {\textbf{#1}}
    {#1}}

% ...

I instantiate my different sections (conference papers, journal papers, etc.) in my CV as follows:

\ressubsec{Conference Talks and Proceedings}
\begin{refsection}[../bib/conf.bib]
\nocite{*}
\printbibliography[heading=noheading]
\end{refsection}

While this doesn't seem super elegant, it mostly works.

citeproc.el

For my publications page, I use citeproc.el to render the bibliographies from BiBTeX to HTML. I have talked about this method already previously here and here. Getting reverse numbering to work with this system was not as easy. First, I tried to use features in the CSL spec to make this happen. The best discussion on this that I could find online was on the Zotero forums. The proposed method here is to modify the CSL spec to include

<bibliography>
<sort>
      <key macro="issued" sort="ascending"/>
      <key macro="citation-number" sort="descending"/>
</sort>
...
</bibliography>

inside of the <bibliography> tag. This didn't appear to work with citeproc.el. It is not immediately clear to my why it doesn't work, but I suspect it is because the citation-number variable gets updated after the items are sorted. (I haven't opened a GitHub issue on this yet, but I will try.)

My solution ended up relying on the workflow to render isolated references using citeproc.el, described here. Basically, my previous method to generate the HTML publication list was to first create a full-blown citeproc.el citation processor object. I then used parsebib to pull out all citation keys from the .bib file before adding all of them to the citation processor as if they were cited in some text. I then used the citeproc-render-bib function to render the bibliography.

My new method now skips the creation of a full citation processor object and instead uses parsebib (and some other, citeproc.el-internal functions, which is why I don't love this approach) to load all bibliography items from the bib file directly. I then sort those bibliography items manually using Emacs Lisp before using the more lightweight function citeproc-render-item, as described in the manual.

;; generate a list of publications in reverse chronological order using
;; `citeproc.el'. for some reason, this one requires some more massaging...
(defun date-from-csl-json-item (obj &optional key)
  ;; OBJ is one item of the list returned from `citeproc-hash-itemgetter-from-any'. this is a hack.
  (let* ((csldate (car (citeproc-date-parse (alist-get (or key 'issued) (cdr obj)) ))))
    (date-to-time (s-join "-" (list (format "%4d" (or (citeproc-date-year csldate)
                                                      (string-to-number (format-time-string "%Y" (current-time)))))
                                    (format "%02d" (or (citeproc-date-month csldate) 12))
                                    (format "%02d" (or (citeproc-date-day csldate) 28)))))))
(defun generate-bib-html-reverse (relfile)
  (let* ((infile (file-name-concat my-website-bib-dir relfile))
         ;; read all items and sort them in reverse chronological order
         (items (with-temp-buffer
                  (insert-file-contents infile)
                  (let* ((raw (car (parsebib-parse-bib-buffer :expand-strings t :inheritance t))) ; parse .bib with parsebib
                         (parsed))
                    (maphash (lambda (k v) (push (cons k (citeproc-blt-entry-to-csl v t t)) parsed)) ; convert to CSL with citeproc
                             raw)
                    parsed)))
         (sorted-items (sort items ; sort by date
                             (lambda (a b)
                               (not (time-less-p (date-from-csl-json-item a)
                                                 (date-from-csl-json-item b))))))
         (nitem (length sorted-items))
         ;; create the bare-bones style object
         (csl-style (file-name-concat my-website-bib-dir "ieee-mod.csl"))
         (csl-locale (file-name-concat my-website-bib-dir "locales-en-US.xml"))
         (style (citeproc-create-style csl-style (citeproc-locale-getter-from-dir my-website-bib-dir) "en-US" t)))
    ;; render the bibliography
    (with-temp-buffer
      (insert "<div class=\"csl-bib-body\">")
      (mapcar (lambda (item)
                (insert "<div class=\"csl-entry\">\n")
                (insert (format "<div class=\"csl-left-margin\">%d.</div>" nitem))
                (cl-decf nitem)
                (insert (citeproc-render-item (cdr item) style 'bib 'html))
                (insert "</div>\n"))
              sorted-items)
      (insert "</div>\n")
      ;; highlight my name
      ;; FIXME should be possible with
      ;; `citeproc-name-postprocess-functions', but this seems to not
      ;; be supported in my version. (see
      ;; https://andras-simonyi.github.io/org-csl-cv-bib-tutorial/)
      (goto-char (point-min))
      (replace-regexp (rx "D." (or (* "&nbsp;") (* whitespace)) "Ogbe") "<b>D.&nbsp;Ogbe</b>")
      (buffer-substring (point-min) (point-max)))))

While this all works, I have a feeling that it is too custom and ultimately too brittle. I'd love to have a solution that is completely contained in the CSL spec instead. Maybe at some point in the future…

P.S.: Since I was messing with the publications page anyways, I decided to spice it up and added some photos!