From de1a3905741b2a6c55cde3e2b86c8a2f0bab16e6 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Wed, 14 Jan 2026 14:51:55 +0100 Subject: [PATCH] feat: add liquid filter `qr_code` --- app/Liquid/Filters/StringMarkup.php | 47 ++++++++++++ tests/Feature/PluginLiquidFilterTest.php | 97 ++++++++++++++++++++++++ 2 files changed, 144 insertions(+) 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/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); +});