From 46b9a27b782a046fa2079d050370bb8efa3c8f7b Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 14 Nov 2021 01:16:04 +0100 Subject: [PATCH 01/20] Add copy button to markdown code blocks Done mostly in JS because I think it's better not to try getting buttons past the markup sanitizer. --- modules/markup/markdown/markdown.go | 19 +++++----------- modules/markup/sanitizer.go | 4 ++++ options/locale/locale_en-US.ini | 2 +- templates/base/head.tmpl | 5 +++++ web_src/js/features/clipboard.js | 7 ++---- web_src/js/markup/codecopy.js | 21 +++++++++++++++++ web_src/js/markup/content.js | 4 +++- web_src/js/markup/mermaid.js | 5 +++-- web_src/js/svg.js | 35 +++++++++++++++++++++-------- web_src/less/animations.less | 17 ++++++++++++++ web_src/less/index.less | 2 ++ web_src/less/markup/codecopy.less | 25 +++++++++++++++++++++ 12 files changed, 115 insertions(+), 31 deletions(-) create mode 100644 web_src/js/markup/codecopy.js create mode 100644 web_src/less/animations.less create mode 100644 web_src/less/markup/codecopy.less diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index 554ee0d4be8cf..594e2473e9977 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -107,26 +107,19 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) languageStr := string(language) - preClasses := []string{} + preClasses := []string{"code-block"} if languageStr == "mermaid" { preClasses = append(preClasses, "is-loading") } - if len(preClasses) > 0 { - _, err := w.WriteString(`
`)
-								if err != nil {
-									return
-								}
-							} else {
-								_, err := w.WriteString(`
`)
-								if err != nil {
-									return
-								}
+							_, err := w.WriteString(`
`)
+							if err != nil {
+								return
 							}
 
 							// include language-x class as part of commonmark spec
-							_, err := w.WriteString(``)
-							if err != nil {
+							_, err2 := w.WriteString(``)
+							if err2 != nil {
 								return
 							}
 						} else {
diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go
index c8f9de33b5fb7..fc23bd472d81c 100644
--- a/modules/markup/sanitizer.go
+++ b/modules/markup/sanitizer.go
@@ -52,6 +52,10 @@ func InitializeSanitizer() {
 
 func createDefaultPolicy() *bluemonday.Policy {
 	policy := bluemonday.UGCPolicy()
+
+	// For JS code copy
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block$`)).OnElements("pre")
+
 	// For Chroma markdown plugin
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^is-loading$`)).OnElements("pre")
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+$`)).OnElements("code")
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 6fad20c87ed00..fa227074dfa2a 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -932,7 +932,7 @@ copy_link_error = Use ⌘C or Ctrl-C to copy
 copy_branch = Copy
 copy_branch_success = Branch name has been copied
 copy_branch_error = Use ⌘C or Ctrl-C to copy
-copied = Copied OK
+copied = Copied
 unwatch = Unwatch
 watch = Watch
 unstar = Unstar
diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl
index 23d1190d94589..5ad49550d4328 100644
--- a/templates/base/head.tmpl
+++ b/templates/base/head.tmpl
@@ -47,6 +47,11 @@
 			{{end}}
 			mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}},
 		};
+
+		window.i18n = {
+			copied: '{{.i18n.Tr "repo.copied"}}',
+			copy_link_error: '{{.i18n.Tr "repo.copy_link_error"}}',
+		};
 	
 	
 	
diff --git a/web_src/js/features/clipboard.js b/web_src/js/features/clipboard.js
index 89aface93aca5..fee12fa7bb954 100644
--- a/web_src/js/features/clipboard.js
+++ b/web_src/js/features/clipboard.js
@@ -1,21 +1,18 @@
 // For all DOM elements with [data-clipboard-target] or [data-clipboard-text], this copy-to-clipboard will work for them
 
-// TODO: replace these with toast-style notifications
 function onSuccess(btn) {
-  if (!btn.dataset.content) return;
   $(btn).popup('destroy');
   const oldContent = btn.dataset.content;
   btn.dataset.content = btn.dataset.success;
   $(btn).popup('show');
-  btn.dataset.content = oldContent;
+  btn.dataset.content = oldContent || '';
 }
 function onError(btn) {
-  if (!btn.dataset.content) return;
   const oldContent = btn.dataset.content;
   $(btn).popup('destroy');
   btn.dataset.content = btn.dataset.error;
   $(btn).popup('show');
-  btn.dataset.content = oldContent;
+  btn.dataset.content = oldContent || '';
 }
 
 /**
diff --git a/web_src/js/markup/codecopy.js b/web_src/js/markup/codecopy.js
new file mode 100644
index 0000000000000..09b2c16be2801
--- /dev/null
+++ b/web_src/js/markup/codecopy.js
@@ -0,0 +1,21 @@
+import {svgNode} from '../svg.js';
+const {copied, copy_link_error} = window.i18n;
+
+// els refers to the code elements
+export function renderCodeCopy() {
+  const els = document.querySelectorAll('.markup .code-block code');
+  if (!els?.length) return;
+
+  const button = document.createElement('button');
+  button.classList.add('code-copy', 'ui', 'button');
+  button.setAttribute('data-success', copied);
+  button.setAttribute('data-error', copy_link_error);
+  button.setAttribute('data-variation', 'inverted tiny');
+  button.appendChild(svgNode('octicon-copy'));
+
+  for (const el of els) {
+    const btn = button.cloneNode(true);
+    btn.setAttribute('data-clipboard-text', el.textContent);
+    el.after(btn);
+  }
+}
diff --git a/web_src/js/markup/content.js b/web_src/js/markup/content.js
index 0564199bbffab..ef5067fd66520 100644
--- a/web_src/js/markup/content.js
+++ b/web_src/js/markup/content.js
@@ -1,9 +1,11 @@
 import {renderMermaid} from './mermaid.js';
+import {renderCodeCopy} from './codecopy.js';
 import {initMarkupTasklist} from './tasklist.js';
 
 // code that runs for all markup content
 export function initMarkupContent() {
-  const _promise = renderMermaid(document.querySelectorAll('code.language-mermaid'));
+  renderMermaid();
+  renderCodeCopy();
 }
 
 // code that only runs for comments
diff --git a/web_src/js/markup/mermaid.js b/web_src/js/markup/mermaid.js
index f9f069ed1e01a..1d9cc82b59839 100644
--- a/web_src/js/markup/mermaid.js
+++ b/web_src/js/markup/mermaid.js
@@ -8,8 +8,9 @@ function displayError(el, err) {
   el.closest('pre').before(errorNode);
 }
 
-export async function renderMermaid(els) {
-  if (!els || !els.length) return;
+export async function renderMermaid() {
+  const els = document.querySelectorAll('.markup code.language-mermaid');
+  if (!els?.length) return;
 
   const {default: mermaid} = await import(/* webpackChunkName: "mermaid" */'mermaid');
 
