├── .gitignore ├── library ├── fields │ ├── kt │ │ └── tag.php │ ├── title │ │ └── tag.php │ ├── date │ │ └── tag.php │ └── email │ │ └── tag.php ├── html │ ├── gist │ │ └── tag.php │ ├── a │ │ └── tag.php │ ├── iframe │ │ └── tag.php │ ├── youtube │ │ └── tag.php │ └── vimeo │ │ └── tag.php └── files │ ├── image │ └── tag.php │ └── images │ └── tag.php ├── composer.json ├── index.php ├── README.md └── lib └── CustomTags └── CustomTags.php /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /library/fields/kt/tag.php: -------------------------------------------------------------------------------- 1 | {$att['field']}()->kt() : ''; 8 | 9 | return $field; 10 | } 11 | -------------------------------------------------------------------------------- /library/html/gist/tag.php: -------------------------------------------------------------------------------- 1 | {$att['field']}()->html() : 'Your Title'; 8 | $class = isset($att['class']) ? $att['class'] : ''; 9 | $wraptag = isset($att['wraptag']) ? $att['wraptag'] : 'h1'; 10 | 11 | $title = Html::tag($wraptag, $field, ["class" => $class]); 12 | 13 | return $title; 14 | } 15 | -------------------------------------------------------------------------------- /library/files/image/tag.php: -------------------------------------------------------------------------------- 1 | image($att['file'])) { 9 | $file = isset($att['file']) ? page()->image($att['file'])->url() : page()->images()->first()->url(); 10 | $class = isset($att['class']) ? $att['class'] : ''; 11 | return Html::img($file, ["class" => $class]); 12 | } else { 13 | return Html::tag('p', ['Image not found']); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /library/fields/date/tag.php: -------------------------------------------------------------------------------- 1 | {$att['field']}()->toDate($format) : date($format); 14 | 15 | return Html::tag($wraptag, $dateval, ["class" => $class]); 16 | } 17 | -------------------------------------------------------------------------------- /library/html/a/tag.php: -------------------------------------------------------------------------------- 1 | $class, "rel" => $rel]); 15 | 16 | return $link; 17 | } 18 | -------------------------------------------------------------------------------- /library/fields/email/tag.php: -------------------------------------------------------------------------------- 1 | {$att['field']}() : 'you@example.com'; 9 | $text = isset($att['text']) ? $att['text'] : null; 10 | $class = isset($att['class']) ? $att['class'] : ''; 11 | 12 | if ($email) { 13 | $email = Html::email($email, $text, ["class" => $class]); 14 | } 15 | 16 | if ($field) { 17 | $email = Html::email($field, $text, ["class" => $class]); 18 | } 19 | 20 | return $email; 21 | } 22 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright James Steel 9 | * @link https://github.com/HashandSalt/carver 10 | * @license MIT 11 | */ 12 | 13 | @include_once __DIR__ . '/vendor/autoload.php'; 14 | 15 | require('lib/CustomTags/CustomTags.php'); 16 | 17 | $ct = new CustomTags(array( 18 | 'tag_name' => 'kb', 19 | 'tag_callback_prefix' => 'kb_', 20 | 'parse_on_shutdown' => true, 21 | 'tag_directory' => [$kirby->root('site').'/carver/', $kirby->root('plugins').'/carver/library/fields/', $kirby->root('plugins').'/carver/library/files/', $kirby->root('plugins').'/carver/library/html/'], 22 | 'sniff_for_buried_tags' => true, 23 | 'hash_tags' => true, 24 | )); 25 | -------------------------------------------------------------------------------- /library/html/iframe/tag.php: -------------------------------------------------------------------------------- 1 | $class, "width" => $width, "height" => $height, "sandbox" => $sandbox]); 19 | 20 | $wrapme = isset($wraptag) ? Html::tag($wraptag, [$iframeembed], ["class" => $wrapclass]) : $iframeembed; 21 | 22 | 23 | return $wrapme ; 24 | } 25 | -------------------------------------------------------------------------------- /library/html/youtube/tag.php: -------------------------------------------------------------------------------- 1 | $autoplay, "loop" => $loop], ["width" => $width, "height" => $height]); 21 | 22 | $wrapme = isset($wraptag) ? Html::tag($wraptag, [$iframeembed], ["class" => $class]) : $iframeembed; 23 | 24 | 25 | return $wrapme ; 26 | } 27 | -------------------------------------------------------------------------------- /library/files/images/tag.php: -------------------------------------------------------------------------------- 1 | {$att['field']}()->toFiles() : page()->images(); 14 | 15 | // Build up the list 16 | $html = ''; 17 | foreach ($images as $image) { 18 | $img = ''; 19 | $html .= Html::tag($breaktag, [$img]). PHP_EOL; 20 | } 21 | 22 | $imageset = Html::tag($wraptag, [$html], ["class" => $class]). PHP_EOL; 23 | 24 | return $imageset; 25 | } 26 | -------------------------------------------------------------------------------- /library/html/vimeo/tag.php: -------------------------------------------------------------------------------- 1 | $autoplay, "loop" => $loop, "autopause" => $autopause], ["width" => $width, "height" => $height]); 21 | 22 | $wrapme = isset($wraptag) ? Html::tag($wraptag, [$iframeembed], ["class" => $class]) : $iframeembed; 23 | 24 | 25 | return $wrapme ; 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kirby Carver 2 | 3 | ## What it does 4 | 5 | This plugin allows you to write custom HTML tags, powered by PHP. 6 | 7 | ## Why? 8 | 9 | Kirby is most awesome, but I do miss the simplicity of Textpattern's templating language, which looks like HTML. Having gone through available alternative templating engines like Twig and Blade, without much love, I decided to put this together. 10 | 11 | For example, to get the a formatted date from a field, normally you would do something like this: 12 | 13 | ``` 14 | 17 | ``` 18 | 19 | What if you could do this instead? 20 | 21 | 22 | ``` 23 | 24 | ``` 25 | 26 | 27 | You can find some example tags in `site/plugins/carver/library` 28 | 29 | ## Installation 30 | 31 | * Download the files and place in `plugins/carver` 32 | * Create a folder called `carver` under the `site` folder. This is where you tags will be stored. 33 | 34 | ## Usage 35 | 36 | Let's create a simple tag as an example to show what this can do, based on the date tag mentioned above. 37 | 38 | Start by creating the following file: 'site/carver/date/tag.php'. 39 | 40 | Add this code to the file: 41 | 42 | ``` 43 | {$att['field']}()->toDate($format) : date($format); 56 | 57 | return Html::tag($wraptag, $dateval, ["class" => $class]); 58 | 59 | } 60 | ``` 61 | 62 | Let's explain how that works. Our tag looks like this: 63 | 64 | ``` 65 | 66 | ``` 67 | 68 | It has three custom attributes, `format`, `field` and `class`. 69 | 70 | If you use that in your template right now, you will get this rendered, assuming you have a date field set in your blueprint with a value stored: 71 | 72 | ``` 73 | 74 | ``` 75 | 76 | Pretty self explanatory, you can set the field to use, the class to give the output, and the date format to use. But thats not all - you can skip some of the attributes because fallbacks have been set. 77 | 78 | Doing this in a template will use todays date instead, format it to 'd/m/Y', and without adding a class: 79 | 80 | ``` 81 | 82 | ``` 83 | 84 | Will result in this: 85 | 86 | ``` 87 |

