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

[rustdoc] Add copy code feature #125779

Merged
merged 4 commits into from
Jul 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 66 additions & 27 deletions src/librustdoc/html/static/css/rustdoc.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,24 @@
--src-sidebar-width: 300px;
--desktop-sidebar-z-index: 100;
--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. */
Expand Down Expand Up @@ -724,6 +742,11 @@ ul.block, .block li {
position: relative;
margin-bottom: 10px;
}

.rustdoc .example-wrap > pre {
border-radius: 6px;
}

/* For the last child of a div, the margin will be taken care of
by the margin-top of the next item. */
.rustdoc .example-wrap:last-child {
Expand Down Expand Up @@ -1423,25 +1446,55 @@ documentation. */
top: 20px;
}

a.test-arrow {
.example-wrap > a.test-arrow, .example-wrap .button-holder {
visibility: hidden;
position: absolute;
padding: 5px 10px 5px 10px;
border-radius: 5px;
font-size: 1.375rem;
top: 5px;
right: 5px;
top: 4px;
right: 4px;
z-index: 1;
}
a.test-arrow {
padding: 5px 7px;
border-radius: var(--button-border-radius);
font-size: 1rem;
color: var(--test-arrow-color);
background-color: var(--test-arrow-background-color);
}
a.test-arrow:hover {
color: var(--test-arrow-hover-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;
}
.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 {
font-weight: 300;
Expand Down Expand Up @@ -1606,7 +1659,7 @@ a.tooltip:hover::after {
}

#settings-menu, #help-button {
margin-left: 4px;
margin-left: var(--button-left-margin);
display: flex;
}
#sidebar-button {
Expand Down Expand Up @@ -1637,7 +1690,7 @@ a.tooltip:hover::after {
justify-content: center;
background-color: var(--button-background-color);
border: 1px solid var(--border-color);
border-radius: 2px;
border-radius: var(--button-border-radius);
color: var(--settings-button-color);
/* Rare exception to specifying font sizes in rem. Since this is acting
as an icon, it's okay to specify their sizes in pixels. */
Expand Down Expand Up @@ -1689,8 +1742,8 @@ a.tooltip:hover::after {
#copy-path {
color: var(--copy-path-button-color);
background: var(--main-background-color);
height: 34px;
width: 33px;
height: var(--copy-path-height);
width: var(--copy-path-width);
margin-left: 10px;
padding: 0;
padding-left: 2px;
Expand All @@ -1699,27 +1752,13 @@ a.tooltip:hover::after {
}
#copy-path::before {
filter: var(--copy-path-img-filter);
/* clipboard <https://github.com/rust-lang/crates.io/commits/main/public/assets/copy.svg> */
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;
content: var(--clipboard-image);
}
#copy-path:hover::before {
filter: var(--copy-path-img-hover-filter);
}
#copy-path.clicked::before {
/* Checkmark <https://www.svgrepo.com/svg/335033/checkmark> */
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>');
content: var(--checkmark-image);
}

@keyframes rotating {
Expand Down
90 changes: 69 additions & 21 deletions src/librustdoc/html/static/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -1769,9 +1769,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
// and the copy buttons on the code examples.
(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");
if (!but) {
return;
Expand All @@ -1786,29 +1814,49 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
}
});

const el = document.createElement("textarea");
el.value = path.join("::");
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");
copyContentToClipboard(path.join("::"));
copyButtonAnimation(but);
};

if (reset_button_timeout !== null) {
window.clearTimeout(reset_button_timeout);
// 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 reset_button() {
reset_button_timeout = null;
but.classList.remove("clicked");
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";
GuillaumeGomez marked this conversation as resolved.
Show resolved Hide resolved
copyButton.title = "Copy code to clipboard";
copyButton.addEventListener("click", () => {
copyCode(elem.querySelector("pre > code"));
copyButtonAnimation(copyButton);
});
parent.appendChild(copyButton);
}

reset_button_timeout = window.setTimeout(reset_button, 1000);
};
onEachLazy(document.querySelectorAll(".docblock .example-wrap"), elem => {
elem.addEventListener("mouseover", addCopyButton);
});
}());
54 changes: 54 additions & 0 deletions tests/rustdoc-gui/copy-code.goml
Original file line number Diff line number Diff line change
@@ -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|,
})
8 changes: 4 additions & 4 deletions tests/rustdoc-gui/run-on-hover.goml
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ define-function: (
"visibility": "visible",
"color": |color|,
"background-color": |background|,
"font-size": "22px",
"border-radius": "5px",
"font-size": "16px",
"border-radius": "2px",
})
move-cursor-to: ".test-arrow"
assert-css: (".test-arrow:hover", {
"visibility": "visible",
"color": |hover_color|,
"background-color": |hover_background|,
"font-size": "22px",
"border-radius": "5px",
"font-size": "16px",
"border-radius": "2px",
})
},
)
Expand Down
Loading