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