02/02/2019

88 | ``` 89 | 90 | ## More tag examples 91 | 92 | ``` 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | ``` 126 | 127 | ## Road Map 128 | 129 | * Bring tag parser up to date 130 | * Refactor tag parser to allow for options to be set via config 131 | * Create a big library of built in tags that work with Kirby's built in functions. 132 | * ~Composer support~ 133 | 134 | ## Dedication 135 | 136 | When I am not using Kirby, I am using Textpattern. Sadly, Dean Allen, who created Textpattern died about a year ago. His philosophy of simplicity in a CMS aligns with that of Kirby, and this plugin is in his honour. 137 | -------------------------------------------------------------------------------- /lib/CustomTags/CustomTags.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * @license BSD 7 | * @copyright Copyright (c) 2008 Oliver Lillie 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation 9 | * files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, 10 | * modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software 11 | * is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be 12 | * included in all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 15 | * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 17 | * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | * 19 | * @package CustomTags 20 | * @version 1.0.0 21 | */ 22 | 23 | 24 | 25 | class CustomTags 26 | { 27 | public $version = '0.2.5'; 28 | private static $_instance = 0; 29 | 30 | /** 31 | * Holds the options array 32 | * @access private 33 | * @var array 34 | */ 35 | private $_options = array( 36 | // parse_on_shutdown; boolean; If 'use_buffer' is enabled and this option is also enalbed it will create a 37 | // register_shutdown_function that will process the buffered output at the end of the script without any hassle. 38 | 'parse_on_shutdown' => false, 39 | // use_buffer; boolean; You can optionally use output buffering instead of providing the html for compacting. 40 | 'use_buffer' => false, 41 | // echo_output; boolean; If after processing you want to output the content set this to true, otherwise it 42 | // will be up to you to echo out the compacted html. 43 | 'echo_output' => false, 44 | // tag_name; string; The custom tag prefix 45 | 'tag_name' => 'ct', 46 | 'tag_callback_prefix' => 'ct_', 47 | // tag_global_callback if this is set ALL callbacks will go through this function, it will be given 3 arguments 48 | // the first is the name of the function to be called, the second is the tag data and the third is the buffer. Note 49 | // YOU MUST make sure this callback is available to be called, otherwise custom tags will fail without error. 50 | 'tag_global_callback' => false, 51 | // tag_directory; The directory of the custom tags. 52 | 'tag_directory' => false, 53 | // missing_tags_error_mode; The error mode for outputting errors. 54 | // CustomTags::ERROR_EXCEPTION throws and exception for catching. 55 | // CustomTags::ERROR_SILENT returns empty data if a tag or error occurs. 56 | // CustomTags::ERROR_ECHO returns an error string. 57 | 'missing_tags_error_mode' => CustomTags::ERROR_EXCEPTION, 58 | // sniff_for_buried_tags; Checks the parsed tag content for buried tags. If you know it's not needed then 59 | // you should not enable the sniffing for code optimisation 60 | 'sniff_for_buried_tags' => false, 61 | // cache_tags; You can optionaly cache the tag output for improved performance. If enabled you will also 62 | // need to set the cache_directory option, alternativley you can use your own cache class. 63 | 'cache_tags' => false, 64 | // cache_directory; The diretory to save the tag cache in. 65 | 'cache_directory' => false, 66 | // custom_cache_tag_class; A custom cache class for your own manipulation of the tag cache 67 | // using the custom_cache_tag_class, the class should have two functions one for checking the cache 68 | // "getCache" and one for saving the cache "cache". 69 | 'custom_cache_tag_class' => false, 70 | // hash_tags; A boolean value that dictates if tags with inner content should have hash tags #{varname} 71 | // matched for values and put into the array returned to the callback function 72 | 'hash_tags' => false, 73 | ); 74 | 75 | const ERROR_EXCEPTION = 'THROW_EXCEPTION'; 76 | const ERROR_SILENT = 'ERROR_SILENT'; 77 | const ERROR_ECHO = 'ERROR_ECHO'; 78 | 79 | public static $name = 'CustomTags'; 80 | public static $nocache_tags = array(); 81 | private static $_required = array(); 82 | private static $_tags_to_collect = array(); 83 | private $_collections = array(); 84 | private $_registered = array(); 85 | private $_buffer_in_use = false; 86 | public static $tag_collections = array(); 87 | private static $_tag_order = array(); 88 | 89 | public static $tag_directory_base = false; 90 | public static $error_mode = false; 91 | 92 | // The custom tag open string 93 | public static $tag_open = '<'; 94 | // The custom tag close string 95 | public static $tag_close = '>'; 96 | 97 | public function __construct($options=array()) 98 | { 99 | $this->setOption($options); 100 | if ($this->_options['parse_on_shutdown']) { 101 | $this->setOption(array( 102 | 'use_buffer' => true, 103 | 'echo_output' => true 104 | )); 105 | } 106 | 107 | 108 | 109 | if ($this->_options['tag_directory'] === false) { 110 | $this->setOption('tag_directory', dirname(__FILE__).DIRECTORY_SEPARATOR.'tags'.DIRECTORY_SEPARATOR); 111 | } 112 | 113 | if ($this->_options['missing_tags_error_mode'] === false) { 114 | $this->setOption('missing_tags_error_mode', self::ERROR_EXCEPTION); 115 | } 116 | 117 | if ($this->_options['cache_tags']) { 118 | if ($this->_options['custom_cache_tag_class'] === false) { 119 | if ($this->_options['cache_directory'] === false) { 120 | $this->setOption('cache_directory', dirname(__FILE__).DIRECTORY_SEPARATOR.'cache'.DIRECTORY_SEPARATOR); 121 | } else { 122 | if (is_dir($this->_options['cache_directory']) || !is_writable($this->_options['cache_directory']) === false) { 123 | $this->_options['cache_tags'] = false; 124 | } 125 | } 126 | } else { 127 | if (class_exists($this->_options['custom_cache_tag_class']) === false) { 128 | $this->_options['cache_tags'] = false; 129 | } 130 | } 131 | } 132 | 133 | if ($this->_options['use_buffer'] === true) { 134 | ob_start(); 135 | } 136 | if ($this->_options['parse_on_shutdown'] === true) { 137 | register_shutdown_function(array(&$this, 'parse')); 138 | } 139 | } 140 | 141 | /** 142 | * Sets an option in the option array(); 143 | * 144 | * @access public 145 | * @param mixed $varname Can take the form of an array of options to set a string of an option name. 146 | * @param mixed $varvalue The value of the option you are setting. 147 | **/ 148 | public function setOption($varname, $varvalue=null) 149 | { 150 | $keys = array_keys($this->_options); 151 | if (gettype($varname) === 'array') { 152 | foreach ($varname as $name=>$value) { 153 | $this->setOption($name, $value); 154 | } 155 | } else { 156 | if (in_array($varname, $keys) === true) { 157 | $this->_options[$varname] = $varvalue; 158 | } 159 | switch ($varname) { 160 | case 'tag_directory': 161 | self::$tag_directory_base = $varvalue; 162 | break; 163 | case 'missing_tags_error_mode': 164 | self::$error_mode = $varvalue; 165 | break; 166 | case 'custom_cache_tag_class': 167 | if (class_exists($varvalue) === false) { 168 | $this->_options['cache_tags'] = false; 169 | } 170 | break; 171 | case 'cache_directory': 172 | if (is_dir($varvalue) === false || is_writable($varvalue) === false) { 173 | $this->_options['cache_tags'] = false; 174 | } 175 | break; 176 | } 177 | } 178 | } 179 | 180 | /** 181 | * Registers a parsed tag. Each tag callback must register the parsed tag so 182 | * deep buried tags can be correctly replaced. 183 | * 184 | * @access public 185 | * @param array $tag 186 | * @return void 187 | */ 188 | 189 | 190 | 191 | 192 | public static function registerParsedTag($tag) 193 | { 194 | self::$registered[$tag['source_marker']] = $tag; 195 | } 196 | 197 | /** 198 | * Parses the source for any custom tags. 199 | * @access public 200 | * @param mixed|boolean|string $source If false then it will capture the output buffer, otherwise if a string 201 | * it will use this value to search for custom tags. 202 | * @param boolean $parse_collections If true then any collected tags will be parsed after the tags are parsed. 203 | * @return string The parsed $source value. 204 | */ 205 | public function parse($source=false, $parse_collections=false, $_internal_loop=false) 206 | { 207 | // increment the parse count so it has unique identifiers 208 | self::$_instance += 1; 209 | // capture the source from the buffer 210 | if ($source === false) { 211 | $source = ob_get_clean(); 212 | $this->_buffer_in_use = true; 213 | $parse_collections = true; 214 | } 215 | 216 | // collect the tags for processing 217 | $tags = $this->collectTags($source); 218 | if (count($tags) > 0) { 219 | // there are tags so process them 220 | $output = $this->_parseTags($tags); 221 | if ($output && $parse_collections === true) { 222 | // parse any collected tags if required 223 | $output = $this->_processCollectedTags($output); 224 | } 225 | if ($this->_options['echo_output'] === true) { 226 | echo $output; 227 | } 228 | return $output; 229 | } 230 | return $source; 231 | } 232 | 233 | /** 234 | * Processes a tag by loading 235 | * @access private 236 | * @param array $tag The tag to parse. 237 | * @return string The content of the tag. 238 | */ 239 | private function _parseTag($tag) 240 | { 241 | // return nothing if the tag is disabled 242 | if (isset($tag['attributes']->disabled) === true && $tag['attributes']->disabled === 'true') { 243 | return ''; 244 | } 245 | 246 | $tag_data = false; 247 | $caching_tag = isset($tag['attributes']->cache) && $tag['attributes']->cache === 'false' ? false : true; 248 | if ($this->_options['cache_tags'] === true && $caching_tag === true) { 249 | if ($this->_options['custom_cache_tag_class'] !== false) { 250 | $tag_data = call_user_func_array(array($this->_options['custom_cache_tag_class'], 'getCache'), array($tag)); 251 | } else { 252 | $cache_file = $this->_options['cache_directory'].md5(serialize($tag)); 253 | if (is_file($cache_file) === true) { 254 | $tag_data = file_get_contents($cache_file); 255 | } 256 | } 257 | if ($tag_data) { 258 | $tag['cached'] = true; 259 | return $tag_data; 260 | } 261 | } 262 | 263 | // look for and load tag function file 264 | $tag_func_name = ucwords(str_replace(array('_', '-'), ' ', $tag['name'])); 265 | $tag_func_name = strtolower(substr($tag_func_name, 0, 1)).substr($tag_func_name, 1); 266 | $func_name = str_replace(' ', '', $this->_options['tag_callback_prefix'].$tag_func_name); 267 | $tag_data = ''; 268 | $collect_tag = false; 269 | $tag_order = false; 270 | if (function_exists($func_name) === false) { 271 | $has_resource = false; 272 | if (is_array($this->_options['tag_directory']) === false) { 273 | $this->_options['tag_directory'] = array($this->_options['tag_directory']); 274 | } 275 | foreach ($this->_options['tag_directory'] as $directory) { 276 | $tag_file = rtrim($directory, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$tag['name'].DIRECTORY_SEPARATOR.'tag.php'; 277 | if (is_file($tag_file) === true) { 278 | if (isset(self::$_required[$tag['name']]) === false) { 279 | self::$_required[$tag['name']] = true; 280 | $collect = false; 281 | if (function_exists($func_name) === false) { 282 | include_once $tag_file; 283 | } 284 | self::$_tags_to_collect[$tag['name']] = $collect; 285 | if (function_exists($func_name) === false) { 286 | return self::throwError($tag['name'], 'tag resource "'.$directory.DIRECTORY_SEPARATOR.'tag.php" found but callback "'.$func_name.'" wasn\'t.'); 287 | } 288 | $has_resource = true; 289 | } 290 | } 291 | } 292 | if ($has_resource === false) { 293 | return self::throwError($tag['name'], 'tag resource not found.'); 294 | } 295 | } 296 | 297 | // do we have to collect this tag for parsing at a later point 298 | if (self::$_tags_to_collect[$tag['name']] !== false) { 299 | if (isset(self::$tag_collections[$tag['name']]) === false) { 300 | self::$tag_collections[$tag['name']] = array(); 301 | } 302 | $tag['collected'] = true; 303 | $index = array_push(self::$tag_collections[$tag['name']], $tag)-1; 304 | if ($tag_order !== false) { 305 | if (isset(self::$_tag_order[$block['name']]) === false) { 306 | self::$_tag_order[$block['name']] = array(); 307 | } 308 | self::$_tag_order[$block['name']] = $tag_order; 309 | } 310 | return self::$tag_collections[$tag['name']][$index]['tag'] = '------@@%'.self::$_instance.'-'.$tag['name'].'-'.$index.'-'.uniqid(time().'-').'%@@------'; 311 | } 312 | 313 | // excecute the tag callback 314 | if ($this->_options['tag_global_callback'] !== false) { 315 | $tag_data = trim(call_user_func($this->_options['tag_global_callback'], 'tag', $func_name, $tag)); 316 | } else { 317 | $tag_data = trim(call_user_func($func_name, $tag)); 318 | } 319 | 320 | // this is where we sniff for buried tags within the returned content 321 | if (empty($tag_data) === false) { 322 | if ($this->_options['sniff_for_buried_tags'] === true && strpos($tag_data, self::$tag_open.$this->_options['tag_name'].':') !== false) { 323 | // we have the possibility of buried tags so lets parse 324 | // but first make sure the output isn't echoed out 325 | $old_echo_value = $this->_options['echo_output']; 326 | $this->_options['echo_output'] = false; 327 | // parse the tag_data 328 | $tag_data = $this->parse($tag_data, false, true); 329 | // restore the echo_output value back to what it was originally 330 | $this->_options['echo_output'] = $old_echo_value; 331 | } 332 | 333 | if ($this->_options['cache_tags'] === true && $caching_tag === true) { 334 | if ($this->_options['custom_cache_tag_class'] !== false) { 335 | call_user_func_array(array($this->_options['custom_cache_tag_class'], 'cache'), array($tag, $tag_data)); 336 | } else { 337 | file_put_contents($this->_options['cache_directory'].md5(serialize($tag)), $tag_data, LOCK_EX); 338 | } 339 | } 340 | } 341 | return $tag_data; 342 | } 343 | 344 | /** 345 | * Produces an error. 346 | * @access public 347 | * @param string $tag The name of the tag producing an error. 348 | * @param string $message The message of the error. 349 | * @return mixed|error|string Either a string or thrown error is returned dependent 350 | * on the 'missing_tags_error_mode' option. 351 | */ 352 | public static function throwError($tag, $message) 353 | { 354 | if (self::$error_mode === self::ERROR_EXCEPTION && !self::_buffer_in_use) { 355 | throw new CustomTagsException(''.$tag.' '.$message.'.'); 356 | } elseif (self::$error_mode !== self::ERROR_SILENT) { 357 | return '['.self::$name.' Error]: '.ucfirst($tag).' Tag - '.$message.'
'; 358 | } 359 | return ''; 360 | } 361 | 362 | /** 363 | * Loops and parses the found custom tags. 364 | * @access private 365 | * @param array $tags An array of found custom tag data. 366 | * @return mixed|string|boolean Returns false if there are no tags, string otherwise. 367 | */ 368 | private function _parseTags($tags) 369 | { 370 | if (count($tags) > 0) { 371 | // loop through the tags 372 | foreach ($tags as $key=>$tag) { 373 | // if a tag is delayed, it is rendered after everything else and it's has to be re parsed. 374 | if (isset($tag['attributes']) === true && isset($tag['attributes']->delayed) === true && $tag['attributes']->delayed === 'true') { 375 | continue; 376 | } 377 | // check for buried preserved tags, so they can be replaced. 378 | // NOTE: this only works with no collected tags. Collected tags are a massive pain in the rectum. 379 | if (($has_buried = preg_match_all('!------@@%([0-9\-]+)%@@------!', $tag['content'], $info)) > 0) { 380 | $containers = $info[0]; 381 | $indexs = $info[1]; 382 | $replacements = array(); 383 | foreach ($indexs as $key2=>$index) { 384 | $index_parts = explode('-', $index); 385 | $tag_index = array_pop($index_parts); 386 | if (isset($tags[$tag_index]['parsed']) === true) { 387 | $replacements[$key2] = $tags[$tag_index]['parsed']; 388 | } else { 389 | if (isset($tags[$tag_index]['block']) === true) { 390 | $block = preg_replace('/ delayed="true"/', '', $tags[$tag_index]['block'], 1); 391 | if (isset($tag['block']) === true) { 392 | $tag['block'] = str_replace($containers[$key2], $block, $tag['block']); 393 | } 394 | $tag['content'] = str_replace($containers[$key2], $block, $tag['content']); 395 | } 396 | } 397 | } 398 | $tags[$key]['buried_source_markers'] = $tag['buried_source_markers'] = $containers; 399 | } 400 | 401 | // if the tag is a nocahe tag then the block must be replaced as is for later processing 402 | if ($tag['name'] === 'nocache') { 403 | array_push(self::$nocache_tags, $tag); 404 | $tags[$key]['parsed'] = $tag['source_marker']; 405 | // $tags[$key]['parsed'] = $tag['block']; 406 | } 407 | // if the tag is just plain text then just shove the content back into the parsed as it doesn't require processing 408 | elseif ($tag['name'] === '___text') { 409 | $tags[$key]['parsed'] = $tag['content']; 410 | } 411 | // otherwise we have a tag and must post process 412 | else { 413 | $tags[$key]['parsed'] = $this->_parseTag($tag); 414 | } 415 | // update any buried tags within the parsed content 416 | $tags[$key]['parsed'] = $has_buried > 0 ? str_replace($containers, $replacements, $tags[$key]['parsed']) : $tags[$key]['parsed']; 417 | } 418 | // // if there are items within the nocache elements, parse the contents to pull them out 419 | // $tagname = $this->_options['tag_name']; 420 | // if(substr_count($tags[$key]['parsed'], self::$tag_open.$tagname.':') > substr_count($tags[$key]['parsed'], self::$tag_open.$tagname.':nocache')) 421 | // { 422 | // $tags[$key]['parsed'] = $this->parse($tags[$key]['parsed'], true, true); 423 | // // $tags[$key]['parsed'] = $this->_processTags($tags[$key]['parsed'], true); 424 | // } 425 | 426 | // Debug::info($tags[$key]); 427 | 428 | return $tags[$key]['parsed']; 429 | } 430 | return false; 431 | } 432 | 433 | /** 434 | * Process collected blocks. These are different types of block that are required to be processed as a group. 435 | * Usual reasons for doing this are to reduce resources and sql queries. 436 | * 437 | * @param string $source The source of the output. 438 | * @return string 439 | */ 440 | public function _processCollectedTags($source) 441 | { 442 | $to_replace = array(); 443 | $ordered = array(); 444 | foreach (self::$tag_collections as $tag_name=>$tags) { 445 | // if this block has a collection order use it 446 | if (isset(self::$_tag_order[$tag_name]) === true) { 447 | $pos = self::$_tag_order[$tag_name]; 448 | $ordered[$pos] = $tags; 449 | continue; 450 | } 451 | // the source should be modified by the collection script 452 | $tag_func_name = ucwords(str_replace(array('_', '-'), ' ', $tag_name)); 453 | $tag_func_name = strtolower(substr($tag_func_name, 0, 1)).substr($tag_func_name, 1); 454 | $tag_func_name = str_replace(' ', '', $this->_options['tag_callback_prefix'].$tag_func_name); 455 | if ($this->_options['tag_global_callback'] !== false) { 456 | $tags = call_user_func($this->_options['tag_global_callback'], 'collection', $tag_func_name, $tags, $source); 457 | } else { 458 | $tags = call_user_func($tag_func_name, $tags, $source); 459 | } 460 | 461 | if ($tags === -1) { 462 | $source = self::throwError($tag_name, 'tag collection parsed however callback "'.$tag_func_name.'" returned -1.'); 463 | } elseif (is_array($tags) === false) { 464 | $source = self::throwError($tag_name, 'tag collection parsed however callback "'.$tag_func_name.'" did not return the array of process tags.'); 465 | } elseif (count($tags) > 0) { 466 | // input the parsed tags back into the source or store for re-intergration 467 | foreach ($tags as $key => $tag) { 468 | if (strpos($source, $tag['tag']) !== false) { 469 | $source = str_replace($tag['tag'], $tag['parsed'], $source); 470 | } else { 471 | // do a reverse tag lookup here as the tag was not found in the source, thus it must be buried and un processed 472 | array_push($to_replace, $tag); 473 | } 474 | } 475 | } 476 | } 477 | foreach ($ordered as $key=>$tags) { 478 | // the source should be modified by the collection script 479 | $tag_func_name = ucwords(str_replace(array('_', '-'), ' ', $tag_name)); 480 | $tag_func_name = strtolower(substr($tag_func_name, 0, 1)).substr($tag_func_name, 1); 481 | $tag_func_name = str_replace(' ', '', $this->_options['tag_callback_prefix'].$tag_func_name); 482 | $tags = call_user_func($tag_func_name, $tags); 483 | if ($tags === -1) { 484 | $source = self::throwError($tag['name'], 'tag collection parsed however callback "'.$tag_func_name.'" returned -1.'); 485 | } 486 | } 487 | return $this->_doBuriedReplacements($to_replace, $source); 488 | } 489 | 490 | private function _doBuriedReplacements($replacements, $source) 491 | { 492 | if (count($replacements) > 0) { 493 | $to_replace = array(); 494 | foreach ($replacements as $key => $tag) { 495 | if (strpos($source, $tag['source_marker']) !== false) { 496 | $source = str_replace($tag['source_marker'], $tag['parsed'], $source); 497 | } else { 498 | array_push($to_replace, $tag); 499 | } 500 | } 501 | if (count($to_replace) > 0) { 502 | $source = $this->_doBuriedReplacements($to_replace, $source); 503 | } 504 | } 505 | return $source; 506 | } 507 | 508 | /** 509 | * Searches and parses a source for custom tags. 510 | * @access public 511 | * @param string $source The source to search for custom tags in. 512 | * @param mixed|boolean|string $tag_name If false then the default option 'tag_name' is used 513 | * when searching for custom tags, if not and $tag_name is a string then a custom tag beginning 514 | * with that prefix will be looked for. 515 | * @return array An array of found tags. 516 | */ 517 | public function collectTags($source, $tag_name=false) 518 | { 519 | $tagname = $tag_name === false ? $this->_options['tag_name'] : $tag_name; 520 | 521 | $tags = $tag_names = array(); 522 | $tag_count = 0; 523 | 524 | $inner_tag_open_pos = $source_len = strlen($source); 525 | $opener = self::$tag_open.$tagname.':'; 526 | $opener_len = strlen($opener); 527 | $closer = self::$tag_open.'/'.$tagname.':'; 528 | $closer_len = strlen($closer); 529 | $closer_end = self::$tag_close; 530 | 531 | while ($inner_tag_open_pos !== false) { 532 | // start getting the last found opener tag 533 | $open_tag_look_source = substr($source, 0, $inner_tag_open_pos); 534 | $open_tag_look_len = strlen($open_tag_look_source); 535 | $inner_tag_open_pos = strrpos($open_tag_look_source, $opener); 536 | 537 | // if there is no last tag then the rest is the final text 538 | if ($inner_tag_open_pos === false) { 539 | array_push($tags, array('content'=>$source, 'name'=>'___text')); 540 | break; 541 | } else { 542 | // get the source from the start of that last tag 543 | $tag_look_source = substr($source, $inner_tag_open_pos); 544 | $open_bracket_pos = strpos($tag_look_source, self::$tag_open, 1); 545 | $short_tag_close_pos = strpos($tag_look_source, '/'.self::$tag_close); 546 | 547 | if ($short_tag_close_pos !== false && $short_tag_close_pos < $open_bracket_pos) { 548 | $inner_tag_close_pos = $short_tag_close_pos + 2; 549 | } else { 550 | $inner_tag_close_pos_begin = strpos($tag_look_source, $closer); 551 | $inner_tag_close_pos = strpos($tag_look_source, $closer_end, $inner_tag_close_pos_begin)+1; 552 | } 553 | 554 | // get the content of the block 555 | $tag_source = substr($tag_look_source, 0, $inner_tag_close_pos); 556 | 557 | $tag = $this->_buildTag($tag_source, $tagname); 558 | $index = count($tags); 559 | $tag['source_marker'] = '------@@%'.self::$_instance.'-'.$index.'%@@------'; 560 | array_push($tags, $tag); 561 | 562 | // modify the source so it doesn't get repeated 563 | $source = substr($source, 0, $inner_tag_open_pos).$tag['source_marker'].substr($source, $inner_tag_open_pos+$inner_tag_close_pos); 564 | // $source = str_replace($tag_source, '------@@%'.$index.'%@@------', $source); 565 | } 566 | } 567 | return $tags; 568 | } 569 | 570 | /** 571 | * Parses a tag for the tag attributes and inner content. 572 | * @access private 573 | * @param string $str The tag string to be parsed. 574 | * @param string $tagname The prefix of the custom tag being parsed. 575 | * @return array The tag 576 | */ 577 | private function _buildTag($str, $tagname) 578 | { 579 | // $tagname = $this->_options['tag_name']; 580 | $tag = array( 581 | 'block' => $str, 582 | 'content' => '', 583 | 'name' => '', 584 | 'attributes' => array() 585 | ); 586 | $begin_len = strlen(self::$tag_open.$tagname.':'); 587 | // echo substr($str, 0, $begin_len)."\r\n"; 588 | if (substr($str, 0, $begin_len) !== self::$tag_open.$tagname.':') { 589 | // closing tag 590 | $tag['name'] = '___text'; 591 | return $tag; 592 | } elseif (substr($str, 1, 1) === '/') { 593 | // closing tag 594 | $tag['name'] = '---ERROR---'; 595 | return $tag; 596 | } else { 597 | // opening tag 598 | $matches = array(); 599 | // check to see if this is a full tag or an openclose tag 600 | $has_closing_tag = substr($tag['block'], -2) !== '/'.self::$tag_close; 601 | // perform data matches 602 | if ($has_closing_tag === true) { 603 | $preg = '!(\\'.self::$tag_open.$tagname.':([_\-A-Za-z0-9]*)[^\\'.self::$tag_close.']*\\'.self::$tag_close.'| \/\\'.self::$tag_close.').*\\'.self::$tag_open.'\/'.$tagname.':[_\-A-Za-z0-9]*\\'.self::$tag_close.'!is'; 604 | // $preg = '!(\<'.$tagname.':([_\-A-Za-z0-9]*)[^\>]*\>| \/\>).*\<\/'.$tagname.':[_\-A-Za-z0-9]*\>!is'; 605 | } else { 606 | $preg = '!(\\'.self::$tag_open.$tagname.':([_\-A-Za-z0-9]*)([^\\'.self::$tag_close.']*))!is'; 607 | // $preg = '!(\<'.$tagname.':([_\-A-Za-z0-9]*)([^\/\>]*))!is'; 608 | } 609 | 610 | if (preg_match_all($preg, $tag['block'], $matches) > 0) { 611 | // get the tag type 612 | $tag['name'] = $matches[2][0]; 613 | 614 | // get the tag inner content 615 | $tag['content'] = ''; 616 | $tag['vars'] = false; 617 | $attribute_string = $matches[1][0]; 618 | if ($has_closing_tag === true) { 619 | $begin_len = strlen($matches[1][0]); 620 | $end_len = $has_closing_tag ? strlen(self::$tag_open.'/'.$tagname.':'.$tag['name'].self::$tag_close) : 0; 621 | $tag['content'] = substr($str, $begin_len, strlen($matches[0][0])-$begin_len-$end_len); 622 | // get hash tags vars? 623 | if ($this->_options['hash_tags'] === true && preg_match_all('/#{([^}]+)?}/i', $tag['content'], $vars) > 0) { 624 | $variables = array(); 625 | foreach ($vars[0] as $key => $var) { 626 | $variables[$var] = $vars[1][$key]; 627 | } 628 | $tag['vars'] = $variables; 629 | } 630 | } else { 631 | $attribute_string = rtrim($attribute_string, '/ '); 632 | } 633 | 634 | // get the attributes 635 | $attributes = array(); 636 | // preg_match_all("/([_\-A-Za-z0-9]*)((=\"|='))([\w\W\s]*)(\"|')/", $opener, $attributes); 637 | // preg_match_all("/([_\-A-Za-z0-9]*)((=\"|='))([_\-A-Za-z0-9]*)(\"|')/", $opener, $atts); 638 | // $result = preg_match_all("!([_\-A-Za-z0-9]*)(=\"|=')([\#\s\:\.\?\&\=\%\+\,\@\_\-A-Za-z0-9]*)(\"|')!is", $matches[1][0], $attributes); 639 | if (preg_match_all("!([_\-A-Za-z0-9]*)(=\")([^\"]*)(\")!is", $attribute_string, $attributes) > 0) { 640 | foreach ($attributes[0] as $key=>$row) { 641 | $tag['attributes'][$attributes[1][$key]] = $attributes[3][$key]; 642 | } 643 | } 644 | } 645 | $tag['attributes'] = (object) $tag['attributes']; 646 | return $tag; 647 | } 648 | } 649 | } 650 | 651 | /** 652 | * The Custom Tags exception. 653 | */ 654 | class CustomTagsException extends \Exception 655 | { 656 | } 657 | --------------------------------------------------------------------------------