diff --git a/assets/js/bioconductor.js b/assets/js/bioconductor.js
index 6c86338b..fdbe6427 100644
--- a/assets/js/bioconductor.js
+++ b/assets/js/bioconductor.js
@@ -320,6 +320,84 @@ var getHrefForSymlinks = function (href) {
}
};
+var fallbackCopyText = function (text) {
+ return new Promise(function (resolve, reject) {
+ var textarea = document.createElement("textarea");
+ textarea.value = text;
+ textarea.setAttribute("readonly", "");
+ textarea.style.position = "absolute";
+ textarea.style.left = "-9999px";
+ document.body.appendChild(textarea);
+ textarea.select();
+ textarea.setSelectionRange(0, textarea.value.length);
+ try {
+ if (document.execCommand("copy")) {
+ resolve();
+ } else {
+ reject(new Error("Copy command failed"));
+ }
+ } catch (e) {
+ reject(e);
+ } finally {
+ document.body.removeChild(textarea);
+ }
+ });
+};
+
+var writeTextToClipboard = function (text) {
+ if (navigator.clipboard && window.isSecureContext) {
+ return navigator.clipboard.writeText(text).catch(function () {
+ return fallbackCopyText(text);
+ });
+ }
+ return fallbackCopyText(text);
+};
+
+var CLIPBOARD_EMOJI = "\uD83D\uDCCB";
+var DOI_BODY_PATTERN = "[\\w.()/_-]+";
+var DOI_PATTERN = "10\\.\\d{4,}\\/" + DOI_BODY_PATTERN;
+
+var encodeForShieldsIO = function (text) {
+ return text
+ .replace(/-/g, "--")
+ .replace(/_/g, "__")
+ .replace(/ /g, "_")
+ .replace(/[^A-Za-z0-9._-]/g, function (c) { return encodeURIComponent(c); });
+};
+
+// Copy text to clipboard and show brief feedback on the button
+var copyToClipboardWithFeedback = function (btn, text, label, statusEl) {
+ writeTextToClipboard(text).then(function () {
+ btn.text("Copied!").addClass("copied");
+ if (statusEl) { statusEl.text("Copied to clipboard."); }
+ setTimeout(function () {
+ btn.text(label).removeClass("copied");
+ if (statusEl) { statusEl.text(""); }
+ }, 2000);
+ }).catch(function () {
+ btn.text("Copy failed");
+ if (statusEl) { statusEl.text("Copy failed. Please copy the text manually."); }
+ setTimeout(function () {
+ btn.text(label);
+ if (statusEl) { statusEl.text(""); }
+ }, 3000);
+ });
+};
+
+// Attach event listeners to static citation copy buttons that use data attributes.
+// Handles .citation-btn elements with data-citation-text or data-bibtex attributes,
+// replacing the need for inline onclick handlers in content pages.
+var handleStaticCitationButtons = function () {
+ jQuery(document).on("click", ".citation-btn[data-citation-text]", function () {
+ var btn = jQuery(this);
+ copyToClipboardWithFeedback(btn, btn.data("citation-text"), btn.text(), null);
+ });
+ jQuery(document).on("click", ".citation-btn[data-bibtex]", function () {
+ var btn = jQuery(this);
+ copyToClipboardWithFeedback(btn, btn.data("bibtex"), btn.text(), null);
+ });
+};
+
var handleCitations = function () {
if (jQuery("#bioc-citation").length) {
jQuery("#bioc-citation-outer").hide();
@@ -331,6 +409,8 @@ var handleCitations = function () {
segs.push(pkg);
segs.push("citation.html");
url = segs.join("/");
+ var bibUrl = url.replace("citation.html", "citation.bib");
+ var pkgName = jQuery("#bioc-citation-outer").data("package") || pkg;
jQuery.ajax({
url: url,
dataType: "html",
@@ -342,6 +422,69 @@ var handleCitations = function () {
data = data.replace(" (????)", "");
jQuery("#bioc-citation").html(data);
+
+ // Extract preferred DOI from citation text.
+ // Use a pattern matching only valid DOI characters per the DOI specification.
+ var citationText = jQuery("#bioc-citation").text();
+ var doiPattern = new RegExp("\\bdoi:?(" + DOI_PATTERN + ")", "i");
+ var urlPattern = new RegExp("https?:\\/\\/doi\\.org\\/(" + DOI_PATTERN + ")", "i");
+ var doiMatch = citationText.match(doiPattern) || citationText.match(urlPattern);
+ var preferredDoi = doiMatch ? doiMatch[1] : "10.18129/B9.bioc." + pkgName;
+
+ // Sanitize DOI: only allow characters valid in a DOI (alphanumeric and
+ // DOI-permitted punctuation). Falls back to the package landing page DOI.
+ if (!(new RegExp("^" + DOI_PATTERN + "$")).test(preferredDoi)) {
+ preferredDoi = "10.18129/B9.bioc." + pkgName;
+ }
+
+ var encodedDoi = encodeForShieldsIO(preferredDoi);
+
+ var doiHref = "https://doi.org/" + encodeURIComponent(preferredDoi);
+ var badgeSrc = "https://img.shields.io/badge/DOI-" + encodedDoi + "-blue";
+ var $badgeLink = jQuery("")
+ .attr("href", doiHref)
+ .attr("title", "Preferred citation DOI");
+ var $badgeImg = jQuery("
Maintainer: <%= filter_emails(@package[:Maintainer])%>
Huber W, Carey VJ, Gentleman R, Anders S, Carlson M, Carvalho BS,
+ Bravo HC, Davis S, Gatto L, Girke T, Gottardo R, Hahne F, Hansen KD, Irizarry RA,
+ Lawrence M, Love MI, MacDonald J, Obenchain V, OleŠAK, Pagès H, Reyes A, Shannon P,
+ Smyth GK, Tenenbaum D, Waldron L, Morgan M (2015). "Orchestrating high-throughput
+ genomic analysis with Bioconductor." Nature Methods, 12(2),
+ 115β121. doi:10.1038/nmeth.3252. Gentleman RC, Carey VJ, Bates DM, Bolstad B, Dettling M, Dudoit S,
+ Ellis B, Gautier L, Ge Y, Gentry J, Hornik K, Hothorn T, Huber W, Iacus S, Irizarry R,
+ Leisch F, Li C, Maechler M, Rossini AJ, Sawitzki G, Smith C, Smyth G, Tierney L, Yang JYH,
+ Zhang J (2004). "Bioconductor: open software development for computational biology and
+ bioinformatics." Genome Biology, 5(10), R80.
+ doi:10.1186/gb-2004-5-10-r80.")
+ .attr("src", badgeSrc)
+ .attr("alt", "DOI badge");
+ jQuery("#citation-doi-badge").empty().append($badgeLink.append($badgeImg));
+
+ // Add copy action buttons
+ var actionsHtml =
+ '' +
+ '';
+ jQuery("#bioc-citation-actions").html(actionsHtml);
+
+ jQuery("#bioc-copy-text-btn").on("click", function () {
+ var btn = jQuery(this);
+ var statusEl = jQuery("#bioc-citation-status");
+ var text = jQuery("#bioc-citation").text().trim();
+ copyToClipboardWithFeedback(btn, text, CLIPBOARD_EMOJI + " Copy Text", statusEl);
+ });
+
+ jQuery("#bioc-copy-bibtex-btn").on("click", function () {
+ var btn = jQuery(this);
+ var statusEl = jQuery("#bioc-citation-status");
+ var origLabel = CLIPBOARD_EMOJI + " Copy BibTeX";
+ btn.prop("disabled", true);
+ jQuery.ajax({
+ url: bibUrl,
+ dataType: "text",
+ success: function (bibData) {
+ copyToClipboardWithFeedback(btn, bibData, origLabel, statusEl);
+ btn.prop("disabled", false);
+ },
+ error: function () {
+ btn.text("BibTeX unavailable");
+ if (statusEl) { statusEl.text("BibTeX format is not available for this package."); }
+ setTimeout(function () {
+ btn.text(origLabel).prop("disabled", false);
+ if (statusEl) { statusEl.text(""); }
+ }, 3000);
+ },
+ });
+ });
+
jQuery("#bioc-citation-outer").show();
},
error: function (data, textStatus, jqXHR) {
@@ -361,6 +504,7 @@ jQuery(function () {
});
jQuery(".rpack").tooltip({ tip: "#tooltip" }); //{ effect: 'slide'});
handleCitations();
+ handleStaticCitationButtons();
});
var submit_tryitnow = function () {
diff --git a/assets/style/pages/packages.css b/assets/style/pages/packages.css
index 850a6315..8c2e1159 100644
--- a/assets/style/pages/packages.css
+++ b/assets/style/pages/packages.css
@@ -128,6 +128,43 @@ ul.ui-autocomplete li::marker {
margin: 0.5rem 0 1rem;
}
+.citation-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ margin: 0.75rem 0 0.25rem;
+}
+
+.citation-btn {
+ background: var(--neutral-n50);
+ color: var(--primary-p400);
+ border: 1px solid var(--neutral-n75);
+ border-radius: 8rem;
+ cursor: pointer;
+ font-size: 0.85rem;
+ padding: 0.2rem 0.75rem;
+ transition: background 0.2s;
+}
+
+.citation-btn:hover {
+ background: var(--primary-p50);
+}
+
+.citation-btn:active {
+ background: var(--primary-p200);
+}
+
+.citation-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.citation-btn.copied {
+ background: #d4edda;
+ color: #155724;
+ border-color: #c3e6cb;
+}
+
@media (max-device-width: 1450px), (max-width: 1450px) {
.biocViewsTreeContainer {
display: flex;
diff --git a/layouts/_bioc_views_package_detail.html b/layouts/_bioc_views_package_detail.html
index 4575078e..d2c40995 100644
--- a/layouts/_bioc_views_package_detail.html
+++ b/layouts/_bioc_views_package_detail.html
@@ -34,9 +34,34 @@
<%= @package[:Title]%>
citation("<%=@package[:Package]%>")):
+
+
+ <%= @package[:Title]%>
<%= render("/components/packages/details/", :package => @package) %>
<%= render("/components/packages/archives/", :package => @package) %>
-
diff --git a/layouts/components/packages/badges.html b/layouts/components/packages/badges.html
index 9a8490d0..dfd203ec 100644
--- a/layouts/components/packages/badges.html
+++ b/layouts/components/packages/badges.html
@@ -161,4 +161,5 @@
10.18129/B9.bioc.<%=@item[:Package]%>