Преглед изворни кода

docs(feat): Add copy as md button to doc pages (#3738)

Co-authored-by: Housein Abo Shaar <76689341+GogoIsProgramming@users.noreply.github.com>
Housein Abo Shaar пре 5 месеци
родитељ
комит
2c2b0c3a96

+ 183 - 0
docs/src/components/CopyMarkdownButton/index.js

@@ -0,0 +1,183 @@
+import IconCopy from '@theme/Icon/Copy';
+import IconSuccess from '@theme/Icon/Success';
+import React, { useState } from 'react';
+import styles from './styles.module.css';
+
+export default function CopyMarkdownButton() {
+    const [copied, setCopied] = useState(false);
+
+    const handleCopy = async () => {
+        try {
+            // Try multiple selectors to find the content area
+            const selectors = [
+                'article[class*="docItemContainer"]',
+                '.theme-doc-markdown',
+                'main[class*="docMainContainer"] .theme-doc-markdown',
+                'article',
+                '.markdown',
+                '[class*="docItemContainer"]',
+            ];
+
+            let contentElement = null;
+            for (const selector of selectors) {
+                contentElement = document.querySelector(selector);
+                if (contentElement) {
+                    break;
+                }
+            }
+
+            if (contentElement) {
+                // Extract content as markdown with proper formatting
+                let content = extractMarkdownContent(contentElement);
+
+                // Ensure proper spacing after H1 headers
+                content = content.replace(/(# [^\n]+)/g, '$1\n');
+
+                // Copy to clipboard using modern API
+                await navigator.clipboard.writeText(content);
+                setCopied(true);
+                setTimeout(() => setCopied(false), 2000);
+            } else {
+                alert('Could not find page content to copy.');
+            }
+        } catch (error) {
+            alert('Failed to copy content. Please try again.');
+        }
+    };
+
+    const extractMarkdownContent = element => {
+        let markdown = '';
+
+        // Process elements in DOM order
+        const walker = document.createTreeWalker(element, NodeFilter.SHOW_ELEMENT, {
+            acceptNode: function (node) {
+                const tagName = node.tagName.toLowerCase();
+                if (
+                    ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'ul', 'ol', 'pre', 'blockquote', 'a'].includes(
+                        tagName,
+                    )
+                ) {
+                    return NodeFilter.FILTER_ACCEPT;
+                }
+                return NodeFilter.FILTER_SKIP;
+            },
+        });
+
+        let currentNode;
+        while ((currentNode = walker.nextNode())) {
+            const tagName = currentNode.tagName.toLowerCase();
+            const textContent = currentNode.textContent.trim();
+
+            switch (tagName) {
+                case 'h1':
+                    markdown += `\n# ${textContent}\n\n`;
+                    break;
+                case 'h2':
+                    markdown += `\n## ${textContent}\n\n`;
+                    break;
+                case 'h3':
+                    markdown += `\n### ${textContent}\n\n`;
+                    break;
+                case 'h4':
+                    markdown += `\n#### ${textContent}\n\n`;
+                    break;
+                case 'h5':
+                    markdown += `\n##### ${textContent}\n\n`;
+                    break;
+                case 'h6':
+                    markdown += `\n###### ${textContent}\n\n`;
+                    break;
+                case 'p':
+                    if (textContent) {
+                        markdown += `${textContent}\n\n`;
+                    }
+                    break;
+                case 'ul':
+                case 'ol':
+                    markdown += processList(currentNode);
+                    break;
+                case 'pre':
+                    const codeElement = currentNode.querySelector('code');
+                    const codeText = codeElement ? codeElement.textContent : textContent;
+                    const language = getCodeLanguage(codeElement);
+                    markdown += `\n\`\`\`${language}\n${codeText}\n\`\`\`\n\n`;
+                    break;
+                case 'blockquote':
+                    markdown += `> ${textContent}\n\n`;
+                    break;
+                case 'a':
+                    const href = currentNode.getAttribute('href');
+                    const linkText = textContent;
+                    if (href && linkText) {
+                        markdown += `[${linkText}](${href})`;
+                    } else if (linkText) {
+                        markdown += linkText;
+                    }
+                    break;
+            }
+        }
+
+        return markdown.replace(/\n{3,}/g, '\n\n').trim();
+    };
+
+    const processList = listElement => {
+        let result = '';
+        const items = listElement.querySelectorAll('li');
+        items.forEach(item => {
+            result += `- ${item.textContent.trim()}\n`;
+        });
+        return result + '\n';
+    };
+
+    const processTable = tableElement => {
+        let result = '';
+        const rows = tableElement.querySelectorAll('tr');
+
+        rows.forEach((row, index) => {
+            if (index === 1) {
+                // Add separator row after header
+                const cells = row.querySelectorAll('th, td');
+                result += '|';
+                cells.forEach(() => {
+                    result += ' --- |';
+                });
+                result += '\n';
+            }
+            result += processTableRow(row);
+        });
+
+        return result + '\n';
+    };
+
+    const processTableRow = rowElement => {
+        let result = '|';
+        const cells = rowElement.querySelectorAll('th, td');
+        cells.forEach(cell => {
+            result += ` ${cell.textContent.trim()} |`;
+        });
+        return result + '\n';
+    };
+
+    const getCodeLanguage = codeElement => {
+        if (!codeElement) return '';
+        const className = codeElement.className || '';
+        const match = className.match(/language-(\w+)/);
+        return match ? match[1] : '';
+    };
+
+    return (
+        <button className={styles.copyButton} onClick={handleCopy} title="Copy page content as markdown">
+            {copied ? (
+                <>
+                    <IconSuccess className={styles.icon} />
+                    <span className={styles.tooltip}>Copied!</span>
+                </>
+            ) : (
+                <>
+                    <IconCopy className={styles.icon} />
+                    <span className={styles.tooltip}>Copy as MD</span>
+                </>
+            )}
+        </button>
+    );
+}

+ 60 - 0
docs/src/components/CopyMarkdownButton/styles.module.css

@@ -0,0 +1,60 @@
+.copyButton {
+    background: var(--ifm-color-primary);
+    color: white;
+    border: none;
+    border-radius: 6px;
+    padding: 6px 10px;
+    margin-bottom: 12px;
+    font-size: 11px;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+    transition: all 0.2s ease;
+    font-family: var(--ifm-font-family-base);
+    white-space: nowrap;
+}
+
+.copyButton:hover {
+    background: var(--ifm-color-primary-dark);
+    transform: translateY(-1px);
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+}
+
+.copyButton:active {
+    transform: translateY(0);
+}
+
+.tooltip {
+    font-size: 12px;
+    font-weight: 500;
+    white-space: nowrap;
+}
+
+.icon {
+    width: 14px;
+    height: 14px;
+}
+
+/* Dark mode adjustments */
+html[data-theme='dark'] .copyButton {
+    background: var(--ifm-color-primary);
+    color: var(--ifm-color-white);
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
+}
+
+html[data-theme='dark'] .copyButton:hover {
+    background: var(--ifm-color-primary-dark);
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
+}
+
+/* Mobile responsiveness */
+@media (max-width: 768px) {
+    .copyButton {
+        top: 70px;
+        right: 10px;
+        padding: 6px 10px;
+        font-size: 11px;
+    }
+}

+ 13 - 0
docs/src/theme/DocBreadcrumbs/index.js

@@ -0,0 +1,13 @@
+import React from 'react';
+import DocBreadcrumbs from '@theme-original/DocBreadcrumbs';
+import CopyMarkdownButton from '@site/src/components/CopyMarkdownButton';
+import styles from './styles.module.css';
+
+export default function DocBreadcrumbsWrapper(props) {
+  return (
+    <div className={styles.breadcrumbsContainer}>
+      <DocBreadcrumbs {...props} />
+      <CopyMarkdownButton />
+    </div>
+  );
+}

+ 6 - 0
docs/src/theme/DocBreadcrumbs/styles.module.css

@@ -0,0 +1,6 @@
+.breadcrumbsContainer {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 1rem;
+}

+ 10 - 0
docs/src/theme/DocItem/Content/index.js

@@ -0,0 +1,10 @@
+import React from 'react';
+import Content from '@theme-original/DocItem/Content';
+
+export default function ContentWrapper(props) {
+  return (
+    <>
+      <Content {...props} />
+    </>
+  );
+}

+ 11 - 0
docs/src/theme/DocItem/Layout/index.js

@@ -0,0 +1,11 @@
+import React from 'react';
+import Layout from '@theme-original/DocItem/Layout';
+import CopyMarkdownButton from '@site/src/components/CopyMarkdownButton';
+
+export default function LayoutWrapper(props) {
+  return (
+    <>
+      <Layout {...props} />
+    </>
+  );
+}