361 lines
12 KiB
JavaScript
361 lines
12 KiB
JavaScript
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 1–8`)
|
||
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()
|
||
}
|