Assets.php 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143
  1. <?php
  2. namespace Grav\Common;
  3. use Closure;
  4. use Exception;
  5. use FilesystemIterator;
  6. use Grav\Common\Config\Config;
  7. use RecursiveDirectoryIterator;
  8. use RecursiveIteratorIterator;
  9. use RegexIterator;
  10. define('CSS_ASSET', true);
  11. define('JS_ASSET', false);
  12. /**
  13. * Handles Asset management (CSS & JS) and also pipelining (combining into a single file for each asset)
  14. *
  15. * Based on stolz/assets (https://github.com/Stolz/Assets) package modified for use with Grav
  16. *
  17. * @author RocketTheme
  18. * @license MIT
  19. */
  20. class Assets
  21. {
  22. use GravTrait;
  23. /** @const Regex to match CSS and JavaScript files */
  24. const DEFAULT_REGEX = '/.\.(css|js)$/i';
  25. /** @const Regex to match CSS files */
  26. const CSS_REGEX = '/.\.css$/i';
  27. /** @const Regex to match JavaScript files */
  28. const JS_REGEX = '/.\.js$/i';
  29. /** @const Regex to match CSS urls */
  30. const CSS_URL_REGEX = '{url\([\'\"]?((?!http|//).*?)[\'\"]?\)}';
  31. /** @const Regex to match CSS sourcemap comments */
  32. const CSS_SOURCEMAP_REGEX = '{\/\*# (.*) \*\/}';
  33. /** @const Regex to match CSS import content */
  34. const CSS_IMPORT_REGEX = '{@import(.*);}';
  35. const HTML_TAG_REGEX = '#(<([A-Z][A-Z0-9]*)>)+(.*)(<\/\2>)#is';
  36. /**
  37. * Closure used by the pipeline to fetch assets.
  38. *
  39. * Useful when file_get_contents() function is not available in your PHP
  40. * installation or when you want to apply any kind of preprocessing to
  41. * your assets before they get pipelined.
  42. *
  43. * The closure will receive as the only parameter a string with the path/URL of the asset and
  44. * it should return the content of the asset file as a string.
  45. *
  46. * @var Closure
  47. */
  48. protected $fetch_command;
  49. // Configuration toggles to enable/disable the pipelining feature
  50. protected $css_pipeline = false;
  51. protected $js_pipeline = false;
  52. // The asset holding arrays
  53. protected $collections = array();
  54. protected $css = array();
  55. protected $js = array();
  56. protected $inline_css = array();
  57. protected $inline_js = array();
  58. // Some configuration variables
  59. protected $config;
  60. protected $base_url;
  61. protected $timestamp = '';
  62. protected $assets_dir;
  63. protected $assets_url;
  64. // Default values for pipeline settings
  65. protected $css_minify = true;
  66. protected $css_minify_windows = false;
  67. protected $css_rewrite = true;
  68. protected $js_minify = true;
  69. // Arrays to hold assets that should NOT be pipelined
  70. protected $css_no_pipeline = array();
  71. protected $js_no_pipeline = array();
  72. public function __construct(array $options = array())
  73. {
  74. // Forward config options
  75. if ($options) {
  76. $this->config((array)$options);
  77. }
  78. }
  79. /**
  80. * Set up configuration options.
  81. *
  82. * All the class properties except 'js' and 'css' are accepted here.
  83. * Also, an extra option 'autoload' may be passed containing an array of
  84. * assets and/or collections that will be automatically added on startup.
  85. *
  86. * @param array $config Configurable options.
  87. *
  88. * @return $this
  89. * @throws \Exception
  90. */
  91. public function config(array $config)
  92. {
  93. // Set pipeline modes
  94. if (isset($config['css_pipeline'])) {
  95. $this->css_pipeline = $config['css_pipeline'];
  96. }
  97. if (isset($config['js_pipeline'])) {
  98. $this->js_pipeline = $config['js_pipeline'];
  99. }
  100. // Pipeline requires public dir
  101. if (($this->js_pipeline || $this->css_pipeline) && !is_dir($this->assets_dir)) {
  102. throw new \Exception('Assets: Public dir not found');
  103. }
  104. // Set custom pipeline fetch command
  105. if (isset($config['fetch_command']) && ($config['fetch_command'] instanceof Closure)) {
  106. $this->fetch_command = $config['fetch_command'];
  107. }
  108. // Set CSS Minify state
  109. if (isset($config['css_minify'])) {
  110. $this->css_minify = $config['css_minify'];
  111. }
  112. if (isset($config['css_minify_windows'])) {
  113. $this->css_minify_windows = $config['css_minify_windows'];
  114. }
  115. if (isset($config['css_rewrite'])) {
  116. $this->css_rewrite = $config['css_rewrite'];
  117. }
  118. // Set JS Minify state
  119. if (isset($config['js_minify'])) {
  120. $this->js_minify = $config['js_minify'];
  121. }
  122. // Set collections
  123. if (isset($config['collections']) && is_array($config['collections'])) {
  124. $this->collections = $config['collections'];
  125. }
  126. // Autoload assets
  127. if (isset($config['autoload']) && is_array($config['autoload'])) {
  128. foreach ($config['autoload'] as $asset) {
  129. $this->add($asset);
  130. }
  131. }
  132. // Set timestamp
  133. if (isset($config['enable_asset_timestamp']) && $config['enable_asset_timestamp'] === true) {
  134. $this->timestamp = '?' . self::getGrav()['cache']->getKey();
  135. }
  136. return $this;
  137. }
  138. /**
  139. * Initialization called in the Grav lifecycle to initialize the Assets with appropriate configuration
  140. */
  141. public function init()
  142. {
  143. /** @var Config $config */
  144. $config = self::getGrav()['config'];
  145. $base_url = self::getGrav()['base_url'];
  146. $asset_config = (array)$config->get('system.assets');
  147. /** @var Locator $locator */
  148. $locator = self::$grav['locator'];
  149. $this->assets_dir = self::getGrav()['locator']->findResource('asset://') . DS;
  150. $this->assets_url = self::getGrav()['locator']->findResource('asset://', false);
  151. $this->config($asset_config);
  152. $this->base_url = $base_url . '/';
  153. // Register any preconfigured collections
  154. foreach ($config->get('system.assets.collections') as $name => $collection) {
  155. $this->registerCollection($name, (array)$collection);
  156. }
  157. }
  158. /**
  159. * Add an asset or a collection of assets.
  160. *
  161. * It automatically detects the asset type (JavaScript, CSS or collection).
  162. * You may add more than one asset passing an array as argument.
  163. *
  164. * @param mixed $asset
  165. * @param int $priority the priority, bigger comes first
  166. * @param bool $pipeline false if this should not be pipelined
  167. *
  168. * @return $this
  169. */
  170. public function add($asset, $priority = null, $pipeline = null)
  171. {
  172. // More than one asset
  173. if (is_array($asset)) {
  174. foreach ($asset as $a) {
  175. $this->add($a, $priority, $pipeline);
  176. }
  177. } elseif (isset($this->collections[$asset])) {
  178. $this->add($this->collections[$asset], $priority, $pipeline);
  179. } else {
  180. // Get extension
  181. $extension = pathinfo(parse_url($asset, PHP_URL_PATH), PATHINFO_EXTENSION);
  182. // JavaScript or CSS
  183. if (strlen($extension) > 0) {
  184. $extension = strtolower($extension);
  185. if ($extension === 'css') {
  186. $this->addCss($asset, $priority, $pipeline);
  187. } elseif ($extension === 'js') {
  188. $this->addJs($asset, $priority, $pipeline);
  189. }
  190. }
  191. }
  192. return $this;
  193. }
  194. /**
  195. * Add a CSS asset.
  196. *
  197. * It checks for duplicates.
  198. * You may add more than one asset passing an array as argument.
  199. *
  200. * @param mixed $asset
  201. * @param int $priority the priority, bigger comes first
  202. * @param bool $pipeline false if this should not be pipelined
  203. * @param null $group
  204. *
  205. * @return $this
  206. */
  207. public function addCss($asset, $priority = null, $pipeline = null, $group = null)
  208. {
  209. if (is_array($asset)) {
  210. foreach ($asset as $a) {
  211. $this->addCss($a, $priority, $pipeline, $group);
  212. }
  213. return $this;
  214. } elseif (isset($this->collections[$asset])) {
  215. $this->add($this->collections[$asset], $priority, $pipeline, $group);
  216. return $this;
  217. }
  218. if (!$this->isRemoteLink($asset)) {
  219. $asset = $this->buildLocalLink($asset);
  220. }
  221. $data = [
  222. 'asset' => $asset,
  223. 'priority' => intval($priority ?: 10),
  224. 'order' => count($this->css),
  225. 'pipeline' => $pipeline ?: true,
  226. 'group' => $group ?: 'head'
  227. ];
  228. // check for dynamic array and merge with defaults
  229. $count_args = func_num_args();
  230. if (func_num_args() == 2) {
  231. $dynamic_arg = func_get_arg(1);
  232. if (is_array($dynamic_arg)) {
  233. $data = array_merge($data, $dynamic_arg);
  234. }
  235. }
  236. $key = md5($asset);
  237. if ($asset) {
  238. $this->css[$key] = $data;
  239. }
  240. return $this;
  241. }
  242. /**
  243. * Add a JavaScript asset.
  244. *
  245. * It checks for duplicates.
  246. * You may add more than one asset passing an array as argument.
  247. *
  248. * @param mixed $asset
  249. * @param int $priority the priority, bigger comes first
  250. * @param bool $pipeline false if this should not be pipelined
  251. * @param string $loading how the asset is loaded (async/defer)
  252. * @param string $group name of the group
  253. * @return $this
  254. */
  255. public function addJs($asset, $priority = null, $pipeline = null, $loading = null, $group = null)
  256. {
  257. if (is_array($asset)) {
  258. foreach ($asset as $a) {
  259. $this->addJs($a, $priority, $pipeline, $loading, $group);
  260. }
  261. return $this;
  262. } elseif (isset($this->collections[$asset])) {
  263. $this->add($this->collections[$asset], $priority, $pipeline, $loading, $group);
  264. return $this;
  265. }
  266. if (!$this->isRemoteLink($asset)) {
  267. $asset = $this->buildLocalLink($asset);
  268. }
  269. $data = [
  270. 'asset' => $asset,
  271. 'priority' => intval($priority ?: 10),
  272. 'order' => count($this->js),
  273. 'pipeline' => $pipeline ?: true,
  274. 'loading' => $loading ?: '',
  275. 'group' => $group ?: 'head'
  276. ];
  277. // check for dynamic array and merge with defaults
  278. $count_args = func_num_args();
  279. if (func_num_args() == 2) {
  280. $dynamic_arg = func_get_arg(1);
  281. if (is_array($dynamic_arg)) {
  282. $data = array_merge($data, $dynamic_arg);
  283. }
  284. }
  285. $key = md5($asset);
  286. if ($asset) {
  287. $this->js[$key] = $data;
  288. }
  289. return $this;
  290. }
  291. /**
  292. * Convenience wrapper for async loading of JavaScript
  293. *
  294. * @param $asset
  295. * @param int $priority
  296. * @param bool $pipeline
  297. * @param string $group name of the group
  298. *
  299. * @deprecated Please use dynamic method with ['loading' => 'async']
  300. *
  301. * @return \Grav\Common\Assets
  302. */
  303. public function addAsyncJs($asset, $priority = null, $pipeline = null, $group = null)
  304. {
  305. return $this->addJs($asset, $priority, $pipeline, 'async', $group);
  306. }
  307. /**
  308. * Convenience wrapper for deferred loading of JavaScript
  309. *
  310. * @param $asset
  311. * @param int $priority
  312. * @param bool $pipeline
  313. * @param string $group name of the group
  314. *
  315. * @deprecated Please use dynamic method with ['loading' => 'defer']
  316. *
  317. * @return \Grav\Common\Assets
  318. */
  319. public function addDeferJs($asset, $priority = null, $pipeline = null, $group = null)
  320. {
  321. return $this->addJs($asset, $priority, $pipeline, 'defer', $group);
  322. }
  323. /**
  324. * Add an inline CSS asset.
  325. *
  326. * It checks for duplicates.
  327. * For adding chunks of string-based inline CSS
  328. *
  329. * @param mixed $asset
  330. * @param int $priority the priority, bigger comes first
  331. * @param null $group
  332. *
  333. * @return $this
  334. */
  335. public function addInlineCss($asset, $priority = null, $group = null)
  336. {
  337. $asset = trim($asset);
  338. if (is_a($asset, 'Twig_Markup')) {
  339. preg_match(self::HTML_TAG_REGEX, $asset, $matches );
  340. if (isset($matches[3])) {
  341. $asset = $matches[3];
  342. }
  343. }
  344. $data = [
  345. 'priority' => intval($priority ?: 10),
  346. 'order' => count($this->inline_css),
  347. 'asset' => $asset,
  348. 'group' => $group ?: 'head'
  349. ];
  350. // check for dynamic array and merge with defaults
  351. $count_args = func_num_args();
  352. if (func_num_args() == 2) {
  353. $dynamic_arg = func_get_arg(1);
  354. if (is_array($dynamic_arg)) {
  355. $data = array_merge($data, $dynamic_arg);
  356. }
  357. }
  358. $key = md5($asset);
  359. if (is_string($asset) && !array_key_exists($key, $this->inline_css)) {
  360. $this->inline_css[$key] = $data;
  361. }
  362. return $this;
  363. }
  364. /**
  365. * Add an inline JS asset.
  366. *
  367. * It checks for duplicates.
  368. * For adding chunks of string-based inline JS
  369. *
  370. * @param mixed $asset
  371. * @param int $priority the priority, bigger comes first
  372. * @param string $group name of the group
  373. *
  374. * @return $this
  375. */
  376. public function addInlineJs($asset, $priority = null, $group = null)
  377. {
  378. $asset = trim($asset);
  379. if (is_a($asset, 'Twig_Markup')) {
  380. preg_match(self::HTML_TAG_REGEX, $asset, $matches );
  381. if (isset($matches[3])) {
  382. $asset = $matches[3];
  383. }
  384. }
  385. $data = [
  386. 'asset' => $asset,
  387. 'priority' => intval($priority ?: 10),
  388. 'order' => count($this->js),
  389. 'group' => $group ?: 'head'
  390. ];
  391. // check for dynamic array and merge with defaults
  392. $count_args = func_num_args();
  393. if (func_num_args() == 2) {
  394. $dynamic_arg = func_get_arg(1);
  395. if (is_array($dynamic_arg)) {
  396. $data = array_merge($data, $dynamic_arg);
  397. }
  398. }
  399. $key = md5($asset);
  400. if (is_string($asset) && !array_key_exists($key, $this->inline_js)) {
  401. $this->inline_js[$key] = $data;
  402. }
  403. return $this;
  404. }
  405. /**
  406. * Build the CSS link tags.
  407. *
  408. * @param string $group name of the group
  409. * @param array $attributes
  410. *
  411. * @return string
  412. */
  413. public function css($group = 'head', $attributes = [])
  414. {
  415. if (!$this->css) {
  416. return null;
  417. }
  418. // Sort array by priorities (larger priority first)
  419. if (self::getGrav()) {
  420. usort($this->css, function ($a, $b) {
  421. if ($a['priority'] == $b['priority']) {
  422. return $b['order'] - $a['order'];
  423. }
  424. return $a['priority'] - $b['priority'];
  425. });
  426. usort($this->inline_css, function ($a, $b) {
  427. if ($a['priority'] == $b['priority']) {
  428. return $b['order'] - $a['order'];
  429. }
  430. return $a['priority'] - $b['priority'];
  431. });
  432. }
  433. $this->css = array_reverse($this->css);
  434. $this->inline_css = array_reverse($this->inline_css);
  435. $attributes = $this->attributes(array_merge(['type' => 'text/css', 'rel' => 'stylesheet'], $attributes));
  436. $output = '';
  437. $inline_css = '';
  438. if ($this->css_pipeline) {
  439. $pipeline_result = $this->pipelineCss($group);
  440. if ($pipeline_result) {
  441. $output .= '<link href="' . $pipeline_result . '"' . $attributes . ' />' . "\n";
  442. }
  443. foreach ($this->css_no_pipeline as $file) {
  444. if ($group && $file['group'] == $group) {
  445. $media = isset($file['media']) ? sprintf(' media="%s"', $file['media']) : '';
  446. $output .= '<link href="' . $file['asset'] . $this->timestamp . '"' . $attributes . $media . ' />' . "\n";
  447. }
  448. }
  449. } else {
  450. foreach ($this->css as $file) {
  451. if ($group && $file['group'] == $group) {
  452. $media = isset($file['media']) ? sprintf(' media="%s"', $file['media']) : '';
  453. $output .= '<link href="' . $file['asset'] . $this->timestamp . '"' . $attributes . $media . ' />' . "\n";
  454. }
  455. }
  456. }
  457. // Render Inline CSS
  458. foreach ($this->inline_css as $inline) {
  459. if ($group && $inline['group'] == $group) {
  460. $inline_css .= $inline['asset'] . "\n";
  461. }
  462. }
  463. if ($inline_css) {
  464. $output .= "\n<style>\n" . $inline_css . "\n</style>\n";
  465. }
  466. return $output;
  467. }
  468. /**
  469. * Build the JavaScript script tags.
  470. *
  471. * @param string $group name of the group
  472. * @param array $attributes
  473. *
  474. * @return string
  475. */
  476. public function js($group = 'head', $attributes = [])
  477. {
  478. if (!$this->js) {
  479. return null;
  480. }
  481. // Sort array by priorities (larger priority first)
  482. usort($this->js, function ($a, $b) {
  483. if ($a['priority'] == $b['priority']) {
  484. return $b['order'] - $a['order'];
  485. }
  486. return $a['priority'] - $b['priority'];
  487. });
  488. usort($this->inline_js, function ($a, $b) {
  489. if ($a['priority'] == $b['priority']) {
  490. return $b['order'] - $a['order'];
  491. }
  492. return $a['priority'] - $b['priority'];
  493. });
  494. $this->js = array_reverse($this->js);
  495. $this->inline_js = array_reverse($this->inline_js);
  496. $attributes = $this->attributes(array_merge(['type' => 'text/javascript'], $attributes));
  497. $output = '';
  498. $inline_js = '';
  499. if ($this->js_pipeline) {
  500. $pipeline_result = $this->pipelineJs($group);
  501. if ($pipeline_result) {
  502. $output .= '<script src="' . $pipeline_result . '"' . $attributes . ' ></script>' . "\n";
  503. }
  504. foreach ($this->js_no_pipeline as $file) {
  505. if ($group && $file['group'] == $group) {
  506. $output .= '<script src="' . $file['asset'] . $this->timestamp . '"' . $attributes . ' ' . $file['loading']. '></script>' . "\n";
  507. }
  508. }
  509. } else {
  510. foreach ($this->js as $file) {
  511. if ($group && $file['group'] == $group) {
  512. $output .= '<script src="' . $file['asset'] . $this->timestamp . '"' . $attributes . ' ' . $file['loading'] . '></script>' . "\n";
  513. }
  514. }
  515. }
  516. // Render Inline JS
  517. foreach ($this->inline_js as $inline) {
  518. if ($group && $inline['group'] == $group) {
  519. $inline_js .= $inline['asset'] . "\n";
  520. }
  521. }
  522. if ($inline_js) {
  523. $output .= "\n<script>\n" . $inline_js . "\n</script>\n";
  524. }
  525. return $output;
  526. }
  527. /**
  528. * Minify and concatenate CSS.
  529. *
  530. * @return string
  531. */
  532. protected function pipelineCss($group = 'head')
  533. {
  534. /** @var Cache $cache */
  535. $cache = self::getGrav()['cache'];
  536. $key = '?' . $cache->getKey();
  537. // temporary list of assets to pipeline
  538. $temp_css = [];
  539. // clear no-pipeline assets lists
  540. $this->css_no_pipeline = [];
  541. $file = md5(json_encode($this->css) . $this->css_minify . $this->css_rewrite . $group) . '.css';
  542. $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
  543. $absolute_path = $this->assets_dir . $file;
  544. // If pipeline exist return it
  545. if (file_exists($absolute_path)) {
  546. return $relative_path . $key;
  547. }
  548. // Remove any non-pipeline files
  549. foreach ($this->css as $id => $asset) {
  550. if ($asset['group'] == $group) {
  551. if (!$asset['pipeline']) {
  552. $this->css_no_pipeline[$id] = $asset;
  553. } else {
  554. $temp_css[$id] = $asset;
  555. }
  556. }
  557. }
  558. //if nothing found get out of here!
  559. if (count($temp_css) == 0) {
  560. return false;
  561. }
  562. $css_minify = $this->css_minify;
  563. // If this is a Windows server, and minify_windows is false (default value) skip the
  564. // minification process because it will cause Apache to die/crash due to insufficient
  565. // ThreadStackSize in httpd.conf - See: https://bugs.php.net/bug.php?id=47689
  566. if (strtoupper(substr(php_uname('s'), 0, 3)) === 'WIN' && !$this->css_minify_windows) {
  567. $css_minify = false;
  568. }
  569. // Concatenate files
  570. $buffer = $this->gatherLinks($temp_css, CSS_ASSET);
  571. if ($css_minify) {
  572. $min = new \CSSmin();
  573. $buffer = $min->run($buffer);
  574. }
  575. // Write file
  576. if (strlen(trim($buffer)) > 0) {
  577. file_put_contents($absolute_path, $buffer);
  578. return $relative_path . $key;
  579. } else {
  580. return false;
  581. }
  582. }
  583. /**
  584. * Minify and concatenate JS files.
  585. *
  586. * @return string
  587. */
  588. protected function pipelineJs($group = 'head')
  589. {
  590. /** @var Cache $cache */
  591. $cache = self::getGrav()['cache'];
  592. $key = '?' . $cache->getKey();
  593. // temporary list of assets to pipeline
  594. $temp_js = [];
  595. // clear no-pipeline assets lists
  596. $this->js_no_pipeline = [];
  597. $file = md5(json_encode($this->js) . $this->js_minify . $group) . '.js';
  598. $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
  599. $absolute_path = $this->assets_dir . $file;
  600. // If pipeline exist return it
  601. if (file_exists($absolute_path)) {
  602. return $relative_path . $key;
  603. }
  604. // Remove any non-pipeline files
  605. foreach ($this->js as $id => $asset) {
  606. if ($asset['group'] == $group) {
  607. if (!$asset['pipeline']) {
  608. $this->js_no_pipeline[] = $asset;
  609. } else {
  610. $temp_js[$id] = $asset;
  611. }
  612. }
  613. }
  614. //if nothing found get out of here!
  615. if (count($temp_js) == 0) {
  616. return false;
  617. }
  618. // Concatenate files
  619. $buffer = $this->gatherLinks($temp_js, JS_ASSET);
  620. if ($this->js_minify) {
  621. $buffer = \JSMin::minify($buffer);
  622. }
  623. // Write file
  624. if (strlen(trim($buffer)) > 0) {
  625. file_put_contents($absolute_path, $buffer);
  626. return $relative_path . $key;
  627. } else {
  628. return false;
  629. }
  630. }
  631. /**
  632. * Return the array of all the registered CSS assets
  633. *
  634. * @return array
  635. */
  636. public function getCss()
  637. {
  638. return $this->css;
  639. }
  640. /**
  641. * Return the array of all the registered JS assets
  642. *
  643. * @return array
  644. */
  645. public function getJs()
  646. {
  647. return $this->js;
  648. }
  649. /**
  650. * Return the array of all the registered collections
  651. *
  652. * @return array
  653. */
  654. public function getCollections()
  655. {
  656. return $this->collections;
  657. }
  658. /**
  659. * Determines if an asset exists as a collection, CSS or JS reference
  660. *
  661. * @param $asset
  662. *
  663. * @return bool
  664. */
  665. public function exists($asset)
  666. {
  667. if (isset($this->collections[$asset]) ||
  668. isset($this->css[$asset]) ||
  669. isset($this->js[$asset])) {
  670. return true;
  671. } else {
  672. return false;
  673. }
  674. }
  675. /**
  676. * Add/replace collection.
  677. *
  678. * @param string $collectionName
  679. * @param array $assets
  680. * @param bool $overwrite
  681. *
  682. * @return $this
  683. */
  684. public function registerCollection($collectionName, Array $assets, $overwrite = false)
  685. {
  686. if ($overwrite || !isset($this->collections[$collectionName])) {
  687. $this->collections[$collectionName] = $assets;
  688. }
  689. return $this;
  690. }
  691. /**
  692. * Reset all assets.
  693. *
  694. * @return $this
  695. */
  696. public function reset()
  697. {
  698. return $this->resetCss()->resetJs();
  699. }
  700. /**
  701. * Reset JavaScript assets.
  702. *
  703. * @return $this
  704. */
  705. public function resetJs()
  706. {
  707. $this->js = array();
  708. return $this;
  709. }
  710. /**
  711. * Reset CSS assets.
  712. *
  713. * @return $this
  714. */
  715. public function resetCss()
  716. {
  717. $this->css = array();
  718. return $this;
  719. }
  720. /**
  721. * Add all CSS assets within $directory (relative to public dir).
  722. *
  723. * @param string $directory Relative to $this->public_dir
  724. *
  725. * @return $this
  726. */
  727. public function addDirCss($directory)
  728. {
  729. return $this->addDir($directory, self::CSS_REGEX);
  730. }
  731. /**
  732. * Add all assets matching $pattern within $directory.
  733. *
  734. * @param string $directory Relative to $this->public_dir
  735. * @param string $pattern (regex)
  736. *
  737. * @return $this
  738. * @throws Exception
  739. */
  740. public function addDir($directory, $pattern = self::DEFAULT_REGEX)
  741. {
  742. // Check if public_dir exists
  743. if (!is_dir($this->assets_dir)) {
  744. throw new Exception('Assets: Public dir not found');
  745. }
  746. // Get files
  747. $files = $this->rglob($this->assets_dir . DIRECTORY_SEPARATOR . $directory, $pattern, $this->assets_dir);
  748. // No luck? Nothing to do
  749. if (!$files) {
  750. return $this;
  751. }
  752. // Add CSS files
  753. if ($pattern === self::CSS_REGEX) {
  754. $this->css = array_unique(array_merge($this->css, $files));
  755. return $this;
  756. }
  757. // Add JavaScript files
  758. if ($pattern === self::JS_REGEX) {
  759. $this->js = array_unique(array_merge($this->js, $files));
  760. return $this;
  761. }
  762. // Unknown pattern. We must poll to know the extension :(
  763. foreach ($files as $asset) {
  764. $info = pathinfo($asset);
  765. if (isset($info['extension'])) {
  766. $ext = strtolower($info['extension']);
  767. if ($ext === 'css' && !in_array($asset, $this->css)) {
  768. $this->css[] = $asset;
  769. } elseif ($ext === 'js' && !in_array($asset, $this->js)) {
  770. $this->js[] = $asset;
  771. }
  772. }
  773. }
  774. return $this;
  775. }
  776. /**
  777. * Determine whether a link is local or remote.
  778. *
  779. * Understands both "http://" and "https://" as well as protocol agnostic links "//"
  780. *
  781. * @param string $link
  782. *
  783. * @return bool
  784. */
  785. protected function isRemoteLink($link)
  786. {
  787. return ('http://' === substr($link, 0, 7) || 'https://' === substr($link, 0, 8)
  788. || '//' === substr($link, 0, 2));
  789. }
  790. /**
  791. * Build local links including grav asset shortcodes
  792. *
  793. * @param string $asset the asset string reference
  794. *
  795. * @return string the final link url to the asset
  796. */
  797. protected function buildLocalLink($asset)
  798. {
  799. try {
  800. $asset = self::getGrav()['locator']->findResource($asset, false);
  801. } catch (\Exception $e) {
  802. }
  803. return $asset ? $this->base_url . ltrim($asset, '/') : false;
  804. }
  805. /**
  806. * Build an HTML attribute string from an array.
  807. *
  808. * @param array $attributes
  809. *
  810. * @return string
  811. */
  812. protected function attributes(array $attributes)
  813. {
  814. $html = '';
  815. foreach ($attributes as $key => $value) {
  816. // For numeric keys we will assume that the key and the value are the same
  817. // as this will convert HTML attributes such as "required" to a correct
  818. // form like required="required" instead of using incorrect numerics.
  819. if (is_numeric($key)) {
  820. $key = $value;
  821. }
  822. if (is_array($value)) {
  823. $value = implode(' ', $value);
  824. }
  825. $element = $key . '="' . htmlentities($value, ENT_QUOTES, 'UTF-8', false) . '"';
  826. $html .= ' ' . $element;
  827. }
  828. return $html;
  829. }
  830. /**
  831. * Download and concatenate the content of several links.
  832. *
  833. * @param array $links
  834. * @param bool $css
  835. *
  836. * @return string
  837. */
  838. protected function gatherLinks(array $links, $css = true)
  839. {
  840. $buffer = '';
  841. $local = true;
  842. foreach ($links as $asset) {
  843. $link = $asset['asset'];
  844. $relative_path = $link;
  845. if ($this->isRemoteLink($link)) {
  846. $local = false;
  847. if ('//' === substr($link, 0, 2)) {
  848. $link = 'http:' . $link;
  849. }
  850. } else {
  851. // Fix to remove relative dir if grav is in one
  852. if (($this->base_url != '/') && (strpos($this->base_url, $link) == 0)) {
  853. $base_url = '#' . preg_quote($this->base_url, '#') . '#';
  854. $relative_path = ltrim(preg_replace($base_url, '/', $link, 1), '/');
  855. }
  856. $relative_dir = dirname($relative_path);
  857. $link = ROOT_DIR . $relative_path;
  858. }
  859. $file = ($this->fetch_command instanceof Closure) ? @$this->fetch_command->__invoke($link) : @file_get_contents($link);
  860. // No file found, skip it...
  861. if ($file === false) {
  862. continue;
  863. }
  864. // Double check last character being
  865. if (!$css) {
  866. $file = rtrim($file, ' ;') . ';';
  867. }
  868. // If this is CSS + the file is local + rewrite enabled
  869. if ($css && $local && $this->css_rewrite) {
  870. $file = $this->cssRewrite($file, $relative_dir);
  871. }
  872. $buffer .= $file;
  873. }
  874. // Pull out @imports and move to top
  875. if ($css) {
  876. $buffer = $this->moveImports($buffer);
  877. }
  878. return $buffer;
  879. }
  880. /**
  881. * Finds relative CSS urls() and rewrites the URL with an absolute one
  882. *
  883. * @param $file the css source file
  884. * @param $relative_path relative path to the css file
  885. *
  886. * @return mixed
  887. */
  888. protected function cssRewrite($file, $relative_path)
  889. {
  890. // Strip any sourcemap comments
  891. $file = preg_replace(self::CSS_SOURCEMAP_REGEX, '', $file);
  892. // Find any css url() elements, grab the URLs and calculate an absolute path
  893. // Then replace the old url with the new one
  894. $file = preg_replace_callback(
  895. self::CSS_URL_REGEX,
  896. function ($matches) use ($relative_path) {
  897. $old_url = $matches[1];
  898. // ensure this is not a data url
  899. if (strpos($old_url, 'data:') === 0) {
  900. return $matches[0];
  901. }
  902. $new_url = $this->base_url . ltrim(Utils::normalizePath($relative_path . '/' . $old_url), '/');
  903. return str_replace($old_url, $new_url, $matches[0]);
  904. },
  905. $file
  906. );
  907. return $file;
  908. }
  909. /**
  910. * Moves @import statements to the top of the file per the CSS specification
  911. *
  912. * @param string $file the file containing the combined CSS files
  913. *
  914. * @return string the modified file with any @imports at the top of the file
  915. */
  916. protected function moveImports($file)
  917. {
  918. $this->imports = array();
  919. $file = preg_replace_callback(
  920. self::CSS_IMPORT_REGEX,
  921. function ($matches) {
  922. $this->imports[] = $matches[0];
  923. return '';
  924. },
  925. $file
  926. );
  927. return implode("\n", $this->imports) . "\n\n" . $file;
  928. }
  929. /**
  930. * Recursively get files matching $pattern within $directory.
  931. *
  932. * @param string $directory
  933. * @param string $pattern (regex)
  934. * @param string $ltrim Will be trimmed from the left of the file path
  935. *
  936. * @return array
  937. */
  938. protected function rglob($directory, $pattern, $ltrim = null)
  939. {
  940. $iterator = new RegexIterator(
  941. new RecursiveIteratorIterator(
  942. new RecursiveDirectoryIterator(
  943. $directory,
  944. FilesystemIterator::SKIP_DOTS
  945. )
  946. ),
  947. $pattern
  948. );
  949. $offset = strlen($ltrim);
  950. $files = array();
  951. foreach ($iterator as $file) {
  952. $files[] = substr($file->getPathname(), $offset);
  953. }
  954. return $files;
  955. }
  956. /**
  957. * Add all JavaScript assets within $directory.
  958. *
  959. * @param string $directory Relative to $this->public_dir
  960. *
  961. * @return $this
  962. */
  963. public function addDirJs($directory)
  964. {
  965. return $this->addDir($directory, self::JS_REGEX);
  966. }
  967. public function __toString()
  968. {
  969. return '';
  970. }
  971. /**
  972. * @param $a
  973. * @param $b
  974. *
  975. * @return mixed
  976. */
  977. protected function priorityCompare($a, $b)
  978. {
  979. return $a ['priority'] - $b ['priority'];
  980. }
  981. }