getEnvironment(); // Add child theme templates directory to the Twig loader so {% include %} // directives in Twig templates can find other child theme templates. $loader = $env->getLoader(); if ( $loader instanceof \Twig\Loader\FilesystemLoader ) { $template_dir = WC_BOOTSTRAP_PATH . 'templates'; if ( is_dir( $template_dir ) ) { $loader->prependPath( $template_dir ); } } // Register WooCommerce functions and filters as Twig extensions. $env->addExtension( new \WcBootstrap\Twig\WooCommerceExtension() ); // Register template interception hooks. $override = new \WcBootstrap\TemplateOverride(); $override->register(); } add_action( 'init', 'wc_bootstrap_init_twig_bridge', 20 ); /** * Enqueue child theme styles. * * Loads parent theme stylesheet first, then child theme overrides. * CSS cascade order: * 1. wp-bootstrap (parent) * 2. woocommerce (plugin styles) * 3. wc-bootstrap-style (child theme style.css) * 4. wc-bootstrap-overrides (plugin CSS overrides) */ function wc_bootstrap_enqueue_styles(): void { $theme_version = wp_get_theme()->get( 'Version' ); // Enqueue parent theme stylesheet. wp_enqueue_style( 'wp-bootstrap-style', get_template_directory_uri() . '/assets/css/style.min.css', array(), wp_get_theme( 'wp-bootstrap' )->get( 'Version' ) ); // Enqueue child theme stylesheet. wp_enqueue_style( 'wc-bootstrap-style', get_stylesheet_directory_uri() . '/style.css', array( 'wp-bootstrap-style' ), $theme_version ); // Enqueue plugin Bootstrap override styles. // Depend on plugin stylesheets so overrides always load after plugin CSS. wp_enqueue_style( 'wc-bootstrap-overrides', get_stylesheet_directory_uri() . '/assets/css/wc-bootstrap.css', array( 'wc-bootstrap-style', 'woocommerce-general' ), $theme_version ); } add_action( 'wp_enqueue_scripts', 'wc_bootstrap_enqueue_styles' ); /** * Enqueue child theme scripts. * * @since 0.1.0 */ function wc_bootstrap_enqueue_scripts(): void { $theme_version = wp_get_theme()->get( 'Version' ); // Quantity +/- button handler for Bootstrap input-group widget. wp_enqueue_script( 'wc-bootstrap-quantity', get_stylesheet_directory_uri() . '/assets/js/quantity.js', array(), $theme_version, true ); // Product gallery thumbnail handler for single product pages. if ( function_exists( 'is_product' ) && is_product() ) { wp_enqueue_script( 'wc-bootstrap-gallery', get_stylesheet_directory_uri() . '/assets/js/product-gallery.js', array(), $theme_version, true ); } } add_action( 'wp_enqueue_scripts', 'wc_bootstrap_enqueue_scripts' ); /** * Build the parent theme context for a page render. * * Caches the ContextBuilder result per request to avoid redundant database * queries when multiple WooCommerce rendering functions need the same context. * * @since 0.1.1 * * @return array Theme context array. */ function wc_bootstrap_get_theme_context(): array { static $cached_context = null; if ( null === $cached_context ) { $context_builder = new \WPBootstrap\Template\ContextBuilder(); $cached_context = $context_builder->build(); } return $cached_context; } /** * Render content inside the parent theme's page shell. * * Injects the given HTML content into the parent theme's page template, * replacing the post content. Title and thumbnail are blanked so the * parent theme does not render its own headings — the content handles that. * * @since 0.1.1 * * @param string $content HTML content to render inside the page shell. */ function wc_bootstrap_render_in_page_shell( string $content ): void { $theme_context = wc_bootstrap_get_theme_context(); $twig = \WPBootstrap\Twig\TwigService::getInstance(); $theme_context['post'] = array_merge( $theme_context['post'] ?? [], [ 'content' => $content, 'title' => '', 'thumbnail' => '', ] ); echo $twig->render( 'pages/page.html.twig', $theme_context ); } /** * Handle plugin page rendering via plugin render filter. * * Delegates page rendering to the parent theme's TwigService so that plugin pages * share the same page shell (header, footer, layout) as native WordPress pages. * Falls back to letting the plugin handle rendering if the parent theme is not available. * * @param bool $rendered Whether the page has been rendered. * @param string $content Pre-rendered plugin HTML content. * @param array $context Plugin template context. * @return bool True if rendering was handled, false to let plugin use fallback. * * @since 0.1.0 */ function wc_bootstrap_render_page( bool $rendered, string $content, array $context ): bool { if ( ! class_exists( '\WPBootstrap\Twig\TwigService' ) || ! class_exists( '\WPBootstrap\Template\ContextBuilder' ) ) { return false; } wc_bootstrap_render_in_page_shell( $content ); return true; } add_filter( 'woocommerce_render_page', 'wc_bootstrap_render_page', 10, 3 ); /** * Signal to the plugin that its content will be wrapped by the parent theme. * * When the parent theme's TwigService is available, the plugin templates should skip * their outer wrapper elements (breadcrumbs, sidebar, page shell) to avoid double-wrapping. * * @param bool $wrapped Whether content is theme-wrapped. * @return bool * * @since 0.1.0 */ function wc_bootstrap_is_wrapped( bool $wrapped ): bool { if ( class_exists( '\WPBootstrap\Twig\TwigService' ) ) { return true; } return $wrapped; } add_filter( 'woocommerce_is_theme_wrapped', 'wc_bootstrap_is_wrapped' ); /** * Add sticky header scroll shadow behavior. * * Toggles an 'is-stuck' class on the navbar when the page is scrolled, * adding a subtle box-shadow to visually separate the header from content. * * @since 0.1.0 */ function wc_bootstrap_sticky_header_script(): void { ?> __( 'Shop Sidebar', 'wc-bootstrap' ), 'id' => 'shop-sidebar', 'description' => __( 'Add widgets here to appear in the shop sidebar.', 'wc-bootstrap' ), 'before_widget' => '
', 'before_title' => '