9194ef5abd
Docs/Test Workflow / Test docs build (push) Failing after 0s
Check links & references / links-check (push) Failing after 1s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.10) (push) Failing after 0s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.11) (push) Failing after 0s
PR Conflict Labeler / main (push) Failing after 2s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.12) (push) Failing after 2s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.13) (push) Failing after 0s
Pytest/Test Workflow / Build this Package (push) Failing after 5s
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.10) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.11) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.12) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.13) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.10) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.11) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.12) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.13) (push) Has been cancelled
Pytest/Test Workflow / testing-guardian (push) Has been cancelled
198 lines
6.3 KiB
JavaScript
198 lines
6.3 KiB
JavaScript
/**
|
|
* Custom copy handler for Python console (pycon) code blocks.
|
|
* Strips >>> and ... prompts when copying code examples.
|
|
*/
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const COPY_BUTTON_SELECTOR = ".md-clipboard, .md-code__button";
|
|
|
|
function handleCopyButtonClick(event) {
|
|
const copyButton = event.target.closest(COPY_BUTTON_SELECTOR);
|
|
if (!copyButton) return;
|
|
|
|
const codeBlock = findCodeBlockForCopyButton(copyButton);
|
|
if (!codeBlock) return;
|
|
|
|
const rawText = codeBlock.textContent || "";
|
|
if (!shouldStripPrompts(codeBlock, rawText)) return;
|
|
|
|
const strippedText = stripPythonPrompts(rawText);
|
|
primeClipboardButton(copyButton, strippedText);
|
|
}
|
|
|
|
function handleCopyButtonPointerDown(event) {
|
|
const copyButton = event.target.closest(COPY_BUTTON_SELECTOR);
|
|
if (!copyButton) return;
|
|
|
|
const codeBlock = findCodeBlockForCopyButton(copyButton);
|
|
if (!codeBlock) return;
|
|
|
|
const rawText = codeBlock.textContent || "";
|
|
if (!shouldStripPrompts(codeBlock, rawText)) return;
|
|
|
|
const strippedText = stripPythonPrompts(rawText);
|
|
primeClipboardButton(copyButton, strippedText);
|
|
}
|
|
|
|
function handleSelectionCopy(event) {
|
|
const selection = window.getSelection();
|
|
if (!selection || selection.rangeCount === 0) return;
|
|
|
|
const range = selection.getRangeAt(0);
|
|
const anchorNode = range.commonAncestorContainer;
|
|
const codeBlock =
|
|
anchorNode.nodeType === Node.ELEMENT_NODE
|
|
? anchorNode.closest("code")
|
|
: anchorNode.parentElement?.closest("code");
|
|
|
|
if (!codeBlock) return;
|
|
|
|
const rawText = selection.toString();
|
|
if (!shouldStripPrompts(codeBlock, rawText)) return;
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
const strippedText = stripPythonPrompts(rawText);
|
|
event.clipboardData?.setData("text/plain", strippedText);
|
|
}
|
|
|
|
function bindCopyButtons(root) {
|
|
root
|
|
.querySelectorAll(COPY_BUTTON_SELECTOR)
|
|
.forEach((button) => {
|
|
button.removeEventListener("click", handleCopyButtonClick, true);
|
|
button.addEventListener("click", handleCopyButtonClick, true);
|
|
button.removeEventListener(
|
|
"pointerdown",
|
|
handleCopyButtonPointerDown,
|
|
true
|
|
);
|
|
button.addEventListener(
|
|
"pointerdown",
|
|
handleCopyButtonPointerDown,
|
|
true
|
|
);
|
|
});
|
|
}
|
|
|
|
function observeDynamicCopyButtons() {
|
|
const observer = new MutationObserver((mutations) => {
|
|
for (const mutation of mutations) {
|
|
if (mutation.type !== "childList") continue;
|
|
mutation.addedNodes.forEach((node) => {
|
|
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
if (node.matches?.(COPY_BUTTON_SELECTOR)) {
|
|
bindCopyButtons(node.parentElement || document);
|
|
return;
|
|
}
|
|
if (node.querySelectorAll) {
|
|
const hasButtons = node.querySelectorAll(COPY_BUTTON_SELECTOR);
|
|
if (hasButtons.length > 0) {
|
|
bindCopyButtons(node);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
}
|
|
|
|
document.addEventListener("click", handleCopyButtonClick, true);
|
|
document.addEventListener("pointerdown", handleCopyButtonPointerDown, true);
|
|
document.addEventListener("copy", handleSelectionCopy, true);
|
|
bindCopyButtons(document);
|
|
observeDynamicCopyButtons();
|
|
});
|
|
|
|
function primeClipboardButton(copyButton, strippedText) {
|
|
copyButton.setAttribute("data-clipboard-text", strippedText);
|
|
copyButton.removeAttribute("data-clipboard-target");
|
|
copyButton.setAttribute("data-md-clipboard", "true");
|
|
}
|
|
|
|
function shouldStripPrompts(codeBlock, rawText) {
|
|
const hasReplPrompts = /(^|\n)[ \t]*(>>>|\.\.\.)/.test(rawText);
|
|
return (
|
|
hasReplPrompts ||
|
|
codeBlock.classList.contains("language-pycon") ||
|
|
codeBlock.closest("pre")?.classList.contains("pycon") ||
|
|
codeBlock.closest(".pycon") !== null ||
|
|
codeBlock.closest(".highlight")?.classList.contains("pycon")
|
|
);
|
|
}
|
|
|
|
function findCodeBlockForCopyButton(copyButton) {
|
|
const targetSelector = copyButton.getAttribute("data-clipboard-target");
|
|
if (targetSelector) {
|
|
const target = document.querySelector(targetSelector);
|
|
const targetCode = target?.querySelector?.("code") || target;
|
|
if (targetCode?.tagName?.toLowerCase() === "code") {
|
|
return targetCode;
|
|
}
|
|
}
|
|
return (
|
|
copyButton.closest("pre")?.querySelector("code") ||
|
|
copyButton.parentElement?.querySelector("pre code") ||
|
|
copyButton
|
|
.closest(".highlight, .codehilite, .md-typeset__scrollwrap, .md-typeset")
|
|
?.querySelector("pre code") ||
|
|
copyButton
|
|
.closest(".highlight, .codehilite, .md-typeset__scrollwrap, .md-typeset")
|
|
?.querySelector("code")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Strips Python REPL prompts (>>> and ...) from code text.
|
|
* Also removes output lines (lines that don't start with >>> or ...).
|
|
*
|
|
* NOTE: This is a best-effort parser. It preserves unprompted lines inside
|
|
* triple-quoted strings, but it does not fully model Python's tokenizer.
|
|
*/
|
|
function stripPythonPrompts(text) {
|
|
const lines = text.split("\n");
|
|
const codeLines = [];
|
|
let inTripleQuotedString = false;
|
|
|
|
function toggleTripleQuoteState(sourceLine) {
|
|
const tripleQuotePattern = /("""|''')/g;
|
|
const matches = sourceLine.match(tripleQuotePattern);
|
|
if (!matches) return;
|
|
if (matches.length % 2 === 1) {
|
|
inTripleQuotedString = !inTripleQuotedString;
|
|
}
|
|
}
|
|
|
|
for (const line of lines) {
|
|
const trimmedLine = line.trimEnd();
|
|
// Primary prompt: ">>> "
|
|
if (trimmedLine.startsWith(">>> ")) {
|
|
const stripped = trimmedLine.slice(4);
|
|
codeLines.push(stripped);
|
|
toggleTripleQuoteState(stripped);
|
|
}
|
|
// Continuation prompt: "... "
|
|
else if (trimmedLine.startsWith("... ")) {
|
|
const stripped = trimmedLine.slice(4);
|
|
codeLines.push(stripped);
|
|
toggleTripleQuoteState(stripped);
|
|
}
|
|
// Handle prompts without space after (edge case)
|
|
else if (trimmedLine === ">>>") {
|
|
codeLines.push("");
|
|
}
|
|
else if (trimmedLine === "...") {
|
|
codeLines.push("");
|
|
}
|
|
else if (inTripleQuotedString) {
|
|
codeLines.push(trimmedLine);
|
|
toggleTripleQuoteState(trimmedLine);
|
|
}
|
|
// Skip output lines (lines that don't start with prompts)
|
|
// This intentionally excludes output like "1.0" from the copied text
|
|
}
|
|
|
|
return codeLines.join("\n").trim();
|
|
}
|