From 9f8e7789187013ab6752c2635b24c9dd1d70cd90 Mon Sep 17 00:00:00 2001
From: Nolann <62215577+Nolann71@users.noreply.github.com>
Date: Fri, 11 Nov 2022 18:02:50 +0100
Subject: [PATCH] Copy citation file content, in APA and BibTex format, on repo
 home page (#19999)

Add feature to easily copy CITATION.cff content in APA and BibTex format.
---
 options/locale/locale_en-US.ini       |   2 +
 package-lock.json                     | 289 ++++++++++++++++++++++++--
 package.json                          |   4 +
 routers/web/repo/view.go              |  45 ++++
 templates/repo/cite/cite_buttons.tmpl |  11 +
 templates/repo/cite/cite_modal.tmpl   |  22 ++
 templates/repo/home.tmpl              |   8 +-
 web_src/js/features/citation.js       |  60 ++++++
 web_src/js/features/repo-legacy.js    |   2 +
 web_src/less/_repository.less         |  52 ++++-
 10 files changed, 474 insertions(+), 21 deletions(-)
 create mode 100644 templates/repo/cite/cite_buttons.tmpl
 create mode 100644 templates/repo/cite/cite_modal.tmpl
 create mode 100644 web_src/js/features/citation.js

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 1d7086ed1d..eb2a1c86db 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1013,10 +1013,12 @@ unstar = Unstar
 star = Star
 fork = Fork
 download_archive = Download Repository
+more_actions = More Actions
 
 no_desc = No Description
 quick_guide = Quick Guide
 clone_this_repo = Clone this repository
+cite_this_repo = Cite this repository
 create_new_repo_command = Creating a new repository on the command line
 push_exist_repo = Pushing an existing repository from the command line
 empty_message = This repository does not contain any content.
diff --git a/package-lock.json b/package-lock.json
index cd11a43e4f..bf8d4b05e2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,6 +7,10 @@
       "name": "gitea",
       "license": "MIT",
       "dependencies": {
+        "@citation-js/core": "0.6.1",
+        "@citation-js/plugin-bibtex": "0.6.1",
+        "@citation-js/plugin-csl": "0.6.3",
+        "@citation-js/plugin-software-formats": "0.6.0",
         "@claviska/jquery-minicolors": "2.3.6",
         "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
         "@primer/octicons": "17.7.0",
@@ -197,6 +201,80 @@
       "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.1.tgz",
       "integrity": "sha512-zr9Qs9KFQiEvMWdZesjcmRJlUck5NR+eKGS1uyKk+oYTWwlYrsoPEi6VmG6/TzBD1hKCGEimrhTgGS6hvn/xIQ=="
     },
+    "node_modules/@citation-js/core": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.6.1.tgz",
+      "integrity": "sha512-zvVxsAP4ciVHiZ60TmKTfjui4m6xeISSp/rtIhOcvZxZ70bBfkt83+kGnuI4xRlhB/oUrZN2fC9BSRKdivSobQ==",
+      "dependencies": {
+        "@citation-js/date": "^0.5.0",
+        "@citation-js/name": "^0.4.2",
+        "fetch-ponyfill": "^7.1.0",
+        "sync-fetch": "^0.4.1"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@citation-js/date": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/@citation-js/date/-/date-0.5.1.tgz",
+      "integrity": "sha512-1iDKAZ4ie48PVhovsOXQ+C6o55dWJloXqtznnnKy6CltJBQLIuLLuUqa8zlIvma0ZigjVjgDUhnVaNU1MErtZw==",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/@citation-js/name": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmjs.org/@citation-js/name/-/name-0.4.2.tgz",
+      "integrity": "sha512-brSPsjs2fOVzSnARLKu0qncn6suWjHVQtrqSUrnqyaRH95r/Ad4wPF5EsoWr+Dx8HzkCGb/ogmoAzfCsqlTwTQ==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/@citation-js/plugin-bibtex": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.6.1.tgz",
+      "integrity": "sha512-JMw9h9MUXH7YWvgN0j+A5xI4Fw3cHYcDMzpweeAcXBfjfnC6q30Dyvs2YxfUxNEKvWDgRQjAiNNIzgWXs9uK1Q==",
+      "dependencies": {
+        "@citation-js/date": "^0.5.0",
+        "@citation-js/name": "^0.4.2",
+        "moo": "^0.5.1"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "@citation-js/core": "^0.6.0"
+      }
+    },
+    "node_modules/@citation-js/plugin-csl": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/@citation-js/plugin-csl/-/plugin-csl-0.6.3.tgz",
+      "integrity": "sha512-SP1/QyHfhcNufQ6VTJUM04Ti0XEWSWYMUhkDGG2lQtnDJU7pSDeAwtsE1kYUIJ9Np0Gm8IEZAfJ3CslMfsbimg==",
+      "dependencies": {
+        "@citation-js/date": "^0.5.0",
+        "citeproc": "^2.4.6"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "@citation-js/core": "^0.6.0"
+      }
+    },
+    "node_modules/@citation-js/plugin-software-formats": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/@citation-js/plugin-software-formats/-/plugin-software-formats-0.6.0.tgz",
+      "integrity": "sha512-l0Vp2h9UNlqsGMgrJulA92csgu8l3WhhBC0F2nFl76aTMOrMzC9/DX1G2Ob5tUQvPfuy4B5ZZsYPJNTJdtPVhw==",
+      "dependencies": {
+        "@citation-js/date": "^0.5.0",
+        "@citation-js/name": "^0.4.2",
+        "js-yaml": "^4.0.0"
+      },
+      "peerDependencies": {
+        "@citation-js/core": "^0.6.0"
+      }
+    },
     "node_modules/@claviska/jquery-minicolors": {
       "version": "2.3.6",
       "resolved": "https://registry.npmjs.org/@claviska/jquery-minicolors/-/jquery-minicolors-2.3.6.tgz",
@@ -1783,8 +1861,7 @@
     "node_modules/argparse": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
-      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
-      "dev": true
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
     },
     "node_modules/array-find-index": {
       "version": "1.0.2",
@@ -1908,6 +1985,25 @@
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
     },
