├── renovate.json ├── composer.json ├── src ├── DDT_Registry.php └── Dynamic_Data_Tag.php └── README.md /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "juvo/bricks-dynamic-data-tags", 3 | "description": "Easy to use dynamic data tags for Bricks", 4 | "keywords" : [ "wordpress", "bricksbuilder", "dynamic", "data", "tags" ], 5 | "license": "GPL-3.0-or-later", 6 | "require": { 7 | "php": ">=7.4.0" 8 | }, 9 | "support" : { 10 | "issues" : "https://github.com/JUVOJustin/bricks-dynamic-data-tags/issues" 11 | }, 12 | "authors": [ 13 | { 14 | "name": "Justin Vogt", 15 | "email": "mail@justin-vogt.de", 16 | "homepage": "https://justin-vogt.com" 17 | } 18 | ], 19 | "autoload": { 20 | "psr-4": { 21 | "juvo\\Bricks_Dynamic_Data_Tags\\": "src/" 22 | } 23 | }, 24 | "require-dev": { 25 | "phpstan/phpstan": "^1.10.6", 26 | "szepeviktor/phpstan-wordpress": "^v1.1.7", 27 | "phpstan/extension-installer": "^1.1" 28 | }, 29 | "config": { 30 | "allow-plugins": { 31 | "phpstan/extension-installer": true 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/DDT_Registry.php: -------------------------------------------------------------------------------- 1 | registerDataTags(); 14 | }, 99); 15 | } 16 | 17 | public static function getInstance(): DDT_Registry { 18 | if (self::$instance === null) { 19 | self::$instance = new self(); 20 | } 21 | return self::$instance; 22 | } 23 | /** 24 | * @param string $tag 25 | * @param string $label 26 | * @param string $group 27 | * @param callable $callback Callable type for PHP callbacks. 28 | * @return void 29 | */ 30 | public function set(string $tag, string $label, string $group, callable $callback): void { 31 | $this->storage[$tag] = new Dynamic_Data_Tag($tag, $label, $group, $callback); 32 | } 33 | 34 | /** 35 | * @param string $tag 36 | * @return Dynamic_Data_Tag|null 37 | */ 38 | public function get(string $tag) { 39 | return $this->storage[$tag] ?? null; 40 | } 41 | 42 | public function getAll():array { 43 | return $this->storage; 44 | } 45 | 46 | /** 47 | * Legacy function to register triggers that do not use the factory 48 | * 49 | * @return void 50 | * @Deprecated 51 | */ 52 | public function registerDataTags(): void 53 | { 54 | $tags = apply_filters('juvo/dynamic_tags/register', $this->storage); 55 | foreach ($tags as $tag) { 56 | add_filter('bricks/dynamic_tags_list', [$tag, 'add_tag_to_builder']); 57 | add_filter('bricks/dynamic_data/render_tag', [$tag, 'get_tag_value'], 10, 3); 58 | add_filter('bricks/dynamic_data/render_content', [$tag, 'render_tag'], 10, 3); 59 | add_filter('bricks/frontend/render_data', [$tag, 'render_tag'], 10, 2); 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/Dynamic_Data_Tag.php: -------------------------------------------------------------------------------- 1 | tag = $tag; 25 | $this->label = $label; 26 | $this->group = $group; 27 | $this->callback = $callback; 28 | } 29 | 30 | /** 31 | * Use the bricks/dynamic_tags_list filter to render your custom dynamic data tag in the builder. 32 | * 33 | * @param array $tags 34 | * @return array 35 | */ 36 | public function add_tag_to_builder(array $tags): array 37 | { 38 | $tags[] = [ 39 | 'name' => '{' . $this->tag . '}', 40 | 'label' => $this->label, 41 | 'group' => $this->group, 42 | ]; 43 | return $tags; 44 | } 45 | 46 | /** 47 | * Callback function for the actual tag. Called in get_tag_value() and render_tag(). This is where you should do your logic. 48 | * 49 | * @param $post 50 | * @param string $context 51 | * @param array $variables 52 | * 53 | * @return mixed 54 | * @throws Exception 55 | */ 56 | public function run_tag($post, string $context, array $variables = []): mixed { 57 | if (is_callable($this->callback)) { 58 | // Post is always the first parameter 59 | $args = array_merge([$post, $context], $variables); 60 | return call_user_func_array($this->callback, $args); 61 | } else { 62 | throw new Exception("The provided callback is not callable."); 63 | } 64 | } 65 | 66 | /** 67 | * This will be used when \Bricks\Integrations\Dynamic_Data\Providers::render_tag() is called to parse a specific tag. 68 | * 69 | * @param mixed $tag 70 | * @param mixed $post 71 | * @param string $context 72 | * @return mixed 73 | * @throws Exception 74 | */ 75 | public function get_tag_value( $tag, $post, string $context = 'text') 76 | { 77 | return $this->render_tag($tag, $post, $context); 78 | } 79 | 80 | /** 81 | * These will be used when \Bricks\Integrations\Dynamic_Data\Providers::render_content() is invoked to parse strings that may contain various dynamic tags within the content. One of the functions that perform this action is bricks_render_dynamic_data(). 82 | * 83 | * @param mixed $content 84 | * @param mixed $post 85 | * @param string $context 86 | * @return mixed 87 | * @throws Exception 88 | */ 89 | public function render_tag( $content, $post, string $context = 'text') 90 | { 91 | // Workaround: In some cases content is not a string, but an array. The reason is unclear. 92 | if (!is_string($content)) { 93 | return $content; 94 | } 95 | 96 | // Exit early if the tag is not in the content 97 | if ( ! str_contains( $content, $this->tag ) ) { 98 | return $content; 99 | } 100 | 101 | // Parse tags in content. Be aware that tags can occur multiple times 102 | $matches = $this->parse_tag($content); 103 | 104 | foreach ($matches as $match_groups) { 105 | 106 | $variables = []; 107 | 108 | // Iterate $match_groups and if key is a string make it a variable to be passed to run_tag 109 | foreach ($match_groups as $key => $value) { 110 | if (!is_string($key)) { 111 | continue; 112 | } 113 | 114 | // Split the parameters and filters syntax 115 | switch ($key) { 116 | case 'filters': 117 | $value = explode(':', $value); 118 | break; 119 | default: 120 | $value = apply_filters("juvo/dynamic_data_tag/parse_tag/pattern_$key", $value); 121 | $value = apply_filters("juvo/dynamic_data_tag/parse_tag/$this->tag/pattern_$key", $value); 122 | break; 123 | } 124 | 125 | // Enforce parameters to be an array 126 | if (is_string($value)) { 127 | $value = [ 128 | $value 129 | ]; 130 | } 131 | 132 | $variables[$key] = $value; 133 | } 134 | 135 | // Start with filters to ensure they are first and second 136 | $sortedVariables = [ 137 | 'filters' => $variables['filters'] ?? [], 138 | ]; 139 | 140 | // Remove filters from the original variables array 141 | unset($variables['filters']); 142 | 143 | // Create sorted variables 144 | $variables = array_merge($sortedVariables, $variables); 145 | 146 | // Run the tag with the variables 147 | try { 148 | $value = $this->run_tag( $post, $context, array_values( $variables ) ); 149 | } catch ( Exception $e ) { 150 | $value = "Error rendering Custom DDT '{$this->tag}': " . $e->getMessage(); 151 | error_log( $value ); 152 | 153 | // If user is not an admin return an empty string. Display the error for admins. 154 | if ( ! current_user_can( 'administrator' ) ) { 155 | return ""; 156 | } 157 | } 158 | 159 | // Images need to be returned as array of ideas. If only the id is returned simplify that. 160 | if ($context === 'image') { 161 | if (is_numeric($value)) { 162 | return [$value]; 163 | } 164 | return $value; 165 | } 166 | 167 | // If the value is null, replace it with an empty string 168 | if ($value === null) { 169 | $value = ""; 170 | } 171 | 172 | // Sanitize value if not empty 173 | if (is_string($value)) { 174 | $allowed_tags = wp_kses_allowed_html("juvo/dynamic_data_tag"); 175 | $allowed_tags = apply_filters("juvo/dynamic_data_tag/allowed_html_tags/$this->tag", $allowed_tags); 176 | $value = wp_kses($value, $allowed_tags, []); 177 | } 178 | 179 | // Replace the tag with the transformed value 180 | $content = str_replace($match_groups[0], $value, $content); 181 | } 182 | 183 | return $content; 184 | } 185 | 186 | /** 187 | * Parse a string to get the parameters from the tag. 188 | * Support data separated by ":" and "|". 189 | * 190 | * @param string $content 191 | * @return string[][] 192 | */ 193 | private function parse_tag(string $content): array 194 | { 195 | $pattern = "/{" .$this->tag. "(?::(?[0-9a-zA-Z_\-=]+(?::[0-9a-zA-Z_\-=]+)*))?}/"; 196 | $pattern = apply_filters("juvo/dynamic_data_tag/parse_tag", $pattern, $this->tag); 197 | $pattern = apply_filters("juvo/dynamic_data_tag/parse_tag/$this->tag", $pattern, $this->tag); 198 | 199 | preg_match_all($pattern, $content, $matches, PREG_SET_ORDER); 200 | return $matches; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # God damn easy Bricks Builder Dynamic Data Tags 2 | You are a developer and want to add dynamic data tags to the bricks builder? This package is for you. From now on you can add your dynamic data tags with 3 lines of code. 3 | It automatically registered tags, parses filters and even allows you to add your very own pattern parsing. 4 | 5 | Feature overview: 6 | - Simple registration of dynamic data tags 7 | - Automatic parsing of filters 8 | - Custom pattern parsing 9 | - Output Filtering with wp_kses 10 | 11 | ## Quick Demo using within Child Theme 12 | [](https://youtu.be/pJ4j5iZKKV0) 13 | 14 | ## Installation 15 | To install the package you can use composer. Run the following command in your terminal: 16 | ```bash 17 | composer require juvo/bricks-dynamic-data-tags 18 | ``` 19 | 20 | You should initiate the registry as early as possible. The best place to do this is in your plugin's main file or the functions.php of your theme. 21 | ```php 22 | add_action('init', function() { 23 | juvo\Bricks_Dynamic_Data_Tags\DDT_Registry::getInstance(); 24 | }); 25 | ``` 26 | 27 | ## Usage 28 | To register a simple dynamic data tag you can use the following code snippet. The first parameter is the tag name, the second parameter is the tag label, the third parameter is the tag group and the last parameter is the callback that returns the tag output. 29 | ```php 30 | DDT_Registry::getInstance() 31 | ->set('my_tag', 'My Tag', 'My Tag Group', function($post, $context, array $filters = []) { 32 | return "Hello World"; 33 | }); 34 | ``` 35 | 36 | To register another tag to the same group you simply do: 37 | ```php 38 | DDT_Registry::getInstance() 39 | ->set('my_tag2', 'My Tag 2', 'My Tag Group', function($post, $context, array $filters = []) { 40 | return "Hello World 2"; 41 | }); 42 | ``` 43 | 44 | [![Bricks Builder Dynamic Data Tags List](https://i.postimg.cc/bNP0y8Tr/Capture-2024-02-17-140801.png)](https://postimg.cc/7bBJ9Fjr) 45 | 46 | ## Filter tags that get registered 47 | The `juvo/register_dynamic_tags` filter allows you to modify the tags that get registered. This filter passes the at this point added data tags as array. You can use this to remove tags. 48 | ```php 49 | apply_filters('juvo/dynamic_tags/register', $tags); 50 | ``` 51 | 52 | ## Filter the tag pattern 53 | The `juvo/dynamic_data_tag/parse_tag` filter allows you to modify how all tags are parsed. This filter passes the tag pattern and the tag name as arguments. It allows you to parse a custom structure of for your data tags. To add new variables that are passed to the callback make sure to add them as a [named capturing group](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Named_capturing_group) with the regex. 54 | ```php 55 | // Adds "modifier" as a named capturing group to the tag pattern 56 | add_filter("juvo/dynamic_data_tag/parse_tag", function($pattern, $tag) { 57 | $pattern = str_replace("}/", "", $pattern); 58 | return $pattern . "(\~(?[0-9a-zA-Z_-]+(\~[0-9a-zA-Z_-]+)*))?}/"; 59 | }, 10, 2); 60 | ``` 61 | 62 | Your callback now needs one more parameter: 63 | ``` 64 | DDT_Registry::getInstance()->set( 'single_tag', 'Single Tag', 'Custom Tags', function( $post, $context, array $filters = [], array $modifier = [] ) 65 | ``` 66 | 67 | ### Filter the tag pattern by tag name 68 | The `juvo/dynamic_data_tag/parse_tag` filter allows you to modify the tag pattern for a specific tag. 69 | ```php 70 | apply_filters("juvo/dynamic_data_tag/parse_tag/$tag", $pattern, $this->tag); 71 | ``` 72 | 73 | ### Modify the data passed to the callback 74 | If you used one of the `parse_tag` filters to add your own structure and variables to a tag, you can use this filter to modify the variable itself. This is needed e.g. if you can add your new strucutre multiple times. 75 | By default parameters are split with ":" and filters are split with "|". If you add a new structure that is for example split with "~" you should use this filter split the data accordingly. 76 | ```php 77 | add_filter("juvo/dynamic_data_tag/parse_tag/pattern_modifier", function($value) { 78 | return explode("~", $value); 79 | }); 80 | ``` 81 | 82 | ### Use named filters 83 | It is possible to register dynamic data tags like these: `{single_tag:tag=tag_slug:link=true}`. In your callback you need to parse the filter values to work with key value pairs. This example allows you to display tags, filter which tag to display by slug and filter the tags name should be wrapped in a link to the term itself. 84 | ```php 85 | DDT_Registry::getInstance()->set( 'single_tag', 'Single Tag', 'Custom Tags', function( $post, $context, array $filters = [] ) { 86 | // Parse filters to be key-value pairs 87 | $parsed_filters = []; 88 | foreach ( $filters as &$item ) { 89 | list( $key, $value ) = explode( '=', $item ); 90 | $parsed_filters[ $key ] = $value; 91 | } 92 | $filters = $parsed_filters; 93 | 94 | $tags = get_the_terms( get_the_ID(), 'post_tag' ); 95 | if ( empty( $tags ) || is_wp_error( $tags ) ) { 96 | return ''; 97 | } 98 | 99 | // Filter to select a specific tag 100 | $selected_tag = $filters['tag'] ?? ''; 101 | $output = []; 102 | 103 | foreach ( $tags as $tag ) { 104 | if ((!empty($selected_tag) && $tag->slug === $selected_tag) || empty($selected_tag)) { 105 | 106 | // Check if we need links or not 107 | if ( isset( $filters['link'] ) && $filters['link'] === 'true' ) { 108 | $output[] = '' . esc_html( $tag->name ) . ''; 109 | } else { 110 | $output[] = esc_html( $tag->name ); 111 | } 112 | } 113 | } 114 | 115 | return implode( ', ', $output ); 116 | } ); 117 | ``` 118 | In this example 119 | 120 | ## Modify allowed html tags 121 | The output of the callback is filtered with `wp_kses` to prevent XSS attacks. You can modify the allowed tags with the `wp_kses_allowed_html` filter. This filter passes the allowed tags and the context as arguments. You can use this to modify allowed html tags. 122 | ```php 123 | add_filter("wp_kses_allowed_html", function($allowedtags, $context) { 124 | if ($context !== "juvo/dynamic_data_tag") { 125 | return $allowedtags; 126 | } 127 | $allowedtags['iframe'] = [ 128 | 'src' => true, 129 | 'width' => true, 130 | 'height' => true, 131 | 'frameborder' => true, 132 | 'allow' => true, 133 | 'allowfullscreen' => true, 134 | 'title' => true, 135 | ]; 136 | return $allowedtags; 137 | }, 10, 2); 138 | ``` 139 | 140 | To allow different html tags per dynamic data tag there is also a special hook. As you can see in the code snippet, general tags are filtered first and then passed to a tag specific filter. 141 | ```php 142 | $allowed_tags = wp_kses_allowed_html("juvo/dynamic_data_tag"); 143 | $allowed_tags = apply_filters("juvo/dynamic_data_tag/allowed_html_tags/$tag", $allowed_tags); 144 | ``` 145 | 146 | ## Advanced examples: 147 | ### iFrame 148 | Registers a dynamic data tag that displays the current post embedded in an iFrame. 149 | ```php 150 | // Register '{collection}' tag 151 | DDT_Registry::getInstance() 152 | ->set('collection', 'Collection', 'Collections', function($post, $context, array $filters = []) { 153 | return ""; 154 | }); 155 | 156 | // Add custom allowed html tags for the collection tag. In this case we allow iframes. 157 | add_filter("juvo/dynamic_data_tag/allowed_html_tags/collection", function($allowedtags) { 158 | $allowedtags['iframe'] = [ 159 | 'src' => true, 160 | 'width' => true, 161 | 'height' => true, 162 | 'frameborder' => true, 163 | 'allow' => true, 164 | 'allowfullscreen' => true, 165 | 'title' => true, 166 | ]; 167 | return $allowedtags; 168 | }); 169 | ``` 170 | ### Post Data 171 | Register a dynamic data tag {post_data} to display post data. A filter allows to select which data to display. Another custom filter "bold" allows to mark certain data to be bolded. 172 | The tag can be used like this: `{post_data:title:post_type~bold=post_type~bold=title}` will be displayed as "**Title**, Post Type". 173 | ```php 174 | DDT_Registry::getInstance()->set( 175 | 'post_data', 176 | 'Post Data', 177 | 'Posts', 178 | function( \WP_Post $post, string $context, array $filters= [], array $bold = [] ) { 179 | 180 | $output = []; 181 | foreach ( $filters as $filter ) { 182 | switch ( $filter ) { 183 | case 'title': 184 | $output['title'] = get_the_title(); 185 | break; 186 | case 'excerpt': 187 | $output['excerpt'] = get_the_excerpt(); 188 | break; 189 | case 'content': 190 | $output['content'] = get_the_content(); 191 | break; 192 | case 'post_type': 193 | $output['post_type'] = get_post_type(); 194 | break; 195 | } 196 | } 197 | 198 | // Add some of the filtered values tobe bold 199 | foreach($bold as $key) { 200 | if (in_array($key, $filters)) { 201 | $output[$key] = "".$output[$key].""; 202 | } 203 | } 204 | 205 | return implode( ', ', $output ); 206 | } 207 | ); 208 | 209 | // Add a custom pattern to introduce a "bold" capture group. 210 | add_filter("juvo/dynamic_data_tag/parse_tag/post_data", function($pattern, $tag) { 211 | $pattern = str_replace("}/", "", $pattern); 212 | return $pattern . "(?:~bold=(?[0-9a-zA-Z_-]+))*}/"; // use ~bold= as separator 213 | }, 10, 2); 214 | 215 | // To allow the freshly added "bold" capture group to have multiple values we need to split the values by the separator 216 | add_filter("juvo/dynamic_data_tag/parse_tag/pattern_bold", function($value) { 217 | return explode("~bold=", $value); 218 | }); 219 | ``` 220 | --------------------------------------------------------------------------------