#!/usr/bin/env python3 """Build the PiSCSI man-page documentation site.""" from __future__ import annotations import argparse import html import json import re import shutil import subprocess from html.parser import HTMLParser from pathlib import Path ROOT = Path(__file__).resolve().parent REPO_ROOT = ROOT.parent SOURCE_DIR = REPO_ROOT / "doc" DIST_DIR = ROOT / "dist" TEMPLATE = ROOT / "templates" / "page.html.tmpl" ASSETS_DIR = ROOT / "assets" LOGO = REPO_ROOT / "doc" / "logo" / "redpanda-32x32.png" class TextExtractor(HTMLParser): def __init__(self) -> None: super().__init__() self.parts: list[str] = [] def handle_data(self, data: str) -> None: self.parts.append(data) def text(self) -> str: return re.sub(r"\s+", " ", " ".join(self.parts)).strip() def description_from_source(source: Path) -> str: for line in source.read_text(encoding="utf-8").splitlines(): match = re.match(r"^\.Nd\s+(.*)$", line) if match: return re.sub(r"\s+", " ", match.group(1)).strip() return "PiSCSI command reference" def render_fragment(source: Path) -> str: try: result = subprocess.run( ["mandoc", "-T", "html", "-O", "fragment", str(source)], check=True, capture_output=True, text=True, ) except FileNotFoundError as error: raise SystemExit("mandoc is required to build the documentation site") from error except subprocess.CalledProcessError as error: raise SystemExit(f"mandoc failed for {source.name}: {error.stderr}") from error return result.stdout.strip() def navigation(pages: list[dict[str, str]], current: str = "") -> str: links = [] for page in pages: active = ' aria-current="page"' if page["url"] == current else "" links.append( f'' f'{page["title"]}{page["section"]}' ) return " ".join(links) def apply_template(template: str, *, title: str, description: str, nav: str, content: str, current: str = "") -> str: values = { "{{TITLE}}": html.escape(title), "{{DESCRIPTION}}": html.escape(description), "{{NAV}}": nav, "{{CONTENT}}": content, "{{CURRENT}}": current, } for marker, value in values.items(): template = template.replace(marker, value) return template def build() -> None: sources = sorted(SOURCE_DIR.glob("*.1")) + sorted(SOURCE_DIR.glob("*.8")) if not sources: raise SystemExit("No man pages found in doc/") pages = [] rendered: dict[str, str] = {} for source in sources: section = source.suffix[1:] title = source.stem url = f"{source.stem}.html" description = description_from_source(source) fragment = render_fragment(source) rendered[url] = fragment pages.append({"title": title, "section": section, "url": url, "description": description}) template = TEMPLATE.read_text(encoding="utf-8") if DIST_DIR.exists(): shutil.rmtree(DIST_DIR) DIST_DIR.mkdir(parents=True) shutil.copytree(ASSETS_DIR, DIST_DIR / "assets") shutil.copy2(LOGO, DIST_DIR / "assets" / LOGO.name) for page in pages: content = ( f'
{html.escape(page["description"])}
{html.escape(paragraph)}
' for paragraph in project_paragraphs ) index_content = ( 'The official man pages for PiSCSI and its companion utilities, generated directly from the project sources.
' f'{project_intro}