Assets.php 13 KB

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