| #!/usr/bin/env node
|
| |
| |
| |
| |
| |
|
|
|
|
| import { readdirSync, readFileSync, writeFileSync, statSync, existsSync, mkdirSync } from 'fs'
|
| import { join, basename, extname } from 'path'
|
| import { spawn } from 'child_process'
|
|
|
| const CONFIG = {
|
| inputDirs: ['src', 'lib', 'proofs', 'docs'],
|
| outputDir: 'docs/generated',
|
| extensions: ['.rs', '.lean', '.py', '.apl', '.hs', '.mjs', '.js', '.pl', '.md'],
|
| ignore: ['node_modules', 'target', 'build', '.git'],
|
| watch: process.argv.includes('--watch'),
|
| interval: 5000
|
| }
|
|
|
| function log(msg, level = 'info') {
|
| const colors = { error: '\x1b[31m', warn: '\x1b[33m', info: '\x1b[36m', success: '\x1b[32m' }
|
| const color = colors[level] || ''
|
| const reset = '\x1b[0m'
|
| console.log(`[${new Date().toISOString()}] ${color}${level.toUpperCase()}: ${msg}${reset}`)
|
| }
|
|
|
| function isIgnored(dir) {
|
| return CONFIG.ignore.includes(basename(dir))
|
| }
|
|
|
| function isValidFile(file) {
|
| return CONFIG.extensions.includes(extname(file).toLowerCase())
|
| }
|
|
|
| function extractDocComment(content, ext) {
|
| const patterns = {
|
| '.rs': /\/\*\*(.*?)\*\/|\/\/\!\s*(.*)/gs,
|
| '.lean': /\/\-\!(.*?)$/gm,
|
| '.py': /"""(.*?)"""|#\s*(.*)/gs,
|
| '.apl': /\⍝\s*(.*)/g,
|
| '.hs': /\-\-\!(.*?)$/gm,
|
| '.mjs': /\/\*\*(.*?)\*\/|\/\/\s*(.*)/gs,
|
| '.js': /\/\*\*(.*?)\*\/|\/\/\s*(.*)/gs,
|
| '.pl': /#\s*(.*)/g,
|
| '.md': /(.*?)/gs
|
| }
|
|
|
| const pattern = patterns[ext] || /\/\*(.*?)\*\//gs
|
| const matches = [...content.matchAll(pattern)]
|
|
|
| return matches.map(m => {
|
| const text = (m[1] || m[2] || m[0] || '').trim()
|
| return text ? text.replace(/^\s*[\/*#⍝\-]+\s*/gm, '').trim() : null
|
| }).filter(Boolean).join('\n\n')
|
| }
|
|
|
| function parseFunction(content, ext) {
|
|
|
| const signatures = []
|
|
|
| if (['.rs', '.mjs', '.js'].includes(ext)) {
|
| const funcPattern = /fn\s+(\w+)\s*\(([^)]*)\)\s*[^{]*{/g
|
| for (const match of content.matchAll(funcPattern)) {
|
| signatures.push(`fn ${match[1]}(${match[2]})`)
|
| }
|
| }
|
|
|
| if (['.lean'].includes(ext)) {
|
| const funcPattern = /def\s+(\w+)\s*\(([^)]*)\)\s*:/g
|
| for (const match of content.matchAll(funcPattern)) {
|
| signatures.push(`def ${match[1]}(${match[2]})`)
|
| }
|
| }
|
|
|
| if (['.py'].includes(ext)) {
|
| const funcPattern = /def\s+(\w+)\s*\(([^)]*)\)\s*:/g
|
| for (const match of content.matchAll(funcPattern)) {
|
| signatures.push(`def ${match[1]}(${match[2]})`)
|
| }
|
| }
|
|
|
| return signatures
|
| }
|
|
|
| function generateFileDoc(filePath) {
|
| try {
|
| const content = readFileSync(filePath, 'utf8')
|
| const ext = extname(filePath)
|
| const relativePath = filePath.replace(process.cwd() + '/', '')
|
| const docComment = extractDocComment(content, ext)
|
| const functions = parseFunction(content, ext)
|
|
|
| let doc = `# ${relativePath}\n\n`
|
|
|
| if (docComment) {
|
| doc += `## Description\n\n${docComment}\n\n`
|
| }
|
|
|
| if (functions.length > 0) {
|
| doc += `## Functions/Definitions\n\n`
|
| doc += functions.map(f => `- \`${f}\``).join('\n')
|
| doc += '\n\n'
|
| }
|
|
|
| doc += `---\n\n`
|
| doc += `[Back to index](./index.md)\n`
|
|
|
| return doc
|
| } catch (e) {
|
| log(`Error processing ${filePath}: ${e.message}`, 'warn')
|
| return null
|
| }
|
| }
|
|
|
| function scanFiles(dir = '.') {
|
| const files = []
|
|
|
| try {
|
| const entries = readdirSync(dir)
|
|
|
| for (const entry of entries) {
|
| const fullPath = join(dir, entry)
|
| const stat = statSync(fullPath)
|
|
|
| if (isIgnored(fullPath)) continue
|
|
|
| if (stat.isDirectory()) {
|
| files.push(...scanFiles(fullPath))
|
| } else if (stat.isFile() && isValidFile(fullPath)) {
|
| files.push(fullPath)
|
| }
|
| }
|
| } catch (e) {
|
| log(`Error scanning ${dir}: ${e.message}`, 'warn')
|
| }
|
|
|
| return files
|
| }
|
|
|
| function generateIndex(files) {
|
| const grouped = {}
|
|
|
| for (const file of files) {
|
| const ext = extname(file)
|
| const dir = basename(join(file, '..'))
|
|
|
| if (!grouped[ext]) grouped[ext] = []
|
| if (!grouped[dir]) grouped[dir] = []
|
|
|
| const relative = file.replace(process.cwd() + '/', '')
|
| grouped[ext].push(relative)
|
| grouped[dir].push(relative)
|
| }
|
|
|
| let index = '# Documentation Index\n\n'
|
| index += 'Automatically generated from source code\n\n'
|
| index += `---\n\n`
|
|
|
|
|
| index += '## By Directory\n\n'
|
| for (const [dir, files] of Object.entries(grouped)) {
|
| if (dir.startsWith('.')) continue
|
| index += `\n### ${dir}/\n\n`
|
| for (const file of files.sort()) {
|
| const link = file.replace(/\\/g, '/').replace(/ /g, '%20')
|
| index += `- [${file}](${link}.md)\n`
|
| }
|
| }
|
|
|
|
|
| index += '\n\n## By Type\n\n'
|
| for (const [ext, files] of Object.entries(grouped)) {
|
| if (!ext.startsWith('.')) continue
|
| const type = ext.substring(1).toUpperCase()
|
| index += `\n### ${type}\n\n`
|
| for (const file of files.sort()) {
|
| const link = file.replace(/\\/g, '/').replace(/ /g, '%20')
|
| index += `- [${file}](${link}.md)\n`
|
| }
|
| }
|
|
|
| return index
|
| }
|
|
|
| function generateAll() {
|
| log('Starting documentation generation', 'info')
|
|
|
|
|
| if (!existsSync(CONFIG.outputDir)) {
|
| mkdirSync(CONFIG.outputDir, { recursive: true })
|
| }
|
|
|
|
|
| const files = scanFiles()
|
| log(`Found ${files.length} source files`, 'info')
|
|
|
|
|
| const index = generateIndex(files)
|
| writeFileSync(join(CONFIG.outputDir, 'index.md'), index)
|
| log('Generated index.md', 'info')
|
|
|
|
|
| for (const file of files) {
|
| const doc = generateFileDoc(file)
|
| if (doc) {
|
| const relativePath = file.replace(process.cwd() + '/', '')
|
| const outputPath = join(CONFIG.outputDir, relativePath + '.md')
|
|
|
|
|
| const outputDir = join(CONFIG.outputDir, join(file, '..').replace(process.cwd() + '/', ''))
|
| if (!existsSync(outputDir)) {
|
| mkdirSync(outputDir, { recursive: true })
|
| }
|
|
|
| writeFileSync(outputPath, doc)
|
| }
|
| }
|
|
|
| log(`Generated documentation for ${files.length} files`, 'success')
|
|
|
|
|
| try {
|
| spawn('git', ['add', CONFIG.outputDir], { stdio: 'inherit' })
|
| spawn('git', ['commit', '-m', 'Update generated docs'], { stdio: 'inherit' })
|
| spawn('git', ['push'], { stdio: 'inherit' })
|
| log('Committed and pushed documentation', 'success')
|
| } catch (e) {
|
| log(`Could not commit docs: ${e.message}`, 'warn')
|
| }
|
| }
|
|
|
| function watch() {
|
| log('Watching for changes...', 'info')
|
| generateAll()
|
|
|
| setInterval(() => {
|
| generateAll()
|
| }, CONFIG.interval)
|
| }
|
|
|
|
|
| if (CONFIG.watch) {
|
| watch()
|
| } else {
|
| generateAll()
|
| }
|
|
|