Adding code block to technical blog is normal. By using GatsbyJS, it’s quite easy. What you have to do is just install gatsby-transformer-remark
and gatsby-remark-prismjs
. Then modify your gatsby-config.js
, add following settings like this:
require("prismjs/themes/prism-solarizedlight.css")
module.exports = {
plugins: [
{
resolve: `gatsby-transformer-remark`,
options: {
plugins: [
`gatsby-remark-prismjs`,
],
},
},
]
}
But, not like gatsby-plugin-mdx
, it’s not easy to add copy button
to code block. So, I wrote this plugin — gatsby-remark-prismjs-copy-button. Let’s see how to make it work.
TL;DR
What we need to do is just analyze markdown ASTs
which are generated by gatsby-transformer-remark
, and then add copy button
html before the code blocks’ ASTs.
I created this plugin to make it work. It can be found here.
https://github.com/thundermiracle/gatsby-remark-prismjs-copy-button
About AST
AST is the abbrev. of Abstract Syntax Tree
. Before compile the source code to machine readable ones, we usually transform the human readable source code to AST first. As the tree structure is much more easier for program to operate. The famous babel, eslint, webpack, etc. are using the same mechanism. Access the following web site if you want to know more about AST.
https://www.twilio.com/blog/abstract-syntax-trees
About MarkdownAST
gatsby-transformer-remark
is using markdown to transpile markdown contents to MarkdownAST. You can confirm the transpiled MarkdownAST by using the following site online.
Select both `Markdownand
remark`, it’ll automatically transpile the sample markdown contents. And if you click the code block in markdown side, it’ll display its MarkdownAST like this.
The plugins in the options refer to the ASTs here and convert them to html for display. For example: gatsby-remark-prismjs
converts all ASTs of type: "code"
from code blocks to html within the pre
tag. Here is the AST of the code block.
{
"type": "code",
"lang": "js",
"value": "console.log('!');",
"meta": null
}
Become to this html:
<div class="gatsby-highlight" data-language="javascript">
<pre class="language-javascript">
<code class="language-javascript">
console
<span class="token punctuation">.</span>
<span class="token function">log</span>
<span class="token punctuation">(</span>
<span class="token string">'!'</span>
<span class="token punctuation">)</span>
<span class="token punctuation">;</span>
</code>
</pre>
</div>
How to add a copy button
So how do I add a copy button? As introduced above, the AST in the code block is not html, so you cannot easily just append
the copy button html. But you can add the html part of the copy button as an AST of type: "html"
before the AST of the code block, and with a few css adjustments you can move
the copy button to the right place
. Here is the flow.
- Get ASTs for all code blocks from MarkdownASTs
- Insert copy button’s html before the code block’s AST
- Adjust the position of the copy button with css (e.g.
margin-top: -10px
) - Add the click event of copy button
So let’s begin.
Get ASTs for all code blocks from MarkdownASTs
First, you can get all MarkdownASTs from index.js
in the plugin’s root folder.
module.exports = function gatsbyRemarkPrismCopyButton({ markdownAST }) {
// output all ASTs
console.log(markdownAST);
}
Then, we can get code blocks’ ASTs by filtering type: "code"
. We’ll use unist-util-visit to do the filtering work.
module.exports = function gatsbyRemarkPrismCopyButton({ markdownAST }) {
visit(markdownAST, 'code', (node, index, parent) => {
}
}
Insert copy button’s html before the code block’s AST
- Get the original code from code block’s MarkdownAST for copy button
let code = parent.children[index].value;
code = code.replace(/"/gm, '"').replace(/`/gm, '\\`').replace(/\$/gm, '\\$');
- Generate copy button’s html MarkdownAST
const buttonNode = {
type: 'html',
value: `
<div class="gatsby-remark-prismjs-copy-button-container">
<div class="gatsby-remark-prismjs-copy-button" tabindex="0" role="button" aria-pressed="false" onclick="gatsbyRemarkCopyToClipboard(\`${code}\`, this)">
Copy
</div>
</div>
`,
};
- Insert the generated MarkdownAST before the code block’s AST
parent.children.splice(index, 0, buttonNode);
- Little tweaks after the insertion
As we mutate the MarkdownASTs directly(insert the copy button’s AST before code block), the node
parameter in (node, index, parent) => {}
will also be the processed node, so it will be an infinite loop. We must add a processed flag to node.lang
and skip it.
const COPY_BUTTON_ADDED = 'copy-button-added-';
// skip already added copy button
if (lang.startsWith(COPY_BUTTON_ADDED)) {
node.lang = lang.substring(COPY_BUTTON_ADDED.length);
return;
}
.
.
.
parent.children.splice(index, 0, buttonNode);
// add flag
node.lang = `${COPY_BUTTON_ADDED}${lang}`;
- Summary
To summarize the above steps, the index.js
will like this:
const visit = require('unist-util-visit');
const COPY_BUTTON_ADDED = 'copy-button-added-';
module.exports = function gatsbyRemarkCopyButton(
{ markdownAST },
) {
visit(markdownAST, 'code', (node, index, parent) => {
const lang = node.lang || '';
if (lang.startsWith(COPY_BUTTON_ADDED)) {
node.lang = lang.substring(COPY_BUTTON_ADDED.length);
return;
}
let code = parent.children[index].value;
code = code.replace(/"/gm, '"').replace(/`/gm, '\\`').replace(/\$/gm, '\\$');
const buttonNode = {
type: 'html',
value: `
<div class="gatsby-remark-prismjs-copy-button-container">
<div class="gatsby-remark-prismjs-copy-button" tabindex="0" role="button" aria-pressed="false" onclick="gatsbyRemarkCopyToClipboard(\`${code}\`, this)">
Copy
</div>
</div>
`,
};
parent.children.splice(index, 0, buttonNode);
node.lang = `${COPY_BUTTON_ADDED}${lang}`;
});
return markdownAST;
};
Adjust the position of the copy button with css
- Add
style.css
.gatsby-remark-prismjs-copy-button-container {
touch-action: none;
display: flex;
justify-content: flex-end;
position: relative;
top: 37px;
left: 8px;
margin-top: -28px;
z-index: 1;
pointer-events: none;
}
.gatsby-remark-prismjs-copy-button {
cursor: pointer;
pointer-events: initial;
font-size: 13px;
padding: 3px 5px 2px;
border-radius: 3px;
color: rgba(255, 255, 255, 0.88);
}
- import
style.css
ingatsby-browser.js
require("./style.css");
Add the click event of copy button
Add the gatesbyRemarkCopyToClipboard
function that we used when adding the copy button html MarkdownAST.
exports.onClientEntry = () => {
window.gatsbyRemarkCopyToClipboard = (str, copyButtonDom) => {
// prevent multiple click
if (copyButtonDom.textContent === 'Copied') {
return;
}
// copy to clipboard
navigator.clipboard.writeText(str);
copyButtonDom.textContent = 'Copied!';
};
};
Finish
The added copy button looks like this.
And the source code.
https://github.com/thundermiracle/gatsby-remark-prismjs-copy-button