/** * Convert repo handbook/*.md into man(7) page objects merged at build time. */ import { readFile, readdir } from 'fs/promises' import { join } from 'path' const SECTION = 7 function stripMdInline(s) { let t = String(s) t = t.replace(/\[([^\]]*)\]\(([^)]+)\)/g, '$1 <$2>') t = t.replace(/\*\*([^*]+)\*\*/g, '$1') t = t.replace(/__([^_]+)__/g, '$1') t = t.replace(/\*([^*]+)\*/g, '$1') t = t.replace(/`([^`]+)`/g, '$1') return t.trim() } /** * @param {string} md * @param {string} sourceFile */ export function handbookMdToDescription(md, sourceFile) { const lines = md.split('\n') const out = [] let i = 0 let inFence = false /** @type {string} */ let fenceKind = '' while (i < lines.length) { const line = lines[i] const fenceM = line.match(/^(\s*)```(\w*)\s*$/) if (fenceM) { if (!inFence) { inFence = true fenceKind = fenceM[2] || '' i++ continue } inFence = false fenceKind = '' i++ continue } if (inFence) { if (fenceKind === 'mermaid') { i++ continue } out.push(' ' + line) i++ continue } const t = line.trim() if (/^---+$/.test(t)) { out.push('') i++ continue } const h = line.match(/^(#{1,6})\s+(.*)$/) if (h) { const text = stripMdInline(h[2]) if (text) { out.push('') out.push(text.toUpperCase()) out.push('') } i++ continue } if (/^\s*[-*]\s+/.test(line)) { out.push(stripMdInline(line.replace(/^\s+/, ''))) i++ continue } if (/^\s*\d+\.\s+/.test(line)) { out.push(stripMdInline(line.trim())) i++ continue } if (t.startsWith('|')) { if (/^\|[\s-:|]+\|$/.test(t)) { i++ continue } out.push(stripMdInline(t)) i++ continue } if (!t) { out.push('') i++ continue } out.push(stripMdInline(line.trim())) i++ } let body = out .join('\n') .replace(/\n{3,}/g, '\n\n') .trim() if (!body) body = '(empty chapter — see ' + sourceFile + ' in the repo)' return body } function firstHeadingTitle(md) { const m = md.match(/^#\s+(.+)$/m) return m ? stripMdInline(m[1]) : 'Bare OS handbook' } /** * @param {string} repoRoot * @returns {Promise} */ export async function buildHandbookManPages(repoRoot) { const handbookDir = join(repoRoot, 'handbook') let names try { names = (await readdir(handbookDir)).filter((f) => f.endsWith('.md')) } catch { return [] } names.sort((a, b) => { if (a === 'README.md') return -1 if (b === 'README.md') return 1 return a.localeCompare(b) }) /** @type {{ file: string, name: string, title: string, aliases?: string[] }[]} */ const meta = [] for (const file of names) { if (file === 'README.md') { meta.push({ file, name: 'bare-os-handbook', title: 'Bare OS handbook — table of contents and reading order', aliases: ['handbook', 'bare-os-handbook-index'] }) } else { const base = file.replace(/\.md$/i, '') meta.push({ file, name: 'handbook-' + base, title: '', aliases: [] }) } } const pages = [] for (let j = 0; j < meta.length; j++) { const m = meta[j] const path = join(handbookDir, m.file) const md = await readFile(path, 'utf8') const title = m.title || firstHeadingTitle(md) const description = handbookMdToDescription(md, 'handbook/' + m.file) 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 }) const kw = new Set([ 'handbook', 'bare-os', 'documentation', 'narrative', 'chapter' ]) for (const t of m.name.split(/[^a-z0-9]+/i)) { if (t.length > 1) kw.add(t.toLowerCase()) } 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, 'Handbook chapter (plain text from handbook/' + m.file + ')' ], description, descriptionMode: 'preserve', options: [], keywords: Array.from(kw), seeAlso, bareOsNotes: 'Generated at build time from handbook/' + m.file + '. Diagrams in ```mermaid blocks are omitted; open the Markdown in the repo for figures.' } if (m.aliases && m.aliases.length) page.aliases = m.aliases pages.push(page) } return pages }