Files
bare-operating-system/packages/bare-os-coreutils/scripts/build-man-db.mjs
T
2026-04-03 05:43:13 -04:00

338 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readFile, writeFile, mkdir } from 'fs/promises'
import { dirname, join } from 'path'
import { fileURLToPath, pathToFileURL } from 'url'
import {
COREUTILS_COMMANDS,
MAN_EXTRA_PAGES
} from '../lib/commands.mjs'
import { buildHandbookManPages } from './ingest-handbook-for-man.mjs'
import { buildDeveloperGuideManPages } from './ingest-developer-guide-for-man.mjs'
const __dirname = dirname(fileURLToPath(import.meta.url))
const pkgRoot = join(__dirname, '..')
const repoRoot = join(pkgRoot, '../..')
const pagesDir = join(pkgRoot, 'man', 'pages')
const kernelManDir = join(repoRoot, 'kernel', 'share', 'man')
const seederManDir = join(
repoRoot,
'packages',
'bare-os-seeder',
'kernel',
'share',
'man'
)
function isPlainObject(x) {
return x !== null && typeof x === 'object' && !Array.isArray(x)
}
function validatePage(raw, pathLabel) {
if (!isPlainObject(raw)) throw new Error(`${pathLabel}: root must be object`)
const allowed = new Set([
'name',
'section',
'title',
'synopsis',
'description',
'options',
'environment',
'files',
'exitStatus',
'diagnostics',
'seeAlso',
'bareOsNotes',
'keywords',
'aliases',
'stub',
'builtins',
'examples',
'descriptionMode',
'listCategory'
])
for (const k of Object.keys(raw)) {
if (!allowed.has(k)) throw new Error(`${pathLabel}: unknown field "${k}"`)
}
const {
name,
section,
title,
synopsis,
description,
options,
keywords
} = raw
if (typeof name !== 'string' || !name)
throw new Error(`${pathLabel}: name must be non-empty string`)
if (typeof section !== 'number' || section < 1 || section > 8)
throw new Error(`${pathLabel}: section must be integer 18`)
if (typeof title !== 'string' || !title)
throw new Error(`${pathLabel}: title must be non-empty string`)
if (!Array.isArray(synopsis) || synopsis.length < 1)
throw new Error(`${pathLabel}: synopsis must be non-empty array`)
for (const s of synopsis) {
if (typeof s !== 'string')
throw new Error(`${pathLabel}: synopsis entries must be strings`)
}
if (typeof description !== 'string' || !description)
throw new Error(`${pathLabel}: description must be non-empty string`)
if (!Array.isArray(options))
throw new Error(`${pathLabel}: options must be array`)
for (const o of options) {
if (!isPlainObject(o) || typeof o.flag !== 'string' || typeof o.meaning !== 'string')
throw new Error(`${pathLabel}: each option needs { flag, meaning }`)
}
if (!Array.isArray(keywords))
throw new Error(`${pathLabel}: keywords must be array`)
for (const kw of keywords) {
if (typeof kw !== 'string')
throw new Error(`${pathLabel}: keywords entries must be strings`)
}
if (raw.environment !== undefined) {
if (!Array.isArray(raw.environment))
throw new Error(`${pathLabel}: environment must be array of strings`)
for (const e of raw.environment) {
if (typeof e !== 'string') throw new Error(`${pathLabel}: environment must be strings`)
}
}
if (raw.files !== undefined) {
if (!Array.isArray(raw.files))
throw new Error(`${pathLabel}: files must be array of strings`)
for (const f of raw.files) {
if (typeof f !== 'string') throw new Error(`${pathLabel}: files must be strings`)
}
}
if (raw.exitStatus !== undefined) {
if (!Array.isArray(raw.exitStatus))
throw new Error(`${pathLabel}: exitStatus must be array of strings`)
for (const e of raw.exitStatus) {
if (typeof e !== 'string') throw new Error(`${pathLabel}: exitStatus must be strings`)
}
}
if (raw.diagnostics !== undefined) {
if (!Array.isArray(raw.diagnostics))
throw new Error(`${pathLabel}: diagnostics must be array of strings`)
for (const d of raw.diagnostics) {
if (typeof d !== 'string') throw new Error(`${pathLabel}: diagnostics must be strings`)
}
}
if (raw.seeAlso !== undefined) {
if (!Array.isArray(raw.seeAlso))
throw new Error(`${pathLabel}: seeAlso must be array`)
for (const ref of raw.seeAlso) {
if (
!isPlainObject(ref) ||
typeof ref.name !== 'string' ||
typeof ref.section !== 'number'
)
throw new Error(`${pathLabel}: seeAlso entries need { name, section }`)
}
}
if (raw.bareOsNotes !== undefined && typeof raw.bareOsNotes !== 'string')
throw new Error(`${pathLabel}: bareOsNotes must be string`)
if (raw.descriptionMode !== undefined) {
if (raw.descriptionMode !== 'wrap' && raw.descriptionMode !== 'preserve')
throw new Error(`${pathLabel}: descriptionMode must be "wrap" or "preserve"`)
}
if (raw.listCategory !== undefined) {
const ok = ['coreutils', 'extra', 'handbook', 'devguide']
if (!ok.includes(raw.listCategory))
throw new Error(`${pathLabel}: listCategory must be one of ${ok.join(', ')}`)
}
if (raw.aliases !== undefined) {
if (!Array.isArray(raw.aliases))
throw new Error(`${pathLabel}: aliases must be array of strings`)
for (const a of raw.aliases) {
if (typeof a !== 'string') throw new Error(`${pathLabel}: aliases must be strings`)
}
}
if (raw.stub !== undefined && typeof raw.stub !== 'boolean')
throw new Error(`${pathLabel}: stub must be boolean`)
if (raw.examples !== undefined) {
if (!Array.isArray(raw.examples))
throw new Error(`${pathLabel}: examples must be array`)
for (const ex of raw.examples) {
if (!isPlainObject(ex) || typeof ex.code !== 'string' || !ex.code.trim())
throw new Error(`${pathLabel}: each example needs non-empty { code }`)
if (ex.caption !== undefined && typeof ex.caption !== 'string')
throw new Error(`${pathLabel}: example caption must be string`)
}
}
if (raw.builtins !== undefined) {
if (!Array.isArray(raw.builtins))
throw new Error(`${pathLabel}: builtins must be array`)
for (const b of raw.builtins) {
if (!isPlainObject(b) || typeof b.name !== 'string' || typeof b.description !== 'string')
throw new Error(`${pathLabel}: each builtin needs { name, description }`)
if (b.synopsis !== undefined) {
if (!Array.isArray(b.synopsis))
throw new Error(`${pathLabel}: builtin synopsis must be array`)
for (const s of b.synopsis) {
if (typeof s !== 'string') throw new Error(`${pathLabel}: builtin synopsis strings`)
}
}
if (b.options !== undefined) {
if (!Array.isArray(b.options))
throw new Error(`${pathLabel}: builtin options must be array`)
for (const o of b.options) {
if (!isPlainObject(o) || typeof o.flag !== 'string' || typeof o.meaning !== 'string')
throw new Error(`${pathLabel}: builtin option { flag, meaning }`)
}
}
if (b.examples !== undefined) {
if (!Array.isArray(b.examples))
throw new Error(`${pathLabel}: builtin examples must be array`)
for (const ex of b.examples) {
if (!isPlainObject(ex) || typeof ex.code !== 'string' || !ex.code.trim())
throw new Error(`${pathLabel}: builtin example needs { code }`)
if (ex.caption !== undefined && typeof ex.caption !== 'string')
throw new Error(`${pathLabel}: builtin example caption must be string`)
}
}
}
}
}
function tokenizeForApropos(text) {
return String(text)
.toLowerCase()
.split(/[^a-z0-9_-]+/)
.filter(Boolean)
}
export async function buildManDb() {
const required = [...COREUTILS_COMMANDS, ...MAN_EXTRA_PAGES]
const extraSet = new Set(MAN_EXTRA_PAGES)
const pages = []
const index = Object.create(null)
const apropos = []
for (let i = 0; i < required.length; i++) {
const cmd = required[i]
const filePath = join(pagesDir, `${cmd}.json`)
let raw
try {
raw = JSON.parse(await readFile(filePath, 'utf8'))
} catch (e) {
throw new Error(`man: missing or invalid JSON for "${cmd}": ${filePath} (${e.message})`)
}
raw.listCategory = extraSet.has(cmd) ? 'extra' : 'coreutils'
validatePage(raw, cmd)
if (raw.name !== cmd)
throw new Error(`man/pages/${cmd}.json: "name" must be "${cmd}", got "${raw.name}"`)
pages.push(raw)
const pageIdx = pages.length - 1
const addIndex = (key) => {
const k = String(key).toLowerCase()
if (index[k] !== undefined && index[k] !== pageIdx)
throw new Error(`man: duplicate index key "${k}"`)
index[k] = pageIdx
}
addIndex(raw.name)
if (Array.isArray(raw.aliases)) {
for (const a of raw.aliases) addIndex(a)
}
const kwSet = new Set()
for (const k of raw.keywords) kwSet.add(k.toLowerCase())
kwSet.add(raw.name.toLowerCase())
for (const t of tokenizeForApropos(raw.title)) kwSet.add(t)
if (Array.isArray(raw.examples)) {
for (const ex of raw.examples) {
if (ex.caption) {
for (const t of tokenizeForApropos(ex.caption)) kwSet.add(t)
}
}
}
for (const kw of kwSet) {
apropos.push({ kw, pageRef: pageIdx })
}
}
const handbookPages = await buildHandbookManPages(repoRoot)
for (let hi = 0; hi < handbookPages.length; hi++) {
const raw = handbookPages[hi]
raw.listCategory = 'handbook'
validatePage(raw, raw.name)
pages.push(raw)
const pageIdx = pages.length - 1
const addIndex = (key) => {
const k = String(key).toLowerCase()
if (index[k] !== undefined && index[k] !== pageIdx)
throw new Error(`man: duplicate index key "${k}"`)
index[k] = pageIdx
}
addIndex(raw.name)
if (Array.isArray(raw.aliases)) {
for (const a of raw.aliases) addIndex(a)
}
const kwSet = new Set()
for (const k of raw.keywords) kwSet.add(k.toLowerCase())
kwSet.add(raw.name.toLowerCase())
for (const t of tokenizeForApropos(raw.title)) kwSet.add(t)
if (Array.isArray(raw.examples)) {
for (const ex of raw.examples) {
if (ex.caption) {
for (const t of tokenizeForApropos(ex.caption)) kwSet.add(t)
}
}
}
for (const kw of kwSet) {
apropos.push({ kw, pageRef: pageIdx })
}
}
const developerGuidePages = await buildDeveloperGuideManPages(repoRoot)
for (let di = 0; di < developerGuidePages.length; di++) {
const raw = developerGuidePages[di]
raw.listCategory = 'devguide'
validatePage(raw, raw.name)
pages.push(raw)
const pageIdx = pages.length - 1
const addIndex = (key) => {
const k = String(key).toLowerCase()
if (index[k] !== undefined && index[k] !== pageIdx)
throw new Error(`man: duplicate index key "${k}"`)
index[k] = pageIdx
}
addIndex(raw.name)
if (Array.isArray(raw.aliases)) {
for (const a of raw.aliases) addIndex(a)
}
const kwSet = new Set()
for (const k of raw.keywords) kwSet.add(k.toLowerCase())
kwSet.add(raw.name.toLowerCase())
for (const t of tokenizeForApropos(raw.title)) kwSet.add(t)
if (Array.isArray(raw.examples)) {
for (const ex of raw.examples) {
if (ex.caption) {
for (const t of tokenizeForApropos(ex.caption)) kwSet.add(t)
}
}
}
for (const kw of kwSet) {
apropos.push({ kw, pageRef: pageIdx })
}
}
const out = {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
pages,
index,
apropos
}
const json = JSON.stringify(out, null, 0) + '\n'
await mkdir(kernelManDir, { recursive: true })
await mkdir(seederManDir, { recursive: true })
await writeFile(join(kernelManDir, 'man.json'), json)
await writeFile(join(seederManDir, 'man.json'), json)
}
const isMain =
process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
if (isMain) {
await buildManDb()
}