const {skipNodeAndChildren} = require('../HtmlDomHelper'); const { isTemplate, AMP_STORY_DVH_POLYFILL_ATTR, isAmpStoryDvhPolyfillScript, } = require('../AmpConstants'); const { insertText, hasAttribute, remove, createElement, nextNode, firstChildByTag, appendChild, } = require('../NodeUtils'); const {DEFAULT_AMP_CACHE_HOST} = require('../AmpConstants.js'); // This string should not be modified, even slightly. This string is strictly // checked by the validator. 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()}'; const ASPECT_RATIO_ATTR = 'aspect-ratio'; class AmpStoryCssTransformer { constructor(config) { this.log_ = config.log.tag('AmpStoryCssTransformer'); this.enabled_ = config.optimizeAmpStory === true; if (!this.enabled_) { this.log_.debug('disabled'); } } transform(root) { if (!this.enabled_) return; const html = firstChildByTag(root, 'html'); if (!html) return; const head = firstChildByTag(html, 'head'); if (!head) return; const body = firstChildByTag(html, 'body'); if (!body) return; let hasAmpStoryScript = false; let hasAmpStoryDvhPolyfillScript = false; let styleAmpCustom = null; for (let node = head.firstChild; node !== null; node = node.nextSibling) { if (isAmpStoryScript(node)) { hasAmpStoryScript = true; continue; } if (isAmpStoryDvhPolyfillScript(node)) { hasAmpStoryDvhPolyfillScript = true; continue; } if (isStyleAmpCustom(node)) { styleAmpCustom = node; continue; } } // We can return early if no amp-story script is found. if (!hasAmpStoryScript) return; appendAmpStoryCssLink(head); if (styleAmpCustom) { modifyAmpCustomCSS(styleAmpCustom); // Make sure to not double install the dvh polyfill. if (!hasAmpStoryDvhPolyfillScript) { appendAmpStoryDvhPolyfillScript(head); } } supportsLandscapeSSR(body, html); aspectRatioSSR(body); } } function modifyAmpCustomCSS(style) { if (!style.children) return; const children = style.children; // Remove all text children from style. // NOTE(erwinm): Is it actually possible in htmlparser2 to have multiple // text children? for (let i = 0; i < children.length; i++) { const child = children[i]; if (child.type == 'text' && child.data) { const newText = child.data.replace( /(-?[\d.]+)v(w|h|min|max)/gim, 'calc($1 * var(--story-page-v$2))' ); remove(child); insertText(style, newText); } } } function supportsLandscapeSSR(body, html) { const story = firstChildByTag(body, 'amp-story'); if (!story) return; if (hasAttribute(story, 'supports-landscape') && html.attribs) { html.attribs['data-story-supports-landscape'] = ''; } } function aspectRatioSSR(body) { for (let node = body; node !== null; node = nextNode(node)) { if (isTemplate(node)) { node = skipNodeAndChildren(node); continue; } if (!isAmpStoryGridLayer(node)) continue; const {attribs} = node; if (!attribs || !attribs[ASPECT_RATIO_ATTR] || typeof attribs[ASPECT_RATIO_ATTR] !== 'string') { continue; } const aspectRatio = attribs[ASPECT_RATIO_ATTR].replace(/:/g, '/'); // We need to a `attribs['style'] || ''` in case there is no style attribute as we // don't want to coerce "undefined" or "null" into a string. attribs['style'] = `--${ASPECT_RATIO_ATTR}:${aspectRatio};${attribs['style'] || ''}`; } } function appendAmpStoryCssLink(head) { const ampStoryCssLink = createElement('link', { 'rel': 'stylesheet', 'amp-extension': 'amp-story', // We rely on the `RewriteAmpUrls` transformer to modify this to // the correct LTS or correct rtv path. 'href': `${DEFAULT_AMP_CACHE_HOST}/v0/amp-story-1.0.css`, }); appendChild(head, ampStoryCssLink); } function appendAmpStoryDvhPolyfillScript(head) { const ampStoryDvhPolyfillScript = createElement('script', { [AMP_STORY_DVH_POLYFILL_ATTR]: '', }); insertText(ampStoryDvhPolyfillScript, AMP_STORY_DVH_POLYFILL_CONTENT); appendChild(head, ampStoryDvhPolyfillScript); } function isAmpStoryGridLayer(node) { return node.tagName === 'amp-story-grid-layer'; } function isAmpStoryScript(node) { return ( node.tagName === 'script' && node.attribs && node.attribs['custom-element'] === 'amp-story' ); } function isStyleAmpCustom(node) { return node.tagName === 'style' && hasAttribute(node, 'amp-custom'); } /** @module AmpStoryCssTransformer */ module.exports = AmpStoryCssTransformer;