Skip to content

Commit ab975f9

Browse files
committed
Added copy code to clipboard button
Resolves #2153
1 parent 7ac546c commit ab975f9

File tree

6 files changed

+99
-22
lines changed

6 files changed

+99
-22
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
### Features
44

55
- Added support for discovering a "module" comment on global files, #2165.
6+
- Added copy code to clipboard button, #2153.
67
- Function `@returns` blocks will now be rendered with the return type, #2180.
78

89
### Bug Fixes
910

1011
- Type parameter constraints now respect the `--hideParameterTypesInTitle` option, #2226.
1112
- Even more contrast fixes, #2248.
1213
- Fix semantic highlighting for predicate type's parameter references, #2249.
14+
- Fixed broken links to heading titles.
1315
- Fixed inconsistent styling between type parameter lists and parameter lists.
1416
- TypeDoc will now warn if more than one `@returns` block is is present in a function, and ignore the duplicate blocks as specified by TSDoc.
1517

src/lib/output/themes/MarkedPlugin.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { RendererEvent, MarkdownEvent, PageEvent } from "../events";
77
import { BindOption, readFile, copySync, isFile } from "../../utils";
88
import { highlight, isSupportedLanguage } from "../../utils/highlighter";
99
import type { Theme } from "shiki";
10-
import { getTextContent } from "../../utils/html";
10+
import { escapeHtml, getTextContent } from "../../utils/html";
1111

1212
/**
1313
* Implements markdown and relativeURL helpers for templates.
@@ -190,14 +190,15 @@ output file :
190190

191191
markedOptions.renderer.heading = (text, level, _, slugger) => {
192192
const slug = slugger.slug(text);
193-
// Prefix the slug with an extra `$` to prevent conflicts with TypeDoc's anchors.
193+
// Prefix the slug with an extra `md:` to prevent conflicts with TypeDoc's anchors.
194194
this.page!.pageHeadings.push({
195195
link: `#md:${slug}`,
196196
text: getTextContent(text),
197197
level,
198198
});
199-
return `<a id="md:${slug}" class="tsd-anchor"></a><h${level}><a href="#$${slug}" style="color:inherit;text-decoration:none">${text}</a></h${level}>`;
199+
return `<a id="md:${slug}" class="tsd-anchor"></a><h${level}><a href="#md:${slug}">${text}</a></h${level}>`;
200200
};
201+
markedOptions.renderer.code = renderCode;
201202
}
202203

203204
markedOptions.mangle ??= false; // See https://github.com/TypeStrong/typedoc/issues/1395
@@ -214,3 +215,33 @@ output file :
214215
event.parsedText = Marked.marked(event.parsedText);
215216
}
216217
}
218+
219+
// Basically a copy/paste of Marked's code, with the addition of the button
220+
// https://github.com/markedjs/marked/blob/v4.3.0/src/Renderer.js#L15-L39
221+
function renderCode(
222+
this: Marked.marked.Renderer,
223+
code: string,
224+
infostring: string | undefined,
225+
escaped: boolean
226+
) {
227+
const lang = (infostring || "").match(/\S*/)![0];
228+
if (this.options.highlight) {
229+
const out = this.options.highlight(code, lang);
230+
if (out != null && out !== code) {
231+
escaped = true;
232+
code = out;
233+
}
234+
}
235+
236+
code = code.replace(/\n$/, "") + "\n";
237+
238+
if (!lang) {
239+
return `<pre><code>${
240+
escaped ? code : escapeHtml(code)
241+
}</code><button>Copy</button></pre>\n`;
242+
}
243+
244+
return `<pre><code class="${this.options.langPrefix + escapeHtml(lang)}">${
245+
escaped ? code : escapeHtml(code)
246+
}</code><button>Copy</button></pre>\n`;
247+
}

src/lib/output/themes/default/assets/bootstrap.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { initTheme } from "./typedoc/Theme";
77

