mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
feat(ui): add switch between formats when previewing CITATION.{cff,bib} files (#9103)
See #8222 for context (loosely related to #4595). ## Implemented changes The conversion logic is kept in the frontend and the related npm libraries are lazy-loaded (unchanged). ### Show some tabs on the preview of the `CITATION.*` file to switch between the formats:   ### Convert the "Cite repository" to a simple link to the citation file So that this change can be considered non-breaking ## Current state (before this PR) The last non-test call of `git.Blob.GetBlobContent` is made to retrieve the content of an eventual CITATION file. This is available in the `...` menu near the clone URL:  And is displayed as a popup:  Co-authored-by: 0ko <0ko@noreply.codeberg.org> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9103 Reviewed-by: Gusted <gusted@noreply.codeberg.org> Reviewed-by: 0ko <0ko@noreply.codeberg.org> Co-authored-by: oliverpool <git@olivier.pfad.fr> Co-committed-by: oliverpool <git@olivier.pfad.fr>
This commit is contained in:
parent
0737196842
commit
8f28cdefe0
23 changed files with 225 additions and 195 deletions
|
|
@ -157,22 +157,6 @@ func (b *Blob) NewTruncatedReader(limit int64) (rc io.ReadCloser, fullSize int64
|
|||
}, fullSize, nil
|
||||
}
|
||||
|
||||
// GetBlobContent Gets the truncated content of the blob as raw text
|
||||
func (b *Blob) GetBlobContent(limit int64) (string, error) {
|
||||
if limit <= 0 {
|
||||
return "", nil
|
||||
}
|
||||
rc, fullSize, err := b.NewTruncatedReader(limit)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
buf := make([]byte, min(fullSize, limit))
|
||||
_, err = io.ReadFull(rc, buf)
|
||||
return string(buf), err
|
||||
}
|
||||
|
||||
type BlobTooLargeError struct {
|
||||
Size, Limit int64
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,24 +45,6 @@ func TestBlob(t *testing.T) {
|
|||
testBlob, err := repo.GetBlob("6c493ff740f9380390d5c9ddef4af18697ac9375")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("GetBlobContent", func(t *testing.T) {
|
||||
r, err := testBlob.GetBlobContent(100)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "file2\n", r)
|
||||
|
||||
r, err = testBlob.GetBlobContent(-1)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, r)
|
||||
|
||||
r, err = testBlob.GetBlobContent(4)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "file", r)
|
||||
|
||||
r, err = testBlob.GetBlobContent(6)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "file2\n", r)
|
||||
})
|
||||
|
||||
t.Run("GetContentBase64", func(t *testing.T) {
|
||||
r, err := testBlob.GetContentBase64(100)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -140,11 +122,6 @@ func TestBlob(t *testing.T) {
|
|||
nonExistingBlob, err := repo.GetBlob("00003ff740f9380390d5c9ddef4af18690000000")
|
||||
require.NoError(t, err)
|
||||
|
||||
r, err := nonExistingBlob.GetBlobContent(100)
|
||||
require.Error(t, err)
|
||||
require.IsType(t, ErrNotExist{}, err)
|
||||
require.Empty(t, r)
|
||||
|
||||
rc, size, err := nonExistingBlob.NewTruncatedReader(100)
|
||||
require.Error(t, err)
|
||||
require.IsType(t, ErrNotExist{}, err)
|
||||
|
|
|
|||
|
|
@ -601,6 +601,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
|
|||
ctx.Data["EscapeStatus"] = status
|
||||
ctx.Data["FileContent"] = fileContent
|
||||
ctx.Data["LineEscapeStatus"] = statuses
|
||||
ctx.Data["IsCitationFile"] = isCitationFile(entry)
|
||||
}
|
||||
if !fInfo.isLFSFile {
|
||||
if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
|
||||
|
|
@ -778,6 +779,10 @@ func checkHomeCodeViewable(ctx *context.Context) {
|
|||
ctx.NotFound("Home", errors.New(ctx.Locale.TrString("units.error.no_unit_allowed_repo")))
|
||||
}
|
||||
|
||||
func isCitationFile(entry *git.TreeEntry) bool {
|
||||
return entry.Name() == "CITATION.cff" || entry.Name() == "CITATION.bib"
|
||||
}
|
||||
|
||||
func checkCitationFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||
if entry.Name() != "" {
|
||||
return
|
||||
|
|
@ -793,16 +798,9 @@ func checkCitationFile(ctx *context.Context, entry *git.TreeEntry) {
|
|||
return
|
||||
}
|
||||
for _, entry := range allEntries {
|
||||
if entry.Name() == "CITATION.cff" || entry.Name() == "CITATION.bib" {
|
||||
// Read Citation file contents
|
||||
if content, err := entry.Blob().GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
|
||||
log.Error("checkCitationFile: GetBlobContent: %v", err)
|
||||
} else {
|
||||
ctx.Data["CitationExist"] = true
|
||||
ctx.Data["CitationFile"] = entry.Name()
|
||||
ctx.PageData["citationFileContent"] = content
|
||||
break
|
||||
}
|
||||
if isCitationFile(entry) {
|
||||
ctx.Data["CitationFile"] = entry.Name()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
<span class="ui citation label primary" id="citation-copy-bibtex" data-text="">
|
||||
BibTeX
|
||||
</span>
|
||||
<!-- 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 icon button" id="citation-clipboard-btn" data-tooltip-content="{{ctx.Locale.Tr "copy"}}" data-clipboard-target="#citation-copy-content">
|
||||
{{svg "octicon-copy"}}
|
||||
</button>
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
<div class="ui small modal" id="cite-repo-modal">
|
||||
<div class="header">
|
||||
{{ctx.Locale.Tr "repo.cite_this_repo"}}
|
||||
</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}}/{{$.CitationFile}}" data-tooltip-content="{{ctx.Locale.Tr "repo.find_file.go_to_file"}}">
|
||||
{{svg "octicon-file-moved"}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="ui cancel button">
|
||||
{{ctx.Locale.Tr "cancel"}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -130,8 +130,8 @@
|
|||
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_tar"}}</a>
|
||||
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.bundle" rel="nofollow">{{svg "octicon-package" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_bundle"}}</a>
|
||||
{{end}}
|
||||
{{if .CitationExist}}
|
||||
<a class="item" id="cite-repo-button">{{svg "octicon-cross-reference" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.cite_this_repo"}}</a>
|
||||
{{if .CitationFile}}
|
||||
<a class="item citation-link" href="{{$.RepoLink}}/src/branch/{{$.BranchName}}/{{$.CitationFile}}">{{svg "octicon-cross-reference" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.cite_this_repo"}}</a>
|
||||
{{end}}
|
||||
{{range .OpenWithEditorApps}}
|
||||
<a class="item js-clone-url-editor" data-href-template="{{.OpenURL}}">{{.IconHTML}}{{ctx.Locale.Tr "repo.open_with_editor" .DisplayName}}</a>
|
||||
|
|
@ -140,7 +140,6 @@
|
|||
</button>
|
||||
{{template "repo/clone_script" .}}{{/* the script will update `.js-clone-url` and related elements */}}
|
||||
</div>
|
||||
{{template "repo/cite/cite_modal" .}}
|
||||
{{end}}
|
||||
{{if and (not $isHomepage) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}}
|
||||
<div class="button-sequence folder-actions">
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@
|
|||
{{end}}
|
||||
|
||||
<h4 class="file-header ui top attached header tw-flex tw-items-center tw-justify-between tw-flex-wrap">
|
||||
<div class="file-header-left tw-flex tw-items-center tw-py-2 tw-pr-4">
|
||||
<div class="file-header-left tw-flex tw-items-center tw-pr-4 tw-flex-wrap {{if .IsCitationFile}}tw-gap-4{{else}}tw-gap-2{{end}}">
|
||||
{{if .ReadmeInList}}
|
||||
{{svg "octicon-book" 16 "tw-mr-2"}}
|
||||
{{svg "octicon-book"}}
|
||||
<strong><a class="default-link muted" href="#readme">{{.FileName}}</a></strong>
|
||||
{{else}}
|
||||
{{template "repo/file_info" .}}
|
||||
|
|
@ -150,6 +150,9 @@
|
|||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
{{if .IsCitationFile}}
|
||||
<lazy-webc tag="citation-information">
|
||||
{{end}}
|
||||
<table>
|
||||
<tbody>
|
||||
{{range $idx, $code := .FileContent}}
|
||||
|
|
@ -164,6 +167,9 @@
|
|||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{if .IsCitationFile}}
|
||||
</lazy-webc>
|
||||
{{end}}
|
||||
<div class="code-line-menu tippy-target">
|
||||
{{if $.Permission.CanRead $.UnitTypeIssues}}
|
||||
<a class="item ref-in-new-issue" role="menuitem" data-url-issue-new="{{.RepoLink}}/issues/new" data-url-param-body-link="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}" rel="nofollow noindex">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</a>
|
||||
|
|
|
|||
47
tests/e2e/repo-viewer.test.e2e.ts
Normal file
47
tests/e2e/repo-viewer.test.e2e.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// @watch start
|
||||
// web_src/js/webcomponents/citation-information.js
|
||||
// @watch end
|
||||
|
||||
import {expect} from '@playwright/test';
|
||||
import {test} from './utils_e2e.ts';
|
||||
|
||||
test('CITATION.cff switch', async ({page}) => {
|
||||
const previewPath = '/user2/rendering-test/src/branch/master/CITATION.cff';
|
||||
|
||||
const response = await page.goto(previewPath);
|
||||
expect(response?.status()).toBe(200);
|
||||
|
||||
await expect(page.getByText('cff-version: 1.2.0')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', {name: 'BibTeX'}).click();
|
||||
await expect(page.getByText('cff-version: 1.2.0')).toBeHidden();
|
||||
await expect(
|
||||
page.getByText('howpublished = {https://forgejo.org/},'),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole('button', {name: 'Citation File Format'}).click();
|
||||
await expect(page.getByText('cff-version: 1.2.0')).toBeVisible();
|
||||
});
|
||||
|
||||
test('glb file with 3D rendering', async ({page}, workerInfo) => {
|
||||
test.skip(
|
||||
workerInfo.project.name !== 'chromium',
|
||||
'needs some investigation to run on other platforms',
|
||||
// https://codeberg.org/forgejo/forgejo/actions/runs/113344/jobs/3/attempt/1
|
||||
);
|
||||
|
||||
const previewPath =
|
||||
'/user2/rendering-test/src/branch/master/Unicode❤♻Test.glb';
|
||||
|
||||
const response = await page.goto(previewPath);
|
||||
expect(response?.status()).toBe(200);
|
||||
|
||||
await page
|
||||
.getByRole('img', {
|
||||
name: '3D model. Use mouse, touch or arrow keys to move.',
|
||||
})
|
||||
.click();
|
||||
});
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1 +1 @@
|
|||
fafaad77cb54665ac800d1bf77e6a55bd355eabc
|
||||
e8196c874f13227602a2b680c30eef433036e213
|
||||
|
|
|
|||
|
|
@ -89,10 +89,13 @@ func testPackageCargo(t *testing.T, _ *neturl.URL) {
|
|||
blob, err := commit.GetBlobByPath(path)
|
||||
require.NoError(t, err)
|
||||
|
||||
content, err := blob.GetBlobContent(1024)
|
||||
rc, _, err := blob.NewTruncatedReader(1024)
|
||||
require.NoError(t, err)
|
||||
|
||||
return content
|
||||
content, err := io.ReadAll(rc)
|
||||
require.NoError(t, err)
|
||||
|
||||
return string(content)
|
||||
}
|
||||
|
||||
root := fmt.Sprintf("%sapi/packages/%s/cargo", setting.AppURL, user.Name)
|
||||
|
|
|
|||
|
|
@ -31,49 +31,58 @@ func TestCitation(t *testing.T) {
|
|||
repo, _, f := tests.CreateDeclarativeRepo(t, user, "citation-no-citation", []unit_model.Type{unit_model.TypeCode}, nil, nil)
|
||||
defer f()
|
||||
|
||||
testCitationButtonExists(t, session, repo, "", false)
|
||||
testCitationButtonExists(t, session, repo, "")
|
||||
})
|
||||
|
||||
t.Run("cff citation", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
repo, f := createRepoWithEmptyFile(t, user, "citation-cff", "CITATION.cff")
|
||||
repo, f := createRepoWithDummyFile(t, user, "citation-cff", "CITATION.cff")
|
||||
defer f()
|
||||
|
||||
testCitationButtonExists(t, session, repo, "CITATION.cff", true)
|
||||
testCitationButtonExists(t, session, repo, "CITATION.cff")
|
||||
})
|
||||
|
||||
t.Run("bib citation", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
repo, f := createRepoWithEmptyFile(t, user, "citation-bib", "CITATION.bib")
|
||||
repo, f := createRepoWithDummyFile(t, user, "citation-bib", "CITATION.bib")
|
||||
defer f()
|
||||
|
||||
testCitationButtonExists(t, session, repo, "CITATION.bib", true)
|
||||
testCitationButtonExists(t, session, repo, "CITATION.bib")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func testCitationButtonExists(t *testing.T, session *TestSession, repo *repo_model.Repository, file string, exists bool) {
|
||||
func testCitationButtonExists(t *testing.T, session *TestSession, repo *repo_model.Repository, file string) {
|
||||
req := NewRequest(t, "GET", repo.HTMLURL())
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
doc.AssertElement(t, "#cite-repo-button", exists)
|
||||
|
||||
if exists {
|
||||
href, exists := doc.doc.Find("#goto-citation-btn").Attr("href")
|
||||
assert.True(t, exists)
|
||||
|
||||
assert.True(t, strings.HasSuffix(href, file))
|
||||
links := doc.Find("a.citation-link")
|
||||
if file == "" {
|
||||
assert.Equal(t, 0, links.Length())
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, links.Length())
|
||||
href, exists := links.Attr("href")
|
||||
assert.True(t, exists)
|
||||
assert.True(t, strings.HasSuffix(href, file))
|
||||
|
||||
// request the citation file to check for webcomponent presence
|
||||
req = NewRequest(t, "GET", href)
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
doc = NewHTMLParser(t, resp.Body)
|
||||
doc.AssertElement(t, `lazy-webc[tag="citation-information"]`, true)
|
||||
}
|
||||
|
||||
func createRepoWithEmptyFile(t *testing.T, user *user_model.User, repoName, fileName string) (*repo_model.Repository, func()) {
|
||||
func createRepoWithDummyFile(t *testing.T, user *user_model.User, repoName, fileName string) (*repo_model.Repository, func()) {
|
||||
repo, _, f := tests.CreateDeclarativeRepo(t, user, repoName, []unit_model.Type{unit_model.TypeCode}, nil, []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: fileName,
|
||||
Operation: "create",
|
||||
TreePath: fileName,
|
||||
ContentReader: strings.NewReader("citation-content"), // viewer requires some content
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
100% { transform: translate(-50%, -50%) rotate(360deg); }
|
||||
}
|
||||
|
||||
lazy-webc {
|
||||
display: block;
|
||||
}
|
||||
|
||||
lazy-webc,
|
||||
.is-loading {
|
||||
pointer-events: none !important;
|
||||
|
|
|
|||
|
|
@ -438,6 +438,10 @@ pdf-object {
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
citation-information .tab:not(.active) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.repository.file.list .non-diff-file-content .plain-text {
|
||||
padding: 1em 2em;
|
||||
}
|
||||
|
|
@ -1897,48 +1901,6 @@ details.repo-search-result summary::marker {
|
|||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
#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-copy-content {
|
||||
border-radius: 0;
|
||||
padding: 5px 10px;
|
||||
font-size: 1.2em;
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#cite-repo-modal #citation-panel #citation-copy-bibtex {
|
||||
font-size: 13px;
|
||||
padding: 7.5px 5px;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
#cite-repo-modal #citation-panel #goto-citation-btn {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
#cite-repo-modal #citation-panel > :first-child {
|
||||
border-radius: var(--border-radius) 0 0 var(--border-radius) !important;
|
||||
}
|
||||
|
||||
#cite-repo-modal #citation-panel > :last-child {
|
||||
border-radius: 0 var(--border-radius) var(--border-radius) 0 !important;
|
||||
}
|
||||
|
||||
#cite-repo-modal #citation-panel .icon.button {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
#search-user-box .results .result .image {
|
||||
order: 0;
|
||||
margin-right: 12px;
|
||||
|
|
@ -2381,6 +2343,7 @@ tbody.commit-list {
|
|||
overflow-x: auto;
|
||||
padding: 6px 12px !important;
|
||||
font-size: 13px !important;
|
||||
min-height: 46px;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
import $ from 'jquery';
|
||||
import {getCurrentLocale} from '../utils.js';
|
||||
|
||||
const {pageData} = window.config;
|
||||
|
||||
async function initInputCitationValue(inputContent) {
|
||||
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'),
|
||||
]);
|
||||
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 = getCurrentLocale() || 'en-US';
|
||||
const bibtexOutput = citationFormatter.format('bibtex', {lang});
|
||||
inputContent.value = bibtexOutput;
|
||||
}
|
||||
|
||||
export async function initCitationFileCopyContent() {
|
||||
if (!pageData.citationFileContent) return;
|
||||
|
||||
const inputContent = document.getElementById('citation-copy-content');
|
||||
|
||||
if (!inputContent) return;
|
||||
|
||||
document.getElementById('cite-repo-button')?.addEventListener('click', async (e) => {
|
||||
const dropdownBtn = e.target.closest('.ui.dropdown.button');
|
||||
dropdownBtn.classList.add('is-loading');
|
||||
|
||||
try {
|
||||
try {
|
||||
await initInputCitationValue(inputContent);
|
||||
} catch (e) {
|
||||
console.error(`initCitationFileCopyContent error: ${e}`, e);
|
||||
return;
|
||||
}
|
||||
|
||||
inputContent.addEventListener('click', () => {
|
||||
inputContent.select();
|
||||
});
|
||||
} finally {
|
||||
dropdownBtn.classList.remove('is-loading');
|
||||
}
|
||||
|
||||
$('#cite-repo-modal').modal('show');
|
||||
});
|
||||
}
|
||||
|
|
@ -13,7 +13,6 @@ import {initRepoBranchTagSelector} from './repo-branch-tag-selector.js';
|
|||
import {
|
||||
initRepoCloneLink, initRepoCommonBranchOrTagDropdown, initRepoCommonFilterSearchDropdown,
|
||||
} from './repo-common.js';
|
||||
import {initCitationFileCopyContent} from './citation.js';
|
||||
import {initCompLabelEdit} from './comp/LabelEdit.js';
|
||||
import {initRepoDiffConversationNav} from './repo-diff.js';
|
||||
import {showErrorToast} from '../modules/toast.js';
|
||||
|
|
@ -457,7 +456,6 @@ export function initRepository() {
|
|||
}
|
||||
|
||||
initRepoCloneLink();
|
||||
initCitationFileCopyContent();
|
||||
initRepoSettingBranches();
|
||||
|
||||
// Issues
|
||||
|
|
|
|||
114
web_src/js/webcomponents/citation-information.js
Normal file
114
web_src/js/webcomponents/citation-information.js
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import '@citation-js/plugin-software-formats';
|
||||
import '@citation-js/plugin-bibtex';
|
||||
import {Cite, plugins} from '@citation-js/core';
|
||||
|
||||
import {getCurrentLocale} from '../utils.js';
|
||||
import {initTab} from '../modules/tab.ts';
|
||||
|
||||
window.customElements.define(
|
||||
'citation-information',
|
||||
class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
const children = this.children; // eslint-disable-line wc/no-child-traversal-in-connectedcallback
|
||||
if (children.length !== 1) {
|
||||
// developer error
|
||||
throw new Error(
|
||||
`<citation-information> expected one child, got ${children.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
const lang = getCurrentLocale() || 'en-US';
|
||||
|
||||
const raw = children[0];
|
||||
raw.dataset.tab = 'raw';
|
||||
raw.classList.add('tab', 'active');
|
||||
|
||||
// like in copy-content
|
||||
const lineEls = raw.querySelectorAll('.lines-code');
|
||||
const code = Array.from(lineEls, (el) => el.textContent).join('');
|
||||
|
||||
const inputType = plugins.input.type(code);
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new Cite(code, {forceType: inputType});
|
||||
} catch (err) {
|
||||
const elContainer = document.createElement('div');
|
||||
elContainer.classList.add('ui', 'warning', 'message');
|
||||
|
||||
const elHeader = document.createElement('div');
|
||||
elHeader.classList.add('header');
|
||||
elHeader.textContent = `Could not parse citation-information (format ${inputType})`; // ideally this message should be localized, however the error below will likely be in english
|
||||
elContainer.append(elHeader);
|
||||
|
||||
const elParagraph = document.createElement('pre');
|
||||
elParagraph.textContent = err;
|
||||
elContainer.append(elParagraph);
|
||||
this.prepend(elContainer);
|
||||
return;
|
||||
}
|
||||
|
||||
const toggleBar = document.createElement('div');
|
||||
toggleBar.classList.add('switch');
|
||||
|
||||
const newButton = (txt, id, tooltip, active) => {
|
||||
const el = document.createElement('button');
|
||||
el.textContent = txt;
|
||||
el.dataset.tab = id;
|
||||
if (tooltip) {
|
||||
el.dataset.tooltipContent = tooltip;
|
||||
}
|
||||
el.classList.add('item');
|
||||
if (active) {
|
||||
el.classList.add('active');
|
||||
}
|
||||
return el;
|
||||
};
|
||||
let originalText = 'Original';
|
||||
let originalTooltip = '';
|
||||
switch (inputType) {
|
||||
case '@biblatex/text':
|
||||
originalText = 'BibTeX';
|
||||
break;
|
||||
case '@else/yaml':
|
||||
originalText = 'CFF';
|
||||
originalTooltip = 'Citation File Format';
|
||||
break;
|
||||
}
|
||||
toggleBar.append(newButton(originalText, 'raw', originalTooltip, true));
|
||||
|
||||
const appendTab = (id, btnLabel, btnTooltip, tabContent) => {
|
||||
const el = document.createElement('pre');
|
||||
el.textContent = tabContent;
|
||||
el.dataset.tab = id;
|
||||
el.classList.add('tab');
|
||||
el.style.padding = '1rem';
|
||||
el.style.margin = 0;
|
||||
this.append(el);
|
||||
toggleBar.append(newButton(btnLabel, id, btnTooltip));
|
||||
};
|
||||
if (inputType !== '@biblatex/text') {
|
||||
appendTab(
|
||||
'bibtex',
|
||||
'BibTeX',
|
||||
'',
|
||||
parsed.format('bibtex', {lang}).trim(),
|
||||
);
|
||||
}
|
||||
if (inputType !== '@else/yaml') {
|
||||
appendTab(
|
||||
'cff',
|
||||
'CFF',
|
||||
'Citation File Format',
|
||||
parsed.format('cff', {lang}).trim(),
|
||||
);
|
||||
}
|
||||
|
||||
const toggleBarParent = document.querySelector('.file-header-left');
|
||||
toggleBarParent.prepend(toggleBar);
|
||||
initTab(toggleBarParent);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import {onDomReady} from '../utils/dom.js';
|
||||
|
||||
/**
|
||||
|
|
@ -22,6 +25,9 @@ const loadableComponents = {
|
|||
'pdf-object': lazyPromise(() => {
|
||||
return import(/* webpackChunkName: "pdf-object" */ './pdf-object.js');
|
||||
}),
|
||||
'citation-information': lazyPromise(() => {
|
||||
return import(/* webpackChunkName: "citation-information" */ './citation-information.js');
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue