PageRenderTime 56ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/classes/fTemplating.php

https://bitbucket.org/dsqmoore/flourish
PHP | 1519 lines | 876 code | 229 blank | 414 comment | 168 complexity | 36ac582c2a7d1bafbb443c55585413b7 MD5 | raw file
  1. <?php
  2. /**
  3. * Allows for quick and flexible HTML templating
  4. *
  5. * @copyright Copyright (c) 2007-2011 Will Bond, others
  6. * @author Will Bond [wb] <will@flourishlib.com>
  7. * @author Matt Nowack [mn] <mdnowack@gmail.com>
  8. * @license http://flourishlib.com/license
  9. *
  10. * @package Flourish
  11. * @link http://flourishlib.com/fTemplating
  12. *
  13. * @version 1.0.0b23
  14. * @changes 1.0.0b23 Added a default `$name` for ::retrieve() to mirror ::attach() [wb, 2011-08-31]
  15. * @changes 1.0.0b22 Backwards Compatibility Break - removed the static method ::create(), added the static method ::attach() to fill its place [wb, 2011-08-31]
  16. * @changes 1.0.0b21 Fixed a bug in ::enableMinification() where the minification cache directory was sometimes not properly converted to a web path [wb, 2011-08-31]
  17. * @changes 1.0.0b20 Fixed a bug in CSS minification that would reduce multiple zeros that are part of a hex color code, fixed minification of `+ ++` and similar constructs in JS [wb, 2011-08-31]
  18. * @changes 1.0.0b19 Corrected a bug in ::enablePHPShortTags() that would prevent proper translation inside of HTML tag attributes [wb, 2011-01-09]
  19. * @changes 1.0.0b18 Fixed a bug with CSS minification and black hex codes [wb, 2010-10-10]
  20. * @changes 1.0.0b17 Backwards Compatibility Break - ::delete() now returns the values of the element or elements that were deleted instead of returning the fTemplating instance [wb, 2010-09-19]
  21. * @changes 1.0.0b16 Fixed another bug with minifying JS regex literals [wb, 2010-09-13]
  22. * @changes 1.0.0b15 Fixed a bug with minifying JS regex literals that occur after a reserved word [wb, 2010-09-12]
  23. * @changes 1.0.0b14 Added documentation about `[sub-key]` syntax [wb, 2010-09-12]
  24. * @changes 1.0.0b13 Backwards Compatibility Break - ::add(), ::delete(), ::get() and ::set() now interpret `[` and `]` as array shorthand and thus they can not be used in element names, renamed ::remove() to ::filter() - added `$beginning` parameter to ::add() and added ::remove() method [wb, 2010-09-12]
  25. * @changes 1.0.0b12 Added ::enableMinification(), ::enablePHPShortTags(), the ability to be able to place child fTemplating objects via a new magic element `__main__` and the `$main_element` parameter for ::__construct() [wb, 2010-08-31]
  26. * @changes 1.0.0b11 Fixed a bug with the elements not being initialized to a blank array [wb, 2010-08-12]
  27. * @changes 1.0.0b10 Updated ::place() to ignore URL query strings when detecting an element type [wb, 2010-07-26]
  28. * @changes 1.0.0b9 Added the methods ::delete() and ::remove() [wb+mn, 2010-07-15]
  29. * @changes 1.0.0b8 Fixed a bug with placing absolute file paths on Windows [wb, 2010-07-09]
  30. * @changes 1.0.0b7 Removed `e` flag from preg_replace() calls [wb, 2010-06-08]
  31. * @changes 1.0.0b6 Changed ::set() and ::add() to return the object for method chaining, changed ::set() and ::get() to accept arrays of elements [wb, 2010-06-02]
  32. * @changes 1.0.0b5 Added ::encode() [wb, 2010-05-20]
  33. * @changes 1.0.0b4 Added ::create() and ::retrieve() for named fTemplating instances [wb, 2010-05-11]
  34. * @changes 1.0.0b3 Fixed an issue with placing relative file path [wb, 2010-04-23]
  35. * @changes 1.0.0b2 Added the ::inject() method [wb, 2009-01-09]
  36. * @changes 1.0.0b The initial implementation [wb, 2007-06-14]
  37. */
  38. class fTemplating
  39. {
  40. const attach = 'fTemplating::attach';
  41. const reset = 'fTemplating::reset';
  42. const retrieve = 'fTemplating::retrieve';
  43. /**
  44. * Named fTemplating instances
  45. *
  46. * @var array
  47. */
  48. static $instances = array();
  49. /**
  50. * Attaches a named template that can be accessed from any scope via ::retrieve()
  51. *
  52. * @param fTemplating $templating The fTemplating object to attach
  53. * @param string $name The name for this templating instance
  54. * @return void
  55. */
  56. static public function attach($templating, $name='default')
  57. {
  58. self::$instances[$name] = $templating;
  59. }
  60. /**
  61. * Resets the configuration of the class
  62. *
  63. * @internal
  64. *
  65. * @return void
  66. */
  67. static public function reset()
  68. {
  69. self::$instances = array();
  70. }
  71. /**
  72. * Retrieves a named template
  73. *
  74. * @param string $name The name of the template to retrieve
  75. * @return fTemplating The specified fTemplating instance
  76. */
  77. static public function retrieve($name='default')
  78. {
  79. if (!isset(self::$instances[$name])) {
  80. throw new fProgrammerException(
  81. 'The named template specified, %s, has not been attached yet',
  82. $name
  83. );
  84. }
  85. return self::$instances[$name];
  86. }
  87. /**
  88. * The buffered object id, used for differentiating different instances when doing replacements
  89. *
  90. * @var integer
  91. */
  92. private $buffered_id;
  93. /**
  94. * A data store for templating
  95. *
  96. * @var array
  97. */
  98. private $elements;
  99. /**
  100. * The directory to store minified code in
  101. *
  102. * @var string
  103. */
  104. private $minification_directory;
  105. /**
  106. * The path prefix to prepend to CSS and JS paths to find them on the filesystem
  107. *
  108. * @var string
  109. */
  110. private $minification_prefix;
  111. /**
  112. * The minification mode: development or production
  113. *
  114. * @var string
  115. */
  116. private $minification_mode;
  117. /**
  118. * The directory to look for files
  119. *
  120. * @var string
  121. */
  122. protected $root;
  123. /**
  124. * The directory to store PHP files with short tags fixed
  125. *
  126. * @var string
  127. */
  128. private $short_tag_directory;
  129. /**
  130. * The short tag mode: development or production
  131. *
  132. * @var string
  133. */
  134. private $short_tag_mode;
  135. /**
  136. * Initializes this templating engine
  137. *
  138. * @param string $root The filesystem path to use when accessing relative files, defaults to `$_SERVER['DOCUMENT_ROOT']`
  139. * @param string $main_element The value for the `__main__` element - this is used when calling ::place() without an element, or when placing fTemplating objects as children
  140. * @return fTemplating
  141. */
  142. public function __construct($root=NULL, $main_element=NULL)
  143. {
  144. if ($root === NULL) {
  145. $root = $_SERVER['DOCUMENT_ROOT'];
  146. }
  147. if (!file_exists($root)) {
  148. throw new fProgrammerException(
  149. 'The root specified, %s, does not exist on the filesystem',
  150. $root
  151. );
  152. }
  153. if (!is_readable($root)) {
  154. throw new fEnvironmentException(
  155. 'The root specified, %s, is not readable',
  156. $root
  157. );
  158. }
  159. if (substr($root, -1) != '/' && substr($root, -1) != '\\') {
  160. $root .= DIRECTORY_SEPARATOR;
  161. }
  162. $this->buffered_id = NULL;
  163. $this->elements = array();
  164. $this->root = $root;
  165. if ($main_element !== NULL) {
  166. $this->set('__main__', $main_element);
  167. }
  168. }
  169. /**
  170. * Finishing placing elements if buffering was used
  171. *
  172. * @internal
  173. *
  174. * @return void
  175. */
  176. public function __destruct()
  177. {
  178. // The __destruct method can't throw unhandled exceptions intelligently, so we will always catch here just in case
  179. try {
  180. $this->placeBuffered();
  181. } catch (Exception $e) {
  182. fCore::handleException($e);
  183. }
  184. }
  185. /**
  186. * All requests that hit this method should be requests for callbacks
  187. *
  188. * @internal
  189. *
  190. * @param string $method The method to create a callback for
  191. * @return callback The callback for the method requested
  192. */
  193. public function __get($method)
  194. {
  195. return array($this, $method);
  196. }
  197. /**
  198. * Adds a value to an array element
  199. *
  200. * @param string $element The element to add to - array elements can be modified via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
  201. * @param mixed $value The value to add
  202. * @param boolean $beginning If the value should be added to the beginning of the element
  203. * @return fTemplating The template object, to allow for method chaining
  204. */
  205. public function add($element, $value, $beginning=FALSE)
  206. {
  207. $tip =& $this->elements;
  208. if ($bracket_pos = strpos($element, '[')) {
  209. $original_element = $element;
  210. $array_dereference = substr($element, $bracket_pos);
  211. $element = substr($element, 0, $bracket_pos);
  212. preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
  213. $array_keys = array_map('current', $array_keys);
  214. array_unshift($array_keys, $element);
  215. foreach (array_slice($array_keys, 0, -1) as $array_key) {
  216. if (!isset($tip[$array_key])) {
  217. $tip[$array_key] = array();
  218. } elseif (!is_array($tip[$array_key])) {
  219. throw new fProgrammerException(
  220. '%1$s was called for an element, %2$s, which is not an array',
  221. 'add()',
  222. $original_element
  223. );
  224. }
  225. $tip =& $tip[$array_key];
  226. }
  227. $element = end($array_keys);
  228. }
  229. if (!isset($tip[$element])) {
  230. $tip[$element] = array();
  231. } elseif (!is_array($tip[$element])) {
  232. throw new fProgrammerException(
  233. '%1$s was called for an element, %2$s, which is not an array',
  234. 'add()',
  235. $element
  236. );
  237. }
  238. if ($beginning) {
  239. array_unshift($tip[$element], $value);
  240. } else {
  241. $tip[$element][] = $value;
  242. }
  243. return $this;
  244. }
  245. /**
  246. * Enables buffered output, allowing ::set() and ::add() to happen after a ::place() but act as if they were done before
  247. *
  248. * Please note that using buffered output will affect the order in which
  249. * code is executed since the elements are not actually ::place()'ed until
  250. * the destructor is called.
  251. *
  252. * If the non-template code depends on template code being executed
  253. * sequentially before it, you may not want to use output buffering.
  254. *
  255. * @return void
  256. */
  257. public function buffer()
  258. {
  259. static $id_sequence = 1;
  260. if ($this->buffered_id) {
  261. throw new fProgrammerException('Buffering has already been started');
  262. }
  263. if (!fBuffer::isStarted()) {
  264. fBuffer::start();
  265. }
  266. $this->buffered_id = $id_sequence;
  267. $id_sequence++;
  268. }
  269. /**
  270. * Deletes an element from the template
  271. *
  272. * @param string $element The element to delete - array elements can be modified via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
  273. * @param mixed $default_value The value to return if the `$element` is not set
  274. * @param array |$elements The elements to delete - an array of element names or an associative array of keys being element names and the values being the default values
  275. * @return mixed The value of the `$element` that was deleted - an associative array of deleted elements will be returned if an array of `$elements` was specified
  276. */
  277. public function delete($element, $default_value=NULL)
  278. {
  279. if (is_array($element)) {
  280. $elements = $element;
  281. if (is_numeric(key($elements))) {
  282. $new_elements = array();
  283. foreach ($elements as $element) {
  284. $new_elements[$element] = NULL;
  285. }
  286. $elements = $new_elements;
  287. }
  288. $output = array();
  289. foreach ($elements as $key => $default_value) {
  290. $output[$key] = $this->delete($key, $default_value);
  291. }
  292. return $output;
  293. }
  294. $tip =& $this->elements;
  295. $value = $default_value;
  296. if ($bracket_pos = strpos($element, '[')) {
  297. $original_element = $element;
  298. $array_dereference = substr($element, $bracket_pos);
  299. $element = substr($element, 0, $bracket_pos);
  300. preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
  301. $array_keys = array_map('current', $array_keys);
  302. array_unshift($array_keys, $element);
  303. foreach (array_slice($array_keys, 0, -1) as $array_key) {
  304. if (!isset($tip[$array_key])) {
  305. return $value;
  306. } elseif (!is_array($tip[$array_key])) {
  307. throw new fProgrammerException(
  308. '%1$s was called for an element, %2$s, which is not an array',
  309. 'delete()',
  310. $original_element
  311. );
  312. }
  313. $tip =& $tip[$array_key];
  314. }
  315. $element = end($array_keys);
  316. }
  317. if (isset($tip[$element])) {
  318. $value = $tip[$element];
  319. unset($tip[$element]);
  320. }
  321. return $value;
  322. }
  323. /**
  324. * Erases all output since the invocation of the template - only works if buffering is on
  325. *
  326. * @return void
  327. */
  328. public function destroy()
  329. {
  330. if (!$this->buffered_id) {
  331. throw new fProgrammerException(
  332. 'A template can only be destroyed if buffering has been enabled'
  333. );
  334. }
  335. $this->buffered_id = NULL;
  336. fBuffer::erase();
  337. fBuffer::stop();
  338. $this->__destruct();
  339. }
  340. /**
  341. * Enables minified output for CSS and JS elements
  342. *
  343. * For CSS and JS, compilation means that the file will be minified and
  344. * cached. The filename will change whenever the content change, allowing
  345. * for far-futures expire headers.
  346. *
  347. * Please note that this option requires that all CSS and JS paths be
  348. * relative to the $_SERVER['DOCUMENT_ROOT'] and start with a `/`. Also
  349. * this class will not clean up old cached files out of the cache
  350. * directory.
  351. *
  352. * This functionality will be inherited by all child fTemplating objects
  353. * that do not have their own explicit minification settings.
  354. *
  355. * @param string $mode The compilation mode - `'development'` means that file modification times will be checked on each load, `'production'` means that the cache files will only be regenerated when missing
  356. * @param fDirectory|string $cache_directory The directory to cache the compiled files into - this needs to be inside the document root or a path added to fFilesystem::addWebPathTranslation()
  357. * @param fDirectory|string $path_prefix The directory to prepend to all CSS and JS paths to load the files from the filesystem - this defaults to `$_SERVER['DOCUMENT_ROOT']`
  358. * @return void
  359. */
  360. public function enableMinification($mode, $cache_directory, $path_prefix=NULL)
  361. {
  362. $valid_modes = array('development', 'production');
  363. if (!in_array($mode, $valid_modes)) {
  364. throw new fProgrammerException(
  365. 'The mode specified, %1$s, is invalid. Must be one of: %2$s.',
  366. $mode,
  367. join(', ', $valid_modes)
  368. );
  369. }
  370. $cache_directory = $cache_directory instanceof fDirectory ? $cache_directory->getPath() : realpath($cache_directory);
  371. if (!is_writable($cache_directory)) {
  372. throw new fEnvironmentException(
  373. 'The cache directory specified, %s, is not writable',
  374. $cache_directory
  375. );
  376. }
  377. $path_prefix = $path_prefix instanceof fDirectory ? $path_prefix->getPath() : $path_prefix;
  378. if ($path_prefix === NULL) {
  379. $path_prefix = $_SERVER['DOCUMENT_ROOT'];
  380. }
  381. $this->minification_mode = $mode;
  382. $this->minification_directory = fDirectory::makeCanonical($cache_directory);
  383. $this->minification_prefix = $path_prefix;
  384. }
  385. /**
  386. * Converts PHP short tags to long tags when short tags are turned off
  387. *
  388. * Please note that this only affects PHP files that are **directly**
  389. * evaluated with ::place() or ::inject(). It will not affect PHP files that
  390. * have been evaluated via `include` or `require` statements inside of the
  391. * directly evaluated PHP files.
  392. *
  393. * This functionality will be inherited by all child fTemplating objects
  394. * that do not have their own explicit short tag settings.
  395. *
  396. * @param string $mode The compilation mode - `'development'` means that file modification times will be checked on each load, `'production'` means that the cache files will only be regenerated when missing
  397. * @param fDirectory|string $cache_directory The directory to cache the compiled files into - this directory should not be accessible from the web
  398. * @return void
  399. */
  400. public function enablePHPShortTags($mode, $cache_directory)
  401. {
  402. // This does not need to be enabled if short tags are on
  403. if (ini_get('short_open_tag') && strtolower(ini_get('short_open_tag')) != 'off') {
  404. return;
  405. }
  406. $valid_modes = array('development', 'production');
  407. if (!in_array($mode, $valid_modes)) {
  408. throw new fProgrammerException(
  409. 'The mode specified, %1$s, is invalid. Must be one of: %2$s.',
  410. $mode,
  411. join(', ', $valid_modes)
  412. );
  413. }
  414. $cache_directory = $cache_directory instanceof fDirectory ? $cache_directory->getPath() : $cache_directory;
  415. if (!is_writable($cache_directory)) {
  416. throw new fEnvironmentException(
  417. 'The cache directory specified, %s, is not writable',
  418. $cache_directory
  419. );
  420. }
  421. $this->short_tag_mode = $mode;
  422. $this->short_tag_directory = fDirectory::makeCanonical($cache_directory);
  423. }
  424. /**
  425. * Gets the value of an element and runs it through fHTML::encode()
  426. *
  427. * @param string $element The element to get - array elements can be accessed via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
  428. * @param mixed $default_value The value to return if the element has not been set
  429. * @return mixed The value of the element specified run through fHTML::encode(), or the default value if it has not been set
  430. */
  431. public function encode($element, $default_value=NULL)
  432. {
  433. return fHTML::encode($this->get($element, $default_value));
  434. }
  435. /**
  436. * Removes a value from an array element
  437. *
  438. * @param string $element The element to remove from - array elements can be modified via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
  439. * @param mixed $value The value to remove - compared in a non-strict manner, such that removing `0` will remove a blank string and false also
  440. * @return fTemplating The template object, to allow for method chaining
  441. */
  442. public function filter($element, $value)
  443. {
  444. $tip =& $this->elements;
  445. if ($bracket_pos = strpos($element, '[')) {
  446. $original_element = $element;
  447. $array_dereference = substr($element, $bracket_pos);
  448. $element = substr($element, 0, $bracket_pos);
  449. preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
  450. $array_keys = array_map('current', $array_keys);
  451. array_unshift($array_keys, $element);
  452. foreach (array_slice($array_keys, 0, -1) as $array_key) {
  453. if (!isset($tip[$array_key])) {
  454. return $this;
  455. } elseif (!is_array($tip[$array_key])) {
  456. throw new fProgrammerException(
  457. '%1$s was called for an element, %2$s, which is not an array',
  458. 'filter()',
  459. $original_element
  460. );
  461. }
  462. $tip =& $tip[$array_key];
  463. }
  464. $element = end($array_keys);
  465. }
  466. if (!isset($tip[$element])) {
  467. return $this;
  468. } elseif (!is_array($tip[$element])) {
  469. throw new fProgrammerException(
  470. '%1$s was called for an element, %2$s, which is not an array',
  471. 'filter()',
  472. $element
  473. );
  474. }
  475. $keys = array_keys($tip[$element], $value);
  476. if ($keys) {
  477. foreach ($keys as $key) {
  478. unset($tip[$element][$key]);
  479. }
  480. $tip[$element] = array_values($tip[$element]);
  481. }
  482. return $this;
  483. }
  484. /**
  485. * Takes an array of PHP files and caches a version with all short tags converted to regular tags
  486. *
  487. * @param array $values The file paths to the PHP files
  488. * @return array An array of file paths to the corresponding converted PHP files
  489. */
  490. private function fixShortTags($values)
  491. {
  492. $fixed_paths = array();
  493. foreach ($values as $value) {
  494. // Check to see if the element is a path relative to the template root
  495. if (!preg_match('#^(/|\\\\|[a-z]:(\\\\|/)|\\\\|//|\./|\.\\\\)#i', $value)) {
  496. $value = $this->root . $value;
  497. }
  498. $real_value = realpath($value);
  499. $cache_path = $this->short_tag_directory . sha1($real_value) . '.php';
  500. $fixed_paths[] = $cache_path;
  501. if (file_exists($cache_path) && ($this->short_tag_mode == 'production' || filemtime($cache_path) >= filemtime($real_value))) {
  502. continue;
  503. }
  504. $code = file_get_contents($real_value);
  505. $output = '';
  506. $in_php = FALSE;
  507. do {
  508. if (!$in_php) {
  509. $token_regex = '<\?';
  510. } else {
  511. $token_regex .= '/\*|//|\\#|\'|"|<<<[a-z_]\w*|<<<\'[a-z_]\w*\'|\?>';
  512. }
  513. if (!preg_match('#' . $token_regex . '#i', $code, $match)) {
  514. $part = $code;
  515. $code = '';
  516. $token = NULL;
  517. } else {
  518. $token = $match[0];
  519. $pos = strpos($code, $token);
  520. if ($pos === FALSE) {
  521. break;
  522. }
  523. $part = substr($code, 0, $pos);
  524. $code = substr($code, $pos);
  525. }
  526. $regex = NULL;
  527. if ($token == "<?") {
  528. $output .= $part;
  529. $in_php = TRUE;
  530. continue;
  531. } elseif ($token == "?>") {
  532. $regex = NULL;
  533. $in_php = FALSE;
  534. } elseif ($token == "//") {
  535. $regex = '#^//.*(\n|$)#D';
  536. } elseif ($token == "#") {
  537. $regex = '@^#.*(\n|$)@D';
  538. } elseif ($token == "/*") {
  539. $regex = '#^.{2}.*?(\*/|$)#sD';
  540. } elseif ($token == "'") {
  541. $regex = '#^\'((\\\\.)+|[^\\\\\']+)*(\'|$)#sD';
  542. } elseif ($token == '"') {
  543. $regex = '#^"((\\\\.)+|[^\\\\"]+)*("|$)#sD';
  544. } elseif ($token) {
  545. $regex = '#\A<<<\'?([a-zA-Z_]\w*)\'?.*?^\1;\n#sm';
  546. }
  547. $part = str_replace('<?=', '<?php echo', $part);
  548. $part = preg_replace('#<\?(?!php)#i', '<?php', $part);
  549. // This makes sure that __FILE__ and __DIR__ stay as the
  550. // original value since the cached file will be in a different
  551. // place with a different filename
  552. $part = preg_replace('#(?<=[^a-zA-Z0-9]|^)__FILE__(?=[^a-zA-Z0-9]|$)#iD', "'" . $real_value . "'", $part);
  553. if (fCore::checkVersion('5.3')) {
  554. $part = preg_replace('#(?<=[^a-zA-Z0-9]|^)__DIR__(?=[^a-zA-Z0-9]|$)#iD', "'" . dirname($real_value) . "'", $part);
  555. }
  556. $output .= $part;
  557. if ($regex) {
  558. preg_match($regex, $code, $match);
  559. $output .= $match[0];
  560. $code = substr($code, strlen($match[0]));
  561. }
  562. } while (strlen($code));
  563. file_put_contents($cache_path, $output);
  564. }
  565. return $fixed_paths;
  566. }
  567. /**
  568. * Gets the value of an element
  569. *
  570. * @param string $element The element to get - array elements can be accessed via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
  571. * @param mixed $default_value The value to return if the element has not been set
  572. * @param array |$elements An array of elements to get, or an associative array where a string key is the element to get and the value is the default value
  573. * @return mixed The value of the element(s) specified, or the default value(s) if it has not been set
  574. */
  575. public function get($element, $default_value=NULL)
  576. {
  577. if (is_array($element)) {
  578. $elements = $element;
  579. // Turn an array of elements into an array of elements with NULL default values
  580. if (array_values($elements) === $elements) {
  581. $elements = array_combine($elements, array_fill(0, count($elements), NULL));
  582. }
  583. $output = array();
  584. foreach ($elements as $element => $default_value) {
  585. $output[$element] = $this->get($element, $default_value);
  586. }
  587. return $output;
  588. }
  589. $array_dereference = NULL;
  590. if ($bracket_pos = strpos($element, '[')) {
  591. $array_dereference = substr($element, $bracket_pos);
  592. $element = substr($element, 0, $bracket_pos);
  593. }
  594. if (!isset($this->elements[$element])) {
  595. return $default_value;
  596. }
  597. $value = $this->elements[$element];
  598. if ($array_dereference) {
  599. preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
  600. $array_keys = array_map('current', $array_keys);
  601. foreach ($array_keys as $array_key) {
  602. if (!is_array($value) || !isset($value[$array_key])) {
  603. $value = $default_value;
  604. break;
  605. }
  606. $value = $value[$array_key];
  607. }
  608. }
  609. return $value;
  610. }
  611. /**
  612. * Combines an array of CSS or JS files and places them as a single file
  613. *
  614. * @param string $type The type of compilation, 'css' or 'js'
  615. * @param string $element The element name
  616. * @param array $values An array of file paths
  617. * @return void
  618. */
  619. protected function handleMinified($type, $element, $values)
  620. {
  621. $paths = array();
  622. $media = NULL;
  623. foreach ($values as $value) {
  624. if (is_array($value)) {
  625. $paths[] = $this->minification_prefix . $value['path'];
  626. if ($type == 'css') {
  627. $media = !empty($value['media']) ? $value['media'] : NULL;
  628. }
  629. } else {
  630. $paths[] = $this->minification_prefix . $value;
  631. }
  632. }
  633. $hash = sha1(join('|', $paths));
  634. $cache_file = $this->minification_directory . $hash . '.' . $type;
  635. $regenerate = FALSE;
  636. $checked_paths = FALSE;
  637. if (!file_exists($cache_file)) {
  638. $regenerate = TRUE;
  639. } elseif ($this->minification_mode == 'development') {
  640. $cache_mtime = filemtime($cache_file);
  641. $checked_paths = TRUE;
  642. foreach ($paths as $path) {
  643. if (!file_exists($path)) {
  644. throw new fEnvironmentException(
  645. 'The file specified, %s, does not exist under the $path_prefix specified',
  646. preg_replace('#^' . preg_quote($this->minification_prefix, '#') . '#', '', $path)
  647. );
  648. }
  649. if (filemtime($path) > $cache_mtime) {
  650. $regenerate = TRUE;
  651. break;
  652. }
  653. }
  654. }
  655. if ($regenerate) {
  656. $minified = '';
  657. foreach ($paths as $path) {
  658. $path_cache_file = $this->minification_directory . sha1($path) . '.' . $type;
  659. if ($checked_paths && !file_exists($path)) {
  660. throw new fEnvironmentException(
  661. 'The file specified, %s, does not exist under the $path_prefix specified',
  662. preg_replace('#^' . preg_quote($this->minification_prefix, '#') . '#', '', $path)
  663. );
  664. }
  665. // Checks if this path has been cached
  666. if (file_exists($path_cache_file) && filemtime($path_cache_file) >= filemtime($path)) {
  667. $minified_path = file_get_contents($path_cache_file);
  668. } else {
  669. $minified_path = trim($this->minify(file_get_contents($path), $type));
  670. file_put_contents($path_cache_file, $minified_path);
  671. }
  672. $minified .= "\n" . $minified_path;
  673. }
  674. file_put_contents($cache_file, substr($minified, 1));
  675. }
  676. $version = filemtime($cache_file);
  677. $compiled_value = fFilesystem::translateToWebPath($cache_file) . '?v=' . $version;
  678. if ($type == 'css' && $media) {
  679. $compiled_value = array(
  680. 'path' => $compiled_value,
  681. 'media' => $media
  682. );
  683. }
  684. $method = 'place' . strtoupper($type);
  685. $this->$method($compiled_value);
  686. }
  687. /**
  688. * Includes the file specified - this is identical to ::place() except a filename is specified instead of an element
  689. *
  690. * Please see the ::place() method for more details about functionality.
  691. *
  692. * @param string $file_path The file to place
  693. * @param string $file_type Will force the file to be placed as this type of file instead of auto-detecting the file type. Valid types include: `'css'`, `'js'`, `'php'` and `'rss'`.
  694. * @return void
  695. */
  696. public function inject($file_path, $file_type=NULL)
  697. {
  698. $prefix = '__injected_';
  699. $num = 1;
  700. while (isset($this->elements[$prefix . $num])) {
  701. $num++;
  702. }
  703. $element = $prefix . $num;
  704. $this->set($element, $file_path);
  705. $this->place($element, $file_type);
  706. }
  707. /**
  708. * Minifies JS or CSS
  709. *
  710. * For JS, this function is based on the JSMin algorithm (not the code) from
  711. * http://www.crockford.com/javascript/jsmin.html with the addition of
  712. * preserving /*! comment blocks for things like licenses. Some other
  713. * versions of JSMin change the contents of special comment blocks, but
  714. * this version does not.
  715. *
  716. * @param string $code The code to minify
  717. * @param string $type The type of code, 'css' or 'js'
  718. * @return string The minified code
  719. */
  720. protected function minify($code, $type)
  721. {
  722. $output = '';
  723. $buffer = '';
  724. $stack = array();
  725. $token_regex = '#/\*|\'|"';
  726. if ($type == 'js') {
  727. $token_regex .= '|//';
  728. $token_regex .= '|/';
  729. } elseif ($type == 'css') {
  730. $token_regex .= '|url\(';
  731. }
  732. $token_regex .= '#i';
  733. do {
  734. if (!preg_match($token_regex, $code, $match)) {
  735. $part = $code;
  736. $code = '';
  737. $token = NULL;
  738. } else {
  739. $token = $match[0];
  740. $pos = strpos($code, $token);
  741. if ($pos === FALSE) {
  742. break;
  743. }
  744. $part = substr($code, 0, $pos);
  745. $code = substr($code, $pos);
  746. }
  747. $regex = NULL;
  748. if ($token == '/') {
  749. if (!preg_match('#([(,=:[!&|?{};\n]|\breturn)\s*$#D', $part)) {
  750. $part .= $token;
  751. $code = substr($code, 1);
  752. } else {
  753. $regex = '#^/((\\\\.)+|[^\\\\/]+)*(/|$)#sD';
  754. }
  755. } elseif ($token == "url(") {
  756. $regex = '#^url\(((\\\\.)+|[^\\\\\\)]+)*(\)|$)#sD';
  757. } elseif ($token == "//") {
  758. $regex = '#^//.*(\n|$)#D';
  759. } elseif ($token == "/*") {
  760. $regex = '#^.{2}.*?(\*/|$)#sD';
  761. } elseif ($token == "'") {
  762. $regex = '#^\'((\\\\.)+|[^\\\\\']+)*(\'|$)#sD';
  763. } elseif ($token == '"') {
  764. $regex = '#^"((\\\\.)+|[^\\\\"]+)*("|$)#sD';
  765. }
  766. $this->minifyCode($part, $buffer, $stack, $type);
  767. $output .= $buffer;
  768. $buffer = $part;
  769. if ($regex) {
  770. preg_match($regex, $code, $match);
  771. $code = substr($code, strlen($match[0]));
  772. $this->minifyLiteral($match[0], $buffer, $type);
  773. $output .= $buffer;
  774. $buffer = $match[0];
  775. } elseif (!$token) {
  776. $output .= $buffer;
  777. }
  778. } while (strlen($code));
  779. return $output;
  780. }
  781. /**
  782. * Takes a block of CSS or JS and reduces the number of characters
  783. *
  784. * @param string &$part The part of code to minify
  785. * @param string &$buffer A buffer containing the last code or literal encountered
  786. * @param array $stack A stack used to keep track of the nesting level of CSS
  787. * @param mixed $type The type of code, `'css'` or `'js'`
  788. * @return void
  789. */
  790. protected function minifyCode(&$part, &$buffer, &$stack, $type='js')
  791. {
  792. // This pulls in the end of the last match for useful context
  793. $end_buffer = substr($buffer, -1);
  794. $lookbehind = in_array($end_buffer, array(' ', "\n")) ? substr($buffer, -2) : $end_buffer;
  795. $buffer = substr($buffer, 0, 0-strlen($lookbehind));
  796. $part = $lookbehind . $part;
  797. if ($type == 'js') {
  798. // All whitespace and control characters are collapsed
  799. $part = preg_replace('#[\x00-\x09\x0B\x0C\x0E-\x20]+#', ' ', $part);
  800. $part = preg_replace('#[\n\r]+#', "\n", $part);
  801. // Whitespace is removed where not needed
  802. $part = preg_replace('#(?<![a-z0-9\x80-\xFF\\\\$_+\-])[ ]+#i', '', $part);
  803. $part = preg_replace('#[ ]+(?![a-z0-9\x80-\xFF\\\\$_+\-])#i', '', $part);
  804. $part = preg_replace('#(?<![a-z0-9\x80-\xFF\\\\$_}\\])"\'+-])\n+#i', '', $part);
  805. $part = preg_replace('#\n+(?![a-z0-9\x80-\xFF\\\\$_{[(+-])#i', '', $part);
  806. // Remove spaces around + and - unless they are followed by a plus or minus
  807. $part = preg_replace('#(?<=[+-])[ ]+(?![+\-])#i', '', $part);
  808. $part = preg_replace('#(?<![+-])[ ]+(?=[+\-])#i', '', $part);
  809. } elseif ($type == 'css') {
  810. // All whitespace is collapsed
  811. $part = preg_replace('#\s+#', ' ', $part);
  812. // Whitespace is removed where not needed
  813. $part = preg_replace('#\s*([;{},>+])\s*#', '\1', $part);
  814. // This keeps track of the current scope since some minification
  815. // rules are different if inside or outside of a rule block
  816. $new_part = '';
  817. do {
  818. if (!preg_match('#@media|\{|\}#', $part, $match)) {
  819. $chunk = $part;
  820. $part = '';
  821. $token = NULL;
  822. } else {
  823. $token = $match[0];
  824. $pos = strpos($part, $token);
  825. if ($pos === FALSE) {
  826. break;
  827. }
  828. $chunk = substr($part, 0, $pos+strlen($token));
  829. $part = substr($part, $pos+strlen($token));
  830. }
  831. if (end($stack) == 'rule_block') {
  832. // Colons don't need space inside of a block
  833. $chunk = preg_replace('#\s*:\s*#', ':', $chunk);
  834. // Useless semi-colons are removed
  835. $chunk = str_replace(';}', '}', $chunk);
  836. // All zero units are reduces to just 0
  837. $chunk = preg_replace('#((?<!\d|\.|\#|\w)0+(\.0+)?|(?<!\d)\.0+)(?=\D|$)((%|in|cm|mm|em|ex|pt|pc|px)(\b|$))?#iD', '0', $chunk);
  838. // All .0 decimals are removed
  839. $chunk = preg_replace('#(\d+)\.0+(?=\D)#iD', '\1', $chunk);
  840. // All leading zeros are removed
  841. $chunk = preg_replace('#(?<!\d)0+(\.\d+)(?=\D)#iD', '\1', $chunk);
  842. // All measurements that contain the same value 4 times are reduced to a single
  843. $chunk = preg_replace('#(?<!\d|\.)([\d\.]+(?:%|in|cm|mm|em|ex|pt|pc|px))(\s*\1){3}#i', '\1', $chunk);
  844. // Hex color codes are reduced if possible
  845. $chunk = preg_replace('@#([a-f0-9])\1([a-f0-9])\2([a-f0-9])\3(?!\d)@iD', '#\1\2\3', $chunk);
  846. $chunk = str_ireplace('! important', '!important', $chunk);
  847. } else {
  848. // This handles an IE6 edge-case
  849. $chunk = preg_replace('#(:first-l(etter|ine))\{#', '\1 {', $chunk);
  850. }
  851. $new_part .= $chunk;
  852. if ($token == '@media') {
  853. $stack[] = 'media_rule';
  854. } elseif ($token == '{' && end($stack) == 'media_rule') {
  855. $stack = array('media_block');
  856. } elseif ($token == '{') {
  857. $stack[] = 'rule_block';
  858. } elseif ($token) {
  859. array_pop($stack);
  860. }
  861. } while (strlen($part));
  862. $part = $new_part;
  863. }
  864. }
  865. /**
  866. * Takes a literal and either discards or keeps it
  867. *
  868. * @param mixed &$part The literal to process
  869. * @param mixed &$buffer The last literal or code processed
  870. * @param string $type The language the literal is in, `'css'` or `'js'`
  871. * @return void
  872. */
  873. protected function minifyLiteral(&$part, &$buffer, $type)
  874. {
  875. // Comments are skipped unless they are special
  876. if (substr($part, 0, 2) == '/*' && substr($part, 0, 3) != '/*!') {
  877. $part = $buffer . ' ';
  878. $buffer = '';
  879. }
  880. if ($type == 'js' && substr($part, 0, 2) == '//') {
  881. $part = $buffer . "\n";
  882. $buffer = '';
  883. }
  884. }
  885. /**
  886. * Includes the element specified - element must be set through ::set() first
  887. *
  888. * If the element is a file path ending in `.css`, `.js`, `.rss` or `.xml`
  889. * an appropriate HTML tag will be printed (files ending in `.xml` will be
  890. * treated as an RSS feed). If the element is a file path ending in `.inc`,
  891. * `.php` or `.php5` it will be included.
  892. *
  893. * Paths that start with `./` will be loaded relative to the current script.
  894. * Paths that start with a file or directory name will be loaded relative
  895. * to the `$root` passed in the constructor. Paths that start with `/` will
  896. * be loaded from the root of the filesystem.
  897. *
  898. * You can pass the `media` attribute of a CSS file or the `title` attribute
  899. * of an RSS feed by adding an associative array with the following formats:
  900. *
  901. * {{{
  902. * array(
  903. * 'path' => (string) {css file path},
  904. * 'media' => (string) {media type}
  905. * );
  906. * array(
  907. * 'path' => (string) {rss file path},
  908. * 'title' => (string) {feed title}
  909. * );
  910. * }}}
  911. *
  912. * @param string $element The element to place
  913. * @param string $file_type Will force the element to be placed as this type of file instead of auto-detecting the file type. Valid types include: `'css'`, `'js'`, `'php'` and `'rss'`.
  914. * @return void
  915. */
  916. public function place($element='__main__', $file_type=NULL)
  917. {
  918. // Put in a buffered placeholder
  919. if ($this->buffered_id) {
  920. echo '%%fTemplating::' . $this->buffered_id . '::' . $element . '::' . $file_type . '%%';
  921. return;
  922. }
  923. if (!isset($this->elements[$element])) {
  924. return;
  925. }
  926. $this->placeElement($element, $file_type);
  927. }
  928. /**
  929. * Prints a CSS `link` HTML tag to the output
  930. *
  931. * @param mixed $info The path or array containing the `'path'` to the CSS file. Array can also contain a key `'media'`.
  932. * @return void
  933. */
  934. protected function placeCSS($info)
  935. {
  936. if (!is_array($info)) {
  937. $info = array('path' => $info);
  938. }
  939. if (!isset($info['media'])) {
  940. $info['media'] = 'all';
  941. }
  942. echo '<link rel="stylesheet" type="text/css" href="' . $info['path'] . '" media="' . $info['media'] . '" />' . "\n";
  943. }
  944. /**
  945. * Performs the action of actually placing an element
  946. *
  947. * @param string $element The element that is being placed
  948. * @param string $file_type The file type to treat all values as
  949. * @return void
  950. */
  951. protected function placeElement($element, $file_type)
  952. {
  953. $values = $this->elements[$element];
  954. if (!is_object($values)) {
  955. settype($values, 'array');
  956. } else {
  957. $values = array($values);
  958. }
  959. $values = array_values($values);
  960. $value_groups = array();
  961. $last_type = NULL;
  962. $last_location = NULL;
  963. $last_media = NULL;
  964. foreach ($values as $i => $value) {
  965. $type = $this->verifyValue($element, $value, $file_type);
  966. $media = is_array($value) && isset($value['media']) ? $value['media'] : NULL;
  967. $path = is_array($value) ? $value['path'] : $value;
  968. $location = is_string($path) && preg_match('#^https?://#', $path) ? 'http' : 'local';
  969. if ($type != $last_type || $location != $last_location || $media != $last_media) {
  970. $value_groups[] = array(
  971. 'type' => $type,
  972. 'location' => $location,
  973. 'values' => array()
  974. );
  975. }
  976. $value_groups[count($value_groups)-1]['values'][] = $value;
  977. $last_type = $type;
  978. $last_location = $location;
  979. $last_media = $media;
  980. }
  981. foreach ($value_groups as $value_group) {
  982. if ($value_group['location'] == 'local') {
  983. if ($this->minification_directory && in_array($value_group['type'], array('js', 'css'))) {
  984. $this->handleMinified($value_group['type'], $element, $value_group['values']);
  985. continue;
  986. }
  987. if ($this->short_tag_directory && $value_group['type'] == 'php') {
  988. $value_group['values'] = $this->fixShortTags($value_group['values']);
  989. }
  990. }
  991. foreach ($value_group['values'] as $value) {
  992. switch ($value_group['type']) {
  993. case 'css':
  994. $this->placeCSS($value);
  995. break;
  996. case 'fTemplating':
  997. // This causes children to inherit settings if they aren't already set
  998. if ($value->minification_directory === NULL) {
  999. $value->minification_directory = $this->minification_directory;
  1000. $value->minification_mode = $this->minification_mode;
  1001. $value->minification_prefix = $this->minification_prefix;
  1002. }
  1003. if ($value->short_tag_directory === NULL) {
  1004. $value->short_tag_directory = $this->short_tag_directory;
  1005. $value->short_tag_mode = $this->short_tag_mode;
  1006. }
  1007. $value->place();
  1008. break;
  1009. case 'js':
  1010. $this->placeJS($value);
  1011. break;
  1012. case 'php':
  1013. $this->placePHP($element, $value);
  1014. break;
  1015. case 'rss':
  1016. $this->placeRSS($value);
  1017. break;
  1018. default:
  1019. throw new fProgrammerException(
  1020. 'The file type specified, %1$s, is invalid. Must be one of: %2$s.',
  1021. $type,
  1022. 'css, js, php, rss'
  1023. );
  1024. }
  1025. }
  1026. }
  1027. }
  1028. /**
  1029. * Prints a java`script` HTML tag to the output
  1030. *
  1031. * @param mixed $info The path or array containing the `'path'` to the javascript file
  1032. * @return void
  1033. */
  1034. protected function placeJS($info)
  1035. {
  1036. if (!is_array($info)) {
  1037. $info = array('path' => $info);
  1038. }
  1039. echo '<script type="text/javascript" src="' . $info['path'] . '"></script>' . "\n";
  1040. }
  1041. /**
  1042. * Includes a PHP file
  1043. *
  1044. * @param string $element The element being placed
  1045. * @param string $path The path to the PHP file
  1046. * @return void
  1047. */
  1048. protected function placePHP($element, $path)
  1049. {
  1050. // Check to see if the element is a path relative to the template root
  1051. if (!preg_match('#^(/|\\\\|[a-z]:(\\\\|/)|\\\\|//|\./|\.\\\\)#i', $path)) {
  1052. $path = $this->root . $path;
  1053. }
  1054. if (!file_exists($path)) {
  1055. throw new fProgrammerException(
  1056. 'The path specified for %1$s, %2$s, does not exist on the filesystem',
  1057. $element,
  1058. $path
  1059. );
  1060. }
  1061. if (!is_readable($path)) {
  1062. throw new fEnvironmentException(
  1063. 'The path specified for %1$s, %2$s, is not readable',
  1064. $element,
  1065. $path
  1066. );
  1067. }
  1068. include($path);
  1069. }
  1070. /**
  1071. * Prints an RSS `link` HTML tag to the output
  1072. *
  1073. * @param mixed $info The path or array containing the `'path'` to the RSS xml file. May also contain a `'title'` key for the title of the RSS feed.
  1074. * @return void
  1075. */
  1076. protected function placeRSS($info)
  1077. {
  1078. if (!is_array($info)) {
  1079. $info = array(
  1080. 'path' => $info,
  1081. 'title' => fGrammar::humanize(
  1082. preg_replace('#.*?([^/]+).(rss|xml)$#iD', '\1', $info)
  1083. )
  1084. );
  1085. }
  1086. if (!isset($info['title'])) {
  1087. throw new fProgrammerException(
  1088. 'The RSS value %s is missing the title key',
  1089. $info
  1090. );
  1091. }
  1092. echo '<link rel="alternate" type="application/rss+xml" href="' . $info['path'] . '" title="' . $info['title'] . '" />' . "\n";
  1093. }
  1094. /**
  1095. * Performs buffered replacements using a breadth-first technique
  1096. *
  1097. * @return void
  1098. */
  1099. private function placeBuffered()
  1100. {
  1101. if (!$this->buffered_id) {
  1102. return;
  1103. }
  1104. $contents = fBuffer::get();
  1105. fBuffer::erase();
  1106. // We are gonna use a regex replacement that is eval()'ed as PHP code
  1107. $regex = '/%%fTemplating::' . $this->buffered_id . '::(.*?)::(.*?)%%/';
  1108. // Remove the buffered id, thus making any nested place() calls be executed immediately
  1109. $this->buffered_id = NULL;
  1110. echo preg_replace_callback($regex, array($this, 'placeBufferedCallback'), $contents);
  1111. }
  1112. /**
  1113. * Performs a captured place of an element to use with buffer placing
  1114. *
  1115. * @param array $match A regex match from ::placeBuffered()
  1116. * @return string The output of placing the element
  1117. */
  1118. private function placeBufferedCallback($match)
  1119. {
  1120. fBuffer::startCapture();
  1121. $this->placeElement($match[1], $match[2]);
  1122. return fBuffer::stopCapture();
  1123. }
  1124. /**
  1125. * Gets the value of an element and runs it through fHTML::prepare()
  1126. *
  1127. * @param string $element The element to get - array elements can be access via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
  1128. * @param mixed $default_value The value to return if the element has not been set
  1129. * @return mixed The value of the element specified run through fHTML::prepare(), or the default value if it has not been set
  1130. */
  1131. public function prepare($element, $default_value=NULL)
  1132. {
  1133. return fHTML::prepare($this->get($element, $default_value));
  1134. }
  1135. /**
  1136. * Removes and returns the value from the end of an array element
  1137. *
  1138. * @param string $element The element to remove from to - array elements can be modified via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
  1139. * @param boolean $beginning If the value should be removed from the beginning of the element
  1140. * @return mixed The value that was removed
  1141. */
  1142. public function remove($element, $beginning=FALSE)
  1143. {
  1144. $tip =& $this->elements;
  1145. if ($bracket_pos = strpos($element, '[')) {
  1146. $original_element = $element;
  1147. $array_dereference = substr($element, $bracket_pos);
  1148. $element = substr($element, 0, $bracket_pos);
  1149. preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
  1150. $array_keys = array_map('current', $array_keys);
  1151. array_unshift($array_keys, $element);
  1152. foreach (array_slice($array_keys, 0, -1) as $array_key) {
  1153. if (!isset($tip[$array_key])) {
  1154. return NULL;
  1155. } elseif (!is_array($tip[$array_key])) {
  1156. throw new fProgrammerException(
  1157. '%1$s was called for an element, %2$s, which is not an array',
  1158. 'remove()',
  1159. $original_element
  1160. );
  1161. }
  1162. $tip =& $tip[$array_key];
  1163. }
  1164. $element = end($array_keys);
  1165. }
  1166. if (!isset($tip[$element])) {
  1167. return NULL;
  1168. } elseif (!is_array($tip[$element])) {
  1169. throw new fProgrammerException(
  1170. '%1$s was called for an element, %2$s, which is not an array',
  1171. 'remove()',
  1172. $element
  1173. );
  1174. }
  1175. if ($beginning) {
  1176. return array_shift($tip[$element]);
  1177. }
  1178. return array_pop($tip[$element]);
  1179. }
  1180. /**
  1181. * Sets the value for an element
  1182. *
  1183. * @param string $element The element to set - the magic element `__main__` is used for placing the current fTemplating object as a child of another fTemplating object - array elements can be modified via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
  1184. * @param mixed $value The value for the element
  1185. * @param array :$elements An associative array with the key being the `$element` to set and the value being the `$value` for that element
  1186. * @return fTemplating The template object, to allow for method chaining
  1187. */
  1188. public function set($element, $value=NULL)
  1189. {
  1190. if ($value === NULL && is_array($element)) {
  1191. foreach ($element as $key => $value) {
  1192. $this->set($key, $value);
  1193. }
  1194. return $this;
  1195. }
  1196. $tip =& $this->elements;
  1197. if ($bracket_pos = strpos($element, '[')) {
  1198. $array_dereference = substr($element, $bracket_pos);
  1199. $element = substr($element, 0, $bracket_pos);
  1200. preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
  1201. $array_keys = array_map('current', $array_keys);
  1202. array_unshift($array_keys, $element);
  1203. foreach (array_slice($array_keys, 0, -1) as $array_key) {
  1204. if (!isset($tip[$array_key]) || !is_array($tip[$array_key])) {
  1205. $tip[$array_key] = array();
  1206. }
  1207. $tip =& $tip[$array_key];
  1208. }
  1209. $element = end($array_keys);
  1210. }
  1211. $tip[$element] = $value;
  1212. return $this;
  1213. }
  1214. /**
  1215. * Ensures the value is valid
  1216. *
  1217. * @param string $element The element that is being placed
  1218. * @param mixed $value A value to be placed
  1219. * @param string $file_type The file type that this element will be displayed as - skips checking file extension
  1220. * @return string The file type of the value being placed
  1221. */
  1222. protected function verifyValue($element, $value, $file_type=NULL)
  1223. {
  1224. if (!$value && !is_numeric($value)) {
  1225. throw new fProgrammerException(
  1226. 'The element specified, %s, has a value that is empty',
  1227. $value
  1228. );
  1229. }
  1230. if (is_array($value) && !isset($value['path'])) {
  1231. throw new fProgrammerException(
  1232. 'The element specified, %1$s, has a value, %2$s, that is missing the path key',
  1233. $element,
  1234. $value
  1235. );
  1236. }
  1237. if ($file_type) {
  1238. return $file_type;
  1239. }
  1240. if ($value instanceof self) {
  1241. return 'fTemplating';
  1242. }
  1243. $path = (is_array($value)) ? $value['path'] : $value;
  1244. $path = preg_replace('#\?.*$#D', '', $path);
  1245. $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
  1246. // Allow some common variations on file extensions
  1247. $extension_map = array(
  1248. 'inc' => 'php',
  1249. 'php5' => 'php',
  1250. 'xml' => 'rss'
  1251. );
  1252. if (isset($extension_map[$extension])) {
  1253. $extension = $extension_map[$extension];
  1254. }
  1255. if (!in_array($extension, array('css', 'js', 'php', 'rss'))) {
  1256. throw new fProgrammerException(
  1257. 'The element specified, %1$s, has a value whose path, %2$s, does not end with a recognized file extension: %3$s.',
  1258. $element,
  1259. $path,
  1260. '.css, .inc, .js, .php, .php5, .rss, .xml'
  1261. );
  1262. }
  1263. return $extension;
  1264. }
  1265. }
  1266. /**
  1267. * Copyright (c) 2007-2011 Will Bond <will@flourishlib.com>, others
  1268. *
  1269. * Permission is hereby granted, free of charge, to any person obtaining a copy
  1270. * of this software and associated documentation files (the "Software"), to deal
  1271. * in the Software without restriction, including without limitation the rights
  1272. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  1273. * copies of the Software, and to permit persons to whom the Software is
  1274. * furnished to do so, subject to the following conditions:
  1275. *
  1276. * The above copyright notice and this permission notice shall be included in
  1277. * all copies or substantial portions of the Software.
  1278. *
  1279. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  1280. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  1281. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  1282. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  1283. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  1284. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  1285. * THE SOFTWARE.
  1286. */