PageRenderTime 61ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/system/cms/libraries/Asset.php

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