PageRenderTime 50ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/system/pyrocms/libraries/Tags.php

https://github.com/Brucee/pyrocms
PHP | 542 lines | 286 code | 67 blank | 189 comment | 31 complexity | 14c0798aeea0bd9ca02563753459ec51 MD5 | raw file
  1. <?php
  2. /**
  3. * Tags
  4. *
  5. * A simple tag parsing library.
  6. *
  7. * @package Tags
  8. * @version 1.0
  9. * @author Dan Horrigan <http://dhorrigan.com>
  10. * @license Apache License v2.0
  11. * @copyright 2010 Dan Horrigan
  12. *
  13. * Licensed under the Apache License, Version 2.0 (the "License");
  14. * you may not use this file except in compliance with the License.
  15. * You may obtain a copy of the License at
  16. *
  17. * http://www.apache.org/licenses/LICENSE-2.0
  18. *
  19. * Unless required by applicable law or agreed to in writing, software
  20. * distributed under the License is distributed on an "AS IS" BASIS,
  21. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  22. * See the License for the specific language governing permissions and
  23. * limitations under the License.
  24. */
  25. class Tags
  26. {
  27. private $_trigger = '';
  28. private $_l_delim = '{';
  29. private $_r_delim = '}';
  30. private $_mark = 'k0dj3j4nJHDj22j';
  31. private $_tag_count = 0;
  32. private $_current_callback = array();
  33. // --------------------------------------------------------------------
  34. /**
  35. * Constructor
  36. *
  37. * @access public
  38. * @param array The custom config
  39. * @return void
  40. */
  41. public function __construct($config = array())
  42. {
  43. foreach ($config as $key => $val)
  44. {
  45. if (isset($this->{'_'.$key}))
  46. {
  47. $this->{'_'.$key} = $val;
  48. }
  49. }
  50. }
  51. // --------------------------------------------------------------------
  52. /**
  53. * Set Delimiters
  54. *
  55. * Set the delimeters for the tags
  56. *
  57. * @access public
  58. * @param string The left delimeter
  59. * @param string The right delimeter
  60. * @return object Returns $this to enable method chaining
  61. */
  62. public function set_delimiters($left, $right)
  63. {
  64. $this->_l_delim = $left;
  65. $this->_r_delim = $right;
  66. return $this;
  67. }
  68. // --------------------------------------------------------------------
  69. /**
  70. * Set Trigger
  71. *
  72. * Sets the tag trigger to use. This allows you to only consider tags
  73. * that have a trigger:
  74. *
  75. * {tag:name}{/tag:name}
  76. *
  77. * @access public
  78. * @param string The tag trigger
  79. * @return object Returns $this to enable method chaining
  80. */
  81. public function set_trigger($trigger)
  82. {
  83. $this->_trigger = $trigger;
  84. return $this;
  85. }
  86. // --------------------------------------------------------------------
  87. /**
  88. * Parse
  89. *
  90. * Parses the content and returns an array with marked content and tags
  91. * or the resulting content from calling the callback for each tag.
  92. *
  93. * @access public
  94. * @param string The content to parse
  95. * @param array The callback for each tag
  96. * @return mixed Either the tags as array or callback results
  97. */
  98. public function parse($content, $data = array(), $callback = array())
  99. {
  100. $this->_current_callback = $callback;
  101. if (trim($this->_trigger) == '')
  102. {
  103. throw new Exception('You must set a trigger before you can parse the content.');
  104. return $content;
  105. }
  106. $orig_content = $this->parse_globals($content, $data);
  107. $open_tag_regex = $this->_l_delim.$this->_trigger.'[^'.$this->_l_delim.$this->_r_delim.']*?'.$this->_r_delim;
  108. while (($start = strpos($orig_content, $this->_l_delim.$this->_trigger)) !== FALSE)
  109. {
  110. $content = $orig_content;
  111. if ( ! preg_match('/'.$open_tag_regex.'/i', $content, $tag))
  112. {
  113. break;
  114. }
  115. // We use these later
  116. $tag_len = strlen($tag[0]);
  117. $full_tag = $tag[0];
  118. // Trim off the left and right delimeters
  119. $tag = trim($full_tag, $this->_l_delim.$this->_r_delim);
  120. // Get the segments of the tag
  121. $segments = preg_replace('/(.*?)\s+.*/', '$1', $tag);
  122. // Get the attribute string
  123. $attributes = (preg_match('/\s+.*/', $tag, $args)) ? trim($args[0]) : '';
  124. // Lets start to create the parsed tag
  125. $parsed['full_tag'] = $full_tag;
  126. $parsed['attributes'] = $this->_parse_attributes($attributes);
  127. $parsed['full_segments'] = str_replace($this->_trigger, '', $segments);
  128. $parsed['segments'] = $this->_parse_segments($parsed['full_segments']);
  129. // Set the end tag to search for
  130. $end_tag = $this->_l_delim.'/'.$segments.$this->_r_delim;
  131. // Lets trim off the first part of the content
  132. $content = substr($content, $start + $tag_len);
  133. // If there is an end tag, get and set the content.
  134. if (($end = strpos($content, $end_tag)) !== FALSE)
  135. {
  136. $parsed['content'] = substr($content, 0, $end);
  137. $parsed['full_tag'] .= $parsed['content'].$end_tag;
  138. }
  139. else
  140. {
  141. $parsed['content'] = '';
  142. }
  143. $parsed['marker'] = 'marker_'.$this->_tag_count.$this->_mark;
  144. $orig_content = str_replace($parsed['full_tag'], $parsed['marker'], $orig_content, $count);
  145. $parsed['replacements'] = $count;
  146. $parsed_tags[] = $parsed;
  147. $this->_tag_count++;
  148. }
  149. if ( ! isset($parsed_tags) OR empty($parsed_tags))
  150. {
  151. $orig_content = $this->parse_php($this->parse_conditionals($orig_content), $data);
  152. return array('content' => $orig_content, 'tags' => array());
  153. }
  154. // Lets replace all the data tags first
  155. if ( ! empty($data))
  156. {
  157. // Clean up the array
  158. $data = $this->_force_array($data);
  159. foreach ($parsed_tags as $key => $tag)
  160. {
  161. // Parse the single tags
  162. if (empty($tag['content']))
  163. {
  164. $return_data = $this->_parse_data_single($tag, $data);
  165. }
  166. // Parse the double tags
  167. else
  168. {
  169. $return_data = FALSE;
  170. if (array_key_exists($tag['segments'][0], $data))
  171. {
  172. $return_data = $this->_parse_data_double($tag, $data);
  173. }
  174. }
  175. // If the tag referenced data then put that data in the content
  176. if ($return_data !== FALSE)
  177. {
  178. $orig_content = str_replace($tag['marker'], $return_data, $orig_content, $count);
  179. // Search and set missing replacements (tags in content and/or attributes of anothers tags)
  180. if ($count < $tag['replacements'])
  181. {
  182. $i = $key;
  183. while (isset($parsed_tags[++$i]))
  184. {
  185. if (strpos($parsed_tags[$i]['full_tag'], $tag['marker']) !== FALSE)
  186. {
  187. $parsed_tags[$i]['full_tag'] = str_replace($tag['marker'], $return_data, $parsed_tags[$i]['full_tag'], $count_full_tag);
  188. $count_content = 0;
  189. if ($parsed_tags[$i]['content'])
  190. {
  191. $parsed_tags[$i]['content'] = str_replace($tag['marker'], $return_data, $parsed_tags[$i]['content'], $count_content);
  192. }
  193. if ($count_content < $count_full_tag && $parsed_tags[$i]['attributes'])
  194. {
  195. $count_attributes = 0;
  196. foreach ($parsed_tags[$i]['attributes'] as &$attr)
  197. {
  198. $attr = str_replace($tag['marker'], $return_data, $attr, $count_attr);
  199. $count_attributes += $count_attr;
  200. if (($count_attributes + $count_content) >= $count_full_tag)
  201. {
  202. break;
  203. }
  204. }
  205. }
  206. $count += $count_full_tag;
  207. if ($count >= $tag['replacements'])
  208. {
  209. break;
  210. }
  211. }
  212. }
  213. }
  214. unset($parsed_tags[$key]);
  215. }
  216. }
  217. }
  218. // If there is a callback, call it for each tag
  219. if ( ! empty($callback) AND is_callable($callback))
  220. {
  221. foreach ($parsed_tags as $tag)
  222. {
  223. $orig_content = str_replace($tag['marker'], call_user_func($callback, $tag), $orig_content);
  224. }
  225. }
  226. // If there is no callback then lets loop through any remaining tags and just set them as ''
  227. else
  228. {
  229. foreach ($parsed_tags as $tag)
  230. {
  231. $orig_content = str_replace($tag['marker'], '', $orig_content);
  232. }
  233. }
  234. $orig_content = $this->parse_php($this->parse_conditionals($orig_content), $data);
  235. return array('content' => $orig_content, 'tags' => $parsed_tags);
  236. }
  237. // --------------------------------------------------------------------
  238. public function parse_conditionals($content)
  239. {
  240. if (strpos($content, '{if ') === false)
  241. {
  242. return $content;
  243. }
  244. preg_match_all('#{if (.*?)}#i', $content, $matches, PREG_OFFSET_CAPTURE);
  245. $len_offset = 0;
  246. foreach ($matches[0] as $match)
  247. {
  248. $replacement = preg_replace('#((^|\(|\)|\s|\+|\-|\*|\/|\.|\||\&|\>|\<|\=)((?!true|false|null)[a-z][a-z0-9]*))#i', '$2\$$3', $match[0]);
  249. $content = substr($content, 0, $match[1] + $len_offset).$replacement.substr($content, $match[1] + strlen($match[0]) + $len_offset);
  250. $len_offset += strlen($replacement) - strlen($match[0]);
  251. }
  252. preg_match_all('#{elseif (.*?)}#i', $content, $matches, PREG_OFFSET_CAPTURE);
  253. $len_offset = 0;
  254. foreach ($matches[0] as $match)
  255. {
  256. $replacement = preg_replace('#((^|\(|\)|\s|\+|\-|\*|\/|\.|\||\&|\>|\<|\=)((?!true|false|null)[a-z][a-z0-9]*))#i', '$2\$$3', $match[0]);
  257. $content = substr($content, 0, $match[1] + $len_offset).$replacement.substr($content, $match[1] + strlen($match[0]) + $len_offset);
  258. $len_offset += strlen($replacement) - strlen($match[0]);
  259. }
  260. $content = preg_replace('#{if (.*?)}#i', '<?php if($1): ?>', $content);
  261. $content = preg_replace('#{elseif (.*?)}#i', '<?php elseif($1): ?>', $content);
  262. $content = preg_replace('#{else}#i', '<?php else: ?>', $content);
  263. $content = preg_replace('#{/if}#i', '<?php endif; ?>', $content);
  264. return $content;
  265. }
  266. // --------------------------------------------------------------------
  267. private function parse_php($_content_to_parse, $data = array())
  268. {
  269. extract($data);
  270. ob_start();
  271. echo eval('?>'.$_content_to_parse.'<?php ');
  272. $_content_to_parse = ob_get_contents();
  273. ob_end_clean();
  274. return $_content_to_parse;
  275. }
  276. // --------------------------------------------------------------------
  277. /**
  278. * Parse Globals
  279. *
  280. * Parses global data tags. These are tags that do not use a trigger
  281. * and have a variable in the $data array. This enables you to use
  282. * globals inside of other tags:
  283. *
  284. * The Tag:
  285. * {tag:blog:posts offset="{offset}"}
  286. *
  287. * The data array:
  288. * array(
  289. * 'offset' => $this->uri->segment(3),
  290. * );
  291. *
  292. * @access public
  293. * @param string The content to parse
  294. * @param array The globals
  295. * @return string The parsed content
  296. */
  297. public function parse_globals($content, $data)
  298. {
  299. foreach ($data as $var => $value)
  300. {
  301. if (is_object($value))
  302. {
  303. $value = (array) $value;
  304. }
  305. if ( ! is_array($value))
  306. {
  307. $content = str_replace('{'.$var.'}', $value, $content);
  308. }
  309. }
  310. return $content;
  311. }
  312. // --------------------------------------------------------------------
  313. /**
  314. * Get the data pertaining to the given single tag.
  315. *
  316. * Example Data:
  317. * $data = array(
  318. * 'books' => array(
  319. * 'count' => 2
  320. * )
  321. * );
  322. *
  323. * Example Tag:
  324. * {books:count}
  325. *
  326. * @access private
  327. * @param array The single tag
  328. * @param array The data to parse
  329. * @return mixed Either the data for the tag or FALSE
  330. */
  331. private function _parse_data_single($tag, $data)
  332. {
  333. foreach ($tag['segments'] as $segment)
  334. {
  335. if ( ! is_array($data) OR ! isset($data[$segment]))
  336. {
  337. return FALSE;
  338. }
  339. $data = $data[$segment];
  340. }
  341. return $data;
  342. }
  343. // --------------------------------------------------------------------
  344. /**
  345. * Get the data pertaining to the given double tag. This will
  346. * loop through arrays of given data
  347. *
  348. * Example Data:
  349. * $data = array(
  350. * 'books' => array(
  351. * array(
  352. * 'title' => 'PHP for Dummies',
  353. * 'author' => 'John Doe'
  354. * ),
  355. * array(
  356. * 'title' => 'CodeIgniter for Dummies',
  357. * 'author' => 'Jane Doe'
  358. * )
  359. * )
  360. * );
  361. *
  362. * Example Tags:
  363. * {books}
  364. * {title} by {author}<br />
  365. * {/books}
  366. *
  367. * @access private
  368. * @param array The double tag
  369. * @param array The data to parse
  370. * @return mixed Either the data for the tag or FALSE
  371. */
  372. private function _parse_data_double($tag, $data)
  373. {
  374. $return_data = '';
  375. $new_data = $data;
  376. foreach ($tag['segments'] as $segment)
  377. {
  378. if ( ! is_array($new_data) OR ! isset($new_data[$segment]))
  379. {
  380. return FALSE;
  381. }
  382. $new_data = $new_data[$segment];
  383. }
  384. $temp = new Tags;
  385. $temp->set_trigger($this->_trigger);
  386. foreach ($new_data as $val)
  387. {
  388. if ( ! is_array($val))
  389. {
  390. $val = array($val);
  391. }
  392. // We add the array element to the full data array so that full data
  393. // tags can work within double data tags
  394. $val = $val + $data;
  395. $return = $temp->parse($tag['content'], $val, $this->_current_callback);
  396. $return_data .= $return['content'];
  397. }
  398. unset($temp);
  399. return $return_data;
  400. }
  401. // --------------------------------------------------------------------
  402. /**
  403. * Forces normal multi-dimensional arrays and objects into an
  404. * array structure that will work with EE/Mojo/CI Parsers.
  405. *
  406. * @author Phil Sturgeon <http://philsturgeon.co.uk>
  407. * @access private
  408. * @param mixed The object or array to clean
  409. * @param int Used for recursion
  410. * @return array The clean array
  411. */
  412. private function _force_array($var, $level = 1)
  413. {
  414. if (is_object($var))
  415. {
  416. $var = (array) $var;
  417. }
  418. if (is_array($var))
  419. {
  420. // Make sure everything else is array or single value
  421. foreach ($var as $index => & $child)
  422. {
  423. $child = $this->_force_array($child, $level + 1);
  424. if (is_object($child))
  425. {
  426. $child = (array) $child;
  427. }
  428. }
  429. }
  430. return $var;
  431. }
  432. // --------------------------------------------------------------------
  433. /**
  434. * Parse Attributes
  435. *
  436. * Parses the string of attributes into a keyed array
  437. *
  438. * @param string The string of attributes
  439. * @return array The keyed array of attributes
  440. */
  441. private function _parse_attributes($attributes)
  442. {
  443. preg_match_all('/(.*?)\s*=\s*(\042|\047)(.*?)\\2/is', $attributes, $parts);
  444. // The tag has no attrbutes
  445. if (empty($parts[0]))
  446. {
  447. return array();
  448. }
  449. // The tag has attributes, so lets parse them
  450. else
  451. {
  452. $attr = array();
  453. for ($i = 0; $i < count($parts[1]); $i++)
  454. {
  455. $attr[trim($parts[1][$i])] = $parts[3][$i];
  456. }
  457. }
  458. return $attr;
  459. }
  460. // --------------------------------------------------------------------
  461. /**
  462. * Parse Segments
  463. *
  464. * Parses the string of segments into an array
  465. *
  466. * @param string The string of segments
  467. * @return array The array of segments
  468. */
  469. private function _parse_segments($segments)
  470. {
  471. $segments = explode(':', $segments);
  472. return $segments;
  473. }
  474. }
  475. /* End of file Tags.php */