diff --git a/documentation/docusaurus.config.ts b/documentation/docusaurus.config.ts index 7520d8e91ad5..c6594ce4eea5 100644 --- a/documentation/docusaurus.config.ts +++ b/documentation/docusaurus.config.ts @@ -29,7 +29,12 @@ const config: Config = { projectName: "goose", // Usually your repo name. onBrokenLinks: "throw", - onBrokenMarkdownLinks: "warn", + + markdown: { + hooks: { + onBrokenMarkdownLinks: "warn", + }, + }, // Even if you don't use internationalization, you can use this field to set // useful metadata like html lang. For example, if your site is Chinese, you @@ -315,6 +320,12 @@ const config: Config = { }, ], tailwindPlugin, + [ + require.resolve("./plugins/markdown-export.cjs"), + { + enabled: true, + }, + ], ], themes: ["@inkeep/docusaurus/chatButton", "@inkeep/docusaurus/searchBar"], themeConfig: { diff --git a/documentation/package.json b/documentation/package.json index 123c9ba67282..bee21ef60665 100644 --- a/documentation/package.json +++ b/documentation/package.json @@ -13,7 +13,8 @@ "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", "typecheck": "tsc", - "generate-detail-pages": "node scripts/generate-detail-pages.js" + "generate-detail-pages": "node scripts/generate-detail-pages.js", + "serve-static": "node scripts/serve-static.js" }, "dependencies": { "@docusaurus/core": "^3.9.2", diff --git a/documentation/plugins/markdown-export.cjs b/documentation/plugins/markdown-export.cjs new file mode 100644 index 000000000000..29dcb9970a8d --- /dev/null +++ b/documentation/plugins/markdown-export.cjs @@ -0,0 +1,61 @@ +const fs = require('fs'); +const path = require('path'); +const globby = require('globby'); + +module.exports = function markdownExportPlugin(context, options) { + const pluginOptions = { + enabled: true, + ...options, + }; + + return { + name: 'markdown-export', + + async postBuild({ outDir }) { + if (!pluginOptions.enabled) { + return; + } + + console.log('[markdown-export] Starting markdown export...'); + + const docsDir = path.join(context.siteDir, 'docs'); + const outputDir = path.join(outDir, 'docs'); + + // Get all markdown files + const files = await globby('**/*.{md,mdx}', { cwd: docsDir }); + + for (const file of files) { + const inputPath = path.join(docsDir, file); + const outputPath = path.join(outputDir, file.replace('.mdx', '.md')); + + // Ensure output subdirectory exists + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + + const content = fs.readFileSync(inputPath, 'utf-8'); + + // Strip frontmatter and clean up + const cleaned = stripFrontmatter(content); + + // Write the cleaned markdown alongside HTML files + fs.writeFileSync(outputPath, cleaned); + } + + console.log(`[markdown-export] Successfully exported ${files.length} markdown files to ${outputDir}`); + }, + }; +}; + +function stripFrontmatter(content) { + // Remove YAML frontmatter (everything between --- at the start) + const withoutFrontmatter = content.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, ''); + + // Clean up any remaining import statements (for .mdx files) + const withoutImports = withoutFrontmatter.replace(/^import .+$/gm, ''); + + // Remove excessive empty lines and trim + return withoutImports + .replace(/\n{3,}/g, '\n\n') + .trim(); +} + + diff --git a/documentation/scripts/serve-static.js b/documentation/scripts/serve-static.js new file mode 100644 index 000000000000..ced17b3a72f4 --- /dev/null +++ b/documentation/scripts/serve-static.js @@ -0,0 +1,51 @@ +#!/usr/bin/env node + +/** + * Simple static file server for testing markdown exports locally. + * Unlike `docusaurus serve`, this serves files as-is without routing logic. + */ + +const http = require('http'); +const serveStatic = require('serve-static'); +const path = require('path'); + +const buildDir = path.join(__dirname, '..', 'build'); +const port = process.env.PORT || 3001; + +const serve = serveStatic(buildDir, { + index: ['index.html'], + setHeaders: (res, filePath) => { + // Set proper content type for markdown files + if (filePath.endsWith('.md')) { + res.setHeader('Content-Type', 'text/plain; charset=utf-8'); + } + } +}); + +const server = http.createServer((req, res) => { + // Handle requests to /goose/ by serving from the build directory + if (req.url.startsWith('/goose/')) { + // Strip /goose/ prefix and serve the file + req.url = req.url.substring(6); // Remove '/goose' + serve(req, res, () => { + res.statusCode = 404; + res.end('Not found'); + }); + } else if (req.url === '/') { + // Redirect root to /goose/ + res.writeHead(302, { Location: '/goose/' }); + res.end(); + } else { + // For any other path, return 404 + res.statusCode = 404; + res.end('Not found - try /goose/'); + } +}); + +server.listen(port, () => { + console.log(`\nšŸš€ Static file server running at http://localhost:${port}`); + console.log(`\nšŸ  Homepage: http://localhost:${port}/goose/`); + console.log(`\nšŸ“ Test markdown exports:`); + console.log(` http://localhost:${port}/goose/docs/quickstart.md`); + console.log(` http://localhost:${port}/goose/docs/getting-started/installation.md\n`); +}); diff --git a/documentation/src/theme/DocItem/Layout/index.tsx b/documentation/src/theme/DocItem/Layout/index.tsx index 25001d5c2014..d7b295681c6e 100644 --- a/documentation/src/theme/DocItem/Layout/index.tsx +++ b/documentation/src/theme/DocItem/Layout/index.tsx @@ -1,4 +1,4 @@ -import React, {type ReactNode, useState, useEffect} from 'react'; +import React, {type ReactNode, useState, useEffect, useRef} from 'react'; import type LayoutType from '@theme/DocItem/Layout'; import type {WrapperProps} from '@docusaurus/types'; import {useDoc} from '@docusaurus/plugin-content-docs/client'; @@ -14,7 +14,7 @@ import DocBreadcrumbs from '@theme/DocBreadcrumbs'; import ContentVisibility from '@theme/ContentVisibility'; import Heading from '@theme/Heading'; import MDXContent from '@theme/MDXContent'; -import {Copy, Check} from 'lucide-react'; +import {Copy, Check, ChevronDown, FileText, ExternalLink, Eye, Code, FileCode} from 'lucide-react'; import layoutStyles from './styles.module.css'; import TurndownService from 'turndown'; @@ -357,6 +357,92 @@ function CopyPageButton(): ReactNode { ); } +// New wrapper component that adds dropdown menu to copy button +function PageActionsMenu(): ReactNode { + const [dropdownOpen, setDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setDropdownOpen(false); + } + }; + + if (dropdownOpen) { + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + } + }, [dropdownOpen]); + + // Handle keyboard navigation (Escape to close) + useEffect(() => { + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setDropdownOpen(false); + } + }; + + if (dropdownOpen) { + document.addEventListener('keydown', handleEscape); + return () => document.removeEventListener('keydown', handleEscape); + } + }, [dropdownOpen]); + + const handleViewMarkdown = () => { + const currentPath = window.location.pathname; + const mdPath = currentPath.endsWith('/') + ? `${currentPath.slice(0, -1)}.md` + : `${currentPath}.md`; + window.open(mdPath, '_blank'); + setDropdownOpen(false); + }; + + return ( +
+ {/* Button group container - unified appearance */} +
+ {/* Original Copy Page Button - keep its original styling but remove right border radius */} +
+ +
+ + {/* Divider */} +
+ + {/* Chevron Dropdown Trigger - attached to copy button */} + +
+ + {/* Dropdown Menu */} + {dropdownOpen && ( +
+ + {/* Future menu items can be added here */} +
+ )} +
+ ); +} + // Hook to determine if we should show the copy button function useShouldShowCopyButton(): boolean { const {metadata} = useDoc(); @@ -395,7 +481,7 @@ function useDocTOC() { }; } -// Custom Content component that includes the copy button +// Custom Content component that includes the page actions menu function CustomDocItemContent({children}: {children: ReactNode}): ReactNode { const shouldShowCopyButton = useShouldShowCopyButton(); const {metadata, frontMatter, contentTitle} = useDoc(); @@ -409,12 +495,12 @@ function CustomDocItemContent({children}: {children: ReactNode}): ReactNode { {syntheticTitle && (
{syntheticTitle} - {shouldShowCopyButton && } + {shouldShowCopyButton && }
)} {!syntheticTitle && shouldShowCopyButton && (
- +
)} {children}