From 985253befa64bd85b74362f9f34b2daddf242b22 Mon Sep 17 00:00:00 2001 From: Oliver O Date: Thu, 23 Mar 2017 14:47:10 +0100 Subject: [PATCH] Add {'loading': 'inline'} option for Js and CSS assets * Assets.php: Extract common functionaly of addCss() and addJs() into addTo(), make css() and js() honor $attributes['loading'], make pipelineCss() and pipelineJs() return generated content instead of URL depending on new option $returnURL. * AssetsTest.php: Accept new 'loading' option for CSS, add tests for adding and pipelining local and remote assets with inline loading. --- system/src/Grav/Common/Assets.php | 216 ++++++++++++++++---------- tests/unit/Grav/Common/AssetsTest.php | 39 +++++ 2 files changed, 170 insertions(+), 85 deletions(-) diff --git a/system/src/Grav/Common/Assets.php b/system/src/Grav/Common/Assets.php index eeb3adabf..4de57376d 100644 --- a/system/src/Grav/Common/Assets.php +++ b/system/src/Grav/Common/Assets.php @@ -255,28 +255,32 @@ public function add($asset, $priority = null, $pipeline = true) } /** - * Add a CSS asset. + * Add an asset to its assembly. * * It checks for duplicates. * You may add more than one asset passing an array as argument. + * The third argument may alternatively contain an array of options which take precedence over positional + * arguments. * - * @param mixed $asset - * @param int $priority the priority, bigger comes first - * @param bool $pipeline false if this should not be pipelined - * @param null $group + * @param array $assembly the array assembling the assets + * @param mixed $asset + * @param int $priority the priority, bigger comes first + * @param bool $pipeline false if this should not be pipelined + * @param string $loading how the asset is loaded (async/defer/inline, for CSS: only inline) + * @param string $group name of the group * * @return $this */ - public function addCss($asset, $priority = null, $pipeline = true, $group = null) + public function addTo(&$assembly, $asset, $priority = null, $pipeline = true, $loading = null, $group = null) { if (is_array($asset)) { foreach ($asset as $a) { - $this->addCss($a, $priority, $pipeline, $group); + $this->addTo($assembly, $a, $priority, $pipeline, $loading, $group); } return $this; } elseif (isset($this->collections[$asset])) { - $this->addCss($this->collections[$asset], $priority, $pipeline, $group); + $this->addTo($assembly, $this->collections[$asset], $priority, $pipeline, $loading, $group); return $this; } @@ -297,15 +301,16 @@ public function addCss($asset, $priority = null, $pipeline = true, $group = null 'asset' => $asset, 'remote' => $remote, 'priority' => intval($priority ?: 10), - 'order' => count($this->css), + 'order' => count($assembly), 'pipeline' => (bool) $pipeline, + 'loading' => $loading ?: '', 'group' => $group ?: 'head', 'modified' => $modified ]; // check for dynamic array and merge with defaults - if (func_num_args() > 1) { - $dynamic_arg = func_get_arg(1); + if (func_num_args() > 2) { + $dynamic_arg = func_get_arg(2); if (is_array($dynamic_arg)) { $data = array_merge($data, $dynamic_arg); } @@ -313,17 +318,40 @@ public function addCss($asset, $priority = null, $pipeline = true, $group = null $key = md5($asset); if ($asset) { - $this->css[$key] = $data; + $assembly[$key] = $data; } return $this; } + /** + * Add a CSS asset. + * + * It checks for duplicates. + * You may add more than one asset passing an array as argument. + * The second argument may alternatively contain an array of options which take precedence over positional + * arguments. + * + * @param mixed $asset + * @param int $priority the priority, bigger comes first + * @param bool $pipeline false if this should not be pipelined + * @param string $group + * @param string $loading how the asset is loaded (async/defer/inline, for CSS: only inline) + * + * @return $this + */ + public function addCss($asset, $priority = null, $pipeline = true, $group = null, $loading = null) + { + return $this->addTo($this->css, $asset, $priority, $pipeline, $loading, $group); + } + /** * Add a JavaScript asset. * * It checks for duplicates. * You may add more than one asset passing an array as argument. + * The second argument may alternatively contain an array of options which take precedence over positional + * arguments. * * @param mixed $asset * @param int $priority the priority, bigger comes first @@ -335,55 +363,7 @@ public function addCss($asset, $priority = null, $pipeline = true, $group = null */ public function addJs($asset, $priority = null, $pipeline = true, $loading = null, $group = null) { - if (is_array($asset)) { - foreach ($asset as $a) { - $this->addJs($a, $priority, $pipeline, $loading, $group); - } - - return $this; - } elseif (isset($this->collections[$asset])) { - $this->addJs($this->collections[$asset], $priority, $pipeline, $loading, $group); - - return $this; - } - - $modified = false; - $remote = $this->isRemoteLink($asset); - if (!$remote) { - $modified = $this->getLastModificationTime($asset); - $asset = $this->buildLocalLink($asset); - } - - // Check for existence - if ($asset === false) { - return $this; - } - - $data = [ - 'asset' => $asset, - 'remote' => $remote , - 'priority' => intval($priority ?: 10), - 'order' => count($this->js), - 'pipeline' => (bool) $pipeline, - 'loading' => $loading ?: '', - 'group' => $group ?: 'head', - 'modified' => $modified - ]; - - // check for dynamic array and merge with defaults - if (func_num_args() > 1) { - $dynamic_arg = func_get_arg(1); - if (is_array($dynamic_arg)) { - $data = array_merge($data, $dynamic_arg); - } - } - - $key = md5($asset); - if ($asset) { - $this->js[$key] = $data; - } - - return $this; + return $this->addTo($this->js, $asset, $priority, $pipeline, $loading, $group); } /** @@ -547,32 +527,54 @@ public function css($group = 'head', $attributes = []) $this->css = array_reverse($this->css); $this->inline_css = array_reverse($this->inline_css); + $inlineGroup = array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline'; + $attributes = $this->attributes(array_merge(['type' => 'text/css', 'rel' => 'stylesheet'], $attributes)); $output = ''; $inline_css = ''; if ($this->css_pipeline) { - $pipeline_result = $this->pipelineCss($group); - $pipeline_html = '' . "\n"; + $pipeline_result = $this->pipelineCss($group, !$inlineGroup); + $pipeline_html = ($inlineGroup ? '' : '' . "\n"); if ($this->css_pipeline_before_excludes && $pipeline_result) { - $output .= $pipeline_html; + if ($inlineGroup) { + $inline_css .= $pipeline_result; + } + else { + $output .= $pipeline_html; + } } foreach ($this->css_no_pipeline as $file) { if ($group && $file['group'] == $group) { - $media = isset($file['media']) ? sprintf(' media="%s"', $file['media']) : ''; - $output .= '' . "\n"; + if ($file['loading'] === 'inline') { + $inline_css .= $this->gatherLinks([$file], CSS_ASSET) . "\n"; + } + else { + $media = isset($file['media']) ? sprintf(' media="%s"', $file['media']) : ''; + $output .= '' . "\n"; + } } } if (!$this->css_pipeline_before_excludes && $pipeline_result) { - $output .= $pipeline_html; + if ($inlineGroup) { + $inline_css .= $pipeline_result; + } + else { + $output .= $pipeline_html; + } } } else { foreach ($this->css as $file) { if ($group && $file['group'] == $group) { - $media = isset($file['media']) ? sprintf(' media="%s"', $file['media']) : ''; - $output .= '' . "\n"; + if ($inlineGroup || $file['loading'] === 'inline') { + $inline_css .= $this->gatherLinks([$file], CSS_ASSET) . "\n"; + } + else { + $media = isset($file['media']) ? sprintf(' media="%s"', $file['media']) : ''; + $output .= '' . "\n"; + } } } } @@ -626,30 +628,52 @@ public function js($group = 'head', $attributes = []) $this->js = array_reverse($this->js); $this->inline_js = array_reverse($this->inline_js); + $inlineGroup = array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline'; + $attributes = $this->attributes(array_merge(['type' => 'text/javascript'], $attributes)); $output = ''; $inline_js = ''; if ($this->js_pipeline) { - $pipeline_result = $this->pipelineJs($group); - $pipeline_html = '' . "\n"; + $pipeline_result = $this->pipelineJs($group, !$inlineGroup); + $pipeline_html = ($inlineGroup ? '' : '' . "\n"); if ($this->js_pipeline_before_excludes && $pipeline_result) { - $output .= $pipeline_html; + if ($inlineGroup) { + $inline_js .= $pipeline_result; + } + else { + $output .= $pipeline_html; + } } foreach ($this->js_no_pipeline as $file) { if ($group && $file['group'] == $group) { - $output .= '' . "\n"; + if ($file['loading'] === 'inline') { + $inline_js .= $this->gatherLinks([$file], JS_ASSET) . "\n"; + } + else { + $output .= '' . "\n"; + } } } if (!$this->js_pipeline_before_excludes && $pipeline_result) { - $output .= $pipeline_html; + if ($inlineGroup) { + $inline_js .= $pipeline_result; + } + else { + $output .= $pipeline_html; + } } } else { foreach ($this->js as $file) { if ($group && $file['group'] == $group) { - $output .= '' . "\n"; + if ($inlineGroup || $file['loading'] === 'inline') { + $inline_js .= $this->gatherLinks([$file], JS_ASSET) . "\n"; + } + else { + $output .= '' . "\n"; + } } } } @@ -672,10 +696,11 @@ public function js($group = 'head', $attributes = []) * Minify and concatenate CSS * * @param string $group + * @param bool $returnURL true if pipeline should return the URL, otherwise the content * - * @return bool|string + * @return bool|string URL or generated content if available, else false */ - protected function pipelineCss($group = 'head') + protected function pipelineCss($group = 'head', $returnURL = true) { // temporary list of assets to pipeline $temp_css = []; @@ -695,9 +720,14 @@ protected function pipelineCss($group = 'head') $this->css_no_pipeline = json_decode(file_get_contents($this->assets_dir . $inline_file), true); } - // If pipeline exist return it + // If pipeline exist return its URL or content if (file_exists($this->assets_dir . $file)) { - return $relative_path . $this->getTimestamp(); + if ($returnURL) { + return $relative_path . $this->getTimestamp(); + } + else { + return file_get_contents($this->assets_dir . $file) . "\n"; + } } // Remove any non-pipeline files @@ -744,7 +774,12 @@ protected function pipelineCss($group = 'head') if (strlen(trim($buffer)) > 0) { file_put_contents($this->assets_dir . $file, $buffer); - return $relative_path . $this->getTimestamp(); + if ($returnURL) { + return $relative_path . $this->getTimestamp(); + } + else { + return $buffer . "\n"; + } } else { return false; } @@ -754,10 +789,11 @@ protected function pipelineCss($group = 'head') * Minify and concatenate JS files. * * @param string $group + * @param bool $returnURL true if pipeline should return the URL, otherwise the content * - * @return string + * @return bool|string URL or generated content if available, else false */ - protected function pipelineJs($group = 'head') + protected function pipelineJs($group = 'head', $returnURL = true) { // temporary list of assets to pipeline $temp_js = []; @@ -777,9 +813,14 @@ protected function pipelineJs($group = 'head') $this->js_no_pipeline = json_decode(file_get_contents($this->assets_dir . $inline_file), true); } - // If pipeline exist return it + // If pipeline exist return its URL or content if (file_exists($this->assets_dir . $file)) { - return $relative_path . $this->getTimestamp(); + if ($returnURL) { + return $relative_path . $this->getTimestamp(); + } + else { + return file_get_contents($this->assets_dir . $file) . "\n"; + } } // Remove any non-pipeline files @@ -816,7 +857,12 @@ protected function pipelineJs($group = 'head') if (strlen(trim($buffer)) > 0) { file_put_contents($this->assets_dir . $file, $buffer); - return $relative_path . $this->getTimestamp(); + if ($returnURL) { + return $relative_path . $this->getTimestamp(); + } + else { + return $buffer . "\n"; + } } else { return false; } diff --git a/tests/unit/Grav/Common/AssetsTest.php b/tests/unit/Grav/Common/AssetsTest.php index e02593cf9..e4b055cc6 100644 --- a/tests/unit/Grav/Common/AssetsTest.php +++ b/tests/unit/Grav/Common/AssetsTest.php @@ -41,6 +41,7 @@ public function testAddingAssets() 'priority' => 10, 'order' => 0, 'pipeline' => true, + 'loading' => '', 'group' => 'head', 'modified' => false ], reset($array)); @@ -56,6 +57,7 @@ public function testAddingAssets() 'priority' => 10, 'order' => 0, 'pipeline' => true, + 'loading' => '', 'group' => 'head', 'modified' => false ], reset($array)); @@ -73,6 +75,7 @@ public function testAddingAssets() 'priority' => 10, 'order' => 0, 'pipeline' => true, + 'loading' => '', 'group' => 'head', 'modified' => false ], reset($array)); @@ -90,6 +93,7 @@ public function testAddingAssets() 'priority' => 10, 'order' => 0, 'pipeline' => true, + 'loading' => '', 'group' => 'head', 'modified' => false ], reset($array)); @@ -133,6 +137,7 @@ public function testAddingAssets() 'priority' => 10, 'order' => 0, 'pipeline' => true, + 'loading' => '', 'group' => 'footer', 'modified' => false ], reset($array)); @@ -192,6 +197,22 @@ public function testAddingAssets() 'modified' => false ], reset($array)); + //Test inline + $this->assets->reset(); + $this->assets->addJs('/system/assets/jquery/jquery-2.x.min.js', null, true, 'inline', null); + $js = $this->assets->js(); + $this->assertContains('jQuery Foundation', $js); + + $this->assets->reset(); + $this->assets->addCss('/system/assets/debugger.css', null, true, null, 'inline'); + $css = $this->assets->css(); + $this->assertContains('div.phpdebugbar', $css); + + $this->assets->reset(); + $this->assets->addCss('https://fonts.googleapis.com/css?family=Roboto', null, true, null, 'inline'); + $css = $this->assets->css(); + $this->assertContains('font-family: \'Roboto\';', $css); + //Test adding media queries $this->assets->reset(); $this->assets->add('test.css', ['media' => 'only screen and (min-width: 640px)']); @@ -336,6 +357,24 @@ public function testPipelineWithTimestamp() $this->assertContains($this->assets->getTimestamp(), $css); } + public function testInlinePipeline() + { + $this->assets->reset(); + + //File not existing. Pipeline searches for that file without reaching it. Output is empty. + $this->assets->add('test.css', null, true); + $this->assets->setCssPipeline(true); + $css = $this->assets->css('head', ['loading' => 'inline']); + $this->assertSame('', $css); + + //Add a core Grav CSS file, which is found. Pipeline will now return its content. + $this->assets->addCss('https://fonts.googleapis.com/css?family=Roboto', null, true); + $this->assets->add('/system/assets/debugger.css', null, true); + $css = $this->assets->css('head', ['loading' => 'inline']); + $this->assertContains('font-family:\'Roboto\';', $css); + $this->assertContains('div.phpdebugbar', $css); + } + public function testAddAsyncJs() { $this->assets->reset();