lthn.io/app/Website/Docs/Controllers/DocsController.php

255 lines
8.1 KiB
PHP
Raw Permalink Normal View History

<?php
declare(strict_types=1);
namespace Website\Docs\Controllers;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
/**
* docs.lthn.io render markdown documentation.
*
* GET / docs homepage
* GET /search?q=query search docs
* GET /{section}/{page?} render a doc page
*/
class DocsController extends Controller
{
private string $contentPath;
/** Sidebar navigation structure */
private array $navigation = [
'getting-started' => [
'label' => 'Getting Started',
'pages' => [
'introduction' => 'Introduction',
'quick-start' => 'Quick Start',
'registration' => 'Register a Name',
'dns-management' => 'Manage DNS',
],
],
'chain' => [
'label' => 'Blockchain',
'pages' => [
'overview' => 'Overview',
'daemon-rpc' => 'Daemon RPC',
'wallet-rpc' => 'Wallet RPC',
'mining' => 'Mining',
'aliases' => 'Chain Aliases',
'hardforks' => 'Hardfork History',
],
],
'names' => [
'label' => 'Name System',
'pages' => [
'overview' => 'Overview',
'registration' => 'Registration',
'dns-records' => 'DNS Records',
'sunrise' => 'Sunrise Period',
'sidechain' => 'ITNS Sidechain',
],
],
'services' => [
'label' => 'Services',
'pages' => [
'dns-hosting' => 'DNS Hosting',
'ssl-certificates' => 'SSL Certificates',
'proxy-network' => 'Proxy Network',
'gateway-operators' => 'Gateway Operators',
],
],
'api' => [
'label' => 'API Reference',
'pages' => [
'overview' => 'Overview',
'names' => 'Names API',
'explorer' => 'Explorer API',
'proxy' => 'Proxy API',
'gateway' => 'Gateway API',
'authentication' => 'Authentication',
],
],
'governance' => [
'label' => 'Governance',
'pages' => [
'cic' => 'Community Interest Company',
'wallet-holders' => 'Wallet Holders',
'economics' => 'Token Economics',
],
],
];
public function __construct()
{
$this->contentPath = base_path('app/Website/Docs/Content');
}
public function index(): \Illuminate\View\View
{
return view('docs::index', [
'navigation' => $this->navigation,
]);
}
public function show(string $section, ?string $page = null): \Illuminate\View\View
{
$page = $page ?? 'overview';
$filePath = "{$this->contentPath}/{$section}/{$page}.md";
if (! file_exists($filePath)) {
abort(404);
}
$markdown = file_get_contents($filePath);
$html = $this->renderMarkdown($markdown);
$title = $this->extractTitle($markdown);
// Find current position in nav for prev/next
$prevNext = $this->findPrevNext($section, $page);
return view('docs::show', [
'navigation' => $this->navigation,
'currentSection' => $section,
'currentPage' => $page,
'content' => $html,
'title' => $title,
'prev' => $prevNext['prev'],
'next' => $prevNext['next'],
]);
}
public function search(Request $request): \Illuminate\View\View
{
$query = trim((string) $request->get('q', ''));
$results = [];
if (strlen($query) >= 2) {
$results = $this->searchContent($query);
}
return view('docs::search', [
'navigation' => $this->navigation,
'query' => $query,
'results' => $results,
]);
}
private function renderMarkdown(string $markdown): string
{
// Strip YAML frontmatter
$markdown = preg_replace('/^---\n.*?\n---\n/s', '', $markdown);
// Basic markdown to HTML (no external dependency)
$html = e($markdown);
// Headers
$html = preg_replace('/^######\s+(.+)$/m', '<h6>$1</h6>', $html);
$html = preg_replace('/^#####\s+(.+)$/m', '<h5>$1</h5>', $html);
$html = preg_replace('/^####\s+(.+)$/m', '<h4 id="' . '${1}' . '">$1</h4>', $html);
$html = preg_replace('/^###\s+(.+)$/m', '<h3>$1</h3>', $html);
$html = preg_replace('/^##\s+(.+)$/m', '<h2>$1</h2>', $html);
$html = preg_replace('/^#\s+(.+)$/m', '<h1>$1</h1>', $html);
// Code blocks
$html = preg_replace('/```(\w*)\n(.*?)\n```/s', '<pre><code class="language-$1">$2</code></pre>', $html);
// Inline code
$html = preg_replace('/`([^`]+)`/', '<code>$1</code>', $html);
// Bold and italic
$html = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $html);
$html = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $html);
// Links
$html = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '<a href="$2">$1</a>', $html);
// Lists
$html = preg_replace('/^- (.+)$/m', '<li>$1</li>', $html);
$html = preg_replace('/(<li>.*<\/li>\n?)+/', '<ul>$0</ul>', $html);
// Paragraphs
$html = preg_replace('/\n\n([^<])/', '</p><p>$1', $html);
$html = '<p>' . $html . '</p>';
// Clean up
$html = str_replace('<p><h', '<h', $html);
$html = preg_replace('/<\/h(\d)><\/p>/', '</h$1>', $html);
$html = str_replace('<p><pre>', '<pre>', $html);
$html = str_replace('</pre></p>', '</pre>', $html);
$html = str_replace('<p><ul>', '<ul>', $html);
$html = str_replace('</ul></p>', '</ul>', $html);
$html = str_replace('<p></p>', '', $html);
return $html;
}
private function extractTitle(string $markdown): string
{
if (preg_match('/^#\s+(.+)$/m', $markdown, $matches)) {
return $matches[1];
}
return 'Documentation';
}
private function findPrevNext(string $section, string $page): array
{
$flat = [];
foreach ($this->navigation as $sec => $data) {
foreach ($data['pages'] as $pg => $label) {
$flat[] = ['section' => $sec, 'page' => $pg, 'label' => $label];
}
}
$current = null;
foreach ($flat as $i => $item) {
if ($item['section'] === $section && $item['page'] === $page) {
$current = $i;
break;
}
}
return [
'prev' => $current !== null && $current > 0 ? $flat[$current - 1] : null,
'next' => $current !== null && $current < count($flat) - 1 ? $flat[$current + 1] : null,
];
}
private function searchContent(string $query): array
{
$results = [];
$query = strtolower($query);
foreach ($this->navigation as $section => $data) {
foreach ($data['pages'] as $page => $label) {
$filePath = "{$this->contentPath}/{$section}/{$page}.md";
if (! file_exists($filePath)) {
continue;
}
$content = strtolower(file_get_contents($filePath));
if (str_contains($content, $query)) {
// Extract snippet around match
$pos = strpos($content, $query);
$start = max(0, $pos - 80);
$snippet = substr(file_get_contents($filePath), $start, 200);
$snippet = trim(preg_replace('/\s+/', ' ', $snippet));
$results[] = [
'section' => $section,
'page' => $page,
'label' => $label,
'sectionLabel' => $data['label'],
'snippet' => $snippet,
];
}
}
}
return $results;
}
}