commit - 84954bcd2e666a66a8ab006b3311573b65575a17
commit + 89383efba9faff5db72cfb37636b84952dfd140a
blob - 2fd121479cbecb4be3d91cad9ff1b7e712aff070
blob + 705409775e17120e3823662e63b2967e508970e5
--- internal/bib/cite.go
+++ internal/bib/cite.go
import (
"fmt"
"regexp"
+ "sort"
"strings"
)
var citeRe = regexp.MustCompile(`\[@([A-Za-z0-9_:.-]+)\]`)
-// Process scans slides in order for [@key] citations, numbers them by
-// first appearance, rewrites each [@key] into a [n] markdown link to a
-// #key anchor (it never resolves anywhere -- it's just how glamour picks
-// up the theme's link_text color without hardcoding one), and appends a
-// References slide listing every cited entry in citation-number order.
-// Citation keys not found in entries are left as literal [@key] text.
+// Process scans slides for [@key] citations, numbers the cited entries
+// 1..N in alphabetical order of the reference (first author's family
+// name, then year, then title), rewrites each [@key] into a [n] markdown
+// link to a #key anchor (it never resolves anywhere -- it's just how
+// glamour picks up the theme's link_text color without hardcoding one),
+// and appends a References slide listing every cited entry in that same
+// alphabetical order. Citation keys not found in entries are left as
+// literal [@key] text.
func Process(slides []string, entries map[string]Entry) []string {
- numbers := make(map[string]int)
- var order []string
-
+ seen := make(map[string]bool)
+ var cited []string
for _, slide := range slides {
for _, m := range citeRe.FindAllStringSubmatch(slide, -1) {
key := m[1]
if _, ok := entries[key]; !ok {
continue
}
- if _, seen := numbers[key]; !seen {
- numbers[key] = len(order) + 1
- order = append(order, key)
+ if !seen[key] {
+ seen[key] = true
+ cited = append(cited, key)
}
}
}
- if len(order) == 0 {
+ if len(cited) == 0 {
return slides
}
+ order := append([]string(nil), cited...)
+ sort.SliceStable(order, func(i, j int) bool {
+ return sortKey(entries[order[i]]) < sortKey(entries[order[j]])
+ })
+ numbers := make(map[string]int, len(order))
+ for i, key := range order {
+ numbers[key] = i + 1
+ }
+
out := make([]string, len(slides))
for i, slide := range slides {
out[i] = citeRe.ReplaceAllStringFunc(slide, func(match string) string {
return out
}
+// sortKey is the alphabetical sort key for an entry: the first author's
+// family name (lowercased), then year, then title, with NUL separators so
+// shorter names sort before longer ones sharing a prefix. Entries with no
+// author fall back to their title.
+func sortKey(e Entry) string {
+ f := e.Fields
+ var name string
+ if as := splitAuthors(f["author"]); len(as) > 0 {
+ if name = strings.ToLower(as[0].Last); name == "" {
+ name = strings.ToLower(as[0].First)
+ }
+ }
+ if name == "" {
+ name = strings.ToLower(strings.TrimLeft(f["title"], `"'`))
+ }
+ return name + "\x00" + f["year"] + "\x00" + strings.ToLower(f["title"])
+}
+
func referencesSlide(order []string, entries map[string]Entry) string {
var b strings.Builder
b.WriteString("## References\n\n")
blob - 859c5d3d3cc5bd63696febf93c6c1ac7d6b6689d
blob + 01ca38f1eddfacd2a801214f74f8f27806ea6985
--- internal/bib/cite_test.go
+++ internal/bib/cite_test.go
}
}
-func TestProcess_NumbersByFirstAppearance(t *testing.T) {
+func TestProcess_NumbersAlphabetically(t *testing.T) {
slides := []string{
"intro slide, no citations",
- "see [@doe2019] and also [@smith2020]",
- "cited again: [@doe2019]",
+ "see [@smith2020] and also [@doe2019]", // Smith cited first
+ "cited again: [@smith2020]",
}
out := Process(slides, testEntries())
if out[0] != slides[0] {
t.Errorf("slide 0 should be untouched, got %q", out[0])
}
- if !strings.Contains(out[1], "[[1](#doe2019)]") || !strings.Contains(out[1], "[[2](#smith2020)]") {
- t.Errorf("slide 1 = %q, want [[1]] (doe2019, first seen) and [[2]] (smith2020)", out[1])
+ // Doe sorts before Smith, so Doe is [1] and Smith is [2] even though
+ // Smith is cited first.
+ if !strings.Contains(out[1], "[[2](#smith2020)]") || !strings.Contains(out[1], "[[1](#doe2019)]") {
+ t.Errorf("slide 1 = %q, want smith=[[2]] doe=[[1]] (alphabetical)", out[1])
}
- if !strings.Contains(out[2], "[[1](#doe2019)]") {
- t.Errorf("slide 2 = %q, want repeated citation to reuse [[1]]", out[2])
+ if !strings.Contains(out[2], "[[2](#smith2020)]") {
+ t.Errorf("slide 2 = %q, want repeated citation to reuse [[2]]", out[2])
}
refs := out[len(out)-1]
if !strings.HasPrefix(refs, "## References") {
t.Errorf("references slide should start with heading, got %q", refs)
}
- if !strings.Contains(refs, "[1] J. Doe") {
- t.Errorf("references slide missing entry 1 (doe2019): %q", refs)
+ if !strings.Contains(refs, "[1] J. Doe") || !strings.Contains(refs, "[2] J. Smith") {
+ t.Errorf("references not numbered alphabetically: %q", refs)
}
- if !strings.Contains(refs, "[2] J. Smith") {
- t.Errorf("references slide missing entry 2 (smith2020): %q", refs)
+ if strings.Index(refs, "[1] J. Doe") > strings.Index(refs, "[2] J. Smith") {
+ t.Errorf("references slide should list [1] before [2]: %q", refs)
}
}