Skip to content

Commit be395b7

Browse files
authored
Merge pull request #460 from ampproject/add/445-amp-story-css-optimizer-transformer
2 parents d90e29b + 3ca83ca commit be395b7

File tree

9 files changed

+566
-3
lines changed

9 files changed

+566
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ Note that this only lets you check whether an error "category" popped up. It can
140140
|-------|-------------|
141141
| [`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. |
142142
| [`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. |
143+
| [`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. |
143144
| [`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. |
144145
| [`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. |
145146
| [`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. |

src/Dom/Element.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,12 @@ final class Element extends DOMElement
4242
/**
4343
* Add CSS styles to the element as an inline style attribute.
4444
*
45-
* @param string $style CSS style(s) to add to the inline style attribute.
45+
* @param string $style CSS style(s) to add to the inline style attribute.
46+
* @param bool $prepend Optional. Whether to prepend the new style to existing styles or not. Defaults to false.
4647
* @return DOMAttr|false The new or modified DOMAttr or false if an error occurred.
4748
* @throws MaxCssByteCountExceeded If the allowed max byte count is exceeded.
4849
*/
49-
public function addInlineStyle($style)
50+
public function addInlineStyle($style, $prepend = false)
5051
{
5152
$style = trim($style, CssRule::CSS_TRIM_CHARACTERS);
5253

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

58-
$newStyle = $existingStyle . $style;
59+
$newStyle = $prepend ? ($style . ';' . $existingStyle) : ($existingStyle . $style);
5960

6061
return $this->setAttribute(Attribute::STYLE, $newStyle);
6162
}

src/Html/Attribute.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ interface Attribute
7474
const AMP_ONERROR = 'amp-onerror';
7575
const AMP_RUNTIME = 'amp-runtime';
7676
const AMP_SCRIPT_SRC = 'amp-script-src';
77+
const AMP_STORY_DVH_POLLYFILL = 'amp-story-dvh-polyfill';
7778
const ANCHOR = 'anchor';
7879
const ANIMATE = 'animate';
7980
const ANIMATE_IN = 'animate-in';
@@ -1109,6 +1110,7 @@ interface Attribute
11091110
const DATA_SORT_TIME = 'data-sort-time';
11101111
const DATA_SRC = 'data-src';
11111112
const DATA_START = 'data-start';
1113+
const DATA_STORY_SUPPORTS_LANDSCAPE = 'data-story-supports-landscape';
11121114
const DATA_STREAMTYPE = 'data-streamtype';
11131115
const DATA_TAG = 'data-tag';
11141116
const DATA_TAGS = 'data-tags';
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace AmpProject\Optimizer\Configuration;
4+
5+
use AmpProject\Optimizer\Configuration\BaseTransformerConfiguration;
6+
use AmpProject\Optimizer\Exception\InvalidConfigurationValue;
7+
8+
/**
9+
* Configuration for the AmpStoryCssOptimizer transformer.
10+
*
11+
* @property bool $optimizeAmpStory Whether to enable AMP Story optimizations or not. Defaults to `false`.
12+
*
13+
* @package ampproject/amp-toolbox
14+
*/
15+
final class AmpStoryCssOptimizerConfiguration extends BaseTransformerConfiguration
16+
{
17+
/**
18+
* Whether optimization is enabled.
19+
*
20+
* @var string
21+
*/
22+
const OPTIMIZE_AMP_STORY = 'optimizeAmpStory';
23+
24+
/**
25+
* Get the associative array of allowed keys and their respective default values.
26+
*
27+
* The array index is the key and the array value is the key's default value.
28+
*
29+
* @return array Associative array of allowed keys and their respective default values.
30+
*/
31+
protected function getAllowedKeys()
32+
{
33+
return [
34+
self::OPTIMIZE_AMP_STORY => false,
35+
];
36+
}
37+
38+
/**
39+
* Validate an individual configuration entry.
40+
*
41+
* @param string $key Key of the configuration entry to validate.
42+
* @param mixed $value Value of the configuration entry to validate.
43+
* @return mixed Validated value.
44+
*/
45+
protected function validate($key, $value)
46+
{
47+
switch ($key) {
48+
case self::OPTIMIZE_AMP_STORY:
49+
if (! is_bool($value)) {
50+
throw InvalidConfigurationValue::forInvalidSubValueType(
51+
self::class,
52+
self::OPTIMIZE_AMP_STORY,
53+
'boolean',
54+
gettype($value)
55+
);
56+
}
57+
break;
58+
}
59+
60+
return $value;
61+
}
62+
}

src/Optimizer/DefaultConfiguration.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class DefaultConfiguration implements Configuration
3939
Transformer\TransformedIdentifier::class => Configuration\TransformedIdentifierConfiguration::class,
4040
Transformer\OptimizeViewport::class => Configuration\OptimizeViewportConfiguration::class,
4141
Transformer\MinifyHtml::class => Configuration\MinifyHtmlConfiguration::class,
42+
Transformer\AmpStoryCssOptimizer::class => Configuration\AmpStoryCssOptimizerConfiguration::class,
4243
];
4344

4445
/**
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
<?php
2+
3+
namespace AmpProject\Optimizer\Transformer;
4+
5+
use AmpProject\Amp;
6+
use AmpProject\Dom\Document;
7+
use AmpProject\Dom\Element;
8+
use AmpProject\Dom\NodeWalker;
9+
use AmpProject\Extension;
10+
use AmpProject\Html\Attribute;
11+
use AmpProject\Html\Tag;
12+
use AmpProject\Optimizer\Configuration\AmpStoryCssOptimizerConfiguration;
13+
use AmpProject\Optimizer\ErrorCollection;
14+
use AmpProject\Optimizer\Transformer;
15+
use AmpProject\Optimizer\TransformerConfiguration;
16+
17+
/**
18+
* AmpStoryCssOptimizer - CSS Optimizer for AMP Story
19+
*
20+
* This transformer will:
21+
* - append `link[rel=stylesheet]` to `amp-story-1.0.css`.
22+
* - modify the `amp-custom` CSS to use `--amp-story-${vh/vw/vmin/vmax}`.
23+
* - append inline `<script>` for the `dvh` polyfill.
24+
* - SSR `data-story-supports-landscape`.
25+
* - SSR `aspect-ratio` into style.
26+
*
27+
* @package ampproject/amp-toolbox
28+
*/
29+
final class AmpStoryCssOptimizer implements Transformer
30+
{
31+
/**
32+
* AMP Story dvh pollyfill script.
33+
*
34+
* @var string
35+
*/
36+
const AMP_STORY_DVH_POLYFILL_CONTENT = '"use strict";if(!self.CSS||!CSS.supports||!CSS.supports("height:1dvh"))'
37+
. '{function e(){document.documentElement.style.setProperty("--story-dvh",innerHeight/100+"px","important")}'
38+
. 'addEventListener("resize",e,{passive:!0}),e()}';
39+
40+
/**
41+
* Configuration store to use.
42+
*
43+
* @var TransformerConfiguration
44+
*/
45+
private $configuration;
46+
47+
/**
48+
* Instantiate an AmpStoryCssOptimizer object.
49+
*
50+
* @param TransformerConfiguration $configuration Configuration store to use.
51+
*/
52+
public function __construct(TransformerConfiguration $configuration)
53+
{
54+
$this->configuration = $configuration;
55+
}
56+
57+
/**
58+
* Apply transformations to the provided DOM document.
59+
*
60+
* @param Document $document DOM document to apply the transformations to.
61+
* @param ErrorCollection $errors Collection of errors that are collected during transformation.
62+
* @return void
63+
*/
64+
public function transform(Document $document, ErrorCollection $errors)
65+
{
66+
if (!$this->configuration->get(AmpStoryCssOptimizerConfiguration::OPTIMIZE_AMP_STORY)) {
67+
return;
68+
}
69+
70+
$hasAmpStoryScript = false;
71+
$hasAmpStoryDvhPolyfillScript = false;
72+
$styleAmpCustom = null;
73+
74+
foreach ($document->head->childNodes as $childNode) {
75+
if (! $childNode instanceof Element) {
76+
continue;
77+
}
78+
79+
if ($this->isAmpStoryScript($childNode)) {
80+
$hasAmpStoryScript = true;
81+
continue;
82+
}
83+
84+
if ($this->isAmpStoryDvhPolyfillScript($childNode)) {
85+
$hasAmpStoryDvhPolyfillScript = true;
86+
continue;
87+
}
88+
89+
if ($this->isStyleAmpCustom($childNode)) {
90+
$styleAmpCustom = $childNode;
91+
continue;
92+
}
93+
}
94+
95+
// We can return early if no amp-story script is found.
96+
if (! $hasAmpStoryScript) {
97+
return;
98+
}
99+
100+
$this->appendAmpStoryCssLink($document);
101+
102+
if ($styleAmpCustom) {
103+
$this->modifyAmpCustomCSS($styleAmpCustom);
104+
// Make sure to not install the dvh polyfill twice.
105+
if (! $hasAmpStoryDvhPolyfillScript) {
106+
$this->appendAmpStoryDvhPolyfillScript($document);
107+
}
108+
}
109+
110+
$this->supportsLandscapeSSR($document);
111+
$this->aspectRatioSSR($document);
112+
}
113+
114+
/**
115+
* Check whether the element is an AMP Story element.
116+
*
117+
* @param Element $element Element to check.
118+
* @return bool Whether the given element is an AMP story.
119+
*/
120+
private function isAmpStoryScript(Element $element)
121+
{
122+
return $element->tagName === Tag::SCRIPT
123+
&& $element->getAttribute(Attribute::CUSTOM_ELEMENT) === Extension::STORY;
124+
}
125+
126+
/**
127+
* Check whether the element is a script[amp-story-dvh-polyfill] element.
128+
*
129+
* @param Element $element Element to check.
130+
* @return bool Whether the element is a script[amp-story-dvh-polyfill] element.
131+
*/
132+
private function isAmpStoryDvhPolyfillScript(Element $element)
133+
{
134+
return $element->tagName === Tag::SCRIPT
135+
&& $element->hasAttribute(Attribute::AMP_STORY_DVH_POLLYFILL);
136+
}
137+
138+
/**
139+
* Check whether the element is a style[amp-custom] element.
140+
*
141+
* @param Element $element Element to check.
142+
* @return bool Whether the element is a style[amp-custom] element.
143+
*/
144+
private function isStyleAmpCustom(Element $element)
145+
{
146+
return $element->tagName === Tag::STYLE
147+
&& $element->hasAttribute(Attribute::AMP_CUSTOM);
148+
}
149+
150+
/**
151+
* Insert a link element with amp-story css source.
152+
*
153+
* @param Document $document Document to append the link.
154+
*/
155+
private function appendAmpStoryCssLink(Document $document)
156+
{
157+
// @TODO Need to take the following into account when deciding on a version:
158+
// - latest stable version available,
159+
// - the channel that the runtime is locked to, i.e. whether LTS is active.
160+
$href = Amp::CACHE_HOST . '/v0/amp-story-1.0.css';
161+
162+
$ampStoryCssLink = $document->createElementWithAttributes(Tag::LINK, [
163+
Attribute::REL => Attribute::REL_STYLESHEET,
164+
Attribute::AMP_EXTENSION => Extension::STORY,
165+
Attribute::HREF => $href,
166+
]);
167+
168+
$document->head->appendChild($ampStoryCssLink);
169+
}
170+
171+
/**
172+
* Replace viewport units in custom css with related css variables.
173+
*
174+
* @param Element $style The style element to modify.
175+
*/
176+
private function modifyAmpCustomCSS(Element $style)
177+
{
178+
$style->nodeValue = preg_replace(
179+
'/(-?[\d.]+)v(w|h|min|max)/',
180+
'calc($1 * var(--story-page-v$2))',
181+
$style->nodeValue
182+
);
183+
}
184+
185+
/**
186+
* Append an inline script tag for the dvh polyfill
187+
*
188+
* @param Document $document The document in which we need to append the script tag.
189+
* @return void
190+
*/
191+
private function appendAmpStoryDvhPolyfillScript(Document $document)
192+
{
193+
$ampStoryDvhPolyfillScript = $document->createElementWithAttributes(
194+
Tag::SCRIPT,
195+
[
196+
Attribute::AMP_STORY_DVH_POLLYFILL => '',
197+
],
198+
self::AMP_STORY_DVH_POLYFILL_CONTENT
199+
);
200+
201+
$document->head->appendChild($ampStoryDvhPolyfillScript);
202+
}
203+
204+
/**
205+
* Add data-story-supports-landscape attribute to support landscape.
206+
*
207+
* @param Document $document The document in which we need to add the attribute.
208+
*/
209+
private function supportsLandscapeSSR(Document $document)
210+
{
211+
$story = $document->body->getElementsByTagName(Extension::STORY)->item(0);
212+
213+
if (! $story instanceof Element) {
214+
return;
215+
}
216+
217+
if ($story->hasAttribute(Attribute::SUPPORTS_LANDSCAPE)) {
218+
$document->html->setAttribute(Attribute::DATA_STORY_SUPPORTS_LANDSCAPE, '');
219+
}
220+
}
221+
222+
/**
223+
* Add aspect-ratio inline style for amp-story-grid-layer.
224+
*
225+
* @param Document $document The document in which we need to add the style.
226+
*/
227+
private function aspectRatioSSR(Document $document)
228+
{
229+
for ($node = $document->body; $node !== null; $node = NodeWalker::nextNode($node)) {
230+
if (! $node instanceof Element) {
231+
continue;
232+
}
233+
234+
if (Amp::isTemplate($node)) {
235+
$node = NodeWalker::skipNodeAndChildren($node);
236+
continue;
237+
}
238+
239+
if ($node->tagName !== Extension::STORY_GRID_LAYER) {
240+
continue;
241+
}
242+
243+
if (! $node->hasAttribute(Attribute::ASPECT_RATIO)) {
244+
continue;
245+
}
246+
247+
$aspectRatio = str_replace(':', '/', $node->getAttribute(Attribute::ASPECT_RATIO));
248+
249+
$node->addInlineStyle("--aspect-ratio:{$aspectRatio}", true);
250+
}
251+
}
252+
}

0 commit comments

Comments
 (0)