Skip to content

Commit 25e6049

Browse files
committed
better copy button
well never known when rehype-pretty/rehype-pretty-code#262 fixes. but we also didnt need it be fixed :)
1 parent 5310cbd commit 25e6049

File tree

3 files changed

+78
-5
lines changed

3 files changed

+78
-5
lines changed

src/locales/zh-CN.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1463,6 +1463,19 @@
14631463
"pageNotFoundText": "糟糕!您似乎来到了一个不存在的页面。",
14641464
"homepageButton": "返回首页"
14651465
},
1466+
"copy_button": {
1467+
"copy": {
1468+
"dataset": {
1469+
"tip": "复制到剪贴板"
1470+
}
1471+
},
1472+
"copied": {
1473+
"dataset": {
1474+
"tip": "已复制到剪贴板!"
1475+
}
1476+
},
1477+
"copy_failed": "无法复制到剪贴板:${error}"
1478+
},
14661479
"pow_captcha": {
14671480
"initial": "我不是机器人",
14681481
"verifying": "正在验证...",

src/pages/scripts/markdown.mjs

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { transformerCopyButton } from 'https://esm.sh/@rehype-pretty/transformers'
21
import { h } from 'https://esm.sh/hastscript'
32
import rehypeKatex from 'https://esm.sh/rehype-katex'
43
import rehypeMermaid from 'https://esm.sh/rehype-mermaid'
@@ -36,6 +35,58 @@ function rehypeAddDaisyuiClass() {
3635
}
3736
}
3837

38+
const ShikiCopyButtonPlugin = {
39+
name: 'copy-button',
40+
root(hast) {
41+
const rawCode = this.tokens.map(line => line.map(token => token.content).join('')).join('\n')
42+
43+
const copyIconSrc = 'https://api.iconify.design/line-md/clipboard.svg'
44+
const successIconSrc = 'https://api.iconify.design/line-md/clipboard-check.svg'
45+
46+
const buttonNode = h('div', {
47+
class: 'absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity duration-200'
48+
}, [
49+
h('div', {
50+
class: 'tooltip tooltip-left',
51+
'data-i18n': 'copy_button.copy',
52+
}, [
53+
h('button', {
54+
class: 'btn btn-ghost btn-square btn-sm',
55+
onclick: `(async () => {
56+
const { getSvgIcon } = await import('/scripts/svgInliner.mjs')
57+
const tooltip = this.parentElement
58+
try {
59+
await navigator.clipboard.writeText(${JSON.stringify(rawCode)})
60+
const successIcon = await getSvgIcon('${successIconSrc}', { class: 'w-5 h-5' })
61+
tooltip.setAttribute('data-i18n', 'copy_button.copied')
62+
const img = this.querySelector('svg')
63+
img.replaceWith(successIcon)
64+
} catch (e) {
65+
const { showToastI18n } = await import('/scripts/toast.mjs')
66+
showToastI18n('error', 'copy_button.copy_failed', { error: e.message })
67+
}
68+
setTimeout(async () => {
69+
tooltip.setAttribute('data-i18n', 'copy_button.copy')
70+
const img = this.querySelector('svg')
71+
img.replaceWith(await getSvgIcon('${copyIconSrc}', { class: 'w-5 h-5' }))
72+
}, 2000)
73+
})()`,
74+
}, [
75+
h('img', {
76+
src: copyIconSrc,
77+
class: 'w-5 h-5'
78+
})
79+
])
80+
])
81+
])
82+
83+
return h('div', { class: 'group', style: 'position: relative;' }, [
84+
hast,
85+
buttonNode
86+
])
87+
}
88+
}
89+
3990
const convertor = unified()
4091
.use(remarkParse)
4192
.use(remarkDisable, { disable: ['codeIndented'] })
@@ -68,10 +119,7 @@ ${diagram}`
68119
light: 'github-light',
69120
},
70121
transformers: [
71-
transformerCopyButton({
72-
visibility: 'always',
73-
feedbackDuration: 3_000,
74-
}),
122+
ShikiCopyButtonPlugin,
75123
],
76124
})
77125
.use(rehypeKatex)

src/pages/scripts/svgInliner.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,15 @@ export async function svgInliner(DOM) {
2020
})).catch(console.error)
2121
return DOM
2222
}
23+
export async function getSvgIcon(url, attributes = {}) {
24+
IconCache[url] ??= fetch(url).then(response => response.text())
25+
let data = IconCache[url] = await IconCache[url]
26+
// 对于每个id="xx"的match,在id后追加uuid
27+
const uuid = Math.random().toString(36).slice(2)
28+
const matches = data.matchAll(/id="([^"]+)"/g)
29+
for (const match of matches) data = data.replaceAll(match[1], `${match[1]}-${uuid}`)
30+
const newSvg = createDocumentFragmentFromHtmlString(data)
31+
for (const attr in attributes)
32+
newSvg.querySelector('svg').setAttribute(attr, attributes[attr])
33+
return newSvg.querySelector('svg')
34+
}

0 commit comments

Comments
 (0)