feat: add liquid filter qr_code

This commit is contained in:
Benjamin Nussbaum 2026-01-14 14:51:55 +01:00
parent 6bc672c3c4
commit de1a390574
2 changed files with 144 additions and 0 deletions

View file

@ -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 <?xml...> and then <svg, so we need to find the <svg tag
// Match <svg followed by whitespace or attributes, and insert class before the first attribute or closing >
if (preg_match('/<svg\s+([^>]*)>/', $svg, $matches)) {
$attributes = $matches[1];
// Check if class already exists
if (mb_strpos($attributes, 'class=') === false) {
$svg = preg_replace('/<svg\s+([^>]*)>/', '<svg class="qr-code" $1>', $svg, 1);
} else {
// If class exists, add qr-code to it
$svg = preg_replace('/(<svg\s+[^>]*class=["\'])([^"\']*)(["\'][^>]*>)/', '$1$2 qr-code$3', $svg, 1);
}
} else {
// Fallback: simple replacement if no attributes
$svg = preg_replace('/<svg>/', '<svg class="qr-code">', $svg, 1);
}
return $svg;
}
}

View file

@ -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('<svg', $result);
$this->assertStringContainsString('</svg>', $result);
// Should contain qr-code class
$this->assertStringContainsString('class="qr-code"', $result);
// Should contain QR code path elements
$this->assertStringContainsString('<path', $result);
});
test('qr_code filter works with custom text', function (): void {
$plugin = Plugin::factory()->create([
'markup_language' => 'liquid',
'render_markup' => '{{ "Hello World" | qr_code }}',
]);
$result = $plugin->render('full');
// Should generate valid SVG
$this->assertStringContainsString('<svg', $result);
$this->assertStringContainsString('</svg>', $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('<svg', $result);
$this->assertStringContainsString('class="qr-code"', $result);
});