twig = new Environment($loader, [ 'cache' => WP_DEBUG ? false : $cacheDir, 'debug' => WP_DEBUG, 'auto_reload' => WP_DEBUG, ]); $this->registerWordPressFunctions(); $this->registerWordPressGlobals(); $this->registerFilters(); } 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 { // Translation functions. $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); })); $this->twig->addFunction(new TwigFunction('_n', function (string $single, string $plural, int $number, string $domain = 'wp-bootstrap'): string { return _n($single, $plural, $number, $domain); })); // 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. & → &). $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']])); // 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']])); $this->twig->addFunction(new TwigFunction('do_shortcode', function (string $content): string { return do_shortcode($content); }, ['is_safe' => ['html']])); // Formatting. $this->twig->addFunction(new TwigFunction('number_format_i18n', function (float $number, int $decimals = 0): string { return number_format_i18n($number, $decimals); })); // 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']])); } 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']])); // Escaping filters — same functions registered above, but as filters for |esc_html syntax. $this->twig->addFilter(new TwigFilter('esc_html', 'esc_html', ['is_safe' => ['html']])); $this->twig->addFilter(new TwigFilter('esc_attr', 'esc_attr', ['is_safe' => ['html']])); $this->twig->addFilter(new TwigFilter('esc_url', 'esc_url', ['is_safe' => ['html']])); } }