+    "node_modules/base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
     "node_modules/big.js": {
       "version": "5.2.2",
       "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@@ -1975,6 +2071,29 @@
         "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
       }
     },
+    "node_modules/buffer": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.1.13"
+      }
+    },
     "node_modules/buffer-from": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -2126,6 +2245,11 @@
       "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==",
       "dev": true
     },
+    "node_modules/citeproc": {
+      "version": "2.4.62",
+      "resolved": "https://registry.npmjs.org/citeproc/-/citeproc-2.4.62.tgz",
+      "integrity": "sha512-l3uFfSEwNZp/jlz/TpgyBs85kOww6VlQHbAth0cpbgOn6iulZd+QlFY43LrRelzcYt3FZHTZ3soDyd8lNmkqdw=="
+    },
     "node_modules/clean-regexp": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz",
@@ -4616,6 +4740,14 @@
         "reusify": "^1.0.4"
       }
     },
+    "node_modules/fetch-ponyfill": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/fetch-ponyfill/-/fetch-ponyfill-7.1.0.tgz",
+      "integrity": "sha512-FhbbL55dj/qdVO3YNK7ZEkshvj3eQ7EuIGV2I6ic/2YiocvyWv+7jg2s4AyS0wdRU75s3tA8ZxI/xPigb0v5Aw==",
+      "dependencies": {
+        "node-fetch": "~2.6.1"
+      }
+    },
     "node_modules/file-entry-cache": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -5221,6 +5353,25 @@
         "postcss": "^8.1.0"
       }
     },
+    "node_modules/ieee754": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
     "node_modules/ignore": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
@@ -5724,7 +5875,6 @@
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
       "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
-      "dev": true,
       "dependencies": {
         "argparse": "^2.0.1"
       },
