├── .babelrc ├── .npmignore ├── .gitignore ├── renovate.json ├── package.json ├── src ├── gatsby-browser.js ├── styles.scss └── index.js └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | node_modules/ 3 | logs 4 | *.log 5 | package-lock.json 6 | yarn.lock 7 | .sass-cache/ 8 | *.css.map 9 | *.sass.map 10 | *.scss.map 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.css 2 | *.js 3 | !src/* 4 | node_modules/ 5 | logs 6 | *.log 7 | package-lock.json 8 | yarn.lock 9 | .sass-cache/ 10 | *.css.map 11 | *.sass.map 12 | *.scss.map 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base", 5 | ":pinDevDependencies", 6 | ":pinDependencies", 7 | ":dependencyDashboard" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-remark-code-buttons", 3 | "version": "2.0.7", 4 | "description": "Add copy buttons to code snippets", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "npm run sass && npm run babel", 8 | "sass": "node_modules/node-sass/bin/node-sass src/styles.scss ./styles.css", 9 | "babel": "node_modules/@babel/cli/bin/babel.js src --out-dir .", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/iamskok/gatsby-remark-code-buttons.git" 15 | }, 16 | "keywords": [ 17 | "gatsby", 18 | "gatsby-plugin", 19 | "gatsby-remark-plugin", 20 | "gatsby-transformer-remark", 21 | "buttons", 22 | "markdown" 23 | ], 24 | "author": "Vladimir Skok (https://github.com/iamskok)", 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/iamskok/gatsby-remark-code-buttons/issues" 28 | }, 29 | "homepage": "https://github.com/iamskok/gatsby-remark-code-buttons#readme", 30 | "devDependencies": { 31 | "@babel/cli": "7.14.5", 32 | "@babel/core": "7.14.6", 33 | "@babel/preset-env": "7.14.7", 34 | "node-sass": "5.0.0" 35 | }, 36 | "dependencies": { 37 | "query-string": "6.14.1", 38 | "unist-util-visit": "2.0.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/gatsby-browser.js: -------------------------------------------------------------------------------- 1 | require('./styles.css'); 2 | 3 | exports.onClientEntry = () => { 4 | window.copyToClipboard = (str, toasterId) => { 5 | const el = document.createElement('textarea'); 6 | el.className = 'gatsby-code-button-buffer'; 7 | el.innerHTML = str; 8 | document.body.appendChild(el); 9 | 10 | const range = document.createRange(); 11 | range.selectNode(el); 12 | window.getSelection().removeAllRanges(); 13 | window.getSelection().addRange(range); 14 | 15 | document.execCommand(`copy`); 16 | document.activeElement.blur(); 17 | 18 | setTimeout(() => { 19 | document.getSelection().removeAllRanges(); 20 | document.body.removeChild(el); 21 | }, 100); 22 | 23 | if (toasterId) { 24 | window.showClipboardToaster(toasterId); 25 | } 26 | } 27 | 28 | window.showClipboardToaster = toasterId => { 29 | const textElem = document.querySelector(`[data-toaster-id="${toasterId}"]`); 30 | 31 | if (!textElem) { 32 | return; 33 | } 34 | 35 | const el = document.createElement('div'); 36 | el.className = textElem.dataset.toasterClass; 37 | el.innerHTML = ` 38 |
39 | ${textElem.dataset.toasterText} 40 |
41 | `.trim(); 42 | 43 | document.body.appendChild(el); 44 | 45 | setTimeout(() => { 46 | document.body.removeChild(el); 47 | }, textElem.dataset.toasterDuration); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | .gatsby-code-button-container { 2 | touch-action: none; 3 | display: flex; 4 | justify-content: flex-end; 5 | position: relative; 6 | top: 28px; 7 | z-index: 100; 8 | pointer-events: none; 9 | } 10 | 11 | .gatsby-code-button { 12 | cursor: pointer; 13 | pointer-events: initial; 14 | 15 | &:after { 16 | visibility: hidden; 17 | position: absolute; 18 | display: none; 19 | padding: 5px; 20 | content: attr(data-tooltip); 21 | color: #fff; 22 | font-size: 16px; 23 | background-color: #000; 24 | white-space: nowrap; 25 | } 26 | 27 | &[data-tooltip] { 28 | &:after { 29 | top: 26px; 30 | right: 0; 31 | } 32 | 33 | &:hover, 34 | &:focus { 35 | &:after { 36 | visibility: visible; 37 | display: block; 38 | z-index: 200; 39 | } 40 | } 41 | } 42 | } 43 | 44 | .gatsby-code-button-toaster { 45 | z-index: 500; 46 | position: fixed; 47 | top: 0; 48 | bottom: 0; 49 | left: 0; 50 | right: 0; 51 | display: flex; 52 | justify-content: center; 53 | align-items: center; 54 | height: 100vh; 55 | width: 100vw; 56 | pointer-events: none; 57 | } 58 | 59 | .gatsby-code-button-toaster-text { 60 | width: 100%; 61 | margin: 0 15px; 62 | max-width: 850px; 63 | padding: 20px; 64 | font-size: 24px; 65 | letter-spacing: -1px; 66 | font-family: monospace; 67 | color: #fff; 68 | background-color: #000; 69 | border-radius: 2px; 70 | text-align: center; 71 | opacity: 0; 72 | animation: animation 3s cubic-bezier(0.98, 0.01, 0.53, 0.47); 73 | } 74 | 75 | .gatsby-code-button-buffer { 76 | position: fixed; 77 | top: -9999px; 78 | opacity: 0; 79 | } 80 | 81 | @keyframes animation { 82 | 0%, 83 | 50% { 84 | opacity: 1; 85 | } 86 | 87 | 50%, 88 | 100% { 89 | opacity: 0; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const visit = require('unist-util-visit'); 2 | const qs = require('query-string'); 3 | 4 | module.exports = function gatsbyRemarkCodeButtons( 5 | { markdownAST }, 6 | { 7 | buttonClass: customButtonClass, 8 | buttonContainerClass: customButtonContainerClass, 9 | buttonText: customButtonText, 10 | svgIconClass: customSvgIconClass, 11 | svgIcon: customSvgIcon, 12 | tooltipText: customTooltipText, 13 | toasterClass: customToasterClass, 14 | toasterTextClass: customToasterTextClass, 15 | toasterText: customToasterText, 16 | toasterDuration: customToasterDuration 17 | } 18 | ) { 19 | visit(markdownAST, 'code', (node, index, parent) => { 20 | const [language, params] = (node.lang || '').split(':'); 21 | const actions = qs.parse(params); 22 | const { clipboard } = actions; 23 | 24 | if (!language) { 25 | return; 26 | } 27 | 28 | if (clipboard === 'false') { 29 | delete actions['clipboard']; 30 | } else { 31 | const buttonClass = ['gatsby-code-button'].concat(customButtonClass || '').join(' ').trim(); 32 | const buttonContainerClass = ['gatsby-code-button-container'].concat(customButtonContainerClass || '').join(' ').trim(); 33 | const buttonText = customButtonText || ''; 34 | const svgIconClass = ['gatsby-code-button-icon'].concat(customSvgIconClass || '').join(' ').trim(); 35 | const svgIcon = customSvgIcon || ``; 36 | const tooltipText = customTooltipText || ''; 37 | const toasterClass = ['gatsby-code-button-toaster'].concat(customToasterClass || '').join(' ').trim(); 38 | const toasterTextClass = ['gatsby-code-button-toaster-text'].concat(customToasterTextClass || '').join(' ').trim(); 39 | const toasterText = (customToasterText ? customToasterText : '').trim(); 40 | const toasterDuration = (customToasterDuration ? customToasterDuration : 3500); 41 | const toasterId = (toasterText ? Math.random() * 100 ** 10 : ''); 42 | 43 | let code = parent.children[index].value; 44 | code = code.replace(/"/gm, '"').replace(/`/gm, '\\`').replace(/\$/gm, '\\$'); 45 | 46 | const buttonNode = { 47 | type: 'html', 48 | value: ` 49 |
58 |
62 | ${buttonText}${svgIcon} 63 |
64 |
65 | `.trim() 66 | }; 67 | 68 | parent.children.splice(index, 0, buttonNode); 69 | actions['clipboard'] = 'false'; 70 | } 71 | 72 | let newQuery = ''; 73 | if (Object.keys(actions).length) { 74 | newQuery = `:` + Object.keys(actions).map(key => `${key}=${actions[key]}`).join('&'); 75 | } 76 | 77 | node.lang = language + newQuery; 78 | }); 79 | }; 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gatsby-remark-code-buttons 2 | 3 | Add buttons to **markdown** code snippets. 4 | 5 | > This plugin doesn't support MDX. Example 6 | of [MDX copy button](https://github.com/gatsbyjs/gatsby/pull/15834). 7 | ## Install 8 | 9 | ```bash 10 | npm install gatsby-remark-code-buttons --save-dev 11 | ``` 12 | 13 | ![](https://media.giphy.com/media/hoHRea4IdkDBrsE4Bu/source.gif) 14 | 15 | ## How to use 16 | 17 | in your `gatsby-config.js` 18 | 19 | ```js 20 | plugins: [ 21 | { 22 | resolve: 'gatsby-transformer-remark', 23 | options: { 24 | plugins: ['gatsby-remark-code-buttons'] 25 | } 26 | } 27 | ] 28 | ``` 29 | 30 | ## Options 31 | 32 | ```js 33 | plugins: [ 34 | { 35 | resolve: 'gatsby-transformer-remark', 36 | options: { 37 | plugins: [ 38 | { 39 | resolve: 'gatsby-remark-code-buttons', 40 | options: { 41 | // Optional button container class name. Defaults 42 | // to 'gatsby-code-button-container'. 43 | buttonContainerClass: `customButtonContainerClass`, 44 | // Optional button class name. Defaults to 'gatsby-code-button'. 45 | buttonClass: `customButtonClass`, 46 | // Optional button text. Defaults to ''. 47 | buttonText: `customButtonText`, 48 | // Optional svg icon class name. Defaults to 'gatsby-code-button-icon'. 49 | svgIconClass: `customSvgIconClass`, 50 | // Optional svg icon. Defaults to svg string and can be 51 | // replaced with any other valid svg. Use custom classes 52 | // in the svg string and skip `iconClass` option. 53 | svgIcon: `customSvgIcon`, 54 | // Optional tooltip text. Defaults to ''. 55 | tooltipText: `customTooltipText`, 56 | // Optional toaster class name. Defaults to ''. 57 | toasterClass: `customToasterClass`, 58 | // Optional toaster text class name. Defaults to ''. 59 | toasterTextClass: `customToasterTextClass`, 60 | // Optional toaster text. Defaults to ''. 61 | toasterText: 'customToasterText', 62 | // Optional toaster duration. Defaults to 3500. 63 | toasterDuration: 5000 64 | } 65 | } 66 | ] 67 | } 68 | } 69 | ] 70 | ``` 71 | 72 | ### Custom styling 73 | 74 | Now that we've injected the custom button, we need to style it! 75 | 76 | ```css 77 | .gatsby-code-button-container {} 78 | .gatsby-code-button {} 79 | .gatsby-code-button-icon {} 80 | .gatsby-code-button-toaster {} 81 | .gatsby-code-button-toaster-text {} 82 | ``` 83 | 84 | To apply custom styles import stylesheet in your app's root `gatsby-browser.js`. 85 | 86 | ```js 87 | // gatsby-browser.js 88 | import './src/styles/custom-code-buttons.scss'; 89 | ``` 90 | 91 | ### Usage in Markdown 92 | 93 | In your Markdown content 94 | 95 | ``````js 96 | ```js 97 | alert('click to copy 💾'); 98 | ``` 99 | `````` 100 | 101 | This plugin will parse the Markdown AST, pluck the button, and then "clean" the code snippet language for further 102 | processing. With the default config options this plugin will create the following structure, injecting a custom `div`: 103 | 104 | ```html 105 |
114 |
115 | ... 116 |
117 |
118 | ``` 119 | 120 | With `toasterText` config enabled this plugin will inject a custom toaster node: 121 | 122 | ```html 123 |
124 |
Copied to clipboard
125 |
126 | ``` 127 | 128 | Don't show button 129 | 130 | ``````js 131 | ```js:clipboard=false 132 | alert('will not be copied 💾'); 133 | ``` 134 | `````` 135 | --------------------------------------------------------------------------------