88
addEventListener("load", () => {
99
initSearch();
10+
initCopyCode();
1011

1112
registerComponent(Toggle, "a[data-toggle]");
1213
registerComponent(Accordion, ".tsd-index-accordion");
@@ -21,3 +22,25 @@ addEventListener("load", () => {
2122

2223
Object.defineProperty(window, "app", { value: app });
2324
});
25+
26+
function initCopyCode() {
27+
document.querySelectorAll("pre > button").forEach((button) => {
28+
let timeout: ReturnType<typeof setTimeout>;
29+
button.addEventListener("click", () => {
30+
if (button.previousElementSibling instanceof HTMLElement) {
31+
navigator.clipboard.writeText(
32+
button.previousElementSibling.innerText.trim()
33+
);
34+
}
35+
button.textContent = "Copied!";
36+
button.classList.add("visible");
37+
clearTimeout(timeout);
38+
timeout = setTimeout(() => {
39+
button.classList.remove("visible");
40+
timeout = setTimeout(() => {
41+
button.textContent = "Copy";
42+
}, 100);
43+
}, 1000);
44+
});
45+
});
46+
}

src/lib/utils/html.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,15 @@ function unescapeEntities(html: string) {
2626
export function getTextContent(text: string) {
2727
return unescapeEntities(text.replace(/<.*?(?:>|$)/g, ""));
2828
}
29+
30+
const htmlEscapes: Record<string, string> = {
31+
"&": "&amp;",
32+
"<": "&lt;",
33+
">": "&gt;",
34+
'"': "&quot;",
35+
"'": "&#39;",
36+
};
37+
38+
export function escapeHtml(html: string) {
39+
return html.replace(/[&<>'"]/g, (c) => htmlEscapes[c as never]);
40+
}

src/lib/utils/jsx.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* @module
1414
*/
1515

16+
import { escapeHtml } from "./html";
1617
import type {
1718
IntrinsicElements,
1819
JsxElement,
@@ -52,18 +53,6 @@ export declare namespace JSX {
5253
export { IntrinsicElements, JsxElement as Element };
5354
}
5455

55-
const htmlEscapes: Record<string, string> = {
56-
"&": "&amp;",
57-
"<": "&lt;",
58-
">": "&gt;",
59-
'"': "&quot;",
60-
"'": "&#39;",
61-
};
62-
63-
function escapeHtml(html: string) {
64-
return html.replace(/[&<>'"]/g, (c) => htmlEscapes[c as never]);
65-
}
66-
6756
const voidElements = new Set([
6857
"area",
6958
"base",

static/style.css

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,16 @@ h6 {
262262
line-height: 1.2;
263263
}
264264

265+
h1 > a,
266+
h2 > a,
267+
h3 > a,
268+
h4 > a,
269+
h5 > a,
270+
h6 > a {
271+
text-decoration: none;
272+
color: var(--color-text);
273+
}
274+
265275
h1 {
266276
font-size: 1.875rem;
267277
margin: 0.67rem 0;
@@ -296,12 +306,6 @@ h6 {
296306
text-transform: uppercase;
297307
}
298308

299-
pre {
300-
white-space: pre;
301-
white-space: pre-wrap;
302-
word-wrap: break-word;
303-
}
304-
305309
dl,
306310
menu,
307311
ol,
@@ -426,13 +430,29 @@ pre {
426430
}
427431

428432
pre {
433+
position: relative;
434+
white-space: pre;
435+
white-space: pre-wrap;
436+
word-wrap: break-word;
429437
padding: 10px;
430-
border: 0.1em solid var(--color-accent);
438+
border: 1px solid var(--color-accent);
431439
}
432440
pre code {
433441
padding: 0;
434442
font-size: 100%;
435443
}
444+
pre > button {
445+
position: absolute;
446+
top: 10px;
447+
right: 10px;
448+
opacity: 0;
449+
transition: opacity 0.1s;
450+
box-sizing: border-box;
451+
}
452+
pre:hover > button,
453+
pre > button.visible {
454+
opacity: 1;
455+
}
436456

437457
blockquote {
438458
margin: 1em 0;

0 commit comments

Comments
 (0)