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
|
}, 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 {
|
type BlobTooLargeError struct {
|
||||||
Size, Limit int64
|
Size, Limit int64
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,24 +45,6 @@ func TestBlob(t *testing.T) {
|
||||||
testBlob, err := repo.GetBlob("6c493ff740f9380390d5c9ddef4af18697ac9375")
|
testBlob, err := repo.GetBlob("6c493ff740f9380390d5c9ddef4af18697ac9375")
|
||||||
require.NoError(t, err)
|
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) {
|
t.Run("GetContentBase64", func(t *testing.T) {
|
||||||
r, err := testBlob.GetContentBase64(100)
|
r, err := testBlob.GetContentBase64(100)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
@ -140,11 +122,6 @@ func TestBlob(t *testing.T) {
|
||||||
nonExistingBlob, err := repo.GetBlob("00003ff740f9380390d5c9ddef4af18690000000")
|
nonExistingBlob, err := repo.GetBlob("00003ff740f9380390d5c9ddef4af18690000000")
|
||||||
require.NoError(t, err)
|
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)
|
rc, size, err := nonExistingBlob.NewTruncatedReader(100)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.IsType(t, ErrNotExist{}, err)
|
require.IsType(t, ErrNotExist{}, err)
|
||||||
|
|
|
||||||
|
|
@ -601,6 +601,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||||
ctx.Data["EscapeStatus"] = status
|
ctx.Data["EscapeStatus"] = status
|
||||||
ctx.Data["FileContent"] = fileContent
|
ctx.Data["FileContent"] = fileContent
|
||||||
ctx.Data["LineEscapeStatus"] = statuses
|
ctx.Data["LineEscapeStatus"] = statuses
|
||||||
|
ctx.Data["IsCitationFile"] = isCitationFile(entry)
|
||||||
}
|
}
|
||||||
if !fInfo.isLFSFile {
|
if !fInfo.isLFSFile {
|
||||||
if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
|
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")))
|
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) {
|
func checkCitationFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||||
if entry.Name() != "" {
|
if entry.Name() != "" {
|
||||||
return
|
return
|
||||||
|
|
@ -793,16 +798,9 @@ func checkCitationFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, entry := range allEntries {
|
for _, entry := range allEntries {
|
||||||
if entry.Name() == "CITATION.cff" || entry.Name() == "CITATION.bib" {
|
if isCitationFile(entry) {
|
||||||
// Read Citation file contents
|
ctx.Data["CitationFile"] = entry.Name()
|
||||||
if content, err := entry.Blob().GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
|
break
|
||||||
log.Error("checkCitationFile: GetBlobContent: %v", err)
|
|
||||||
} else {
|
|
||||||
ctx.Data["CitationExist"] = true
|
|
||||||
ctx.Data["CitationFile"] = entry.Name()
|
|
||||||
ctx.PageData["citationFileContent"] = content
|
|
||||||
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}}.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>
|
<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}}
|
{{end}}
|
||||||
{{if .CitationExist}}
|
{{if .CitationFile}}
|
||||||
<a class="item" id="cite-repo-button">{{svg "octicon-cross-reference" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.cite_this_repo"}}</a>
|
<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}}
|
{{end}}
|
||||||
{{range .OpenWithEditorApps}}
|
{{range .OpenWithEditorApps}}
|
||||||
<a class="item js-clone-url-editor" data-href-template="{{.OpenURL}}">{{.IconHTML}}{{ctx.Locale.Tr "repo.open_with_editor" .DisplayName}}</a>
|
<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>
|
</button>
|
||||||
{{template "repo/clone_script" .}}{{/* the script will update `.js-clone-url` and related elements */}}
|
{{template "repo/clone_script" .}}{{/* the script will update `.js-clone-url` and related elements */}}
|
||||||
</div>
|
</div>
|
||||||
{{template "repo/cite/cite_modal" .}}
|
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if and (not $isHomepage) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}}
|
{{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">
|
<div class="button-sequence folder-actions">
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,9 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<h4 class="file-header ui top attached header tw-flex tw-items-center tw-justify-between tw-flex-wrap">
|
<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}}
|
{{if .ReadmeInList}}
|
||||||
{{svg "octicon-book" 16 "tw-mr-2"}}
|
{{svg "octicon-book"}}
|
||||||
<strong><a class="default-link muted" href="#readme">{{.FileName}}</a></strong>
|
<strong><a class="default-link muted" href="#readme">{{.FileName}}</a></strong>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{template "repo/file_info" .}}
|
{{template "repo/file_info" .}}
|
||||||
|
|
@ -150,6 +150,9 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
{{if .IsCitationFile}}
|
||||||
|
<lazy-webc tag="citation-information">
|
||||||
|
{{end}}
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{range $idx, $code := .FileContent}}
|
{{range $idx, $code := .FileContent}}
|
||||||
|
|
@ -164,6 +167,9 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
{{if .IsCitationFile}}
|
||||||
|
</lazy-webc>
|
||||||
|
{{end}}
|
||||||
<div class="code-line-menu tippy-target">
|
<div class="code-line-menu tippy-target">
|
||||||
{{if $.Permission.CanRead $.UnitTypeIssues}}
|
{{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>
|
<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)
|
blob, err := commit.GetBlobByPath(path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
content, err := blob.GetBlobContent(1024)
|
rc, _, err := blob.NewTruncatedReader(1024)
|
||||||
require.NoError(t, err)
|
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)
|
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)
|
repo, _, f := tests.CreateDeclarativeRepo(t, user, "citation-no-citation", []unit_model.Type{unit_model.TypeCode}, nil, nil)
|
||||||
defer f()
|
defer f()
|
||||||
|
|
||||||
testCitationButtonExists(t, session, repo, "", false)
|
testCitationButtonExists(t, session, repo, "")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("cff citation", func(t *testing.T) {
|
t.Run("cff citation", func(t *testing.T) {
|
||||||
defer tests.PrintCurrentTest(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()
|
defer f()
|
||||||
|
|
||||||
testCitationButtonExists(t, session, repo, "CITATION.cff", true)
|
testCitationButtonExists(t, session, repo, "CITATION.cff")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("bib citation", func(t *testing.T) {
|
t.Run("bib citation", func(t *testing.T) {
|
||||||
defer tests.PrintCurrentTest(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()
|
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())
|
req := NewRequest(t, "GET", repo.HTMLURL())
|
||||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
doc := NewHTMLParser(t, resp.Body)
|
doc := NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
doc.AssertElement(t, "#cite-repo-button", exists)
|
links := doc.Find("a.citation-link")
|
||||||
|
if file == "" {
|
||||||
if exists {
|
assert.Equal(t, 0, links.Length())
|
||||||
href, exists := doc.doc.Find("#goto-citation-btn").Attr("href")
|
return
|
||||||
assert.True(t, exists)
|
|
||||||
|
|
||||||
assert.True(t, strings.HasSuffix(href, file))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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{
|
repo, _, f := tests.CreateDeclarativeRepo(t, user, repoName, []unit_model.Type{unit_model.TypeCode}, nil, []*files_service.ChangeRepoFile{
|
||||||
{
|
{
|
||||||
Operation: "create",
|
Operation: "create",
|
||||||
TreePath: fileName,
|
TreePath: fileName,
|
||||||
|
ContentReader: strings.NewReader("citation-content"), // viewer requires some content
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@
|
||||||
100% { transform: translate(-50%, -50%) rotate(360deg); }
|
100% { transform: translate(-50%, -50%) rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lazy-webc {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
lazy-webc,
|
lazy-webc,
|
||||||
.is-loading {
|
.is-loading {
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
|
|
|
||||||
|
|
@ -438,6 +438,10 @@ pdf-object {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
citation-information .tab:not(.active) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.repository.file.list .non-diff-file-content .plain-text {
|
.repository.file.list .non-diff-file-content .plain-text {
|
||||||
padding: 1em 2em;
|
padding: 1em 2em;
|
||||||
}
|
}
|
||||||
|
|
@ -1897,48 +1901,6 @@ details.repo-search-result summary::marker {
|
||||||
font-weight: var(--font-weight-medium);
|
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 {
|
#search-user-box .results .result .image {
|
||||||
order: 0;
|
order: 0;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
|
|
@ -2381,6 +2343,7 @@ tbody.commit-list {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
padding: 6px 12px !important;
|
padding: 6px 12px !important;
|
||||||
font-size: 13px !important;
|
font-size: 13px !important;
|
||||||
|
min-height: 46px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-info {
|
.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 {
|
import {
|
||||||
initRepoCloneLink, initRepoCommonBranchOrTagDropdown, initRepoCommonFilterSearchDropdown,
|
initRepoCloneLink, initRepoCommonBranchOrTagDropdown, initRepoCommonFilterSearchDropdown,
|
||||||
} from './repo-common.js';
|
} from './repo-common.js';
|
||||||
import {initCitationFileCopyContent} from './citation.js';
|
|
||||||
import {initCompLabelEdit} from './comp/LabelEdit.js';
|
import {initCompLabelEdit} from './comp/LabelEdit.js';
|
||||||
import {initRepoDiffConversationNav} from './repo-diff.js';
|
import {initRepoDiffConversationNav} from './repo-diff.js';
|
||||||
import {showErrorToast} from '../modules/toast.js';
|
import {showErrorToast} from '../modules/toast.js';
|
||||||
|
|
@ -457,7 +456,6 @@ export function initRepository() {
|
||||||
}
|
}
|
||||||
|
|
||||||
initRepoCloneLink();
|
initRepoCloneLink();
|
||||||
initCitationFileCopyContent();
|
|
||||||
initRepoSettingBranches();
|
initRepoSettingBranches();
|
||||||
|
|
||||||
// Issues
|
// 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';
|
import {onDomReady} from '../utils/dom.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -22,6 +25,9 @@ const loadableComponents = {
|
||||||
'pdf-object': lazyPromise(() => {
|
'pdf-object': lazyPromise(() => {
|
||||||
return import(/* webpackChunkName: "pdf-object" */ './pdf-object.js');
|
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