Assets.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. <?php
  2. /**
  3. * @package Grav\Common
  4. *
  5. * @copyright Copyright (c) 2015 - 2022 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 array_slice;
  17. use function call_user_func_array;
  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 LINK = 'link';
  29. const CSS = 'css';
  30. const JS = 'js';
  31. const JS_MODULE = 'js_module';
  32. const LINK_COLLECTION = 'assets_link';
  33. const CSS_COLLECTION = 'assets_css';
  34. const JS_COLLECTION = 'assets_js';
  35. const JS_MODULE_COLLECTION = 'assets_js_module';
  36. const LINK_TYPE = Assets\Link::class;
  37. const CSS_TYPE = Assets\Css::class;
  38. const JS_TYPE = Assets\Js::class;
  39. const JS_MODULE_TYPE = Assets\JsModule::class;
  40. const INLINE_CSS_TYPE = Assets\InlineCss::class;
  41. const INLINE_JS_TYPE = Assets\InlineJs::class;
  42. const INLINE_JS_MODULE_TYPE = Assets\InlineJsModule::class;
  43. /** @const Regex to match CSS and JavaScript files */
  44. const DEFAULT_REGEX = '/.\.(css|js)$/i';
  45. /** @const Regex to match CSS files */
  46. const CSS_REGEX = '/.\.css$/i';
  47. /** @const Regex to match JavaScript files */
  48. const JS_REGEX = '/.\.js$/i';
  49. /** @const Regex to match JavaScriptModyle files */
  50. const JS_MODULE_REGEX = '/.\.mjs$/i';
  51. /** @var string */
  52. protected $assets_dir;
  53. /** @var string */
  54. protected $assets_url;
  55. /** @var array */
  56. protected $assets_link = [];
  57. /** @var array */
  58. protected $assets_css = [];
  59. /** @var array */
  60. protected $assets_js = [];
  61. /** @var array */
  62. protected $assets_js_module = [];
  63. // Following variables come from the configuration:
  64. /** @var bool */
  65. protected $css_pipeline;
  66. /** @var bool */
  67. protected $css_pipeline_include_externals;
  68. /** @var bool */
  69. protected $css_pipeline_before_excludes;
  70. /** @var bool */
  71. protected $js_pipeline;
  72. /** @var bool */
  73. protected $js_pipeline_include_externals;
  74. /** @var bool */
  75. protected $js_pipeline_before_excludes;
  76. /** @var bool */
  77. protected $js_module_pipeline;
  78. /** @var bool */
  79. protected $js_module_pipeline_include_externals;
  80. /** @var bool */
  81. protected $js_module_pipeline_before_excludes;
  82. /** @var array */
  83. protected $pipeline_options = [];
  84. /** @var Closure|string */
  85. protected $fetch_command;
  86. /** @var string */
  87. protected $autoload;
  88. /** @var bool */
  89. protected $enable_asset_timestamp;
  90. /** @var array|null */
  91. protected $collections;
  92. /** @var string */
  93. protected $timestamp;
  94. /** @var array Keeping track for order counts (for sorting) */
  95. protected $order = [];
  96. /**
  97. * Initialization called in the Grav lifecycle to initialize the Assets with appropriate configuration
  98. *
  99. * @return void
  100. */
  101. public function init()
  102. {
  103. $grav = Grav::instance();
  104. /** @var Config $config */
  105. $config = $grav['config'];
  106. $asset_config = (array)$config->get('system.assets');
  107. /** @var UniformResourceLocator $locator */
  108. $locator = $grav['locator'];
  109. $this->assets_dir = $locator->findResource('asset://');
  110. $this->assets_url = $locator->findResource('asset://', false);
  111. $this->config($asset_config);
  112. // Register any preconfigured collections
  113. foreach ((array) $this->collections as $name => $collection) {
  114. $this->registerCollection($name, (array)$collection);
  115. }
  116. }
  117. /**
  118. * Set up configuration options.
  119. *
  120. * All the class properties except 'js' and 'css' are accepted here.
  121. * Also, an extra option 'autoload' may be passed containing an array of
  122. * assets and/or collections that will be automatically added on startup.
  123. *
  124. * @param array $config Configurable options.
  125. * @return $this
  126. */
  127. public function config(array $config)
  128. {
  129. foreach ($config as $key => $value) {
  130. if ($this->hasProperty($key)) {
  131. $this->setProperty($key, $value);
  132. } elseif (Utils::startsWith($key, 'css_') || Utils::startsWith($key, 'js_')) {
  133. $this->pipeline_options[$key] = $value;
  134. }
  135. }
  136. // Add timestamp if it's enabled
  137. if ($this->enable_asset_timestamp) {
  138. $this->timestamp = Grav::instance()['cache']->getKey();
  139. }
  140. return $this;
  141. }
  142. /**
  143. * Add an asset or a collection of assets.
  144. *
  145. * It automatically detects the asset type (JavaScript, CSS or collection).
  146. * You may add more than one asset passing an array as argument.
  147. *
  148. * @param string|string[] $asset
  149. * @return $this
  150. */
  151. public function add($asset)
  152. {
  153. if (!$asset) {
  154. return $this;
  155. }
  156. $args = func_get_args();
  157. // More than one asset
  158. if (is_array($asset)) {
  159. foreach ($asset as $index => $location) {
  160. $params = array_slice($args, 1);
  161. if (is_array($location)) {
  162. $params = array_shift($params);
  163. if (is_numeric($params)) {
  164. $params = [ 'priority' => $params ];
  165. }
  166. $params = [array_replace_recursive([], $location, $params)];
  167. $location = $index;
  168. }
  169. $params = array_merge([$location], $params);
  170. call_user_func_array([$this, 'add'], $params);
  171. }
  172. } elseif (isset($this->collections[$asset])) {
  173. array_shift($args);
  174. $args = array_merge([$this->collections[$asset]], $args);
  175. call_user_func_array([$this, 'add'], $args);
  176. } else {
  177. // Get extension
  178. $path = parse_url($asset, PHP_URL_PATH);
  179. $extension = $path ? Utils::pathinfo($path, PATHINFO_EXTENSION) : '';
  180. // JavaScript or CSS
  181. if ($extension !== '') {
  182. $extension = strtolower($extension);
  183. if ($extension === 'css') {
  184. call_user_func_array([$this, 'addCss'], $args);
  185. } elseif ($extension === 'js') {
  186. call_user_func_array([$this, 'addJs'], $args);
  187. } elseif ($extension === 'mjs') {
  188. call_user_func_array([$this, 'addJsModule'], $args);
  189. }
  190. }
  191. }
  192. return $this;
  193. }
  194. /**
  195. * @param string $collection
  196. * @param string $type
  197. * @param string|string[] $asset
  198. * @param array $options
  199. * @return $this
  200. */
  201. protected function addType($collection, $type, $asset, $options)
  202. {
  203. if (is_array($asset)) {
  204. foreach ($asset as $index => $location) {
  205. $assetOptions = $options;
  206. if (is_array($location)) {
  207. $assetOptions = array_replace_recursive([], $options, $location);
  208. $location = $index;
  209. }
  210. $this->addType($collection, $type, $location, $assetOptions);
  211. }
  212. return $this;
  213. }
  214. if ($this->isValidType($type) && isset($this->collections[$asset])) {
  215. $this->addType($collection, $type, $this->collections[$asset], $options);
  216. return $this;
  217. }
  218. // If pipeline disabled, set to position if provided, else after
  219. if (isset($options['pipeline'])) {
  220. if ($options['pipeline'] === false) {
  221. $exclude_type = $this->getBaseType($type);
  222. $excludes = strtolower($exclude_type . '_pipeline_before_excludes');
  223. if ($this->{$excludes}) {
  224. $default = 'after';
  225. } else {
  226. $default = 'before';
  227. }
  228. $options['position'] = $options['position'] ?? $default;
  229. }
  230. unset($options['pipeline']);
  231. }
  232. // Add timestamp
  233. $options['timestamp'] = $this->timestamp;
  234. // Set order
  235. $group = $options['group'] ?? 'head';
  236. $position = $options['position'] ?? 'pipeline';
  237. $orderKey = "{$type}|{$group}|{$position}";
  238. if (!isset($this->order[$orderKey])) {
  239. $this->order[$orderKey] = 0;
  240. }
  241. $options['order'] = $this->order[$orderKey]++;
  242. // Create asset of correct type
  243. $asset_object = new $type();
  244. // If exists
  245. if ($asset_object->init($asset, $options)) {
  246. $this->$collection[md5($asset)] = $asset_object;
  247. }
  248. return $this;
  249. }
  250. /**
  251. * Add a CSS asset or a collection of assets.
  252. *
  253. * @return $this
  254. */
  255. public function addLink($asset)
  256. {
  257. return $this->addType($this::LINK_COLLECTION, $this::LINK_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::LINK_TYPE));
  258. }
  259. /**
  260. * Add a CSS asset or a collection of assets.
  261. *
  262. * @return $this
  263. */
  264. public function addCss($asset)
  265. {
  266. return $this->addType($this::CSS_COLLECTION, $this::CSS_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::CSS_TYPE));
  267. }
  268. /**
  269. * Add an Inline CSS asset or a collection of assets.
  270. *
  271. * @return $this
  272. */
  273. public function addInlineCss($asset)
  274. {
  275. return $this->addType($this::CSS_COLLECTION, $this::INLINE_CSS_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::INLINE_CSS_TYPE));
  276. }
  277. /**
  278. * Add a JS asset or a collection of assets.
  279. *
  280. * @return $this
  281. */
  282. public function addJs($asset)
  283. {
  284. return $this->addType($this::JS_COLLECTION, $this::JS_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::JS_TYPE));
  285. }
  286. /**
  287. * Add an Inline JS asset or a collection of assets.
  288. *
  289. * @return $this
  290. */
  291. public function addInlineJs($asset)
  292. {
  293. return $this->addType($this::JS_COLLECTION, $this::INLINE_JS_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::INLINE_JS_TYPE));
  294. }
  295. /**
  296. * Add a JS asset or a collection of assets.
  297. *
  298. * @return $this
  299. */
  300. public function addJsModule($asset)
  301. {
  302. return $this->addType($this::JS_MODULE_COLLECTION, $this::JS_MODULE_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::JS_MODULE_TYPE));
  303. }
  304. /**
  305. * Add an Inline JS asset or a collection of assets.
  306. *
  307. * @return $this
  308. */
  309. public function addInlineJsModule($asset)
  310. {
  311. return $this->addType($this::JS_MODULE_COLLECTION, $this::INLINE_JS_MODULE_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::INLINE_JS_MODULE_TYPE));
  312. }
  313. /**
  314. * Add/replace collection.
  315. *
  316. * @param string $collectionName
  317. * @param array $assets
  318. * @param bool $overwrite
  319. * @return $this
  320. */
  321. public function registerCollection($collectionName, array $assets, $overwrite = false)
  322. {
  323. if ($overwrite || !isset($this->collections[$collectionName])) {
  324. $this->collections[$collectionName] = $assets;
  325. }
  326. return $this;
  327. }
  328. /**
  329. * @param array $assets
  330. * @param string $key
  331. * @param string $value
  332. * @param bool $sort
  333. * @return array|false
  334. */
  335. protected function filterAssets($assets, $key, $value, $sort = false)
  336. {
  337. $results = array_filter($assets, function ($asset) use ($key, $value) {
  338. if ($key === 'position' && $value === 'pipeline') {
  339. $type = $asset->getType();
  340. if ($asset->getRemote() && $this->{strtolower($type) . '_pipeline_include_externals'} === false && $asset['position'] === 'pipeline') {
  341. if ($this->{strtolower($type) . '_pipeline_before_excludes'}) {
  342. $asset->setPosition('after');
  343. } else {
  344. $asset->setPosition('before');
  345. }
  346. return false;
  347. }
  348. }
  349. if ($asset[$key] === $value) {
  350. return true;
  351. }
  352. return false;
  353. });
  354. if ($sort && !empty($results)) {
  355. $results = $this->sortAssets($results);
  356. }
  357. return $results;
  358. }
  359. /**
  360. * @param array $assets
  361. * @return array
  362. */
  363. protected function sortAssets($assets)
  364. {
  365. uasort($assets, static function ($a, $b) {
  366. return $b['priority'] <=> $a['priority'] ?: $a['order'] <=> $b['order'];
  367. });
  368. return $assets;
  369. }
  370. /**
  371. * @param string $type
  372. * @param string $group
  373. * @param array $attributes
  374. * @return string
  375. */
  376. public function render($type, $group = 'head', $attributes = [])
  377. {
  378. $before_output = '';
  379. $pipeline_output = '';
  380. $after_output = '';
  381. $assets = 'assets_' . $type;
  382. $pipeline_enabled = $type . '_pipeline';
  383. $render_pipeline = 'render' . ucfirst($type);
  384. $group_assets = $this->filterAssets($this->$assets, 'group', $group);
  385. $pipeline_assets = $this->filterAssets($group_assets, 'position', 'pipeline', true);
  386. $before_assets = $this->filterAssets($group_assets, 'position', 'before', true);
  387. $after_assets = $this->filterAssets($group_assets, 'position', 'after', true);
  388. // Pipeline
  389. if ($this->{$pipeline_enabled} ?? false) {
  390. $options = array_merge($this->pipeline_options, ['timestamp' => $this->timestamp]);
  391. $pipeline = new Pipeline($options);
  392. $pipeline_output = $pipeline->$render_pipeline($pipeline_assets, $group, $attributes);
  393. } else {
  394. foreach ($pipeline_assets as $asset) {
  395. $pipeline_output .= $asset->render();
  396. }
  397. }
  398. // Before Pipeline
  399. foreach ($before_assets as $asset) {
  400. $before_output .= $asset->render();
  401. }
  402. // After Pipeline
  403. foreach ($after_assets as $asset) {
  404. $after_output .= $asset->render();
  405. }
  406. return $before_output . $pipeline_output . $after_output;
  407. }
  408. /**
  409. * Build the CSS link tags.
  410. *
  411. * @param string $group name of the group
  412. * @param array $attributes
  413. * @return string
  414. */
  415. public function css($group = 'head', $attributes = [], $include_link = true)
  416. {
  417. $output = '';
  418. if ($include_link) {
  419. $output = $this->link($group, $attributes);
  420. }
  421. $output .= $this->render(self::CSS, $group, $attributes);
  422. return $output;
  423. }
  424. /**
  425. * Build the CSS link tags.
  426. *
  427. * @param string $group name of the group
  428. * @param array $attributes
  429. * @return string
  430. */
  431. public function link($group = 'head', $attributes = [])
  432. {
  433. return $this->render(self::LINK, $group, $attributes);
  434. }
  435. /**
  436. * Build the JavaScript script tags.
  437. *
  438. * @param string $group name of the group
  439. * @param array $attributes
  440. * @return string
  441. */
  442. public function js($group = 'head', $attributes = [], $include_js_module = true)
  443. {
  444. $output = $this->render(self::JS, $group, $attributes);
  445. if ($include_js_module) {
  446. $output .= $this->jsModule($group, $attributes);
  447. }
  448. return $output;
  449. }
  450. /**
  451. * Build the Javascript Modules tags
  452. *
  453. * @param string $group
  454. * @param array $attributes
  455. * @return string
  456. */
  457. public function jsModule($group = 'head', $attributes = [])
  458. {
  459. return $this->render(self::JS_MODULE, $group, $attributes);
  460. }
  461. /**
  462. * @param string $group
  463. * @param array $attributes
  464. * @return string
  465. */
  466. public function all($group = 'head', $attributes = [])
  467. {
  468. $output = $this->css($group, $attributes, false);
  469. $output .= $this->link($group, $attributes);
  470. $output .= $this->js($group, $attributes, false);
  471. $output .= $this->jsModule($group, $attributes);
  472. return $output;
  473. }
  474. /**
  475. * @param class-string $type
  476. * @return bool
  477. */
  478. protected function isValidType($type)
  479. {
  480. return in_array($type, [self::CSS_TYPE, self::JS_TYPE, self::JS_MODULE_TYPE]);
  481. }
  482. /**
  483. * @param class-string $type
  484. * @return string
  485. */
  486. protected function getBaseType($type)
  487. {
  488. switch ($type) {
  489. case $this::JS_TYPE:
  490. case $this::INLINE_JS_TYPE:
  491. $base_type = $this::JS;
  492. break;
  493. case $this::JS_MODULE_TYPE:
  494. case $this::INLINE_JS_MODULE_TYPE:
  495. $base_type = $this::JS_MODULE;
  496. break;
  497. default:
  498. $base_type = $this::CSS;
  499. }
  500. return $base_type;
  501. }
  502. }