From bc24cd8250c0cdb90c27bca4a0e800cda0c93f99 Mon Sep 17 00:00:00 2001 From: Isakdl Date: Wed, 6 May 2026 19:00:12 +0200 Subject: [PATCH] fix: Strip $ from the copy clipboard --- .../CodeBlock/Buttons/CopyButton/index.js | 101 ++++++++++++++++++ .../Buttons/CopyButton/styles.module.css | 47 ++++++++ 2 files changed, 148 insertions(+) create mode 100644 src/theme/CodeBlock/Buttons/CopyButton/index.js create mode 100644 src/theme/CodeBlock/Buttons/CopyButton/styles.module.css diff --git a/src/theme/CodeBlock/Buttons/CopyButton/index.js b/src/theme/CodeBlock/Buttons/CopyButton/index.js new file mode 100644 index 00000000..8399edd1 --- /dev/null +++ b/src/theme/CodeBlock/Buttons/CopyButton/index.js @@ -0,0 +1,101 @@ +/** + * Swizzled from @docusaurus/theme-classic to strip leading shell prompts + * (`$ `) from text written to the clipboard. The prompt remains visible + * in the rendered code block, but the copied text is the runnable command. + */ +import React, {useCallback, useState, useRef, useEffect} from 'react'; +import clsx from 'clsx'; +import {translate} from '@docusaurus/Translate'; +import {useCodeBlockContext} from '@docusaurus/theme-common/internal'; +import Button from '@theme/CodeBlock/Buttons/Button'; +import IconCopy from '@theme/Icon/Copy'; +import IconSuccess from '@theme/Icon/Success'; +import styles from './styles.module.css'; + +function stripShellPrompts(code) { + if (typeof code !== 'string' || code.length === 0) { + return code; + } + const lines = code.split('\n'); + const nonEmpty = lines.filter((line) => line.trim().length > 0); + if (nonEmpty.length === 0) { + return code; + } + const allPrompted = nonEmpty.every((line) => /^\s*\$\s+/.test(line)); + if (!allPrompted) { + return code; + } + return lines + .map((line) => + line.trim().length === 0 ? line : line.replace(/^(\s*)\$\s+/, '$1'), + ) + .join('\n'); +} + +function title() { + return translate({ + id: 'theme.CodeBlock.copy', + message: 'Copy', + description: 'The copy button label on code blocks', + }); +} + +function ariaLabel(isCopied) { + return isCopied + ? translate({ + id: 'theme.CodeBlock.copied', + message: 'Copied', + description: 'The copied button label on code blocks', + }) + : translate({ + id: 'theme.CodeBlock.copyButtonAriaLabel', + message: 'Copy code to clipboard', + description: 'The ARIA label for copy code blocks button', + }); +} + +async function copyToClipboard(text) { + if (navigator.clipboard) { + return navigator.clipboard.writeText(text); + } + const {default: copy} = await import('copy-text-to-clipboard'); + return copy(text); +} + +function useCopyButton() { + const { + metadata: {code}, + } = useCodeBlockContext(); + const [isCopied, setIsCopied] = useState(false); + const copyTimeout = useRef(undefined); + const copyCode = useCallback(() => { + copyToClipboard(stripShellPrompts(code)).then(() => { + setIsCopied(true); + copyTimeout.current = window.setTimeout(() => { + setIsCopied(false); + }, 1000); + }); + }, [code]); + useEffect(() => () => window.clearTimeout(copyTimeout.current), []); + return {copyCode, isCopied}; +} + +export default function CopyButton({className}) { + const {copyCode, isCopied} = useCopyButton(); + return ( + + ); +} diff --git a/src/theme/CodeBlock/Buttons/CopyButton/styles.module.css b/src/theme/CodeBlock/Buttons/CopyButton/styles.module.css new file mode 100644 index 00000000..c9e0ac5a --- /dev/null +++ b/src/theme/CodeBlock/Buttons/CopyButton/styles.module.css @@ -0,0 +1,47 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +:global(.theme-code-block:hover) .copyButtonCopied { + opacity: 1 !important; +} + +.copyButtonIcons { + position: relative; + width: 1.125rem; + height: 1.125rem; +} + +.copyButtonIcon, +.copyButtonSuccessIcon { + position: absolute; + top: 0; + left: 0; + fill: currentColor; + opacity: inherit; + width: inherit; + height: inherit; + transition: all var(--ifm-transition-fast) ease; +} + +.copyButtonSuccessIcon { + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0.33); + opacity: 0; + color: #00d600; +} + +.copyButtonCopied .copyButtonIcon { + transform: scale(0.33); + opacity: 0; +} + +.copyButtonCopied .copyButtonSuccessIcon { + transform: translate(-50%, -50%) scale(1); + opacity: 1; + transition-delay: 0.075s; +}