@@ -6575,6 +6725,11 @@
         "webpack": "^4.5.0 || 5.x"
       }
     },
+    "node_modules/moo": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz",
+      "integrity": "sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w=="
+    },
     "node_modules/ms": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -6671,7 +6826,6 @@
       "version": "2.6.7",
       "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
       "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
-      "dev": true,
       "dependencies": {
         "whatwg-url": "^5.0.0"
       },
@@ -6690,20 +6844,17 @@
     "node_modules/node-fetch/node_modules/tr46": {
       "version": "0.0.3",
       "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
-      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
-      "dev": true
+      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
     },
     "node_modules/node-fetch/node_modules/webidl-conversions": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
-      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
-      "dev": true
+      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
     },
     "node_modules/node-fetch/node_modules/whatwg-url": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
       "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
-      "dev": true,
       "dependencies": {
         "tr46": "~0.0.3",
         "webidl-conversions": "^3.0.0"
@@ -8498,6 +8649,18 @@
       "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
       "dev": true
     },
+    "node_modules/sync-fetch": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.4.2.tgz",
+      "integrity": "sha512-vilDD6yTGwyUjm7/W5WUUOCw1GH1aV591zC21XhbV6MJNZqfZcNMs9DVPHzy1UAmQ2GAg6S03F5TQ3paegKSdg==",
+      "dependencies": {
+        "buffer": "^5.7.1",
+        "node-fetch": "^2.6.1"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
     "node_modules/table": {
       "version": "6.8.0",
       "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz",
@@ -9845,6 +10008,56 @@
       "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.1.tgz",
       "integrity": "sha512-zr9Qs9KFQiEvMWdZesjcmRJlUck5NR+eKGS1uyKk+oYTWwlYrsoPEi6VmG6/TzBD1hKCGEimrhTgGS6hvn/xIQ=="
     },
+    "@citation-js/core": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.6.1.tgz",
+      "integrity": "sha512-zvVxsAP4ciVHiZ60TmKTfjui4m6xeISSp/rtIhOcvZxZ70bBfkt83+kGnuI4xRlhB/oUrZN2fC9BSRKdivSobQ==",
+      "requires": {
+        "@citation-js/date": "^0.5.0",
+        "@citation-js/name": "^0.4.2",
+        "fetch-ponyfill": "^7.1.0",
+        "sync-fetch": "^0.4.1"
+      }
+    },
+    "@citation-js/date": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/@citation-js/date/-/date-0.5.1.tgz",
+      "integrity": "sha512-1iDKAZ4ie48PVhovsOXQ+C6o55dWJloXqtznnnKy6CltJBQLIuLLuUqa8zlIvma0ZigjVjgDUhnVaNU1MErtZw=="
+    },
+    "@citation-js/name": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmjs.org/@citation-js/name/-/name-0.4.2.tgz",
+      "integrity": "sha512-brSPsjs2fOVzSnARLKu0qncn6suWjHVQtrqSUrnqyaRH95r/Ad4wPF5EsoWr+Dx8HzkCGb/ogmoAzfCsqlTwTQ=="
+    },
+    "@citation-js/plugin-bibtex": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.6.1.tgz",
+      "integrity": "sha512-JMw9h9MUXH7YWvgN0j+A5xI4Fw3cHYcDMzpweeAcXBfjfnC6q30Dyvs2YxfUxNEKvWDgRQjAiNNIzgWXs9uK1Q==",
+      "requires": {
+        "@citation-js/date": "^0.5.0",
+        "@citation-js/name": "^0.4.2",
+        "moo": "^0.5.1"
+      }
+    },
+    "@citation-js/plugin-csl": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/@citation-js/plugin-csl/-/plugin-csl-0.6.3.tgz",
+      "integrity": "sha512-SP1/QyHfhcNufQ6VTJUM04Ti0XEWSWYMUhkDGG2lQtnDJU7pSDeAwtsE1kYUIJ9Np0Gm8IEZAfJ3CslMfsbimg==",
+      "requires": {
+        "@citation-js/date": "^0.5.0",
+        "citeproc": "^2.4.6"
+      }
+    },
+    "@citation-js/plugin-software-formats": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/@citation-js/plugin-software-formats/-/plugin-software-formats-0.6.0.tgz",
+      "integrity": "sha512-l0Vp2h9UNlqsGMgrJulA92csgu8l3WhhBC0F2nFl76aTMOrMzC9/DX1G2Ob5tUQvPfuy4B5ZZsYPJNTJdtPVhw==",
+      "requires": {
+        "@citation-js/date": "^0.5.0",
+        "@citation-js/name": "^0.4.2",
+        "js-yaml": "^4.0.0"
+      }
+    },
     "@claviska/jquery-minicolors": {
       "version": "2.3.6",
       "resolved": "https://registry.npmjs.org/@claviska/jquery-minicolors/-/jquery-minicolors-2.3.6.tgz",
@@ -11114,8 +11327,7 @@
     "argparse": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
-      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
-      "dev": true
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
     },
     "array-find-index": {
       "version": "1.0.2",
@@ -11206,6 +11418,11 @@
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
     },
