mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-27 00:06:30 +01:00 
			
		
		
		
	Compare commits
	
		
			6 Commits
		
	
	
		
			fix/mkdocs
			...
			feat/push-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 53f31f9c78 | ||
|  | 22c5651eeb | ||
|  | 68a10a9813 | ||
|  | b204964e57 | ||
|  | b2c869d7ab | ||
|  | 307e17f9c8 | 
							
								
								
									
										453
									
								
								.github/scripts/sync-docs-to-wiki-with-wiki-syntax.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										453
									
								
								.github/scripts/sync-docs-to-wiki-with-wiki-syntax.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,453 @@ | ||||
| #!/usr/bin/env tsx | ||||
|  | ||||
| import * as fs from 'fs/promises'; | ||||
| import * as path from 'path'; | ||||
| import { exec } from 'child_process'; | ||||
| import { promisify } from 'util'; | ||||
| import { Dirent } from 'fs'; | ||||
|  | ||||
| const execAsync = promisify(exec); | ||||
|  | ||||
| // Configuration | ||||
| const FILE_EXTENSIONS = ['.md', '.png', '.jpg', '.jpeg', '.gif', '.svg'] as const; | ||||
| const README_PATTERN = /^README(?:[-.](.+))?\.md$/; | ||||
|  | ||||
| interface SyncConfig { | ||||
|   mainRepoPath: string; | ||||
|   wikiPath: string; | ||||
|   docsPath: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Convert markdown to GitHub Wiki format | ||||
|  * - Images:  → [[image.png]] | ||||
|  * - Links: [text](page.md) → [[text|page]] | ||||
|  */ | ||||
| async function convertToWikiFormat(wikiDir: string): Promise<void> { | ||||
|   console.log('Converting to GitHub Wiki format...'); | ||||
|   const mdFiles = await findFiles(wikiDir, ['.md']); | ||||
|   let convertedCount = 0; | ||||
|    | ||||
|   for (const file of mdFiles) { | ||||
|     let content = await fs.readFile(file, 'utf-8'); | ||||
|     const originalContent = content; | ||||
|      | ||||
|     // Convert image references to wiki format | ||||
|     //  → [[image.png]] | ||||
|     content = content.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => { | ||||
|       // Skip external URLs | ||||
|       if (src.startsWith('http://') || src.startsWith('https://')) { | ||||
|         return match; | ||||
|       } | ||||
|        | ||||
|       // Decode URL encoding | ||||
|       let imagePath = src; | ||||
|       if (src.includes('%')) { | ||||
|         try { | ||||
|           imagePath = decodeURIComponent(src); | ||||
|         } catch { | ||||
|           imagePath = src; | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Extract just the filename for wiki syntax | ||||
|       const filename = path.basename(imagePath); | ||||
|        | ||||
|       // Use wiki syntax for images | ||||
|       // If alt text exists, add it after pipe | ||||
|       if (alt && alt.trim()) { | ||||
|         return `[[${filename}|alt=${alt}]]`; | ||||
|       } else { | ||||
|         return `[[${filename}]]`; | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Convert internal markdown links to wiki format | ||||
|     // [text](../path/to/Page.md) → [[text|Page]] | ||||
|     content = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, href) => { | ||||
|       // Skip external URLs, anchors, and images | ||||
|       if (href.startsWith('http://') ||  | ||||
|           href.startsWith('https://') ||  | ||||
|           href.startsWith('#') || | ||||
|           href.match(/\.(png|jpg|jpeg|gif|svg)$/i)) { | ||||
|         return match; | ||||
|       } | ||||
|        | ||||
|       // Check if it's a markdown file link | ||||
|       if (href.endsWith('.md') || href.includes('.md#')) { | ||||
|         // Decode URL encoding | ||||
|         let decodedHref = href; | ||||
|         if (href.includes('%')) { | ||||
|           try { | ||||
|             decodedHref = decodeURIComponent(href); | ||||
|           } catch { | ||||
|             decodedHref = href; | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         // Extract page name without extension and path | ||||
|         let pageName = decodedHref | ||||
|           .replace(/\.md(#.*)?$/, '') // Remove .md and anchor | ||||
|           .split('/')                 // Split by path | ||||
|           .pop() || '';               // Get last part (filename) | ||||
|          | ||||
|         // Convert spaces to hyphens (GitHub wiki convention) | ||||
|         pageName = pageName.replace(/ /g, '-'); | ||||
|          | ||||
|         // Use wiki link syntax | ||||
|         if (text === pageName || text === pageName.replace(/-/g, ' ')) { | ||||
|           return `[[${pageName}]]`; | ||||
|         } else { | ||||
|           return `[[${text}|${pageName}]]`; | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // For other internal links, just decode URL encoding | ||||
|       if (href.includes('%') && !href.startsWith('http')) { | ||||
|         try { | ||||
|           const decodedHref = decodeURIComponent(href); | ||||
|           return `[${text}](${decodedHref})`; | ||||
|         } catch { | ||||
|           return match; | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       return match; | ||||
|     }); | ||||
|      | ||||
|     // Save if modified | ||||
|     if (content !== originalContent) { | ||||
|       await fs.writeFile(file, content, 'utf-8'); | ||||
|       const relativePath = path.relative(wikiDir, file); | ||||
|       console.log(`  Converted: ${relativePath}`); | ||||
|       convertedCount++; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   if (convertedCount > 0) { | ||||
|     console.log(`Converted ${convertedCount} files to wiki format`); | ||||
|   } else { | ||||
|     console.log('No files needed conversion'); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Recursively find all files matching the given extensions | ||||
|  */ | ||||
| async function findFiles(dir: string, extensions: readonly string[]): Promise<string[]> { | ||||
|   const files: string[] = []; | ||||
|    | ||||
|   async function walk(currentDir: string): Promise<void> { | ||||
|     const entries: Dirent[] = await fs.readdir(currentDir, { withFileTypes: true }); | ||||
|      | ||||
|     for (const entry of entries) { | ||||
|       const fullPath = path.join(currentDir, entry.name); | ||||
|        | ||||
|       if (entry.isDirectory()) { | ||||
|         await walk(fullPath); | ||||
|       } else if (entry.isFile()) { | ||||
|         const ext = path.extname(entry.name).toLowerCase(); | ||||
|         if (extensions.includes(ext)) { | ||||
|           files.push(fullPath); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   await walk(dir); | ||||
|   return files; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get all files in a directory recursively | ||||
|  */ | ||||
| async function getAllFiles(dir: string): Promise<Set<string>> { | ||||
|   const files = new Set<string>(); | ||||
|    | ||||
|   async function walk(currentDir: string): Promise<void> { | ||||
|     try { | ||||
|       const entries = await fs.readdir(currentDir, { withFileTypes: true }); | ||||
|        | ||||
|       for (const entry of entries) { | ||||
|         const fullPath = path.join(currentDir, entry.name); | ||||
|         const relativePath = path.relative(dir, fullPath); | ||||
|          | ||||
|         // Skip .git directory | ||||
|         if (entry.name === '.git' || relativePath.startsWith('.git')) continue; | ||||
|          | ||||
|         if (entry.isDirectory()) { | ||||
|           await walk(fullPath); | ||||
|         } else if (entry.isFile()) { | ||||
|           files.add(relativePath); | ||||
|         } | ||||
|       } | ||||
|     } catch (error) { | ||||
|       // Directory might not exist yet | ||||
|       if ((error as any).code !== 'ENOENT') { | ||||
|         throw error; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   await walk(dir); | ||||
|   return files; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Flatten directory structure - move all files to root | ||||
|  * GitHub Wiki prefers flat structure | ||||
|  */ | ||||
| async function flattenStructure(wikiDir: string): Promise<void> { | ||||
|   console.log('Flattening directory structure for wiki...'); | ||||
|   const allFiles = await getAllFiles(wikiDir); | ||||
|   let movedCount = 0; | ||||
|    | ||||
|   for (const file of allFiles) { | ||||
|     // Skip if already at root | ||||
|     if (!file.includes('/')) continue; | ||||
|      | ||||
|     const oldPath = path.join(wikiDir, file); | ||||
|     const basename = path.basename(file); | ||||
|      | ||||
|     // Create unique name if file already exists at root | ||||
|     let newName = basename; | ||||
|     let counter = 1; | ||||
|     while (await fileExists(path.join(wikiDir, newName))) { | ||||
|       const ext = path.extname(basename); | ||||
|       const nameWithoutExt = basename.slice(0, -ext.length); | ||||
|       newName = `${nameWithoutExt}-${counter}${ext}`; | ||||
|       counter++; | ||||
|     } | ||||
|      | ||||
|     const newPath = path.join(wikiDir, newName); | ||||
|      | ||||
|     // Move file to root | ||||
|     await fs.rename(oldPath, newPath); | ||||
|     console.log(`  Moved: ${file} → ${newName}`); | ||||
|     movedCount++; | ||||
|   } | ||||
|    | ||||
|   if (movedCount > 0) { | ||||
|     console.log(`Moved ${movedCount} files to root`); | ||||
|      | ||||
|     // Clean up empty directories | ||||
|     await cleanEmptyDirectories(wikiDir); | ||||
|   } | ||||
| } | ||||
|  | ||||
| async function fileExists(path: string): Promise<boolean> { | ||||
|   try { | ||||
|     await fs.access(path); | ||||
|     return true; | ||||
|   } catch { | ||||
|     return false; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Remove empty directories recursively | ||||
|  */ | ||||
| async function cleanEmptyDirectories(dir: string): Promise<void> { | ||||
|   const allDirs = await getAllDirectories(dir); | ||||
|    | ||||
|   for (const subDir of allDirs) { | ||||
|     try { | ||||
|       const entries = await fs.readdir(subDir); | ||||
|       if (entries.length === 0 || (entries.length === 1 && entries[0] === '.git')) { | ||||
|         await fs.rmdir(subDir); | ||||
|       } | ||||
|     } catch { | ||||
|       // Ignore errors | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get all directories recursively | ||||
|  */ | ||||
| async function getAllDirectories(dir: string): Promise<string[]> { | ||||
|   const dirs: string[] = []; | ||||
|    | ||||
|   async function walk(currentDir: string): Promise<void> { | ||||
|     try { | ||||
|       const entries = await fs.readdir(currentDir, { withFileTypes: true }); | ||||
|        | ||||
|       for (const entry of entries) { | ||||
|         if (entry.isDirectory() && entry.name !== '.git') { | ||||
|           const fullPath = path.join(currentDir, entry.name); | ||||
|           dirs.push(fullPath); | ||||
|           await walk(fullPath); | ||||
|         } | ||||
|       } | ||||
|     } catch { | ||||
|       // Ignore errors | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   await walk(dir); | ||||
|   return dirs.sort((a, b) => b.length - a.length); // Sort longest first for cleanup | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Sync files from source to wiki | ||||
|  */ | ||||
| async function syncFiles(sourceDir: string, wikiDir: string): Promise<void> { | ||||
|   console.log('Syncing files to wiki...'); | ||||
|    | ||||
|   // Get all valid source files | ||||
|   const sourceFiles = await findFiles(sourceDir, FILE_EXTENSIONS); | ||||
|   const sourceRelativePaths = new Set<string>(); | ||||
|    | ||||
|   // Copy all source files | ||||
|   console.log(`Found ${sourceFiles.length} files to sync`); | ||||
|    | ||||
|   for (const file of sourceFiles) { | ||||
|     const relativePath = path.relative(sourceDir, file); | ||||
|     sourceRelativePaths.add(relativePath); | ||||
|      | ||||
|     const targetPath = path.join(wikiDir, relativePath); | ||||
|     const targetDir = path.dirname(targetPath); | ||||
|      | ||||
|     // Create directory structure | ||||
|     await fs.mkdir(targetDir, { recursive: true }); | ||||
|      | ||||
|     // Copy file | ||||
|     await fs.copyFile(file, targetPath); | ||||
|   } | ||||
|    | ||||
|   // Remove orphaned files | ||||
|   const wikiFiles = await getAllFiles(wikiDir); | ||||
|   for (const wikiFile of wikiFiles) { | ||||
|     if (!sourceRelativePaths.has(wikiFile) && !wikiFile.startsWith('Home')) { | ||||
|       const fullPath = path.join(wikiDir, wikiFile); | ||||
|       await fs.unlink(fullPath); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Copy root README.md to wiki as Home.md if it exists | ||||
|  */ | ||||
| async function copyRootReadme(mainRepoPath: string, wikiPath: string): Promise<void> { | ||||
|   const rootReadmePath = path.join(mainRepoPath, 'README.md'); | ||||
|   const wikiHomePath = path.join(wikiPath, 'Home.md'); | ||||
|    | ||||
|   try { | ||||
|     await fs.access(rootReadmePath); | ||||
|     await fs.copyFile(rootReadmePath, wikiHomePath); | ||||
|     console.log('  Copied root README.md as Home.md'); | ||||
|   } catch (error) { | ||||
|     console.log('  No root README.md found to use as Home page'); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Rename README files to wiki-compatible names | ||||
|  */ | ||||
| async function renameReadmeFiles(wikiDir: string): Promise<void> { | ||||
|   console.log('Converting README files for wiki compatibility...'); | ||||
|   const files = await fs.readdir(wikiDir); | ||||
|    | ||||
|   for (const file of files) { | ||||
|     const match = file.match(README_PATTERN); | ||||
|     if (match) { | ||||
|       const oldPath = path.join(wikiDir, file); | ||||
|       let newName: string; | ||||
|        | ||||
|       if (match[1]) { | ||||
|         // Language-specific README | ||||
|         newName = `Home-${match[1]}.md`; | ||||
|       } else { | ||||
|         // Main README | ||||
|         newName = 'Home.md'; | ||||
|       } | ||||
|        | ||||
|       const newPath = path.join(wikiDir, newName); | ||||
|       await fs.rename(oldPath, newPath); | ||||
|       console.log(`  Renamed: ${file} → ${newName}`); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Check if there are any changes in the wiki | ||||
|  */ | ||||
| async function hasChanges(wikiDir: string): Promise<boolean> { | ||||
|   try { | ||||
|     const { stdout } = await execAsync('git status --porcelain', { cwd: wikiDir }); | ||||
|     return stdout.trim().length > 0; | ||||
|   } catch (error) { | ||||
|     console.error('Error checking git status:', error); | ||||
|     return false; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get configuration from environment variables | ||||
|  */ | ||||
| function getConfig(): SyncConfig { | ||||
|   const mainRepoPath = process.env.MAIN_REPO_PATH || 'main-repo'; | ||||
|   const wikiPath = process.env.WIKI_PATH || 'wiki'; | ||||
|   const docsPath = path.join(mainRepoPath, 'docs'); | ||||
|    | ||||
|   return { mainRepoPath, wikiPath, docsPath }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Main sync function | ||||
|  */ | ||||
| async function syncDocsToWiki(): Promise<void> { | ||||
|   const config = getConfig(); | ||||
|   const flattenWiki = process.env.FLATTEN_WIKI === 'true'; | ||||
|    | ||||
|   console.log('Starting documentation sync to wiki...'); | ||||
|   console.log(`Source: ${config.docsPath}`); | ||||
|   console.log(`Target: ${config.wikiPath}`); | ||||
|   console.log(`Flatten structure: ${flattenWiki}`); | ||||
|    | ||||
|   try { | ||||
|     // Verify paths exist | ||||
|     await fs.access(config.docsPath); | ||||
|     await fs.access(config.wikiPath); | ||||
|      | ||||
|     // Sync files | ||||
|     await syncFiles(config.docsPath, config.wikiPath); | ||||
|      | ||||
|     // Copy root README.md as Home.md | ||||
|     await copyRootReadme(config.mainRepoPath, config.wikiPath); | ||||
|      | ||||
|     // Convert to wiki format | ||||
|     await convertToWikiFormat(config.wikiPath); | ||||
|      | ||||
|     // Optionally flatten directory structure | ||||
|     if (flattenWiki) { | ||||
|       await flattenStructure(config.wikiPath); | ||||
|     } | ||||
|      | ||||
|     // Rename README files to wiki-compatible names | ||||
|     await renameReadmeFiles(config.wikiPath); | ||||
|      | ||||
|     // Check for changes | ||||
|     const changed = await hasChanges(config.wikiPath); | ||||
|      | ||||
|     if (changed) { | ||||
|       console.log('\nChanges detected in wiki'); | ||||
|       process.stdout.write('::set-output name=changes::true\n'); | ||||
|     } else { | ||||
|       console.log('\nNo changes detected in wiki'); | ||||
|       process.stdout.write('::set-output name=changes::false\n'); | ||||
|     } | ||||
|      | ||||
|     console.log('Sync completed successfully!'); | ||||
|   } catch (error) { | ||||
|     console.error('Error during sync:', error); | ||||
|     process.exit(1); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Run if called directly | ||||
| if (require.main === module) { | ||||
|   syncDocsToWiki(); | ||||
| } | ||||
|  | ||||
| export { syncDocsToWiki }; | ||||
							
								
								
									
										437
									
								
								.github/scripts/sync-docs-to-wiki.ts
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										437
									
								
								.github/scripts/sync-docs-to-wiki.ts
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @@ -0,0 +1,437 @@ | ||||
| #!/usr/bin/env tsx | ||||
|  | ||||
| import * as fs from 'fs/promises'; | ||||
| import * as path from 'path'; | ||||
| import { exec } from 'child_process'; | ||||
| import { promisify } from 'util'; | ||||
| import { Dirent } from 'fs'; | ||||
|  | ||||
| const execAsync = promisify(exec); | ||||
|  | ||||
| // Configuration | ||||
| const FILE_EXTENSIONS = ['.md', '.png', '.jpg', '.jpeg', '.gif', '.svg'] as const; | ||||
| const README_PATTERN = /^README(?:[-.](.+))?\.md$/; | ||||
|  | ||||
| interface SyncConfig { | ||||
|   mainRepoPath: string; | ||||
|   wikiPath: string; | ||||
|   docsPath: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Recursively find all files matching the given extensions | ||||
|  */ | ||||
| async function findFiles(dir: string, extensions: readonly string[]): Promise<string[]> { | ||||
|   const files: string[] = []; | ||||
|    | ||||
|   async function walk(currentDir: string): Promise<void> { | ||||
|     const entries: Dirent[] = await fs.readdir(currentDir, { withFileTypes: true }); | ||||
|      | ||||
|     for (const entry of entries) { | ||||
|       const fullPath = path.join(currentDir, entry.name); | ||||
|        | ||||
|       if (entry.isDirectory()) { | ||||
|         await walk(fullPath); | ||||
|       } else if (entry.isFile()) { | ||||
|         const ext = path.extname(entry.name).toLowerCase(); | ||||
|         if (extensions.includes(ext)) { | ||||
|           files.push(fullPath); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   await walk(dir); | ||||
|   return files; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get all files in a directory recursively | ||||
|  */ | ||||
| async function getAllFiles(dir: string): Promise<Set<string>> { | ||||
|   const files = new Set<string>(); | ||||
|    | ||||
|   async function walk(currentDir: string): Promise<void> { | ||||
|     try { | ||||
|       const entries = await fs.readdir(currentDir, { withFileTypes: true }); | ||||
|        | ||||
|       for (const entry of entries) { | ||||
|         const fullPath = path.join(currentDir, entry.name); | ||||
|         const relativePath = path.relative(dir, fullPath); | ||||
|          | ||||
|         // Skip .git directory | ||||
|         if (entry.name === '.git' || relativePath.startsWith('.git')) continue; | ||||
|          | ||||
|         if (entry.isDirectory()) { | ||||
|           await walk(fullPath); | ||||
|         } else if (entry.isFile()) { | ||||
|           files.add(relativePath); | ||||
|         } | ||||
|       } | ||||
|     } catch (error) { | ||||
|       // Directory might not exist yet | ||||
|       if ((error as any).code !== 'ENOENT') { | ||||
|         throw error; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   await walk(dir); | ||||
|   return files; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Sync files from source to wiki, preserving directory structure and removing orphaned files | ||||
|  */ | ||||
| async function syncFiles(sourceDir: string, wikiDir: string): Promise<void> { | ||||
|   console.log('Analyzing files to sync...'); | ||||
|    | ||||
|   // Get all valid source files | ||||
|   const sourceFiles = await findFiles(sourceDir, FILE_EXTENSIONS); | ||||
|   const sourceRelativePaths = new Set<string>(); | ||||
|    | ||||
|   // Copy all source files and track their paths | ||||
|   console.log(`Found ${sourceFiles.length} files to sync`); | ||||
|   let copiedCount = 0; | ||||
|   let skippedCount = 0; | ||||
|    | ||||
|   for (const file of sourceFiles) { | ||||
|     const relativePath = path.relative(sourceDir, file); | ||||
|     sourceRelativePaths.add(relativePath); | ||||
|      | ||||
|     const targetPath = path.join(wikiDir, relativePath); | ||||
|     const targetDir = path.dirname(targetPath); | ||||
|      | ||||
|     // Create directory structure | ||||
|     await fs.mkdir(targetDir, { recursive: true }); | ||||
|      | ||||
|     // Check if file needs updating (compare modification times) | ||||
|     let needsCopy = true; | ||||
|     try { | ||||
|       const sourceStat = await fs.stat(file); | ||||
|       const targetStat = await fs.stat(targetPath); | ||||
|       // Only copy if source is newer or sizes differ | ||||
|       needsCopy = sourceStat.mtime > targetStat.mtime || sourceStat.size !== targetStat.size; | ||||
|     } catch { | ||||
|       // Target doesn't exist, needs copy | ||||
|       needsCopy = true; | ||||
|     } | ||||
|      | ||||
|     if (needsCopy) { | ||||
|       await fs.copyFile(file, targetPath); | ||||
|       console.log(`  Updated: ${relativePath}`); | ||||
|       copiedCount++; | ||||
|     } else { | ||||
|       skippedCount++; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   console.log(`Updated ${copiedCount} files, ${skippedCount} unchanged`); | ||||
|    | ||||
|   // Find and remove files that don't exist in source | ||||
|   console.log('Checking for orphaned files in wiki...'); | ||||
|   const wikiFiles = await getAllFiles(wikiDir); | ||||
|   let removedCount = 0; | ||||
|    | ||||
|   for (const wikiFile of wikiFiles) { | ||||
|     // Check if this file should exist (either as-is or will be renamed) | ||||
|     let shouldExist = sourceRelativePaths.has(wikiFile); | ||||
|      | ||||
|     // Special handling for Home files that will be created from READMEs | ||||
|     if (wikiFile.startsWith('Home')) { | ||||
|       const readmeVariant1 = wikiFile.replace(/^Home(-.*)?\.md$/, 'README$1.md'); | ||||
|       const readmeVariant2 = wikiFile.replace(/^Home-(.+)\.md$/, 'README.$1.md'); | ||||
|       shouldExist = sourceRelativePaths.has(readmeVariant1) || sourceRelativePaths.has(readmeVariant2) || sourceRelativePaths.has('README.md'); | ||||
|     } | ||||
|      | ||||
|     if (!shouldExist) { | ||||
|       const fullPath = path.join(wikiDir, wikiFile); | ||||
|       await fs.unlink(fullPath); | ||||
|       console.log(`  Removed: ${wikiFile}`); | ||||
|       removedCount++; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   if (removedCount > 0) { | ||||
|     console.log(`Removed ${removedCount} orphaned files`); | ||||
|      | ||||
|     // Clean up empty directories | ||||
|     await cleanEmptyDirectories(wikiDir); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Remove empty directories recursively | ||||
|  */ | ||||
| async function cleanEmptyDirectories(dir: string): Promise<void> { | ||||
|   async function removeEmptyDirs(currentDir: string): Promise<boolean> { | ||||
|     if (currentDir === dir) return false; // Don't remove root | ||||
|      | ||||
|     try { | ||||
|       const entries = await fs.readdir(currentDir, { withFileTypes: true }); | ||||
|        | ||||
|       // Skip .git directory | ||||
|       const filteredEntries = entries.filter(e => e.name !== '.git'); | ||||
|        | ||||
|       if (filteredEntries.length === 0) { | ||||
|         await fs.rmdir(currentDir); | ||||
|         return true; | ||||
|       } | ||||
|        | ||||
|       // Check subdirectories | ||||
|       for (const entry of filteredEntries) { | ||||
|         if (entry.isDirectory()) { | ||||
|           const subDir = path.join(currentDir, entry.name); | ||||
|           await removeEmptyDirs(subDir); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Check again after cleaning subdirectories | ||||
|       const remainingEntries = await fs.readdir(currentDir); | ||||
|       const filteredRemaining = remainingEntries.filter(e => e !== '.git'); | ||||
|        | ||||
|       if (filteredRemaining.length === 0 && currentDir !== dir) { | ||||
|         await fs.rmdir(currentDir); | ||||
|         return true; | ||||
|       } | ||||
|        | ||||
|       return false; | ||||
|     } catch (error) { | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // Get all directories and process them | ||||
|   const allDirs = await getAllDirectories(dir); | ||||
|   for (const subDir of allDirs) { | ||||
|     await removeEmptyDirs(subDir); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get all directories recursively | ||||
|  */ | ||||
| async function getAllDirectories(dir: string): Promise<string[]> { | ||||
|   const dirs: string[] = []; | ||||
|    | ||||
|   async function walk(currentDir: string): Promise<void> { | ||||
|     try { | ||||
|       const entries = await fs.readdir(currentDir, { withFileTypes: true }); | ||||
|        | ||||
|       for (const entry of entries) { | ||||
|         if (entry.isDirectory() && entry.name !== '.git') { | ||||
|           const fullPath = path.join(currentDir, entry.name); | ||||
|           dirs.push(fullPath); | ||||
|           await walk(fullPath); | ||||
|         } | ||||
|       } | ||||
|     } catch { | ||||
|       // Ignore errors | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   await walk(dir); | ||||
|   return dirs.sort((a, b) => b.length - a.length); // Sort longest first for cleanup | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Fix references in markdown files for wiki compatibility | ||||
|  *  | ||||
|  * Issues fixed: | ||||
|  * 1. URL-encoded image references (spaces as %20) need to match actual filenames | ||||
|  * 2. Internal markdown links need to be converted to wiki syntax | ||||
|  * 3. Images can optionally use wiki syntax [[image.png]] for better compatibility | ||||
|  */ | ||||
| async function fixImageReferences(wikiDir: string): Promise<void> { | ||||
|   console.log('Fixing references for GitHub Wiki compatibility...'); | ||||
|   const mdFiles = await findFiles(wikiDir, ['.md']); | ||||
|   let fixedCount = 0; | ||||
|    | ||||
|   for (const file of mdFiles) { | ||||
|     let content = await fs.readFile(file, 'utf-8'); | ||||
|     let modified = false; | ||||
|     const originalContent = content; | ||||
|      | ||||
|     // Step 1: Fix URL-encoded image references | ||||
|     // Convert  to  | ||||
|     content = content.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => { | ||||
|       // Skip external URLs | ||||
|       if (src.startsWith('http://') || src.startsWith('https://')) { | ||||
|         return match; | ||||
|       } | ||||
|        | ||||
|       // Decode URL encoding if present | ||||
|       if (src.includes('%')) { | ||||
|         try { | ||||
|           const decodedSrc = decodeURIComponent(src); | ||||
|           return ``; | ||||
|         } catch { | ||||
|           return match; | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       return match; | ||||
|     }); | ||||
|      | ||||
|     // Step 2: Fix internal links - decode URL encoding but keep standard markdown format | ||||
|     // GitHub Wiki actually supports standard markdown links with relative paths | ||||
|     content = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, href) => { | ||||
|       // Skip external URLs, anchors, and images | ||||
|       if (href.startsWith('http://') ||  | ||||
|           href.startsWith('https://') ||  | ||||
|           href.startsWith('#') || | ||||
|           href.match(/\.(png|jpg|jpeg|gif|svg)$/i)) { | ||||
|         return match; | ||||
|       } | ||||
|        | ||||
|       // Decode URL encoding for all internal links | ||||
|       if (href.includes('%')) { | ||||
|         try { | ||||
|           const decodedHref = decodeURIComponent(href); | ||||
|           return `[${text}](${decodedHref})`; | ||||
|         } catch { | ||||
|           return match; | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       return match; | ||||
|     }); | ||||
|      | ||||
|     // Check if content was modified | ||||
|     if (content !== originalContent) { | ||||
|       modified = true; | ||||
|       await fs.writeFile(file, content, 'utf-8'); | ||||
|       const relativePath = path.relative(wikiDir, file); | ||||
|       console.log(`  Fixed references in: ${relativePath}`); | ||||
|       fixedCount++; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   if (fixedCount > 0) { | ||||
|     console.log(`Fixed references in ${fixedCount} files`); | ||||
|   } else { | ||||
|     console.log('No references needed fixing'); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Rename README files to wiki-compatible names | ||||
|  */ | ||||
| async function renameReadmeFiles(wikiDir: string): Promise<void> { | ||||
|   console.log('Converting README files for wiki compatibility...'); | ||||
|   const files = await fs.readdir(wikiDir); | ||||
|    | ||||
|   for (const file of files) { | ||||
|     const match = file.match(README_PATTERN); | ||||
|     if (match) { | ||||
|       const oldPath = path.join(wikiDir, file); | ||||
|       let newName: string; | ||||
|        | ||||
|       if (match[1]) { | ||||
|         // Language-specific README (e.g., README-ZH_CN.md or README.es.md) | ||||
|         newName = `Home-${match[1]}.md`; | ||||
|       } else { | ||||
|         // Main README | ||||
|         newName = 'Home.md'; | ||||
|       } | ||||
|        | ||||
|       const newPath = path.join(wikiDir, newName); | ||||
|       await fs.rename(oldPath, newPath); | ||||
|       console.log(`  Renamed: ${file} → ${newName}`); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Check if there are any changes in the wiki | ||||
|  */ | ||||
| async function hasChanges(wikiDir: string): Promise<boolean> { | ||||
|   try { | ||||
|     const { stdout } = await execAsync('git status --porcelain', { cwd: wikiDir }); | ||||
|     return stdout.trim().length > 0; | ||||
|   } catch (error) { | ||||
|     console.error('Error checking git status:', error); | ||||
|     return false; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Copy root README.md to wiki as Home.md if it exists | ||||
|  */ | ||||
| async function copyRootReadme(mainRepoPath: string, wikiPath: string): Promise<void> { | ||||
|   const rootReadmePath = path.join(mainRepoPath, 'README.md'); | ||||
|   const wikiHomePath = path.join(wikiPath, 'Home.md'); | ||||
|    | ||||
|   try { | ||||
|     await fs.access(rootReadmePath); | ||||
|     await fs.copyFile(rootReadmePath, wikiHomePath); | ||||
|     console.log('  Copied root README.md as Home.md'); | ||||
|   } catch (error) { | ||||
|     // Root README doesn't exist or can't be accessed | ||||
|     console.log('  No root README.md found to use as Home page'); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get configuration from environment variables | ||||
|  */ | ||||
| function getConfig(): SyncConfig { | ||||
|   const mainRepoPath = process.env.MAIN_REPO_PATH || 'main-repo'; | ||||
|   const wikiPath = process.env.WIKI_PATH || 'wiki'; | ||||
|   const docsPath = path.join(mainRepoPath, 'docs'); | ||||
|    | ||||
|   return { mainRepoPath, wikiPath, docsPath }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Main sync function | ||||
|  */ | ||||
| async function syncDocsToWiki(): Promise<void> { | ||||
|   const config = getConfig(); | ||||
|    | ||||
|   console.log('Starting documentation sync to wiki...'); | ||||
|   console.log(`Source: ${config.docsPath}`); | ||||
|   console.log(`Target: ${config.wikiPath}`); | ||||
|    | ||||
|   try { | ||||
|     // Verify paths exist | ||||
|     await fs.access(config.docsPath); | ||||
|     await fs.access(config.wikiPath); | ||||
|      | ||||
|     // Sync files (copy new/updated, remove orphaned) | ||||
|     await syncFiles(config.docsPath, config.wikiPath); | ||||
|      | ||||
|     // Copy root README.md as Home.md | ||||
|     await copyRootReadme(config.mainRepoPath, config.wikiPath); | ||||
|      | ||||
|     // Fix image and link references for wiki compatibility | ||||
|     await fixImageReferences(config.wikiPath); | ||||
|      | ||||
|     // Rename README files to wiki-compatible names | ||||
|     await renameReadmeFiles(config.wikiPath); | ||||
|      | ||||
|     // Check for changes | ||||
|     const changed = await hasChanges(config.wikiPath); | ||||
|      | ||||
|     if (changed) { | ||||
|       console.log('\nChanges detected in wiki'); | ||||
|       // GitHub Actions output format | ||||
|       process.stdout.write('::set-output name=changes::true\n'); | ||||
|     } else { | ||||
|       console.log('\nNo changes detected in wiki'); | ||||
|       process.stdout.write('::set-output name=changes::false\n'); | ||||
|     } | ||||
|      | ||||
|     console.log('Sync completed successfully!'); | ||||
|   } catch (error) { | ||||
|     console.error('Error during sync:', error); | ||||
|     process.exit(1); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Run if called directly | ||||
| if (require.main === module) { | ||||
|   syncDocsToWiki(); | ||||
| } | ||||
|  | ||||
| export { syncDocsToWiki }; | ||||
							
								
								
									
										67
									
								
								.github/workflows/sync-docs-to-wiki.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								.github/workflows/sync-docs-to-wiki.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| name: Sync Docs to Wiki | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|     paths: | ||||
|       - 'docs/**' | ||||
|   workflow_dispatch: # Allow manual triggering | ||||
|  | ||||
| permissions: | ||||
|   contents: read  # Read access to repository contents | ||||
|   # Note: Writing to wiki requires a PAT or GITHUB_TOKEN with additional permissions | ||||
|   # The default GITHUB_TOKEN cannot write to wikis, so we need to: | ||||
|   # 1. Create a Personal Access Token (PAT) with 'repo' scope | ||||
|   # 2. Add it as a repository secret named WIKI_TOKEN | ||||
|  | ||||
| jobs: | ||||
|   sync-wiki: | ||||
|     runs-on: ubuntu-latest | ||||
|      | ||||
|     steps: | ||||
|       - name: Checkout main repository | ||||
|         uses: actions/checkout@v4 | ||||
|         with: | ||||
|           path: main-repo | ||||
|            | ||||
|       - name: Checkout wiki repository | ||||
|         uses: actions/checkout@v4 | ||||
|         with: | ||||
|           repository: TriliumNext/Trilium.wiki | ||||
|           path: wiki | ||||
|           token: ${{ secrets.WIKI_TOKEN }} | ||||
|            | ||||
|       - name: Setup Node.js | ||||
|         uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version: '20' | ||||
|            | ||||
|       - name: Install tsx for TypeScript execution | ||||
|         run: npm install -g tsx | ||||
|            | ||||
|       - name: Setup Git | ||||
|         run: | | ||||
|           git config --global user.email "action@github.com" | ||||
|           git config --global user.name "GitHub Action" | ||||
|            | ||||
|       - name: Sync documentation to wiki | ||||
|         id: sync | ||||
|         run: | | ||||
|           tsx main-repo/.github/scripts/sync-docs-to-wiki.ts | ||||
|         env: | ||||
|           MAIN_REPO_PATH: main-repo | ||||
|           WIKI_PATH: wiki | ||||
|            | ||||
|       - name: Commit and push changes | ||||
|         if: contains(steps.sync.outputs.changes, 'true') | ||||
|         run: | | ||||
|           cd wiki | ||||
|           git add . | ||||
|           git commit -m "Sync documentation from main repository | ||||
|            | ||||
|           Source commit: ${{ github.sha }} | ||||
|           Triggered by: ${{ github.event.head_commit.message }}" | ||||
|           git push | ||||
|         env: | ||||
|           GITHUB_TOKEN: ${{ secrets.WIKI_TOKEN }} | ||||
		Reference in New Issue
	
	Block a user