From 85708c83b2af8246a585b7c0b878af794e4832db Mon Sep 17 00:00:00 2001 From: soderlind Date: Thu, 12 Oct 2017 22:18:32 +0200 Subject: [PATCH] Use transient instead of option as cache. Transients are inherently sped up by caching plugins, where normal Options are not. Add `$deps` to `wp_dynamic_css_enqueue`, $deps is an array of registered stylesheet handles this stylesheet depends on. Reformat code according to WordPress Coding Standards --- README.md | 31 +-- bootstrap.php | 14 +- cache.php | 178 +++++++------- compiler.php | 669 +++++++++++++++++++++++++------------------------- functions.php | 200 ++++++++------- readme.txt | 14 +- tests.php | 320 +++++++++++++++--------- 7 files changed, 759 insertions(+), 667 deletions(-) diff --git a/README.md b/README.md index a9509eb..740f96a 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ A library for generating static stylesheets from dynamic content, to be used in **Contributors:** ykadosh **Tags:** theme, mods, wordpress, dynamic, css, stylesheet -**Tested up to:** 4.7 -**Stable tag:** 1.0.5 +**Tested up to:** 4.8 +**Stable tag:** 1.0.6 **Requires:** PHP 5.3.0 or newer **WordPress plugin:** [wordpress.org/plugins/wp-dynamic-css/](https://wordpress.org/plugins/wp-dynamic-css/) **License:** GPLv3 or later @@ -133,7 +133,7 @@ require_once 'path/to/wp-dynamic-css/bootstrap.php'; ## Dynamic CSS Syntax -This library allows you to use special CSS syntax that is similar to regular CSS syntax with added support for variables with the syntax `$my_variable_name`. Since these variables are replaced by values during run time (when the page loads), files that are using this syntax are therefore called **dynamic CSS** files. Any variable in the dynamic CSS file is replaced by a value that is retrieved by a custom 'value callback' function. +This library allows you to use special CSS syntax that is similar to regular CSS syntax with added support for variables with the syntax `$my_variable_name`. Since these variables are replaced by values during run time (when the page loads), files that are using this syntax are therefore called **dynamic CSS** files. Any variable in the dynamic CSS file is replaced by a value that is retrieved by a custom 'value callback' function. A **dynamic CSS** file will look exactly like a regular CSS file, only with variables. For example: @@ -157,7 +157,7 @@ The callback function should accept a second variable that will hold an array of Future releases may support a more compex syntax, so any suggestions are welcome. You can make a suggestion by creating an issue or submitting a pull request. -**Piped filters (since 1.0.5)** +**Piped filters (since 1.0.5)** Version 1.0.5 added support for piped filters. Filters can be registered using the function `wp_dynamic_css_register_filter( $handle, $filter_name, $callback )` where `$filter_name` corresponds to the name fo the filter to be used in the stylesheet. For example, if a filter named `myFilter` was registered, it can be applied using the following syntax: @@ -309,10 +309,10 @@ body { ## Registering Filters -Filters are functions the alter the value of the variables. Filters can be registered using the function `wp_dynamic_css_register_filter( $handle, $filter_name, $callback )`. A registered filter can only be used within the stylesheet whose handle is given. A filter callback function takes the value of the variable as a parameter and should return the filtered value. For example: +Filters are functions the alter the value of the variables. Filters can be registered using the function `wp_dynamic_css_register_filter( $handle, $filter_name, $callback )`. A registered filter can only be used within the stylesheet whose handle is given. A filter callback function takes the value of the variable as a parameter and should return the filtered value. For example: ```php -function my_filter_callback( $value ) +function my_filter_callback( $value ) { return trim( $value ); } @@ -327,10 +327,10 @@ body { } ``` -Filters can also take additional arguments. For example: +Filters can also take additional arguments. For example: ```php -function my_filter_callback( $value, $arg1, $arg2 ) +function my_filter_callback( $value, $arg1, $arg2 ) { return $value.$arg1.$arg2; } @@ -359,8 +359,8 @@ body { *Enqueue a dynamic stylesheet* -```php -function wp_dynamic_css_enqueue( $handle, $path, $print = true, $minify = false, $cache = false ) +```php +function wp_dynamic_css_enqueue( $handle, $path, $deps = [], $print = true, $minify = false, $cache = false, $expiration = 0 ) ``` This function will either print the compiled version of the stylesheet to the document's section, or load it as an external stylesheet if `$print` is set to false. If `$cache` is set to true, a compiled version of the stylesheet will be stored in the database as soon as it is first compiled. The compiled version will be served thereafter until `wp_dynamic_css_clear_cache()` is called. @@ -368,15 +368,18 @@ This function will either print the compiled version of the stylesheet to the do **Parameters** * `$handle` (*string*) The stylesheet's name/id * `$path` (*string*) The absolute path to the dynamic CSS file +* `$deps` (*array*) An array of registered stylesheet handles this stylesheet depends on. * `$print` (*boolean*) Whether to print the compiled CSS to the document head, or load it as an external CSS file via an http request * `$minify` (*boolean*) Whether to minify the CSS output -* `$cache` (*boolean*) Whether to store the compiled version of this stylesheet in cache to avoid compilation on every page load. +* `$cache` (*boolean*) Whether to store the compiled version of this stylesheet in cache to avoid +compilation on every page load. +* `$expiration` (*int*) Time until expiration in seconds. ### wp_dynamic_css_set_callback *Set the value retrieval callback function* -```php +```php function wp_dynamic_css_set_callback( $handle, $callback ) ``` @@ -390,7 +393,7 @@ Set a callback function that will be used to convert variables to actual values. *Clear the cached compiled CSS for the given handle.* -```php +```php function wp_dynamic_css_clear_cache( $handle ) ``` @@ -403,7 +406,7 @@ Registered dynamic stylesheets that have the `$cache` flag set to true are compi *Register a filter function for a given stylesheet handle.* -```php +```php function wp_dynamic_css_register_filter( $handle, $filter_name, $callback ) ``` diff --git a/bootstrap.php b/bootstrap.php index 43360b0..cdfb1e5 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -26,18 +26,20 @@ /** * Prevent loading the library more than once */ -if( defined( 'WP_DYNAMIC_CSS' ) ) return; +if ( defined( 'WP_DYNAMIC_CSS' ) ) { + return; +} define( 'WP_DYNAMIC_CSS', true ); /** * Load required files */ -require_once dirname(__FILE__).'/compiler.php'; -require_once dirname(__FILE__).'/cache.php'; -require_once dirname(__FILE__).'/functions.php'; +require_once dirname( __FILE__ ) . '/compiler.php'; +require_once dirname( __FILE__ ) . '/cache.php'; +require_once dirname( __FILE__ ) . '/functions.php'; /** - * The following actions are used for printing or loading the compiled + * The following actions are used for printing or loading the compiled * stylesheets externally. * Priority is set to high (100) to allow the dynamic CSS to override static * styles. @@ -45,4 +47,4 @@ $dcss = DynamicCSSCompiler::get_instance(); add_action( 'wp_enqueue_scripts', array( $dcss, 'enqueue_styles' ), 100 ); add_action( 'wp_ajax_wp_dynamic_css', array( $dcss, 'ajax_callback' ) ); -add_action( 'wp_ajax_nopriv_wp_dynamic_css', array( $dcss, 'ajax_callback' ) ); \ No newline at end of file +add_action( 'wp_ajax_nopriv_wp_dynamic_css', array( $dcss, 'ajax_callback' ) ); diff --git a/cache.php b/cache.php index c06d61f..b3cda3e 100644 --- a/cache.php +++ b/cache.php @@ -10,91 +10,93 @@ /** * Dynamic CSS Cache Handler */ -class DynamicCSSCache -{ - /** - * @var DynamicCSSCache The reference to *Singleton* instance of this class - */ - private static $instance; - - /** - * @var array The reference to the cached compiled CSS that is stored in the database - */ - private static $cache; - - /** - * Returns the *Singleton* instance of this class. - * - * @return DynamicCSSCache The *Singleton* instance. - */ - public static function get_instance() - { - if (null === static::$instance) - { - static::$instance = new static(); - self::load_cache(); - } - return static::$instance; - } - - /** - * Returns the compiled CSS for the given handle if it exists in the cache. - * Returns false otherwise. - * - * @param string $handle The handle of the stylesheet - * @return boolean|string - */ - public function get( $handle ) - { - if( array_key_exists( $handle, self::$cache ) ) - { - return self::$cache[$handle]; - } - return false; - } - - /** - * Update the compiled CSS for the given handle. - * - * @param string $handle The handle of the stylesheet - * @param string $compiled_css - */ - public function update( $handle, $compiled_css ) - { - self::$cache[$handle] = $compiled_css; - $this->update_option(); - } - - /** - * Clear the compiled CSS from cache for the given handle. - * - * @param string $handle The handle of the stylesheet - */ - public function clear( $handle ) - { - unset(self::$cache[$handle]); - $this->update_option(); - } - - /** - * Load the cache value from the database or create an empty array if the - * cache is empty. - */ - private static function load_cache() - { - self::$cache = get_option('wp-dynamic-css-cache'); - - if( false === self::$cache ) - { - self::$cache = array(); - } - } - - /** - * Update the database option with the local cache value. - */ - private function update_option() - { - update_option('wp-dynamic-css-cache', self::$cache); - } -} \ No newline at end of file +class DynamicCSSCache { + + /** + * @var DynamicCSSCache The reference to *Singleton* instance of this class + */ + private static $instance; + + /** + * @var array The reference to the cached compiled CSS that is stored in the database + */ + private static $cache; + + /** + * Returns the *Singleton* instance of this class. + * + * @return DynamicCSSCache The *Singleton* instance. + */ + public static function get_instance() { + + if ( null === static::$instance ) { + static::$instance = new static(); + self::load_cache(); + } + return static::$instance; + } + + /** + * Returns the compiled CSS for the given handle if it exists in the cache. + * Returns false otherwise. + * + * @param string $handle The handle of the stylesheet + * @return boolean|string + */ + public function get( $handle ) { + + if ( array_key_exists( $handle, self::$cache ) ) { + return self::$cache[ $handle ]; + } + return false; + } + + /** + * Update the compiled CSS for the given handle. + * + * @param string $handle The handle of the stylesheet + * @param string $compiled_css + */ + public function update( $handle, $compiled_css, $expiration ) { + + self::$cache[ $handle ] = $compiled_css; + $this->update_option( $expiration ); + } + + /** + * Clear the compiled CSS from cache for the given handle. + * + * @param string $handle The handle of the stylesheet + */ + public function clear( $handle ) { + + unset( self::$cache[ $handle ] ); + delete_transient( 'wp-dynamic-css-cache' ); + } + + /** + * Load the cache value from the database or create an empty array if the + * cache is empty. + */ + private static function load_cache() { + + if ( false === ( self::$cache = get_transient( 'wp-dynamic-css-cache' ) ) ) { + self::$cache = []; + } + // self::$cache = get_option('wp-dynamic-css-cache'); + // + // if( false === self::$cache ) + // { + // self::$cache = array(); + // } + } + + /** + * Update the database option with the local cache value. + */ + private function update_option( $expiration ) { + + // update_option('wp-dynamic-css-cache', self::$cache); + set_transient( 'wp-dynamic-css-cache', self::$cache, $expiration ); + } +} diff --git a/compiler.php b/compiler.php index 33c82c3..74655ab 100644 --- a/compiler.php +++ b/compiler.php @@ -9,347 +9,342 @@ /** * Dynamic CSS Compiler Utility Class - * - * + * + * * Dynamic CSS Syntax * ------------------ *
- * body {color: $body_color;} 
+ * body {color: $body_color;}
  * 
- * In the above example, the variable $body_color is replaced by a value - * retrieved by the value callback function. The function is passed the variable - * name without the dollar sign, which can be used with get_option() or + * In the above example, the variable $body_color is replaced by a value + * retrieved by the value callback function. The function is passed the variable + * name without the dollar sign, which can be used with get_option() or * get_theme_mod() etc. */ -class DynamicCSSCompiler -{ - /** - * @var DynamicCSSCompiler The reference to *Singleton* instance of this class - */ - private static $instance; - - /** - * @var array The list of dynamic styles paths to compile - */ - private $stylesheets = array(); - - /** - * @var array The list of registered callbacks - */ - private $callbacks = array(); - - /** - * @var aray The list of registered filters - */ - private $filters = array(); - - /** - * Returns the *Singleton* instance of this class. - * - * @return DynamicCSSCompiler The *Singleton* instance. - */ - public static function get_instance() - { - if (null === static::$instance) - { - static::$instance = new static(); - } - return static::$instance; - } - - /** - * Enqueue all registered stylesheets. - */ - public function enqueue_styles() - { - foreach( $this->stylesheets as $stylesheet ) - { - if( $this->callback_exists( $stylesheet['handle'] ) ) - { - $this->enqueue_style( $stylesheet ); - } - } - } - - /** - * Enqueue a single registered stylesheet. - * - * @param array $stylesheet - */ - public function enqueue_style( $stylesheet ) - { - $handle = 'wp-dynamic-css-'.$stylesheet['handle']; - $print = $stylesheet['print']; - - wp_register_style( - $handle, - // Don't pass a URL if this style is to be printed - $print ? false : $this->get_ajax_callback_url( $stylesheet['handle'] ) - ); - - wp_enqueue_style( $handle ); - - // Add inline styles for styles that are set to be printed - if( $print ) - { - // Inline styles only work if the handle has already been registered and enqueued - wp_add_inline_style( $handle, $this->get_compiled_style( $stylesheet ) ); - } - } - - /** - * This is the AJAX callback used for loading styles externally via an http - * request. - */ - public function ajax_callback() - { - header( "Content-type: text/css; charset: UTF-8" ); - $handle = filter_input( INPUT_GET, 'handle' ); - - foreach( $this->stylesheets as $stylesheet ) - { - if( $handle === $stylesheet['handle'] ) - { - echo $this->get_compiled_style( $stylesheet ); - } - } - - wp_die(); - } - - /** - * Add a style path to the pool of styles to be compiled - * - * @param string $handle The stylesheet's name/id - * @param string $path The absolute path to the dynamic style - * @param boolean $print Whether to print the compiled CSS to the document - * head, or include it as an external CSS file - * @param boolean $minify Whether to minify the CSS output - * @param boolean $cache Whether to store the compiled version of this - * stylesheet in cache to avoid compilation on every page load. - */ - public function register_style( $handle, $path, $print, $minify, $cache ) - { - $this->stylesheets[] = array( - 'handle'=> $handle, - 'path' => $path, - 'print' => $print, - 'minify'=> $minify, - 'cache' => $cache - ); - } - - /** - * Register a value retrieval function and associate it with the given handle - * - * @param string $handle The stylesheet's name/id - * @param callable $callback - */ - public function register_callback( $handle, $callback ) - { - $this->callbacks[$handle] = $callback; - } - - /** - * Register a filter function for a given stylesheet handle. - */ - public function register_filter( $handle, $filter_name, $callback ) - { - if( !array_key_exists( $handle, $this->filters ) ) - { - $this->filters[$handle] = array(); - } - $this->filters[$handle][$filter_name] = $callback; - } - - /** - * Get the compiled CSS for the given style. Skips compilation if the compiled - * version can be found in cache. - * - * @param array $style List of styles with the same structure as they are - * stored in $this->stylesheets - * @return string The compiled CSS for this stylesheet - */ - protected function get_compiled_style( $style ) - { - $cache = DynamicCSSCache::get_instance(); - - // Use cached compiled CSS if applicable - if( $style['cache'] ) - { - $cached_css = $cache->get( $style['handle'] ); - if( false !== $cached_css ) - { - return $cached_css; - } - } - - $css = file_get_contents( $style['path'] ); - if( $style['minify'] ) $css = $this->minify_css( $css ); - - // Compile the dynamic CSS - $compiled_css = $this->compile_css( - $css, - $this->callbacks[$style['handle']], - (array) @$this->filters[$style['handle']] - ); - - $cache->update( $style['handle'], $compiled_css ); - return $this->add_meta_info( $compiled_css ); - } - - /** - * Add meta information to the compiled CSS - * - * @param string $compiled_css The compiled CSS - * @return string The compiled CSS with the meta information added to it - */ - protected function add_meta_info( $compiled_css ) - { - return "/**\n". - " * Compiled using wp-dynamic-css\n". - " * https://github.com/askupasoftware/wp-dynamic-css\n". - " */\n\n". - $compiled_css; - } - - /** - * Get the callback URL for enqueuing the stylesheet extrnally - * - * @param string $handle The stylesheet's handle - * @return string The URL for the given handle - */ - protected function get_ajax_callback_url( $handle ) - { - return esc_url_raw( - add_query_arg(array( - 'action' => 'wp_dynamic_css', - 'handle' => $handle - ), - admin_url( 'admin-ajax.php')) - ); - } - - /** - * Minify a given CSS string by removing comments, whitespaces and newlines - * - * @see http://stackoverflow.com/a/6630103/1096470 - * @param string $css CSS style to minify - * @return string Minified CSS - */ - protected function minify_css( $css ) - { - return preg_replace( '@({)\s+|(\;)\s+|/\*.+?\*\/|\R@is', '$1$2 ', $css ); - } - - /** - * Check if a callback function has been register for the given handle. - * - * @param string $handle - * @return boolean - */ - protected function callback_exists( $handle ) - { - if( array_key_exists( $handle, $this->callbacks ) ) - { - return true; - } - trigger_error( - "There is no callback function associated with the handle '$handle'. ". - "Use wp_dynamic_css_set_callback() to register a callback function for this handle." - ); - return false; - } - - /** - * Parse the given CSS string by converting the variables to their - * corresponding values retrieved by applying the callback function - * - * @param callable $callback A function that replaces the variables with - * their values. The function accepts the variable's name as a parameter - * @param string $css A string containing dynamic CSS (pre-compiled CSS with - * variables) - * @return string The compiled CSS after converting the variables to their - * corresponding values - */ - protected function compile_css( $css, $callback, $filters ) - { - return preg_replace_callback( - - "#". // Begin - "\\$". // Must start with $ - "([\\w-]+)". // Match alphanumeric characters and dashes - "((?:\\['?[\\w-]+'?\\])*)". // Optionally match array subscripts i.e. $myVar['index'] - "((?:". // Optionally match pipe filters i.e. $myVar|myFilter - "\\|[\\w-]+". // Starting with the | character - "(\([\w\.,']+\))?". // Filters can have strings and numbers i.e myFilter('string',1,2.5) - ")*)". // Allow for 0 or more piped filters - "#", // End - - function( $matches ) use ( $callback, $filters ) - { - $subscripts = array(); - - // If this variable is an array, get the subscripts - if( '' !== $matches[2] ) - { - preg_match_all('/[\w-]+/i', $matches[2], $subscripts); - } - - $val = call_user_func_array( $callback, array( $matches[1],@$subscripts[0] ) ); - - // If there are filters, apply them - if( '' !== $matches[3] ) - { - $val = $this->apply_filters( substr( $matches[3], 1 ), $val, $filters ); - } - - return $val; - }, $css - ); - } - - /** - * Apply the filters specified in $filters_string to the given $value. - * - * @param string $filters_string - * @param string $value - * @param array $filters Array of callback functions - * @return string The value after all filters have been applied - */ - protected function apply_filters( $filters_string, $value, $filters = array() ) - { - foreach( explode( '|', $filters_string) as $filter ) - { - $args = array( $value ); - - if( false !== strrpos( $filters_string, "(" ) ) - { - $pieces = explode( '(', $filter ); - $filter = $pieces[0]; - $params = explode( ',', str_replace( ')', '', $pieces[1] ) ); - array_walk( $params, array( $this, 'strtoval' ) ); // Convert string values to actual values - $args = array_merge( $args, $params ); - } - - if( key_exists( $filter, $filters ) ) - { - $value = call_user_func_array( $filters[$filter], $args ); - } - } - return $value; - } - - /** - * Convert the given string to its actual value. - * - * @param string $str The string to be converted (passed by reference) - */ - protected function strtoval( &$str ) - { - if( 'false' === strtolower($str) ) $str = false; - if( 'true' === strtolower($str) ) $str = true; - if( false !== strrpos( $str, "'" ) ) $str = str_replace ( "'", "", $str ); - if( is_numeric( $str ) ) $str = floatval( $str ); - } +class DynamicCSSCompiler { + + /** + * @var DynamicCSSCompiler The reference to *Singleton* instance of this class + */ + private static $instance; + + /** + * @var array The list of dynamic styles paths to compile + */ + private $stylesheets = []; + + /** + * @var array The list of registered callbacks + */ + private $callbacks = []; + + /** + * @var aray The list of registered filters + */ + private $filters = []; + + /** + * Returns the *Singleton* instance of this class. + * + * @return DynamicCSSCompiler The *Singleton* instance. + */ + public static function get_instance() { + + if ( null === static::$instance ) { + static::$instance = new static(); + } + return static::$instance; + } + + /** + * Enqueue all registered stylesheets. + */ + public function enqueue_styles() { + + foreach ( $this->stylesheets as $stylesheet ) { + if ( $this->callback_exists( $stylesheet['handle'] ) ) { + $this->enqueue_style( $stylesheet ); + } + } + } + + /** + * Enqueue a single registered stylesheet. + * + * @param array $stylesheet + */ + public function enqueue_style( $stylesheet ) { + + $handle = 'wp-dynamic-css-' . $stylesheet['handle']; + $print = $stylesheet['print']; + + wp_register_style( + $handle, + // Don't pass a URL if this style is to be printed + $print ? false : $this->get_ajax_callback_url( $stylesheet['handle'] ), + $stylesheet['deps'] + ); + + wp_enqueue_style( $handle, $stylesheet['handle'], $stylesheet['deps'] ); + + // Add inline styles for styles that are set to be printed + if ( $print ) { + // Inline styles only work if the handle has already been registered and enqueued + wp_add_inline_style( $handle, $this->get_compiled_style( $stylesheet ), $stylesheet['deps'] ); + } + } + + /** + * This is the AJAX callback used for loading styles externally via an http + * request. + */ + public function ajax_callback() { + + header( 'Content-type: text/css; charset: UTF-8' ); + $handle = filter_input( INPUT_GET, 'handle' ); + + foreach ( $this->stylesheets as $stylesheet ) { + if ( $handle === $stylesheet['handle'] ) { + echo $this->get_compiled_style( $stylesheet ); + } + } + + wp_die(); + } + + /** + * Add a style path to the pool of styles to be compiled + * + * @param string $handle The stylesheet's name/id + * @param string $path The absolute path to the dynamic style + * @param boolean $print Whether to print the compiled CSS to the document + * head, or include it as an external CSS file + * @param boolean $minify Whether to minify the CSS output + * @param boolean $cache Whether to store the compiled version of this + * stylesheet in cache to avoid compilation on every page load. + */ + public function register_style( $handle, $path, $deps, $print, $minify, $cache, $expiration ) { + + $this->stylesheets[] = [ + 'handle' => $handle, + 'path' => $path, + 'deps' => $deps, + 'print' => $print, + 'minify' => $minify, + 'cache' => $cache, + 'cache_time' => $expiration, + ]; + } + + /** + * Register a value retrieval function and associate it with the given handle + * + * @param string $handle The stylesheet's name/id + * @param callable $callback + */ + public function register_callback( $handle, $callback ) { + + $this->callbacks[ $handle ] = $callback; + } + + /** + * Register a filter function for a given stylesheet handle. + */ + public function register_filter( $handle, $filter_name, $callback ) { + + if ( ! array_key_exists( $handle, $this->filters ) ) { + $this->filters[ $handle ] = []; + } + $this->filters[ $handle ][ $filter_name ] = $callback; + } + + /** + * Get the compiled CSS for the given style. Skips compilation if the compiled + * version can be found in cache. + * + * @param array $style List of styles with the same structure as they are + * stored in $this->stylesheets + * @return string The compiled CSS for this stylesheet + */ + protected function get_compiled_style( $style ) { + + $cache = DynamicCSSCache::get_instance(); + + // Use cached compiled CSS if applicable + if ( $style['cache'] ) { + $cached_css = $cache->get( $style['handle'] ); + if ( false !== $cached_css ) { + return $cached_css; + } + } + + $css = file_get_contents( $style['path'] ); + if ( $style['minify'] ) { + $css = $this->minify_css( $css ); + } + + // Compile the dynamic CSS + $compiled_css = $this->compile_css( + $css, + $this->callbacks[ $style['handle'] ], + (array) @$this->filters[ $style['handle'] ] + ); + + $cache->update( $style['handle'], $compiled_css, $style['cache_time'] ); + return $this->add_meta_info( $compiled_css ); + } + + /** + * Add meta information to the compiled CSS + * + * @param string $compiled_css The compiled CSS + * @return string The compiled CSS with the meta information added to it + */ + protected function add_meta_info( $compiled_css ) { + + return "/**\n" . + " * Compiled using wp-dynamic-css\n" . + " * https://github.com/askupasoftware/wp-dynamic-css\n" . + " */\n\n" . + $compiled_css; + } + + /** + * Get the callback URL for enqueuing the stylesheet extrnally + * + * @param string $handle The stylesheet's handle + * @return string The URL for the given handle + */ + protected function get_ajax_callback_url( $handle ) { + + return esc_url_raw( + add_query_arg( [ + 'action' => 'wp_dynamic_css', + 'handle' => $handle, + ], admin_url( 'admin-ajax.php' ) ) + ); + } + + /** + * Minify a given CSS string by removing comments, whitespaces and newlines + * + * @see http://stackoverflow.com/a/6630103/1096470 + * @param string $css CSS style to minify + * @return string Minified CSS + */ + protected function minify_css( $css ) { + + return preg_replace( '@({)\s+|(\;)\s+|/\*.+?\*\/|\R@is', '$1$2 ', $css ); + } + + /** + * Check if a callback function has been register for the given handle. + * + * @param string $handle + * @return boolean + */ + protected function callback_exists( $handle ) { + + if ( array_key_exists( $handle, $this->callbacks ) ) { + return true; + } + trigger_error( + "There is no callback function associated with the handle '$handle'. " . + 'Use wp_dynamic_css_set_callback() to register a callback function for this handle.' + ); + return false; + } + + /** + * Parse the given CSS string by converting the variables to their + * corresponding values retrieved by applying the callback function + * + * @param callable $callback A function that replaces the variables with + * their values. The function accepts the variable's name as a parameter + * @param string $css A string containing dynamic CSS (pre-compiled CSS with + * variables) + * @return string The compiled CSS after converting the variables to their + * corresponding values + */ + protected function compile_css( $css, $callback, $filters ) { + + return preg_replace_callback( + + '#' . // Begin + '\\$' . // Must start with $ + '([\\w-]+)' . // Match alphanumeric characters and dashes + "((?:\\['?[\\w-]+'?\\])*)" . // Optionally match array subscripts i.e. $myVar['index'] + '((?:' . // Optionally match pipe filters i.e. $myVar|myFilter + '\\|[\\w-]+' . // Starting with the | character + "(\([\w\.,']+\))?" . // Filters can have strings and numbers i.e myFilter('string',1,2.5) + ')*)' . // Allow for 0 or more piped filters + '#', // End + function( $matches ) use ( $callback, $filters ) { + $subscripts = []; + + // If this variable is an array, get the subscripts + if ( '' !== $matches[2] ) { + preg_match_all( '/[\w-]+/i', $matches[2], $subscripts ); + } + + $val = call_user_func_array( $callback, [ $matches[1], @$subscripts[0] ] ); + + // If there are filters, apply them + if ( '' !== $matches[3] ) { + $val = $this->apply_filters( substr( $matches[3], 1 ), $val, $filters ); + } + + return $val; + }, $css + ); + } + + /** + * Apply the filters specified in $filters_string to the given $value. + * + * @param string $filters_string + * @param string $value + * @param array $filters Array of callback functions + * @return string The value after all filters have been applied + */ + protected function apply_filters( $filters_string, $value, $filters = [] ) { + + foreach ( explode( '|', $filters_string ) as $filter ) { + $args = [ $value ]; + + if ( false !== strrpos( $filters_string, '(' ) ) { + $pieces = explode( '(', $filter ); + $filter = $pieces[0]; + $params = explode( ',', str_replace( ')', '', $pieces[1] ) ); + array_walk( $params, [ $this, 'strtoval' ] ); // Convert string values to actual values + $args = array_merge( $args, $params ); + } + + if ( key_exists( $filter, $filters ) ) { + $value = call_user_func_array( $filters[ $filter ], $args ); + } + } + return $value; + } + + /** + * Convert the given string to its actual value. + * + * @param string $str The string to be converted (passed by reference) + */ + protected function strtoval( &$str ) { + + if ( 'false' === strtolower( $str ) ) { + $str = false; + } + if ( 'true' === strtolower( $str ) ) { + $str = true; + } + if ( false !== strrpos( $str, "'" ) ) { + $str = str_replace( "'", '', $str ); + } + if ( is_numeric( $str ) ) { + $str = floatval( $str ); + } + } } diff --git a/functions.php b/functions.php index e08b2b5..43961f8 100644 --- a/functions.php +++ b/functions.php @@ -7,111 +7,109 @@ * @copyright 2016 Askupa Software */ -if( !function_exists('wp_dynamic_css_enqueue') ) -{ - /** - * Enqueue a dynamic stylesheet - * - * This will either print the compiled version of the stylesheet to the - * document's section, or load it as an external stylesheet if $print - * is set to false - * - * @param string $handle The stylesheet's name/id - * @param string $path The absolute path to the dynamic CSS file - * @paran boolean $print Whether to print the compiled CSS to the document - * head, or include it as an external CSS file - * @param boolean $minify Whether to minify the CSS output - * @param boolean $cache Whether to store the compiled version of this - * stylesheet in cache to avoid compilation on every page load. - */ - function wp_dynamic_css_enqueue( $handle, $path, $print = true, $minify = false, $cache = false ) - { - $dcss = DynamicCSSCompiler::get_instance(); - $dcss->register_style( $handle, $path, $print, $minify, $cache ); - } +if ( ! function_exists( 'wp_dynamic_css_enqueue' ) ) { + /** + * Enqueue a dynamic stylesheet + * + * This will either print the compiled version of the stylesheet to the + * document's section, or load it as an external stylesheet if $print + * is set to false + * + * @param string $handle The stylesheet's name/id + * @param string $path The absolute path to the dynamic CSS file + * @param array $deps An array of registered stylesheet handles this stylesheet depends on. + * @param boolean $print Whether to print the compiled CSS to the document + * head, or include it as an external CSS file + * @param boolean $minify Whether to minify the CSS output + * @param boolean $cache Whether to store the compiled version of this + * stylesheet in cache to avoid compilation on every page load. + * @param int $expiration Time until expiration in seconds. + */ + function wp_dynamic_css_enqueue( $handle, $path, $deps = [], $print = true, $minify = false, $cache = false, $expiration = 0 ) { + + $dcss = DynamicCSSCompiler::get_instance(); + $dcss->register_style( $handle, $path, $deps, $print, $minify, $cache, $expiration ); + } } -if( !function_exists('wp_dynamic_css_set_callback') ) -{ - /** - * Set the value retrieval callback function - * - * Set a callback function that will be used to get the values of the - * variables when the dynamic CSS file is compiled. The function accepts 1 - * parameter which is the name of the variable, without the $ sign - * - * @param string $handle The name of the stylesheet to be associated with this - * callback function - * @param string|array $callback A callback (or "callable" as of PHP 5.4) - * can either be a reference to a function name or method within an - * class/object. - */ - function wp_dynamic_css_set_callback( $handle, $callback ) - { - $dcss = DynamicCSSCompiler::get_instance(); - $dcss->register_callback( $handle, $callback ); - } +if ( ! function_exists( 'wp_dynamic_css_set_callback' ) ) { + /** + * Set the value retrieval callback function + * + * Set a callback function that will be used to get the values of the + * variables when the dynamic CSS file is compiled. The function accepts 1 + * parameter which is the name of the variable, without the $ sign + * + * @param string $handle The name of the stylesheet to be associated with this + * callback function + * @param string|array $callback A callback (or "callable" as of PHP 5.4) + * can either be a reference to a function name or method within an + * class/object. + */ + function wp_dynamic_css_set_callback( $handle, $callback ) { + + $dcss = DynamicCSSCompiler::get_instance(); + $dcss->register_callback( $handle, $callback ); + } } -if( !function_exists('wp_dynamic_css_clear_cache') ) -{ - /** - * Clear the cached compiled CSS for the given handle. - * - * Registered dynamic stylesheets that have the $cache flag set to true are - * compiled only once and then stored in cache. Subsequesnt requests are - * served statically from cache until wp_dynamic_css_clear_cache() is called - * and clears it, forcing the compiler to recompile the CSS. - * - * @param string $handle The name of the stylesheet to be cleared from cache - */ - function wp_dynamic_css_clear_cache( $handle ) - { - $cache = DynamicCSSCache::get_instance(); - $cache->clear( $handle ); - } +if ( ! function_exists( 'wp_dynamic_css_clear_cache' ) ) { + /** + * Clear the cached compiled CSS for the given handle. + * + * Registered dynamic stylesheets that have the $cache flag set to true are + * compiled only once and then stored in cache. Subsequesnt requests are + * served statically from cache until wp_dynamic_css_clear_cache() is called + * and clears it, forcing the compiler to recompile the CSS. + * + * @param string $handle The name of the stylesheet to be cleared from cache + */ + function wp_dynamic_css_clear_cache( $handle ) { + + $cache = DynamicCSSCache::get_instance(); + $cache->clear( $handle ); + } } -if( !function_exists('wp_dynamic_css_register_filter') ) -{ - /** - * Register a filter function for a given stylesheet handle. - * - * For example, a registered filter named 'myFilter' can be used in a dynamic - * CSS file like so: - * - *
-     * body {
-     *     $myVar|myFilter
-     * }
-     * 
- * - * Filters can also accept arguments: - * - *
-     * body {
-     *     $myVar|myFilter('1',2,3.4)
-     * }
-     * 
- * - * And can be stacked together: - * - *
-     * body {
-     *     $myVar|myFilter1|filter2|filter3
-     * }
-     * 
- * - * @param string $handle The handle of the stylesheet in which this filter - * is to be used. - * @param string $filter_name The name of the filter to be used in the - * dynamic CSS file. - * @param Callable $callback The actual filter function. Accepts the $value - * as an argument. Should return the filtered value. - */ - function wp_dynamic_css_register_filter( $handle, $filter_name, $callback ) - { - $dcss = DynamicCSSCompiler::get_instance(); - $dcss->register_filter( $handle, $filter_name, $callback ); - } +if ( ! function_exists( 'wp_dynamic_css_register_filter' ) ) { + /** + * Register a filter function for a given stylesheet handle. + * + * For example, a registered filter named 'myFilter' can be used in a dynamic + * CSS file like so: + * + *
+	 * body {
+	 *     $myVar|myFilter
+	 * }
+	 * 
+ * + * Filters can also accept arguments: + * + *
+	 * body {
+	 *     $myVar|myFilter('1',2,3.4)
+	 * }
+	 * 
+ * + * And can be stacked together: + * + *
+	 * body {
+	 *     $myVar|myFilter1|filter2|filter3
+	 * }
+	 * 
+ * + * @param string $handle The handle of the stylesheet in which this filter + * is to be used. + * @param string $filter_name The name of the filter to be used in the + * dynamic CSS file. + * @param Callable $callback The actual filter function. Accepts the $value + * as an argument. Should return the filtered value. + */ + function wp_dynamic_css_register_filter( $handle, $filter_name, $callback ) { + + $dcss = DynamicCSSCompiler::get_instance(); + $dcss->register_filter( $handle, $filter_name, $callback ); + } } diff --git a/readme.txt b/readme.txt index 63c11de..919dd55 100644 --- a/readme.txt +++ b/readme.txt @@ -2,8 +2,8 @@ Contributors: Askupa Software, ykadosh Tags: dynamic css, css, customizer, get_theme_mod, css variables, css compiler Requires at least: 3.0 -Tested up to: 4.7 -Stable tag: 1.0.5 +Tested up to: 4.8 +Stable tag: 1.0.6 License: GPLv3 or later License URI: http://www.gnu.org/licenses/gpl-3.0.html @@ -11,8 +11,8 @@ Dynamic CSS compiler for WordPress themes and plugins == Description == -**WordPress Dynamic CSS** is a lightweight library for generating CSS stylesheets from dynamic content (i.e. content that can be modified by the user). -The most obvious use case for this library is for creating stylesheets based on Customizer options. +**WordPress Dynamic CSS** is a lightweight library for generating CSS stylesheets from dynamic content (i.e. content that can be modified by the user). +The most obvious use case for this library is for creating stylesheets based on Customizer options. Using the special dynamic CSS syntax you can write CSS rules with variables that will be replaced by static values using a custom callback function that you provide. **As of version 1.0.2** this plugin supports multiple callback functions, thus making it safe to use by multiple plugins/themes at the same time. @@ -78,7 +78,10 @@ You can find detailed documentation on how to use this library on the [GitHub pa Please follow the instructions on the plugin's [GitHub page](https://github.com/askupasoftware/wp-dynamic-css) for detailed explanation and examples. == Changelog == - += 1.0.6 = +* Use transient instead of option as cache. Transients are inherently sped up by caching plugins, where normal Options are not. +* Add `$deps` to `wp_dynamic_css_enqueue`, $deps is an array of registered stylesheet handles this stylesheet depends on. +* Reformat code according to [WPCS](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards) = 1.0.5 = * (NEW) Added support for cache * (NEW) Added support for piped filters @@ -103,4 +106,3 @@ Please follow the instructions on the plugin's [GitHub page](https://github.com/ * Initial release == Upgrade Notice == - diff --git a/tests.php b/tests.php index c3d77e4..6d38d84 100644 --- a/tests.php +++ b/tests.php @@ -5,138 +5,228 @@ * To run the tests, use the following command: * $ phpunit --bootstrap compiler.php tests.php */ -class CompilerTest extends TestCase -{ - /** - * DynamicCSSCompiler::strtoval test - */ - public function testStrToVal() - { - // Make protected functions accessible through a reflection class - $class = new ReflectionClass('DynamicCSSCompiler'); - $method = $class->getMethod('strtoval'); - $method->setAccessible(true); - $dcss = DynamicCSSCompiler::get_instance(); - - // Assert - $str = "'value'"; - $method->invokeArgs($dcss, array(&$str)); - $this->assertEquals($str, 'value'); - - $str = "false"; - $method->invokeArgs($dcss, array(&$str)); - $this->assertEquals($str, false); - - $str = "true"; - $method->invokeArgs($dcss, array(&$str)); - $this->assertEquals($str, true); - - $str = "4"; - $method->invokeArgs($dcss, array(&$str)); - $this->assertEquals($str, 4); - - $str = "4.321"; - $method->invokeArgs($dcss, array(&$str)); - $this->assertEquals($str, 4.321); - } - - /** - * DynamicCSSCompiler::apply_filters test - */ - public function testFilterApplication() - { - // Make protected functions accessible through a reflection class - $class = new ReflectionClass('DynamicCSSCompiler'); - $method = $class->getMethod('apply_filters'); - $method->setAccessible(true); - $dcss = DynamicCSSCompiler::get_instance(); - - // Assert - $this->assertEquals($method->invokeArgs($dcss, array('simple_filter', 'foo', array('simple_filter' => 'simple_filter_callback'))), 'foobar'); - $this->assertEquals($method->invokeArgs($dcss, array('simple_filter|simple_filter', 'foo', array('simple_filter' => 'simple_filter_callback'))), 'foobarbar'); - $this->assertEquals($method->invokeArgs($dcss, array('complex_filter(\'bar\')', 'foo', array('complex_filter' => 'complex_filter_callback'))), 'foobar'); - $this->assertEquals($method->invokeArgs($dcss, array('complex_filter(\'bar\',\'foo\')', 'foo', array('complex_filter' => 'complex_filter_callback'))), 'foobarfoo'); - $this->assertEquals($method->invokeArgs($dcss, array('complex_filter(\'bar\')|complex_filter(\'foo\')', 'foo', array('complex_filter' => 'complex_filter_callback'))), 'foobarfoo'); - $this->assertEquals($method->invokeArgs($dcss, array('add_filter(5,5)', '5 + 5 = ', array('add_filter' => 'add_filter_callback'))), '5 + 5 = 10'); - $this->assertEquals($method->invokeArgs($dcss, array('add_filter(5.5,5.5)', '5.5 + 5.5 = ', array('add_filter' => 'add_filter_callback'))), '5.5 + 5.5 = 11'); - } - - /** - * DynamicCSSCompiler::compile_css test - */ - public function testCompilation() - { - // Make protected functions accessible through a reflection class - $class = new ReflectionClass('DynamicCSSCompiler'); - $method = $class->getMethod('compile_css'); - $method->setAccessible(true); - $dcss = DynamicCSSCompiler::get_instance(); - - // Assert - $this->assertEquals($method->invokeArgs($dcss, array('$var1', 'callback', array())), 'value1'); - $this->assertEquals($method->invokeArgs($dcss, array('$var2', 'callback', array())), 'value2'); - $this->assertEquals($method->invokeArgs($dcss, array('$var3[\'index1\']', 'callback', array())), 'value3'); - $this->assertEquals($method->invokeArgs($dcss, array('$var3[\'index2\'][\'subindex1\']', 'callback', array())), 'value4'); - $this->assertEquals($method->invokeArgs($dcss, array('$var3[index2][subindex1]', 'callback', array())), 'value4'); - $this->assertEquals($method->invokeArgs($dcss, array('$var3[index2][subindex1]', 'callback', array())), 'value4'); - $this->assertEquals($method->invokeArgs($dcss, array('$var4|simpleFilter', 'callback', array('simpleFilter' => 'simple_filter_callback'))), 'valuebar'); - $this->assertEquals($method->invokeArgs($dcss, array('$var4|simpleFilter|simpleFilter', 'callback', array('simpleFilter' => 'simple_filter_callback'))), 'valuebarbar'); - $this->assertEquals($method->invokeArgs($dcss, array('$var4|complexFilter(6)', 'callback', array('complexFilter' => 'complex_filter_callback'))), 'value6'); - $this->assertEquals($method->invokeArgs($dcss, array('$var5|complexFilter(e)|complexFilter(7)', 'callback', array('complexFilter' => 'complex_filter_callback'))), 'value7'); - $this->assertEquals($method->invokeArgs($dcss, array('$var3[index1]|simpleFilter', 'callback', array('simpleFilter' => 'simple_filter_callback'))), 'value3bar'); - } +class CompilerTest extends TestCase { + + /** + * DynamicCSSCompiler::strtoval test + */ + public function testStrToVal() { + + // Make protected functions accessible through a reflection class + $class = new ReflectionClass( 'DynamicCSSCompiler' ); + $method = $class->getMethod( 'strtoval' ); + $method->setAccessible( true ); + $dcss = DynamicCSSCompiler::get_instance(); + + // Assert + $str = "'value'"; + $method->invokeArgs( $dcss, [ &$str ] ); + $this->assertEquals( $str, 'value' ); + + $str = 'false'; + $method->invokeArgs( $dcss, [ &$str ] ); + $this->assertEquals( $str, false ); + + $str = 'true'; + $method->invokeArgs( $dcss, [ &$str ] ); + $this->assertEquals( $str, true ); + + $str = '4'; + $method->invokeArgs( $dcss, [ &$str ] ); + $this->assertEquals( $str, 4 ); + + $str = '4.321'; + $method->invokeArgs( $dcss, [ &$str ] ); + $this->assertEquals( $str, 4.321 ); + } + + /** + * DynamicCSSCompiler::apply_filters test + */ + public function testFilterApplication() { + + // Make protected functions accessible through a reflection class + $class = new ReflectionClass( 'DynamicCSSCompiler' ); + $method = $class->getMethod( 'apply_filters' ); + $method->setAccessible( true ); + $dcss = DynamicCSSCompiler::get_instance(); + + // Assert + $this->assertEquals( $method->invokeArgs( $dcss, [ + 'simple_filter', + 'foo', + [ + 'simple_filter' => 'simple_filter_callback', + ], + ] ), 'foobar' ); + $this->assertEquals( $method->invokeArgs( $dcss, [ + 'simple_filter|simple_filter', + 'foo', + [ + 'simple_filter' => 'simple_filter_callback', + ], + ] ), 'foobarbar' ); + $this->assertEquals( $method->invokeArgs( $dcss, [ + 'complex_filter(\'bar\')', + 'foo', + [ + 'complex_filter' => 'complex_filter_callback', + ], + ] ), 'foobar' ); + $this->assertEquals( $method->invokeArgs( $dcss, [ + 'complex_filter(\'bar\',\'foo\')', + 'foo', + [ + 'complex_filter' => 'complex_filter_callback', + ], + ] ), 'foobarfoo' ); + $this->assertEquals( $method->invokeArgs( $dcss, [ + 'complex_filter(\'bar\')|complex_filter(\'foo\')', + 'foo', + [ + 'complex_filter' => 'complex_filter_callback', + ], + ] ), 'foobarfoo' ); + $this->assertEquals( $method->invokeArgs( $dcss, [ + 'add_filter(5,5)', + '5 + 5 = ', + [ + 'add_filter' => 'add_filter_callback', + ], + ] ), '5 + 5 = 10' ); + $this->assertEquals( $method->invokeArgs( $dcss, [ + 'add_filter(5.5,5.5)', + '5.5 + 5.5 = ', + [ + 'add_filter' => 'add_filter_callback', + ], + ] ), '5.5 + 5.5 = 11' ); + } + + /** + * DynamicCSSCompiler::compile_css test + */ + public function testCompilation() { + + // Make protected functions accessible through a reflection class + $class = new ReflectionClass( 'DynamicCSSCompiler' ); + $method = $class->getMethod( 'compile_css' ); + $method->setAccessible( true ); + $dcss = DynamicCSSCompiler::get_instance(); + + // Assert + $this->assertEquals( $method->invokeArgs( $dcss, [ + '$var1', + 'callback', + [], + ] ), 'value1' ); + $this->assertEquals( $method->invokeArgs( $dcss, [ + '$var2', + 'callback', + [], + ] ), 'value2' ); + $this->assertEquals( $method->invokeArgs( $dcss, [ + '$var3[\'index1\']', + 'callback', + [], + ] ), 'value3' ); + $this->assertEquals( $method->invokeArgs( $dcss, [ + '$var3[\'index2\'][\'subindex1\']', + 'callback', + [], + ] ), 'value4' ); + $this->assertEquals( $method->invokeArgs( $dcss, [ + '$var3[index2][subindex1]', + 'callback', + [], + ] ), 'value4' ); + $this->assertEquals( $method->invokeArgs( $dcss, [ + '$var3[index2][subindex1]', + 'callback', + [], + ] ), 'value4' ); + $this->assertEquals( $method->invokeArgs( $dcss, [ + '$var4|simpleFilter', + 'callback', + [ + 'simpleFilter' => 'simple_filter_callback', + ], + ] ), 'valuebar' ); + $this->assertEquals( $method->invokeArgs( $dcss, [ + '$var4|simpleFilter|simpleFilter', + 'callback', + [ + 'simpleFilter' => 'simple_filter_callback', + ], + ] ), 'valuebarbar' ); + $this->assertEquals( $method->invokeArgs( $dcss, [ + '$var4|complexFilter(6)', + 'callback', + [ + 'complexFilter' => 'complex_filter_callback', + ], + ] ), 'value6' ); + $this->assertEquals( $method->invokeArgs( $dcss, [ + '$var5|complexFilter(e)|complexFilter(7)', + 'callback', + [ + 'complexFilter' => 'complex_filter_callback', + ], + ] ), 'value7' ); + $this->assertEquals( $method->invokeArgs( $dcss, [ + '$var3[index1]|simpleFilter', + 'callback', + [ + 'simpleFilter' => 'simple_filter_callback', + ], + ] ), 'value3bar' ); + } } /** * Value retrieval function */ -function callback( $varname, $subscripts ) -{ - $values = array( - 'var1' => 'value1', - 'var2' => 'value2', - 'var3' => array( - 'index1' => 'value3', - 'index2' => array( - 'subindex1' => 'value4' - ) - ), - 'var4' => 'value', - 'var5' => 'valu' - ); - - $val = $values[$varname]; - if( null !== $subscripts ) - { - foreach( $subscripts as $subscript ) - { - $val = $val[$subscript]; - } - } - - return $val; +function callback( $varname, $subscripts ) { + $values = [ + 'var1' => 'value1', + 'var2' => 'value2', + 'var3' => [ + 'index1' => 'value3', + 'index2' => [ + 'subindex1' => 'value4', + ], + ], + 'var4' => 'value', + 'var5' => 'valu', + ]; + + $val = $values[ $varname ]; + if ( null !== $subscripts ) { + foreach ( $subscripts as $subscript ) { + $val = $val[ $subscript ]; + } + } + + return $val; } /** * Simple string concat filter */ -function simple_filter_callback( $foo ) -{ - return $foo.'bar'; +function simple_filter_callback( $foo ) { + return $foo . 'bar'; } /** * String concat filter with parameters */ -function complex_filter_callback( $value, $arg1 = '', $arg2 = '') -{ - return $value.$arg1.$arg2; +function complex_filter_callback( $value, $arg1 = '', $arg2 = '' ) { + return $value . $arg1 . $arg2; } /** * Number adding filter */ -function add_filter_callback( $value, $a, $b ) -{ - return $value.($a+$b); -} \ No newline at end of file +function add_filter_callback( $value, $a, $b ) { + return $value . ($a + $b); +}