diff --git a/src/generate.js b/src/generate.js index 3aaeb4de4ea..92c1e27c1b6 100644 --- a/src/generate.js +++ b/src/generate.js @@ -31,6 +31,8 @@ const watchProject = process.argv[3]; const forcedVersion = process.argv.find(arg => arg.startsWith('--version='))?.substring('--version='.length); const srcDir = path.join(process.env.SRC_DIR || '../playwright', 'docs', 'src'); +const sourceImagesDir = path.join(process.env.SRC_DIR || '../playwright', 'docs', 'src', 'images'); +const targetImagesDir = path.join(__dirname, '..', 'static', 'images'); const lang2Folder = { 'js': 'nodejs', @@ -127,6 +129,192 @@ async function generateDocsForLanguages () { }); }; +/** + * Check if file is an image + * @param {string} filePath + * @returns {boolean} + */ +function isImageFile(filePath) { + const ext = path.extname(filePath).toLowerCase(); + const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp']; + return imageExtensions.includes(ext); +} + +/** + * Get relative path from source images directory + * @param {string} fullPath + * @returns {string} + */ +function getRelativePath(fullPath) { + return path.relative(sourceImagesDir, fullPath); +} + +/** + * Copy a single file from source to target + * @param {string} sourcePath + * @param {string} targetPath + */ +function copyImageFile(sourcePath, targetPath) { + if (!isImageFile(sourcePath)) { + return; + } + + // Ensure target directory exists + const targetDir = path.dirname(targetPath); + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); + } + + console.log(`Copying image: ${getRelativePath(sourcePath)}`); + fs.copyFileSync(sourcePath, targetPath); +} + +/** + * Remove a file from target + * @param {string} targetPath + */ +function removeImageFile(targetPath) { + if (fs.existsSync(targetPath)) { + console.log(`Removing image: ${path.relative(targetImagesDir, targetPath)}`); + fs.unlinkSync(targetPath); + } +} + +/** + * Recursively copy all images from source directory to target directory + * @param {string} sourceDir + * @param {string} targetDir + */ +function copyImagesRecursive(sourceDir, targetDir) { + // Create target directory if it doesn't exist + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); + } + + // Read the source directory + const files = fs.readdirSync(sourceDir); + let imageCount = 0; + + files.forEach(file => { + const sourcePath = path.join(sourceDir, file); + const targetPath = path.join(targetDir, file); + + if (fs.statSync(sourcePath).isDirectory()) { + // Recursively copy subdirectories + imageCount += copyImagesRecursive(sourcePath, targetPath); + } else if (isImageFile(sourcePath)) { + copyImageFile(sourcePath, targetPath); + imageCount++; + } + }); + + return imageCount; +} + +/** + * Handle image file system events when watching + * @param {string} event + * @param {string} sourcePath + */ +function handleImageEvent(event, sourcePath) { + if (!isImageFile(sourcePath)) { + return; + } + + const relativePath = getRelativePath(sourcePath); + const targetPath = path.join(targetImagesDir, relativePath); + + switch (event) { + case 'add': + case 'change': + copyImageFile(sourcePath, targetPath); + break; + case 'unlink': + removeImageFile(targetPath); + break; + case 'addDir': + // Directory events are handled automatically by file events + break; + case 'unlinkDir': + const targetDir = path.join(targetImagesDir, relativePath); + if (fs.existsSync(targetDir)) { + console.log(`Removing directory: ${relativePath}`); + try { + fs.rmSync(targetDir, { recursive: true, force: true }); + } catch (error) { + console.error(`Failed to remove directory: ${relativePath}. Error: ${error.message}`); + } + } + break; + } +} + +/** + * Sync all images from upstream playwright repo + */ +function syncImages() { + console.log('Syncing images from upstream playwright repo...'); + console.log(`Source: ${sourceImagesDir}`); + console.log(`Target: ${targetImagesDir}`); + + if (!fs.existsSync(sourceImagesDir)) { + console.warn(`Source images directory does not exist: ${sourceImagesDir}`); + return; + } + + const imageCount = copyImagesRecursive(sourceImagesDir, targetImagesDir); + console.log(`Image sync completed! Copied ${imageCount} images.`); +} + +/** + * Convert relative image paths to absolute paths in generated markdown files + */ +function fixImagePathsInGeneratedDocs() { + console.log('Fixing image paths in generated documentation...'); + + const docsDirs = [ + path.join(__dirname, '..', 'nodejs', 'docs'), + path.join(__dirname, '..', 'python', 'docs'), + path.join(__dirname, '..', 'java', 'docs'), + path.join(__dirname, '..', 'dotnet', 'docs'), + ]; + + let filesFixed = 0; + + docsDirs.forEach(docsDir => { + if (!fs.existsSync(docsDir)) return; + + const walkDir = (dir) => { + const files = fs.readdirSync(dir); + files.forEach(file => { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + + if (stat.isDirectory()) { + walkDir(filePath); + } else if (file.endsWith('.mdx') || file.endsWith('.md')) { + let content = fs.readFileSync(filePath, 'utf8'); + const originalContent = content; + + // Convert relative image paths to absolute paths + content = content.replace(/!\[([^\]]*)\]\(\.\/images\//g, '![$1](/images/'); + + if (content !== originalContent) { + fs.writeFileSync(filePath, content); + filesFixed++; + } + } + }); + }; + + walkDir(docsDir); + }); + + if (filesFixed > 0) { + console.log(`Fixed image paths in ${filesFixed} documentation files.`); + } +} + /** * @param {import('chokidar').FSWatcherEventMap['all'][0]} event * @param {string} from @@ -159,14 +347,34 @@ async function syncWithWorkingDirectory(event, from) { console.error(`Error auto syncing docs (generating)`, error); }) }); - chokidar.watch(path.join(__dirname, '..', lang2Folder[watchProject])).on('all', (event, path) => { - syncWithWorkingDirectory(event, path).catch(error => { - console.error(`Error auto syncing docs (mirroring)`, error); - }) - }); + + // Watch for image changes + if (fs.existsSync(sourceImagesDir)) { + chokidar.watch(sourceImagesDir, { + ignored: /(^|[\/\\])\../, // ignore dotfiles + persistent: true, + ignoreInitial: true + }).on('all', (event, imagePath) => { + handleImageEvent(event, imagePath); + }); + } + + const watchPath = watchProject ? path.join(__dirname, '..', lang2Folder[watchProject]) : null; + if (watchPath && fs.existsSync(watchPath)) { + chokidar.watch(watchPath).on('all', (event, path) => { + syncWithWorkingDirectory(event, path).catch(error => { + console.error(`Error auto syncing docs (mirroring)`, error); + }); + }); + } + await generateDocsForLanguages(); + syncImages(); + fixImagePathsInGeneratedDocs(); } else { await generateDocsForLanguages(); + syncImages(); + fixImagePathsInGeneratedDocs(); await updateStarsButton(); }