diff --git a/src/components/copy-as-markdown.tsx b/src/components/copy-as-markdown.tsx new file mode 100644 index 0000000..27e732c --- /dev/null +++ b/src/components/copy-as-markdown.tsx @@ -0,0 +1,115 @@ +'use client' + +import { useRouter } from 'next/router' +import { useCallback, useEffect, useRef, useState } from 'react' +import { useConfig } from 'nextra-theme-docs' + +const REPO_RE = /github\.com\/([^/]+)\/([^/]+)(?:\/tree\/([^/]+))?/ +const DEFAULT_REPO = 'https://github.com/storacha/docs/tree/main' +const RESET_MS = 2000 + +type Status = 'idle' | 'loading' | 'copied' | 'error' + +const LABELS: Record = { + idle: 'Copy as Markdown', + loading: 'Loading...', + copied: 'Copied', + error: 'Failed', +} + +function getRawBase(repo: string) { + const m = repo.match(REPO_RE) + return m + ? `https://raw.githubusercontent.com/${m[1]}/${m[2]}/${m[3] || 'main'}/src/pages` + : null +} + +function candidateUrls(path: string, base: string) { + const p = path.split('?')[0].replace(/\/+$/, '') || '/index' + return p === '/index' + ? [`${base}/index.mdx`, `${base}/index.md`] + : [`${base}${p}.mdx`, `${base}${p}.md`, `${base}${p}/index.mdx`, `${base}${p}/index.md`] +} + +async function fetchMarkdown(path: string, base: string, signal?: AbortSignal) { + const results = await Promise.allSettled( + candidateUrls(path, base).map((url) => fetch(url, { signal }).then((r) => (r.ok ? r.text() : Promise.reject()))) + ) + const hit = results.find((r): r is PromiseFulfilledResult => r.status === 'fulfilled') + if (!hit) throw new Error('Not found') + return hit.value +} + +function strip(raw: string) { + return raw + .replace(/^---[\s\S]*?---\s*\n?/, '') + .replace(/^import\s[\s\S]*?from\s+['"][^'"]*['"];?\s*$/gm, '') + .replace(/^export\s+\w+\s+.*$/gm, '') + .trim() +} + +const iconClass = 'nx-w-4 nx-h-4 nx-shrink-0' + +const CopyIcon = () => ( + + + +) + +const CheckIcon = () => ( + + + +) + +export function CopyAsMarkdown() { + const router = useRouter() + const { docsRepositoryBase } = useConfig() + const [status, setStatus] = useState('idle') + const timerRef = useRef>() + const abortRef = useRef() + + useEffect(() => { + return () => { + clearTimeout(timerRef.current) + abortRef.current?.abort() + } + }, []) + + const resetAfterDelay = useCallback((s: Status) => { + setStatus(s) + clearTimeout(timerRef.current) + timerRef.current = setTimeout(() => setStatus('idle'), RESET_MS) + }, []) + + const onClick = useCallback(async () => { + const base = getRawBase(docsRepositoryBase || DEFAULT_REPO) + if (!base) return resetAfterDelay('error') + + abortRef.current?.abort() + const ac = new AbortController() + abortRef.current = ac + + setStatus('loading') + try { + const raw = await fetchMarkdown(router.asPath, base, ac.signal) + await navigator.clipboard.writeText(strip(raw)) + resetAfterDelay('copied') + } catch { + if (!ac.signal.aborted) resetAfterDelay('error') + } + }, [router.asPath, docsRepositoryBase, resetAfterDelay]) + + return ( + + ) +} diff --git a/src/theme.config.jsx b/src/theme.config.jsx index c7191c9..d23c39d 100644 --- a/src/theme.config.jsx +++ b/src/theme.config.jsx @@ -1,5 +1,6 @@ import { useConfig } from 'nextra-theme-docs' import { DocsLogo } from './components/brand' +import { CopyAsMarkdown } from './components/copy-as-markdown' /** * @type {import('nextra-theme-docs').DocsThemeConfig} @@ -15,6 +16,12 @@ const config = { link: 'https://github.com/storacha/upload-service' }, docsRepositoryBase: 'https://github.com/storacha/docs/tree/main', + main: ({ children }) => ( + <> + {children} + + + ), footer: { component: },