diff --git a/src/parser/copycode.ts b/src/parser/copycode.ts new file mode 100644 index 0000000..cb611b9 --- /dev/null +++ b/src/parser/copycode.ts @@ -0,0 +1,30 @@ +import MarkdownIt from 'markdown-it'; +import octicons from '@primer/octicons'; + +const copyIcon = octicons['copy'].toSVG({ class: 'icon-copy' }); +const checkIcon = octicons['check'].toSVG({ class: 'icon-check' }); +const xIcon = octicons['x'].toSVG({ class: 'icon-x' }); + +export default function copycode(md: MarkdownIt) { + const defaultRender = md.renderer.rules.fence!; + md.renderer.rules.fence = (tokens, idx, options, env, self) => { + const renderedPreBlock = defaultRender(tokens, idx, options, env, self); + const content = tokens[idx].content; + return ` +
+ ${renderedPreBlock} +
+ +
+ ${checkIcon} +
+
+ ${xIcon} +
+
+
+`; + }; +} diff --git a/src/parser/markdown.ts b/src/parser/markdown.ts index 9a0fe9d..ffdadeb 100644 --- a/src/parser/markdown.ts +++ b/src/parser/markdown.ts @@ -1,5 +1,6 @@ import MarkdownIt from 'markdown-it'; import anchor from 'markdown-it-anchor'; +import copycode from './copycode.js'; import frontMatter from 'markdown-it-front-matter'; import highlight from './highlight.js'; import graphviz from './dot.js'; @@ -68,6 +69,7 @@ mdit.use(anchor, { placement: 'before', }), }); +mdit.use(copycode); mdit.use(graphviz); mdit.use(githubAlerts); mdit.use(mermaid); diff --git a/src/routes/viewer.ts b/src/routes/viewer.ts index b53e80a..ec569c4 100644 --- a/src/routes/viewer.ts +++ b/src/routes/viewer.ts @@ -88,6 +88,8 @@ router.get(/.*/, async (req: Request, res: Response) => { ${config.scripts ? `` : ''} + + `); }); diff --git a/static/colors.css b/static/colors.css index d3bf14c..a101441 100644 --- a/static/colors.css +++ b/static/colors.css @@ -19,6 +19,13 @@ --icon-file: var(--text-secondary); --icon-back: var(--text-secondary); + --copy-button-fg: #aaa; + --copy-button-bg: #333; + --copy-success-fg: #ddffcc; + --copy-success-bg: #5c993d; + --copy-fail-fg: #ffcccc; + --copy-fail-bg: #993d3d; + --syntax-text: var(--text-primary); --syntax-keyword: #aed7ff; --syntax-entity: #aeafff; @@ -69,6 +76,13 @@ --icon-file: #636c76; --icon-back: var(--text-secondary); + --copy-button-fg: #1f2328; + --copy-button-bg: #e7e9eb; + --copy-success-fg: #154000; + --copy-success-bg: #c7e6b8; + --copy-fail-fg: #400000; + --copy-fail-bg: #e6b8b8; + /* source for light mode syntax theme: * https://github.com/highlightjs/highlight.js/blob/main/src/styles/github.css * */ diff --git a/static/copycode/client.js b/static/copycode/client.js new file mode 100644 index 0000000..bb0080f --- /dev/null +++ b/static/copycode/client.js @@ -0,0 +1,26 @@ +document.querySelectorAll('.copy-success, .copy-fail').forEach((element) => { + element.style.display = 'none'; +}); + +const Notify = { + Success: '.copy-success', + Fail: '.copy-fail', +}; + +let clipboard = new ClipboardJS('.copy-button'); + +clipboard.on('success', function (e) { + showNotification(e.trigger, Notify.Success); +}); + +clipboard.on('error', function (e) { + showNotification(e.trigger, Notify.Fail); +}); + +function showNotification(btn, notify) { + const notificationElement = btn.parentElement.querySelector(notify); + notificationElement.style.display = 'flex'; + setTimeout(() => { + notificationElement.style.display = 'none'; + }, 2000); +} diff --git a/static/copycode/clipboard.min.js b/static/copycode/clipboard.min.js new file mode 100644 index 0000000..1103f81 --- /dev/null +++ b/static/copycode/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={686:function(t,e,n){"use strict";n.d(e,{default:function(){return b}});var e=n(279),i=n.n(e),e=n(370),u=n.n(e),e=n(817),r=n.n(e);function c(t){try{return document.execCommand(t)}catch(t){return}}var a=function(t){t=r()(t);return c("cut"),t};function o(t,e){var n,o,t=(n=t,o="rtl"===document.documentElement.getAttribute("dir"),(t=document.createElement("textarea")).style.fontSize="12pt",t.style.border="0",t.style.padding="0",t.style.margin="0",t.style.position="absolute",t.style[o?"right":"left"]="-9999px",o=window.pageYOffset||document.documentElement.scrollTop,t.style.top="".concat(o,"px"),t.setAttribute("readonly",""),t.value=n,t);return e.container.appendChild(t),e=r()(t),c("copy"),t.remove(),e}var f=function(t){var e=1 .copy-button { + opacity: 0.3; +} +.copy-button:hover { + opacity: 1; + background-color: var(--copy-button-bg); +} +.copy-success { + background: var(--copy-success-bg); + fill: var(--copy-success-fg); +} +.copy-fail { + background: var(--copy-fail-bg); + fill: var(--copy-fail-fg); +} + /* -------------------------------------------------------------------------- * MISCELLANEOUS ------------------------------------------------------------ */