OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / lib / web / pages / docs.js

133 lines · javascript ref:
/**
 * 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">&larr; 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)
}