diff --git a/app/Liquid/Filters/StringMarkup.php b/app/Liquid/Filters/StringMarkup.php index 65fa7ed..10c5abc 100644 --- a/app/Liquid/Filters/StringMarkup.php +++ b/app/Liquid/Filters/StringMarkup.php @@ -7,6 +7,7 @@ use Illuminate\Support\Str; use Keepsuit\Liquid\Filters\FiltersProvider; use League\CommonMark\CommonMarkConverter; use League\CommonMark\Exception\CommonMarkException; +use SimpleSoftwareIO\QrCode\Facades\QrCode; /** * String, Markup, and HTML filters for Liquid templates @@ -58,4 +59,50 @@ class StringMarkup extends FiltersProvider { return strip_tags($html); } + + /** + * Generate a QR code as SVG from the input text + * + * @param string $text The text to encode in the QR code + * @param int|null $moduleSize Optional module size (defaults to 11, which equals 319px) + * @param string|null $errorCorrection Optional error correction level: 'l', 'm', 'q', 'h' (defaults to 'm') + * @return string The SVG QR code + */ + public function qr_code(string $text, ?int $moduleSize = null, ?string $errorCorrection = null): string + { + // Default module_size is 11 + // Size calculation: (21 modules for QR code + 4 modules margin on each side * 2) * module_size + // = (21 + 8) * module_size = 29 * module_size + $moduleSize = $moduleSize ?? 11; + $size = 29 * $moduleSize; + + $qrCode = QrCode::format('svg') + ->size($size); + + // Set error correction level if provided + if ($errorCorrection !== null) { + $qrCode->errorCorrection($errorCorrection); + } + + $svg = (string) $qrCode->generate($text); + + // Add class="qr-code" to the SVG element + // The SVG may start with and then + if (preg_match('/]*)>/', $svg, $matches)) { + $attributes = $matches[1]; + // Check if class already exists + if (mb_strpos($attributes, 'class=') === false) { + $svg = preg_replace('/]*)>/', '', $svg, 1); + } else { + // If class exists, add qr-code to it + $svg = preg_replace('/(]*class=["\'])([^"\']*)(["\'][^>]*>)/', '$1$2 qr-code$3', $svg, 1); + } + } else { + // Fallback: simple replacement if no attributes + $svg = preg_replace('//', '', $svg, 1); + } + + return $svg; + } } diff --git a/composer.json b/composer.json index 8903e17..f59e0f3 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "laravel/sanctum": "^4.0", "laravel/socialite": "^5.23", "laravel/tinker": "^2.10.1", + "livewire/livewire": "^3.7", "livewire/flux": "^2.0", "livewire/volt": "^1.7", "om/icalparser": "^3.2", diff --git a/composer.lock b/composer.lock index a469e55..e5840ef 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4de5f1df0160f59d08f428e36e81262e", + "content-hash": "ac6b1e352cb66f858a50b64e7e3c70d0", "packages": [ { "name": "aws/aws-crt-php", @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.369.12", + "version": "3.369.13", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "36ee8894743a254ae2650bad4968c874b76bc7de" + "reference": "bedc36250c92b8287be855a2d25427fb0e065483" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/36ee8894743a254ae2650bad4968c874b76bc7de", - "reference": "36ee8894743a254ae2650bad4968c874b76bc7de", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/bedc36250c92b8287be855a2d25427fb0e065483", + "reference": "bedc36250c92b8287be855a2d25427fb0e065483", "shasum": "" }, "require": { @@ -153,9 +153,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.369.12" + "source": "https://github.com/aws/aws-sdk-php/tree/3.369.13" }, - "time": "2026-01-13T19:12:08+00:00" + "time": "2026-01-14T19:13:46+00:00" }, { "name": "bacon/bacon-qr-code", @@ -3082,16 +3082,16 @@ }, { "name": "livewire/livewire", - "version": "v3.7.3", + "version": "v3.7.4", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "a5384df9fbd3eaf02e053bc49aabc8ace293fc1c" + "reference": "5a8dffd4c0ab357ff7ed5b39e7c2453d962a68e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/a5384df9fbd3eaf02e053bc49aabc8ace293fc1c", - "reference": "a5384df9fbd3eaf02e053bc49aabc8ace293fc1c", + "url": "https://api.github.com/repos/livewire/livewire/zipball/5a8dffd4c0ab357ff7ed5b39e7c2453d962a68e0", + "reference": "5a8dffd4c0ab357ff7ed5b39e7c2453d962a68e0", "shasum": "" }, "require": { @@ -3146,7 +3146,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.7.3" + "source": "https://github.com/livewire/livewire/tree/v3.7.4" }, "funding": [ { @@ -3154,7 +3154,7 @@ "type": "github" } ], - "time": "2025-12-19T02:00:29+00:00" + "time": "2026-01-13T09:37:21+00:00" }, { "name": "livewire/volt", @@ -8846,16 +8846,16 @@ }, { "name": "laravel/boost", - "version": "v1.8.9", + "version": "v1.8.10", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "1f2c2d41b5216618170fb6730ec13bf894c5bffd" + "reference": "aad8b2a423b0a886c2ce7ee92abbfde69992ff32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/1f2c2d41b5216618170fb6730ec13bf894c5bffd", - "reference": "1f2c2d41b5216618170fb6730ec13bf894c5bffd", + "url": "https://api.github.com/repos/laravel/boost/zipball/aad8b2a423b0a886c2ce7ee92abbfde69992ff32", + "reference": "aad8b2a423b0a886c2ce7ee92abbfde69992ff32", "shasum": "" }, "require": { @@ -8908,7 +8908,7 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2026-01-07T18:43:11+00:00" + "time": "2026-01-14T14:51:16+00:00" }, { "name": "laravel/mcp", diff --git a/resources/views/livewire/catalog/index.blade.php b/resources/views/livewire/catalog/index.blade.php index fdf7f34..29738ab 100644 --- a/resources/views/livewire/catalog/index.blade.php +++ b/resources/views/livewire/catalog/index.blade.php @@ -81,6 +81,7 @@ class extends Component 'logo_url' => Arr::get($plugin, 'logo_url'), 'screenshot_url' => Arr::get($plugin, 'screenshot_url'), 'learn_more_url' => Arr::get($plugin, 'author_bio.learn_more_url'), + 'preferred_renderer' => Arr::get($plugin, 'byos.byos_laravel.renderer'), ]; }) ->sortBy('name') @@ -112,7 +113,7 @@ class extends Component $plugin['zip_url'], auth()->user(), $plugin['zip_entry_path'] ?? null, - null, + config('services.trmnl.liquid_enabled') ? $plugin['preferred_renderer'] : null, $plugin['logo_url'] ?? null, allowDuplicate: true ); diff --git a/tests/Feature/PluginLiquidFilterTest.php b/tests/Feature/PluginLiquidFilterTest.php index e6272c7..2e80f55 100644 --- a/tests/Feature/PluginLiquidFilterTest.php +++ b/tests/Feature/PluginLiquidFilterTest.php @@ -174,3 +174,100 @@ LIQUID // Should not contain users < 30 $this->assertStringNotContainsString('Alice (25)', $result); }); + +test('qr_code filter generates SVG QR code with qr-code class', function (): void { + $plugin = Plugin::factory()->create([ + 'markup_language' => 'liquid', + 'render_markup' => '{{ "https://example.com" | qr_code }}', + ]); + + $result = $plugin->render('full'); + + // Should contain SVG elements + $this->assertStringContainsString('assertStringContainsString('', $result); + // Should contain qr-code class + $this->assertStringContainsString('class="qr-code"', $result); + // Should contain QR code path elements + $this->assertStringContainsString('create([ + 'markup_language' => 'liquid', + 'render_markup' => '{{ "Hello World" | qr_code }}', + ]); + + $result = $plugin->render('full'); + + // Should generate valid SVG + $this->assertStringContainsString('assertStringContainsString('', $result); + // Should contain qr-code class + $this->assertStringContainsString('class="qr-code"', $result); +}); + +test('qr_code filter calculates correct size for module_size 11', function (): void { + $plugin = Plugin::factory()->create([ + 'markup_language' => 'liquid', + 'render_markup' => '{{ "test" | qr_code: 11 }}', + ]); + + $result = $plugin->render('full'); + + // Should have width="319" and height="319" (29 * 11 = 319) + $this->assertStringContainsString('width="319"', $result); + $this->assertStringContainsString('height="319"', $result); +}); + +test('qr_code filter calculates correct size for module_size 16', function (): void { + $plugin = Plugin::factory()->create([ + 'markup_language' => 'liquid', + 'render_markup' => '{{ "test" | qr_code: 16 }}', + ]); + + $result = $plugin->render('full'); + + // Should have width="464" and height="464" (29 * 16 = 464) + $this->assertStringContainsString('width="464"', $result); + $this->assertStringContainsString('height="464"', $result); +}); + +test('qr_code filter calculates correct size for module_size 10', function (): void { + $plugin = Plugin::factory()->create([ + 'markup_language' => 'liquid', + 'render_markup' => '{{ "test" | qr_code: 10 }}', + ]); + + $result = $plugin->render('full'); + + // Should have width="290" and height="290" (29 * 10 = 290) + $this->assertStringContainsString('width="290"', $result); + $this->assertStringContainsString('height="290"', $result); +}); + +test('qr_code filter calculates correct size for module_size 5', function (): void { + $plugin = Plugin::factory()->create([ + 'markup_language' => 'liquid', + 'render_markup' => '{{ "test" | qr_code: 5 }}', + ]); + + $result = $plugin->render('full'); + + // Should have width="145" and height="145" (29 * 5 = 145) + $this->assertStringContainsString('width="145"', $result); + $this->assertStringContainsString('height="145"', $result); +}); + +test('qr_code filter supports error correction level parameter', function (): void { + $plugin = Plugin::factory()->create([ + 'markup_language' => 'liquid', + 'render_markup' => '{{ "test" | qr_code: 11, "l" }}', + ]); + + $result = $plugin->render('full'); + + // Should generate valid SVG with qr-code class + $this->assertStringContainsString('assertStringContainsString('class="qr-code"', $result); +});