Full documentation site with sidebar navigation, search, markdown rendering, and prev/next navigation. Initial content: introduction, chain overview, name system, API reference, CIC governance. Lethean Boot.php now only registers routes on matching domains (lthn.io, testnet.lthn.io, localhost) — no longer bleeds onto api.*, docs.*, explorer.* subdomains. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
254 lines
8.1 KiB
PHP
254 lines
8.1 KiB
PHP
<?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;
|
|
}
|
|
}
|