mirror of
https://github.com/zadam/trilium.git
synced 2025-10-26 15:56:29 +01:00
Compare commits
6 Commits
v0.99.0
...
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