PageRenderTime 52ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/system/cms/libraries/Asset.php

https://bitbucket.org/viktorfabry/banditos
PHP | 1635 lines | 991 code | 176 blank | 468 comment | 74 complexity | f196d4ed49aea5819b800ff5edac9879 MD5 | raw file
Possible License(s): CC-BY-3.0, BSD-3-Clause, MIT
  1. <?php
  2. /**
  3. * Asset: Convenient asset library
  4. *
  5. * This was a FuelPHP package converted for use within PyroCMS by Phil Sturgeon with
  6. * express written consent from Antony Male to be used within PyroCMS Community and Professional
  7. *
  8. * @version v1.11
  9. * @author Antony Male
  10. * @license MIT License
  11. * @copyright 2011 Antony Male
  12. * @author Phil Sturgeon
  13. * @package PyroCMS\Core\Libraries\Asset
  14. */
  15. class Asset_Exception extends Exception {}
  16. include(dirname(__FILE__).'/Asset/jsmin.php');
  17. include(dirname(__FILE__).'/Asset/csscompressor.php');
  18. include(dirname(__FILE__).'/Asset/cssurirewriter.php');
  19. class Asset {
  20. /**
  21. * @var array Array of paths in which the css, js, img directory structure
  22. * can be found, relative to $asset_url
  23. */
  24. protected static $asset_paths = array(
  25. 'core' => 'assets/',
  26. );
  27. /*
  28. * @var string The key in $asset_paths to use if no key is given
  29. */
  30. protected static $default_path_key = 'core';
  31. /**
  32. * @var string The URL to be prepanded to all assets.
  33. */
  34. protected static $asset_url = null;
  35. /**
  36. * @var array The folders in which css, js, and images can be found.
  37. */
  38. protected static $default_folders = array(
  39. 'css' => 'css/',
  40. 'js' => 'js/',
  41. 'img' => 'img/',
  42. );
  43. /**
  44. * @var string The directory, relative to public/, where cached minified failes
  45. * are stored.
  46. */
  47. protected static $cache_path = 'assets/cache/';
  48. /**
  49. * @var array Holds groups of assets. Is documenented fully in the config file.
  50. */
  51. protected static $groups = array(
  52. 'css' => array(),
  53. 'js' => array(),
  54. );
  55. /**
  56. * @var array Holds inline js and css.
  57. */
  58. protected static $inline_assets = array(
  59. 'css' => array(),
  60. 'js' => array(),
  61. );
  62. /**
  63. *
  64. * @var array Defaults for a group
  65. */
  66. protected static $default_options = array(
  67. 'enabled' => true,
  68. 'combine' => true,
  69. 'min' => true,
  70. 'inline' => false,
  71. 'attr' => array(),
  72. 'deps' => array(),
  73. );
  74. /**
  75. * @var int How deep to go when resolving deps
  76. */
  77. protected static $deps_max_depth = 5;
  78. /**
  79. * @var bool Whether to show comments above the <script>/<link> tags showing
  80. * which files have been minified into that file.
  81. */
  82. protected static $show_files = false;
  83. /**
  84. * @var bool Whether to show comments inside minified files showing which
  85. * original file is where.
  86. */
  87. protected static $show_files_inline = false;
  88. /**
  89. * @var string If given, the name of the function to call when we have
  90. * read a file, before minifying. Note: It is only called if $combine
  91. * for the file is `true`.
  92. * Prototype: callback(content, filename, type, group_name);
  93. */
  94. protected static $post_load_callback = null;
  95. /*
  96. * @var function If given, the function to call when we've decided on the name
  97. * for a file, but want to allow the user to tweak it before we
  98. * write it to the page.
  99. * Prototype: callback($filepath, $type, $remote);
  100. */
  101. protected static $filepath_callback = null;
  102. /**
  103. * @var array Keeps a record of which groups have been rendered.
  104. * We then check this when deciding whether to render a dep.
  105. */
  106. protected static $rendered_groups = array('js' => array(), 'css' => array());
  107. /**
  108. * @var array Symlink-ed directories and their targets. Since the paths to assets and
  109. * paths inside the assets get rewritten, we have to provide the symlink-ed directories
  110. * and their targets
  111. */
  112. protected static $symlinks = array();
  113. /**
  114. * Loads in the config and sets the variables
  115. */
  116. public function __construct()
  117. {
  118. $ci = get_instance();
  119. $ci->config->load('asset');
  120. $paths = $ci->config->item('asset_paths') ? $ci->config->item('asset_paths') : self::$asset_paths;
  121. self::$symlinks = $ci->config->item('asset_symlinks') ? $ci->config->item('asset_symlinks') : array();
  122. foreach ($paths as $key => $path)
  123. {
  124. self::add_path($key, $path);
  125. }
  126. self::$asset_url = $ci->config->item('asset_url') ? $ci->config->item('asset_url') : $ci->config->item('base_url');
  127. self::$default_folders = array(
  128. 'css' => $ci->config->item('asset_css_dir'),
  129. 'js' => $ci->config->item('asset_js_dir'),
  130. 'img' => $ci->config->item('asset_img_dir'),
  131. );
  132. is_null($ci->config->item('asset_cache_path')) or self::$cache_path = $ci->config->item('asset_cache_path');
  133. is_null($ci->config->item('asset_min')) or self::$default_options['min'] = $ci->config->item('asset_min');
  134. is_null($ci->config->item('asset_combine')) or self::$default_options['combine'] = $ci->config->item('asset_combine');
  135. is_null($ci->config->item('asset_deps_max_depth')) or self::$deps_max_depth = $ci->config->item('asset_deps_max_depth');
  136. $group_sets = $ci->config->item('asset_groups') ? $ci->config->item('asset_groups') : array();
  137. foreach ($group_sets as $group_type => $groups)
  138. {
  139. foreach ($groups as $group_name => $group)
  140. {
  141. $options = self::prep_new_group_options($group);
  142. self::add_group($group_type, $group_name, $group['files'], $options);
  143. }
  144. }
  145. // Add the global group if it doesn't already exist.
  146. // This is so that set_group_option() can be used on it. This function will
  147. // throw an exception if the named group doesn't exist.
  148. if (!self::group_exists('js', 'global'))
  149. {
  150. self::add_group_base('js', 'global');
  151. }
  152. if (!self::group_exists('css', 'global'))
  153. {
  154. self::add_group_base('css', 'global');
  155. }
  156. is_null($ci->config->item('asset_show_files')) or self::$show_files = $ci->config->item('asset_show_files');
  157. is_null($ci->config->item('asset_show_files_inline')) or self::$show_files_inline = $ci->config->item('asset_show_files_inline');
  158. is_null($ci->config->item('asset_post_load_callback')) or self::$post_load_callback = $ci->config->item('asset_post_load_callback');
  159. is_null($ci->config->item('asset_filepath_callback')) or self::$filepath_callback = $ci->config->item('asset_filepath_callback');
  160. }
  161. /**
  162. * Sets up options for new groups setup via casset/config.php.
  163. * Abstracts away from _init method. Also easier if options are
  164. * added in future as iterates through defaults to do checking.
  165. *
  166. * @param array $group_options Options as defined in group in config.php
  167. *
  168. * @return array
  169. */
  170. protected static function prep_new_group_options($group_options)
  171. {
  172. $options = array();
  173. foreach (self::$default_options as $key => $option_val)
  174. {
  175. if (array_key_exists($key, $group_options))
  176. {
  177. $options[$key] = $group_options[$key];
  178. }
  179. }
  180. return $options;
  181. }
  182. /**
  183. * Parses one of the 'paths' config keys into the format used internally.
  184. * Config file format:
  185. * <code>
  186. * 'paths' => array(
  187. * 'assets/',
  188. * array(
  189. * 'path' => 'assets_2/',
  190. * 'js_dir' => 'js/',
  191. * 'css_dir' => 'css/',
  192. * ),
  193. * ),
  194. * </code>
  195. * In the event that the value is not an array, it is turned into one.
  196. * If js_dir, css_dir or img_dir are not given, they are populated with
  197. * the defaults, giving in the 'js_dir', 'css_dir' and 'img_dir' config keys.
  198. *
  199. * @param string $path_key The key of the path
  200. * @param mixed $path_attr The path attributes, as described above
  201. */
  202. public static function add_path($path_key, $path_attr)
  203. {
  204. $path_val = array();
  205. if (!is_array($path_attr))
  206. {
  207. $path_attr = array('path' => $path_attr, 'dirs' => array());
  208. }
  209. elseif (!array_key_exists('dirs', $path_attr))
  210. {
  211. $path_attr['dirs'] = array();
  212. }
  213. $path_val['path'] = $path_attr['path'];
  214. $path_val['dirs'] = array(
  215. 'js' => array_key_exists('js_dir', $path_attr) ? $path_attr['js_dir'] : self::$default_folders['js'],
  216. 'css' => array_key_exists('css_dir', $path_attr) ? $path_attr['css_dir'] : self::$default_folders['css'],
  217. 'img' => array_key_exists('img_dir', $path_attr) ? $path_attr['img_dir'] : self::$default_folders['img'],
  218. );
  219. self::$asset_paths[$path_key] = $path_val;
  220. }
  221. /**
  222. * Set the current default path
  223. *
  224. * @param string $path_key The path key to set the default to.
  225. *
  226. * @throws Asset_Exception
  227. */
  228. public static function set_path($path_key = 'core')
  229. {
  230. if (!array_key_exists($path_key, self::$asset_paths))
  231. {
  232. throw new Asset_Exception("Asset path key $path_key doesn't exist");
  233. }
  234. self::$default_path_key = $path_key;
  235. }
  236. /**
  237. * Set the asset_url, to allow for CDN url's and such
  238. *
  239. * @param string $url The url to use.
  240. */
  241. public static function set_url($url)
  242. {
  243. self::$asset_url = $url;
  244. }
  245. /**
  246. * Adds a group of assets. If a group of this name exists, the function returns.
  247. *
  248. * The options array should be like:
  249. * <code>
  250. * array(
  251. * 'enabled' => true|false,
  252. * 'combine' => true|false,
  253. * 'min' => true|false,
  254. * 'inline' => true|false,
  255. * 'deps' => array(),
  256. * );
  257. * </code>
  258. *
  259. * @param string $group_type 'js' or 'css'
  260. * @param string $group_name The name of the group
  261. * @param array $options An array of options.
  262. *
  263. * @throws Asset_Exception
  264. */
  265. protected static function add_group_base($group_type, $group_name, $options = array())
  266. {
  267. // Insert defaults
  268. $options += self::$default_options;
  269. if (!is_array($options['deps']))
  270. {
  271. $options['deps'] = array($options['deps']);
  272. }
  273. $options['files'] = array();
  274. // If it already exists, do not overwrite it.
  275. if (array_key_exists($group_name, self::$groups[$group_type]))
  276. {
  277. throw new Asset_Exception("Group $group_name already exists: can't create it.");
  278. }
  279. self::$groups[$group_type][$group_name] = $options;
  280. }
  281. /**
  282. * Adds a group for assets, and adds assets to that group.
  283. *
  284. * The options array should be like:
  285. * <code>
  286. * array(
  287. * 'enabled' => true|false,
  288. * 'combine' => true|false,
  289. * 'min' => true|false,
  290. * 'inline' => true|false,
  291. * 'deps' => array(),
  292. * );
  293. * </code>
  294. * To maintain backwards compatibility, you can also pass $enabled here instead of an array of options..
  295. *
  296. *
  297. * @param string $group_type 'js' or 'css'
  298. * @param string $group_name The name of the group
  299. * @param array $files
  300. * @param array $options An array of options.
  301. * @param null|bool $combine_dep @deprecated Whether to combine files in this group. Default (null) means use config setting
  302. * @param null|bool $min_dep @deprecated Whether to minify files in this group. Default (null) means use config setting
  303. */
  304. public static function add_group($group_type, $group_name, $files, $options = array(), $combine_dep = null, $min_dep = null)
  305. {
  306. // Bit of backwards compatibity.
  307. // Order used to be add_group(group_type, group_name, files, enabled, combine, min)
  308. if (!is_array($options))
  309. {
  310. $options = array(
  311. 'enabled' => $options,
  312. 'combine' => $combine_dep,
  313. 'min' => $min_dep,
  314. );
  315. }
  316. // We are basically faking the old add_group.
  317. // However, the approach has changed since those days
  318. // Therefore we create the group if it does not already
  319. // exist, then add the files to it.
  320. self::add_group_base($group_type, $group_name, $options);
  321. foreach ($files as $file)
  322. {
  323. if (!is_array($file))
  324. {
  325. $file = array($file, false);
  326. }
  327. self::add_asset($group_type, $file[0], $file[1], $group_name);
  328. }
  329. }
  330. /**
  331. * Returns true if the given group exists
  332. *
  333. * @param string $group_type 'js' or 'css'
  334. * @param string $group_name The name of the group
  335. *
  336. * @return bool
  337. */
  338. public static function group_exists($group_type, $group_name)
  339. {
  340. return array_key_exists($group_name, self::$groups[$group_type]);
  341. }
  342. /**
  343. * Enables both js and css groups of the given name.
  344. *
  345. * @param string|array $groups The group to enable, or array of groups
  346. */
  347. public static function enable($groups)
  348. {
  349. self::asset_enabled('js', $groups, true);
  350. self::asset_enabled('css', $groups, true);
  351. }
  352. /**
  353. * Disables both js and css groups of the given name.
  354. *
  355. * @param string|array $groups The group to disable, or array of groups
  356. */
  357. public static function disable($groups)
  358. {
  359. self::asset_enabled('js', $groups, false);
  360. self::asset_enabled('css', $groups, false);
  361. }
  362. /**
  363. * Enable a group of javascript assets.
  364. *
  365. * @param string|array $groups The group to enable, or array of groups
  366. */
  367. public static function enable_js($groups)
  368. {
  369. self::asset_enabled('js', $groups, true);
  370. }
  371. /**
  372. * Disable a group of javascript assets.
  373. *
  374. * @param string|array $groups The group to disable, or array of groups
  375. */
  376. public static function disable_js($groups)
  377. {
  378. self::asset_enabled('js', $groups, false);
  379. }
  380. /**
  381. * Enable a group of css assets.
  382. *
  383. * @param string|array $groups The group to enable, or array of groups
  384. */
  385. public static function enable_css($groups)
  386. {
  387. self::asset_enabled('css', $groups, true);
  388. }
  389. /**
  390. * Disable a group of css assets.
  391. *
  392. * @param string|array $groups The group to disable, or array of groups
  393. */
  394. public static function disable_css($groups)
  395. {
  396. self::asset_enabled('css', $groups, false);
  397. }
  398. /**
  399. * Enables or disables an asset.
  400. *
  401. * @param string $type 'css' or 'js'.
  402. * @param string|array $groups The group to enable/disable, or array of groups.
  403. * @param bool $enabled True to enable the group, false to disable.
  404. */
  405. protected static function asset_enabled($type, $groups, $enabled)
  406. {
  407. if (!is_array($groups))
  408. {
  409. $groups = array($groups);
  410. }
  411. foreach ($groups as $group)
  412. {
  413. // If the group does not exist it is of no consequence.
  414. if (!array_key_exists($group, self::$groups[$type]))
  415. {
  416. continue;
  417. }
  418. self::$groups[$type][$group]['enabled'] = $enabled;
  419. }
  420. }
  421. /**
  422. * Set group options on-the-fly.
  423. *
  424. * @param string $type 'css' or 'js'.
  425. * @param string|array $group_names Group name to change, or array of groups to change,
  426. * or '' for global group, or '*' for all groups.
  427. * @param string $option_key The name of the option to change.
  428. * @param mixed $option_value What to set the option to.
  429. *
  430. * @throws Asset_Exception
  431. */
  432. public static function set_group_option($type, $group_names, $option_key, $option_value)
  433. {
  434. if ($group_names == '')
  435. {
  436. $group_names = array('global');
  437. }
  438. else
  439. {
  440. if ($group_names == '*')
  441. {
  442. // Change the default
  443. self::$default_options[$option_key] = $option_value;
  444. $group_names = array_keys(self::$groups[$type]);
  445. }
  446. else
  447. {
  448. if (!is_array($group_names))
  449. {
  450. $group_names = array($group_names);
  451. }
  452. }
  453. }
  454. // Allow them to specify a single string dep
  455. if ($option_key == 'deps' && !is_array($option_value))
  456. {
  457. $option_value = array($option_value);
  458. }
  459. foreach ($group_names as $group_name)
  460. {
  461. // If the group doesn't exist, throw a fuss
  462. if (!self::group_exists($type, $group_name))
  463. {
  464. throw new Asset_Exception("Can't set option for group '$group_name' ($type), as it doesn't exist.");
  465. }
  466. self::$groups[$type][$group_name][$option_key] = $option_value;
  467. }
  468. }
  469. /**
  470. * Set group options on-the-fly, js version
  471. *
  472. * @param string|array $group_names Group name to change, or array of groups to change,
  473. * or '' for global group, or '*' for all groups.
  474. * @param string $option_key The name of the option to change.
  475. * @param mixed $option_value What to set the option to.
  476. */
  477. public static function set_js_option($group_names, $option_key, $option_value)
  478. {
  479. self::set_group_option('js', $group_names, $option_key, $option_value);
  480. }
  481. /**
  482. * Set group options on-the-fly, css version
  483. *
  484. * @param mixed $group_names Group name to change, or array of groups to change,
  485. * or '' for global group, or '*' for all groups.
  486. * @param string $option_key The name of the option to change
  487. * @param mixed $option_value What to set the option to
  488. */
  489. public static function set_css_option($group_names, $option_key, $option_value)
  490. {
  491. self::set_group_option('css', $group_names, $option_key, $option_value);
  492. }
  493. /**
  494. * Add a javascript asset.
  495. *
  496. * @param string|array $script The script to add.
  497. * @param bool $script_min If given, will be used when $min = true
  498. * If omitted, $script will be minified internally.
  499. * @param string $group The group to add this asset to. Defaults to 'global'
  500. *
  501. * @return mixed
  502. */
  503. public static function js($script, $script_min = false, $group = 'global')
  504. {
  505. if (is_array($script))
  506. {
  507. foreach ($script as $each)
  508. {
  509. self::add_asset('js', $each, $script_min, $group);
  510. }
  511. return;
  512. }
  513. self::add_asset('js', $script, $script_min, $group);
  514. }
  515. /**
  516. * Add a css asset.
  517. *
  518. * @param string|array $sheet The script to add
  519. * @param bool $sheet_min If given, will be used when $min = true
  520. * If omitted, $script will be minified internally.
  521. * @param string $group The group to add this asset to. Defaults to 'global'
  522. * @return mixed
  523. */
  524. public static function css($sheet, $sheet_min = false, $group = 'global')
  525. {
  526. if (is_array($sheet))
  527. {
  528. foreach ($sheet as $each)
  529. {
  530. self::add_asset('css', $each, $sheet_min, $group);
  531. }
  532. return;
  533. }
  534. self::add_asset('css', $sheet, $sheet_min, $group);
  535. }
  536. /**
  537. * Abstraction of js() and css().
  538. *
  539. * @param string $type 'css' or 'js'.
  540. * @param string $script The script to add.
  541. * @param bool $script_min If given, will be used when $min = true
  542. * If omitted, $script will be minified internally.
  543. * @param string $group The group to add this asset to
  544. *
  545. * @return mixed
  546. */
  547. protected static function add_asset($type, $script, $script_min, $group)
  548. {
  549. // Allow the user to specify any non-string value for an asset, and it
  550. // will be ignore. This can be handy when using ternary operators
  551. // in the groups config.
  552. if (!is_string($script))
  553. {
  554. return;
  555. }
  556. // Do not force the user to remember that 'false' is used when not supplying
  557. // a pre-minified file.
  558. if (!is_string($script_min))
  559. {
  560. $script_min = false;
  561. }
  562. $files = array($script, $script_min);
  563. // If a path key has not been specified, add $default_path_key
  564. foreach ($files as &$file)
  565. {
  566. if ($file != false && strpos($file, '::') === false)
  567. {
  568. $file = self::$default_path_key . '::' . $file;
  569. }
  570. }
  571. if (!array_key_exists($group, self::$groups[$type]))
  572. {
  573. // Assume they want the group enabled
  574. self::add_group_base($type, $group);
  575. }
  576. array_push(self::$groups[$type][$group]['files'], $files);
  577. }
  578. /**
  579. * Add a string containing javascript, which can be printed inline with
  580. * js_render_inline().
  581. *
  582. * @param string $content The javascript to add.
  583. */
  584. public static function js_inline($content)
  585. {
  586. self::add_asset_inline('js', $content);
  587. }
  588. /**
  589. * Add a string containing css, which can be printed inline with
  590. * render_css_inline().
  591. *
  592. * @param string $content The css to add.
  593. */
  594. public static function css_inline($content)
  595. {
  596. self::add_asset_inline('css', $content);
  597. }
  598. /**
  599. * Abstraction of js_inline() and css_inline().
  600. *
  601. * @param string $type 'css' or 'js'.
  602. * @param string $content The css or js to add.
  603. */
  604. protected static function add_asset_inline($type, $content)
  605. {
  606. array_push(self::$inline_assets[$type], $content);
  607. }
  608. /**
  609. * Return the path for the given JS asset. Ties into find_files, so supports
  610. * everything that, say, Casset::js() does.
  611. * Throws an exception if the file isn't found.
  612. *
  613. * @param string $filename the name of the asset to find
  614. * @param bool $add_url whether to add the 'url' config key to the filename
  615. * @param bool $force_array By default, when one file is found a string is
  616. * returned. Setting this to true causes a single-element array to be returned.
  617. *
  618. * @return string
  619. */
  620. public static function get_filepath_js($filename, $add_url = false, $force_array = false)
  621. {
  622. return self::get_filepath($filename, 'js', $add_url, $force_array);
  623. }
  624. /**
  625. * Return the path for the given CSS asset. Ties into find_files, so supports
  626. * everything that, say, Casset::js() does.
  627. * Throws an exception if the file isn't found.
  628. *
  629. * @param string $filename the name of the asset to find.
  630. * @param bool $add_url whether to add the 'url' config key to the filename.
  631. * @param bool $force_array By default, when one file is found a string is
  632. * returned. Setting this to true causes a single-element array to be returned.
  633. *
  634. * @return string
  635. */
  636. public static function get_filepath_css($filename, $add_url = false, $force_array = false)
  637. {
  638. return self::get_filepath($filename, 'css', $add_url, $force_array);
  639. }
  640. /**
  641. * Return the path for the given img asset. Ties into find_files, so supports
  642. * everything that, say, Casset::js() does.
  643. * Throws an exception if the file isn't found.
  644. *
  645. * @param string $filename the name of the asset to find.
  646. * @param bool $add_url whether to add the 'url' config key to the filename.
  647. * @param bool $force_array By default, when one file is found a string is
  648. * returned. Setting this to true causes a single-element array to be returned.
  649. *
  650. * @return string
  651. */
  652. public static function get_filepath_img($filename, $add_url = false, $force_array = false)
  653. {
  654. return self::get_filepath($filename, 'img', $add_url, $force_array);
  655. }
  656. /**
  657. * Return the path for the given asset. Ties into find_files, so supports
  658. * everything that, say, Casset::js() does.
  659. * Throws an exception if the file isn't found.
  660. *
  661. * @param string $filename the name of the asset to find.
  662. * @param string $type 'js', 'css' or 'img'.
  663. * @param bool $add_url Whether to add the 'url' config key to the filename
  664. * @param bool $force_array By default, when one file is found a string is
  665. * returned. Setting this to true causes a single-element array to be returned.
  666. *
  667. * @return string
  668. */
  669. public static function get_filepath($filename, $type, $add_url = false, $force_array = false)
  670. {
  671. if (strpos($filename, '::') === false)
  672. {
  673. $filename = self::$default_path_key . '::' . $filename;
  674. }
  675. $files = self::find_files($filename, $type);
  676. foreach ($files as &$file)
  677. {
  678. $remote = (strpos($file, '//') !== false);
  679. $file = self::process_filepath($file, $type, $remote);
  680. if ($remote)
  681. {
  682. continue;
  683. }
  684. if ($add_url)
  685. {
  686. $file = self::$asset_url . $file;
  687. }
  688. }
  689. if (count($files) == 1 && !$force_array)
  690. {
  691. return $files[0];
  692. }
  693. return $files;
  694. }
  695. /**
  696. * Can be used to add deps to a group.
  697. *
  698. * @param string $type 'css' or 'js'.
  699. * @param string $group The group name to add deps to.
  700. * @param array $deps An array of group names to add as deps.
  701. *
  702. * @throws Exception
  703. */
  704. public static function add_deps($type, $group, $deps)
  705. {
  706. if (!is_array($deps))
  707. {
  708. $deps = array($deps);
  709. }
  710. if (!array_key_exists($group, self::$groups[$type]))
  711. {
  712. throw new Exception("Group $group ($type) doesn't exist, so can't add deps to it.");
  713. }
  714. array_push(self::$groups[$type][$group]['deps'], $deps);
  715. }
  716. /**
  717. * Sugar for add_deps(), for js groups
  718. * @param string $group The group name to add deps to
  719. * @param array $deps An array of group names to add as deps.
  720. */
  721. public static function add_js_deps($group, $deps)
  722. {
  723. self::add_deps('js', $group, $deps);
  724. }
  725. /**
  726. * Sugar for add_deps(), for css groups
  727. * @param string $group The group name to add deps to
  728. * @param array $deps An array of group names to add as deps.
  729. */
  730. public static function add_css_deps($group, $deps)
  731. {
  732. self::add_deps('css', $group, $deps);
  733. }
  734. /**
  735. * Sticks the given filename through the filepath callback, if given.
  736. *
  737. * @param string $filepath The filepath to process
  738. * @param string $type The type of asset, passed to the callback
  739. * @param bool $remote Whether the asset is on another machine, passed to the callback
  740. */
  741. protected static function process_filepath($filepath, $type, $remote = null)
  742. {
  743. if (self::$filepath_callback)
  744. {
  745. if ($remote === null)
  746. $remote = (strpos($filepath, '//') !== false);
  747. $func = self::$filepath_callback;
  748. $filepath = $func($filepath, $type, $remote);
  749. }
  750. return $filepath;
  751. }
  752. /**
  753. * Shortcut to render_js() and render_css().
  754. *
  755. * @param bool|string $group Which group to render. If omitted renders all groups
  756. * @param bool|null $inline_dep @deprecated If true, the result is printed inline.
  757. * If false, is written to a file and linked to. In fact, $inline = true also
  758. * causes a cache file to be written for speed purposes.
  759. * @param string|array $attr The javascript tags to be written to the page
  760. *
  761. * @return string
  762. */
  763. public static function render($group = false, $inline_dep = null, $attr = array())
  764. {
  765. $r = self::render_css($group, $inline_dep, $attr);
  766. $r .= self::render_js($group, $inline_dep, $attr);
  767. return $r;
  768. }
  769. /**
  770. * Renders the specific javascript group, or all groups if no group specified.
  771. *
  772. * @param bool|string $group Which group to render. If omitted renders all groups.
  773. * @param bool|null $inline_dep @deprecated If true, the result is printed inline.
  774. * If false, is written to a file and linked to. In fact, $inline = true also
  775. * causes a cache file to be written for speed purposes.
  776. * @param array $attr_dep @todo Document this
  777. *
  778. * @return string The javascript tags to be written to the page
  779. */
  780. public static function render_js($group = false, $inline_dep = null, $attr_dep = array())
  781. {
  782. // Do nοt force the user to remember that false is used for ommitted non-bool arguments
  783. if (!is_string($group))
  784. {
  785. $group = false;
  786. }
  787. if (!is_array($attr_dep))
  788. {
  789. $attr_dep = array();
  790. }
  791. $file_groups = self::files_to_render('js', $group);
  792. $ret = '';
  793. foreach ($file_groups as $group_name => $file_group)
  794. {
  795. // We used to take $inline as 2nd argument. However, we now use a group option.
  796. // It ιs easiest if we let $inline override this group option, though.
  797. $inline = ($inline_dep === null) ? self::$groups['js'][$group_name]['inline'] : $inline_dep;
  798. // $attr is also deprecated. If specified, entirely overrides the group option.
  799. $attr = (!count($attr_dep)) ? self::$groups['js'][$group_name]['attr'] : $attr_dep;
  800. // the type attribute is not required for script elements under html5
  801. // @link http://www.w3.org/TR/html5/scripting-1.html#attr-script-type
  802. // if (!\Html::$html5)
  803. // $attr = array( 'type' => 'text/javascript' ) + $attr;
  804. if (self::$groups['js'][$group_name]['combine'])
  805. {
  806. $filename = self::combine('js', $file_group, self::$groups['js'][$group_name]['min'], $inline);
  807. if (!$inline && self::$show_files)
  808. {
  809. // $ret .= '<!--'.PHP_EOL.'Group: '.$group_name.PHP_EOL.implode('', array_map(function($a){
  810. // return "\t".$a['file'].PHP_EOL;
  811. // }, $file_group)).'-->'.PHP_EOL;
  812. // PHP 5.2 hack
  813. $bits = array();
  814. foreach ($file_group as $a)
  815. {
  816. $bits[] = "\t".$a['file'].PHP_EOL;
  817. }
  818. $ret .= '<!--'.PHP_EOL.'Group: '.$group_name.PHP_EOL.implode('', $bits).'-->'.PHP_EOL;
  819. }
  820. if ($inline)
  821. {
  822. $ret .= self::html_tag('script', $attr, PHP_EOL.file_get_contents(FCPATH.self::$cache_path.$filename).PHP_EOL).PHP_EOL;
  823. }
  824. else
  825. {
  826. $filepath = self::process_filepath(self::$cache_path.$filename, 'js');
  827. $ret .= self::html_tag('script', array(
  828. 'src' => self::$asset_url.$filepath,
  829. ) + $attr, '').PHP_EOL;
  830. }
  831. }
  832. else
  833. {
  834. foreach ($file_group as $file)
  835. {
  836. if ($inline)
  837. {
  838. $ret .= self::html_tag('script', $attr, PHP_EOL.file_get_contents($file['file']).PHP_EOL).PHP_EOL;
  839. }
  840. else
  841. {
  842. $remote = (strpos($file['file'], '//') !== false);
  843. $base = ($remote) ? '' : self::$asset_url;
  844. $filepath = self::process_filepath($file['file'], 'js', $remote);
  845. $ret .= self::html_tag('script', array(
  846. 'src' => $base.$filepath,
  847. ) + $attr, '').PHP_EOL;
  848. }
  849. }
  850. }
  851. }
  852. return $ret;
  853. }
  854. /**
  855. * Renders the specific css group, or all groups if no group specified.
  856. *
  857. * @param bool|string $group Which group to render. If omitted renders all groups.
  858. * @param null $inline_dep @deprecated If true, the result is printed inline.
  859. * If false, is written to a file and linked to. In fact, $inline = true also
  860. * causes a cache file to be written for speed purposes.
  861. * @param array $attr_dep
  862. *
  863. * @return string The css tags to be written to the page.
  864. */
  865. public static function render_css($group = false, $inline_dep = null, $attr_dep = array())
  866. {
  867. // Don't force the user to remember that false is used for ommitted non-bool arguments
  868. if (!is_string($group))
  869. {
  870. $group = false;
  871. }
  872. if (!is_array($attr_dep))
  873. {
  874. $attr_dep = array();
  875. }
  876. $file_groups = self::files_to_render('css', $group);
  877. $ret = '';
  878. foreach ($file_groups as $group_name => $file_group)
  879. {
  880. // We used to take $inline as 2nd argument. However, we now use a group option.
  881. // It's easiest if we let $inline override this group option, though.
  882. $inline = ($inline_dep === null) ? self::$groups['css'][$group_name]['inline'] : $inline_dep;
  883. // $attr is also deprecated. If specified, entirely overrides the group option.
  884. $attr = (!count($attr_dep)) ? self::$groups['css'][$group_name]['attr'] : $attr_dep;
  885. // the type attribute is not required for style or link[rel="stylesheet"] elements under html5
  886. // @link http://www.w3.org/TR/html5/links.html#link-type-stylesheet
  887. // @link http://www.w3.org/TR/html5/semantics.html#attr-style-type
  888. // if (!\Html::$html5)
  889. // $attr = array( 'type' => 'text/css' ) + $attr;
  890. if (self::$groups['css'][$group_name]['combine'])
  891. {
  892. $filename = self::combine('css', $file_group, self::$groups['css'][$group_name]['min'], $inline);
  893. if (!$inline && self::$show_files)
  894. {
  895. // $ret .= '<!--'.PHP_EOL.'Group: '.$group_name.PHP_EOL.implode('', array_map(function($a){
  896. // return "\t".$a['file'].PHP_EOL;
  897. // }, $file_group)).'-->'.PHP_EOL;
  898. // PHP 5.2 hack
  899. $bits = array();
  900. foreach ($file_group as $a)
  901. {
  902. $bits[] = "\t".$a['file'].PHP_EOL;
  903. }
  904. $ret .= '<!--'.PHP_EOL.'Group: '.$group_name.PHP_EOL.implode('', $bits).'-->'.PHP_EOL;
  905. }
  906. if ($inline)
  907. {
  908. $ret .= self::html_tag('style', $attr, PHP_EOL.file_get_contents(FCPATH.self::$cache_path.$filename).PHP_EOL).PHP_EOL;
  909. }
  910. else
  911. {
  912. $filepath = self::process_filepath(self::$cache_path.$filename, 'css');
  913. $ret .= self::html_tag('link', array(
  914. 'rel' => 'stylesheet',
  915. 'href' => self::$asset_url.$filepath,
  916. ) + $attr).PHP_EOL;
  917. }
  918. }
  919. else
  920. {
  921. foreach ($file_group as $file)
  922. {
  923. if ($inline)
  924. {
  925. $ret .= self::html_tag('style', $attr, PHP_EOL.file_get_contents($file['file']).PHP_EOL).PHP_EOL;
  926. }
  927. else
  928. {
  929. $remote = (strpos($file['file'], '//') !== false);
  930. $base = ($remote) ? '' : self::$asset_url;
  931. $filepath = self::process_filepath($file['file'], 'css', $remote);
  932. $ret .= self::html_tag('link', array(
  933. 'rel' => 'stylesheet',
  934. 'href' => $base.$filepath,
  935. ) + $attr).PHP_EOL;
  936. }
  937. }
  938. }
  939. }
  940. return $ret;
  941. }
  942. /**
  943. * Figures out where a file should be, based on its namespace and type.
  944. *
  945. * @param string $file The name of the asset to search for.
  946. * @param string $asset_type 'css', 'js' or 'img'
  947. *
  948. * @return array The paths to the assets, relative to $asset_url.
  949. * @throws Asset_Exception
  950. */
  951. protected static function find_files($file, $asset_type)
  952. {
  953. $parts = explode('::', $file, 2);
  954. if (!array_key_exists($parts[0], self::$asset_paths))
  955. {
  956. throw new Asset_Exception("Could not find namespace {$parts[0]}");
  957. }
  958. $path = self::$asset_paths[$parts[0]]['path'];
  959. $file = $parts[1];
  960. $folder = self::$asset_paths[$parts[0]]['dirs'][$asset_type];
  961. $file = ltrim($file, '/');
  962. $remote = (strpos($path, '//') !== false);
  963. if ($remote)
  964. {
  965. // Glob doesn't work on remote locations, so just assume they
  966. // specified a file, not a glob pattern.
  967. // Do not look for the file now either. That will be done
  968. // by file_get_contents() later on, if need be.
  969. return array($path.$folder.$file);
  970. }
  971. else
  972. {
  973. $glob_files = glob($path.$folder.$file);
  974. if (!$glob_files || !count($glob_files))
  975. {
  976. throw new Asset_Exception("Found no files matching $path$folder$file");
  977. }
  978. return $glob_files;
  979. }
  980. }
  981. /**
  982. * Given a list of group names, adds to that list, in the appropriate places,
  983. * and groups which are listed as dependencies of those group.
  984. * Duplicate group names are not a problem, as a group is disabled when it's
  985. * rendered.
  986. *
  987. * @param string $type 'js' or 'css'.
  988. * @param array $group_names Array of group names to check.
  989. * @param int $depth Used by this function to check for potentially infinite recursion.
  990. *
  991. * @return array List of group names with deps resolved.
  992. * @throws Asset_Exception
  993. */
  994. protected static function resolve_deps($type, $group_names, $depth = 0)
  995. {
  996. if ($depth > self::$deps_max_depth)
  997. {
  998. throw new Asset_Exception("Reached depth $depth trying to resolve dependencies. ".
  999. "You've probably got some circular ones involving ".implode(',', $group_names).". ".
  1000. "If not, adjust the config key deps_max_depth.");
  1001. }
  1002. // Insert the dep just before what it's a dep for.
  1003. foreach ($group_names as $i => $group_name)
  1004. {
  1005. // If the group has already been rendered, bottle.
  1006. if (in_array($group_name, self::$rendered_groups[$type]))
  1007. {
  1008. continue;
  1009. }
  1010. // Do not pay attention to bottom-level groups which are disabled
  1011. if (!self::$groups[$type][$group_name]['enabled'] && $depth == 0)
  1012. {
  1013. continue;
  1014. }
  1015. // Otherwise, enable the group. Fairly obvious, as the whole point of
  1016. // deps is to render disabled groups
  1017. self::asset_enabled($type, $group_name, true);
  1018. if (count(self::$groups[$type][$group_name]['deps']))
  1019. {
  1020. array_splice($group_names, $i, 0, self::resolve_deps($type, self::$groups[$type][$group_name]['deps'], $depth + 1));
  1021. }
  1022. }
  1023. return $group_names;
  1024. }
  1025. /**
  1026. * Determines the list of files to be rendered, along with whether they
  1027. * have been minified already.
  1028. *
  1029. * @param string $type 'css' / 'js'
  1030. * @param array $group The groups to render. If false, takes all groups
  1031. *
  1032. * @return array An array of array('file' => file_name, 'minified' => whether_minified)
  1033. */
  1034. protected static function files_to_render($type, $group)
  1035. {
  1036. // If no group specified, print all groups.
  1037. if ($group == false)
  1038. {
  1039. $group_names = array_keys(self::$groups[$type]);
  1040. }
  1041. // If a group was specified, but it does not exist.
  1042. else
  1043. {
  1044. if (!array_key_exists($group, self::$groups[$type]))
  1045. {
  1046. return array();
  1047. }
  1048. $group_names = array($group);
  1049. }
  1050. $files = array();
  1051. $minified = false;
  1052. $group_names = self::resolve_deps($type, $group_names);
  1053. foreach ($group_names as $group_name)
  1054. {
  1055. if (self::$groups[$type][$group_name]['enabled'] == false)
  1056. {
  1057. continue;
  1058. }
  1059. // If there are no files in the group, there's no point in printing it.
  1060. if (count(self::$groups[$type][$group_name]['files']) == 0)
  1061. {
  1062. continue;
  1063. }
  1064. $files[$group_name] = array();
  1065. // Mark the group as disabled to avoid the same group being printed twice
  1066. self::asset_enabled($type, $group_name, false);
  1067. // Add it to the list of rendered groups
  1068. array_push(self::$rendered_groups[$type], $group_name);
  1069. foreach (self::$groups[$type][$group_name]['files'] as $file_set)
  1070. {
  1071. if (self::$groups[$type][$group_name]['min'])
  1072. {
  1073. $assets = self::find_files(($file_set[1]) ? $file_set[1] : $file_set[0], $type);
  1074. $minified = ($file_set[1] != false);
  1075. }
  1076. else
  1077. {
  1078. $assets = self::find_files($file_set[0], $type);
  1079. }
  1080. foreach ($assets as $file)
  1081. {
  1082. array_push($files[$group_name], array('file' => $file, 'minified' => $minified,));
  1083. }
  1084. }
  1085. }
  1086. return $files;
  1087. }
  1088. /**
  1089. * Used to load a file from disk.
  1090. *
  1091. * Also calls the post_load callback.
  1092. *
  1093. * @todo Document this function.
  1094. *
  1095. * @param string $filename
  1096. * @param string $type 'css' or 'js'
  1097. * @param string $file_group
  1098. *
  1099. * @return string
  1100. */
  1101. protected static function load_file($filename, $type, $file_group)
  1102. {
  1103. $content = file_get_contents($filename);
  1104. if (self::$post_load_callback != null)
  1105. {
  1106. // For some reason, PHP doesn't like you calling member closure directly
  1107. $func = self::$post_load_callback;
  1108. $content = $func($content, $filename, $type, $file_group);
  1109. }
  1110. return $content;
  1111. }
  1112. /**
  1113. * Takes a list of files, and combines them into a single minified file.
  1114. * Doesn't bother if none of the files have been modified since the cache
  1115. * file was written.
  1116. *
  1117. * @param string $type 'css' or 'js'.
  1118. * @param array $file_group Array of ('file' => filename, 'minified' => is_minified) to combine and minify.
  1119. * @param bool $minify Whether to minify the files, as well as combining them.
  1120. * @param $inline @todo Not used, document this
  1121. *
  1122. * @return string The path to the cache file which was written.
  1123. * @throws Asset_Exception
  1124. */
  1125. protected static function combine($type, $file_group, $minify, $inline)
  1126. {
  1127. // Get the last modified time of all of the component files.
  1128. $last_mod = 0;
  1129. foreach ($file_group as $file)
  1130. {
  1131. // If it is a remote file just assume it is not modified,
  1132. // otherwise we are stuck making a ton of HTTP requests.
  1133. if (strpos($file['file'], '//') !== false)
  1134. {
  1135. continue;
  1136. }
  1137. $mod = filemtime(FCPATH.$file['file']);
  1138. if ($mod > $last_mod)
  1139. {
  1140. $last_mod = $mod;
  1141. }
  1142. }
  1143. // $filename = md5(implode('', array_map(function($a) {
  1144. // return $a['file'];
  1145. // }, $file_group)).($minify ? 'min' : '').$last_mod).'.'.$type;
  1146. // PHP 5.2
  1147. $bits = array();
  1148. foreach ($file_group as $a)
  1149. {
  1150. $bits[] = $a['file'];
  1151. }
  1152. $filename = md5(implode('', $bits).($minify ? 'min' : '').$last_mod).'.'.$type;
  1153. $filepath = FCPATH.self::$cache_path.'/'.$filename;
  1154. $needs_update = (!file_exists($filepath));
  1155. if ($needs_update)
  1156. {
  1157. $content = '';
  1158. foreach ($file_group as $file)
  1159. {
  1160. if (self::$show_files_inline)
  1161. {
  1162. $content .= PHP_EOL.'/* '.$file['file'].' */'.PHP_EOL.PHP_EOL;
  1163. }
  1164. if ($file['minified'] || !$minify)
  1165. {
  1166. $content_temp = self::load_file($file['file'], $type, $file_group).PHP_EOL;
  1167. if ($type == 'css')
  1168. {
  1169. $content .= Asset_Cssurirewriter::rewrite($content_temp, dirname($file['file']), null, self::$symlinks);
  1170. }
  1171. else
  1172. {
  1173. $content .= $content_temp;
  1174. }
  1175. }
  1176. else
  1177. {
  1178. $file_content = self::load_file($file['file'], $type, $file_group);
  1179. if ($file_content === false)
  1180. {
  1181. throw new Asset_Exception("Couldn't not open file {$file['file']}");
  1182. }
  1183. if ($type == 'js')
  1184. {
  1185. $content .= Asset_JSMin::minify($file_content).PHP_EOL;
  1186. }
  1187. elseif ($type == 'css')
  1188. {
  1189. $css = Asset_Csscompressor::process($file_content).PHP_EOL;
  1190. $content .= Asset_Cssurirewriter::rewrite($css, dirname($file['file']), null, self::$symlinks);
  1191. }
  1192. }
  1193. }
  1194. file_put_contents($filepath, $content, LOCK_EX);
  1195. $mtime = time(); // @todo Not used variable.
  1196. }
  1197. return $filename;
  1198. }
  1199. /**
  1200. * Renders the javascript added through js_inline().
  1201. *
  1202. * @return string The <script /> tags containing the inline javascript.
  1203. */
  1204. public static function render_js_inline()
  1205. {
  1206. // the type attribute is not required for script elements under html5
  1207. // @link http://www.w3.org/TR/html5/scripting-1.html#attr-script-type
  1208. // if (!\Html::$html5)
  1209. // $attr = array( 'type' => 'text/javascript' );
  1210. // else
  1211. $attr = array();
  1212. $ret = '';
  1213. foreach (self::$inline_assets['js'] as $content)
  1214. {
  1215. $ret .= self::html_tag('script', $attr, PHP_EOL.$content.PHP_EOL).PHP_EOL;
  1216. }
  1217. return $ret;
  1218. }
  1219. /**
  1220. * Renders the css added through css_inline().
  1221. *
  1222. * @return string The <style> tags containing the inline css
  1223. */
  1224. public static function render_css_inline()
  1225. {
  1226. // the type attribute is not required for style elements under html5
  1227. // @link http://www.w3.org/TR/html5/semantics.html#attr-style-type
  1228. // if (!\Html::$html5)
  1229. // $attr = array( 'type' => 'text/css' );
  1230. // else
  1231. $attr = array();
  1232. $ret = '';
  1233. foreach (self::$inline_assets['css'] as $content)
  1234. {
  1235. $ret .= self::html_tag('style', $attr, PHP_EOL.$content.PHP_EOL).PHP_EOL;
  1236. }
  1237. return $ret;
  1238. }
  1239. /**
  1240. * Sets the post_load file callback.
  1241. *
  1242. * It's pretty basic, and you're expected to handle
  1243. * e.g. filtering for the right file yourself.
  1244. *
  1245. * @param string $callback The name of the function for the callback.
  1246. */
  1247. public static function set_post_load_callback($callback) {
  1248. self::$post_load_callback = $callback;
  1249. }
  1250. /**
  1251. * Sets the filepath callback.
  1252. *
  1253. * @param string $callback The name of the function for the callback.
  1254. */
  1255. public static function set_filepath_callback($callback) {
  1256. self::$filepath_callback = $callback;
  1257. }
  1258. /**
  1259. * Locates the given image(s), and returns the resulting <img> tag.
  1260. *
  1261. * @param string|array $images Image or images to print.
  1262. * @param string $alt The alternate text.
  1263. * @param array $attr Attributes to apply to each image (e.g. width)
  1264. *
  1265. * @return string The resulting <img /> tag(s)
  1266. */
  1267. public static function img($images, $alt, $attr = array())
  1268. {
  1269. if (!is_array($images))
  1270. {
  1271. $images = array($images);
  1272. }
  1273. $attr['alt'] = $alt;
  1274. $ret = '';
  1275. foreach ($images as $image)
  1276. {
  1277. if (strpos($image, '::') === false)
  1278. {
  1279. $image = self::$default_path_key.'::'.$image;
  1280. }
  1281. $image_paths = self::find_files($image, 'img');
  1282. foreach ($image_paths as $image_path)
  1283. {
  1284. $remote = (strpos($image_path, '//') !== false);
  1285. $image_path = self::process_filepath($image_path, 'img', $remote);
  1286. $base = ($remote) ? '' : self::$asset_url;
  1287. $attr['src'] = $base.$image_path;
  1288. $ret .= self::html_tag('img', $attr);
  1289. }
  1290. }
  1291. return $ret;
  1292. }
  1293. /**
  1294. * Cleares all cache files last modified before $before.
  1295. *
  1296. * @param string $before Time before which to delete files.
  1297. * Defaults to 'now'. Uses strtotime.
  1298. */
  1299. public static function clear_cache($before = 'now')
  1300. {
  1301. self::clear_cache_base('*', $before);
  1302. }
  1303. /**
  1304. * Cleares all JS cache files last modified before $before.
  1305. *
  1306. * @param string $before Time before which to delete files.
  1307. * Defaults to 'now'. Uses strtotime.
  1308. */
  1309. public static function clear_js_cache($before = 'now')
  1310. {
  1311. self::clear_cache_base('*.js', $before);
  1312. }
  1313. /**
  1314. * Cleares CSS all cache files last modified before $before.
  1315. *
  1316. * @param string $before Time before which to delete files.
  1317. * Defaults to 'now'. Uses strtotime.
  1318. */
  1319. public static function clear_css_cache($before = 'now')
  1320. {
  1321. self::clear_cache_base('*.css', $before);
  1322. }
  1323. /**
  1324. * Base cache clear function.
  1325. *
  1326. * @param string $filter Glob filter to use when selecting files to delete.
  1327. * @param string $before Time before which to delete files.
  1328. * Defaults to 'now'. Uses strtotime.
  1329. */
  1330. protected static function clear_cache_base($filter = '*', $before = 'now')
  1331. {
  1332. $before = strtotime($before);
  1333. $files = glob(FCPATH.self::$cache_path.$filter);
  1334. foreach ($files as $file)
  1335. {
  1336. if (filemtime($file) < $before)
  1337. {
  1338. unlink($file);
  1339. }
  1340. }
  1341. }
  1342. /**
  1343. * Reset assets that have already been added in this request.
  1344. * This is useful when one module is planning to handle the
  1345. * output but another then takes over (such as a 404 handler)
  1346. */
  1347. public static function reset()
  1348. {
  1349. foreach (self::$groups as $type => $groups)
  1350. {
  1351. foreach ($groups as $group => $meta)
  1352. {
  1353. unset(self::$groups[$type][$group]);
  1354. }
  1355. }
  1356. }
  1357. /**
  1358. * Create a XHTML tag
  1359. *
  1360. * @param string $tag The tag name.
  1361. * @param string|array $attr The tag attributes.
  1362. * @param string|bool $content The content to place in the tag, or false for no closing tag.
  1363. *
  1364. * @return string
  1365. */
  1366. protected static function html_tag($tag, $attr = array(), $content = false)
  1367. {
  1368. $has_content = (bool)($content !== false and $content !== null);
  1369. $html = '<'.$tag;
  1370. $html .= (!empty($attr)) ? ' '.(is_array($attr) ? self::array_to_attr($attr) : $attr) : '';
  1371. $html .= $has_content ? '>' : ' />';
  1372. $html .= $has_content ? $content.'</'.$tag.'>' : '';
  1373. return $html;
  1374. }
  1375. /**
  1376. * Takes an array of attributes and turns it into a string for an html tag
  1377. *
  1378. * @param array $attr
  1379. *
  1380. * @return string
  1381. */
  1382. protected static function array_to_attr($attr)
  1383. {
  1384. $attr_str = '';
  1385. if (!is_array($attr))
  1386. {
  1387. $attr = (array)$attr;
  1388. }
  1389. foreach ($attr as $property => $value)
  1390. {
  1391. // Ignore null values
  1392. if (is_null($value))
  1393. {
  1394. continue;
  1395. }
  1396. // If the key is numeric then it must be something like selected="selected"
  1397. if (is_numeric($property))
  1398. {
  1399. $property = $value;
  1400. }
  1401. $attr_str .= $property . '="' . $value . '" ';
  1402. }
  1403. // We strip off the last space for return
  1404. return trim($attr_str);
  1405. }
  1406. }
  1407. /* End of file asset_php */