diff --git a/web_src/js/svg.js b/web_src/js/svg.js
index 11be6b476c711..976abaae7f7a2 100644
--- a/web_src/js/svg.js
+++ b/web_src/js/svg.js
@@ -1,5 +1,6 @@
 import octiconChevronDown from '../../public/img/svg/octicon-chevron-down.svg';
 import octiconChevronRight from '../../public/img/svg/octicon-chevron-right.svg';
+import octiconCopy from '../../public/img/svg/octicon-copy.svg';
 import octiconGitMerge from '../../public/img/svg/octicon-git-merge.svg';
 import octiconGitPullRequest from '../../public/img/svg/octicon-git-pull-request.svg';
 import octiconIssueClosed from '../../public/img/svg/octicon-issue-closed.svg';
@@ -20,6 +21,7 @@ import Vue from 'vue';
 export const svgs = {
   'octicon-chevron-down': octiconChevronDown,
   'octicon-chevron-right': octiconChevronRight,
+  'octicon-copy': octiconCopy,
   'octicon-git-merge': octiconGitMerge,
   'octicon-git-pull-request': octiconGitPullRequest,
   'octicon-issue-closed': octiconIssueClosed,
@@ -38,18 +40,33 @@ export const svgs = {
 
 const parser = new DOMParser();
 const serializer = new XMLSerializer();
+const parsedSvgs = new Map();
 
-// retrieve a HTML string for given SVG icon name, size and additional classes
-export function svg(name, size = 16, className = '') {
+function getParsedSvg(name) {
+  if (parsedSvgs.has(name)) return parsedSvgs.get(name);
+  const root = parser.parseFromString(svgs[name], 'text/html');
+  const svgNode = root.querySelector('svg');
+  parsedSvgs.set(name, svgNode);
+  return svgNode;
+}
+
+function applyAttributes(node, size, className) {
+  if (size !== 16) node.setAttribute('width', String(size));
+  if (size !== 16) node.setAttribute('height', String(size));
+  if (className) node.classList.add(...className.split(/\s+/));
+  return node;
+}
+
+// returns a SVG node for given SVG icon name, size and additional classes
+export function svgNode(name, size = 16, className = '') {
+  return applyAttributes(getParsedSvg(name), size, className);
+}
+
+// returns a HTML string for given SVG icon name, size and additional classes
+export function svg(name, size, className) {
   if (!(name in svgs)) return '';
   if (size === 16 && !className) return svgs[name];
-
-  const document = parser.parseFromString(svgs[name], 'image/svg+xml');
-  const svgNode = document.firstChild;
-  if (size !== 16) svgNode.setAttribute('width', String(size));
-  if (size !== 16) svgNode.setAttribute('height', String(size));
-  if (className) svgNode.classList.add(...className.split(/\s+/));
-  return serializer.serializeToString(svgNode);
+  return serializer.serializeToString(svgNode(name, size, className));
 }
 
 export const SvgIcon = Vue.component('SvgIcon', {
diff --git a/web_src/less/animations.less b/web_src/less/animations.less
new file mode 100644
index 0000000000000..17e8c20084dbe
--- /dev/null
+++ b/web_src/less/animations.less
@@ -0,0 +1,17 @@
+@keyframes fadein {
+  0% {
+    opacity: 0;
+  }
+  100% {
+    opacity: 1;
+  }
+}
+
+@keyframes fadeout {
+  0% {
+    opacity: 1;
+  }
+  100% {
+    opacity: 0;
+  }
+}
diff --git a/web_src/less/index.less b/web_src/less/index.less
index d96fe3df82612..128ba59ba1d39 100644
--- a/web_src/less/index.less
+++ b/web_src/less/index.less
@@ -1,6 +1,7 @@
 @import "font-awesome/css/font-awesome.css";
 
 @import "./variables.less";
+@import "./animations.less";
 @import "./shared/issuelist.less";
 @import "./features/animations.less";
 @import "./features/dropzone.less";
@@ -11,6 +12,7 @@
 @import "./features/projects.less";
 @import "./markup/content.less";
 @import "./markup/mermaid.less";
+@import "./markup/codecopy.less";
 @import "./code/linebutton.less";
 
 @import "./chroma/base.less";
diff --git a/web_src/less/markup/codecopy.less b/web_src/less/markup/codecopy.less
new file mode 100644
index 0000000000000..f1899be1b8fd0
--- /dev/null
+++ b/web_src/less/markup/codecopy.less
@@ -0,0 +1,25 @@
+.markup .code-block {
+  position: relative;
+}
+
+.markup .code-copy {
+  position: absolute;
+  top: .5rem;
+  right: .5rem;
+  padding: 10px;
+  visibility: hidden;
+  animation: fadeout .2s both;
+}
+
+.markup .code-copy:hover {
+  background: var(--color-secondary) !important;
+}
+
+.markup .code-copy:active {
+  background: var(--color-secondary-dark-1) !important;
+}
+
+.markup .code-block:hover .code-copy {
+  visibility: visible;
+  animation: fadein .2s both;
+}

From e6a47337f5d43d3c814ea1aa4a65fffab9902672 Mon Sep 17 00:00:00 2001
From: silverwind 
Date: Sun, 14 Nov 2021 01:35:32 +0100
Subject: [PATCH 02/20] add svg module tests

---
 jest.config.js         |  4 +++-
 package-lock.json      | 13 +++++++++++++
 package.json           |  1 +
 web_src/js/svg.test.js |  9 +++++++++
 4 files changed, 26 insertions(+), 1 deletion(-)
 create mode 100644 web_src/js/svg.test.js

diff --git a/jest.config.js b/jest.config.js
index c94113d6f423c..690f58d177199 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -4,7 +4,9 @@ export default {
   testEnvironment: 'jsdom',
   testMatch: ['/**/*.test.js'],
   testTimeout: 20000,
-  transform: {},
+  transform: {
+    '\\.svg$': 'jest-raw-loader',
+  },
   verbose: false,
 };
 
diff --git a/package-lock.json b/package-lock.json
index df4c575469fa2..2ebeff0f305d8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -51,6 +51,7 @@
         "eslint-plugin-vue": "8.0.3",
         "jest": "27.3.1",
         "jest-extended": "1.1.0",
+        "jest-raw-loader": "1.0.1",
         "postcss-less": "5.0.0",
         "stylelint": "14.0.1",
         "stylelint-config-standard": "23.0.0",
@@ -6221,6 +6222,12 @@
         }
       }
     },
+    "node_modules/jest-raw-loader": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/jest-raw-loader/-/jest-raw-loader-1.0.1.tgz",
+      "integrity": "sha1-zp9W1UZQ8VfEp9FtIkul1hO81iY=",
+      "dev": true
+    },
     "node_modules/jest-regex-util": {
       "version": "27.0.6",
       "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.0.6.tgz",
@@ -14693,6 +14700,12 @@
       "dev": true,
       "requires": {}
     },
+    "jest-raw-loader": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/jest-raw-loader/-/jest-raw-loader-1.0.1.tgz",
+      "integrity": "sha1-zp9W1UZQ8VfEp9FtIkul1hO81iY=",
+      "dev": true
+    },
     "jest-regex-util": {
       "version": "27.0.6",
       "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.0.6.tgz",
diff --git a/package.json b/package.json
index 71c9ab40fd4fe..3c63141922f08 100644
--- a/package.json
+++ b/package.json
@@ -51,6 +51,7 @@
     "eslint-plugin-vue": "8.0.3",
     "jest": "27.3.1",
     "jest-extended": "1.1.0",
+    "jest-raw-loader": "1.0.1",
     "postcss-less": "5.0.0",
     "stylelint": "14.0.1",
     "stylelint-config-standard": "23.0.0",
diff --git a/web_src/js/svg.test.js b/web_src/js/svg.test.js
new file mode 100644
index 0000000000000..e5eda64106408
--- /dev/null
+++ b/web_src/js/svg.test.js
@@ -0,0 +1,9 @@
+import {svg, svgNode} from './svg.js';
+
+test('svg', () => {
+  expect(svg('octicon-repo')).toStartWith(' {
+  expect(svgNode('octicon-repo')).toBeInstanceOf(Element);
+});

From a742cf712a6c0d6555e0005d3bda784039c528db Mon Sep 17 00:00:00 2001
From: silverwind 
Date: Sun, 14 Nov 2021 01:47:24 +0100
Subject: [PATCH 03/20] fix sanitizer regexp

---
 modules/markup/sanitizer.go | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go
index fc23bd472d81c..5ff26a3109425 100644
--- a/modules/markup/sanitizer.go
+++ b/modules/markup/sanitizer.go
@@ -53,11 +53,10 @@ func InitializeSanitizer() {
 func createDefaultPolicy() *bluemonday.Policy {
 	policy := bluemonday.UGCPolicy()
 
-	// For JS code copy
-	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block$`)).OnElements("pre")
+	// For JS code copy and Mermaid loading state
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre")
 
 	// For Chroma markdown plugin
-	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^is-loading$`)).OnElements("pre")
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+$`)).OnElements("code")
 
 	// Checkboxes

From d360b09efe65e6eefd60348cc13293ed4b9f0dcc Mon Sep 17 00:00:00 2001
From: silverwind 
Date: Sun, 14 Nov 2021 01:48:59 +0100
Subject: [PATCH 04/20] remove outdated comment

---
 web_src/js/markup/codecopy.js | 1 -
 1 file changed, 1 deletion(-)

diff --git a/web_src/js/markup/codecopy.js b/web_src/js/markup/codecopy.js
index 09b2c16be2801..2baaeeaad1084 100644
--- a/web_src/js/markup/codecopy.js
+++ b/web_src/js/markup/codecopy.js
@@ -1,7 +1,6 @@
 import {svgNode} from '../svg.js';
 const {copied, copy_link_error} = window.i18n;
 
-// els refers to the code elements
 export function renderCodeCopy() {
   const els = document.querySelectorAll('.markup .code-block code');
   if (!els?.length) return;

From eb1344d011bbc670abf673ae7f25a95dd6e96263 Mon Sep 17 00:00:00 2001
From: silverwind 
Date: Sun, 14 Nov 2021 02:03:21 +0100
Subject: [PATCH 05/20] vertically center button in issue comments as well

---
 web_src/less/markup/codecopy.less | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/web_src/less/markup/codecopy.less b/web_src/less/markup/codecopy.less
index f1899be1b8fd0..d5daa92afc936 100644
--- a/web_src/less/markup/codecopy.less
+++ b/web_src/less/markup/codecopy.less
@@ -11,6 +11,12 @@
   animation: fadeout .2s both;
 }
 
+/* comment content has 14px font size, reduce padding to make the button appear
+   vertically centered on single-line content, like it does elsewhere */
+.repository.view.issue .comment-list .comment .markup .code-copy {
+  padding: 9px;
+}
+
 .markup .code-copy:hover {
   background: var(--color-secondary) !important;
 }

From 466d4f638e933e4f3ebbd752abe3570f92a59d87 Mon Sep 17 00:00:00 2001
From: silverwind 
Date: Sun, 14 Nov 2021 02:07:22 +0100
Subject: [PATCH 06/20] add comment to css

---
 web_src/less/markup/codecopy.less | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/web_src/less/markup/codecopy.less b/web_src/less/markup/codecopy.less
index d5daa92afc936..605b7c9fcd60d 100644
--- a/web_src/less/markup/codecopy.less
+++ b/web_src/less/markup/codecopy.less
@@ -17,10 +17,11 @@
   padding: 9px;
 }
 
+/* can not use regular transparent button colors for hover and active states because
+   we need opaque colors here as code can appear behind the button */
 .markup .code-copy:hover {
   background: var(--color-secondary) !important;
 }
-
 .markup .code-copy:active {
   background: var(--color-secondary-dark-1) !important;
 }

From a169d3680e5a82ab2988c0656dc72ccf7bb6b96b Mon Sep 17 00:00:00 2001
From: silverwind 
Date: Sun, 14 Nov 2021 02:26:09 +0100
Subject: [PATCH 07/20] fix undefined on view file line copy

---
 web_src/js/features/clipboard.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/web_src/js/features/clipboard.js b/web_src/js/features/clipboard.js
index fee12fa7bb954..82e6521b6958d 100644
--- a/web_src/js/features/clipboard.js
+++ b/web_src/js/features/clipboard.js
@@ -3,14 +3,14 @@
 function onSuccess(btn) {
   $(btn).popup('destroy');
   const oldContent = btn.dataset.content;
-  btn.dataset.content = btn.dataset.success;
+  btn.dataset.content = btn.dataset.success || '';
   $(btn).popup('show');
   btn.dataset.content = oldContent || '';
 }
 function onError(btn) {
   const oldContent = btn.dataset.content;
   $(btn).popup('destroy');
-  btn.dataset.content = btn.dataset.error;
+  btn.dataset.content = btn.dataset.error || '';
   $(btn).popup('show');
   btn.dataset.content = oldContent || '';
 }

From 028e533a22b5e8fac4206d551631cb945f4d8de9 Mon Sep 17 00:00:00 2001
From: silverwind 
Date: Sun, 14 Nov 2021 02:43:39 +0100
Subject: [PATCH 08/20] combine animation less files

---
 web_src/less/animations.less          | 35 +++++++++++++++++++++++++++
 web_src/less/features/animations.less | 34 --------------------------
 web_src/less/index.less               |  1 -
 3 files changed, 35 insertions(+), 35 deletions(-)
 delete mode 100644 web_src/less/features/animations.less

diff --git a/web_src/less/animations.less b/web_src/less/animations.less
index 17e8c20084dbe..cdb10236fbcc3 100644
--- a/web_src/less/animations.less
+++ b/web_src/less/animations.less
@@ -1,3 +1,38 @@
+@keyframes isloadingspin {
+  0% { transform: translate(-50%, -50%) rotate(0deg); }
+  100% { transform: translate(-50%, -50%) rotate(360deg); }
+}
+
+.is-loading {
+  background: transparent !important;
+  color: transparent !important;
+  border: transparent !important;
+  pointer-events: none !important;
+  position: relative !important;
+  overflow: hidden !important;
+}
+
+.is-loading::after {
+  content: "";
+  position: absolute;
+  display: block;
+  width: 4rem;
+  height: 4rem;
+  left: 50%;
+  top: 50%;
+  transform: translate(-50%, -50%);
+  animation: isloadingspin 500ms infinite linear;
+  border-width: 4px;
+  border-style: solid;
+  border-color: #ececec #ececec #666 #666;
+  border-radius: 100%;
+}
+
+.markup pre.is-loading,
+.editor-loading.is-loading {
+  height: 12rem;
+}
+
 @keyframes fadein {
   0% {
     opacity: 0;
diff --git a/web_src/less/features/animations.less b/web_src/less/features/animations.less
deleted file mode 100644
index f3491155cd9b2..0000000000000
--- a/web_src/less/features/animations.less
+++ /dev/null
@@ -1,34 +0,0 @@
-@keyframes isloadingspin {
-  0% { transform: translate(-50%, -50%) rotate(0deg); }
-  100% { transform: translate(-50%, -50%) rotate(360deg); }
-}
-
-.is-loading {
-  background: transparent !important;
-  color: transparent !important;
-  border: transparent !important;
-  pointer-events: none !important;
-  position: relative !important;
-  overflow: hidden !important;
-}
-
-.is-loading::after {
-  content: "";
-  position: absolute;
-  display: block;
-  width: 4rem;
-  height: 4rem;
-  left: 50%;
-  top: 50%;
-  transform: translate(-50%, -50%);
-  animation: isloadingspin 500ms infinite linear;
-  border-width: 4px;
-  border-style: solid;
-  border-color: #ececec #ececec #666 #666;
-  border-radius: 100%;
-}
-
-.markup pre.is-loading,
-.editor-loading.is-loading {
-  height: 12rem;
-}
diff --git a/web_src/less/index.less b/web_src/less/index.less
index 128ba59ba1d39..0aa4a2f8f8704 100644
--- a/web_src/less/index.less
+++ b/web_src/less/index.less
@@ -3,7 +3,6 @@
 @import "./variables.less";
 @import "./animations.less";
 @import "./shared/issuelist.less";
-@import "./features/animations.less";
 @import "./features/dropzone.less";
 @import "./features/gitgraph.less";
 @import "./features/heatmap.less";

From b5323e5e8eb00a7de9a6245e238d7b0bbcbe5475 Mon Sep 17 00:00:00 2001
From: silverwind 
Date: Sun, 14 Nov 2021 03:28:30 +0100
Subject: [PATCH 09/20] Update modules/markup/markdown/markdown.go

Co-authored-by: wxiaoguang 
---
 modules/markup/markdown/markdown.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go
index 594e2473e9977..2574585573f49 100644
--- a/modules/markup/markdown/markdown.go
+++ b/modules/markup/markdown/markdown.go
@@ -118,8 +118,8 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer)
 							}
 
 							// include language-x class as part of commonmark spec
-							_, err2 := w.WriteString(``)
-							if err2 != nil {
+							_, err = w.WriteString(``)
+							if err != nil {
 								return
 							}
 						} else {

From 139d7d29e2863626803ebc0c115786e45ef2d45e Mon Sep 17 00:00:00 2001
From: silverwind 
Date: Sun, 14 Nov 2021 03:37:11 +0100
Subject: [PATCH 10/20] add test for different sizes

---
 web_src/js/svg.test.js | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/web_src/js/svg.test.js b/web_src/js/svg.test.js
index e5eda64106408..d3c708e46e02b 100644
--- a/web_src/js/svg.test.js
+++ b/web_src/js/svg.test.js
@@ -6,4 +6,6 @@ test('svg', () => {
 
 test('svgNode', () => {
   expect(svgNode('octicon-repo')).toBeInstanceOf(Element);
+  expect(svgNode('octicon-repo', 16).getAttribute('width')).toEqual('16');
+  expect(svgNode('octicon-repo', 32).getAttribute('width')).toEqual('32');
 });

From 0b7374939bb0ac037d71e40ed5b261dcbe16c87b Mon Sep 17 00:00:00 2001
From: silverwind 
Date: Sun, 14 Nov 2021 03:42:36 +0100
Subject: [PATCH 11/20] add cloneNode and add tests for it

---
 web_src/js/svg.js      | 2 +-
 web_src/js/svg.test.js | 9 +++++++--
 2 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/web_src/js/svg.js b/web_src/js/svg.js
index 976abaae7f7a2..bf4c41f654afb 100644
--- a/web_src/js/svg.js
+++ b/web_src/js/svg.js
@@ -43,7 +43,7 @@ const serializer = new XMLSerializer();
 const parsedSvgs = new Map();
 
 function getParsedSvg(name) {
-  if (parsedSvgs.has(name)) return parsedSvgs.get(name);
+  if (parsedSvgs.has(name)) return parsedSvgs.get(name).cloneNode();
   const root = parser.parseFromString(svgs[name], 'text/html');
   const svgNode = root.querySelector('svg');
   parsedSvgs.set(name, svgNode);
diff --git a/web_src/js/svg.test.js b/web_src/js/svg.test.js
index d3c708e46e02b..46d5863f88b15 100644
--- a/web_src/js/svg.test.js
+++ b/web_src/js/svg.test.js
@@ -6,6 +6,11 @@ test('svg', () => {
 
 test('svgNode', () => {
   expect(svgNode('octicon-repo')).toBeInstanceOf(Element);
-  expect(svgNode('octicon-repo', 16).getAttribute('width')).toEqual('16');
-  expect(svgNode('octicon-repo', 32).getAttribute('width')).toEqual('32');
+
+  const node1 = svgNode('octicon-repo', 16);
+  expect(node1.getAttribute('width')).toEqual('16');
+  const node2 = svgNode('octicon-repo', 32);
+  expect(node1.getAttribute('width')).toEqual('16');
+  expect(node2.getAttribute('width')).toEqual('32');
+  expect(node1).not.toEqual(node2);
 });

From 9e854e1f091cfa5e5a9e64d993e888259cedfd33 Mon Sep 17 00:00:00 2001
From: silverwind 
Date: Sun, 14 Nov 2021 03:48:16 +0100
Subject: [PATCH 12/20] use deep clone

---
 web_src/js/svg.js      | 2 +-
 web_src/js/svg.test.js | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/web_src/js/svg.js b/web_src/js/svg.js
index bf4c41f654afb..097c2aee9d10a 100644
--- a/web_src/js/svg.js
+++ b/web_src/js/svg.js
@@ -43,7 +43,7 @@ const serializer = new XMLSerializer();
 const parsedSvgs = new Map();
 
 function getParsedSvg(name) {
-  if (parsedSvgs.has(name)) return parsedSvgs.get(name).cloneNode();
+  if (parsedSvgs.has(name)) return parsedSvgs.get(name).cloneNode(true);
   const root = parser.parseFromString(svgs[name], 'text/html');
   const svgNode = root.querySelector('svg');
   parsedSvgs.set(name, svgNode);
diff --git a/web_src/js/svg.test.js b/web_src/js/svg.test.js
index 46d5863f88b15..31ab9893caaf9 100644
--- a/web_src/js/svg.test.js
+++ b/web_src/js/svg.test.js
@@ -13,4 +13,6 @@ test('svgNode', () => {
   expect(node1.getAttribute('width')).toEqual('16');
   expect(node2.getAttribute('width')).toEqual('32');
   expect(node1).not.toEqual(node2);
+  expect(node1.childNodes.length).toBeGreaterThan(0);
+  expect(node2.childNodes.length).toBeGreaterThan(0);
 });

From 970285e48be7fb3b0f61bbe78683b372456c9b36 Mon Sep 17 00:00:00 2001
From: silverwind 
Date: Sun, 14 Nov 2021 03:50:00 +0100
Subject: [PATCH 13/20] remove useless optional chaining

---
 web_src/js/markup/codecopy.js | 2 +-
 web_src/js/markup/mermaid.js  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/web_src/js/markup/codecopy.js b/web_src/js/markup/codecopy.js
index 2baaeeaad1084..8df0861129798 100644
--- a/web_src/js/markup/codecopy.js
+++ b/web_src/js/markup/codecopy.js
@@ -3,7 +3,7 @@ const {copied, copy_link_error} = window.i18n;
 
 export function renderCodeCopy() {
   const els = document.querySelectorAll('.markup .code-block code');
-  if (!els?.length) return;
+  if (!els.length) return;
 
   const button = document.createElement('button');
   button.classList.add('code-copy', 'ui', 'button');
diff --git a/web_src/js/markup/mermaid.js b/web_src/js/markup/mermaid.js
index 1d9cc82b59839..7c7ee26c3c503 100644
--- a/web_src/js/markup/mermaid.js
+++ b/web_src/js/markup/mermaid.js
@@ -10,7 +10,7 @@ function displayError(el, err) {
 
 export async function renderMermaid() {
   const els = document.querySelectorAll('.markup code.language-mermaid');
-  if (!els?.length) return;
+  if (!els.length) return;
 
   const {default: mermaid} = await import(/* webpackChunkName: "mermaid" */'mermaid');
 

From 5ae4fc5c9466f19a0bdff44475aac9af82a61d7e Mon Sep 17 00:00:00 2001
From: silverwind 
Date: Sun, 14 Nov 2021 03:54:45 +0100
Subject: [PATCH 14/20] remove the svg node cache

---
 web_src/js/svg.js | 18 +++---------------
 1 file changed, 3 insertions(+), 15 deletions(-)

diff --git a/web_src/js/svg.js b/web_src/js/svg.js
index 097c2aee9d10a..8965f86a1da7e 100644
--- a/web_src/js/svg.js
+++ b/web_src/js/svg.js
@@ -40,28 +40,16 @@ export const svgs = {
 
 const parser = new DOMParser();
 const serializer = new XMLSerializer();
-const parsedSvgs = new Map();
 
-function getParsedSvg(name) {
-  if (parsedSvgs.has(name)) return parsedSvgs.get(name).cloneNode(true);
-  const root = parser.parseFromString(svgs[name], 'text/html');
-  const svgNode = root.querySelector('svg');
-  parsedSvgs.set(name, svgNode);
-  return svgNode;
-}
-
-function applyAttributes(node, size, className) {
+// returns a SVG node for given SVG icon name, size and additional classes
+export function svgNode(name, size = 16, className = '') {
+  const node = parser.parseFromString(svgs[name], 'text/html').querySelector('svg');
   if (size !== 16) node.setAttribute('width', String(size));
   if (size !== 16) node.setAttribute('height', String(size));
   if (className) node.classList.add(...className.split(/\s+/));
   return node;
 }
 
-// returns a SVG node for given SVG icon name, size and additional classes
-export function svgNode(name, size = 16, className = '') {
-  return applyAttributes(getParsedSvg(name), size, className);
-}
-
 // returns a HTML string for given SVG icon name, size and additional classes
 export function svg(name, size, className) {
   if (!(name in svgs)) return '';

From b136b5105704d94cd8d37f7d493b708bd5effec9 Mon Sep 17 00:00:00 2001
From: silverwind 
Date: Sun, 14 Nov 2021 19:29:31 +0100
Subject: [PATCH 15/20] unify clipboard copy string and i18n

---
 options/locale/locale_en-US.ini      | 14 ++++++-------
 templates/base/head.tmpl             | 10 ++++-----
 templates/repo/clone_buttons.tmpl    |  2 +-
 templates/repo/issue/view_title.tmpl |  2 +-
 web_src/js/features/clipboard.js     | 31 ++++++++++++++++------------
 web_src/js/features/common-global.js |  2 +-
 web_src/js/markup/codecopy.js        |  7 ++-----
 web_src/js/svg.js                    | 21 ++++++++-----------
 web_src/js/svg.test.js               | 17 +++------------
 9 files changed, 47 insertions(+), 59 deletions(-)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index fa227074dfa2a..d29cccb96f817 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -85,6 +85,13 @@ remove = Remove
 remove_all = Remove All
 edit = Edit
 
+copy = Copy
+copy_link = Copy link
+copy_url = Copy URL
+copy_branch = Copy branch name
+copy_success = Copied!
+copy_error = Copy failed
+
 write = Write
 preview = Preview
 loading = Loading…
@@ -926,13 +933,6 @@ fork_from_self = You cannot fork a repository you own.
 fork_guest_user = Sign in to fork this repository.
 watch_guest_user = Sign in to watch this repository.
 star_guest_user = Sign in to star this repository.
-copy_link = Copy
-copy_link_success = Link has been copied
-copy_link_error = Use ⌘C or Ctrl-C to copy
-copy_branch = Copy
-copy_branch_success = Branch name has been copied
-copy_branch_error = Use ⌘C or Ctrl-C to copy
-copied = Copied
 unwatch = Unwatch
 watch = Watch
 unstar = Unstar
diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl
index 5ad49550d4328..8b1f228e660d9 100644
--- a/templates/base/head.tmpl
+++ b/templates/base/head.tmpl
@@ -46,11 +46,11 @@
 			]).values()),
 			{{end}}
 			mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}},
-		};
-
-		window.i18n = {
-			copied: '{{.i18n.Tr "repo.copied"}}',
-			copy_link_error: '{{.i18n.Tr "repo.copy_link_error"}}',
+			i18n: {
+				copy: '{{.i18n.Tr "copy"}}',
+				copy_success: '{{.i18n.Tr "copy_success"}}',
+				copy_error: '{{.i18n.Tr "copy_error"}}',
+			}
 		};
 	
 	
diff --git a/templates/repo/clone_buttons.tmpl b/templates/repo/clone_buttons.tmpl
index 0a86e586fc9f3..37a88af945fc5 100644
--- a/templates/repo/clone_buttons.tmpl
+++ b/templates/repo/clone_buttons.tmpl
@@ -14,7 +14,7 @@
 	
 {{end}}
 {{if or (not $.DisableHTTP) (and (not $.DisableSSH) (or $.IsSigned $.ExposeAnonSSH))}}
-	
 {{end}}
diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl
index 798ab7638ccfa..a21e58068c792 100644
--- a/templates/repo/issue/view_title.tmpl
+++ b/templates/repo/issue/view_title.tmpl
@@ -34,7 +34,7 @@
 		{{if .HeadBranchHTMLURL}}
 			{{$headHref = printf "%s" (.HeadBranchHTMLURL | Escape) $headHref}}
 		{{end}}
-		{{$headHref = printf "%s %s" $headHref (.i18n.Tr "repo.copy_branch") (.i18n.Tr "repo.copy_branch_success") (.i18n.Tr "repo.copy_branch_error") (.HeadTarget | Escape) (svg "octicon-copy" 14)}}
+		{{$headHref = printf "%s %s" $headHref (.i18n.Tr "copy_branch") (.HeadTarget | Escape) (svg "octicon-copy" 14)}}
 		{{$baseHref := .BaseTarget|Escape}}
 		{{if .BaseBranchHTMLURL}}
 			{{$baseHref = printf "%s" (.BaseBranchHTMLURL | Escape) $baseHref}}
diff --git a/web_src/js/features/clipboard.js b/web_src/js/features/clipboard.js
index 82e6521b6958d..b0c4134537fc3 100644
--- a/web_src/js/features/clipboard.js
+++ b/web_src/js/features/clipboard.js
@@ -1,24 +1,25 @@
-// For all DOM elements with [data-clipboard-target] or [data-clipboard-text], this copy-to-clipboard will work for them
+const {copy_success, copy_error} = window.config.i18n;
 
 function onSuccess(btn) {
+  btn.setAttribute('data-variation', 'inverted tiny');
   $(btn).popup('destroy');
-  const oldContent = btn.dataset.content;
-  btn.dataset.content = btn.dataset.success || '';
+  const oldContent = btn.getAttribute('data-content');
+  btn.setAttribute('data-content', copy_success);
   $(btn).popup('show');
-  btn.dataset.content = oldContent || '';
+  btn.setAttribute('data-content', oldContent || '');
 }
 function onError(btn) {
-  const oldContent = btn.dataset.content;
+  btn.setAttribute('data-variation', 'inverted tiny');
+  const oldContent = btn.getAttribute('data-content');
   $(btn).popup('destroy');
-  btn.dataset.content = btn.dataset.error || '';
+  btn.setAttribute('data-content', copy_error);
   $(btn).popup('show');
-  btn.dataset.content = oldContent || '';
+  btn.setAttribute('data-content', oldContent || '');
 }
 
-/**
- * Fallback to use if navigator.clipboard doesn't exist.
- * Achieved via creating a temporary textarea element, selecting the text, and using document.execCommand.
- */
+
+// Fallback to use if navigator.clipboard doesn't exist. Achieved via creating
+// a temporary textarea element, selecting the text, and using document.execCommand
 function fallbackCopyToClipboard(text) {
   if (!document.execCommand) return false;
 
@@ -34,7 +35,8 @@ function fallbackCopyToClipboard(text) {
 
   tempTextArea.select();
 
-  // if unsecure (not https), there is no navigator.clipboard, but we can still use document.execCommand to copy to clipboard
+  // if unsecure (not https), there is no navigator.clipboard, but we can still
+  // use document.execCommand to copy to clipboard
   const success = document.execCommand('copy');
 
   document.body.removeChild(tempTextArea);
@@ -42,10 +44,13 @@ function fallbackCopyToClipboard(text) {
   return success;
 }
 
+// For all DOM elements with [data-clipboard-target] or [data-clipboard-text],
+// this copy-to-clipboard will work for them
 export default function initGlobalCopyToClipboardListener() {
   document.addEventListener('click', (e) => {
     let target = e.target;
-    // in case , so we just search up to 3 levels for performance.
+    // in case , so we just search
+    // up to 3 levels for performance
     for (let i = 0; i < 3 && target; i++) {
       let text;
       if (target.dataset.clipboardText) {
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index da3fb9d1e3836..ac9d0cc92df47 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -104,7 +104,7 @@ export function initGlobalCommon() {
   $('.ui.progress').progress({
     showActivity: false
   });
-  $('.poping.up').popup();
+  $('.poping.up').attr('data-variation', 'inverted tiny').popup();
   $('.top.menu .poping.up').popup({
     onShow() {
       if ($('.top.menu .menu.transition').hasClass('visible')) {
diff --git a/web_src/js/markup/codecopy.js b/web_src/js/markup/codecopy.js
index 8df0861129798..c462f45d62850 100644
--- a/web_src/js/markup/codecopy.js
+++ b/web_src/js/markup/codecopy.js
@@ -1,5 +1,4 @@
-import {svgNode} from '../svg.js';
-const {copied, copy_link_error} = window.i18n;
+import {svg} from '../svg.js';
 
 export function renderCodeCopy() {
   const els = document.querySelectorAll('.markup .code-block code');
@@ -7,10 +6,8 @@ export function renderCodeCopy() {
 
   const button = document.createElement('button');
   button.classList.add('code-copy', 'ui', 'button');
-  button.setAttribute('data-success', copied);
-  button.setAttribute('data-error', copy_link_error);
   button.setAttribute('data-variation', 'inverted tiny');
-  button.appendChild(svgNode('octicon-copy'));
+  button.innerHTML = svg('octicon-copy');
 
   for (const el of els) {
     const btn = button.cloneNode(true);
diff --git a/web_src/js/svg.js b/web_src/js/svg.js
index 8965f86a1da7e..77aa1e7ca79fa 100644
--- a/web_src/js/svg.js
+++ b/web_src/js/svg.js
@@ -41,20 +41,17 @@ export const svgs = {
 const parser = new DOMParser();
 const serializer = new XMLSerializer();
 
-// returns a SVG node for given SVG icon name, size and additional classes
-export function svgNode(name, size = 16, className = '') {
-  const node = parser.parseFromString(svgs[name], 'text/html').querySelector('svg');
-  if (size !== 16) node.setAttribute('width', String(size));
-  if (size !== 16) node.setAttribute('height', String(size));
-  if (className) node.classList.add(...className.split(/\s+/));
-  return node;
-}
-
-// returns a HTML string for given SVG icon name, size and additional classes
-export function svg(name, size, className) {
+// retrieve a HTML string for given SVG icon name, size and additional classes
+export function svg(name, size = 16, className = '') {
   if (!(name in svgs)) return '';
   if (size === 16 && !className) return svgs[name];
-  return serializer.serializeToString(svgNode(name, size, className));
+
+  const document = parser.parseFromString(svgs[name], 'image/svg+xml');
+  const svgNode = document.firstChild;
+  if (size !== 16) svgNode.setAttribute('width', String(size));
+  if (size !== 16) svgNode.setAttribute('height', String(size));
+  if (className) svgNode.classList.add(...className.split(/\s+/));
+  return serializer.serializeToString(svgNode);
 }
 
 export const SvgIcon = Vue.component('SvgIcon', {
diff --git a/web_src/js/svg.test.js b/web_src/js/svg.test.js
index 31ab9893caaf9..f1939c3a46a54 100644
--- a/web_src/js/svg.test.js
+++ b/web_src/js/svg.test.js
@@ -1,18 +1,7 @@
-import {svg, svgNode} from './svg.js';
+import {svg} from './svg.js';
 
 test('svg', () => {
   expect(svg('octicon-repo')).toStartWith(' {
-  expect(svgNode('octicon-repo')).toBeInstanceOf(Element);
-
-  const node1 = svgNode('octicon-repo', 16);
-  expect(node1.getAttribute('width')).toEqual('16');
-  const node2 = svgNode('octicon-repo', 32);
-  expect(node1.getAttribute('width')).toEqual('16');
-  expect(node2.getAttribute('width')).toEqual('32');
-  expect(node1).not.toEqual(node2);
-  expect(node1.childNodes.length).toBeGreaterThan(0);
-  expect(node2.childNodes.length).toBeGreaterThan(0);
+  expect(svg('octicon-repo', 16)).toInclude('width="16"');
+  expect(svg('octicon-repo', 32)).toInclude('width="32"');
 });

From 01fe5052fd1d0ba4ef12593c8addb4e1eabced31 Mon Sep 17 00:00:00 2001
From: silverwind 
Date: Sun, 14 Nov 2021 19:32:43 +0100
Subject: [PATCH 16/20] remove unused var

---
 templates/base/head.tmpl | 1 -
 1 file changed, 1 deletion(-)

diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl
index 8b1f228e660d9..bf1fcd24bcbfa 100644
--- a/templates/base/head.tmpl
+++ b/templates/base/head.tmpl
@@ -47,7 +47,6 @@
 			{{end}}
 			mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}},
 			i18n: {
-				copy: '{{.i18n.Tr "copy"}}',
 				copy_success: '{{.i18n.Tr "copy_success"}}',
 				copy_error: '{{.i18n.Tr "copy_error"}}',
 			}

From 610d0e40c83626aee657048a22f4451a9c9ceb26 Mon Sep 17 00:00:00 2001
From: silverwind 
Date: Sun, 14 Nov 2021 21:31:49 +0100
Subject: [PATCH 17/20] remove unused localization

---
 options/locale/locale_en-US.ini | 1 -
 1 file changed, 1 deletion(-)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index d29cccb96f817..5784751fa46ed 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -86,7 +86,6 @@ remove_all = Remove All
 edit = Edit
 
 copy = Copy
-copy_link = Copy link
 copy_url = Copy URL
 copy_branch = Copy branch name
 copy_success = Copied!

From 1b8302293f8abe8019d9558f7373d33c679fd45e Mon Sep 17 00:00:00 2001
From: silverwind 
Date: Mon, 15 Nov 2021 21:19:56 +0100
Subject: [PATCH 18/20] minor css tweaks to the button

---
 web_src/less/markup/codecopy.less | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/web_src/less/markup/codecopy.less b/web_src/less/markup/codecopy.less
index 605b7c9fcd60d..33f4bd629a0d7 100644
--- a/web_src/less/markup/codecopy.less
+++ b/web_src/less/markup/codecopy.less
@@ -4,9 +4,9 @@
 
 .markup .code-copy {
   position: absolute;
-  top: .5rem;
-  right: .5rem;
-  padding: 10px;
+  top: 8px;
+  right: 6px;
+  padding: 9px;
   visibility: hidden;
   animation: fadeout .2s both;
 }
@@ -14,7 +14,8 @@
 /* comment content has 14px font size, reduce padding to make the button appear
    vertically centered on single-line content, like it does elsewhere */
 .repository.view.issue .comment-list .comment .markup .code-copy {
-  padding: 9px;
+  padding: 8px;
+  right: 5px;
 }
 
 /* can not use regular transparent button colors for hover and active states because

From b51cc9d0369561f23b1870b5335ffe5968d49ce3 Mon Sep 17 00:00:00 2001
From: silverwind 
Date: Mon, 15 Nov 2021 21:23:36 +0100
Subject: [PATCH 19/20] comment tweak

---
 web_src/less/markup/codecopy.less | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/web_src/less/markup/codecopy.less b/web_src/less/markup/codecopy.less
index 33f4bd629a0d7..b2ce77aaa157b 100644
--- a/web_src/less/markup/codecopy.less
+++ b/web_src/less/markup/codecopy.less
@@ -11,11 +11,10 @@
   animation: fadeout .2s both;
 }
 
-/* comment content has 14px font size, reduce padding to make the button appear
-   vertically centered on single-line content, like it does elsewhere */
+/* adjustments for comment content having only 14px font size */
 .repository.view.issue .comment-list .comment .markup .code-copy {
-  padding: 8px;
   right: 5px;
+  padding: 8px;
 }
 
 /* can not use regular transparent button colors for hover and active states because

From f7ce664168e1a37121ba2d08c39ccb926d3c19cc Mon Sep 17 00:00:00 2001
From: silverwind 
Date: Mon, 15 Nov 2021 21:27:00 +0100
Subject: [PATCH 20/20] remove useless attribute

---
 web_src/js/markup/codecopy.js | 1 -
 1 file changed, 1 deletion(-)

diff --git a/web_src/js/markup/codecopy.js b/web_src/js/markup/codecopy.js
index c462f45d62850..2aa7070c72ded 100644
--- a/web_src/js/markup/codecopy.js
+++ b/web_src/js/markup/codecopy.js
@@ -6,7 +6,6 @@ export function renderCodeCopy() {
 
   const button = document.createElement('button');
   button.classList.add('code-copy', 'ui', 'button');
-  button.setAttribute('data-variation', 'inverted tiny');
   button.innerHTML = svg('octicon-copy');
 
   for (const el of els) {