Dev guide
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
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, '..')
|
||||
@@ -46,7 +47,8 @@ function validatePage(raw, pathLabel) {
|
||||
'stub',
|
||||
'builtins',
|
||||
'examples',
|
||||
'descriptionMode'
|
||||
'descriptionMode',
|
||||
'listCategory'
|
||||
])
|
||||
for (const k of Object.keys(raw)) {
|
||||
if (!allowed.has(k)) throw new Error(`${pathLabel}: unknown field "${k}"`)
|
||||
@@ -132,6 +134,11 @@ function validatePage(raw, pathLabel) {
|
||||
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`)
|
||||
@@ -195,6 +202,7 @@ function tokenizeForApropos(text) {
|
||||
|
||||
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 = []
|
||||
@@ -208,6 +216,7 @@ export async function buildManDb() {
|
||||
} 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}"`)
|
||||
@@ -242,6 +251,40 @@ export async function buildManDb() {
|
||||
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
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Convert repo developer-guide/*.md into man(7) page objects (same pipeline as handbook).
|
||||
*/
|
||||
import { readFile, readdir } from 'fs/promises'
|
||||
import { join } 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 developer guide'
|
||||
let t = m[1]
|
||||
t = t.replace(/\*\*([^*]+)\*\*/g, '$1')
|
||||
t = t.replace(/`([^`]+)`/g, '$1')
|
||||
return t.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} repoRoot
|
||||
* @returns {Promise<object[]>}
|
||||
*/
|
||||
export async function buildDeveloperGuideManPages(repoRoot) {
|
||||
const dir = join(repoRoot, 'developer-guide')
|
||||
let names
|
||||
try {
|
||||
names = (await readdir(dir)).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-developer-guide',
|
||||
title: 'Bare OS developer guide — index and reading order',
|
||||
aliases: ['developer-guide', 'devguide']
|
||||
})
|
||||
} else {
|
||||
const base = file.replace(/\.md$/i, '')
|
||||
meta.push({
|
||||
file,
|
||||
name: 'devguide-' + base,
|
||||
title: '',
|
||||
aliases: []
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const pages = []
|
||||
for (let j = 0; j < meta.length; j++) {
|
||||
const m = meta[j]
|
||||
const filePath = join(dir, m.file)
|
||||
const md = await readFile(filePath, 'utf8')
|
||||
const title = m.title || firstHeadingTitle(md)
|
||||
const description = handbookMdToDescription(md, 'developer-guide/' + 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 })
|
||||
seeAlso.push({ name: 'bare-os-handbook', section: SECTION })
|
||||
|
||||
const kw = new Set([
|
||||
'developer',
|
||||
'devguide',
|
||||
'develop',
|
||||
'script',
|
||||
'asyncfunction',
|
||||
'ctx',
|
||||
'bare-os',
|
||||
'guide'
|
||||
])
|
||||
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,
|
||||
'Developer guide chapter (developer-guide/' + m.file + ')'
|
||||
],
|
||||
description,
|
||||
descriptionMode: 'preserve',
|
||||
options: [],
|
||||
keywords: Array.from(kw),
|
||||
seeAlso,
|
||||
bareOsNotes:
|
||||
'Generated at build time from developer-guide/' +
|
||||
m.file +
|
||||
'. 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
|
||||
}
|
||||
@@ -448,15 +448,23 @@ EXTRA.man = {
|
||||
'man reads /share/man/man.json on the system drive.'
|
||||
],
|
||||
description:
|
||||
'Displays manual pages from the merged JSON database. Section 1 only in this release.',
|
||||
'Displays manual pages from the merged JSON database. Section 1: /bin and git/shell pages. Section 7: handbook (man handbook) and developer guide (man devguide), merged at build from handbook/*.md and developer-guide/*.md.',
|
||||
options: [
|
||||
{ flag: '-k, --apropos', meaning: 'Search keywords and titles (substring)' },
|
||||
{ flag: '-f, --whatis', meaning: 'One-line description for exact name' },
|
||||
{ flag: '-l, --list', meaning: 'List all manual page names' }
|
||||
{
|
||||
flag: '-l, --list',
|
||||
meaning:
|
||||
'List pages grouped by category (/bin, git/shell, handbook, developer guide), then alphabetically'
|
||||
}
|
||||
],
|
||||
environment: ['MANWIDTH — wrap width (default 72, min 40)', 'NO_COLOR — disable bold headings on TTY'],
|
||||
keywords: ['man', 'manual', 'help', 'documentation', 'apropos', 'whatis', 'cheat', 'examples'],
|
||||
seeAlso: [{ name: 'help', section: 1 }],
|
||||
seeAlso: [
|
||||
{ name: 'help', section: 1 },
|
||||
{ name: 'bare-os-handbook', section: 7 },
|
||||
{ name: 'bare-os-developer-guide', section: 7 }
|
||||
],
|
||||
bareOsNotes: 'No troff; no embedded DB fallback in v1.'
|
||||
}
|
||||
EXTRA.help = {
|
||||
|
||||
Reference in New Issue
Block a user