Compare commits

..

3 commits

Author SHA1 Message Date
Benjamin Nussbaum
c94dd89361 fix: enable trmnl-liquid renderer in OSS catalog
Some checks are pending
tests / ci (push) Waiting to run
2026-01-14 23:13:49 +01:00
Benjamin Nussbaum
de1a390574 feat: add liquid filter qr_code 2026-01-14 23:13:49 +01:00
Benjamin Nussbaum
6bc672c3c4 chore: update dependencies 2026-01-14 23:13:49 +01:00
5 changed files with 165 additions and 19 deletions

View file

@ -7,6 +7,7 @@ use Illuminate\Support\Str;
use Keepsuit\Liquid\Filters\FiltersProvider; use Keepsuit\Liquid\Filters\FiltersProvider;
use League\CommonMark\CommonMarkConverter; use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Exception\CommonMarkException; use League\CommonMark\Exception\CommonMarkException;
use SimpleSoftwareIO\QrCode\Facades\QrCode;
/** /**
* String, Markup, and HTML filters for Liquid templates * String, Markup, and HTML filters for Liquid templates
@ -58,4 +59,50 @@ class StringMarkup extends FiltersProvider
{ {
return strip_tags($html); 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

@ -22,6 +22,7 @@
"laravel/sanctum": "^4.0", "laravel/sanctum": "^4.0",
"laravel/socialite": "^5.23", "laravel/socialite": "^5.23",
"laravel/tinker": "^2.10.1", "laravel/tinker": "^2.10.1",
"livewire/livewire": "^3.7",
"livewire/flux": "^2.0", "livewire/flux": "^2.0",
"livewire/volt": "^1.7", "livewire/volt": "^1.7",
"om/icalparser": "^3.2", "om/icalparser": "^3.2",

36
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "4de5f1df0160f59d08f428e36e81262e", "content-hash": "ac6b1e352cb66f858a50b64e7e3c70d0",
"packages": [ "packages": [
{ {
"name": "aws/aws-crt-php", "name": "aws/aws-crt-php",
@ -62,16 +62,16 @@
}, },
{ {
"name": "aws/aws-sdk-php", "name": "aws/aws-sdk-php",
"version": "3.369.12", "version": "3.369.13",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/aws/aws-sdk-php.git", "url": "https://github.com/aws/aws-sdk-php.git",
"reference": "36ee8894743a254ae2650bad4968c874b76bc7de" "reference": "bedc36250c92b8287be855a2d25427fb0e065483"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/36ee8894743a254ae2650bad4968c874b76bc7de", "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/bedc36250c92b8287be855a2d25427fb0e065483",
"reference": "36ee8894743a254ae2650bad4968c874b76bc7de", "reference": "bedc36250c92b8287be855a2d25427fb0e065483",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -153,9 +153,9 @@
"support": { "support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions", "forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues", "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", "name": "bacon/bacon-qr-code",
@ -3082,16 +3082,16 @@
}, },
{ {
"name": "livewire/livewire", "name": "livewire/livewire",
"version": "v3.7.3", "version": "v3.7.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/livewire/livewire.git", "url": "https://github.com/livewire/livewire.git",
"reference": "a5384df9fbd3eaf02e053bc49aabc8ace293fc1c" "reference": "5a8dffd4c0ab357ff7ed5b39e7c2453d962a68e0"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/livewire/livewire/zipball/a5384df9fbd3eaf02e053bc49aabc8ace293fc1c", "url": "https://api.github.com/repos/livewire/livewire/zipball/5a8dffd4c0ab357ff7ed5b39e7c2453d962a68e0",
"reference": "a5384df9fbd3eaf02e053bc49aabc8ace293fc1c", "reference": "5a8dffd4c0ab357ff7ed5b39e7c2453d962a68e0",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3146,7 +3146,7 @@
"description": "A front-end framework for Laravel.", "description": "A front-end framework for Laravel.",
"support": { "support": {
"issues": "https://github.com/livewire/livewire/issues", "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": [ "funding": [
{ {
@ -3154,7 +3154,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-12-19T02:00:29+00:00" "time": "2026-01-13T09:37:21+00:00"
}, },
{ {
"name": "livewire/volt", "name": "livewire/volt",
@ -8846,16 +8846,16 @@
}, },
{ {
"name": "laravel/boost", "name": "laravel/boost",
"version": "v1.8.9", "version": "v1.8.10",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/boost.git", "url": "https://github.com/laravel/boost.git",
"reference": "1f2c2d41b5216618170fb6730ec13bf894c5bffd" "reference": "aad8b2a423b0a886c2ce7ee92abbfde69992ff32"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/boost/zipball/1f2c2d41b5216618170fb6730ec13bf894c5bffd", "url": "https://api.github.com/repos/laravel/boost/zipball/aad8b2a423b0a886c2ce7ee92abbfde69992ff32",
"reference": "1f2c2d41b5216618170fb6730ec13bf894c5bffd", "reference": "aad8b2a423b0a886c2ce7ee92abbfde69992ff32",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -8908,7 +8908,7 @@
"issues": "https://github.com/laravel/boost/issues", "issues": "https://github.com/laravel/boost/issues",
"source": "https://github.com/laravel/boost" "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", "name": "laravel/mcp",

View file

@ -81,6 +81,7 @@ class extends Component
'logo_url' => Arr::get($plugin, 'logo_url'), 'logo_url' => Arr::get($plugin, 'logo_url'),
'screenshot_url' => Arr::get($plugin, 'screenshot_url'), 'screenshot_url' => Arr::get($plugin, 'screenshot_url'),
'learn_more_url' => Arr::get($plugin, 'author_bio.learn_more_url'), 'learn_more_url' => Arr::get($plugin, 'author_bio.learn_more_url'),
'preferred_renderer' => Arr::get($plugin, 'byos.byos_laravel.renderer'),
]; ];
}) })
->sortBy('name') ->sortBy('name')
@ -112,7 +113,7 @@ class extends Component
$plugin['zip_url'], $plugin['zip_url'],
auth()->user(), auth()->user(),
$plugin['zip_entry_path'] ?? null, $plugin['zip_entry_path'] ?? null,
null, config('services.trmnl.liquid_enabled') ? $plugin['preferred_renderer'] : null,
$plugin['logo_url'] ?? null, $plugin['logo_url'] ?? null,
allowDuplicate: true allowDuplicate: true
); );

View file

@ -174,3 +174,100 @@ LIQUID
// Should not contain users < 30 // Should not contain users < 30
$this->assertStringNotContainsString('Alice (25)', $result); $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);
});