diff --git a/.gitignore b/.gitignore index eaa0af3b..645e7bae 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules pnpm-lock.yaml public/full-documentation.txt +public/pages .specstory/ # SpecStory explanation file .specstory/.what-is-this.md diff --git a/components/copy-page.tsx b/components/copy-page.tsx index a2a613ed..e9b148d7 100644 --- a/components/copy-page.tsx +++ b/components/copy-page.tsx @@ -11,7 +11,9 @@ import AnthropicIcon from './icons/anthropic'; const CopyPage: React.FC = () => { const [isOpen, setIsOpen] = useState(false); const [isCopied, setIsCopied] = useState(false); + const [prefetchedContent, setPrefetchedContent] = useState(null); const dropdownRef = useRef(null); + const router = useRouter(); // Close dropdown when clicking outside useEffect(() => { @@ -25,16 +27,31 @@ const CopyPage: React.FC = () => { return () => document.removeEventListener('mousedown', handleClickOutside); }, []); + // Prefetch markdown content when component mounts + // this is needed to avoid a security issue on safari where you can't fetch and paste in the clipboard + useEffect(() => { + const prefetchContent = async () => { + try { + const currentPath = router.asPath; + const cleanPath = currentPath.split('?')[0].split('#')[0]; + const mdUrl = cleanPath === '/' ? '/pages/index.md' : `/pages${cleanPath}.md`; + + const response = await fetch(mdUrl); + if (response.ok) { + const content = await response.text(); + setPrefetchedContent(content); + } + } catch (error) { + console.log('Prefetch failed, will use fallback:', error); + } + }; + + prefetchContent(); + }, [router.asPath]); + const copyPageAsMarkdown = async () => { try { - // Get the current page content - const pageContent = document.querySelector('main')?.innerText || ''; - const pageTitle = document.title; - - // Create markdown content - const markdownContent = `# ${pageTitle}\n\n${pageContent}`; - - await navigator.clipboard.writeText(markdownContent); + await navigator.clipboard.writeText(prefetchedContent || ''); // Show success feedback setIsCopied(true); @@ -48,21 +65,23 @@ const CopyPage: React.FC = () => { }; const viewAsMarkdown = () => { - const pageContent = document.querySelector('main')?.innerText || ''; - const pageTitle = document.title; - const markdownContent = `# ${pageTitle}\n\n${pageContent}`; + const currentPath = router.asPath; + + const cleanPath = currentPath.split('?')[0].split('#')[0]; - // Open in new window/tab - const blob = new Blob([markdownContent], { type: 'text/markdown' }); - const url = URL.createObjectURL(blob); - window.open(url, '_blank'); - URL.revokeObjectURL(url); + const mdUrl = cleanPath === '/' ? '/pages/index.md' : `/pages${cleanPath}.md`; + window.open(mdUrl, '_blank'); setIsOpen(false); }; const openInAI = (platform: 'chatgpt' | 'claude') => { - const currentUrl = window.location.href; - const prompt = `I'm building with GenLayer - can you read this docs page ${currentUrl} so I can ask you questions about it?`; + const currentPath = router.asPath; + const cleanPath = currentPath.split('?')[0].split('#')[0]; + + const mdUrl = cleanPath === '/' ? '/pages/index.md' : `/pages${cleanPath}.md`; + const fullMdUrl = `${window.location.origin}${mdUrl}`; + + const prompt = `I'm building with GenLayer - can you read this markdown file ${fullMdUrl} so I can ask you questions about it?`; const encodedPrompt = encodeURIComponent(prompt); const urls = { diff --git a/package.json b/package.json index 75b3c576..b19d92fe 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,15 @@ "version": "0.0.1", "description": "GenLayer documentation", "scripts": { - "dev": "npm run node-generate-changelog && npm run node-update-setup-guide && npm run node-update-config && npm run node-generate-api-docs && node scripts/generate-full-docs.js && next dev", - "build": "npm run node-generate-changelog && npm run node-update-setup-guide && npm run node-update-config && npm run node-generate-api-docs && node scripts/generate-full-docs.js && next build", + "dev": "npm run node-generate-changelog && npm run node-update-setup-guide && npm run node-update-config && npm run node-generate-api-docs && node scripts/generate-full-docs.js && npm run sync-mdx && next dev", + "build": "npm run node-generate-changelog && npm run node-update-setup-guide && npm run node-update-config && npm run node-generate-api-docs && node scripts/generate-full-docs.js && npm run sync-mdx && next build", "start": "next start", "generate-sitemap": "node scripts/generate-sitemap-xml.js", "node-generate-changelog": "node scripts/generate-changelog.js", "node-generate-api-docs": "node scripts/generate-api-docs.js", "node-update-setup-guide": "node scripts/update-setup-guide-versions.js", - "node-update-config": "node scripts/update-config-in-setup-guide.js" + "node-update-config": "node scripts/update-config-in-setup-guide.js", + "sync-mdx": "node scripts/process-mdx-to-md.js" }, "repository": { "type": "git", diff --git a/scripts/process-mdx-to-md.js b/scripts/process-mdx-to-md.js new file mode 100644 index 00000000..079e0c8b --- /dev/null +++ b/scripts/process-mdx-to-md.js @@ -0,0 +1,263 @@ +const fs = require('fs'); +const path = require('path'); + +function stripTopLevelImports(input) { + const lines = input.split('\n'); + let inFence = false; + return lines + .map((line) => { + const trimmed = line.trim(); + if (trimmed.startsWith('```')) { + inFence = !inFence; + return line; + } + if (!inFence && /^import\s+.*$/.test(line)) { + return ''; + } + return line; + }) + .join('\n'); +} + +function processMdxToMarkdown(content) { + const baseUrl = 'https://docs.genlayer.com'; + + // Helper function to convert relative URLs to absolute + function makeAbsoluteUrl(url) { + // Keep anchors as-is + if (url.startsWith('#')) return url; + + // Absolute URLs + if (url.startsWith('http://') || url.startsWith('https://')) { + return encodeURI(url); + } + + // Relative to root + if (url.startsWith('/')) { + return encodeURI(baseUrl + url); + } + + // Other relative paths + return encodeURI(url); + } + + let processed = content; + + // Remove top-level MDX import statements (skip fenced code blocks) + processed = stripTopLevelImports(processed); + + // Convert CustomCard components to markdown links + processed = processed.replace( + /]*)\/>/g, + (match, attrs) => { + // Extract attributes regardless of order + const titleMatch = attrs.match(/title="([^"]*)"/); + const descMatch = attrs.match(/description="([^"]*)"/); + const hrefMatch = attrs.match(/href="([^"]*)"/); + + if (titleMatch && hrefMatch) { + const title = titleMatch[1]; + const description = descMatch ? descMatch[1] : ''; + const absoluteUrl = makeAbsoluteUrl(hrefMatch[1]); + return description + ? `- **[${title}](${absoluteUrl})**: ${description}` + : `- **[${title}](${absoluteUrl})**`; + } + return match; // Return unchanged if required attributes are missing + } + ); + + // Convert Card components to markdown links + processed = processed.replace( + /]*)\/>/g, + (match, attrs) => { + // Extract attributes regardless of order + const titleMatch = attrs.match(/title="([^"]*)"/); + const hrefMatch = attrs.match(/href="([^"]*)"/); + + if (titleMatch && hrefMatch) { + const title = titleMatch[1]; + const absoluteUrl = makeAbsoluteUrl(hrefMatch[1]); + return `- **[${title}](${absoluteUrl})**`; + } + return match; // Return unchanged if required attributes are missing + } + ); + + // Convert simple JSX links to markdown + processed = processed.replace( + /]*>([^<]*)<\/a>/g, + (match, href, text) => { + const absoluteUrl = makeAbsoluteUrl(href); + return `[${text}](${absoluteUrl})`; + } + ); + + // Convert Callout components to markdown blockquotes + processed = processed.replace( + /]*)>([\s\S]*?)<\/Callout>/g, + (match, attrs, content) => { + // Extract attributes regardless of order + const typeMatch = attrs.match(/type="([^"]*)"/); + const type = typeMatch ? typeMatch[1] : ''; + const cleanContent = content.trim(); + const prefix = type === 'warning' ? '⚠️ ' : type === 'info' ? 'ℹ️ ' : ''; + return `> ${prefix}${cleanContent}`; + } + ); + + // Convert Tabs with labeled items into headings with their respective content + processed = processed.replace( + /]*items=\{\[([\s\S]*?)\]\}[^>]*>([\s\S]*?)<\/Tabs>/g, + (match, itemsRaw, inner) => { + const tabTitles = itemsRaw + .split(',') + .map(s => s.trim()) + .map(s => s.replace(/^["']|["']$/g, '')) + .filter(Boolean); + + const tabContents = []; + const tabRegex = /]*>([\s\S]*?)<\/Tabs\.Tab>/g; + let m; + while ((m = tabRegex.exec(inner)) !== null) { + tabContents.push(m[1].trim()); + } + + // Map titles to contents; if counts mismatch, best-effort pairing + const sections = []; + const count = Math.max(tabTitles.length, tabContents.length); + for (let i = 0; i < count; i++) { + const title = tabTitles[i] || `Tab ${i + 1}`; + const content = (tabContents[i] || '').trim(); + if (content) { + sections.push(`### ${title}\n\n${content}`); + } else { + sections.push(`### ${title}`); + } + } + return sections.join('\n\n'); + } + ); + + // Fallback: strip any remaining Tabs wrappers (keep inner content) + processed = processed.replace(/]*>/g, ''); + processed = processed.replace(/<\/Tabs>/g, ''); + processed = processed.replace(/]*>/g, ''); + processed = processed.replace(/<\/Tabs\.Tab>/g, ''); + + // Strip Cards container (individual handled above) + processed = processed.replace(/]*>/g, ''); + processed = processed.replace(/<\/Cards>/g, ''); + + // Strip Bleed wrapper + processed = processed.replace(/]*>/g, ''); + processed = processed.replace(/<\/Bleed>/g, ''); + + // Strip Fragment wrapper + processed = processed.replace(/]*>/g, ''); + processed = processed.replace(/<\/Fragment>/g, ''); + + // Convert simple HTML divs to text (remove div tags but keep content) + processed = processed.replace(/]*>([\s\S]*?)<\/div>/g, '$1'); + + // Convert
tags to line breaks + processed = processed.replace(/(?!\s*<\/)/g, '\n'); + + // Convert Image components to markdown images (with alt) - leave src as-is to avoid double-encoding + processed = processed.replace( + /]*\s+src="([^"]*)"[^>]*\s+alt="([^"]*)"[^>]*\/?>(?!\s*<\/Image>)/g, + (match, src, alt) => { + return `![${alt}](${src})`; + } + ); + + // Convert Image components to markdown images (without alt) - leave src as-is + processed = processed.replace( + /]*\s+src="([^"]*)"[^>]*\/?>(?!\s*<\/Image>)/g, + (match, src) => { + return `![Image](${src})`; + } + ); + + // Convert regular tags to markdown images - leave src as-is + processed = processed.replace( + /]*\s+src="([^"]*)"[^>]*\s+alt="([^"]*)"[^>]*\/?>(?!\s*<\/img>)/g, + (match, src, alt) => { + return `![${alt}](${src})`; + } + ); + + // Convert regular markdown images to absolute URLs + processed = processed.replace( + /!\[([^\]]*)\]\(([^)]*)\)/g, + (match, alt, src) => { + const absoluteUrl = makeAbsoluteUrl(src); + return `![${alt}](${absoluteUrl})`; + } + ); + + // Convert regular markdown links to absolute URLs (skip images) + processed = processed.replace( + /(^|[^!])\[([^\]]*)\]\(([^)]*)\)/gm, + (match, prefix, text, href) => { + const absoluteUrl = makeAbsoluteUrl(href); + return `${prefix}[${text}](${absoluteUrl})`; + } + ); + + // Remove empty lines created by import removal and clean up + processed = processed + .split('\n') + .filter((line, index, array) => { + // Remove empty lines at the start + if (index === 0 && line.trim() === '') return false; + // Remove multiple consecutive empty lines + if (line.trim() === '' && array[index - 1] && array[index - 1].trim() === '') return false; + return true; + }) + .join('\n') + .trim(); + + // Normalize list indentation (remove unintended leading spaces before list markers) + processed = processed.replace(/^[ \t]+- /gm, '- '); + + return processed; +} + +function processAllMdxFiles() { + const pagesDir = path.join(process.cwd(), 'pages'); + const publicPagesDir = path.join(process.cwd(), 'public', 'pages'); + + // Create public/pages directory if it doesn't exist + if (!fs.existsSync(publicPagesDir)) { + fs.mkdirSync(publicPagesDir, { recursive: true }); + } + + function processDirectory(sourceDir, targetDir) { + const items = fs.readdirSync(sourceDir); + + items.forEach(item => { + const sourcePath = path.join(sourceDir, item); + const stat = fs.statSync(sourcePath); + + if (stat.isDirectory()) { + const newTargetDir = path.join(targetDir, item); + if (!fs.existsSync(newTargetDir)) { + fs.mkdirSync(newTargetDir, { recursive: true }); + } + processDirectory(sourcePath, newTargetDir); + } else if (item.endsWith('.mdx')) { + const content = fs.readFileSync(sourcePath, 'utf8'); + const processedContent = processMdxToMarkdown(content); + + const targetPath = path.join(targetDir, item.replace('.mdx', '.md')); + fs.writeFileSync(targetPath, processedContent); + } + }); + } + + processDirectory(pagesDir, publicPagesDir); + console.log('✅ Processed all MDX files to clean Markdown'); +} + +processAllMdxFiles();