thefoldwithin-earth/tools/generate-index.mjs

107 lines
3.8 KiB
JavaScript
Raw Normal View History

2025-11-09 19:37:10 +00:00
// ΔFIELD: Node.js script to generate public/index.json from filesystem.
// Rationale: Async fs for efficiency; walks public/ excluding tools/ and index.json.
// Dependencies: fs/promises, path, gray-matter (for frontmatter parsing).
// Install: npm install gray-matter
// Usage: node tools/generate-index.mjs
import fs from 'fs/promises';
import path from 'path';
import matter from 'gray-matter';
const PUBLIC_DIR = path.join(process.cwd(), 'public');
const EXCLUDE_DIRS = ['tools'];
const INDEX_FILE = 'index.json';
const EXCERPT_LENGTH = 200;
// ΔRECURSION: Walk directory recursively, collect file metadata.
async function walk(dir, base = '') {
const entries = await fs.readdir(dir, { withFileTypes: true });
let files = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const relPath = path.join(base, entry.name);
if (entry.isDirectory()) {
if (!EXCLUDE_DIRS.includes(entry.name)) {
files = [...files, ...(await walk(fullPath, relPath))];
}
} else if (['.md', '.html'].includes(path.extname(entry.name))) {
const stats = await fs.stat(fullPath);
const content = await fs.readFile(fullPath, 'utf-8');
const { data: frontmatter, content: body } = matter(content);
const title = frontmatter.title || extractTitle(body) || entry.name.replace(/\.[^/.]+$/, '');
const excerpt = extractExcerpt(body).slice(0, EXCERPT_LENGTH) + '...';
const tags = frontmatter.tags || [];
const isPinned = !!frontmatter.pinned;
const isIndex = entry.name.startsWith('index.');
files.push({
path: relPath.replace(/\\/g, '/'), // Normalize to /
title,
tags,
isIndex,
isPinned,
ctime: stats.birthtimeMs,
mtime: stats.mtimeMs,
ext: path.extname(entry.name),
excerpt
});
}
2025-11-08 16:07:52 -06:00
}
2025-11-09 19:37:10 +00:00
return files;
2025-11-08 16:07:52 -06:00
}
2025-11-09 19:37:10 +00:00
// ΔTRUTH: Extract title from first # in MD or <title>/<h1> in HTML.
function extractTitle(content) {
const mdMatch = content.match(/^#+\s*(.+)/m);
if (mdMatch) return mdMatch[1].trim();
const htmlMatch = content.match(/<title>(.+?)<\/title>|<h1>(.+?)<\/h1>/i);
return htmlMatch ? (htmlMatch[1] || htmlMatch[2]).trim() : '';
}
2025-11-08 18:21:53 -06:00
2025-11-09 19:37:10 +00:00
// ΔTRUTH: Extract first non-empty paragraph.
function extractExcerpt(content) {
const lines = content.split('\n').filter(line => line.trim());
let excerpt = '';
for (const line of lines) {
if (!line.startsWith('#') && !line.startsWith('<')) {
excerpt += line + ' ';
if (excerpt.length > EXCERPT_LENGTH) break;
2025-11-08 15:47:51 -06:00
}
2025-11-08 11:30:41 -06:00
}
2025-11-09 19:37:10 +00:00
return excerpt.trim();
2025-11-08 11:30:41 -06:00
}
2025-11-09 19:37:10 +00:00
// ΔRECURSION: Build hierarchies from paths.
function buildHierarchies(files) {
const hierarchies = {};
files.forEach(file => {
const parts = file.path.split('/').slice(0, -1);
for (let i = 0; i < parts.length; i++) {
const parent = parts.slice(0, i).join('/') || null;
const child = parts[i];
if (parent) {
2025-11-08 23:24:54 -06:00
if (!hierarchies[parent]) hierarchies[parent] = [];
2025-11-09 19:37:10 +00:00
if (!hierarchies[parent].includes(child)) hierarchies[parent].push(child);
} else {
if (!hierarchies.root) hierarchies.root = [];
if (!hierarchies.root.includes(child)) hierarchies.root.push(child);
2025-11-08 23:24:54 -06:00
}
}
2025-11-09 19:37:10 +00:00
});
delete hierarchies.root; // Focus on section-level
Object.values(hierarchies).forEach(subs => subs.sort());
return hierarchies;
}
2025-11-08 23:24:54 -06:00
2025-11-09 19:37:10 +00:00
// ΔFIELD: Main execution.
async function generateIndex() {
const flat = await walk(PUBLIC_DIR);
const sections = [...new Set(flat.map(f => f.path.split('/')[0]).filter(Boolean))].sort();
const tags = [...new Set(flat.flatMap(f => f.tags))].sort();
const hierarchies = buildHierarchies(flat);
const indexData = { flat, sections, tags, hierarchies };
await fs.writeFile(path.join(PUBLIC_DIR, INDEX_FILE), JSON.stringify(indexData, null, 2));
console.log('index.json generated.');
}
generateIndex().catch(console.error);