From dc092e3fa55f45c1e1b8df3cc7d000527d3a27fa Mon Sep 17 00:00:00 2001
From: Abhishek Singh
Date: Wed, 29 Apr 2026 13:00:44 +0530
Subject: [PATCH 01/19] Add Component based rendering with priority
---
inc/Framework/ComponentLoader.php | 165 ++++++++++++
inc/helpers/custom-functions.php | 22 +-
src/Components/Button/Button.php | 43 +++
src/Components/Card/Card.php | 63 +++++
.../php/inc/Framework/ComponentLoaderTest.php | 247 ++++++++++++++++++
5 files changed, 539 insertions(+), 1 deletion(-)
create mode 100644 inc/Framework/ComponentLoader.php
create mode 100644 src/Components/Button/Button.php
create mode 100644 src/Components/Card/Card.php
create mode 100644 tests/php/inc/Framework/ComponentLoaderTest.php
diff --git a/inc/Framework/ComponentLoader.php b/inc/Framework/ComponentLoader.php
new file mode 100644
index 00000000..fb5faa68
--- /dev/null
+++ b/inc/Framework/ComponentLoader.php
@@ -0,0 +1,165 @@
+ directory path.
+ * @param string $name Component name being resolved.
+ * @param array $options Options passed to render().
+ */
+ $paths = apply_filters(
+ 'elementary_theme_component_paths',
+ [
+ 'theme' => ELEMENTARY_THEME_TEMP_DIR . '/src/Components',
+ ],
+ $name,
+ $options
+ );
+
+ // Order sources based on priority.
+ $order = self::get_source_order( $priority, $paths );
+
+ foreach ( $order as $source ) {
+
+ if ( empty( $paths[ $source ] ) ) {
+ continue;
+ }
+
+ $file = trailingslashit( $paths[ $source ] ) . $name . '/' . $name . '.php';
+
+ if ( file_exists( $file ) && is_readable( $file ) ) {
+ return $file;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the resolution priority.
+ *
+ * @param array $options Options array potentially containing 'priority'.
+ *
+ * @return string 'theme' or 'plugin'.
+ */
+ private static function get_priority( array $options ): string {
+
+ if ( ! empty( $options['priority'] ) && in_array( $options['priority'], [ 'theme', 'plugin' ], true ) ) {
+ return $options['priority'];
+ }
+
+ /**
+ * Filters the default component resolution priority.
+ *
+ * @since 1.0.0
+ *
+ * @param string $priority Default priority. Accepts 'theme' or 'plugin'.
+ */
+ $default = apply_filters( 'elementary_theme_component_default_priority', 'theme' );
+
+ if ( in_array( $default, [ 'theme', 'plugin' ], true ) ) {
+ return $default;
+ }
+
+ return 'theme';
+ }
+
+ /**
+ * Get the source resolution order based on priority.
+ *
+ * @param string $priority 'theme' or 'plugin'.
+ * @param array $paths Registered paths keyed by source.
+ *
+ * @return array Ordered list of source keys to check.
+ */
+ private static function get_source_order( string $priority, array $paths ): array {
+
+ $sources = array_keys( $paths );
+
+ if ( 'plugin' === $priority ) {
+ // Move 'plugin' to front if it exists.
+ $key = array_search( 'plugin', $sources, true );
+
+ if ( false !== $key ) {
+ unset( $sources[ $key ] );
+ array_unshift( $sources, 'plugin' );
+ }
+ } else {
+ // Move 'theme' to front if it exists.
+ $key = array_search( 'theme', $sources, true );
+
+ if ( false !== $key ) {
+ unset( $sources[ $key ] );
+ array_unshift( $sources, 'theme' );
+ }
+ }
+
+ return array_values( $sources );
+ }
+}
diff --git a/inc/helpers/custom-functions.php b/inc/helpers/custom-functions.php
index 98e7996f..a973f7bb 100644
--- a/inc/helpers/custom-functions.php
+++ b/inc/helpers/custom-functions.php
@@ -7,4 +7,24 @@
declare( strict_types = 1 );
-// Define custom functions here.
+use rtCamp\Theme\Elementary\Framework\ComponentLoader;
+
+if ( ! function_exists( 'elementary_theme_component' ) ) {
+
+ /**
+ * Render a component by name.
+ *
+ * Global convenience wrapper for ComponentLoader::render().
+ *
+ * @since 1.0.0
+ *
+ * @param string $name Component name (e.g. 'Button', 'Card').
+ * @param array $args Arguments to pass to the component.
+ * @param array $options Optional. Resolution options. See ComponentLoader::render().
+ *
+ * @return void
+ */
+ function elementary_theme_component( string $name, array $args = [], array $options = [] ): void {
+ ComponentLoader::render( $name, $args, $options );
+ }
+}
diff --git a/src/Components/Button/Button.php b/src/Components/Button/Button.php
new file mode 100644
index 00000000..4b0f682e
--- /dev/null
+++ b/src/Components/Button/Button.php
@@ -0,0 +1,43 @@
+%s',
+ esc_url( $url ),
+ esc_attr( $css_class ),
+ esc_html( $label )
+ );
+} else {
+ printf(
+ '',
+ esc_attr( $css_class ),
+ esc_html( $label )
+ );
+}
diff --git a/src/Components/Card/Card.php b/src/Components/Card/Card.php
new file mode 100644
index 00000000..8e0cd48e
--- /dev/null
+++ b/src/Components/Card/Card.php
@@ -0,0 +1,63 @@
+
+
+
+
+
; ?>)
+
+
+
+
+
+
+
+
+
+
+
+
+ $title,
+ 'url' => $url,
+ 'class' => 'elementary-card__button',
+ ]
+ );
+ ?>
+
+
+
+
+assertTrue( class_exists( ComponentLoader::class ) );
+ }
+
+ /**
+ * Test render outputs component HTML for a known component.
+ */
+ public function test_render_outputs_button_component(): void {
+ ob_start();
+ ComponentLoader::render( 'Button', [ 'label' => 'Test Button' ] );
+ $output = ob_get_clean();
+
+ $this->assertStringContainsString( 'Test Button', $output );
+ $this->assertStringContainsString( '
instead of in the
.
- *
- * @return bool Whether the script has been registered.
- */
- private static function register_component_script( string $handle, array $asset, array $deps = [], string|bool|null $ver = false, bool $in_footer = true ): bool {
- if (
- empty( $asset['url'] ) ||
- empty( $asset['file'] ) ||
- ! file_exists( $asset['file'] )
- ) {
- return false;
- }
-
- if ( wp_script_is( $handle, 'registered' ) ) {
- return true;
- }
-
- $asset_meta = self::get_component_asset_meta( (string) $asset['file'], $deps, $ver );
-
- return wp_register_script( $handle, (string) $asset['url'], $asset_meta['dependencies'], $asset_meta['version'], $in_footer );
- }
-
- /**
- * Register a component stylesheet.
- *
- * @param string $handle Name of the stylesheet. Should be unique.
- * @param array $asset Component asset metadata.
- * @param array $deps Optional. An array of registered stylesheet handles this stylesheet depends on. Default empty array.
- * @param string|bool|null $ver Optional. String specifying style version number, if not set, filetime will be used as version number.
- * @param string $media Optional. The media for which this stylesheet has been defined.
- *
- * @return bool Whether the style has been registered.
- */
- private static function register_component_style( string $handle, array $asset, array $deps = [], string|bool|null $ver = false, string $media = 'all' ): bool {
- if (
- empty( $asset['url'] ) ||
- empty( $asset['file'] ) ||
- ! file_exists( $asset['file'] )
- ) {
- return false;
- }
-
- if ( wp_style_is( $handle, 'registered' ) ) {
- return true;
- }
-
- $asset_meta = self::get_component_asset_meta( (string) $asset['file'], $deps, $ver );
-
- return wp_register_style( $handle, (string) $asset['url'], $asset_meta['dependencies'], $asset_meta['version'], $media );
- }
-
- /**
- * Get component asset dependencies and version info from a matching .asset.php file.
- *
- * @param string $file Asset file path.
- * @param array $deps Asset dependencies to merge with.
- * @param string|bool|null $ver Asset version string.
- *
- * @return array{dependencies: array, version: string|bool} Asset meta information including dependencies and version.
- */
- private static function get_component_asset_meta( string $file, array $deps = [], string|bool|null $ver = false ): array {
- $asset_meta_file = preg_replace( '/\.[^\/\\\\.]+$/', '.asset.php', $file );
- $asset_meta_file = ! empty( $asset_meta_file ) ? $asset_meta_file : $file . '.asset.php';
-
- if ( ! array_key_exists( $asset_meta_file, self::$asset_meta_cache ) ) {
- self::$asset_meta_cache[ $asset_meta_file ] = is_readable( $asset_meta_file ) ? require $asset_meta_file : [];
- }
-
- $asset_meta = self::$asset_meta_cache[ $asset_meta_file ];
-
- if ( ! is_array( $asset_meta ) ) {
- $asset_meta = [];
- }
-
- $dependencies = $asset_meta['dependencies'] ?? [];
- $version = $asset_meta['version'] ?? self::get_component_file_version( $file, $ver );
-
- if ( ! is_array( $dependencies ) ) {
- $dependencies = [];
- }
-
- $dependencies = array_values( array_filter( $dependencies, 'is_string' ) );
-
- return [
- 'dependencies' => array_merge( $deps, $dependencies ),
- 'version' => is_string( $version ) || is_bool( $version )
- ? $version
- : ( is_int( $version ) ? (string) $version : self::get_component_file_version( $file, $ver ) ),
- ];
- }
-
- /**
- * Get component asset file version.
- *
- * @param string $file File path.
- * @param string|bool|null $ver File version.
- *
- * @return string|bool File version based on file modification time or provided version.
- */
- private static function get_component_file_version( string $file, string|bool|null $ver = false ): string|bool {
- if ( ! empty( $ver ) ) {
- return $ver;
- }
-
- return file_exists( $file ) ? (string) filemtime( $file ) : false;
- }
-
- /**
- * Normalize and validate a component name before using it in filesystem paths.
- *
- * Normalization trims surrounding whitespace. Validation then enforces
- * length bounds, blocks traversal and path separators, and allows only
- * alphanumeric characters, underscores and dashes.
- *
- * @param string $name Component name to normalize and validate.
- *
- * @return string|false Normalized component name, or false when invalid.
- */
- private static function normalize_component_name( string $name ): string|false {
- $name = trim( $name );
-
- if (
- '' === $name ||
- strlen( $name ) > 128 ||
- str_contains( $name, '..' ) ||
- str_contains( $name, '/' ) ||
- str_contains( $name, '\\' ) ||
- 1 !== preg_match( '/^[A-Za-z0-9_-]+$/', $name )
- ) {
- return false;
- }
-
- return $name;
- }
-
-}
diff --git a/tests/php/inc/Framework/ComponentLoaderTest.php b/tests/php/inc/Framework/ComponentLoaderTest.php
deleted file mode 100644
index 2fc0b0dd..00000000
--- a/tests/php/inc/Framework/ComponentLoaderTest.php
+++ /dev/null
@@ -1,1312 +0,0 @@
-assertTrue( class_exists( ComponentLoader::class ) );
- }
-
- /**
- * Test render outputs component HTML for a known component.
- */
- public function test_render_outputs_button_component(): void {
- ob_start();
- ComponentLoader::render( 'Button', [ 'label' => 'Test Button' ] );
- $output = ob_get_clean();
-
- $this->assertStringContainsString( 'Test Button', $output );
- $this->assertStringContainsString( '