doc: generate wiki from repository docs

This commit is contained in:
KernelDeimos
2025-02-25 14:56:12 -05:00
parent 7f372fd1be
commit 99684d80e4
3 changed files with 253 additions and 5 deletions

View File

@@ -1,12 +1,26 @@
# Meta Documentation
## Notes
Guidelines for documentation.
- > This document is a work-in-progress
## How documentation is organized
## Rules
This documentation exists in the Puter repository.
You may be reading this on the GitHub wiki instead, which we generate
from the repository docs. These docs are always under a directory
named `doc/`.
From [./contributors/structure.md](./contributors/structure.md):
> The top-level `doc` directory contains the file you're reading right now.
> Its scope is documentation for using and contributing to Puter in general,
> and linking to more specific documentation in other places.
>
> All `doc` directories will have a `README.md` which should be considered as
> the index file for the documentation. All documentation under a `doc`
> directory should be accessible via a path of links starting from `README.md`.
## Docs Styleguide
### "is" and "is not"
- When "A is B", always bold "is": "A **is** B" (`A **is** B`)
- When "A is not B", always bold "not": "A is **not** B" (`A is **not** B`)
- When "A is B", bold "is": "A **is** B" (`A **is** B`)
- When "A is not B", bold "not": "A is **not** B" (`A is **not** B`)

223
tools/genwiki/main.js Normal file
View File

@@ -0,0 +1,223 @@
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 => {
// 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');
const isReadmeB = b.outputName.toLowerCase().includes('readme');
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)';
}
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();

View File

@@ -0,0 +1,11 @@
{
"name": "genwiki",
"version": "0.0.0",
"description": "Generate github wiki",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Puter Technologies Inc.",
"license": "AGPL-3.0-only"
}