diff --git a/app/Commands/HttpServeCommand.php b/app/Commands/HttpServeCommand.php new file mode 100644 index 0000000..1552c90 --- /dev/null +++ b/app/Commands/HttpServeCommand.php @@ -0,0 +1,82 @@ +call('clean'); + + $this->line("Starting development server: http://{$this->host()}:{$this->port()}"); + + $process = $this->startProcess(); + + while ($process->isRunning()) { + usleep(0.5 * 1000000); + } + + $status = $process->getExitCode(); + + if ($status && $this->portOffset++ < 10) { + $this->handle(); + } + + return $status; + } + + protected function startProcess() + { + $process = new Process($this->serverCommand(), timeout: 0); + + $process->start(function ($type, $data) { +// $this->output->write($data); + }); + + // Stop the server when the user hits Ctrl+C + // to void the port in used error + $this->trap(fn () => [SIGTERM, SIGINT, SIGHUP, SIGUSR1, SIGUSR2, SIGQUIT], function ($signal) use ($process) { + if ($process->isRunning()) { + $process->stop(10, $signal); + } + + exit; + }); + + return $process; + } + + protected function serverCommand() + { + return [ + (new PhpExecutableFinder)->find(false), + '-S', + $this->host().':'.$this->port(), + '-t', + 'public', + base_path('server.php') + ]; + } + + protected function host() + { + return '127.0.0.1'; + } + + protected function port() + { + return 6969 + $this->portOffset; + } +} diff --git a/app/Commands/ServeCommand.php b/app/Commands/ServeCommand.php index 75418fe..21d2c6d 100644 --- a/app/Commands/ServeCommand.php +++ b/app/Commands/ServeCommand.php @@ -2,79 +2,32 @@ namespace BangNokia\Lina\Commands; +use Illuminate\Process\Pool; +use Illuminate\Support\Facades\Process; use LaravelZero\Framework\Commands\Command; use Symfony\Component\Process\PhpExecutableFinder; -use Symfony\Component\Process\Process; class ServeCommand extends Command { protected $signature = 'serve'; - protected $description = 'Start simple web server for development'; + protected $description = 'Start development server'; - protected int $portOffset = 0; - - public function handle() + public function handle(): int { - $this->call('clean'); - - $this->line("Starting development server: http://{$this->host()}:{$this->port()}"); - - $process = $this->startProcess(); + $phpBinary = (new PhpExecutableFinder())->find(); + // get the current php binary path which is running the command + $pool = Process::pool(function (Pool $pool) use ($phpBinary) { + $pool->path(getcwd())->command([$phpBinary, base_path('lina'), 'serve:http']); + $pool->path(getcwd())->command([$phpBinary, base_path('lina'), 'serve:ws']); + })->start(function (string $type, string $output, string $key) { + $this->output->write($output); + }); - while ($process->isRunning()) { + while ($pool->running()->isNotEmpty()) { usleep(0.5 * 1000000); } - $status = $process->getExitCode(); - - if ($status && $this->portOffset++ < 10) { - $this->handle(); - } - - return $status; - } - - protected function startProcess() - { - $process = new Process($this->serverCommand(), timeout: 0); - - $process->start(function ($type, $data) { - $this->output->write($data); - }); - - // Stop the server when the user hits Ctrl+C - // to void the port in used error - $this->trap(fn () => [SIGTERM, SIGINT, SIGHUP, SIGUSR1, SIGUSR2, SIGQUIT], function ($signal) use ($process) { - if ($process->isRunning()) { - $process->stop(10, $signal); - } - - exit; - }); - - return $process; - } - - protected function serverCommand() - { - return [ - (new PhpExecutableFinder)->find(false), - '-S', - $this->host().':'.$this->port(), - '-t', - 'public', - base_path('server.php') - ]; - } - - protected function host() - { - return '127.0.0.1'; - } - - protected function port() - { - return 6969 + $this->portOffset; + $pool->wait(); } } diff --git a/app/Commands/WebsocketServeCommand.php b/app/Commands/WebsocketServeCommand.php new file mode 100644 index 0000000..b6bd79c --- /dev/null +++ b/app/Commands/WebsocketServeCommand.php @@ -0,0 +1,127 @@ +loop = Loop::get(); + + $this->loop->futureTick(function () { + $this->line("Starting websocket server: ws://{$this->host()}:{$this->port()}"); + }); + + $this + ->startWatcher() + ->startServer(); + } + + protected function startWatcher(): static + { + $dirs = $this->dirs(); + + if (empty($dirs)) { + $this->warn('No directory to watch, please check you are in the correct directory.'); + return $this; + } + + $finder = (new Finder())->files()->in($this->dirs()); + + (new Watcher($this->loop, $finder)) + ->startWatching(function () { + $this->info('Changes detected, reloading...'); + collect(Socket::$clients) + ->map(function (ConnectionInterface $client) { + $client->send('reload'); + }); + }); + + return $this; + } + + protected function startServer(): static + { + try { + $this->server = new IoServer( + new HttpServer(new WsServer(new Socket())), + new Reactor("{$this->host()}:{$this->port()}", [], $this->loop), + $this->loop + ); + $this->loop->addPeriodicTimer(1, fn() => Cache::put('ws_is_running', true, 5)); + + $this->server->run(); + } catch (\Exception $exception) { + if (static::$portOffset < 10) { + static::$portOffset++; + $this->startServer(); + } + } + + return $this; + } + + public static function isRunning(): bool + { + return Cache::get('ws_is_running', false); + } + + public static function host() + { + return '127.0.0.1'; + } + + public static function port() + { + return static::$port + static::$portOffset; + } + + protected function dirs(): array + { + $currentDir = getcwd(); + + $proposalDirs = [ + $currentDir . '/content', + $currentDir . '/public', + $currentDir . '/resources/views', + ]; + + $realDirs = []; + + foreach ($proposalDirs as $dir) { + if (is_dir($dir)) { + $realDirs[] = $dir; + } + } + + return $realDirs; + } +} diff --git a/app/MarkdownParser.php b/app/MarkdownParser.php index 8eee382..9368ed9 100644 --- a/app/MarkdownParser.php +++ b/app/MarkdownParser.php @@ -3,7 +3,6 @@ namespace BangNokia\Lina; use BangNokia\Lina\Contracts\MarkdownParser as MarkdownParserContract; -use ParsedownToC; class MarkdownParser implements MarkdownParserContract { @@ -16,8 +15,6 @@ public function __construct() public function parse(string $text): string { - $content = trim($this->driver->text($text)); -// dd($content); - return $content; + return trim($this->driver->text($text)); } } diff --git a/app/MarkdownRenderer.php b/app/MarkdownRenderer.php index 234bfc8..d56c8fe 100644 --- a/app/MarkdownRenderer.php +++ b/app/MarkdownRenderer.php @@ -3,7 +3,6 @@ namespace BangNokia\Lina; use BangNokia\Lina\Contracts\Renderer; -use Illuminate\Support\Facades\Blade; class MarkdownRenderer implements Renderer { @@ -17,9 +16,9 @@ public function __construct(protected string $rootDir) config(['view.compiled' => $this->rootDir . '/resources/cache']); } - public function render(string $realPath): string + public function render(string $file): string { - $content = app(ContentFinder::class)->get($realPath, true); + $content = app(ContentFinder::class)->get($file, true); return view($content->layout, [ 'data' => $content, diff --git a/app/Router.php b/app/Router.php index 83711ab..bfa7368 100644 --- a/app/Router.php +++ b/app/Router.php @@ -2,6 +2,8 @@ namespace BangNokia\Lina; +use BangNokia\Lina\Commands\WebsocketServeCommand; +use Illuminate\Support\Facades\Cache; use Symfony\Component\Finder\Finder; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -29,13 +31,37 @@ public function parse(Request $request): Response $contentFileRealPath = $this->contentFinder->tryFind($path); + $html = app(MarkdownRenderer::class)->render($contentFileRealPath); + + // we don't have middleware so let inject the websocket script here + $html = $this->injectWebSocketScript($html); + return new Response( - app(MarkdownRenderer::class)->render($contentFileRealPath), + $html, 200, ['Content-Type' => 'text/html'] ); } + protected function injectWebSocketScript(string $html): string + { + $port = WebsocketServeCommand::port(); + + $script = << + (new WebSocket('ws://127.0.0.1:$port')).onmessage = function (message) { + if (message.data === 'reload') { + window.location.reload(true); + } + }; + +JS; + + $html = $html . $script; // so who care about well-formed html here xD! + + return $html; + } + protected function isStaticFile(string $path): bool { return in_array(pathinfo($path, PATHINFO_EXTENSION), ['css', 'js', 'png', 'jpg', 'jpeg', 'gif', 'svg']); diff --git a/app/Socket.php b/app/Socket.php new file mode 100644 index 0000000..3803a5f --- /dev/null +++ b/app/Socket.php @@ -0,0 +1,36 @@ +attach($conn); + } + + function onClose(ConnectionInterface $conn) + { + static::$clients->detach($conn); + } + + function onError(ConnectionInterface $conn, \Exception $e) + { + } + + public function onMessage(ConnectionInterface $conn, MessageInterface $msg) + { + } +} diff --git a/app/Watcher.php b/app/Watcher.php new file mode 100644 index 0000000..f5d9124 --- /dev/null +++ b/app/Watcher.php @@ -0,0 +1,37 @@ +loop = $loop; + $this->finder = $finder; + } + + public function startWatching($callback) + { + $watcher = new ResourceWatcher( + new ResourceCacheMemory(), + $this->finder, + new Crc32ContentHash() + ); + + $this->loop->addPeriodicTimer(1, function () use ($watcher, $callback) { + Cache::put('serve_websockets_running', true, 5); + if ($watcher->findChanges()->hasChanges()) { + call_user_func($callback); + } + }); + } + +} diff --git a/composer.json b/composer.json index b43f196..29a704a 100644 --- a/composer.json +++ b/composer.json @@ -19,13 +19,15 @@ ], "require": { "php": "^8.3", + "cboden/ratchet": "^0.4.4", "erusev/parsedown": "^v1.7.2", "erusev/parsedown-extra": "^0.8.1", "illuminate/view": "^v10.0", "keinos/parsedown-toc": "^1.1", "laravel-zero/framework": "^v10.3", - "symfony/http-foundation": "^7.0", - "symfony/yaml": "^7.0", + "linaphp/resource-watcher": "^0.1.1", + "symfony/http-foundation": "^6.0", + "symfony/yaml": "^6.0", "tempest/highlight": "1.*" }, "require-dev": { diff --git a/skeleton/content/index.md b/skeleton/content/index.md index 5da124c..c8c9634 100644 --- a/skeleton/content/index.md +++ b/skeleton/content/index.md @@ -3,9 +3,12 @@ title: Welcome to Lina layout: home --- -Welcome to Lina, a simple and lightweight blog platform built on top of Laravel Blade. Lina is designed to be easy to use and. - -Checkout the [documentation](https://github.com/bangnokia/lina) on our Github repository to get started. +Welcome to [Lina](https://lina.daudau.cc), a simple and blazing fast blog platform built on top of Laravel Blade. +## Get started +- **content** folder: contains all the markdown files content. +- **resources/views** folder: contains all the views. +- **public** folder: contains all the assets such as css, js, images. +Checkout the [documentation](https://github.com/bangnokia/lina) on our Github repository for more information. diff --git a/skeleton/resources/views/post.blade.php b/skeleton/resources/views/post.blade.php index f588e88..1741e92 100644 --- a/skeleton/resources/views/post.blade.php +++ b/skeleton/resources/views/post.blade.php @@ -2,10 +2,9 @@ @section('content')

{{ $data->title }}

- +
{!! $data->content !!} -{{-- {{ $data->content }}--}}
@endsection