|
| 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