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

+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,