Initial theme scaffolding (v0.0.1)
- Bootstrap 5 CSS/JS integration via Yarn (served locally)
- Dart Sass build pipeline with PostCSS, Autoprefixer, cssnano
- Twig 3.0 via Composer with PSR-4 autoloading
- FSE block theme templates (index, home, single, page, archive, search, 404)
- Template parts (header, footer) and block patterns
- theme.json with Bootstrap 5-aligned design tokens
- Gitea CI/CD workflow for automated release packages
- WordPress i18n support (en_US base, de_CH translation)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-08 02:25:33 +01:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* Twig Template Engine Service.
|
|
|
|
|
*
|
|
|
|
|
* @package WPBootstrap
|
|
|
|
|
* @since 0.0.1
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
namespace WPBootstrap\Twig;
|
|
|
|
|
|
|
|
|
|
use Twig\Environment;
|
|
|
|
|
use Twig\Loader\FilesystemLoader;
|
|
|
|
|
use Twig\TwigFunction;
|
2026-02-08 15:11:00 +01:00
|
|
|
use Twig\TwigFilter;
|
Initial theme scaffolding (v0.0.1)
- Bootstrap 5 CSS/JS integration via Yarn (served locally)
- Dart Sass build pipeline with PostCSS, Autoprefixer, cssnano
- Twig 3.0 via Composer with PSR-4 autoloading
- FSE block theme templates (index, home, single, page, archive, search, 404)
- Template parts (header, footer) and block patterns
- theme.json with Bootstrap 5-aligned design tokens
- Gitea CI/CD workflow for automated release packages
- WordPress i18n support (en_US base, de_CH translation)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-08 02:25:33 +01:00
|
|
|
|
|
|
|
|
class TwigService
|
|
|
|
|
{
|
|
|
|
|
private static ?TwigService $instance = null;
|
|
|
|
|
private Environment $twig;
|
|
|
|
|
|
|
|
|
|
private function __construct()
|
|
|
|
|
{
|
|
|
|
|
$viewsDir = get_template_directory() . '/views';
|
|
|
|
|
$cacheDir = WP_CONTENT_DIR . '/cache/twig';
|
|
|
|
|
|
|
|
|
|
$loader = new FilesystemLoader($viewsDir);
|
|
|
|
|
$this->twig = new Environment($loader, [
|
2026-02-19 18:26:40 +01:00
|
|
|
'cache' => WP_DEBUG ? false : $cacheDir,
|
|
|
|
|
'debug' => WP_DEBUG,
|
|
|
|
|
'auto_reload' => WP_DEBUG,
|
Initial theme scaffolding (v0.0.1)
- Bootstrap 5 CSS/JS integration via Yarn (served locally)
- Dart Sass build pipeline with PostCSS, Autoprefixer, cssnano
- Twig 3.0 via Composer with PSR-4 autoloading
- FSE block theme templates (index, home, single, page, archive, search, 404)
- Template parts (header, footer) and block patterns
- theme.json with Bootstrap 5-aligned design tokens
- Gitea CI/CD workflow for automated release packages
- WordPress i18n support (en_US base, de_CH translation)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-08 02:25:33 +01:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->registerWordPressFunctions();
|
2026-02-08 15:11:00 +01:00
|
|
|
$this->registerWordPressGlobals();
|
|
|
|
|
$this->registerFilters();
|
Initial theme scaffolding (v0.0.1)
- Bootstrap 5 CSS/JS integration via Yarn (served locally)
- Dart Sass build pipeline with PostCSS, Autoprefixer, cssnano
- Twig 3.0 via Composer with PSR-4 autoloading
- FSE block theme templates (index, home, single, page, archive, search, 404)
- Template parts (header, footer) and block patterns
- theme.json with Bootstrap 5-aligned design tokens
- Gitea CI/CD workflow for automated release packages
- WordPress i18n support (en_US base, de_CH translation)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-08 02:25:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function getInstance(): self
|
|
|
|
|
{
|
|
|
|
|
if (self::$instance === null) {
|
|
|
|
|
self::$instance = new self();
|
|
|
|
|
}
|
|
|
|
|
return self::$instance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function render(string $template, array $context = []): string
|
|
|
|
|
{
|
|
|
|
|
return $this->twig->render($template, $context);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function display(string $template, array $context = []): void
|
|
|
|
|
{
|
|
|
|
|
$this->twig->display($template, $context);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getEnvironment(): Environment
|
|
|
|
|
{
|
|
|
|
|
return $this->twig;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function registerWordPressFunctions(): void
|
|
|
|
|
{
|
2026-02-08 15:11:00 +01:00
|
|
|
// Translation functions.
|
Initial theme scaffolding (v0.0.1)
- Bootstrap 5 CSS/JS integration via Yarn (served locally)
- Dart Sass build pipeline with PostCSS, Autoprefixer, cssnano
- Twig 3.0 via Composer with PSR-4 autoloading
- FSE block theme templates (index, home, single, page, archive, search, 404)
- Template parts (header, footer) and block patterns
- theme.json with Bootstrap 5-aligned design tokens
- Gitea CI/CD workflow for automated release packages
- WordPress i18n support (en_US base, de_CH translation)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-08 02:25:33 +01:00
|
|
|
$this->twig->addFunction(new TwigFunction('__', function (string $text, string $domain = 'wp-bootstrap'): string {
|
|
|
|
|
return __($text, $domain);
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
$this->twig->addFunction(new TwigFunction('_e', function (string $text, string $domain = 'wp-bootstrap'): void {
|
|
|
|
|
_e($text, $domain);
|
|
|
|
|
}));
|
|
|
|
|
|
2026-02-08 15:11:00 +01:00
|
|
|
$this->twig->addFunction(new TwigFunction('_n', function (string $single, string $plural, int $number, string $domain = 'wp-bootstrap'): string {
|
|
|
|
|
return _n($single, $plural, $number, $domain);
|
|
|
|
|
}));
|
|
|
|
|
|
2026-02-19 13:23:33 +01:00
|
|
|
// Escaping functions — marked is_safe so Twig does not double-escape their output.
|
|
|
|
|
// These functions already return HTML-safe strings; without is_safe, enabling
|
|
|
|
|
// Twig autoescape would double-encode the result (e.g. & → &amp;).
|
|
|
|
|
$this->twig->addFunction(new TwigFunction('esc_html', 'esc_html', ['is_safe' => ['html']]));
|
|
|
|
|
$this->twig->addFunction(new TwigFunction('esc_attr', 'esc_attr', ['is_safe' => ['html']]));
|
|
|
|
|
$this->twig->addFunction(new TwigFunction('esc_url', 'esc_url', ['is_safe' => ['html']]));
|
2026-02-08 15:11:00 +01:00
|
|
|
|
|
|
|
|
// WordPress head/footer output (captured via output buffering).
|
|
|
|
|
$this->twig->addFunction(new TwigFunction('wp_head', function (): string {
|
|
|
|
|
ob_start();
|
|
|
|
|
wp_head();
|
|
|
|
|
return ob_get_clean();
|
|
|
|
|
}, ['is_safe' => ['html']]));
|
|
|
|
|
|
|
|
|
|
$this->twig->addFunction(new TwigFunction('wp_footer', function (): string {
|
|
|
|
|
ob_start();
|
|
|
|
|
wp_footer();
|
|
|
|
|
return ob_get_clean();
|
|
|
|
|
}, ['is_safe' => ['html']]));
|
|
|
|
|
|
|
|
|
|
$this->twig->addFunction(new TwigFunction('wp_body_open', function (): string {
|
|
|
|
|
ob_start();
|
|
|
|
|
wp_body_open();
|
|
|
|
|
return ob_get_clean();
|
|
|
|
|
}, ['is_safe' => ['html']]));
|
|
|
|
|
|
|
|
|
|
// HTML attribute helpers.
|
|
|
|
|
$this->twig->addFunction(new TwigFunction('language_attributes', function (): string {
|
|
|
|
|
ob_start();
|
|
|
|
|
language_attributes();
|
|
|
|
|
return ob_get_clean();
|
|
|
|
|
}, ['is_safe' => ['html']]));
|
|
|
|
|
|
|
|
|
|
$this->twig->addFunction(new TwigFunction('body_class', function (string $extra = ''): string {
|
|
|
|
|
ob_start();
|
|
|
|
|
body_class($extra);
|
|
|
|
|
return ob_get_clean();
|
|
|
|
|
}, ['is_safe' => ['html']]));
|
|
|
|
|
|
|
|
|
|
// URL and info helpers.
|
|
|
|
|
$this->twig->addFunction(new TwigFunction('home_url', function (string $path = '/'): string {
|
|
|
|
|
return home_url($path);
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
$this->twig->addFunction(new TwigFunction('get_template_directory_uri', function (): string {
|
|
|
|
|
return get_template_directory_uri();
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
$this->twig->addFunction(new TwigFunction('get_bloginfo', function (string $show): string {
|
|
|
|
|
return get_bloginfo($show);
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
$this->twig->addFunction(new TwigFunction('get_search_query', function (): string {
|
|
|
|
|
return get_search_query();
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// Content filtering.
|
|
|
|
|
$this->twig->addFunction(new TwigFunction('wp_kses_post', function (string $content): string {
|
|
|
|
|
return wp_kses_post($content);
|
|
|
|
|
}, ['is_safe' => ['html']]));
|
|
|
|
|
|
2026-02-18 15:06:15 +01:00
|
|
|
$this->twig->addFunction(new TwigFunction('do_shortcode', function (string $content): string {
|
|
|
|
|
return do_shortcode($content);
|
|
|
|
|
}, ['is_safe' => ['html']]));
|
|
|
|
|
|
2026-02-08 15:11:00 +01:00
|
|
|
// Formatting.
|
|
|
|
|
$this->twig->addFunction(new TwigFunction('number_format_i18n', function (float $number, int $decimals = 0): string {
|
|
|
|
|
return number_format_i18n($number, $decimals);
|
|
|
|
|
}));
|
2026-02-14 21:44:45 +01:00
|
|
|
|
|
|
|
|
// Block template parts (allows FSE Template Editor changes to take effect).
|
|
|
|
|
$this->twig->addFunction(new TwigFunction('block_template_part', function (string $part): string {
|
|
|
|
|
ob_start();
|
|
|
|
|
block_template_part($part);
|
|
|
|
|
return ob_get_clean();
|
|
|
|
|
}, ['is_safe' => ['html']]));
|
2026-02-08 15:11:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function registerWordPressGlobals(): void
|
|
|
|
|
{
|
|
|
|
|
$this->twig->addGlobal('site_name', get_bloginfo('name'));
|
|
|
|
|
$this->twig->addGlobal('site_description', get_bloginfo('description'));
|
|
|
|
|
$this->twig->addGlobal('site_url', home_url('/'));
|
|
|
|
|
$this->twig->addGlobal('theme_uri', get_template_directory_uri());
|
|
|
|
|
$this->twig->addGlobal('charset', get_bloginfo('charset'));
|
|
|
|
|
$this->twig->addGlobal('current_year', date('Y'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function registerFilters(): void
|
|
|
|
|
{
|
|
|
|
|
$this->twig->addFilter(new TwigFilter('wpautop', 'wpautop', ['is_safe' => ['html']]));
|
|
|
|
|
$this->twig->addFilter(new TwigFilter('wp_kses_post', 'wp_kses_post', ['is_safe' => ['html']]));
|
Initial theme scaffolding (v0.0.1)
- Bootstrap 5 CSS/JS integration via Yarn (served locally)
- Dart Sass build pipeline with PostCSS, Autoprefixer, cssnano
- Twig 3.0 via Composer with PSR-4 autoloading
- FSE block theme templates (index, home, single, page, archive, search, 404)
- Template parts (header, footer) and block patterns
- theme.json with Bootstrap 5-aligned design tokens
- Gitea CI/CD workflow for automated release packages
- WordPress i18n support (en_US base, de_CH translation)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-08 02:25:33 +01:00
|
|
|
}
|
|
|
|
|
}
|