diff --git a/docusaurus.config.ts b/docusaurus.config.ts index cd70d93..9b0df79 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -26,8 +26,7 @@ const config: Config = { [ 'docusaurus-plugin-copy-page-button', { - placement: 'article', - enabledActions: ['copy'], + injectButton: false, generateMarkdownRoutes: true, }, ], diff --git a/i18n/en/code.json b/i18n/en/code.json new file mode 100644 index 0000000..bb90250 --- /dev/null +++ b/i18n/en/code.json @@ -0,0 +1,14 @@ +{ + "copyPageButton.idle": { + "message": "Copy page as Markdown" + }, + "copyPageButton.copying": { + "message": "Copying…" + }, + "copyPageButton.success": { + "message": "Copied" + }, + "copyPageButton.error": { + "message": "Copy failed" + } +} diff --git a/scripts/generate-llms-index.mjs b/scripts/generate-llms-index.mjs index 8fa3e8e..a6e640e 100644 --- a/scripts/generate-llms-index.mjs +++ b/scripts/generate-llms-index.mjs @@ -66,13 +66,16 @@ const allMarkdownFiles = await findMarkdownFiles(outputDirectory); const documentationChecks = await Promise.all( allMarkdownFiles.map((file) => isDocumentationPage(file)), ); +const documentationFiles = allMarkdownFiles.filter( + (_file, index) => documentationChecks[index], +); const defaultLocaleFiles = allMarkdownFiles.filter( (file, index) => !isExcludedLocale(path.relative(outputDirectory, file)) && documentationChecks[index], ); const excludedMarkdownFiles = allMarkdownFiles.filter( - (file) => !defaultLocaleFiles.includes(file), + (file) => !documentationFiles.includes(file), ); await Promise.all(excludedMarkdownFiles.map((file) => unlink(file))); @@ -114,5 +117,5 @@ const llmsIndex = [ await writeFile(path.join(outputDirectory, 'llms.txt'), llmsIndex, 'utf8'); console.log( - `Generated llms.txt with ${documents.length} Chinese documents; removed ${excludedMarkdownFiles.length} non-Chinese or non-document Markdown routes.`, + `Generated llms.txt with ${documents.length} Chinese documents; preserved ${documentationFiles.length} localized Markdown routes and removed ${excludedMarkdownFiles.length} non-document routes.`, ); diff --git a/src/components/CopyPageButton/index.tsx b/src/components/CopyPageButton/index.tsx new file mode 100644 index 0000000..a5b5212 --- /dev/null +++ b/src/components/CopyPageButton/index.tsx @@ -0,0 +1,117 @@ +import {translate} from '@docusaurus/Translate'; +import {useEffect, useRef, useState} from 'react'; +import type {ReactNode} from 'react'; + +import styles from './styles.module.css'; + +type CopyStatus = 'idle' | 'copying' | 'success' | 'error'; + +const labels: Record string> = { + idle: () => + translate({ + id: 'copyPageButton.idle', + message: '复制整篇 Markdown', + }), + copying: () => + translate({ + id: 'copyPageButton.copying', + message: '正在复制…', + }), + success: () => + translate({ + id: 'copyPageButton.success', + message: '已复制', + }), + error: () => + translate({ + id: 'copyPageButton.error', + message: '复制失败', + }), +}; + +function getMarkdownUrl(): string { + const url = new URL(window.location.href); + url.hash = ''; + url.search = ''; + url.pathname = + url.pathname === '/' + ? '/index.md' + : `${url.pathname.replace(/\/$/, '')}.md`; + return url.toString(); +} + +async function writeToClipboard(markdown: string): Promise { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(markdown); + return; + } + + const textArea = document.createElement('textarea'); + textArea.value = markdown; + textArea.setAttribute('readonly', ''); + textArea.style.position = 'fixed'; + textArea.style.opacity = '0'; + document.body.appendChild(textArea); + textArea.select(); + + try { + if (!document.execCommand('copy')) { + throw new Error('Clipboard copy command failed'); + } + } finally { + textArea.remove(); + } +} + +export default function CopyPageButton(): ReactNode { + const [status, setStatus] = useState('idle'); + const resetTimer = useRef | undefined>( + undefined, + ); + + useEffect( + () => () => { + if (resetTimer.current) { + clearTimeout(resetTimer.current); + } + }, + [], + ); + + const copyPage = async () => { + setStatus('copying'); + + try { + const response = await fetch(getMarkdownUrl()); + if (!response.ok) { + throw new Error(`Markdown request failed with ${response.status}`); + } + await writeToClipboard(await response.text()); + setStatus('success'); + } catch { + setStatus('error'); + } + + if (resetTimer.current) { + clearTimeout(resetTimer.current); + } + resetTimer.current = setTimeout(() => setStatus('idle'), 2000); + }; + + const isCopying = status === 'copying'; + + return ( +
+ +
+ ); +} diff --git a/src/components/CopyPageButton/styles.module.css b/src/components/CopyPageButton/styles.module.css new file mode 100644 index 0000000..8a8a2c7 --- /dev/null +++ b/src/components/CopyPageButton/styles.module.css @@ -0,0 +1,57 @@ +.container { + display: flex; + justify-content: flex-end; + margin: 0.75rem 0 1rem; +} + +.button { + align-items: center; + background: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 0.45rem; + color: var(--ifm-font-color-base); + cursor: pointer; + display: inline-flex; + font: inherit; + font-size: 0.875rem; + font-weight: 600; + gap: 0.45rem; + min-height: 2.25rem; + padding: 0.4rem 0.75rem; + transition: + background-color var(--ifm-transition-fast), + border-color var(--ifm-transition-fast), + color var(--ifm-transition-fast); +} + +.button:hover:not(:disabled) { + background: var(--ifm-color-emphasis-100); + border-color: var(--ifm-color-primary); + color: var(--ifm-color-primary); +} + +.button:focus-visible { + outline: 2px solid var(--ifm-color-primary); + outline-offset: 2px; +} + +.button:disabled { + cursor: wait; + opacity: 0.7; +} + +.icon { + font-size: 1rem; + line-height: 1; +} + +@media (max-width: 576px) { + .container { + justify-content: stretch; + } + + .button { + justify-content: center; + width: 100%; + } +} diff --git a/src/theme/DocItem/Content/index.tsx b/src/theme/DocItem/Content/index.tsx new file mode 100644 index 0000000..a32f92e --- /dev/null +++ b/src/theme/DocItem/Content/index.tsx @@ -0,0 +1,13 @@ +import type {ReactNode} from 'react'; +import CopyPageButton from '@site/src/components/CopyPageButton'; +import OriginalDocItemContent from '@theme-original/DocItem/Content'; +import type {Props} from '@theme/DocItem/Content'; + +export default function DocItemContent(props: Props): ReactNode { + return ( + <> + + + + ); +}