+    "base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
+    },
     "big.js": {
       "version": "5.2.2",
       "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@@ -11251,6 +11468,15 @@
         "update-browserslist-db": "^1.0.9"
       }
     },
+    "buffer": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+      "requires": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.1.13"
+      }
+    },
     "buffer-from": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -11353,6 +11579,11 @@
       "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==",
       "dev": true
     },
+    "citeproc": {
+      "version": "2.4.62",
+      "resolved": "https://registry.npmjs.org/citeproc/-/citeproc-2.4.62.tgz",
+      "integrity": "sha512-l3uFfSEwNZp/jlz/TpgyBs85kOww6VlQHbAth0cpbgOn6iulZd+QlFY43LrRelzcYt3FZHTZ3soDyd8lNmkqdw=="
+    },
     "clean-regexp": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz",
@@ -13191,6 +13422,14 @@
         "reusify": "^1.0.4"
       }
     },
+    "fetch-ponyfill": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/fetch-ponyfill/-/fetch-ponyfill-7.1.0.tgz",
+      "integrity": "sha512-FhbbL55dj/qdVO3YNK7ZEkshvj3eQ7EuIGV2I6ic/2YiocvyWv+7jg2s4AyS0wdRU75s3tA8ZxI/xPigb0v5Aw==",
+      "requires": {
+        "node-fetch": "~2.6.1"
+      }
+    },
     "file-entry-cache": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -13640,6 +13879,11 @@
       "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
       "requires": {}
     },
+    "ieee754": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
+    },
     "ignore": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
@@ -13991,7 +14235,6 @@
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
       "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
-      "dev": true,
       "requires": {
         "argparse": "^2.0.1"
       }
@@ -14642,6 +14885,11 @@
         "loader-utils": "^2.0.2"
       }
     },
+    "moo": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz",
+      "integrity": "sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w=="
+    },
     "ms": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -14719,7 +14967,6 @@
       "version": "2.6.7",
       "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
       "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
-      "dev": true,
       "requires": {
         "whatwg-url": "^5.0.0"
       },
