$vars */ public function render(string $template, array $vars): string { $vars = array_change_key_case($vars, CASE_LOWER); // Blocs conditionnels, du plus interne vers l'extérieur. $guard = 0; while (preg_match(self::BLOCK, $template) && $guard++ < 50) { $template = preg_replace_callback(self::BLOCK, function (array $m) use ($vars) { $filled = $this->filled($vars[strtolower($m[2])] ?? null); $keep = strtolower($m[1]) === 'if' ? $filled : ! $filled; return $keep ? $m[3] : ''; }, $template); } // Valeurs brutes puis échappées. $template = preg_replace_callback('/\{\{\{\s*([a-z0-9_]+)\s*\}\}\}/i', fn ($m) => (string) ($vars[strtolower($m[1])] ?? ''), $template); return preg_replace_callback('/\{\{\s*([a-z0-9_]+)\s*\}\}/i', fn ($m) => htmlspecialchars((string) ($vars[strtolower($m[1])] ?? ''), ENT_QUOTES | ENT_HTML5, 'UTF-8'), $template); } /** Liste des variables utilisées par un template (aide à l'édition). */ public function variables(string $template): array { preg_match_all('/\{\{\{?\s*(?:#(?:if|unless)\s+)?([a-z0-9_]+)\s*\}?\}\}/i', $template, $m); return array_values(array_unique(array_map('strtolower', $m[1]))); } private function filled(mixed $v): bool { return $v !== null && trim((string) $v) !== ''; } }