From 8f28cdefe047ab5e405af42904d9b645d3e85012 Mon Sep 17 00:00:00 2001 From: oliverpool Date: Fri, 14 Nov 2025 14:39:20 +0100 Subject: [PATCH] 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: ![image](/attachments/be02656f-d906-4191-aa84-d666ee5a90ba) ![image](/attachments/240384e3-dec8-4f02-94e6-261143193541) ### 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: ![image](/attachments/ef79128d-ee3f-4e43-a74d-a00e4dcfe6b4) And is displayed as a popup: ![image](/attachments/7aa930f9-0766-47b9-8145-cbebb5b051b0) Co-authored-by: 0ko <0ko@noreply.codeberg.org> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9103 Reviewed-by: Gusted Reviewed-by: 0ko <0ko@noreply.codeberg.org> Co-authored-by: oliverpool Co-committed-by: oliverpool --- modules/git/blob.go | 16 --- modules/git/blob_test.go | 23 ---- routers/web/repo/view.go | 18 ++- templates/repo/cite/cite_buttons.tmpl | 8 -- templates/repo/cite/cite_modal.tmpl | 20 --- templates/repo/home.tmpl | 5 +- templates/repo/view_file.tmpl | 10 +- tests/e2e/repo-viewer.test.e2e.ts | 47 ++++++++ .../32/b0f4a0e8e3f701bf47538436eee8caee863e0e | Bin 0 -> 7128 bytes .../59/d5df6b2b9c8053cf720403d5716b8ce4a2c6ec | Bin 0 -> 234 bytes .../74/9b58220a05257c538e036c6196f679f6ccc682 | Bin 0 -> 336 bytes .../ad/93f47c782e0e2d9d4c05e7e41114de929683e8 | Bin 0 -> 188 bytes .../e8/196c874f13227602a2b680c30eef433036e213 | Bin 0 -> 188 bytes .../e8/27e62cc9ac9db89f6716ee991c8f16507d87e6 | Bin 0 -> 197 bytes .../rendering-test.git/refs/heads/master | 2 +- tests/integration/api_packages_cargo_test.go | 7 +- tests/integration/repo_citation_test.go | 41 ++++--- web_src/css/modules/animations.css | 4 + web_src/css/repo.css | 47 +------- web_src/js/features/citation.js | 50 -------- web_src/js/features/repo-legacy.js | 2 - .../js/webcomponents/citation-information.js | 114 ++++++++++++++++++ web_src/js/webcomponents/lazy-webc.js | 6 + 23 files changed, 225 insertions(+), 195 deletions(-) delete mode 100644 templates/repo/cite/cite_buttons.tmpl delete mode 100644 templates/repo/cite/cite_modal.tmpl create mode 100644 tests/e2e/repo-viewer.test.e2e.ts create mode 100644 tests/gitea-repositories-meta/user2/rendering-test.git/objects/32/b0f4a0e8e3f701bf47538436eee8caee863e0e create mode 100644 tests/gitea-repositories-meta/user2/rendering-test.git/objects/59/d5df6b2b9c8053cf720403d5716b8ce4a2c6ec create mode 100644 tests/gitea-repositories-meta/user2/rendering-test.git/objects/74/9b58220a05257c538e036c6196f679f6ccc682 create mode 100644 tests/gitea-repositories-meta/user2/rendering-test.git/objects/ad/93f47c782e0e2d9d4c05e7e41114de929683e8 create mode 100644 tests/gitea-repositories-meta/user2/rendering-test.git/objects/e8/196c874f13227602a2b680c30eef433036e213 create mode 100644 tests/gitea-repositories-meta/user2/rendering-test.git/objects/e8/27e62cc9ac9db89f6716ee991c8f16507d87e6 delete mode 100644 web_src/js/features/citation.js create mode 100644 web_src/js/webcomponents/citation-information.js diff --git a/modules/git/blob.go b/modules/git/blob.go index 4eef5f0e2a..e2dc624e86 100644 --- a/modules/git/blob.go +++ b/modules/git/blob.go @@ -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 } diff --git a/modules/git/blob_test.go b/modules/git/blob_test.go index a4b8033941..7caa2d2de3 100644 --- a/modules/git/blob_test.go +++ b/modules/git/blob_test.go @@ -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) diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 3b11d73390..5edf1163ae 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -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 } } } diff --git a/templates/repo/cite/cite_buttons.tmpl b/templates/repo/cite/cite_buttons.tmpl deleted file mode 100644 index 5a6de23c5c..0000000000 --- a/templates/repo/cite/cite_buttons.tmpl +++ /dev/null @@ -1,8 +0,0 @@ - -BibTeX - - - - diff --git a/templates/repo/cite/cite_modal.tmpl b/templates/repo/cite/cite_modal.tmpl deleted file mode 100644 index 1ce959a5c5..0000000000 --- a/templates/repo/cite/cite_modal.tmpl +++ /dev/null @@ -1,20 +0,0 @@ - diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index d72b8aaa32..eb27af5d54 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -130,8 +130,8 @@ {{svg "octicon-file-zip" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_tar"}} {{svg "octicon-package" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_bundle"}} {{end}} - {{if .CitationExist}} - {{svg "octicon-cross-reference" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.cite_this_repo"}} + {{if .CitationFile}} + {{svg "octicon-cross-reference" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.cite_this_repo"}} {{end}} {{range .OpenWithEditorApps}} {{.IconHTML}}{{ctx.Locale.Tr "repo.open_with_editor" .DisplayName}} @@ -140,7 +140,6 @@ {{template "repo/clone_script" .}}{{/* the script will update `.js-clone-url` and related elements */}} - {{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 */}}
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index 65be791405..411abfa7b4 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -26,9 +26,9 @@ {{end}}

