diff --git a/src/client/theme-default/components/VPDoc.vue b/src/client/theme-default/components/VPDoc.vue
index 89c9cd7e..516ce66e 100644
--- a/src/client/theme-default/components/VPDoc.vue
+++ b/src/client/theme-default/components/VPDoc.vue
@@ -1,6 +1,7 @@
diff --git a/src/client/theme-default/composables/copy-code.ts b/src/client/theme-default/composables/copy-code.ts
new file mode 100644
index 00000000..9a95c06d
--- /dev/null
+++ b/src/client/theme-default/composables/copy-code.ts
@@ -0,0 +1,42 @@
+import { nextTick, watch } from 'vue'
+import { inBrowser, useData } from 'vitepress'
+
+const handleElement = (el: HTMLElement) => {
+ el.onclick = () => {
+ const parent = el.parentElement
+ if (!parent) return
+
+ const isShell =
+ parent.classList.contains('language-sh') ||
+ parent.classList.contains('language-bash')
+
+ let { innerText: text = '' } = parent
+ if (isShell) text = text.replace(/^ *\$ /gm, '')
+
+ navigator.clipboard.writeText(text).then(() => {
+ el.classList.add('copied')
+ setTimeout(() => {
+ el.classList.remove('copied')
+ }, 800)
+ })
+ }
+}
+
+export function useCopyCode() {
+ const { page } = useData()
+
+ if (inBrowser)
+ watch(
+ () => page.value.relativePath,
+ () => {
+ nextTick(() => {
+ document
+ .querySelectorAll(
+ '.vp-doc div[class*="language-"]>span.copy'
+ )
+ .forEach(handleElement)
+ })
+ },
+ { immediate: true, flush: 'post' }
+ )
+}
diff --git a/src/client/theme-default/styles/components/vp-doc.css b/src/client/theme-default/styles/components/vp-doc.css
index 6e9e4ef0..ba08d070 100644
--- a/src/client/theme-default/styles/components/vp-doc.css
+++ b/src/client/theme-default/styles/components/vp-doc.css
@@ -366,6 +366,29 @@
transition: border-color 0.5s, color 0.5s;
}
+.vp-doc [class*='language-'] > span.copy:before {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ z-index: 2;
+ content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' height='20' width='20' stroke='rgba(235,235,235,0.38)' stroke-width='2' class='h-6 w-6' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2'/%3E%3C/svg%3E");
+ opacity: 0;
+ cursor: pointer;
+ transition: opacity 0.5s;
+}
+
+.vp-doc [class*='language-'] > span.copied:before {
+ content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' height='20' width='20' stroke='rgba(235,235,235,0.38)' stroke-width='2' class='h-6 w-6' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2m-6 9 2 2 4-4'/%3E%3C/svg%3E");
+}
+
+.vp-doc [class*='language-']:hover > span.copy:before {
+ opacity: 1;
+}
+
+.vp-doc [class*='language-']:hover:before {
+ opacity: 0;
+}
+
.vp-doc [class*='language-']:before {
position: absolute;
top: 6px;
@@ -374,7 +397,7 @@
font-size: 12px;
font-weight: 500;
color: var(--vp-c-text-dark-3);
- transition: color 0.5s;
+ transition: color 0.5s, opacity 0.5s;
}
.vp-doc [class~='language-c']:before { content: 'c'; }
diff --git a/src/node/markdown/plugins/preWrapper.ts b/src/node/markdown/plugins/preWrapper.ts
index fcd06f17..9a23b797 100644
--- a/src/node/markdown/plugins/preWrapper.ts
+++ b/src/node/markdown/plugins/preWrapper.ts
@@ -15,6 +15,6 @@ export const preWrapperPlugin = (md: MarkdownIt) => {
const [tokens, idx] = args
const token = tokens[idx]
const rawCode = fence(...args)
- return `${rawCode}
`
+ return `${rawCode}
`
}
}