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

+468
View File
@@ -0,0 +1,468 @@
// Copyright 2026 The Gitea Authors.
// Copyright 2018-2024 The Ruby Citation File Format Developers. Licensed under the Apache License, Version 2.0
// SPDX-License-Identifier: Apache-2.0
// Package citation formats CITATION.cff files, ported from the formatters of ruby-cff 1.3.0
package citation
import (
"cmp"
"maps"
"regexp"
"slices"
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"
"gitea.dev/modules/util"
"go.yaml.in/yaml/v4"
"golang.org/x/text/runes"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
)
type date struct{ time.Time }
var dateSeparators = strings.NewReplacer("/", "-", ". ", " ", ".", "-", ",", "")
func (d *date) UnmarshalYAML(node *yaml.Node) error {
value := dateSeparators.Replace(node.Value)
for _, layout := range []string{"2006-1-2", "2-1-2006", "2 Jan 2006", "2 January 2006", "Jan 2 2006", "January 2 2006"} {
if parsed, err := time.Parse(layout, value); err == nil {
d.Time = parsed
break
}
}
return nil
}
type license string
func (l *license) UnmarshalYAML(node *yaml.Node) error {
*l = license(node.Value)
if node.Kind != yaml.ScalarNode {
*l = license(inspectNode(node))
}
return nil
}
type actor struct {
Name string `yaml:"name"`
Alias string `yaml:"alias"`
FamilyNames string `yaml:"family-names"`
GivenNames string `yaml:"given-names"`
NameParticle string `yaml:"name-particle"`
NameSuffix string `yaml:"name-suffix"`
Affiliation string `yaml:"affiliation"`
City string `yaml:"city"`
Region string `yaml:"region"`
Country string `yaml:"country"`
DateStart date `yaml:"date-start"`
DateEnd date `yaml:"date-end"`
}
func (a *actor) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.ScalarNode:
a.Name = node.Value
return nil
case yaml.SequenceNode:
return nil
}
type plainActor actor
return node.Load((*plainActor)(a), yaml.WithUniqueKeys(false))
}
type metadata struct {
Type string `yaml:"type"`
Title string `yaml:"title"`
Authors []actor `yaml:"authors"`
Version string `yaml:"version"`
DOI string `yaml:"doi"`
URL string `yaml:"url"`
RepositoryCode string `yaml:"repository-code"`
License license `yaml:"license"`
DateReleased date `yaml:"date-released"`
}
type reference struct {
metadata `yaml:",inline"`
isTopLevel bool
Editors []actor `yaml:"editors"`
EditorsSeries []actor `yaml:"editors-series"`
DatePublished date `yaml:"date-published"`
Year string `yaml:"year"`
Month string `yaml:"month"`
Status string `yaml:"status"`
Journal string `yaml:"journal"`
Volume string `yaml:"volume"`
Issue string `yaml:"issue"`
Start string `yaml:"start"`
End string `yaml:"end"`
ISBN string `yaml:"isbn"`
Notes string `yaml:"notes"`
CollectionTitle string `yaml:"collection-title"`
ThesisType string `yaml:"thesis-type"`
Publisher actor `yaml:"publisher"`
Institution *actor `yaml:"institution"`
Conference actor `yaml:"conference"`
}
// FormatCFF returns the APA and BibTeX citations of a CITATION.cff file, both empty if it has no title or authors
func FormatCFF(content string) (apa, bibtex string) {
var node yaml.Node
if yaml.Unmarshal([]byte(content), &node) != nil {
return "", ""
}
retagTimestamps(&node)
var file struct {
TopLevel metadata `yaml:",inline"`
PreferredCitation *reference `yaml:"preferred-citation"`
}
if node.Load(&file, yaml.WithUniqueKeys(false)) != nil {
return "", ""
}
ref := file.PreferredCitation
if ref == nil {
ref = &reference{metadata: file.TopLevel, isTopLevel: true}
}
if ref.Title == "" || len(ref.Authors) == 0 {
return "", ""
}
return ref.formatAPA(), ref.formatBibTeX()
}
func retagTimestamps(node *yaml.Node) {
if node.ShortTag() == "!!timestamp" {
node.Tag = "!!str"
}
for _, child := range node.Content {
retagTimestamps(child)
}
}
func inspectNode(node *yaml.Node) string {
var parts []string
switch node.Kind {
case yaml.AliasNode:
if node.Alias.Kind == yaml.ScalarNode { // collection aliases can be recursive
return inspectNode(node.Alias)
}
case yaml.SequenceNode:
for _, child := range node.Content {
parts = append(parts, inspectNode(child))
}
return "[" + strings.Join(parts, ", ") + "]"
case yaml.MappingNode:
for i := 0; i < len(node.Content); i += 2 {
parts = append(parts, inspectNode(node.Content[i])+" => "+inspectNode(node.Content[i+1]))
}
return "{" + strings.Join(parts, ", ") + "}"
}
if node.ShortTag() == "!!null" {
return "nil"
}
return strconv.Quote(node.Value)
}
var statusNotes = map[string]string{
"advance-online": "Advance online publication",
"in-preparation": "Manuscript in preparation.",
"submitted": "Manuscript submitted for publication.",
}
func joinNonEmpty(sep string, parts ...string) string {
return strings.Join(util.SliceRemoveAll(parts, ""), sep)
}
func (r *reference) conferenceDates() (start, end date) {
if r.Type == "conference-paper" {
return r.Conference.DateStart, r.Conference.DateEnd
}
return date{}, date{}
}
func (r *reference) monthAndYear() (month, year string) {
when, _ := r.conferenceDates()
if when.IsZero() {
if r.Status == "in-press" {
return "", "in press"
}
if r.Year != "" {
return r.Month, r.Year
}
when = cmp.Or(r.DateReleased, r.DatePublished)
}
if when.IsZero() {
return "", ""
}
return strconv.Itoa(int(when.Month())), strconv.Itoa(when.Year())
}
func (r *reference) pages(dash string) string {
if r.Start == "" || r.End == "" || r.Start == r.End {
return r.Start
}
return r.Start + dash + r.End
}
func (r *reference) volume() string {
if r.Volume == "" || r.Issue == "" {
return r.Volume
}
return r.Volume + "(" + r.Issue + ")"
}
func (r *reference) institution() string {
if r.Institution == nil {
return r.Authors[0].Affiliation
}
return r.Institution.Name
}
func (r *reference) formatAPA() string {
authors := make([]string, 0, len(r.Authors))
for _, author := range r.Authors {
authors = append(authors, apaAuthor(author))
}
date := r.apaDate()
if date != "" {
date = "(" + date + ")"
}
version := ""
if r.Version != "" {
version = " (Version " + r.Version + ")"
}
url := cmp.Or(r.RepositoryCode, r.URL)
if r.DOI != "" {
url = "https://doi.org/" + r.DOI
}
return joinNonEmpty(". ", combineAuthors(authors), date, r.Title+version+r.apaTypeLabel(), r.apaPublicationData(), url)
}
func apaAuthor(author actor) string {
if author.Name != "" {
return author.Name
}
name := cmp.Or(author.FamilyNames, author.GivenNames, author.Alias)
if author.FamilyNames != "" && author.GivenNames != "" {
name += ", " + initials(author.GivenNames) + "."
}
if author.NameParticle != "" {
name = author.NameParticle + " " + name
}
if author.NameSuffix != "" {
name += ", " + author.NameSuffix
}
return name
}
func initials(names string) string {
parts := splitWords(names)
for i, part := range parts {
first, _ := utf8.DecodeRuneInString(part)
parts[i] = util.ToTitleCase(string(first))
}
return strings.Join(parts, ". ")
}
func splitWords(text string) []string {
return strings.FieldsFunc(text, func(char rune) bool { return char <= unicode.MaxASCII && unicode.IsSpace(char) })
}
func combineAuthors(authors []string) string {
if len(authors) == 1 {
return strings.TrimSuffix(authors[0], ".")
}
return strings.TrimSuffix(strings.Join(authors[:len(authors)-1], ", ")+", & "+authors[len(authors)-1], ".")
}
func (r *reference) apaDate() string {
start, end := r.conferenceDates()
if start.IsZero() || end.IsZero() || !start.Before(end.Time) {
_, year := r.monthAndYear()
return year
}
endLayout := "2"
if end.Month() != start.Month() {
endLayout = "January 2"
}
if end.Year() != start.Year() {
endLayout = "2006, " + endLayout
}
return start.Format("2006, January 2") + "–" + end.Format(endLayout)
}
func (r *reference) apaTypeLabel() string {
switch {
case strings.Contains(r.Type, "data"):
return " [Data set]"
case strings.Contains(r.Type, "conference"):
return " [Conference paper]"
case !r.isTopLevel && !strings.Contains(r.Type, "software"):
return ""
}
return " [Computer software]"
}
func (r *reference) apaPublicationData() string {
switch r.Type {
case "article":
return joinNonEmpty(", ", r.Journal, r.volume(), r.pages("–"), statusNotes[r.Status])
case "book":
return r.Publisher.Name
case "conference-paper":
return joinNonEmpty(", ", r.CollectionTitle, r.volume(), r.pages("–"))
case "report":
return r.institution()
case "phdthesis":
return "[" + cmp.Or(r.ThesisType, "Doctoral dissertation") + ", " + r.institution() + "]"
case "mastersthesis":
return "[" + cmp.Or(r.ThesisType, "Master's thesis") + ", " + r.institution() + "]"
case "unpublished":
return statusNotes[r.Status]
}
return ""
}
var bibtexTypeFields = map[string][]string{
"article": {"journal", "note", "number", "pages", "volume"},
"book": {"address", "editor", "isbn", "number", "pages", "publisher", "volume"},
"booklet": {"address"},
"inproceedings": {"address", "booktitle", "editor", "pages", "publisher", "series"},
"manual": {"address"},
"mastersthesis": {"address", "school", "type"},
"misc": {"pages"},
"phdthesis": {"address", "school", "type"},
"proceedings": {"address", "booktitle", "editor", "pages", "publisher", "series"},
"software": {"license", "version"},
"techreport": {"address", "institution", "number"},
"unpublished": {"note"},
}
var (
bibtexEscaper = strings.NewReplacer("&", `\&`, "%", `\%`, "$", `\$`, "#", `\#`, "_", `\_`, "{", `\{`, "}", `\}`)
keyLetters = strings.NewReplacer(
"Æ", "AE", "æ", "ae", "Ð", "D", "ð", "d", "Ø", "O", "ø", "o", "Þ", "Th", "þ", "th", "ß", "ss", "×", "x",
"Đ", "D", "đ", "d", "Ħ", "H", "ħ", "h", "ı", "i", "IJ", "IJ", "ij", "ij", "ĸ", "k", "Ŀ", "L", "ŀ", "l",
"Ł", "L", "ł", "l", "ʼn", "'n", "Ŋ", "NG", "ŋ", "ng", "Œ", "OE", "œ", "oe", "Ŧ", "T", "ŧ", "t",
)
keyUnsafeChars = regexp.MustCompile(`[^a-zA-Z0-9-]+`)
)
func keyToASCII() transform.Transformer {
return transform.Chain(
runes.Remove(runes.Predicate(func(char rune) bool {
return char > unicode.MaxASCII && (char < 'À' || char > 'ž') && char != 'ệ'
})),
norm.NFD,
runes.Remove(runes.Predicate(func(char rune) bool { return char > unicode.MaxASCII })),
)
}
func (r *reference) formatBibTeX() string {
place := r.Publisher
if r.Type == "conference-paper" {
place = r.Conference
}
editors := r.Editors
if len(editors) == 0 {
editors = r.EditorsSeries
}
typeFields := map[string]string{
"address": joinNonEmpty(", ", place.City, place.Region, place.Country),
"booktitle": bibtexEscaper.Replace(r.CollectionTitle),
"editor": bibtexActors(editors),
"institution": bibtexEscaper.Replace(r.institution()),
"isbn": bibtexEscaper.Replace(r.ISBN),
"journal": bibtexEscaper.Replace(r.Journal),
"license": bibtexEscaper.Replace(string(r.License)),
"note": statusNotes[r.Status],
"number": r.Issue,
"pages": r.pages("--"),
"publisher": bibtexEscaper.Replace(r.Publisher.Name),
"school": bibtexEscaper.Replace(r.institution()),
"series": bibtexEscaper.Replace(r.Conference.Name),
"type": r.ThesisType,
"version": bibtexEscaper.Replace(r.Version),
"volume": bibtexEscaper.Replace(r.Volume),
}
entryType := bibtexType(r.Type)
fields := map[string]string{
"author": bibtexActors(r.Authors),
"title": "{" + bibtexEscaper.Replace(r.Title) + "}",
"doi": bibtexEscaper.Replace(r.DOI),
}
for _, name := range bibtexTypeFields[entryType] {
fields[name] = typeFields[name]
}
month, year := r.monthAndYear()
if num, _ := strconv.Atoi(month); num >= 1 && num <= 12 {
fields["month"] = strings.ToLower(time.Month(num).String()[:3])
}
fields["year"] = year
fields["url"] = cmp.Or(r.RepositoryCode, r.URL)
fields["note"] = cmp.Or(fields["note"], r.Notes)
maps.DeleteFunc(fields, func(_, value string) bool { return value == "" })
lines := []string{bibtexKey(fields)}
for _, name := range slices.Sorted(maps.Keys(fields)) {
value := fields[name]
if name != "month" {
value = "{" + value + "}"
}
lines = append(lines, name+" = "+value)
}
return "@" + entryType + "{" + strings.Join(lines, ",\n") + "\n}"
}
func bibtexType(cffType string) string {
if cffType == "" || strings.Contains(cffType, "software") {
return "software"
}
switch cffType {
case "article", "book", "manual", "unpublished", "phdthesis", "mastersthesis":
return cffType
case "conference", "proceedings":
return "proceedings"
case "conference-paper":
return "inproceedings"
case "magazine-article", "newspaper-article":
return "article"
case "pamphlet":
return "booklet"
case "report":
return "techreport"
}
return "misc"
}
func bibtexActors(actors []actor) string {
names := make([]string, 0, len(actors))
for _, entry := range actors {
switch {
case entry.Name != "":
names = append(names, "{"+bibtexEscaper.Replace(entry.Name)+"}")
case entry.FamilyNames == "" && entry.GivenNames == "":
names = append(names, bibtexEscaper.Replace(entry.Alias))
default:
family := entry.FamilyNames
if entry.NameParticle != "" {
family = entry.NameParticle + " " + family
}
names = append(names, joinNonEmpty(", ", family, entry.NameSuffix, entry.GivenNames))
}
}
return strings.Join(names, " and ")
}
func bibtexKey(fields map[string]string) string {
author, _, _ := strings.Cut(fields["author"], ",")
titleWords := splitWords(fields["title"])
key := joinNonEmpty("_", author, strings.Join(titleWords[:min(3, len(titleWords))], "_"), fields["year"])
key, _, _ = transform.String(keyToASCII(), keyLetters.Replace(key))
return strings.Trim(keyUnsafeChars.ReplaceAllString(key, "_"), "_")
}
+137
View File
@@ -0,0 +1,137 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package citation
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestFormatCFF(t *testing.T) {
cases := []struct{ cff, apa, bibtex string }{
{
cff: `title: Overridden
message: &mit MIT
authors: [{family-names: Haines, given-names: Robert}, {name: "The {curly_braces} Collective"}]
title: "Software that uses the following symbols: &, %, $, #"
version: 2024-01-16
license: [*mit, Apache-2.0]
date-released: 2024-01-16
`,
apa: "Haines, R., & The {curly_braces} Collective. (2024). Software that uses the following symbols: &, %, $, # (Version 2024-01-16) [Computer software]",
bibtex: `@software{Haines_Software_that_uses_2024,
author = {Haines, Robert and {The \{curly\_braces\} Collective}},
license = {["MIT", "Apache-2.0"]},
month = jan,
title = {{Software that uses the following symbols: \&, \%, \$, \#}},
version = {2024-01-16},
year = {2024}
}`,
},
{
cff: `authors:
- family-names: Smith
given-names: Arfon M.
title: "Software citation principles"
license: MIT
preferred-citation:
authors:
- family-names: Smith
given-names: A. M.
- name: "FORCE11 Software Citation Working Group"
doi: "10.7717/peerj-cs.86"
journal: "PeerJ Computer Science"
month: 9
start: e86
title: "Software citation principles"
type: article
volume: 2
issue: 123
year: 2016
publisher:
- name: The Open Journal
`,
apa: "Smith, A. M., & FORCE11 Software Citation Working Group. (2016). Software citation principles. PeerJ Computer Science, 2(123), e86. https://doi.org/10.7717/peerj-cs.86",
bibtex: `@article{Smith_Software_citation_principles_2016,
author = {Smith, A. M. and {FORCE11 Software Citation Working Group}},
doi = {10.7717/peerj-cs.86},
journal = {PeerJ Computer Science},
month = sep,
number = {123},
pages = {e86},
title = {{Software citation principles}},
volume = {2},
year = {2016}
}`,
},
{
cff: `preferred-citation:
type: conference-paper
version: 1.10
title: "Über tools"
authors:
- family-names: Ørsted
given-names: ǰan christian
name-particle: van
name-suffix: Jr.
collection-title: "Proceedings of X & Y"
conference:
name: "Conf_2020"
city: Berlin
country: DE
date-start: 30.06.2020
date-end: 2020-07-02
start: 10
end: 20
editors:
- affiliation: Press
editors-series:
- family-names: Editor
given-names: Eve
`,
apa: "van Ørsted, J̌. C., Jr. (2020, June 30–July 2). Über tools (Version 1.10) [Conference paper]. Proceedings of X & Y, 10–20",
bibtex: `@inproceedings{van_Orsted_Uber_tools_2020,
address = {Berlin, DE},
author = {van Ørsted, Jr., ǰan christian},
booktitle = {Proceedings of X \& Y},
month = jun,
pages = {10--20},
series = {Conf\_2020},
title = {{Über tools}},
year = {2020}
}`,
},
{
cff: `thesis: &thesis {type: phdthesis, title: Thesis}
preferred-citation:
<<: *thesis
authors:
- family-names: Nguyễn
given-names: Sam
affiliation: "Uni_A"
institution: {city: Hanoi}
license: &loop [*loop]
status: in-press
notes: A note
`,
apa: "Nguyễn, S. (in press). Thesis. [Doctoral dissertation, ]",
bibtex: `@phdthesis{Nguyn_Thesis_in_press,
author = {Nguyễn, Sam},
note = {A note},
title = {{Thesis}},
year = {in press}
}`,
},
{cff: "title: No authors\nauthor:\n - name: Typo\n"},
}
for _, tc := range cases {
t.Run("", func(t *testing.T) {
t.Parallel()
apa, bibtex := FormatCFF(tc.cff)
assert.Equal(t, tc.apa, apa)
assert.Equal(t, tc.bibtex, bibtex)
})
}
}
-4
View File
@@ -6,10 +6,6 @@
"pnpm": ">= 11.0.0"
},
"dependencies": {
"@citation-js/core": "0.7.21",
"@citation-js/plugin-bibtex": "0.7.21",
"@citation-js/plugin-csl": "0.7.22",
"@citation-js/plugin-software-formats": "0.6.2",
"@codemirror/autocomplete": "6.20.3",
"@codemirror/commands": "6.11.1",
"@codemirror/lang-json": "6.0.2",
-210
View File
@@ -166,18 +166,6 @@ importers:
.:
dependencies:
'@citation-js/core':
specifier: 0.7.21
version: 0.7.21
'@citation-js/plugin-bibtex':
specifier: 0.7.21
version: 0.7.21(@citation-js/core@0.7.21)
'@citation-js/plugin-csl':
specifier: 0.7.22
version: 0.7.22(@citation-js/core@0.7.21)
'@citation-js/plugin-software-formats':
specifier: 0.6.2
version: 0.6.2
'@codemirror/autocomplete':
specifier: 6.20.3
version: 6.20.3
@@ -535,54 +523,6 @@ packages:
'@chevrotain/utils@11.1.2':
resolution: {integrity: sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA==}
'@citation-js/core@0.7.21':
resolution: {integrity: sha512-Vobv2/Yfnn6C6BVO/pvj7madQ7Mfzl83/jAWwixbemGF6ZThhGMz8++FD9hWHyHXDMYuLGa6fK68c2VsolZmTA==}
engines: {node: '>=16.0.0'}
'@citation-js/date@0.5.1':
resolution: {integrity: sha512-1iDKAZ4ie48PVhovsOXQ+C6o55dWJloXqtznnnKy6CltJBQLIuLLuUqa8zlIvma0ZigjVjgDUhnVaNU1MErtZw==}
engines: {node: '>=10.0.0'}
'@citation-js/name@0.4.2':
resolution: {integrity: sha512-brSPsjs2fOVzSnARLKu0qncn6suWjHVQtrqSUrnqyaRH95r/Ad4wPF5EsoWr+Dx8HzkCGb/ogmoAzfCsqlTwTQ==}
engines: {node: '>=6'}
'@citation-js/plugin-bibtex@0.7.21':
resolution: {integrity: sha512-O008pSsJgiYKn4+7gAWrbNpNdUH++aMeYmZaJ2oFQ8X1tcY5jNBxJcr0zZojNtUi5CVOaXXHQ0yIifoUhuF2Vg==}
engines: {node: '>=16.0.0'}
peerDependencies:
'@citation-js/core': ^0.7.0
'@citation-js/plugin-cff@0.6.2':
resolution: {integrity: sha512-jvERDFbtQQOBb9s+E8VbRIYsEIb2YEbcLH3yVDxXK0xqBGQDE5m8JZAYUkENy4FmbaD979l0+xJTWAsYN1pV/w==}
engines: {node: '>=14.0.0'}
'@citation-js/plugin-csl@0.7.22':
resolution: {integrity: sha512-/rGdtbeP3nS4uZDdEbQUHT8PrUcIs0da2t+sWMKYXoOhXQqfw3oJJ7p4tUD+R8lptyIR5Eq20/DFk/kQDdLpYg==}
engines: {node: '>=16.0.0'}
peerDependencies:
'@citation-js/core': ^0.7.0
'@citation-js/plugin-github@0.6.2':
resolution: {integrity: sha512-NKq/1Ja060o4II1Z4p1+utwpvMsx+XIWdNiFvnJDfR2Z9E1xGETjByPpdobGBsteUTpJPEe9OVfF8Dee/Q7zLQ==}
engines: {node: '>=14.0.0'}
'@citation-js/plugin-npm@0.6.2':
resolution: {integrity: sha512-mbQg/N9HM+gOqHJCdDZEElSW+h/oM94snKCl3llXuZ4MEH3tHraElS6CYRW/vW7s8KUTTHhgE62Q6ua5aRml8g==}
engines: {node: '>=14.0.0'}
'@citation-js/plugin-software-formats@0.6.2':
resolution: {integrity: sha512-x1IG0LBKglBU6SuiiKfvOtn7g7o7s+YhQhB44o7zrFaKEO8jkyQ5qMKtM5VFdCBL7teLfzZLjpjNkdJXtZ6XZw==}
engines: {node: '>=14.0.0'}
'@citation-js/plugin-yaml@0.6.2':
resolution: {integrity: sha512-qw53Uy2fDekKAzNhv8pkAWpIccIxyKQ3nQuClMgzDPdyeWg34ElIs4bDub9ZZup15fy+X//2gP8k12RJqNo4lA==}
engines: {node: '>=14.0.0'}
'@citation-js/plugin-zenodo@0.6.2':
resolution: {integrity: sha512-3XQOO3u4WXY/7AWZyQ+9SuBzS8bYTlJ+NF1uCgrZO64g36nK5iIc5YV9cBl2TL2QhHF6S36nvAsXsj5fX9FeHw==}
engines: {node: '>=14.0.0'}
'@codemirror/autocomplete@6.20.3':
resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==}
@@ -1958,9 +1898,6 @@ packages:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
baseline-browser-mapping@2.11.21:
resolution: {integrity: sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==}
engines: {node: '>=6.0.0'}
@@ -1993,9 +1930,6 @@ packages:
resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==}
engines: {node: '>=4.0'}
buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
builtin-modules@5.2.0:
resolution: {integrity: sha512-02yxLeyxF4dNl6SlY6/5HfRSrSdZ/sCPoxy2kZNP5dZZX8LSAD9aE2gtJIUgWrsQTiMPl3mxESyrobSwvRGisQ==}
engines: {node: '>=18.20'}
@@ -2073,9 +2007,6 @@ packages:
resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==}
engines: {node: '>=8'}
citeproc@2.4.63:
resolution: {integrity: sha512-68F95Bp4UbgZU/DBUGQn0qV3HDZLCdI9+Bb2ByrTaNJDL5VEm9LqaiNaxljsvoaExSLEXe1/r6n2Z06SCzW3/Q==}
clippie@4.2.1:
resolution: {integrity: sha512-+Shk4WLaaBtxTwgaspGnFm/WrtjZCVCKsl66mPglNRj6B+OzgIYnp4LX7EjW1UTG5NfJsW60jxwV8EvqtyrDUA==}
@@ -2789,9 +2720,6 @@ packages:
picomatch:
optional: true
fetch-ponyfill@7.1.0:
resolution: {integrity: sha512-FhbbL55dj/qdVO3YNK7ZEkshvj3eQ7EuIGV2I6ic/2YiocvyWv+7jg2s4AyS0wdRU75s3tA8ZxI/xPigb0v5Aw==}
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
@@ -2985,9 +2913,6 @@ packages:
idiomorph@0.8.0:
resolution: {integrity: sha512-R47JBL+6jHDQvcnbppIUtXXcxi5h6+wuCEEZXnRhUltsgzPFKzThL5PuHppP9ypJuy+7WawcY1TsSBD/iQbMhw==}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -3565,9 +3490,6 @@ packages:
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
moo@0.5.3:
resolution: {integrity: sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==}
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
@@ -3601,24 +3523,6 @@ packages:
resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==}
engines: {node: '>= 0.4'}
node-fetch@2.6.13:
resolution: {integrity: sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
node-releases@2.0.54:
resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==}
engines: {node: '>=18'}
@@ -4236,10 +4140,6 @@ packages:
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
sync-fetch@0.4.5:
resolution: {integrity: sha512-esiWJ7ixSKGpd9DJPBTC4ckChqdOjIwJfYhVHkcQ2Gnm41323p1TRmEI+esTQ9ppD+b5opps2OTEGTCGX5kF+g==}
engines: {node: '>=14'}
table@6.9.0:
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
engines: {node: '>=10.0.0'}
@@ -4293,9 +4193,6 @@ packages:
resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
engines: {node: '>=6'}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
tr46@3.0.0:
resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==}
engines: {node: '>=12'}
@@ -4523,9 +4420,6 @@ packages:
resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==}
engines: {node: '>=14'}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
@@ -4543,9 +4437,6 @@ packages:
resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==}
engines: {node: '>=12'}
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
which-boxed-primitive@1.1.1:
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
engines: {node: '>= 0.4'}
@@ -4679,64 +4570,6 @@ snapshots:
'@chevrotain/utils@11.1.2': {}
'@citation-js/core@0.7.21':
dependencies:
'@citation-js/date': 0.5.1
'@citation-js/name': 0.4.2
fetch-ponyfill: 7.1.0
sync-fetch: 0.4.5
transitivePeerDependencies:
- encoding
'@citation-js/date@0.5.1': {}
'@citation-js/name@0.4.2': {}
'@citation-js/plugin-bibtex@0.7.21(@citation-js/core@0.7.21)':
dependencies:
'@citation-js/core': 0.7.21
'@citation-js/date': 0.5.1
'@citation-js/name': 0.4.2
moo: 0.5.3
'@citation-js/plugin-cff@0.6.2':
dependencies:
'@citation-js/date': 0.5.1
'@citation-js/plugin-yaml': 0.6.2
'@citation-js/plugin-csl@0.7.22(@citation-js/core@0.7.21)':
dependencies:
'@citation-js/core': 0.7.21
'@citation-js/date': 0.5.1
citeproc: 2.4.63
'@citation-js/plugin-github@0.6.2':
dependencies:
'@citation-js/date': 0.5.1
'@citation-js/name': 0.4.2
'@citation-js/plugin-npm@0.6.2':
dependencies:
'@citation-js/date': 0.5.1
'@citation-js/name': 0.4.2
'@citation-js/plugin-software-formats@0.6.2':
dependencies:
'@citation-js/plugin-cff': 0.6.2
'@citation-js/plugin-github': 0.6.2
'@citation-js/plugin-npm': 0.6.2
'@citation-js/plugin-yaml': 0.6.2
'@citation-js/plugin-zenodo': 0.6.2
'@citation-js/plugin-yaml@0.6.2':
dependencies:
js-yaml: 4.2.0
'@citation-js/plugin-zenodo@0.6.2':
dependencies:
'@citation-js/date': 0.5.1
'@citation-js/name': 0.4.2
'@codemirror/autocomplete@6.20.3':
dependencies:
'@codemirror/language': 6.12.4
@@ -6309,8 +6142,6 @@ snapshots:
balanced-match@4.0.4: {}
base64-js@1.5.1: {}
baseline-browser-mapping@2.11.21: {}
binary-extensions@2.3.0: {}
@@ -6344,11 +6175,6 @@ snapshots:
'@types/node': 26.6.2
optional: true
buffer@5.7.1:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
builtin-modules@5.2.0: {}
cacheable@2.5.0:
@@ -6434,8 +6260,6 @@ snapshots:
ci-info@4.4.0: {}
citeproc@2.4.63: {}
clippie@4.2.1: {}
codemirror-lang-elixir@4.0.1:
@@ -7285,12 +7109,6 @@ snapshots:
optionalDependencies:
picomatch: 4.0.7
fetch-ponyfill@7.1.0:
dependencies:
node-fetch: 2.6.13
transitivePeerDependencies:
- encoding
fflate@0.8.2: {}
fflate@0.8.3: {}
@@ -7507,8 +7325,6 @@ snapshots:
idiomorph@0.8.0: {}
ieee754@1.2.1: {}
ignore@5.3.2: {}
ignore@7.0.6: {}
@@ -8200,8 +8016,6 @@ snapshots:
minimist@1.2.8: {}
moo@0.5.3: {}
mrmime@2.0.1: {}
ms@2.1.3: {}
@@ -8230,14 +8044,6 @@ snapshots:
semver: 6.3.1
optional: true
node-fetch@2.6.13:
dependencies:
whatwg-url: 5.0.0
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
node-releases@2.0.54: {}
normalize-path@3.0.0: {}
@@ -8926,13 +8732,6 @@ snapshots:
symbol-tree@3.2.4: {}
sync-fetch@0.4.5:
dependencies:
buffer: 5.7.1
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
table@6.9.0:
dependencies:
ajv: 8.20.0
@@ -9009,8 +8808,6 @@ snapshots:
universalify: 0.2.0
url-parse: 1.5.10
tr46@0.0.3: {}
tr46@3.0.0:
dependencies:
punycode: 2.3.1
@@ -9231,8 +9028,6 @@ snapshots:
dependencies:
xml-name-validator: 4.0.0
webidl-conversions@3.0.1: {}
webidl-conversions@7.0.0: {}
whatwg-encoding@2.0.0:
@@ -9246,11 +9041,6 @@ snapshots:
tr46: 3.0.0
webidl-conversions: 7.0.0
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
which-boxed-primitive@1.1.1:
dependencies:
is-bigint: 1.1.0
-4
View File
@@ -78,10 +78,6 @@
"matchPackageNames": ["tailwindcss"],
"allowedVersions": "^3", // need to migrate
},
{
"matchPackageNames": ["@citation-js/core", "@citation-js/plugin-bibtex", "@citation-js/plugin-csl"],
"allowedVersions": "<0.8", // https://github.com/citation-js/citation-js/pull/277
},
{
"matchPackageNames": ["typescript"],
"allowedVersions": "^6", // typescript-eslint is not yet compatible with typescript 7
+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,
)
-11
View File
@@ -1,11 +0,0 @@
<button type="button" class="ui citation button" id="citation-copy-apa" data-text="">
APA
</button>
<button type="button" class="ui citation button" id="citation-copy-bibtex" data-text="">
BibTeX
</button>
<!-- the value will be updated by initCitationFileCopyContent, the code below is used to avoid UI flicking -->
<input id="citation-copy-content" value="" size="1" readonly>
<button type="button" class="ui icon button" id="citation-clipboard-btn" data-tooltip-content="{{ctx.Locale.Tr "copy"}}" data-clipboard-target="#citation-copy-content">
{{svg "octicon-copy"}}
</button>
+14 -3
View File
@@ -4,9 +4,20 @@
</div>
<div class="content">
<div class="ui stackable secondary menu">
<div class="ui action input" id="citation-panel">
{{template "repo/cite/cite_buttons" .}}
<a id="goto-citation-btn" class="ui basic jump icon button" href="{{$.RepoLink}}/src/{{$.BranchName}}/CITATION.cff" data-tooltip-content="{{ctx.Locale.Tr "repo.find_file.go_to_file"}}">
<div class="ui action input tw-flex tw-w-full" data-global-init="initRepoCitationPanel">
{{if .CitationAPA}}
<button type="button" class="ui citation button citation-apa" data-text="{{.CitationAPA}}">
APA
</button>
{{end}}
<button type="button" class="ui citation button citation-bibtex" data-text="{{.CitationBibTeX}}">
BibTeX
</button>
<input value="" size="1" readonly autofocus>
<button type="button" class="ui icon button citation-copy" data-tooltip-content="{{ctx.Locale.Tr "copy"}}">
{{svg "octicon-copy"}}
</button>
<a class="ui basic jump icon button" href="{{$.RepoLink}}/src/{{$.RefTypeNameSubURL}}/{{PathEscapeSegments $.CitationFileName}}" data-tooltip-content="{{ctx.Locale.Tr "repo.find_file.go_to_file"}}">
{{svg "octicon-file-moved"}}
</a>
</div>
+2 -2
View File
@@ -58,9 +58,9 @@
</a>
{{end}}
{{if .CitiationExist}}
{{if .CitationFileName}}
{{template "repo/cite/cite_modal" .}}
<a class="flex-text-block muted" id="cite-repo-button">
<a class="flex-text-block muted show-modal" href="#" data-modal="#cite-repo-modal">
{{svg "octicon-cross-reference"}} {{ctx.Locale.Tr "repo.cite_this_repo"}}
</a>
{{end}}
+19
View File
@@ -44,6 +44,7 @@ func TestRepoView(t *testing.T) {
t.Run("ViewRepoDirectory", testViewRepoDirectory)
t.Run("ViewRepoDirectoryReadme", testViewRepoDirectoryReadme)
t.Run("ViewRepoSymlink", testViewRepoSymlink)
t.Run("ViewRepoCitationPicksCFFCaseInsensitivelyThroughSymlink", testViewRepoCitationPicksCFFCaseInsensitivelyThroughSymlink)
t.Run("MarkDownReadmeImage", testMarkDownReadmeImage)
t.Run("MarkDownReadmeImageSubfolder", testMarkDownReadmeImageSubfolder)
t.Run("GeneratedSourceLink", testGeneratedSourceLink)
@@ -512,6 +513,24 @@ func testViewRepoSymlink(t *testing.T) {
assert.Equal(t, "/user2/repo1/src/branch/symlink/some/other/path/awefulcake.txt?follow_symlink=1", resp.Header().Get("Location"))
}
func testViewRepoCitationPicksCFFCaseInsensitivelyThroughSymlink(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 2, ID: 1})
require.NoError(t, git.ForceFastImport(t.Context(), repo.CodeStorageRepo(), []git.FastImportCommit{
{
Ref: "refs/heads/citation", Message: "test", Files: []git.FastImportFile{
{Path: "CITATION.CFF/README.md", Content: "not a citation"},
{Path: "CITATION.bib", Content: "@misc{key}"},
{Mode: git.EntryModeSymlink, Path: "citation.cff", Content: "meta/citation.yaml"},
{Path: "meta/citation.yaml", Content: "title: Tool\nauthors: [{name: Team}]\n"},
},
},
}))
resp := MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/src/branch/citation"), http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
assert.Equal(t, "Team. Tool [Computer software]", htmlDoc.Find("#cite-repo-modal .citation-apa").AttrOr("data-text", ""))
assert.Equal(t, "/user2/repo1/src/branch/citation/citation.cff", htmlDoc.Find("#cite-repo-modal a.jump").AttrOr("href", ""))
}
func testMarkDownReadmeImage(t *testing.T) {
defer tests.PrintCurrentTest(t)()
Vendored
-16
View File
@@ -30,19 +30,3 @@ declare module 'swagger-ui-dist/swagger-ui-es-bundle.js' {
const value = await import('swagger-ui-dist');
export default value.SwaggerUIBundle;
}
declare module '@citation-js/core' {
export class Cite {
constructor(data: string);
format(format: string, options?: Record<string, any>): string;
}
export const plugins: {
config: {
get(name: string): any;
};
};
}
declare module '@citation-js/plugin-software-formats' {}
declare module '@citation-js/plugin-bibtex' {}
declare module '@citation-js/plugin-csl' {}
+7 -23
View File
@@ -990,24 +990,13 @@
background: var(--color-secondary);
}
#cite-repo-modal #citation-panel {
display: flex;
width: 100%;
}
#cite-repo-modal #citation-panel input {
border-radius: 0;
padding: 5px 10px;
width: 50%;
line-height: 1.4;
}
#cite-repo-modal #citation-panel .citation.button {
#cite-repo-modal .citation.button {
font-size: 13px;
padding: 7.5px 5px;
border-right: none;
}
#cite-repo-modal #citation-panel #citation-copy-content {
#cite-repo-modal input {
border-radius: 0;
padding: 5px 10px;
font-size: 1.2em;
@@ -1015,24 +1004,19 @@
flex: 1;
}
#cite-repo-modal #citation-panel #citation-copy-apa,
#cite-repo-modal #citation-panel #citation-copy-bibtex {
border-right: none;
}
#cite-repo-modal #citation-panel #goto-citation-btn {
#cite-repo-modal .jump.button {
border-left: none;
}
#cite-repo-modal #citation-panel > :first-child {
#cite-repo-modal .action.input > :first-child {
border-radius: var(--border-radius) 0 0 var(--border-radius) !important;
}
#cite-repo-modal #citation-panel > :last-child {
#cite-repo-modal .action.input > :last-child {
border-radius: 0 var(--border-radius) var(--border-radius) 0 !important;
}
#cite-repo-modal #citation-panel .icon.button {
#cite-repo-modal .icon.button {
padding: 0 10px;
}
+16 -60
View File
@@ -1,71 +1,27 @@
import {getCurrentLocale} from '../utils.ts';
import {errorMessage} from '../modules/errors.ts';
import {showFomanticModal} from '../modules/fomantic/modal.ts';
import {localUserSettings} from '../modules/user-settings.ts';
const {pageData} = window.config;
async function initInputCitationValue(citationCopyApa: HTMLButtonElement, citationCopyBibtex: HTMLButtonElement) {
const [{Cite, plugins}] = await Promise.all([
import('@citation-js/core'),
import('@citation-js/plugin-software-formats'),
import('@citation-js/plugin-bibtex'),
import('@citation-js/plugin-csl'),
]);
const citationFileContent = pageData.citationFileContent!;
const config = plugins.config.get('@bibtex');
config.constants.fieldTypes.doi = ['field', 'literal'];
config.constants.fieldTypes.version = ['field', 'literal'];
const citationFormatter = new Cite(citationFileContent);
const lang = getCurrentLocale() || 'en-US';
const apaOutput = citationFormatter.format('bibliography', {template: 'apa', lang});
const bibtexOutput = citationFormatter.format('bibtex', {lang});
citationCopyBibtex.setAttribute('data-text', bibtexOutput);
citationCopyApa.setAttribute('data-text', apaOutput);
}
export function initCitationFileCopyContent() {
const defaultCitationFormat = 'apa'; // apa or bibtex
if (!pageData.citationFileContent) return;
const citationCopyApa = document.querySelector<HTMLButtonElement>('#citation-copy-apa')!;
const citationCopyBibtex = document.querySelector<HTMLButtonElement>('#citation-copy-bibtex')!;
const inputContent = document.querySelector<HTMLInputElement>('#citation-copy-content');
if ((!citationCopyApa && !citationCopyBibtex) || !inputContent) return;
export function initRepoCitationPanel(elPanel: HTMLElement) {
const citationCopyApa = elPanel.querySelector<HTMLButtonElement>('.citation-apa');
const citationCopyBibtex = elPanel.querySelector<HTMLButtonElement>('.citation-bibtex')!;
const inputContent = elPanel.querySelector('input')!;
const clipboardBtn = elPanel.querySelector('.citation-copy')!;
const updateUi = () => {
const isBibtex = localUserSettings.getString('citation-copy-format', defaultCitationFormat) === 'bibtex';
const copyContent = (isBibtex ? citationCopyBibtex : citationCopyApa).getAttribute('data-text')!;
inputContent.value = copyContent;
const isBibtex = !citationCopyApa || localUserSettings.getString('citation-copy-format', 'apa') === 'bibtex';
const text = (isBibtex ? citationCopyBibtex : citationCopyApa).getAttribute('data-text')!;
inputContent.value = text;
inputContent.setSelectionRange(0, 0);
clipboardBtn.setAttribute('data-clipboard-text', text); // the input strips newlines
citationCopyBibtex.classList.toggle('primary', isBibtex);
citationCopyApa.classList.toggle('primary', !isBibtex);
citationCopyApa?.classList.toggle('primary', !isBibtex);
};
document.querySelector('#cite-repo-button')?.addEventListener('click', async () => {
try {
await initInputCitationValue(citationCopyApa, citationCopyBibtex);
} catch (e) {
console.error(`initCitationFileCopyContent error: ${errorMessage(e)}`, e);
return;
}
updateUi();
citationCopyApa.addEventListener('click', () => {
localUserSettings.setString('citation-copy-format', 'apa');
for (const [format, button] of Object.entries({apa: citationCopyApa, bibtex: citationCopyBibtex})) {
button?.addEventListener('click', () => {
localUserSettings.setString('citation-copy-format', format);
updateUi();
});
}
citationCopyBibtex.addEventListener('click', () => {
localUserSettings.setString('citation-copy-format', 'bibtex');
updateUi();
});
inputContent.addEventListener('click', () => {
inputContent.select();
});
showFomanticModal(document.querySelector('#cite-repo-modal'));
});
updateUi();
}
+2 -2
View File
@@ -7,7 +7,7 @@ import {
} from './repo-issue.ts';
import {initUnicodeEscapeButton} from './repo-unicode-escape.ts';
import {initRepoCloneButtons} from './repo-common.ts';
import {initCitationFileCopyContent} from './citation.ts';
import {initRepoCitationPanel} from './citation.ts';
import {initCompLabelEdit} from './comp/LabelEdit.ts';
import {initCompReactionSelector} from './comp/ReactionSelector.ts';
import {initRepoSettings} from './repo-settings.ts';
@@ -40,6 +40,7 @@ export function initBranchSelectorTabs() {
export function initRepository() {
registerGlobalInitFunc('initRepoPullMergeBox', initRepoPullMergeBox);
registerGlobalInitFunc('initRepoPullRequestUpdate', initRepoPullRequestUpdate);
registerGlobalInitFunc('initRepoCitationPanel', initRepoCitationPanel);
const pageContent = document.querySelector('.page-content.repository');
if (!pageContent) return;
@@ -53,7 +54,6 @@ export function initRepository() {
initRepoNew();
initRepoCloneButtons();
initCitationFileCopyContent();
initRepoSettings();
initRepoIssueWipNewTitle();
-1
View File
@@ -31,7 +31,6 @@ interface Window {
SortType: string,
StatusFilterMap: Record<string, string>,
},
citationFileContent?: string,
prReview?: {
numberOfFiles: number,
numberOfViewedFiles: number,