Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Copy Button to Code Blocks #138

Closed
wants to merge 18 commits into from
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"linkedom": "^0.15.6",
"markdown-it": "^13.0.2",
"markdown-it-anchor": "^8.6.7",
"markdown-it-copy": "^1.2.0",
"mime": "^3.0.0",
"send": "^0.18.0",
"tsx": "^3.13.0",
Expand Down
65 changes: 65 additions & 0 deletions public/markdown-it-copy.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/* hide the langauge label provided by MarkdownIt */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/* hide the langauge label provided by MarkdownIt */
/* hide the language label provided by MarkdownIt */


pre code::after {
display: none;
}

/* copied from default.css with minimal changes positioning & color */

pre {
position: relative;
}
.m-mdic-copy-wrapper {
position: absolute;
top: 4px;
right: 8px;
}
.m-mdic-copy-wrapper span.u-mdic-copy-code_lang {
position: absolute;
top: 3px;
right: calc(100% + 4px);
font-family: system-ui;
font-size: 12px;
line-height: 18px;
color: #666;
opacity: 1;
}
.m-mdic-copy-wrapper div.u-mdic-copy-notify {
display: none;
position: absolute;
top: 0;
right: 0;
padding: 3px 6px;
border: 0;
border-radius: 3px;
background: none;
font-family: system-ui;
font-size: 12px;
line-height: 18px;
color: #666;
opacity: 0.3;
outline: none;
opacity: 1;
right: 100%;
padding-right: 35px;
}
.m-mdic-copy-wrapper button.u-mdic-copy-btn {
position: relative;
top: 0;
right: 0;
padding: 3px 6px;
border: 0;
border-radius: 3px;
background: none;
font-family: system-ui;
font-size: 12px;
line-height: 18px;
color: #666;
opacity: 1;
outline: none;
cursor: pointer;
transition: opacity 0.2s;
}
.m-mdic-copy-wrapper button.u-mdic-copy-btn:hover {
opacity: 1;
}
6 changes: 6 additions & 0 deletions src/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {type RuleCore} from "markdown-it/lib/parser_core.js";
import {type RuleInline} from "markdown-it/lib/parser_inline.js";
import {type RenderRule, type default as Renderer} from "markdown-it/lib/renderer.js";
import MarkdownItAnchor from "markdown-it-anchor";
import MarkdownItCopy from "markdown-it-copy";
import mime from "mime";
import {getLocalPath} from "./files.js";
import {computeHash} from "./hash.js";
Expand Down Expand Up @@ -371,6 +372,11 @@ export function parseMarkdown(source: string, root: string, sourcePath: string):
}
});
md.use(MarkdownItAnchor, {permalink: MarkdownItAnchor.permalink.headerLink({class: "observablehq-header-anchor"})});
md.use(MarkdownItCopy, {
showCodeLanguage: true,
successText: "success",
failText: "fail"
});
md.inline.ruler.push("placeholder", transformPlaceholderInline);
md.core.ruler.before("linkify", "placeholder", transformPlaceholderCore);
md.renderer.rules.placeholder = makePlaceholderRenderer(root, sourcePath);
Expand Down
1 change: 1 addition & 0 deletions src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ ${
}<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&display=swap">
<link rel="stylesheet" type="text/css" href="/_observablehq/style.css">
<link rel="stylesheet" type="text/css" href="/_observablehq/markdown-it-copy.css">
${Array.from(getImportPreloads(parseResult, path))
.map((href) => `<link rel="modulepreload" href="${href}">`)
.join("\n")}
Expand Down
13 changes: 12 additions & 1 deletion test/markdown-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe("parseMarkdown(input)", () => {
const snapshot = parseMarkdown(await readFile(path, "utf8"), "test/input", name);
let allequal = true;
for (const ext of ["html", "json"]) {
const actual = ext === "json" ? jsonMeta(snapshot) : snapshot[ext];
const actual = applySpecialCaseFilters(ext === "json" ? jsonMeta(snapshot) : snapshot[ext]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll want the build to be always reproducible, not only in tests; otherwise it's going to be painful for people who track changes of the artifacts, or when you who use a deploy script based on file differences.

const outfile = resolve(outputRoot, `${basename(outname, ".md")}.${ext}`);
const diffile = resolve(outputRoot, `${basename(outname, ".md")}-changed.${ext}`);
let expected;
Expand Down Expand Up @@ -70,3 +70,14 @@ function jsonMeta({html, ...rest}: ParseResult): string {
function jsonEqual(a: string, b: string): boolean {
return deepEqual(JSON.parse(a), JSON.parse(b));
}

function applySpecialCaseFilters(snapshotContent: string): string {
// if the snapshot contains markdown-it-copy code, strip out j-notify id,
// which is regenerated on every run and thus always different

if (snapshotContent.includes("mdic")) {
return snapshotContent.replace(/(id=\\?\\?"j-notify)([\d-]*)/g, "$1");
}

return snapshotContent;
}
1 change: 1 addition & 0 deletions test/output/build/config/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&display=swap">
<link rel="stylesheet" type="text/css" href="/_observablehq/style.css">
<link rel="stylesheet" type="text/css" href="/_observablehq/markdown-it-copy.css">
<link rel="modulepreload" href="/_observablehq/runtime.js">
<script type="module">

Expand Down
1 change: 1 addition & 0 deletions test/output/build/config/one.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&display=swap">
<link rel="stylesheet" type="text/css" href="/_observablehq/style.css">
<link rel="stylesheet" type="text/css" href="/_observablehq/markdown-it-copy.css">
<link rel="modulepreload" href="/_observablehq/runtime.js">
<script type="module">

Expand Down
1 change: 1 addition & 0 deletions test/output/build/config/sub/two.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&display=swap">
<link rel="stylesheet" type="text/css" href="/_observablehq/style.css">
<link rel="stylesheet" type="text/css" href="/_observablehq/markdown-it-copy.css">
<link rel="modulepreload" href="/_observablehq/runtime.js">
<script type="module">

Expand Down
1 change: 1 addition & 0 deletions test/output/build/imports/foo/foo.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&display=swap">
<link rel="stylesheet" type="text/css" href="/_observablehq/style.css">
<link rel="stylesheet" type="text/css" href="/_observablehq/markdown-it-copy.css">
<link rel="modulepreload" href="/_observablehq/runtime.js">
<link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/d3/+esm">
<link rel="modulepreload" href="/_import/bar/bar.js">
Expand Down
1 change: 1 addition & 0 deletions test/output/build/multi/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&display=swap">
<link rel="stylesheet" type="text/css" href="/_observablehq/style.css">
<link rel="stylesheet" type="text/css" href="/_observablehq/markdown-it-copy.css">
<link rel="modulepreload" href="/_observablehq/runtime.js">
<script type="module">

Expand Down
1 change: 1 addition & 0 deletions test/output/build/multi/subsection/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&display=swap">
<link rel="stylesheet" type="text/css" href="/_observablehq/style.css">
<link rel="stylesheet" type="text/css" href="/_observablehq/markdown-it-copy.css">
<link rel="modulepreload" href="/_observablehq/runtime.js">
<script type="module">

Expand Down
1 change: 1 addition & 0 deletions test/output/build/simple/simple.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&display=swap">
<link rel="stylesheet" type="text/css" href="/_observablehq/style.css">
<link rel="stylesheet" type="text/css" href="/_observablehq/markdown-it-copy.css">
<link rel="modulepreload" href="/_observablehq/runtime.js">
<script type="module">

Expand Down
5 changes: 4 additions & 1 deletion test/output/fenced-code.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ <h1 id="fenced-code" tabindex="-1"><a class="observablehq-header-anchor" href="#
<pre><code class="language-js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">add</span>(<span class="hljs-params">a, b</span>) {
<span class="hljs-keyword">return</span> a + b;
}
</code></pre>
</code><div class="m-mdic-copy-wrapper"><span class="u-mdic-copy-code_lang">js</span><div class="u-mdic-copy-notify" id="j-notify">success</div><button class="u-mdic-copy-btn j-mdic-copy-btn" data-mdic-content="function add(a, b) {
return a + b;
}
" data-mdic-attach-content="" data-mdic-notify-id="j-notify" data-mdic-notify-delay="2000" data-mdic-copy-fail-text="fail" onclick="!function(t){const e={copy:(t='',e='')=>new Promise((c,o)=>{const n=document.createElement('textarea'),d=e?`\n\n${e}`:e;n.value=`${t}${d}`,document.body.appendChild(n),n.select();try{const t=document.execCommand('copy');document.body.removeChild(n),t?c():o()}catch(t){document.body.removeChild(n),o()}}),btnClick(t){const c=t&&t.dataset?t.dataset:{},o=c.mdicNotifyId,n=document.getElementById(o),d=c.mdicNotifyDelay,i=c.mdicCopyFailText;e.copy(c.mdicContent,c.mdicAttachContent).then(()=>{n.style.display='block',setTimeout(()=>{n.style.display='none'},d)}).catch(()=>{alert(i)})}};e.btnClick(t)}(this);">copy</button></div></pre>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should roll our own, without this obfuscated code.

</div>
4 changes: 2 additions & 2 deletions test/output/fenced-code.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"cellIds": [
"fe9e095e"
],
"html": "<div><div id=\"cell-fe9e095e\" class=\"observablehq observablehq--block\"></div>\n<pre><code class=\"language-js\"><span class=\"hljs-keyword\">function</span> <span class=\"hljs-title function_\">add</span>(<span class=\"hljs-params\">a, b</span>) {\n <span class=\"hljs-keyword\">return</span> a + b;\n}\n</code></pre>\n</div>"
"html": "<div><div id=\"cell-fe9e095e\" class=\"observablehq observablehq--block\"></div>\n<pre><code class=\"language-js\"><span class=\"hljs-keyword\">function</span> <span class=\"hljs-title function_\">add</span>(<span class=\"hljs-params\">a, b</span>) {\n <span class=\"hljs-keyword\">return</span> a + b;\n}\n</code><div class=\"m-mdic-copy-wrapper\"><span class=\"u-mdic-copy-code_lang\">js</span><div class=\"u-mdic-copy-notify\" id=\"j-notify\">success</div><button class=\"u-mdic-copy-btn j-mdic-copy-btn\" data-mdic-content=\"function add(a, b) {\n return a + b;\n}\n\" data-mdic-attach-content=\"\" data-mdic-notify-id=\"j-notify\" data-mdic-notify-delay=\"2000\" data-mdic-copy-fail-text=\"fail\" onclick=\"!function(t){const e={copy:(t='',e='')=>new Promise((c,o)=>{const n=document.createElement('textarea'),d=e?`\\n\\n${e}`:e;n.value=`${t}${d}`,document.body.appendChild(n),n.select();try{const t=document.execCommand('copy');document.body.removeChild(n),t?c():o()}catch(t){document.body.removeChild(n),o()}}),btnClick(t){const c=t&&t.dataset?t.dataset:{},o=c.mdicNotifyId,n=document.getElementById(o),d=c.mdicNotifyDelay,i=c.mdicCopyFailText;e.copy(c.mdicContent,c.mdicAttachContent).then(()=>{n.style.display='block',setTimeout(()=>{n.style.display='none'},d)}).catch(()=>{alert(i)})}};e.btnClick(t)}(this);\">copy</button></div></pre>\n</div>"
}
],
"cells": [
Expand All @@ -29,4 +29,4 @@
"body": "() => {\nfunction add(a, b) {\n return a + b;\n}\nreturn {add};\n}"
}
]
}
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1864,6 +1864,11 @@ markdown-it-anchor@^8.6.7:
resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz#ee6926daf3ad1ed5e4e3968b1740eef1c6399634"
integrity sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==

markdown-it-copy@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/markdown-it-copy/-/markdown-it-copy-1.2.0.tgz#6fcaae495e23a0060013d1344acdee929d1fcac5"
integrity sha512-8Pm/cF7kpr+9VArhICCndb31zmz75hMR0WpmelUNn1RYe8vkqumLKt0xaInTT4mELhMqmnIr+8dm30w7Pzqc4A==

markdown-it@^13.0.2:
version "13.0.2"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.2.tgz#1bc22e23379a6952e5d56217fbed881e0c94d536"
Expand Down