/**
* The forge's own documentation, served from pear-git-v3/docs/*.md.
*
* GET /docs → index of every doc (renderDocsIndex)
* GET /docs/<slug> → one rendered doc (renderDoc)
*
* Slug is constrained to [a-z0-9-]+, so there is no path traversal and
* only files directly under docs/ are reachable (no subdirectories).
*
* Markdown is rendered client-side by static/pgv3-markdown.js — the
* source travels as base64 in a data attribute, never as JSON inside an
* inline <script> (which breaks on any doc containing `</script`).
*/
import { readFile, readdir } from '#node:fs/promises'
import { join, dirname } from '#node:path'
import { fileURLToPath } from '#node:url'
import { layout, escapeHtml, markdownBody } from '../helpers.js'
// docs/ lives at the pear-git-v3 package root, three levels above this file
const __dirname = dirname(fileURLToPath(import.meta.url))
const DOCS_ROOT = join(__dirname, '..', '..', '..', 'docs')
const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/
/** First `# Heading` in a markdown source, or null. */
function titleOf (source) {
const m = source.match(/^#\s+(.+?)\s*$/m)
return m ? m[1] : null
}
/**
* Every servable doc: files directly under docs/ whose basename is a
* valid slug. Sorted by title. A doc that can't be read is skipped
* rather than failing the index.
*/
async function listDocs () {
let names
try {
names = await readdir(DOCS_ROOT)
} catch {
return []
}
const out = []
for (const name of names) {
if (!name.endsWith('.md')) continue
const slug = name.slice(0, -3)
if (!SLUG_RE.test(slug)) continue
let source
try {
source = await readFile(join(DOCS_ROOT, name), 'utf8')
} catch {
continue
}
out.push({
slug,
title: titleOf(source) || slug,
// First non-heading, non-blank line, as a one-line blurb.
summary: (source.split('\n').find(
(l) => l.trim() && !l.startsWith('#')
) || '').trim().slice(0, 180)
})
}
out.sort((a, b) => a.title.localeCompare(b.title))
return out
}
export async function renderDocsIndex (ctx) {
const prefix = (ctx && ctx.prefix) || ''
const docs = await listDocs()
const list = docs.length === 0
? '<div class="empty">No documentation found on this daemon.</div>'
: docs.map((d) => `
<div class="item">
<div><a class="title" href="${prefix}/docs/${encodeURIComponent(d.slug)}">${escapeHtml(d.title)}</a></div>
<div class="meta">${escapeHtml(d.slug)}${d.summary ? ' — ' + escapeHtml(d.summary) : ''}</div>
</div>`).join('')
const body = `
<div class="card">
<h2>Docs</h2>
<div class="meta" style="margin-bottom:0.8rem">
Operating documentation for this pear-git daemon. Per-repo documentation
lives on each repo's <strong>Docs</strong> tab.
</div>
${list}
</div>`
return layout('docs', '/docs', body, ctx)
}
export async function renderDoc (ctx, slug) {
const prefix = (ctx && ctx.prefix) || ''
if (!SLUG_RE.test(slug)) {
return layoutError('Invalid doc name', ctx)
}
let source
try {
source = await readFile(join(DOCS_ROOT, slug + '.md'), 'utf8')
} catch (err) {
if (err.code === 'ENOENT') return layoutError('Doc not found: ' + slug, ctx)
throw err
}
const title = titleOf(source) || slug
const body = `
<div class="card">
<h2>${escapeHtml(title)}</h2>
<div class="meta" style="margin-bottom:0.8rem;font-size:0.7rem">
<a href="${prefix}/docs">← all docs</a>
</div>
${markdownBody(source, {
id: 'doc-content',
// Sibling docs are addressed by SLUG, so './origin-setup.md' has to
// land on /docs/origin-setup. Links that climb above docs/ (e.g.
// '../ROADMAP.md') have no route and are left untouched.
linkBase: prefix + '/docs/',
stripExt: 'md,markdown'
})}
</div>`
return layout('docs · ' + title, '/docs/' + slug, body, ctx)
}
function layoutError (msg, ctx) {
const body = `<div class="card"><h2>Error</h2><div class="empty">${escapeHtml(msg)}</div></div>`
return layout('Error', '/docs', body, ctx)
}