150 lines
4.6 KiB
Go
150 lines
4.6 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|||
|
|
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||
|
|
|
||
|
|
package post
|
||
|
|
|
||
|
|
import (
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"sourcedock.dev/petrbalvin/volumen/internal/biblio"
|
||
|
|
)
|
||
|
|
|
||
|
|
// refsSample is a full post file with a [[refs]] array of tables, run
|
||
|
|
// through the real frontmatter parser so the metadata shape the store
|
||
|
|
// actually delivers is what is under test.
|
||
|
|
const refsSample = `+++
|
||
|
|
title = "Citující práce"
|
||
|
|
slug = "cite"
|
||
|
|
lang = "cs"
|
||
|
|
date = 2026-08-18
|
||
|
|
|
||
|
|
[[refs]]
|
||
|
|
title = "Observational evidence from supernovae"
|
||
|
|
authors = ["Riess, A. G. a kol."]
|
||
|
|
venue = "The Astronomical Journal"
|
||
|
|
year = 1998
|
||
|
|
doi = "doi:10.1103/PhysRevD.59.103502"
|
||
|
|
|
||
|
|
[[refs]]
|
||
|
|
raw = "Goldhaber, G. a kol., arXiv:astro-ph/0408076"
|
||
|
|
+++
|
||
|
|
|
||
|
|
První tvrzení [1], druhé tvrzení [2] a inline kód ` + "`list[2] = 3`" + `.
|
||
|
|
|
||
|
|
` + "```" + `go
|
||
|
|
pole[1] se nemění
|
||
|
|
` + "```" + `
|
||
|
|
|
||
|
|
` + biblio.Marker + `
|
||
|
|
`
|
||
|
|
|
||
|
|
func TestRefsParseAndWeave(t *testing.T) {
|
||
|
|
p := mustParse(t, refsSample)
|
||
|
|
refs := p.Refs()
|
||
|
|
if len(refs) != 2 {
|
||
|
|
t.Fatalf("refs = %d, want 2", len(refs))
|
||
|
|
}
|
||
|
|
if refs[0].Title == "" || refs[0].DOI != "10.1103/PhysRevD.59.103502" {
|
||
|
|
t.Fatalf("structured ref = %+v", refs[0])
|
||
|
|
}
|
||
|
|
if refs[1].ArXiv != "astro-ph/0408076" {
|
||
|
|
t.Fatalf("raw ref not enriched: %+v", refs[1])
|
||
|
|
}
|
||
|
|
// refs is reserved: it never leaks into the custom fields object.
|
||
|
|
if fields := p.CustomFields(); fields != nil {
|
||
|
|
if _, ok := fields["refs"]; ok {
|
||
|
|
t.Fatal("refs leaked into custom fields")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
htmlOut, err := p.HTML()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("HTML: %v", err)
|
||
|
|
}
|
||
|
|
// Inline citations become anchors, the list is spliced at the marker,
|
||
|
|
// identifiers in entries become resolver links, and bracketed
|
||
|
|
// numbers inside code stay exactly as written.
|
||
|
|
for _, want := range []string{
|
||
|
|
`<a class="ref-cite" href="#ref-1">[1]</a>`,
|
||
|
|
`<section class="refs" id="references">`,
|
||
|
|
`<li id="ref-2">`,
|
||
|
|
`href="https://doi.org/10.1103/PhysRevD.59.103502"`,
|
||
|
|
"list[2] = 3",
|
||
|
|
"pole[1] se nemění",
|
||
|
|
} {
|
||
|
|
if !strings.Contains(htmlOut, want) {
|
||
|
|
t.Fatalf("html missing %q:\n%s", want, htmlOut)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if strings.Contains(htmlOut, biblio.Marker) {
|
||
|
|
t.Fatalf("marker not replaced:\n%s", htmlOut)
|
||
|
|
}
|
||
|
|
// A post without refs renders unchanged and carries none.
|
||
|
|
bare := mustParse(t, "+++\ntitle = \"T\"\nslug = \"t\"\n+++\ntext [1]\n")
|
||
|
|
if len(bare.Refs()) != 0 {
|
||
|
|
t.Fatal("bare post has refs")
|
||
|
|
}
|
||
|
|
if html, _ := bare.HTML(); strings.Contains(html, "ref-cite") {
|
||
|
|
t.Fatal("bare post got citation links")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// A save from the editor must not drop the bibliography: the form never
|
||
|
|
// carries refs, and the merge keeps untouched keys.
|
||
|
|
func TestToFileSyncPreservesRefs(t *testing.T) {
|
||
|
|
p := mustParse(t, refsSample)
|
||
|
|
out, err := p.ToFile()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("ToFile: %v", err)
|
||
|
|
}
|
||
|
|
if !strings.Contains(out, "[[refs]]") || !strings.Contains(out, "10.1103/PhysRevD.59.103502") {
|
||
|
|
t.Fatalf("refs lost on serialise:\n%s", out)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestInternalCrossLinksAtRender(t *testing.T) {
|
||
|
|
// The cited work is published under another slug in the instance:
|
||
|
|
// the rendered list links to that post, and the entry's resolver
|
||
|
|
// link disappears from the page.
|
||
|
|
p := mustParse(t, refsSample)
|
||
|
|
p.SetLinkIndex(map[string]string{"10.1103/physrevd.59.103502": "riess-1998"})
|
||
|
|
htmlOut, err := p.HTML()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("HTML: %v", err)
|
||
|
|
}
|
||
|
|
if !strings.Contains(htmlOut, `href="/api/volumen/posts/riess-1998"`) {
|
||
|
|
t.Fatalf("internal link missing:\n%s", htmlOut)
|
||
|
|
}
|
||
|
|
if strings.Contains(htmlOut, `href="https://doi.org/10.1103/PhysRevD.59.103502"`) {
|
||
|
|
t.Fatal("external resolver survived")
|
||
|
|
}
|
||
|
|
// The arXiv entry is not published here: it keeps its resolver.
|
||
|
|
if !strings.Contains(htmlOut, `href="https://arxiv.org/abs/astro-ph/0408076"`) {
|
||
|
|
t.Fatalf("unmatched entry lost its link:\n%s", htmlOut)
|
||
|
|
}
|
||
|
|
|
||
|
|
// A post citing its own DOI must not link to itself.
|
||
|
|
self := mustParse(t, strings.Replace(refsSample, `slug = "cite"`, `slug = "cite"
|
||
|
|
doi = "10.1103/PhysRevD.59.103502"`, 1))
|
||
|
|
self.SetLinkIndex(map[string]string{"10.1103/physrevd.59.103502": "cite"})
|
||
|
|
htmlOut, err = self.HTML()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("HTML: %v", err)
|
||
|
|
}
|
||
|
|
if strings.Contains(htmlOut, `href="/api/volumen/posts/cite"`) {
|
||
|
|
t.Fatal("self-link was built")
|
||
|
|
}
|
||
|
|
if !strings.Contains(htmlOut, `href="https://doi.org/10.1103/PhysRevD.59.103502"`) {
|
||
|
|
t.Fatalf("self cite lost its resolver link:\n%s", htmlOut)
|
||
|
|
}
|
||
|
|
|
||
|
|
// RefsLinked exposes the resolution to the API layer.
|
||
|
|
linked := p.RefsLinked()
|
||
|
|
if linked[0].Internal != "/api/volumen/posts/riess-1998" {
|
||
|
|
t.Fatalf("RefsLinked[0] = %+v", linked[0])
|
||
|
|
}
|
||
|
|
if linked[1].Internal != "" {
|
||
|
|
t.Fatalf("RefsLinked[1] should be external: %+v", linked[1])
|
||
|
|
}
|
||
|
|
}
|