213 lines
4.8 KiB
JavaScript
213 lines
4.8 KiB
JavaScript
/**
|
|
* 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<object[]>}
|
|
*/
|
|
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
|
|
}
|