Files
puter/tools/genwiki/main.js
2025-02-25 15:31:40 -05:00

232 lines
7.6 KiB
JavaScript

const { walk, EXCLUDE_LISTS } = require('../file-walker/test');
const fs = require('fs').promises;
const path_ = require('node:path');
const FILE_EXCLUDES = [
/(^|\/)\.git/,
/^volatile\//,
/^node_modules\//,
/\/node_modules$/,
/^submodules\//,
/^node_modules$/,
/package-lock\.json/,
/^src\/dev-center\/js/,
/src\/backend\/src\/public\/assets/,
/^src\/gui\/src\/lib/,
/^eslint\.config\.js$/,
// translation readme copies
/(^|\/)doc\/i18n/,
// irrelevant documentation
/(^|\/)doc\/graveyard/,
// development logs
/\/devlog\.md$/,
]
const ROOT_DIR = path_.join(__dirname, '../..');
const WIKI_DIR = path_.join(__dirname, '../../submodules/wiki');
const path_to_name = path => {
// Special case for Home.md
if ( path === 'doc/README.md' ) return 'Home';
// Remove src/ and doc/ components
// path = path.replace(/src\//g, '')
path = path.replace(/doc\//g, '')
// Hyphenate components
path = path.replace(/-/g, '_')
path = path.replace(/\//g, '-')
// Remove extension
path = path.replace(/\.md$/, '')
return path;
}
const fix_relative_links = (content, entry) => {
const originalDir = path_.dirname(entry);
// Markdown links: [text](path/to/file.md), [text](path/to/file#section), etc
return content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, link) => {
// Skip external links
if (link.startsWith('http://') || link.startsWith('https://') || link.startsWith('/')) {
return match;
}
// Anchor links within the same file aren't changed
if (link.startsWith('#')) return match;
// Split the link to separate the path from the anchor
const [linkPath, anchor] = link.split('#');
// Resolve the relative path
let resolvedPath = path_.normalize(path_.join(originalDir, linkPath));
// Find the matching wiki path
const wikiPath = path_to_name(resolvedPath);
const newLink = anchor ? `${wikiPath}#${anchor}` : wikiPath;
return `[${text}](${newLink})`;
});
};
const main = async () => {
const walk_iter = walk({
excludes: FILE_EXCLUDES,
}, ROOT_DIR);
const documents = [];
for await ( const value of walk_iter ) {
let path = value.path;
path = path_.relative(ROOT_DIR, path);
// File must be under a doc/ directory
if ( ! path.match(/(^|\/)doc\//) ) continue;
// File must be markdown
if ( ! path.match(/\.md/) ) continue;
let outputName = path_to_name(path);
// Read file content
let content = await fs.readFile(value.path, 'utf8');
// Get the first heading from the file to use as title
const titleMatch = content.match(/^#\s+(.+)$/m);
const title = titleMatch ? titleMatch[1] : outputName.replace(/-/g, ' ');
// Fix internal links
content = fix_relative_links(content, path);
// Write the modified content to the wiki directory
await fs.writeFile(path_.join(WIKI_DIR, outputName + '.md'), content);
// Store information for sidebar
const sidebarPath = outputName.split('-');
// The original path structure (minus doc/) helps determine the hierarchy
documents.push({
sidebarPath,
outputName,
title: title
});
}
// Generate _Sidebar.md
const sidebarContent = generate_sidebar(documents);
await fs.writeFile(path_.join(WIKI_DIR, '_Sidebar.md'), sidebarContent);
}
const format_name = name => {
if ( name === 'api' ) return 'API';
if ( name === 'contributors' ) return 'For Contributors';
return name.charAt(0).toUpperCase() + name.slice(1);
}
const generate_sidebar = (documents) => {
// Sort entries by path to group related files together
documents.sort((a, b) => {
const pathA = a.sidebarPath.slice(0, -1).join('/');
const pathB = b.sidebarPath.slice(0, -1).join('/');
if ( pathA !== pathB ) {
return pathA.localeCompare(pathB);
}
// README.md always goes first
const isReadmeA = a.outputName.toLowerCase().includes('readme') ||
a.outputName.toLowerCase().includes('home');
const isReadmeB = b.outputName.toLowerCase().includes('readme') ||
b.outputName.toLowerCase().includes('home');
if (isReadmeA) return -1;
if (isReadmeB) return 1;
return a.title.localeCompare(b.title);
});
// Format a document link the same way everywhere
const formatDocumentLink = (document) => {
let title = document.title;
if ( document.outputName.split('-').slice(-1)[0].toLowerCase() === 'readme' ) {
title = 'Index (README.md)';
}
if ( document.outputName.split('-').slice(-1)[0].toLowerCase() === 'home' ) {
title = `Home`;
}
return `* [${title}](${document.outputName.replace('.md', '')})\n`;
};
// Recursive function to build sidebar sections
const buildSection = (docs, depth = 0, prefix = '') => {
let result = '';
const directDocs = [];
const subSections = new Map();
// Separate direct documents from those in subsections
for (const doc of docs) {
if (doc.sidebarPath.length <= depth + 1) {
// Direct document at this level
directDocs.push(doc);
} else {
// Document belongs in a subsection
const sectionName = doc.sidebarPath[depth];
if (!subSections.has(sectionName)) {
subSections.set(sectionName, []);
}
subSections.get(sectionName).push(doc);
}
}
// Add direct documents
for (const doc of directDocs) {
result += formatDocumentLink(doc);
}
// Process subsections recursively
for (const [sectionName, sectionDocs] of subSections.entries()) {
// Generate heading with appropriate level
const headingLevel = '#'.repeat(depth + 2);
const formattedName = format_name(sectionName)
result += `\n${headingLevel} ${formattedName}\n`;
// Process the subsection documents
result += buildSection(sectionDocs, depth + 1, `${prefix}${sectionName}/`);
}
return result;
};
// Start with the main heading
let sidebar = "## General\n\n";
// Split documents into top-level and those in sections
const topLevelDocs = documents.filter(doc => doc.sidebarPath.length <= 1);
const sectionDocs = documents.filter(doc => doc.sidebarPath.length > 1);
// Add top-level documents
for (const doc of topLevelDocs) {
sidebar += formatDocumentLink(doc);
}
// Group the remaining documents by their top-level sections
const topLevelSections = new Map();
for (const doc of sectionDocs) {
const sectionName = doc.sidebarPath[0];
if (!topLevelSections.has(sectionName)) {
topLevelSections.set(sectionName, []);
}
topLevelSections.get(sectionName).push(doc);
}
// Process each top-level section
for (const [sectionName, sectionDocs] of topLevelSections.entries()) {
const formattedName = format_name(sectionName);
sidebar += `\n## ${formattedName}\n`;
sidebar += buildSection(sectionDocs, 1, `${sectionName}/`);
}
return sidebar;
};
main();