Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
node_modules
pnpm-lock.yaml
public/full-documentation.txt
public/pages
.specstory/
# SpecStory explanation file
.specstory/.what-is-this.md
Expand Down
55 changes: 37 additions & 18 deletions components/copy-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const router = useRouter();

// Close dropdown when clicking outside
useEffect(() => {
Expand All @@ -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);
Expand All @@ -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 = {
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
263 changes: 263 additions & 0 deletions scripts/process-mdx-to-md.js
Original file line number Diff line number Diff line change
@@ -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(
/<CustomCard([^>]*)\/>/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(
/<Card([^>]*)\/>/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\s+href="([^"]*)"[^>]*>([^<]*)<\/a>/g,
(match, href, text) => {
const absoluteUrl = makeAbsoluteUrl(href);
return `[${text}](${absoluteUrl})`;
}
);

// Convert Callout components to markdown blockquotes
processed = processed.replace(
/<Callout([^>]*)>([\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(
/<Tabs[^>]*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 = /<Tabs\.Tab[^>]*>([\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(/<Tabs[^>]*>/g, '');
processed = processed.replace(/<\/Tabs>/g, '');
processed = processed.replace(/<Tabs\.Tab[^>]*>/g, '');
processed = processed.replace(/<\/Tabs\.Tab>/g, '');

// Strip Cards container (individual <Card/> handled above)
processed = processed.replace(/<Cards[^>]*>/g, '');
processed = processed.replace(/<\/Cards>/g, '');

// Strip Bleed wrapper
processed = processed.replace(/<Bleed[^>]*>/g, '');
processed = processed.replace(/<\/Bleed>/g, '');

// Strip Fragment wrapper
processed = processed.replace(/<Fragment[^>]*>/g, '');
processed = processed.replace(/<\/Fragment>/g, '');

// Convert simple HTML divs to text (remove div tags but keep content)
processed = processed.replace(/<div[^>]*>([\s\S]*?)<\/div>/g, '$1');

// Convert <br/> tags to line breaks
processed = processed.replace(/<br\s*\/?>(?!\s*<\/)/g, '\n');

// Convert Image components to markdown images (with alt) - leave src as-is to avoid double-encoding
processed = processed.replace(
/<Image[^>]*\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(
/<Image[^>]*\s+src="([^"]*)"[^>]*\/?>(?!\s*<\/Image>)/g,
(match, src) => {
return `![Image](${src})`;
}
);

// Convert regular <img> tags to markdown images - leave src as-is
processed = processed.replace(
/<img[^>]*\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();