Rollup merge of #125779 - GuillaumeGomez:copy-code, r=rustdoc-team
[rustdoc] Add copy code feature This PR adds a "copy code" to code blocks. Since this is a JS only feature, the HTML is generated with JS when the user hovers the code block to prevent generating DOM unless needed. Two things to note: 1. I voluntarily kept the current behaviour of the run button (only when hovering a code block with a mouse) so it doesn't do anything on mobile. I plan to send a follow-up where the buttons would "expandable" or something. Still need to think which approach would be the best. 2. I used a picture and not text like the run button to remain consistent with the "copy path" button. I'd also prefer for the run button to use a picture (like what is used in mdbook) but again, that's something to be discussed later on. The rendering looks like this: ![Screenshot from 2024-06-03 21-29-48](https://github.com/rust-lang/rust/assets/3050060/a0b18f9c-b3dd-4a65-89a7-5a7a303b5c2b) ![Screenshot from 2024-06-03 21-30-20](https://github.com/rust-lang/rust/assets/3050060/b3b084ff-2716-4160-820b-d4774681a961) It can be tested [here](https://guillaume-gomez.fr/rustdoc/bar/struct.Bar.html) (without the run button) and [here](https://guillaume-gomez.fr/rustdoc/foo/struct.Bar.html) (with the run button). Fixes #86851. r? ``@notriddle``
This commit is contained in:
commit
a8cc24a1ab
@ -16,6 +16,24 @@
|
|||||||
--src-sidebar-width: 300px;
|
--src-sidebar-width: 300px;
|
||||||
--desktop-sidebar-z-index: 100;
|
--desktop-sidebar-z-index: 100;
|
||||||
--sidebar-elems-left-padding: 24px;
|
--sidebar-elems-left-padding: 24px;
|
||||||
|
/* clipboard <https://github.com/rust-lang/crates.io/commits/main/public/assets/copy.svg> */
|
||||||
|
--clipboard-image: url('data:image/svg+xml,<svg width="19" height="18" viewBox="0 0 24 25" \
|
||||||
|
xmlns="http://www.w3.org/2000/svg" aria-label="Copy to clipboard">\
|
||||||
|
<path d="M18 20h2v3c0 1-1 2-2 2H2c-.998 0-2-1-2-2V5c0-.911.755-1.667 1.667-1.667h5A3.323 3.323 0 \
|
||||||
|
0110 0a3.323 3.323 0 013.333 3.333h5C19.245 3.333 20 4.09 20 5v8.333h-2V9H2v14h16v-3zM3 \
|
||||||
|
7h14c0-.911-.793-1.667-1.75-1.667H13.5c-.957 0-1.75-.755-1.75-1.666C11.75 2.755 10.957 2 10 \
|
||||||
|
2s-1.75.755-1.75 1.667c0 .911-.793 1.666-1.75 1.666H4.75C3.793 5.333 3 6.09 3 7z"/>\
|
||||||
|
<path d="M4 19h6v2H4zM12 11H4v2h8zM4 17h4v-2H4zM15 15v-3l-4.5 4.5L15 21v-3l8.027-.032L23 15z"/>\
|
||||||
|
</svg>');
|
||||||
|
--copy-path-height: 34px;
|
||||||
|
--copy-path-width: 33px;
|
||||||
|
/* Checkmark <https://www.svgrepo.com/svg/335033/checkmark> */
|
||||||
|
--checkmark-image: url('data:image/svg+xml,<svg viewBox="-1 -1 23 23" \
|
||||||
|
xmlns="http://www.w3.org/2000/svg" fill="black" height="18px">\
|
||||||
|
<g><path d="M9 19.414l-6.707-6.707 1.414-1.414L9 16.586 20.293 5.293l1.414 1.414"></path>\
|
||||||
|
</g></svg>');
|
||||||
|
--button-left-margin: 4px;
|
||||||
|
--button-border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* See FiraSans-LICENSE.txt for the Fira Sans license. */
|
/* See FiraSans-LICENSE.txt for the Fira Sans license. */
|
||||||
@ -723,6 +741,11 @@ ul.block, .block li {
|
|||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rustdoc .example-wrap > pre {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
/* For the last child of a div, the margin will be taken care of
|
/* For the last child of a div, the margin will be taken care of
|
||||||
by the margin-top of the next item. */
|
by the margin-top of the next item. */
|
||||||
.rustdoc .example-wrap:last-child {
|
.rustdoc .example-wrap:last-child {
|
||||||
@ -1427,15 +1450,17 @@ documentation. */
|
|||||||
top: 20px;
|
top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.test-arrow {
|
.example-wrap > a.test-arrow, .example-wrap .button-holder {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding: 5px 10px 5px 10px;
|
top: 4px;
|
||||||
border-radius: 5px;
|
right: 4px;
|
||||||
font-size: 1.375rem;
|
|
||||||
top: 5px;
|
|
||||||
right: 5px;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
}
|
||||||
|
a.test-arrow {
|
||||||
|
padding: 5px 7px;
|
||||||
|
border-radius: var(--button-border-radius);
|
||||||
|
font-size: 1rem;
|
||||||
color: var(--test-arrow-color);
|
color: var(--test-arrow-color);
|
||||||
background-color: var(--test-arrow-background-color);
|
background-color: var(--test-arrow-background-color);
|
||||||
}
|
}
|
||||||
@ -1443,9 +1468,37 @@ a.test-arrow:hover {
|
|||||||
color: var(--test-arrow-hover-color);
|
color: var(--test-arrow-hover-color);
|
||||||
background-color: var(--test-arrow-hover-background-color);
|
background-color: var(--test-arrow-hover-background-color);
|
||||||
}
|
}
|
||||||
.example-wrap:hover .test-arrow {
|
.example-wrap .button-holder {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.example-wrap:hover > .test-arrow {
|
||||||
|
padding: 2px 7px;
|
||||||
|
}
|
||||||
|
.example-wrap:hover > .test-arrow, .example-wrap:hover > .button-holder {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
.example-wrap .button-holder .copy-button {
|
||||||
|
color: var(--copy-path-button-color);
|
||||||
|
background: var(--main-background-color);
|
||||||
|
height: var(--copy-path-height);
|
||||||
|
width: var(--copy-path-width);
|
||||||
|
margin-left: var(--button-left-margin);
|
||||||
|
padding: 2px 0 0 4px;
|
||||||
|
border: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--button-border-radius);
|
||||||
|
}
|
||||||
|
.example-wrap .button-holder .copy-button::before {
|
||||||
|
filter: var(--copy-path-img-filter);
|
||||||
|
content: var(--clipboard-image);
|
||||||
|
}
|
||||||
|
.example-wrap .button-holder .copy-button:hover::before {
|
||||||
|
filter: var(--copy-path-img-hover-filter);
|
||||||
|
}
|
||||||
|
.example-wrap .button-holder .copy-button.clicked::before {
|
||||||
|
content: var(--checkmark-image);
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.code-attribute {
|
.code-attribute {
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
@ -1610,7 +1663,7 @@ a.tooltip:hover::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#settings-menu, #help-button {
|
#settings-menu, #help-button {
|
||||||
margin-left: 4px;
|
margin-left: var(--button-left-margin);
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
#sidebar-button {
|
#sidebar-button {
|
||||||
@ -1641,7 +1694,7 @@ a.tooltip:hover::after {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: var(--button-background-color);
|
background-color: var(--button-background-color);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 2px;
|
border-radius: var(--button-border-radius);
|
||||||
color: var(--settings-button-color);
|
color: var(--settings-button-color);
|
||||||
/* Rare exception to specifying font sizes in rem. Since this is acting
|
/* Rare exception to specifying font sizes in rem. Since this is acting
|
||||||
as an icon, it's okay to specify their sizes in pixels. */
|
as an icon, it's okay to specify their sizes in pixels. */
|
||||||
@ -1693,8 +1746,8 @@ a.tooltip:hover::after {
|
|||||||
#copy-path {
|
#copy-path {
|
||||||
color: var(--copy-path-button-color);
|
color: var(--copy-path-button-color);
|
||||||
background: var(--main-background-color);
|
background: var(--main-background-color);
|
||||||
height: 34px;
|
height: var(--copy-path-height);
|
||||||
width: 33px;
|
width: var(--copy-path-width);
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
padding-left: 2px;
|
padding-left: 2px;
|
||||||
@ -1703,27 +1756,13 @@ a.tooltip:hover::after {
|
|||||||
}
|
}
|
||||||
#copy-path::before {
|
#copy-path::before {
|
||||||
filter: var(--copy-path-img-filter);
|
filter: var(--copy-path-img-filter);
|
||||||
/* clipboard <https://github.com/rust-lang/crates.io/commits/main/public/assets/copy.svg> */
|
content: var(--clipboard-image);
|
||||||
content: url('data:image/svg+xml,<svg width="19" height="18" viewBox="0 0 24 25" \
|
|
||||||
xmlns="http://www.w3.org/2000/svg" aria-label="Copy to clipboard">\
|
|
||||||
<path d="M18 20h2v3c0 1-1 2-2 2H2c-.998 0-2-1-2-2V5c0-.911.755-1.667 1.667-1.667h5A3.323 3.323 0 \
|
|
||||||
0110 0a3.323 3.323 0 013.333 3.333h5C19.245 3.333 20 4.09 20 5v8.333h-2V9H2v14h16v-3zM3 \
|
|
||||||
7h14c0-.911-.793-1.667-1.75-1.667H13.5c-.957 0-1.75-.755-1.75-1.666C11.75 2.755 10.957 2 10 \
|
|
||||||
2s-1.75.755-1.75 1.667c0 .911-.793 1.666-1.75 1.666H4.75C3.793 5.333 3 6.09 3 7z"/>\
|
|
||||||
<path d="M4 19h6v2H4zM12 11H4v2h8zM4 17h4v-2H4zM15 15v-3l-4.5 4.5L15 21v-3l8.027-.032L23 15z"/>\
|
|
||||||
</svg>');
|
|
||||||
width: 19px;
|
|
||||||
height: 18px;
|
|
||||||
}
|
}
|
||||||
#copy-path:hover::before {
|
#copy-path:hover::before {
|
||||||
filter: var(--copy-path-img-hover-filter);
|
filter: var(--copy-path-img-hover-filter);
|
||||||
}
|
}
|
||||||
#copy-path.clicked::before {
|
#copy-path.clicked::before {
|
||||||
/* Checkmark <https://www.svgrepo.com/svg/335033/checkmark> */
|
content: var(--checkmark-image);
|
||||||
content: url('data:image/svg+xml,<svg viewBox="-1 -1 23 23" xmlns="http://www.w3.org/2000/svg" \
|
|
||||||
fill="black" height="18px">\
|
|
||||||
<g><path d="M9 19.414l-6.707-6.707 1.414-1.414L9 16.586 20.293 5.293l1.414 1.414"></path>\
|
|
||||||
</g></svg>');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes rotating {
|
@keyframes rotating {
|
||||||
|
@ -1771,9 +1771,37 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
|
|||||||
}());
|
}());
|
||||||
|
|
||||||
// This section handles the copy button that appears next to the path breadcrumbs
|
// This section handles the copy button that appears next to the path breadcrumbs
|
||||||
|
// and the copy buttons on the code examples.
|
||||||
(function() {
|
(function() {
|
||||||
let reset_button_timeout = null;
|
// Common functions to copy buttons.
|
||||||
|
function copyContentToClipboard(content) {
|
||||||
|
const el = document.createElement("textarea");
|
||||||
|
el.value = content;
|
||||||
|
el.setAttribute("readonly", "");
|
||||||
|
// To not make it appear on the screen.
|
||||||
|
el.style.position = "absolute";
|
||||||
|
el.style.left = "-9999px";
|
||||||
|
|
||||||
|
document.body.appendChild(el);
|
||||||
|
el.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
document.body.removeChild(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyButtonAnimation(button) {
|
||||||
|
button.classList.add("clicked");
|
||||||
|
|
||||||
|
if (button.reset_button_timeout !== undefined) {
|
||||||
|
window.clearTimeout(button.reset_button_timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.reset_button_timeout = window.setTimeout(() => {
|
||||||
|
button.reset_button_timeout = undefined;
|
||||||
|
button.classList.remove("clicked");
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy button that appears next to the path breadcrumbs.
|
||||||
const but = document.getElementById("copy-path");
|
const but = document.getElementById("copy-path");
|
||||||
if (!but) {
|
if (!but) {
|
||||||
return;
|
return;
|
||||||
@ -1788,29 +1816,49 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const el = document.createElement("textarea");
|
copyContentToClipboard(path.join("::"));
|
||||||
el.value = path.join("::");
|
copyButtonAnimation(but);
|
||||||
el.setAttribute("readonly", "");
|
|
||||||
// To not make it appear on the screen.
|
|
||||||
el.style.position = "absolute";
|
|
||||||
el.style.left = "-9999px";
|
|
||||||
|
|
||||||
document.body.appendChild(el);
|
|
||||||
el.select();
|
|
||||||
document.execCommand("copy");
|
|
||||||
document.body.removeChild(el);
|
|
||||||
|
|
||||||
but.classList.add("clicked");
|
|
||||||
|
|
||||||
if (reset_button_timeout !== null) {
|
|
||||||
window.clearTimeout(reset_button_timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset_button() {
|
|
||||||
reset_button_timeout = null;
|
|
||||||
but.classList.remove("clicked");
|
|
||||||
}
|
|
||||||
|
|
||||||
reset_button_timeout = window.setTimeout(reset_button, 1000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Copy buttons on code examples.
|
||||||
|
function copyCode(codeElem) {
|
||||||
|
if (!codeElem) {
|
||||||
|
// Should never happen, but the world is a dark and dangerous place.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
copyContentToClipboard(codeElem.textContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCopyButton(event) {
|
||||||
|
let elem = event.target;
|
||||||
|
while (!hasClass(elem, "example-wrap")) {
|
||||||
|
elem = elem.parentElement;
|
||||||
|
if (elem.tagName === "body" || hasClass(elem, "docblock")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Since the button will be added, no need to keep this listener around.
|
||||||
|
elem.removeEventListener("mouseover", addCopyButton);
|
||||||
|
|
||||||
|
const parent = document.createElement("div");
|
||||||
|
parent.className = "button-holder";
|
||||||
|
const runButton = elem.querySelector(".test-arrow");
|
||||||
|
if (runButton !== null) {
|
||||||
|
// If there is a run button, we move it into the same div.
|
||||||
|
parent.appendChild(runButton);
|
||||||
|
}
|
||||||
|
elem.appendChild(parent);
|
||||||
|
const copyButton = document.createElement("button");
|
||||||
|
copyButton.className = "copy-button";
|
||||||
|
copyButton.title = "Copy code to clipboard";
|
||||||
|
copyButton.addEventListener("click", () => {
|
||||||
|
copyCode(elem.querySelector("pre > code"));
|
||||||
|
copyButtonAnimation(copyButton);
|
||||||
|
});
|
||||||
|
parent.appendChild(copyButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
onEachLazy(document.querySelectorAll(".docblock .example-wrap"), elem => {
|
||||||
|
elem.addEventListener("mouseover", addCopyButton);
|
||||||
|
});
|
||||||
}());
|
}());
|
||||||
|
54
tests/rustdoc-gui/copy-code.goml
Normal file
54
tests/rustdoc-gui/copy-code.goml
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// Checks that the "copy code" button is not triggering JS error and its display
|
||||||
|
// isn't broken.
|
||||||
|
go-to: "file://" + |DOC_PATH| + "/test_docs/fn.foo.html"
|
||||||
|
|
||||||
|
define-function: (
|
||||||
|
"check-copy-button",
|
||||||
|
[],
|
||||||
|
block {
|
||||||
|
// First we ensure that there are no "copy code" currently existing.
|
||||||
|
assert-count: (".example-wrap .copy-button", 0)
|
||||||
|
move-cursor-to: ".example-wrap"
|
||||||
|
assert-count: (".example-wrap .copy-button", 1)
|
||||||
|
// We now ensure it's only displayed when the example is hovered.
|
||||||
|
assert-css: (".example-wrap .copy-button", { "visibility": "visible" })
|
||||||
|
move-cursor-to: ".search-input"
|
||||||
|
assert-css: (".example-wrap .copy-button", { "visibility": "hidden" })
|
||||||
|
// Checking that the copy button has the same size as the "copy path" button.
|
||||||
|
compare-elements-size: (
|
||||||
|
"#copy-path",
|
||||||
|
".example-wrap:nth-of-type(1) .copy-button",
|
||||||
|
["height", "width"],
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
call-function: ("check-copy-button", {})
|
||||||
|
// Checking that the run button and the copy button have the same height.
|
||||||
|
compare-elements-size: (
|
||||||
|
".example-wrap:nth-of-type(1) .test-arrow",
|
||||||
|
".example-wrap:nth-of-type(1) .copy-button",
|
||||||
|
["height"],
|
||||||
|
)
|
||||||
|
// ... and the same y position.
|
||||||
|
compare-elements-position: (
|
||||||
|
".example-wrap:nth-of-type(1) .test-arrow",
|
||||||
|
".example-wrap:nth-of-type(1) .copy-button",
|
||||||
|
["y"],
|
||||||
|
)
|
||||||
|
store-size: (".example-wrap:nth-of-type(1) .copy-button", {
|
||||||
|
"height": copy_height,
|
||||||
|
"width": copy_width,
|
||||||
|
})
|
||||||
|
assert: |copy_height| > 0 && |copy_width| > 0
|
||||||
|
|
||||||
|
// Checking same things for the copy button when there is no run button.
|
||||||
|
go-to: "file://" + |DOC_PATH| + "/lib2/sub_mod/struct.Foo.html"
|
||||||
|
call-function: ("check-copy-button", {})
|
||||||
|
// Ensure there is no run button.
|
||||||
|
assert-count: (".example-wrap .test-arrow", 0)
|
||||||
|
// Check it's the same size without a run button.
|
||||||
|
assert-size: (".example-wrap:nth-of-type(1) .copy-button", {
|
||||||
|
"height": |copy_height|,
|
||||||
|
"width": |copy_width|,
|
||||||
|
})
|
@ -17,16 +17,16 @@ define-function: (
|
|||||||
"visibility": "visible",
|
"visibility": "visible",
|
||||||
"color": |color|,
|
"color": |color|,
|
||||||
"background-color": |background|,
|
"background-color": |background|,
|
||||||
"font-size": "22px",
|
"font-size": "16px",
|
||||||
"border-radius": "5px",
|
"border-radius": "2px",
|
||||||
})
|
})
|
||||||
move-cursor-to: ".test-arrow"
|
move-cursor-to: ".test-arrow"
|
||||||
assert-css: (".test-arrow:hover", {
|
assert-css: (".test-arrow:hover", {
|
||||||
"visibility": "visible",
|
"visibility": "visible",
|
||||||
"color": |hover_color|,
|
"color": |hover_color|,
|
||||||
"background-color": |hover_background|,
|
"background-color": |hover_background|,
|
||||||
"font-size": "22px",
|
"font-size": "16px",
|
||||||
"border-radius": "5px",
|
"border-radius": "2px",
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user