Assets.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. <?php
  2. /**
  3. * @package Grav\Common
  4. *
  5. * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common;
  9. use Grav\Common\Assets\Pipeline;
  10. use Grav\Common\Assets\Traits\LegacyAssetsTrait;
  11. use Grav\Common\Assets\Traits\TestingAssetsTrait;
  12. use Grav\Common\Config\Config;
  13. use Grav\Framework\Object\PropertyObject;
  14. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  15. class Assets extends PropertyObject
  16. {
  17. use TestingAssetsTrait;
  18. use LegacyAssetsTrait;
  19. const CSS_COLLECTION = 'assets_css';
  20. const JS_COLLECTION = 'assets_js';
  21. const CSS_TYPE = 'Css';
  22. const JS_TYPE = 'Js';
  23. const INLINE_CSS_TYPE = 'InlineCss';
  24. const INLINE_JS_TYPE = 'InlineJs';
  25. /** @const Regex to match CSS and JavaScript files */
  26. const DEFAULT_REGEX = '/.\.(css|js)$/i';
  27. /** @const Regex to match CSS files */
  28. const CSS_REGEX = '/.\.css$/i';
  29. /** @const Regex to match JavaScript files */
  30. const JS_REGEX = '/.\.js$/i';
  31. protected $assets_dir;
  32. protected $assets_url;
  33. protected $assets_css = [];
  34. protected $assets_js = [];
  35. // Config Options
  36. protected $css_pipeline;
  37. protected $css_pipeline_include_externals;
  38. protected $css_pipeline_before_excludes;
  39. protected $js_pipeline;
  40. protected $js_pipeline_include_externals;
  41. protected $js_pipeline_before_excludes;
  42. protected $pipeline_options = [];
  43. protected $fetch_command;
  44. protected $autoload;
  45. protected $enable_asset_timestamp;
  46. protected $collections;
  47. protected $timestamp;
  48. /**
  49. * Initialization called in the Grav lifecycle to initialize the Assets with appropriate configuration
  50. */
  51. public function init()
  52. {
  53. $grav = Grav::instance();
  54. /** @var Config $config */
  55. $config = $grav['config'];
  56. $asset_config = (array)$config->get('system.assets');
  57. /** @var UniformResourceLocator $locator */
  58. $locator = $grav['locator'];
  59. $this->assets_dir = $locator->findResource('asset://') . DS;
  60. $this->assets_url = $locator->findResource('asset://', false);
  61. $this->config($asset_config);
  62. // Register any preconfigured collections
  63. foreach ((array) $this->collections as $name => $collection) {
  64. $this->registerCollection($name, (array)$collection);
  65. }
  66. }
  67. /**
  68. * Set up configuration options.
  69. *
  70. * All the class properties except 'js' and 'css' are accepted here.
  71. * Also, an extra option 'autoload' may be passed containing an array of
  72. * assets and/or collections that will be automatically added on startup.
  73. *
  74. * @param array $config Configurable options.
  75. *
  76. * @return $this
  77. */
  78. public function config(array $config)
  79. {
  80. foreach ($config as $key => $value) {
  81. if ($this->hasProperty($key)) {
  82. $this->setProperty($key, $value);
  83. } elseif (Utils::startsWith($key, 'css_') || Utils::startsWith($key, 'js_')) {
  84. $this->pipeline_options[$key] = $value;
  85. }
  86. }
  87. // Add timestamp if it's enabled
  88. if ($this->enable_asset_timestamp) {
  89. $this->timestamp = Grav::instance()['cache']->getKey();
  90. }
  91. return $this;
  92. }
  93. /**
  94. * Add an asset or a collection of assets.
  95. *
  96. * It automatically detects the asset type (JavaScript, CSS or collection).
  97. * You may add more than one asset passing an array as argument.
  98. *
  99. * @param array|string $asset
  100. * @return $this
  101. */
  102. public function add($asset)
  103. {
  104. $args = \func_get_args();
  105. // More than one asset
  106. if (\is_array($asset)) {
  107. foreach ($asset as $a) {
  108. array_shift($args);
  109. $args = array_merge([$a], $args);
  110. \call_user_func_array([$this, 'add'], $args);
  111. }
  112. } elseif (isset($this->collections[$asset])) {
  113. array_shift($args);
  114. $args = array_merge([$this->collections[$asset]], $args);
  115. \call_user_func_array([$this, 'add'], $args);
  116. } else {
  117. // Get extension
  118. $extension = pathinfo(parse_url($asset, PHP_URL_PATH), PATHINFO_EXTENSION);
  119. // JavaScript or CSS
  120. if (\strlen($extension) > 0) {
  121. $extension = strtolower($extension);
  122. if ($extension === 'css') {
  123. \call_user_func_array([$this, 'addCss'], $args);
  124. } elseif ($extension === 'js') {
  125. \call_user_func_array([$this, 'addJs'], $args);
  126. }
  127. }
  128. }
  129. return $this;
  130. }
  131. protected function addType($collection, $type, $asset, $options)
  132. {
  133. if (\is_array($asset)) {
  134. foreach ($asset as $a) {
  135. $this->addType($collection, $type, $a, $options);
  136. }
  137. return $this;
  138. }
  139. if (($type === $this::CSS_TYPE || $type === $this::JS_TYPE) && isset($this->collections[$asset])) {
  140. $this->addType($collection, $type, $this->collections[$asset], $options);
  141. return $this;
  142. }
  143. // If pipeline disabled, set to position if provided, else after
  144. if (isset($options['pipeline'])) {
  145. if ($options['pipeline'] === false) {
  146. $exclude_type = ($type === $this::JS_TYPE || $type === $this::INLINE_JS_TYPE) ? $this::JS_TYPE : $this::CSS_TYPE;
  147. $excludes = strtolower($exclude_type . '_pipeline_before_excludes');
  148. if ($this->{$excludes}) {
  149. $default = 'after';
  150. } else {
  151. $default = 'before';
  152. }
  153. $options['position'] = $options['position'] ?? $default;
  154. }
  155. unset($options['pipeline']);
  156. }
  157. // Add timestamp
  158. $options['timestamp'] = $this->timestamp;
  159. // Set order
  160. $options['order'] = \count($this->$collection);
  161. // Create asset of correct type
  162. $asset_class = "\\Grav\\Common\\Assets\\{$type}";
  163. $asset_object = new $asset_class();
  164. // If exists
  165. if ($asset_object->init($asset, $options)) {
  166. $this->$collection[md5($asset)] = $asset_object;
  167. }
  168. return $this;
  169. }
  170. /**
  171. * Add a CSS asset or a collection of assets.
  172. *
  173. * @return $this
  174. */
  175. public function addCss($asset)
  176. {
  177. return $this->addType(Assets::CSS_COLLECTION,Assets::CSS_TYPE, $asset, $this->unifyLegacyArguments(\func_get_args(), Assets::CSS_TYPE));
  178. }
  179. /**
  180. * Add an Inline CSS asset or a collection of assets.
  181. *
  182. * @return $this
  183. */
  184. public function addInlineCss($asset)
  185. {
  186. return $this->addType(Assets::CSS_COLLECTION, Assets::INLINE_CSS_TYPE, $asset, $this->unifyLegacyArguments(\func_get_args(), Assets::INLINE_CSS_TYPE));
  187. }
  188. /**
  189. * Add a JS asset or a collection of assets.
  190. *
  191. * @return $this
  192. */
  193. public function addJs($asset)
  194. {
  195. return $this->addType(Assets::JS_COLLECTION, Assets::JS_TYPE, $asset, $this->unifyLegacyArguments(\func_get_args(), Assets::JS_TYPE));
  196. }
  197. /**
  198. * Add an Inline JS asset or a collection of assets.
  199. *
  200. * @return $this
  201. */
  202. public function addInlineJs($asset)
  203. {
  204. return $this->addType(Assets::JS_COLLECTION, Assets::INLINE_JS_TYPE, $asset, $this->unifyLegacyArguments(\func_get_args(), Assets::INLINE_JS_TYPE));
  205. }
  206. /**
  207. * Add/replace collection.
  208. *
  209. * @param string $collectionName
  210. * @param array $assets
  211. * @param bool $overwrite
  212. *
  213. * @return $this
  214. */
  215. public function registerCollection($collectionName, Array $assets, $overwrite = false)
  216. {
  217. if ($overwrite || !isset($this->collections[$collectionName])) {
  218. $this->collections[$collectionName] = $assets;
  219. }
  220. return $this;
  221. }
  222. protected function filterAssets($assets, $key, $value, $sort = false)
  223. {
  224. $results = array_filter($assets, function($asset) use ($key, $value) {
  225. if ($key === 'position' && $value === 'pipeline') {
  226. $type = $asset->getType();
  227. if ($asset->getRemote() && $this->{$type . '_pipeline_include_externals'} === false && $asset['position'] === 'pipeline' ) {
  228. if ($this->{$type . '_pipeline_before_excludes'}) {
  229. $asset->setPosition('after');
  230. } else {
  231. $asset->setPosition('before');
  232. }
  233. return false;
  234. }
  235. }
  236. if ($asset[$key] === $value) return true;
  237. return false;
  238. });
  239. if ($sort && !empty($results)) {
  240. $results = $this->sortAssets($results);
  241. }
  242. return $results;
  243. }
  244. protected function sortAssets($assets)
  245. {
  246. uasort ($assets, function($a, $b) {
  247. if ($a['priority'] == $b['priority']) {
  248. return $a['order'] - $b['order'];
  249. }
  250. return $b['priority'] - $a['priority'];
  251. });
  252. return $assets;
  253. }
  254. public function render($type, $group = 'head', $attributes = [])
  255. {
  256. $before_output = '';
  257. $pipeline_output = '';
  258. $after_output = '';
  259. $assets = 'assets_' . $type;
  260. $pipeline_enabled = $type . '_pipeline';
  261. $render_pipeline = 'render' . ucfirst($type);
  262. $group_assets = $this->filterAssets($this->$assets, 'group', $group);
  263. $pipeline_assets = $this->filterAssets($group_assets, 'position', 'pipeline', true);
  264. $before_assets = $this->filterAssets($group_assets, 'position', 'before', true);
  265. $after_assets = $this->filterAssets($group_assets, 'position', 'after', true);
  266. // Pipeline
  267. if ($this->{$pipeline_enabled}) {
  268. $options = array_merge($this->pipeline_options, ['timestamp' => $this->timestamp]);
  269. $pipeline = new Pipeline($options);
  270. $pipeline_output = $pipeline->$render_pipeline($pipeline_assets, $group, $attributes);
  271. } else {
  272. foreach ($pipeline_assets as $asset) {
  273. $pipeline_output .= $asset->render();
  274. }
  275. }
  276. // Before Pipeline
  277. foreach ($before_assets as $asset) {
  278. $before_output .= $asset->render();
  279. }
  280. // After Pipeline
  281. foreach ($after_assets as $asset) {
  282. $after_output .= $asset->render();
  283. }
  284. return $before_output . $pipeline_output . $after_output;
  285. }
  286. /**
  287. * Build the CSS link tags.
  288. *
  289. * @param string $group name of the group
  290. * @param array $attributes
  291. *
  292. * @return string
  293. */
  294. public function css($group = 'head', $attributes = [])
  295. {
  296. return $this->render('css', $group, $attributes);
  297. }
  298. /**
  299. * Build the JavaScript script tags.
  300. *
  301. * @param string $group name of the group
  302. * @param array $attributes
  303. *
  304. * @return string
  305. */
  306. public function js($group = 'head', $attributes = [])
  307. {
  308. return $this->render('js', $group, $attributes);
  309. }
  310. }