diff --git a/docs/javascripts/pycon_copy.js b/docs/javascripts/pycon_copy.js new file mode 100644 index 00000000..283753d5 --- /dev/null +++ b/docs/javascripts/pycon_copy.js @@ -0,0 +1,197 @@ +/** + * 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(); +} diff --git a/docs/theme/main.html b/docs/theme/main.html index d4b99844..a19af310 100644 --- a/docs/theme/main.html +++ b/docs/theme/main.html @@ -12,6 +12,7 @@ {% endblock content %} {% block extrahead %} +{{ super() }} {% endblock %} diff --git a/mkdocs.yml b/mkdocs.yml index a678b519..5223928d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -198,6 +198,7 @@ markdown_extensions: extra_javascript: - "javascripts/init_kapa_widget.js" - "javascripts/cookbooks-card.js" + - "javascripts/pycon_copy.js" - "javascripts/segment.js" - "javascripts/mathjax.js" - "https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.8/purify.min.js" diff --git a/src/supervision/metrics/mean_average_precision.py b/src/supervision/metrics/mean_average_precision.py index 24c4cc49..c29b2839 100644 --- a/src/supervision/metrics/mean_average_precision.py +++ b/src/supervision/metrics/mean_average_precision.py @@ -94,7 +94,7 @@ class MeanAveragePrecisionResult: Average Precision (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.284 Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.497 Average Precision (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.629 - ``` + ``` """ if ( self.small_objects is None