@@ -14727,20 +14974,17 @@
         "tr46": {
           "version": "0.0.3",
           "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
-          "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
-          "dev": true
+          "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
         },
         "webidl-conversions": {
           "version": "3.0.1",
           "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
-          "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
-          "dev": true
+          "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
         },
         "whatwg-url": {
           "version": "5.0.0",
           "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
           "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
-          "dev": true,
           "requires": {
             "tr46": "~0.0.3",
             "webidl-conversions": "^3.0.0"
@@ -16067,6 +16311,15 @@
       "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
       "dev": true
     },
+    "sync-fetch": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.4.2.tgz",
+      "integrity": "sha512-vilDD6yTGwyUjm7/W5WUUOCw1GH1aV591zC21XhbV6MJNZqfZcNMs9DVPHzy1UAmQ2GAg6S03F5TQ3paegKSdg==",
+      "requires": {
+        "buffer": "^5.7.1",
+        "node-fetch": "^2.6.1"
+      }
+    },
     "table": {
       "version": "6.8.0",
       "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz",
diff --git a/package.json b/package.json
index 035f3fd0b0..d221a1379a 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,10 @@
     "node": ">= 14.0.0"
   },
   "dependencies": {
+    "@citation-js/core": "0.6.1",
+    "@citation-js/plugin-bibtex": "0.6.1",
+    "@citation-js/plugin-csl": "0.6.3",
+    "@citation-js/plugin-software-formats": "0.6.0",
     "@claviska/jquery-minicolors": "2.3.6",
     "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
     "@primer/octicons": "17.7.0",
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index d35ec48df0..e7aca04819 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -730,6 +730,44 @@ func checkHomeCodeViewable(ctx *context.Context) {
 	ctx.NotFound("Home", fmt.Errorf(ctx.Tr("units.error.no_unit_allowed_repo")))
 }
 
+func checkCitationFile(ctx *context.Context, entry *git.TreeEntry) {
+	if entry.Name() != "" {
+		return
+	}
+	tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath)
+	if err != nil {
+		ctx.NotFoundOrServerError("Repo.Commit.SubTree", git.IsErrNotExist, err)
+		return
+	}
+	allEntries, err := tree.ListEntries()
+	if err != nil {
+		ctx.ServerError("ListEntries", err)
+		return
+	}
+	for _, entry := range allEntries {
+		if entry.Name() == "CITATION.cff" || entry.Name() == "CITATION.bib" {
+			ctx.Data["CitiationExist"] = true
+			// Read Citation file contents
+			blob := entry.Blob()
+			dataRc, err := blob.DataAsync()
+			if err != nil {
+				ctx.ServerError("DataAsync", err)
+				return
+			}
+			defer dataRc.Close()
+			buf := make([]byte, 1024)
+			n, err := util.ReadAtMost(dataRc, buf)
+			if err != nil {
+				ctx.ServerError("ReadAtMost", err)
+				return
+			}
+			buf = buf[:n]
+			ctx.PageData["citationFileContent"] = string(buf)
+			break
+		}
+	}
+}
+
 // Home render repository home page
 func Home(ctx *context.Context) {
 	isFeed, _, showFeedType := feed.GetFeedType(ctx.Params(":reponame"), ctx.Req)
@@ -954,6 +992,13 @@ func renderCode(ctx *context.Context) {
 		return
 	}
 
+	if !ctx.Repo.Repository.IsEmpty {
+		checkCitationFile(ctx, entry)
+		if ctx.Written() {
+			return
+		}
+	}
+
 	renderLanguageStats(ctx)
 	if ctx.Written() {
 		return
diff --git a/templates/repo/cite/cite_buttons.tmpl b/templates/repo/cite/cite_buttons.tmpl
new file mode 100644
index 0000000000..0f4fb43484
--- /dev/null
+++ b/templates/repo/cite/cite_buttons.tmpl
@@ -0,0 +1,11 @@
+<button class="ui basic citation button" id="citation-copy-apa" data-text="">
+APA
+</button>
+<button class="ui basic 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 class="ui basic icon button tooltip" id="citation-clipboard-btn" data-content="{{.locale.Tr "copy"}}" data-clipboard-text="" data-clipboard-target="#citation-copy-content">
+	{{svg "octicon-copy"}}
+</button>
diff --git a/templates/repo/cite/cite_modal.tmpl b/templates/repo/cite/cite_modal.tmpl
new file mode 100644
index 0000000000..185b34173d
--- /dev/null
+++ b/templates/repo/cite/cite_modal.tmpl
@@ -0,0 +1,22 @@
+<div class="ui tiny modal" id="cite-repo-modal">
+	<div class="header">
+		{{.locale.Tr "repo.cite_this_repo"}}
+	</div>
+	<div class="content">
+		<div class="ui stackable secondary menu mobile--margin-between-items mobile--no-negative-margins no-vertical-tabs">
+			<div class="fitted item">
+				<div class="ui action input" id="citation-panel">
+					{{template "repo/cite/cite_buttons" .}}
+					<a id="goto-citation-btn" class="ui basic jump icon button tooltip" href="{{$.RepoLink}}/src/{{$.BranchName}}/CITATION.cff" data-position="top right" data-content="{{.locale.Tr "repo.find_file.go_to_file"}}">
+						{{svg "octicon-file-moved"}}
+					</a>
+				</div>
+			</div>
+		</div>
+	</div>
+	<div class="actions">
+		<div class="ui black deny button">
+			{{.locale.Tr "cancel"}}
+		</div>
+	</div>
+</div>
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index 69eaf17429..6b4a0008d6 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -117,19 +117,23 @@
 				{{if eq $n 0}}
 					<div class="ui action tiny input" id="clone-panel">
 						{{template "repo/clone_buttons" .}}
-						<button id="download-btn" class="ui basic small compact jump dropdown icon button tooltip" data-content="{{.locale.Tr "repo.download_archive"}}" data-position="top right">
-							{{svg "octicon-download"}}
+						<button id="more-btn" class="ui basic small compact jump dropdown icon button tooltip" data-content="{{.locale.Tr "repo.more_actions"}}" data-position="top right">
+							{{svg "octicon-kebab-horizontal"}}
 							<div class="menu">
 								{{if not $.DisableDownloadSourceArchives}}
 									<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "mr-3"}}{{.locale.Tr "repo.download_zip"}}</a>
 									<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "mr-3"}}{{.locale.Tr "repo.download_tar"}}</a>
 									<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.bundle" rel="nofollow">{{svg "octicon-package" 16 "mr-3"}}{{.locale.Tr "repo.download_bundle"}}</a>
