/** * Convert repo docs/ tree (.md files, recursive) into man(7) page objects (same pipeline as handbook). */ import { readFile, readdir } from 'fs/promises' import { join, relative } from 'path' import { handbookMdToDescription } from './ingest-handbook-for-man.mjs' const SECTION = 7 function firstHeadingTitle(md) { const m = md.match(/^#\s+(.+)$/m) if (!m) return 'Bare OS documentation' let t = m[1] t = t.replace(/\*\*([^*]+)\*\*/g, '$1') t = t.replace(/`([^`]+)`/g, '$1') return t.trim() } /** * @param {string} absDir * @param {string} baseAbs * @returns {Promise} */ async function collectMdRelPaths(absDir, baseAbs) { /** @type {string[]} */ const out = [] const ents = await readdir(absDir, { withFileTypes: true }) ents.sort((a, b) => a.name.localeCompare(b.name)) for (const ent of ents) { const full = join(absDir, ent.name) if (ent.isDirectory()) { out.push(...(await collectMdRelPaths(full, baseAbs))) } else if (ent.isFile() && ent.name.endsWith('.md')) { out.push(relative(baseAbs, full).replace(/\\/g, '/')) } } return out } /** * @param {string} relPosix path under docs/, e.g. reference/foo.md */ function relPathToManMeta(relPosix) { if (relPosix === 'README.md') { return { rel: relPosix, name: 'bare-os-docs', title: 'Bare OS documentation (docs/) — hub, maps, and where to read next', aliases: ['docs', 'documentation', 'bare-os-documentation'] } } const base = relPosix.replace(/\.md$/i, '') const slug = base.split('/').map((s) => s.toLowerCase()).join('-') return { rel: relPosix, name: 'docs-' + slug, title: '', aliases: [] } } /** * @param {string} repoRoot * @returns {Promise} */ export async function buildDocsManPages(repoRoot) { const docsDir = join(repoRoot, 'docs') let relPaths try { relPaths = await collectMdRelPaths(docsDir, docsDir) } catch { return [] } relPaths.sort((a, b) => a.localeCompare(b)) /** @type {{ rel: string, name: string, title: string, aliases?: string[] }[]} */ const meta = relPaths.map(relPathToManMeta) const seen = new Set() for (const m of meta) { if (seen.has(m.name)) throw new Error(`ingest-docs-for-man: duplicate man name "${m.name}"`) seen.add(m.name) } const pages = [] for (let j = 0; j < meta.length; j++) { const m = meta[j] const filePath = join(docsDir, m.rel) const md = await readFile(filePath, 'utf8') const title = m.title || firstHeadingTitle(md) const description = handbookMdToDescription(md, 'docs/' + m.rel) const next = meta[j + 1] const prev = meta[j - 1] /** @type {{ name: string, section: number }[]} */ const seeAlso = [] if (next) seeAlso.push({ name: next.name, section: SECTION }) if (prev) seeAlso.push({ name: prev.name, section: SECTION }) seeAlso.push({ name: 'man', section: 1 }) seeAlso.push({ name: 'bare-os-handbook', section: SECTION }) const kw = new Set([ 'docs', 'documentation', 'bare-os', 'reference', 'markdown' ]) for (const t of m.name.split(/[^a-z0-9]+/i)) { if (t.length > 1) kw.add(t.toLowerCase()) } for (const seg of m.rel.replace(/\.md$/i, '').split(/[/\\]+/)) { for (const t of seg.toLowerCase().split(/[^a-z0-9]+/)) { if (t.length > 2) kw.add(t) } } for (const t of title.toLowerCase().split(/[^a-z0-9]+/)) { if (t.length > 2) kw.add(t) } const page = { name: m.name, section: SECTION, title, synopsis: [ 'man 7 ' + m.name, 'Documentation page (plain text from docs/' + m.rel + ')' ], description, descriptionMode: 'preserve', options: [], keywords: Array.from(kw), seeAlso, bareOsNotes: 'Generated at build time from docs/' + m.rel + '. Mermaid diagrams omitted in terminal; see repo Markdown for figures.' } if (m.aliases && m.aliases.length) page.aliases = m.aliases pages.push(page) } return pages }