-
+
{{if .ReadmeInList}} - {{svg "octicon-book" 16 "tw-mr-2"}} + {{svg "octicon-book"}} {{.FileName}} {{else}} {{template "repo/file_info" .}} @@ -150,6 +150,9 @@ {{else}} + {{if .IsCitationFile}} + + {{end}} {{range $idx, $code := .FileContent}} @@ -164,6 +167,9 @@ {{end}}
+ {{if .IsCitationFile}} +
+ {{end}}
{{if $.Permission.CanRead $.UnitTypeIssues}} {{ctx.Locale.Tr "repo.issues.context.reference_issue"}} diff --git a/tests/e2e/repo-viewer.test.e2e.ts b/tests/e2e/repo-viewer.test.e2e.ts new file mode 100644 index 0000000000..39e3d1d5b8 --- /dev/null +++ b/tests/e2e/repo-viewer.test.e2e.ts @@ -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(); +}); diff --git a/tests/gitea-repositories-meta/user2/rendering-test.git/objects/32/b0f4a0e8e3f701bf47538436eee8caee863e0e b/tests/gitea-repositories-meta/user2/rendering-test.git/objects/32/b0f4a0e8e3f701bf47538436eee8caee863e0e new file mode 100644 index 0000000000000000000000000000000000000000..b7ad82e28a5d07688fa2d3645f034bb228340011 GIT binary patch literal 7128 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 kRBcIO4vMkcFD1VaE^)k5_C3 literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/rendering-test.git/objects/74/9b58220a05257c538e036c6196f679f6ccc682 b/tests/gitea-repositories-meta/user2/rendering-test.git/objects/74/9b58220a05257c538e036c6196f679f6ccc682 new file mode 100644 index 0000000000000000000000000000000000000000..385fabe8ea17e5dca165d1d2f31a094e545c8709 GIT binary patch literal 336 zcmV-W0k8ge0ZYosPf{>7v}8z5OVce&Eh^5;&$Ci6)HBjE;3~;1$w{?RaLX@BPtD5b z%1tdUPE1d=QgBMG%+E_vNX}2m%uCl(2v1c=11ZttDyb|;wNfa~Pb(=;EK21{EG@~% zFDka;Qc%!U$V<#cn5E#HpI1_pnN(VmUsTMcprD|Tlb@Ve0=C;RCr2T_EVW3XBqLR! zJijO>1+1VvHK{nWB-KhmJ)@+gpx8=ZA8ct>zFvM&y1qJBW=d*aNoHDRD$K$XuwzP# za=_Zl5_3v%YX-R>CAB!YD6;_Kh;WE)3i)YZXA~6WXQd{Wa22H%GZ iIjM=osm1DCIho0+dBv$#3hn_uy2g42+FSr8p|vHT!Itm< literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/rendering-test.git/objects/ad/93f47c782e0e2d9d4c05e7e41114de929683e8 b/tests/gitea-repositories-meta/user2/rendering-test.git/objects/ad/93f47c782e0e2d9d4c05e7e41114de929683e8 new file mode 100644 index 0000000000000000000000000000000000000000..425444f3cab6deba95fe6af238a892b7d72c1157 GIT binary patch literal 188 zcmV;t07L(H0V^p=O;s>5G-oh0FfcPQQE>JQaSZYF_tQ&GOJgXR9ihaJpQR^K(i|QuQ*^iV|~EGfHxE7;fFEVoSf2E`M>`u^o+m|5Y3|@3w|2 zPfN_qK~`!S`%h(Zk3HLxqwM{Q_r}|#9<1qyDlN{)FDgM+Ym{|uN5F-O+|v&=e#;yx q^;>Rvl@Y48BrylY*zK2+UkH~t-YNSY>1W!rrB3o({X+l(_E`=U%33@C literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/rendering-test.git/objects/e8/196c874f13227602a2b680c30eef433036e213 b/tests/gitea-repositories-meta/user2/rendering-test.git/objects/e8/196c874f13227602a2b680c30eef433036e213 new file mode 100644 index 0000000000000000000000000000000000000000..092105285866f365735c7c5eb12c02a3b8de2a2d GIT binary patch literal 188 zcmV;t07L(H0ZY!$&CM)PFfuY_C@D%!RWP+oF-=J`OEOBbOtvsEHBL@5H!?6WFitTw zH#AGKNKQ3LG)gv0P39^{EK1EQQAo8gGEX%#N=~*+Otwr(van1uGdDC#O|`T%Otwff zG&40YPq8phHRDPwEy>6)QpnHAEK4ma$j{GFuu0D>aRBi%Q;YNp(h^hj(u(X949(39 q%}h*Hwcsj9EK1EQQAkTnOH52LH&0G7H8C?YO-!~hFi0^>N;5Z4HA^%# zO-eB~HBC)SO6E!|Ey>6)QpnHAEK4ma$j{GFuu0D>aRBi%Q;YNp(h^hj(u(X949(39 z%}h)UEsPbk4GawoxF9YpNhL=wS7J(vg0p9cV~D4}pI&lWnnGG;PAV4wECx#wY^g>2 literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/rendering-test.git/refs/heads/master b/tests/gitea-repositories-meta/user2/rendering-test.git/refs/heads/master index 69d0f8e29c..0e5299ddca 100644 --- a/tests/gitea-repositories-meta/user2/rendering-test.git/refs/heads/master +++ b/tests/gitea-repositories-meta/user2/rendering-test.git/refs/heads/master @@ -1 +1 @@ -fafaad77cb54665ac800d1bf77e6a55bd355eabc +e8196c874f13227602a2b680c30eef433036e213 diff --git a/tests/integration/api_packages_cargo_test.go b/tests/integration/api_packages_cargo_test.go index a72ae4a394..8a7f05d6d3 100644 --- a/tests/integration/api_packages_cargo_test.go +++ b/tests/integration/api_packages_cargo_test.go @@ -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) diff --git a/tests/integration/repo_citation_test.go b/tests/integration/repo_citation_test.go index 7a3251d987..7925966f4f 100644 --- a/tests/integration/repo_citation_test.go +++ b/tests/integration/repo_citation_test.go @@ -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 }, }) diff --git a/web_src/css/modules/animations.css b/web_src/css/modules/animations.css index 88b128c643..a820787b16 100644 --- a/web_src/css/modules/animations.css +++ b/web_src/css/modules/animations.css @@ -3,6 +3,10 @@ 100% { transform: translate(-50%, -50%) rotate(360deg); } } +lazy-webc { + display: block; +} + lazy-webc, .is-loading { pointer-events: none !important; diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 3d47ffece1..d51b340e0f 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -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 { diff --git a/web_src/js/features/citation.js b/web_src/js/features/citation.js deleted file mode 100644 index 7e26bff276..0000000000 --- a/web_src/js/features/citation.js +++ /dev/null @@ -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'); - }); -} diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js index 74efdaa530..05766c6c78 100644 --- a/web_src/js/features/repo-legacy.js +++ b/web_src/js/features/repo-legacy.js @@ -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 diff --git a/web_src/js/webcomponents/citation-information.js b/web_src/js/webcomponents/citation-information.js new file mode 100644 index 0000000000..6ea9fdcc0c --- /dev/null +++ b/web_src/js/webcomponents/citation-information.js @@ -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( + ` 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); + } + }, +); diff --git a/web_src/js/webcomponents/lazy-webc.js b/web_src/js/webcomponents/lazy-webc.js index 3570df3b5d..e1a8513d8d 100644 --- a/web_src/js/webcomponents/lazy-webc.js +++ b/web_src/js/webcomponents/lazy-webc.js @@ -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'); + }), }; /**