Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Assisted-by: GLM 5.3
208 lines
7.0 KiB
Go
208 lines
7.0 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
|
|
|
package biblio
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseStructuredAndRaw(t *testing.T) {
|
|
entries := Parse([]map[string]any{
|
|
{
|
|
"title": "Observational evidence from supernovae",
|
|
"authors": []any{
|
|
map[string]any{"name": "Riess, A. G.", "orcid": "0000-0002-1825-0097"},
|
|
"Filippenko, A. V.",
|
|
},
|
|
"venue": "The Astronomical Journal",
|
|
"year": "1998",
|
|
"doi": "doi:10.1103/PhysRevD.59.103502.",
|
|
},
|
|
{
|
|
"raw": "Goldhaber, G. a kol., arXiv:astro-ph/0408076, doi:10.1088/1475-7516/2004/12/010.",
|
|
},
|
|
})
|
|
if len(entries) != 2 {
|
|
t.Fatalf("entries = %d", len(entries))
|
|
}
|
|
first := entries[0]
|
|
if first.Num != 1 || first.Title == "" || len(first.Authors) != 2 {
|
|
t.Fatalf("first = %+v", first)
|
|
}
|
|
if first.Authors[0].ORCID != "0000-0002-1825-0097" {
|
|
t.Fatalf("author orcid lost: %+v", first.Authors[0])
|
|
}
|
|
if first.DOI != "10.1103/PhysRevD.59.103502" {
|
|
t.Fatalf("doi not cleaned: %q", first.DOI)
|
|
}
|
|
second := entries[1]
|
|
if second.Num != 2 || second.DOI != "10.1088/1475-7516/2004/12/010" ||
|
|
second.ArXiv != "astro-ph/0408076" {
|
|
t.Fatalf("raw entry not enriched: %+v", second)
|
|
}
|
|
}
|
|
|
|
func TestListHTMLAndPlace(t *testing.T) {
|
|
entries := Parse([]map[string]any{
|
|
{"title": "Alpha", "doi": "10.1000/alpha", "authors": []any{"A. Author"}},
|
|
})
|
|
list := ListHTML(entries)
|
|
for _, want := range []string{
|
|
`<section class="refs" id="references">`, `<li id="ref-1">`,
|
|
`href="https://doi.org/10.1000/alpha"`, "orcid", "A. Author.",
|
|
} {
|
|
if want == "orcid" {
|
|
if strings.Contains(list, want) {
|
|
t.Fatalf("unexpected orcid: %s", list)
|
|
}
|
|
continue
|
|
}
|
|
if !strings.Contains(list, want) {
|
|
t.Fatalf("list missing %q:\n%s", want, list)
|
|
}
|
|
}
|
|
// The marker is replaced in place; without it the list is appended.
|
|
body := "<p>Intro.</p>\n<p>" + Marker + "</p>\n<p>After.</p>\n"
|
|
placed := Place(body, entries)
|
|
if !strings.Contains(placed, "Intro.") || !strings.Contains(placed, "After.") ||
|
|
strings.Contains(placed, Marker) {
|
|
t.Fatalf("marker placement wrong:\n%s", placed)
|
|
}
|
|
if !strings.HasSuffix(Place("<p>Only.</p>", entries), "</section>\n") {
|
|
t.Fatal("append mode missing")
|
|
}
|
|
if Place("<p>x</p>", nil) != "<p>x</p>" {
|
|
t.Fatal("empty entries should not change the body")
|
|
}
|
|
}
|
|
|
|
func TestLinkCitationsRewritesTextOnly(t *testing.T) {
|
|
entries := Parse([]map[string]any{{"title": "A"}, {"title": "B"}})
|
|
html := "<p>Work [1] and [2], even [3] out of range.</p>" +
|
|
"<p>Code stays: <code>list[1] = 2</code>.</p>" +
|
|
"<pre>array[2]=x</pre>" +
|
|
`<a href="/x#ref-1">text [1] inside link</a>` +
|
|
`<a href="/y" data-q="[2]">href untouched</a>`
|
|
out := LinkCitations(html, entries)
|
|
if !strings.Contains(out, `<a class="ref-cite" href="#ref-1">[1]</a>`) {
|
|
t.Fatalf("inline [1] not linked:\n%s", out)
|
|
}
|
|
if strings.Contains(out, `href="#ref-3"`) {
|
|
t.Fatal("out-of-range [3] was linked")
|
|
}
|
|
if !strings.Contains(out, "<code>list[1] = 2</code>") {
|
|
t.Fatal("code content was rewritten")
|
|
}
|
|
if !strings.Contains(out, "<pre>array[2]=x</pre>") {
|
|
t.Fatal("pre content was rewritten")
|
|
}
|
|
// Inside an existing anchor the marker stays text; no nesting.
|
|
if !strings.Contains(out, ">text [1] inside link</a>") {
|
|
t.Fatalf("anchor text was rewritten:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, `data-q="[2]"`) {
|
|
t.Fatal("attribute value was rewritten")
|
|
}
|
|
}
|
|
|
|
func TestStructuredEntryStripsTokensFromText(t *testing.T) {
|
|
entries := Parse([]map[string]any{{
|
|
"title": "Timescale stretch parameterization, doi:10.1088/1475-7516/2004/12/010.",
|
|
"authors": []any{"Goldhaber, G. a kol."},
|
|
}})
|
|
e := entries[0]
|
|
if e.DOI != "10.1088/1475-7516/2004/12/010" {
|
|
t.Fatalf("doi not pulled out: %q", e.DOI)
|
|
}
|
|
line := ListHTML(entries)
|
|
if got := strings.Count(line, "refs-link"); got != 1 {
|
|
t.Fatalf("expected one identifier link, got %d:\n%s", got, line)
|
|
}
|
|
if before := line[:strings.Index(line, `<a `)]; strings.Contains(before, "10.1088") {
|
|
t.Fatalf("identifier left as prose before the link:\n%s", before)
|
|
}
|
|
if !strings.Contains(line, `href="https://doi.org/10.1088/1475-7516/2004/12/010"`) {
|
|
t.Fatalf("resolver link missing:\n%s", line)
|
|
}
|
|
}
|
|
|
|
func TestCitationsJSONLDShape(t *testing.T) {
|
|
entries := Parse([]map[string]any{
|
|
{
|
|
"title": "Alpha",
|
|
"authors": []any{map[string]any{"name": "A. Author", "orcid": "0000-0002-1825-0097"}},
|
|
"venue": "Journal", "year": "2020", "doi": "10.1000/a",
|
|
},
|
|
})
|
|
cits := Citations(entries)
|
|
if len(cits) != 1 {
|
|
t.Fatalf("citations = %v", cits)
|
|
}
|
|
c := cits[0]
|
|
if c["@type"] != "ScholarlyArticle" || c["identifier"] != "https://doi.org/10.1000/a" {
|
|
t.Fatalf("citation shape wrong: %v", c)
|
|
}
|
|
persons, ok := c["author"].([]map[string]any)
|
|
if !ok || persons[0]["identifier"] != "https://orcid.org/0000-0002-1825-0097" {
|
|
t.Fatalf("author orcid missing: %v", c["author"])
|
|
}
|
|
}
|
|
|
|
func TestInternalCrossLinks(t *testing.T) {
|
|
entries := Parse([]map[string]any{
|
|
{"num": int64(1), "title": "Teorie deterministického substrátu", "doi": "10.5555/tdssc.2026"},
|
|
{"num": int64(2), "raw": "Balvín, P.: TDSSC, viz doi:10.5555/tdssc.2026; externí doi:10.9999/other (2020)."},
|
|
{"num": int64(3), "title": "Externí práce", "doi": "10.9999/other"},
|
|
{"num": int64(4), "title": "Bez identifikátoru"},
|
|
})
|
|
entries[0].Internal = "/api/volumen/posts/tdssc"
|
|
entries[1].Internal = "/api/volumen/posts/tdssc"
|
|
|
|
list := ListHTML(entries)
|
|
for _, want := range []string{
|
|
`href="/api/volumen/posts/tdssc">doi:10.5555/tdssc.2026<`,
|
|
`href="https://doi.org/10.9999/other">doi:10.9999/other<`,
|
|
} {
|
|
if !strings.Contains(list, want) {
|
|
t.Fatalf("list missing %q:\n%s", want, list)
|
|
}
|
|
}
|
|
if strings.Count(list, `href="/api/volumen/posts/tdssc"`) != 2 {
|
|
t.Fatalf("internal link count wrong:\n%s", list)
|
|
}
|
|
|
|
cits := Citations(entries)
|
|
if cits[0]["url"] != "/api/volumen/posts/tdssc" {
|
|
t.Fatalf("citation url not internal: %v", cits[0])
|
|
}
|
|
// The canonical identifier survives unchanged next to the internal link.
|
|
if cits[0]["identifier"] != "https://doi.org/10.5555/tdssc.2026" {
|
|
t.Fatalf("citation identifier changed: %v", cits[0])
|
|
}
|
|
if _, ok := cits[2]["url"]; ok {
|
|
t.Fatalf("external citation gained a url: %v", cits[2])
|
|
}
|
|
if _, ok := cits[3]["identifier"]; ok {
|
|
t.Fatalf("bare citation gained an identifier: %v", cits[3])
|
|
}
|
|
}
|
|
|
|
// A raw entry's internal link is escaped like every other href: the slug
|
|
// it names comes from the store, and a hand-edited file could carry a
|
|
// quote that must not open an attribute in the rendered list.
|
|
func TestRawEntryEscapesInternalLink(t *testing.T) {
|
|
entries := Parse([]map[string]any{
|
|
{"raw": "Viz doi:10.5555/hostile.2026 (2020).", "doi": "10.5555/hostile.2026"},
|
|
})
|
|
entries[0].Internal = `/api/volumen/posts/x" onmouseover="alert(1)`
|
|
out := entries[0].identifierLinks()
|
|
if strings.Contains(out, `" onmouseover=`) {
|
|
t.Fatalf("the internal link broke out of its attribute: %q", out)
|
|
}
|
|
if !strings.Contains(out, "href=\"/api/volumen/posts/x" onmouseover="alert(1)\"") {
|
|
t.Fatalf("the internal link lost its target: %q", out)
|
|
}
|
|
}
|