"php", "sh", "bash" => "bash", "css" => "css", "js" => "javascript", "json" => "json", "md" => "markdown", default => "text", }; } /** * Recursively lists every file under client-package/, relative to it. * * @return string[] sorted relative paths */ function handbookScanPackage(): array { $root = realpath(HANDBOOK_PACKAGE_DIR); if ($root === false || !is_dir($root)) { return []; } $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST, ); $files = []; foreach ($iterator as $item) { if (!$item->isFile()) { continue; } $relative = str_replace("\\", "/", substr($item->getPathname(), strlen($root) + 1)); $name = basename($relative); // Local noise that is not part of the handed-out package. if ($name === ".DS_Store" || str_ends_with($name, ".log")) { continue; } if (in_array($relative, HANDBOOK_EXCLUDED, true)) { continue; } $files[] = $relative; } sort($files, SORT_STRING); return $files; } /** * Splits the package into the documentation chapters and the source files. * * @return array{docs: array, code: string[], vendored: string[]} * docs maps relative path -> chapter title, in reading order */ function handbookInventory(): array { $docs = []; $code = []; $vendored = []; foreach (handbookScanPackage() as $relative) { if (in_array($relative, HANDBOOK_VENDORED, true)) { $vendored[] = $relative; continue; } if (str_ends_with($relative, ".md")) { $number = preg_match('#/(\d+)_#', $relative, $match) === 1 ? $match[1] . " · " : ""; $docs[$relative] = $number . handbookChapterTitle($relative); continue; } $code[] = $relative; } // README first, then the numbered chapters in order. uksort($docs, static function (string $a, string $b): int { if ($a === "README.md") { return -1; } if ($b === "README.md") { return 1; } return strcmp($a, $b); }); usort($code, static function (string $a, string $b): int { return [handbookCodeWeight($a), $a] <=> [handbookCodeWeight($b), $b]; }); return ["docs" => $docs, "code" => $code, "vendored" => $vendored]; } /** * Reading order for the source part: the files that are copied into the host * project first, entry point before the modules it pulls in, examples and * tooling last. Anything unrecognised sorts to the end alphabetically, so a * newly added file still lands somewhere sensible. */ function handbookCodeWeight(string $relative): int { return match (true) { $relative === "manage-client/config.sample.php" => 10, $relative === "manage-client/lib/client.php" => 11, $relative === "manage-client/lib/updater.php" => 12, $relative === "manage-client/lib/backup.php" => 13, $relative === "manage-client/lib/remote.php" => 14, str_starts_with($relative, "manage-client/lib/") => 15, str_starts_with($relative, "manage-client/bin/") => 16, str_starts_with($relative, "manage-client/ui/") => 17, str_starts_with($relative, "manage-client/") => 18, str_starts_with($relative, "examples/") => 20, str_starts_with($relative, "scripts/") => 21, default => 30, }; } /** * Title of a chapter: its own first-level heading, so the handbook shows what * the document calls itself rather than a name derived from the file. */ function handbookChapterTitle(string $relative): string { if (preg_match('/^#\s+(.+)$/m', handbookReadDoc($relative), $match) === 1) { return trim($match[1]); } return ucwords(strtolower(str_replace("_", " ", basename($relative, ".md")))); } function handbookReadDoc(string $relative): string { $path = HANDBOOK_PACKAGE_DIR . "/" . $relative; return is_file($path) ? (string) file_get_contents($path) : ""; } /** Drops the leading h1; the assembly emits it as the chapter heading instead. */ function handbookStripTitle(string $markdown): string { return preg_replace('/^#\s+.+\R+/', "", ltrim($markdown), 1) ?? $markdown; } /** Reads an authored page from content/, with the placeholders filled in. */ function handbookContent(string $name, array $replacements = []): string { $path = HANDBOOK_CONTENT_DIR . "/" . $name; $text = is_file($path) ? (string) file_get_contents($path) : ""; foreach ($replacements as $key => $value) { $text = str_replace("{{" . $key . "}}", (string) $value, $text); } return rtrim($text) . "\n"; } /** * Rewrites the relative links between the chapters so they point at the * anchors of this single page instead of at neighbouring .md files. */ function handbookRewriteLinks(string $markdown, array $docs): string { $targets = []; foreach ($docs as $relative => $_title) { $targets[basename($relative)] = "#" . handbookAnchor("doc", $relative); } return preg_replace_callback( '/\]\(([^)\s]+\.md)(#[^)\s]*)?\)/', static function (array $match) use ($targets): string { $file = basename($match[1]); return isset($targets[$file]) ? "](" . $targets[$file] . ")" : $match[0]; }, $markdown, ) ?? $markdown; } /** * Demotes every heading of an embedded chapter by one level, so the assembled * document keeps a single h1 and the chapters sit below the part headings. */ function handbookDemoteHeadings(string $markdown): string { $lines = explode("\n", $markdown); $inFence = false; foreach ($lines as $index => $line) { if (preg_match('/^\s*(```|~~~)/', $line) === 1) { $inFence = !$inFence; continue; } if (!$inFence && preg_match('/^(#{1,5})\s/', $line) === 1) { $lines[$index] = "#" . $line; } } return implode("\n", $lines); } /** * Assembles the complete handbook as one Markdown document. * * @param bool $withAnchors emit HTML anchor targets for the in-page navigation. * Off for the plain-text rendering, which has no nav. */ function handbookBuildMarkdown(bool $withAnchors): string { $inventory = handbookInventory(); $base = handbookBaseUrl(); $self = handbookSelfUrl(); $anchor = static function (string $id) use ($withAnchors): string { return $withAnchors ? '' . "\n\n" : ""; }; $out = []; $out[] = $anchor("part-intro") . handbookContent("00_UEBERBLICK.md", [ "BASE_URL" => $base, "SELF_URL" => $self, "DOC_COUNT" => (string) count($inventory["docs"]), "CODE_COUNT" => (string) count($inventory["code"]), ]); // ---- Table of contents ------------------------------------------------- $toc = ["## Inhalt", ""]; $toc[] = "**Teil 1 – Dokumentation**"; $toc[] = ""; foreach ($inventory["docs"] as $relative => $title) { $link = $withAnchors ? "[" . $title . "](#" . handbookAnchor("doc", $relative) . ")" : $title; $toc[] = "- " . $link . " — `client-package/" . $relative . "`"; } $toc[] = ""; $toc[] = "**Teil 2 – HTTP-API**"; $toc[] = ""; $toc[] = $withAnchors ? "- [OpenAPI-Spezifikation](#part-api)" : "- OpenAPI-Spezifikation"; $toc[] = ""; $toc[] = "**Teil 3 – Quellcode**"; $toc[] = ""; foreach ($inventory["code"] as $relative) { $link = $withAnchors ? "[" . $relative . "](#" . handbookAnchor("code", $relative) . ")" : $relative; $toc[] = "- " . $link; } $out[] = implode("\n", $toc) . "\n"; // ---- Part 1: documentation -------------------------------------------- $out[] = $anchor("part-docs") . "# Teil 1 – Dokumentation\n"; foreach ($inventory["docs"] as $relative => $title) { $body = handbookStripTitle(handbookReadDoc($relative)); $body = handbookRewriteLinks($body, $inventory["docs"]); $body = handbookDemoteHeadings($body); $out[] = $anchor(handbookAnchor("doc", $relative)) . "## " . $title . "\n\n" . "> Quelle: `client-package/" . $relative . "`\n\n" . rtrim($body) . "\n"; } // ---- Part 2: the HTTP API --------------------------------------------- $spec = handbookOpenApiJson(); $out[] = $anchor("part-api") . handbookContent("50_API.md", [ "BASE_URL" => $base, "SELF_URL" => $self, ]) . "\n" . "## OpenAPI 3.1 (vollständig)\n\n" . "```json\n" . rtrim($spec) . "\n```\n"; // ---- Part 3: the source ------------------------------------------------ $out[] = $anchor("part-code") . "# Teil 3 – Quellcode\n\n" . "Alle " . count($inventory["code"]) . " Quelldateien des Client-Pakets, vollständig und\n" . "unverändert. Die Pfade sind relativ zu `client-package/`.\n\n" . "Die Reihenfolge folgt der Wichtigkeit für eine Einbindung: zuerst die Vorlage der\n" . "Konfiguration und `lib/client.php` als einziger Einstiegspunkt, dann Updater, Backup\n" . "und die übrigen Module, danach Kommandozeile und Oberfläche, zuletzt Beispiele und\n" . "Werkzeuge. Für eine Übernahme wird der Ordner `manage-client/` gebraucht; alles\n" . "darunter gehört dazu, `examples/`, `docs/` und `scripts/` nicht.\n"; foreach ($inventory["code"] as $relative) { $path = HANDBOOK_PACKAGE_DIR . "/" . $relative; $body = is_file($path) ? (string) file_get_contents($path) : ""; $fence = handbookFenceFor($body); $out[] = $anchor(handbookAnchor("code", $relative)) . "## `" . $relative . "`\n\n" . $fence . handbookLanguage($relative) . "\n" . rtrim($body) . "\n" . $fence . "\n"; } // ---- Inventory --------------------------------------------------------- $out[] = $anchor("part-inventory") . handbookInventoryTable($inventory); return implode("\n", $out); } /** * Picks a fence long enough to survive content that itself contains one. * lib/zip.php and the docs both embed triple backticks. */ function handbookFenceFor(string $body): string { $longest = 0; if (preg_match_all('/^\s*(`{3,})/m', $body, $matches) > 0) { foreach ($matches[1] as $run) { $longest = max($longest, strlen($run)); } } return str_repeat("`", max(3, $longest + 1)); } function handbookInventoryTable(array $inventory): string { $rows = ["# Dateiübersicht", "", "| Datei | Größe | Rolle |", "|---|---:|---|"]; foreach ($inventory["docs"] as $relative => $_title) { $rows[] = handbookInventoryRow($relative, "Dokumentation"); } foreach ($inventory["code"] as $relative) { $rows[] = handbookInventoryRow($relative, "Quellcode"); } foreach ($inventory["vendored"] as $relative) { $rows[] = handbookInventoryRow($relative, "Fremdbibliothek, hier nicht abgedruckt"); } return implode("\n", $rows) . "\n"; } function handbookInventoryRow(string $relative, string $role): string { $size = @filesize(HANDBOOK_PACKAGE_DIR . "/" . $relative); return "| `" . $relative . "` | " . ($size === false ? "–" : number_format((int) $size, 0, ",", ".") . " B") . " | " . $role . " |"; } /** The OpenAPI document, with the live server URL substituted. */ function handbookOpenApiJson(): string { $path = __DIR__ . "/../openapi.json"; $raw = is_file($path) ? (string) file_get_contents($path) : "{}"; $spec = json_decode($raw, true); if (!is_array($spec)) { return $raw; } $spec["servers"] = [[ "url" => handbookBaseUrl() . "/api/v1", "description" => "Diese Manage-Installation", ]]; return (string) json_encode( $spec, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, ); }