Skip to content

Commit

Permalink
Merge pull request #460 from ampproject/add/445-amp-story-css-optimiz…
Browse files Browse the repository at this point in the history
…er-transformer
  • Loading branch information
schlessera authored Jan 7, 2022
2 parents d90e29b + 3ca83ca commit be395b7
Show file tree
Hide file tree
Showing 9 changed files with 566 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ Note that this only lets you check whether an error "category" popped up. It can
|-------|-------------|
| [`AmpBoilerplate`](https://github.com/ampproject/amp-toolbox-php/blob/main/src/Optimizer/Transformer/AmpBoilerplate.php) | Transformer that removes AMP boilerplate `<style>` and `<noscript>` tags in `<head>`, keeping only the `amp-custom` style tag. It then (re-)inserts the `amp-boilerplate` unless the document is marked with the `i-amphtml-no-boilerplate` attribute. |
| [`AmpRuntimeCss`](https://github.com/ampproject/amp-toolbox-php/blob/main/src/Optimizer/Transformer/AmpRuntimeCss.php) | Transformer adding `https://cdn.ampproject.org/v0.css` if server-side-rendering is applied (known by the presence of the `<style amp-runtime>` tag). AMP runtime css (`v0.css`) will always be inlined as it'll get automatically updated to the latest version once the AMP runtime has loaded. |
| [`AmpStoryCssOptimizer`](https://github.com/ampproject/amp-toolbox-php/blob/main/src/Optimizer/Transformer/AmpStoryCssOptimizer.php) | Enables AMP Story optimizations such as linking to the `amp-story-1.0.css`, and server-side rendering of attributes. |
| [`AutoExtensions`](https://github.com/ampproject/amp-toolbox-php/blob/main/src/Optimizer/Transformer/AutoExtensions.php) | Transformer that analyzes the HTML source code to identify the required AMP extensions and automatically imports missing AMP extension scripts as well as removes the ones that are unused. |
| [`OptimizeHeroImages`](https://github.com/ampproject/amp-toolbox-php/blob/main/src/Optimizer/Transformer/OptimizeHeroImages.php) | Transformer that optimizes image rendering times for hero images by adding preload and serverside-rendered `<img>` tags when possible. Viable hero images are `<amp-img>` tags, `<amp-video>` tags with a `poster` attribute as well as `<amp-iframe>` and `<amp-video-iframe>` tags with a `placeholder` attribute. The first viable image that is encountered is used by default, but this behavior can be overridden by adding the `data-hero` attribute to a maximum of two images. The preloads only work work images that don't use `srcset`, as that is not supported as a preload in most browsers. The serverside-rendered image will not be created for `<amp-video>` tags. |
| [`OptimizeViewport`](https://github.com/ampproject/amp-toolbox-php/blob/main/src/Optimizer/Transformer/OptimizeViewport.php) | Transformer that normalizes and optimizes the viewport meta tag. By default it will add `<meta name="viewport" content="width=device-width">` if viewport is missing, which is the bare minimum that AMP requires. |
Expand Down
7 changes: 4 additions & 3 deletions src/Dom/Element.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ final class Element extends DOMElement
/**
* Add CSS styles to the element as an inline style attribute.
*
* @param string $style CSS style(s) to add to the inline style attribute.
* @param string $style CSS style(s) to add to the inline style attribute.
* @param bool $prepend Optional. Whether to prepend the new style to existing styles or not. Defaults to false.
* @return DOMAttr|false The new or modified DOMAttr or false if an error occurred.
* @throws MaxCssByteCountExceeded If the allowed max byte count is exceeded.
*/
public function addInlineStyle($style)
public function addInlineStyle($style, $prepend = false)
{
$style = trim($style, CssRule::CSS_TRIM_CHARACTERS);

Expand All @@ -55,7 +56,7 @@ public function addInlineStyle($style)
$existingStyle = rtrim($existingStyle, ';') . ';';
}

$newStyle = $existingStyle . $style;
$newStyle = $prepend ? ($style . ';' . $existingStyle) : ($existingStyle . $style);

return $this->setAttribute(Attribute::STYLE, $newStyle);
}
Expand Down
2 changes: 2 additions & 0 deletions src/Html/Attribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ interface Attribute
const AMP_ONERROR = 'amp-onerror';
const AMP_RUNTIME = 'amp-runtime';
const AMP_SCRIPT_SRC = 'amp-script-src';
const AMP_STORY_DVH_POLLYFILL = 'amp-story-dvh-polyfill';
const ANCHOR = 'anchor';
const ANIMATE = 'animate';
const ANIMATE_IN = 'animate-in';
Expand Down Expand Up @@ -1109,6 +1110,7 @@ interface Attribute
const DATA_SORT_TIME = 'data-sort-time';
const DATA_SRC = 'data-src';
const DATA_START = 'data-start';
const DATA_STORY_SUPPORTS_LANDSCAPE = 'data-story-supports-landscape';
const DATA_STREAMTYPE = 'data-streamtype';
const DATA_TAG = 'data-tag';
const DATA_TAGS = 'data-tags';
Expand Down
62 changes: 62 additions & 0 deletions src/Optimizer/Configuration/AmpStoryCssOptimizerConfiguration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace AmpProject\Optimizer\Configuration;

use AmpProject\Optimizer\Configuration\BaseTransformerConfiguration;
use AmpProject\Optimizer\Exception\InvalidConfigurationValue;

/**
* Configuration for the AmpStoryCssOptimizer transformer.
*
* @property bool $optimizeAmpStory Whether to enable AMP Story optimizations or not. Defaults to `false`.
*
* @package ampproject/amp-toolbox
*/
final class AmpStoryCssOptimizerConfiguration extends BaseTransformerConfiguration
{
/**
* Whether optimization is enabled.
*
* @var string
*/
const OPTIMIZE_AMP_STORY = 'optimizeAmpStory';

/**
* Get the associative array of allowed keys and their respective default values.
*
* The array index is the key and the array value is the key's default value.
*
* @return array Associative array of allowed keys and their respective default values.
*/
protected function getAllowedKeys()
{
return [
self::OPTIMIZE_AMP_STORY => false,
];
}

/**
* Validate an individual configuration entry.
*
* @param string $key Key of the configuration entry to validate.
* @param mixed $value Value of the configuration entry to validate.
* @return mixed Validated value.
*/
protected function validate($key, $value)
{
switch ($key) {
case self::OPTIMIZE_AMP_STORY:
if (! is_bool($value)) {
throw InvalidConfigurationValue::forInvalidSubValueType(
self::class,
self::OPTIMIZE_AMP_STORY,
'boolean',
gettype($value)
);
}
break;
}

return $value;
}
}
1 change: 1 addition & 0 deletions src/Optimizer/DefaultConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class DefaultConfiguration implements Configuration
Transformer\TransformedIdentifier::class => Configuration\TransformedIdentifierConfiguration::class,
Transformer\OptimizeViewport::class => Configuration\OptimizeViewportConfiguration::class,
Transformer\MinifyHtml::class => Configuration\MinifyHtmlConfiguration::class,
Transformer\AmpStoryCssOptimizer::class => Configuration\AmpStoryCssOptimizerConfiguration::class,
];

/**
Expand Down
252 changes: 252 additions & 0 deletions src/Optimizer/Transformer/AmpStoryCssOptimizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
<?php

namespace AmpProject\Optimizer\Transformer;

use AmpProject\Amp;
use AmpProject\Dom\Document;
use AmpProject\Dom\Element;
use AmpProject\Dom\NodeWalker;
use AmpProject\Extension;
use AmpProject\Html\Attribute;
use AmpProject\Html\Tag;
use AmpProject\Optimizer\Configuration\AmpStoryCssOptimizerConfiguration;
use AmpProject\Optimizer\ErrorCollection;
use AmpProject\Optimizer\Transformer;
use AmpProject\Optimizer\TransformerConfiguration;

/**
* AmpStoryCssOptimizer - CSS Optimizer for AMP Story
*
* This transformer will:
* - append `link[rel=stylesheet]` to `amp-story-1.0.css`.
* - modify the `amp-custom` CSS to use `--amp-story-${vh/vw/vmin/vmax}`.
* - append inline `<script>` for the `dvh` polyfill.
* - SSR `data-story-supports-landscape`.
* - SSR `aspect-ratio` into style.
*
* @package ampproject/amp-toolbox
*/
final class AmpStoryCssOptimizer implements Transformer
{
/**
* AMP Story dvh pollyfill script.
*
* @var string
*/
const AMP_STORY_DVH_POLYFILL_CONTENT = '"use strict";if(!self.CSS||!CSS.supports||!CSS.supports("height:1dvh"))'
. '{function e(){document.documentElement.style.setProperty("--story-dvh",innerHeight/100+"px","important")}'
. 'addEventListener("resize",e,{passive:!0}),e()}';

/**
* Configuration store to use.
*
* @var TransformerConfiguration
*/
private $configuration;

/**
* Instantiate an AmpStoryCssOptimizer object.
*
* @param TransformerConfiguration $configuration Configuration store to use.
*/
public function __construct(TransformerConfiguration $configuration)
{
$this->configuration = $configuration;
}

/**
* Apply transformations to the provided DOM document.
*
* @param Document $document DOM document to apply the transformations to.
* @param ErrorCollection $errors Collection of errors that are collected during transformation.
* @return void
*/
public function transform(Document $document, ErrorCollection $errors)
{
if (!$this->configuration->get(AmpStoryCssOptimizerConfiguration::OPTIMIZE_AMP_STORY)) {
return;
}

$hasAmpStoryScript = false;
$hasAmpStoryDvhPolyfillScript = false;
$styleAmpCustom = null;

foreach ($document->head->childNodes as $childNode) {
if (! $childNode instanceof Element) {
continue;
}

if ($this->isAmpStoryScript($childNode)) {
$hasAmpStoryScript = true;
continue;
}

if ($this->isAmpStoryDvhPolyfillScript($childNode)) {
$hasAmpStoryDvhPolyfillScript = true;
continue;
}

if ($this->isStyleAmpCustom($childNode)) {
$styleAmpCustom = $childNode;
continue;
}
}

// We can return early if no amp-story script is found.
if (! $hasAmpStoryScript) {
return;
}

$this->appendAmpStoryCssLink($document);

if ($styleAmpCustom) {
$this->modifyAmpCustomCSS($styleAmpCustom);
// Make sure to not install the dvh polyfill twice.
if (! $hasAmpStoryDvhPolyfillScript) {
$this->appendAmpStoryDvhPolyfillScript($document);
}
}

$this->supportsLandscapeSSR($document);
$this->aspectRatioSSR($document);
}

/**
* Check whether the element is an AMP Story element.
*
* @param Element $element Element to check.
* @return bool Whether the given element is an AMP story.
*/
private function isAmpStoryScript(Element $element)
{
return $element->tagName === Tag::SCRIPT
&& $element->getAttribute(Attribute::CUSTOM_ELEMENT) === Extension::STORY;
}

/**
* Check whether the element is a script[amp-story-dvh-polyfill] element.
*
* @param Element $element Element to check.
* @return bool Whether the element is a script[amp-story-dvh-polyfill] element.
*/
private function isAmpStoryDvhPolyfillScript(Element $element)
{
return $element->tagName === Tag::SCRIPT
&& $element->hasAttribute(Attribute::AMP_STORY_DVH_POLLYFILL);
}

/**
* Check whether the element is a style[amp-custom] element.
*
* @param Element $element Element to check.
* @return bool Whether the element is a style[amp-custom] element.
*/
private function isStyleAmpCustom(Element $element)
{
return $element->tagName === Tag::STYLE
&& $element->hasAttribute(Attribute::AMP_CUSTOM);
}

/**
* Insert a link element with amp-story css source.
*
* @param Document $document Document to append the link.
*/
private function appendAmpStoryCssLink(Document $document)
{
// @TODO Need to take the following into account when deciding on a version:
// - latest stable version available,
// - the channel that the runtime is locked to, i.e. whether LTS is active.
$href = Amp::CACHE_HOST . '/v0/amp-story-1.0.css';

$ampStoryCssLink = $document->createElementWithAttributes(Tag::LINK, [
Attribute::REL => Attribute::REL_STYLESHEET,
Attribute::AMP_EXTENSION => Extension::STORY,
Attribute::HREF => $href,
]);

$document->head->appendChild($ampStoryCssLink);
}

/**
* Replace viewport units in custom css with related css variables.
*
* @param Element $style The style element to modify.
*/
private function modifyAmpCustomCSS(Element $style)
{
$style->nodeValue = preg_replace(
'/(-?[\d.]+)v(w|h|min|max)/',
'calc($1 * var(--story-page-v$2))',
$style->nodeValue
);
}

/**
* Append an inline script tag for the dvh polyfill
*
* @param Document $document The document in which we need to append the script tag.
* @return void
*/
private function appendAmpStoryDvhPolyfillScript(Document $document)
{
$ampStoryDvhPolyfillScript = $document->createElementWithAttributes(
Tag::SCRIPT,
[
Attribute::AMP_STORY_DVH_POLLYFILL => '',
],
self::AMP_STORY_DVH_POLYFILL_CONTENT
);

$document->head->appendChild($ampStoryDvhPolyfillScript);
}

/**
* Add data-story-supports-landscape attribute to support landscape.
*
* @param Document $document The document in which we need to add the attribute.
*/
private function supportsLandscapeSSR(Document $document)
{
$story = $document->body->getElementsByTagName(Extension::STORY)->item(0);

if (! $story instanceof Element) {
return;
}

if ($story->hasAttribute(Attribute::SUPPORTS_LANDSCAPE)) {
$document->html->setAttribute(Attribute::DATA_STORY_SUPPORTS_LANDSCAPE, '');
}
}

/**
* Add aspect-ratio inline style for amp-story-grid-layer.
*
* @param Document $document The document in which we need to add the style.
*/
private function aspectRatioSSR(Document $document)
{
for ($node = $document->body; $node !== null; $node = NodeWalker::nextNode($node)) {
if (! $node instanceof Element) {
continue;
}

if (Amp::isTemplate($node)) {
$node = NodeWalker::skipNodeAndChildren($node);
continue;
}

if ($node->tagName !== Extension::STORY_GRID_LAYER) {
continue;
}

if (! $node->hasAttribute(Attribute::ASPECT_RATIO)) {
continue;
}

$aspectRatio = str_replace(':', '/', $node->getAttribute(Attribute::ASPECT_RATIO));

$node->addInlineStyle("--aspect-ratio:{$aspectRatio}", true);
}
}
}
Loading

0 comments on commit be395b7

Please sign in to comment.