+									{{if .CitiationExist}}
+										<a class="item" id="cite-repo-button">{{svg "octicon-cross-reference" 16 "mr-3"}}{{.locale.Tr "repo.cite_this_repo"}}</a>
+									{{end}}
 								{{end}}
 								<a class="item js-clone-url-vsc" href="vscode://vscode.git/clone?url={{.CloneButtonOriginLink.HTTPS}}">{{svg "gitea-vscode" 16 "mr-3"}}{{.locale.Tr "repo.clone_in_vsc"}}</a>
 							</div>
 						</button>
 						{{template "repo/clone_script" .}}{{/* the script will update `.js-clone-url` and related elements */}}
 					</div>
+					{{template "repo/cite/cite_modal" .}}
 				{{end}}
 				{{if and (ne $n 0) (not .IsViewFile) (not .IsBlame)}}
 					<a class="ui button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}">
diff --git a/web_src/js/features/citation.js b/web_src/js/features/citation.js
new file mode 100644
index 0000000000..01fcd95a1c
--- /dev/null
+++ b/web_src/js/features/citation.js
@@ -0,0 +1,60 @@
+import $ from 'jquery';
+
+const {pageData} = window.config;
+
+const initInputCitationValue = async ($citationCopyBibtex, $citationCopyApa) => {
+  const [{Cite, plugins}] = await Promise.all([
+    import(/* webpackChunkName: "citation-js-core" */'@citation-js/core'),
+    import(/* webpackChunkName: "citation-js-formats" */'@citation-js/plugin-software-formats'),
+    import(/* webpackChunkName: "citation-js-bibtex" */'@citation-js/plugin-bibtex'),
+    import(/* webpackChunkName: "citation-js-bibtex" */'@citation-js/plugin-csl'),
+  ]);
+  const {citationFileContent} = pageData;
+  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 = document.documentElement.lang || 'en-US';
+  const apaOutput = citationFormatter.format('bibliography', {template: 'apa', lang});
+  const bibtexOutput = citationFormatter.format('bibtex', {lang});
+  $citationCopyBibtex.attr('data-text', bibtexOutput);
+  $citationCopyApa.attr('data-text', apaOutput);
+};
+
+export function initCitationFileCopyContent() {
+  const defaultCitationFormat = 'apa'; // apa or bibtex
+
+  if (!pageData.citationFileContent) return;
+
+  const $citationCopyApa = $('#citation-copy-apa');
+  const $citationCopyBibtex = $('#citation-copy-bibtex');
+  const $inputContent = $('#citation-copy-content');
+
+  if ((!$citationCopyApa.length && !$citationCopyBibtex.length) || !$inputContent.length) return;
+  const updateUi = () => {
+    const isBibtex = (localStorage.getItem('citation-copy-format') || defaultCitationFormat) === 'bibtex';
+    const copyContent = (isBibtex ? $citationCopyBibtex : $citationCopyApa).attr('data-text');
+
+    $inputContent.val(copyContent);
+    $citationCopyBibtex.toggleClass('primary', isBibtex);
+    $citationCopyApa.toggleClass('primary', !isBibtex);
+  };
+  initInputCitationValue($citationCopyApa, $citationCopyBibtex).then(updateUi);
+
+  $citationCopyApa.on('click', () => {
+    localStorage.setItem('citation-copy-format', 'apa');
+    updateUi();
+  });
+  $citationCopyBibtex.on('click', () => {
+    localStorage.setItem('citation-copy-format', 'bibtex');
+    updateUi();
+  });
+
+  $inputContent.on('click', () => {
+    $inputContent.select();
+  });
+
+  $('#cite-repo-button').on('click', () => {
+    $('#cite-repo-modal').modal('show');
+  });
+}
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 2c93ca0342..e6a7c6dcd1 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -21,6 +21,7 @@ import {
   initRepoCommonFilterSearchDropdown,
   initRepoCommonLanguageStats,
 } from './repo-common.js';
