feat: add trmnl-liquid renderer option

This commit is contained in:
Benjamin Nussbaum 2025-11-04 18:35:08 +01:00
parent a7a541da42
commit aa80e944f9
3 changed files with 135 additions and 51 deletions

View file

@ -19,6 +19,7 @@ use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Keepsuit\LaravelLiquid\LaravelLiquidExtension; use Keepsuit\LaravelLiquid\LaravelLiquidExtension;
use Keepsuit\Liquid\Exceptions\LiquidException; use Keepsuit\Liquid\Exceptions\LiquidException;
@ -40,6 +41,7 @@ class Plugin extends Model
'configuration_template' => 'json', 'configuration_template' => 'json',
'no_bleed' => 'boolean', 'no_bleed' => 'boolean',
'dark_mode' => 'boolean', 'dark_mode' => 'boolean',
'preferred_renderer' => 'string',
]; ];
protected static function boot() protected static function boot()
@ -363,6 +365,50 @@ class Plugin extends Model
return $liquidTemplate->render($context); return $liquidTemplate->render($context);
} }
/**
* Render template using external Ruby liquid renderer
*
* @param string $template The liquid template string
* @param array $context The render context data
* @return string The rendered HTML
*
* @throws LiquidException
*/
private function renderWithExternalLiquidRenderer(string $template, array $context): string
{
$liquidPath = config('services.trmnl.liquid_path');
if (empty($liquidPath)) {
throw new LiquidException('External liquid renderer path is not configured');
}
// HTML encode the template
$encodedTemplate = htmlspecialchars($template, ENT_QUOTES, 'UTF-8');
// Encode context as JSON
$jsonContext = json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($jsonContext === false) {
throw new LiquidException('Failed to encode render context as JSON: '.json_last_error_msg());
}
// Execute the external renderer
$process = Process::run([
$liquidPath,
'--template',
$encodedTemplate,
'--context',
$jsonContext,
]);
if (! $process->successful()) {
$errorOutput = $process->errorOutput() ?: $process->output();
throw new Exception('External liquid renderer failed: '.$errorOutput);
}
return $process->output();
}
/** /**
* Render the plugin's markup * Render the plugin's markup
* *
@ -374,59 +420,67 @@ class Plugin extends Model
$renderedContent = ''; $renderedContent = '';
if ($this->markup_language === 'liquid') { if ($this->markup_language === 'liquid') {
// Create a custom environment with inline templates support // Build render context
$inlineFileSystem = new InlineTemplatesFileSystem(); $context = [
$environment = new \Keepsuit\Liquid\Environment( 'size' => $size,
fileSystem: $inlineFileSystem, 'data' => $this->data_payload,
extensions: [new StandardExtension(), new LaravelLiquidExtension()] 'config' => $this->configuration ?? [],
); ...(is_array($this->data_payload) ? $this->data_payload : []),
'trmnl' => [
// Register all custom filters 'system' => [
$environment->filterRegistry->register(Data::class); 'timestamp_utc' => now()->utc()->timestamp,
$environment->filterRegistry->register(Date::class); ],
$environment->filterRegistry->register(Localization::class); 'user' => [
$environment->filterRegistry->register(Numbers::class); 'utc_offset' => '0',
$environment->filterRegistry->register(StringMarkup::class); 'name' => $this->user->name ?? 'Unknown User',
$environment->filterRegistry->register(Uniqueness::class); 'locale' => 'en',
'time_zone_iana' => config('app.timezone'),
// Register the template tag for inline templates ],
$environment->tagRegistry->register(TemplateTag::class); 'plugin_settings' => [
'instance_name' => $this->name,
// Apply Liquid replacements (including 'with' syntax conversion) 'strategy' => $this->data_strategy,
$processedMarkup = $this->applyLiquidReplacements($this->render_markup); 'dark_mode' => $this->dark_mode ? 'yes' : 'no',
'no_screen_padding' => $this->no_bleed ? 'yes' : 'no',
$template = $environment->parseString($processedMarkup); 'polling_headers' => $this->polling_header,
$context = $environment->newRenderContext( 'polling_url' => $this->polling_url,
data: [ 'custom_fields_values' => [
'size' => $size, ...(is_array($this->configuration) ? $this->configuration : []),
'data' => $this->data_payload,
'config' => $this->configuration ?? [],
...(is_array($this->data_payload) ? $this->data_payload : []),
'trmnl' => [
'system' => [
'timestamp_utc' => now()->utc()->timestamp,
],
'user' => [
'utc_offset' => '0',
'name' => $this->user->name ?? 'Unknown User',
'locale' => 'en',
'time_zone_iana' => config('app.timezone'),
],
'plugin_settings' => [
'instance_name' => $this->name,
'strategy' => $this->data_strategy,
'dark_mode' => $this->dark_mode ? 'yes' : 'no',
'no_screen_padding' => $this->no_bleed ? 'yes' : 'no',
'polling_headers' => $this->polling_header,
'polling_url' => $this->polling_url,
'custom_fields_values' => [
...(is_array($this->configuration) ? $this->configuration : []),
],
], ],
], ],
] ],
); ];
$renderedContent = $template->render($context);
// Check if external renderer should be used
if ($this->preferred_renderer === 'trmnl-liquid' && config('services.trmnl.liquid_enabled')) {
// Use external Ruby renderer - pass raw template without preprocessing
$renderedContent = $this->renderWithExternalLiquidRenderer($this->render_markup, $context);
} else {
// Use PHP keepsuit/liquid renderer
// Create a custom environment with inline templates support
$inlineFileSystem = new InlineTemplatesFileSystem();
$environment = new \Keepsuit\Liquid\Environment(
fileSystem: $inlineFileSystem,
extensions: [new StandardExtension(), new LaravelLiquidExtension()]
);
// Register all custom filters
$environment->filterRegistry->register(Data::class);
$environment->filterRegistry->register(Date::class);
$environment->filterRegistry->register(Localization::class);
$environment->filterRegistry->register(Numbers::class);
$environment->filterRegistry->register(StringMarkup::class);
$environment->filterRegistry->register(Uniqueness::class);
// Register the template tag for inline templates
$environment->tagRegistry->register(TemplateTag::class);
// Apply Liquid replacements (including 'with' syntax conversion)
$processedMarkup = $this->applyLiquidReplacements($this->render_markup);
$template = $environment->parseString($processedMarkup);
$liquidContext = $environment->newRenderContext(data: $context);
$renderedContent = $template->render($liquidContext);
}
} else { } else {
$renderedContent = Blade::render($this->render_markup, [ $renderedContent = Blade::render($this->render_markup, [
'size' => $size, 'size' => $size,

View file

@ -41,6 +41,8 @@ return [
'proxy_refresh_cron' => env('TRMNL_PROXY_REFRESH_CRON'), 'proxy_refresh_cron' => env('TRMNL_PROXY_REFRESH_CRON'),
'override_orig_icon' => env('TRMNL_OVERRIDE_ORIG_ICON', false), 'override_orig_icon' => env('TRMNL_OVERRIDE_ORIG_ICON', false),
'image_url_timeout' => env('TRMNL_IMAGE_URL_TIMEOUT', 30), // 30 seconds; increase on low-powered devices 'image_url_timeout' => env('TRMNL_IMAGE_URL_TIMEOUT', 30), // 30 seconds; increase on low-powered devices
'liquid_enabled' => env('TRMNL_LIQUID_ENABLED', false),
'liquid_path' => env('TRMNL_LIQUID_PATH', '/usr/local/bin/trmnl-liquid-cli'),
], ],
'webhook' => [ 'webhook' => [

View file

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('plugins', function (Blueprint $table) {
$table->string('preferred_renderer')->nullable()->after('markup_language');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('plugins', function (Blueprint $table) {
$table->dropColumn('preferred_renderer');
});
}
};