Assets.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. <?php
  2. /**
  3. * @package Grav\Common
  4. *
  5. * @copyright Copyright (c) 2015 - 2023 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. $timestamp_override = $options['timestamp'] ?? true;
  234. if (filter_var($timestamp_override, FILTER_VALIDATE_BOOLEAN)) {
  235. $options['timestamp'] = $this->timestamp;
  236. } else {
  237. $options['timestamp'] = null;
  238. }
  239. // Set order
  240. $group = $options['group'] ?? 'head';
  241. $position = $options['position'] ?? 'pipeline';
  242. $orderKey = "{$type}|{$group}|{$position}";
  243. if (!isset($this->order[$orderKey])) {
  244. $this->order[$orderKey] = 0;
  245. }
  246. $options['order'] = $this->order[$orderKey]++;
  247. // Create asset of correct type
  248. $asset_object = new $type();
  249. // If exists
  250. if ($asset_object->init($asset, $options)) {
  251. $this->$collection[md5($asset)] = $asset_object;
  252. }
  253. return $this;
  254. }
  255. /**
  256. * Add a CSS asset or a collection of assets.
  257. *
  258. * @return $this
  259. */
  260. public function addLink($asset)
  261. {
  262. return $this->addType($this::LINK_COLLECTION, $this::LINK_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::LINK_TYPE));
  263. }
  264. /**
  265. * Add a CSS asset or a collection of assets.
  266. *
  267. * @return $this
  268. */
  269. public function addCss($asset)
  270. {
  271. return $this->addType($this::CSS_COLLECTION, $this::CSS_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::CSS_TYPE));
  272. }
  273. /**
  274. * Add an Inline CSS asset or a collection of assets.
  275. *
  276. * @return $this
  277. */
  278. public function addInlineCss($asset)
  279. {
  280. return $this->addType($this::CSS_COLLECTION, $this::INLINE_CSS_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::INLINE_CSS_TYPE));
  281. }
  282. /**
  283. * Add a JS asset or a collection of assets.
  284. *
  285. * @return $this
  286. */
  287. public function addJs($asset)
  288. {
  289. return $this->addType($this::JS_COLLECTION, $this::JS_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::JS_TYPE));
  290. }
  291. /**
  292. * Add an Inline JS asset or a collection of assets.
  293. *
  294. * @return $this
  295. */
  296. public function addInlineJs($asset)
  297. {
  298. return $this->addType($this::JS_COLLECTION, $this::INLINE_JS_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::INLINE_JS_TYPE));
  299. }
  300. /**
  301. * Add a JS asset or a collection of assets.
  302. *
  303. * @return $this
  304. */
  305. public function addJsModule($asset)
  306. {
  307. return $this->addType($this::JS_MODULE_COLLECTION, $this::JS_MODULE_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::JS_MODULE_TYPE));
  308. }
  309. /**
  310. * Add an Inline JS asset or a collection of assets.
  311. *
  312. * @return $this
  313. */
  314. public function addInlineJsModule($asset)
  315. {
  316. return $this->addType($this::JS_MODULE_COLLECTION, $this::INLINE_JS_MODULE_TYPE, $asset, $this->unifyLegacyArguments(func_get_args(), $this::INLINE_JS_MODULE_TYPE));
  317. }
  318. /**
  319. * Add/replace collection.
  320. *
  321. * @param string $collectionName
  322. * @param array $assets
  323. * @param bool $overwrite
  324. * @return $this
  325. */
  326. public function registerCollection($collectionName, array $assets, $overwrite = false)
  327. {
  328. if ($overwrite || !isset($this->collections[$collectionName])) {
  329. $this->collections[$collectionName] = $assets;
  330. }
  331. return $this;
  332. }
  333. /**
  334. * @param array $assets
  335. * @param string $key
  336. * @param string $value
  337. * @param bool $sort
  338. * @return array|false
  339. */
  340. protected function filterAssets($assets, $key, $value, $sort = false)
  341. {
  342. $results = array_filter($assets, function ($asset) use ($key, $value) {
  343. if ($key === 'position' && $value === 'pipeline') {
  344. $type = $asset->getType();
  345. if ($asset->getRemote() && $this->{strtolower($type) . '_pipeline_include_externals'} === false && $asset['position'] === 'pipeline') {
  346. if ($this->{strtolower($type) . '_pipeline_before_excludes'}) {
  347. $asset->setPosition('after');
  348. } else {
  349. $asset->setPosition('before');
  350. }
  351. return false;
  352. }
  353. }
  354. if ($asset[$key] === $value) {
  355. return true;
  356. }
  357. return false;
  358. });
  359. if ($sort && !empty($results)) {
  360. $results = $this->sortAssets($results);
  361. }
  362. return $results;
  363. }
  364. /**
  365. * @param array $assets
  366. * @return array
  367. */
  368. protected function sortAssets($assets)
  369. {
  370. uasort($assets, static function ($a, $b) {
  371. return $b['priority'] <=> $a['priority'] ?: $a['order'] <=> $b['order'];
  372. });
  373. return $assets;
  374. }
  375. /**
  376. * @param string $type
  377. * @param string $group
  378. * @param array $attributes
  379. * @return string
  380. */
  381. public function render($type, $group = 'head', $attributes = [])
  382. {
  383. $before_output = '';
  384. $pipeline_output = '';
  385. $after_output = '';
  386. $assets = 'assets_' . $type;
  387. $pipeline_enabled = $type . '_pipeline';
  388. $render_pipeline = 'render' . ucfirst($type);
  389. $group_assets = $this->filterAssets($this->$assets, 'group', $group);
  390. $pipeline_assets = $this->filterAssets($group_assets, 'position', 'pipeline', true);
  391. $before_assets = $this->filterAssets($group_assets, 'position', 'before', true);
  392. $after_assets = $this->filterAssets($group_assets, 'position', 'after', true);
  393. // Pipeline
  394. if ($this->{$pipeline_enabled} ?? false) {
  395. $options = array_merge($this->pipeline_options, ['timestamp' => $this->timestamp]);
  396. $pipeline = new Pipeline($options);
  397. $pipeline_output = $pipeline->$render_pipeline($pipeline_assets, $group, $attributes);
  398. } else {
  399. foreach ($pipeline_assets as $asset) {
  400. $pipeline_output .= $asset->render();
  401. }
  402. }
  403. // Before Pipeline
  404. foreach ($before_assets as $asset) {
  405. $before_output .= $asset->render();
  406. }
  407. // After Pipeline
  408. foreach ($after_assets as $asset) {
  409. $after_output .= $asset->render();
  410. }
  411. return $before_output . $pipeline_output . $after_output;
  412. }
  413. /**
  414. * Build the CSS link tags.
  415. *
  416. * @param string $group name of the group
  417. * @param array $attributes
  418. * @return string
  419. */
  420. public function css($group = 'head', $attributes = [], $include_link = true)
  421. {
  422. $output = '';
  423. if ($include_link) {
  424. $output = $this->link($group, $attributes);
  425. }
  426. $output .= $this->render(self::CSS, $group, $attributes);
  427. return $output;
  428. }
  429. /**
  430. * Build the CSS link tags.
  431. *
  432. * @param string $group name of the group
  433. * @param array $attributes
  434. * @return string
  435. */
  436. public function link($group = 'head', $attributes = [])
  437. {
  438. return $this->render(self::LINK, $group, $attributes);
  439. }
  440. /**
  441. * Build the JavaScript script tags.
  442. *
  443. * @param string $group name of the group
  444. * @param array $attributes
  445. * @return string
  446. */
  447. public function js($group = 'head', $attributes = [], $include_js_module = true)
  448. {
  449. $output = $this->render(self::JS, $group, $attributes);
  450. if ($include_js_module) {
  451. $output .= $this->jsModule($group, $attributes);
  452. }
  453. return $output;
  454. }
  455. /**
  456. * Build the Javascript Modules tags
  457. *
  458. * @param string $group
  459. * @param array $attributes
  460. * @return string
  461. */
  462. public function jsModule($group = 'head', $attributes = [])
  463. {
  464. return $this->render(self::JS_MODULE, $group, $attributes);
  465. }
  466. /**
  467. * @param string $group
  468. * @param array $attributes
  469. * @return string
  470. */
  471. public function all($group = 'head', $attributes = [])
  472. {
  473. $output = $this->css($group, $attributes, false);
  474. $output .= $this->link($group, $attributes);
  475. $output .= $this->js($group, $attributes, false);
  476. $output .= $this->jsModule($group, $attributes);
  477. return $output;
  478. }
  479. /**
  480. * @param class-string $type
  481. * @return bool
  482. */
  483. protected function isValidType($type)
  484. {
  485. return in_array($type, [self::CSS_TYPE, self::JS_TYPE, self::JS_MODULE_TYPE]);
  486. }
  487. /**
  488. * @param class-string $type
  489. * @return string
  490. */
  491. protected function getBaseType($type)
  492. {
  493. switch ($type) {
  494. case $this::JS_TYPE:
  495. case $this::INLINE_JS_TYPE:
  496. $base_type = $this::JS;
  497. break;
  498. case $this::JS_MODULE_TYPE:
  499. case $this::INLINE_JS_MODULE_TYPE:
  500. $base_type = $this::JS_MODULE;
  501. break;
  502. default:
  503. $base_type = $this::CSS;
  504. }
  505. return $base_type;
  506. }
  507. }