+import {initCitationFileCopyContent} from './citation.js';
 import {initCompLabelEdit} from './comp/LabelEdit.js';
 import {initRepoDiffConversationNav} from './repo-diff.js';
 import attachTribute from './tribute.js';
@@ -505,6 +506,7 @@ export function initRepository() {
   }
 
   initRepoCloneLink();
+  initCitationFileCopyContent();
   initRepoCommonLanguageStats();
   initRepoSettingBranches();
 
diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less
index 193a73c706..bf1fb53e2b 100644
--- a/web_src/less/_repository.less
+++ b/web_src/less/_repository.less
@@ -227,7 +227,7 @@
       border-right: none;
     }
 
-    #download-btn {
+    #more-btn {
       border-left: none;
     }
 
@@ -2468,6 +2468,56 @@
 
 // End of .repository
 
+#cite-repo-modal {
+  #citation-panel {
+    width: 500px;
+
+    @media @mediaSm {
+      width: 100%;
+    }
+
+    input {
+      border-radius: 0;
+      padding: 5px 10px;
+      width: 50%;
+      line-height: 1.4;
+    }
+
+    .citation.button {
+      font-size: 13px;
+      padding: 7.5px 5px;
+    }
+
+    #citation-copy-content {
+      border-radius: 0;
+      padding: 5px 10px;
+      font-size: 1.2em;
+      line-height: 1.4;
+    }
+
+    #citation-copy-apa,
+    #citation-copy-bibtex {
+      border-right: none;
+    }
+
+    #goto-citation-btn {
+      border-left: none;
+    }
+
+    >:first-child {
+      border-radius: var(--border-radius) 0 0 var(--border-radius) !important;
+    }
+
+    >:last-child {
+      border-radius: 0 var(--border-radius) var(--border-radius) 0 !important;
+    }
+
+    .icon.button {
+      padding: 0 10px;
+    }
+  }
+}
+
 &.user-cards {
   .list {
     padding: 0;