enhance(citation): render citations server-side (#39373)

Replace citation-js with a Go port of ruby-cff, which GitHub uses for
"Cite this repository", rendering APA and BibTeX server-side and
dropping about 770KB of JS. Output matches GitHub on 1568 of 1571
real-world files, the rest are improvements over GitHub.

- `CITATION.cff` wins over `CITATION.bib`, matched case-insensitively
and through symlinks
- `CITATION.bib` is offered as-is, without APA

Signed-off-by: silverwind <me@silverwind.io>
This commit is contained in:
silverwind authored and GitHub committed 2026-10-03 02:28:34 +00:00
1 parent fb067e1115
commit 2baa5a16f8
15 files changed
+704 -361

No files matched your search

+39 -25
View File
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"net/http"
"slices"
"strconv"
"strings"
"time"
@@ -17,9 +18,11 @@ import (
repo_model "gitea.dev/models/repo"
unit_model "gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/citation"
"gitea.dev/modules/git"
"gitea.dev/modules/htmlutil"
"gitea.dev/modules/httplib"
"gitea.dev/modules/lfs"
"gitea.dev/modules/log"
repo_module "gitea.dev/modules/repository"
"gitea.dev/modules/setting"
@@ -93,32 +96,43 @@ func prepareClonePanel(ctx *context.Context) {
}
}
func prepareHomeSidebarCitationFile(entry *git.TreeEntry) func(ctx *context.Context) {
return func(ctx *context.Context) {
if entry.Name() != "" {
return
func prepareHomeSidebarCitationFile(ctx *context.Context) {
allEntries, err := ctx.Repo.Commit.Tree().ListEntries(ctx, ctx.Repo.GitRepo)
if err != nil {
ctx.ServerError("ListEntries", err)
return
}
isBlob := func(entry *git.TreeEntry) bool { return !entry.IsDir() && !entry.IsSubModule() }
for _, name := range []string{"CITATION.cff", "CITATION.bib"} {
idx := slices.IndexFunc(allEntries, func(entry *git.TreeEntry) bool { return isBlob(entry) && strings.EqualFold(entry.Name(), name) })
if idx == -1 {
continue
}
tree, err := ctx.Repo.Commit.SubTree(ctx, ctx.Repo.GitRepo, ctx.Repo.TreePath)
if err != nil {
HandleGitError(ctx, "Repo.Commit.SubTree", err)
return
}
allEntries, err := tree.ListEntries(ctx, ctx.Repo.GitRepo)
if err != nil {
ctx.ServerError("ListEntries", err)
return
}
for _, entry := range allEntries {
if entry.Name() == "CITATION.cff" || entry.Name() == "CITATION.bib" {
// Read Citation file contents
if content, err := entry.Blob(ctx.Repo.GitRepo).GetBlobContent(ctx, setting.UI.MaxDisplayFileSize); err != nil {
log.Error("checkCitationFile: GetBlobContent: %v", err)
} else {
ctx.Data["CitiationExist"] = true
ctx.PageData["citationFileContent"] = content
break
}
entry := allEntries[idx]
if entry.IsLink() {
res, err := git.EntryFollowLinks(ctx, ctx.Repo.GitRepo, ctx.Repo.Commit, entry.Name(), entry)
if err != nil || !isBlob(res.TargetEntry) {
continue
}
entry = res.TargetEntry
}
content, err := entry.Blob(ctx.Repo.GitRepo).GetBlobContent(ctx, setting.UI.MaxDisplayFileSize)
if err != nil {
log.Error("prepareHomeSidebarCitationFile: GetBlobContent: %v", err)
continue
}
if pointer, _ := lfs.ReadPointerFromBuffer([]byte(content)); pointer.IsValid() {
continue
}
apa, bibtex := "", content
if name == "CITATION.cff" {
apa, bibtex = citation.FormatCFF(content)
}
if bibtex != "" {
ctx.Data["CitationFileName"] = allEntries[idx].Name()
ctx.Data["CitationAPA"] = apa
ctx.Data["CitationBibTeX"] = bibtex
return
}
}
}
@@ -470,7 +484,7 @@ func Home(ctx *context.Context) {
checkOutdatedBranch,
prepareUpstreamDivergingInfo,
prepareHomeSidebarLicenses,
prepareHomeSidebarCitationFile(entry),
prepareHomeSidebarCitationFile,
prepareHomeSidebarLanguageStats,
prepareHomeSidebarLatestRelease,
)