Skip to content

Commit 959e621

Browse files
authored
Add AmpStoryCssTransformer (#1270)
* temp * temp * lint fix * add aspect-ratio transformation * add other required transformations * lint fixes * update reorderhead transformer * lint * add some comments
1 parent 1c33cc7 commit 959e621

File tree

20 files changed

+449
-13
lines changed

20 files changed

+449
-13
lines changed

packages/optimizer/lib/AmpConstants.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@
1717

1818
const {hasAttribute} = require('./NodeUtils');
1919

20+
const AMP_STORY_DVH_POLYFILL_ATTR = 'amp-story-dvh-polyfill';
21+
2022
module.exports = {
2123
AMP_TAGS: ['amp', '⚡', '⚡4ads', 'amp4ads', '⚡4email', 'amp4email'],
2224
AMP_CACHE_HOST: 'https://cdn.ampproject.org',
2325
AMP_VALIDATION_RULES_URL: 'https://cdn.ampproject.org/v0/validator.json',
2426
AMP_FORMATS: ['AMP', 'AMP4EMAIL', 'AMP4ADS'],
2527
AMP_RUNTIME_CSS_PATH: '/v0.css',
28+
AMP_STORY_DVH_POLYFILL_ATTR,
2629
appendRuntimeVersion: (prefix, version) => prefix + '/rtv/' + version,
2730
isTemplate: (node) => {
2831
if (!node) {
@@ -48,6 +51,9 @@ module.exports = {
4851
}
4952
return false;
5053
},
54+
isAmpStoryDvhPolyfillScript: (node) => {
55+
return node.tagName === 'script' && hasAttribute(node, AMP_STORY_DVH_POLYFILL_ATTR);
56+
},
5157
};
5258

5359
function isAmpScriptImport(scriptNode) {

packages/optimizer/lib/extensionConfig.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
const {skipNodeAndChildren} = require('../HtmlDomHelper');
2+
const {
3+
isTemplate,
4+
AMP_STORY_DVH_POLYFILL_ATTR,
5+
isAmpStoryDvhPolyfillScript,
6+
} = require('../AmpConstants');
7+
const {calculateHost} = require('../RuntimeHostHelper');
8+
const {
9+
insertText,
10+
hasAttribute,
11+
remove,
12+
createElement,
13+
nextNode,
14+
firstChildByTag,
15+
appendChild,
16+
} = require('../NodeUtils');
17+
18+
// This string should not be modified, even slightly. This string is strictly
19+
// checked by the validator.
20+
const AMP_STORY_DVH_POLYFILL_CONTENT =
21+
'"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()})';
22+
23+
const ASPECT_RATIO_ATTR = 'aspect-ratio';
24+
25+
class AmpStoryCssTransformer {
26+
constructor(config) {
27+
this.log_ = config.log.tag('AmpStoryCssTransformer');
28+
29+
this.enabled_ = !!config.optimizeAmpStory;
30+
31+
if (!this.enabled_) {
32+
this.log_.debug('disabled');
33+
}
34+
}
35+
36+
transform(root, params) {
37+
if (!this.enabled_) return;
38+
39+
const html = firstChildByTag(root, 'html');
40+
if (!html) return;
41+
42+
const head = firstChildByTag(html, 'head');
43+
if (!head) return;
44+
45+
const body = firstChildByTag(html, 'body');
46+
if (!body) return;
47+
48+
let hasAmpStoryScript = false;
49+
let hasAmpStoryDvhPolyfillScript = false;
50+
let styleAmpCustom = null;
51+
52+
for (let node = head.firstChild; node !== null; node = node.nextSibling) {
53+
if (isAmpStoryScript(node)) {
54+
hasAmpStoryScript = true;
55+
continue;
56+
}
57+
58+
if (isAmpStoryDvhPolyfillScript(node)) {
59+
hasAmpStoryDvhPolyfillScript = true;
60+
continue;
61+
}
62+
63+
if (isStyleAmpCustom(node)) {
64+
styleAmpCustom = node;
65+
continue;
66+
}
67+
}
68+
69+
// We can return early if no amp-story script is found.
70+
if (!hasAmpStoryScript) return;
71+
72+
const host = calculateHost(params);
73+
74+
appendAmpStoryCssLink(host, head);
75+
76+
if (styleAmpCustom) {
77+
modifyAmpCustomCSS(styleAmpCustom);
78+
// Make sure to not double install the dvh polyfill.
79+
if (!hasAmpStoryDvhPolyfillScript) {
80+
appendAmpStoryDvhPolyfillScript(head);
81+
}
82+
}
83+
84+
supportsLandscapeSSR(body, html);
85+
86+
aspectRatioSSR(body);
87+
}
88+
}
89+
90+
function modifyAmpCustomCSS(style) {
91+
if (!style.children) return;
92+
const children = style.children;
93+
// Remove all text children from style.
94+
// NOTE(erwinm): Is it actually possible in htmlparser2 to have multiple
95+
// text children?
96+
for (let i = 0; i < children.length; i++) {
97+
const child = children[i];
98+
if (child.type == 'text' && child.data) {
99+
const newText = child.data.replace(
100+
/(-?[\d.]+)v(w|h|min|max)/gim,
101+
'calc($1 * var(--story-page-v$2))'
102+
);
103+
remove(child);
104+
insertText(style, newText);
105+
}
106+
}
107+
}
108+
109+
function supportsLandscapeSSR(body, html) {
110+
const story = firstChildByTag(body, 'amp-story');
111+
if (!story) return;
112+
if (hasAttribute(story, 'supports-landscape') && html.attribs) {
113+
html.attribs['data-story-supports-landscape'] = '';
114+
}
115+
}
116+
117+
function aspectRatioSSR(body) {
118+
for (let node = body; node !== null; node = nextNode(node)) {
119+
if (isTemplate(node)) {
120+
node = skipNodeAndChildren(node);
121+
continue;
122+
}
123+
124+
if (!isAmpStoryGridLayer(node)) continue;
125+
126+
const {attribs} = node;
127+
if (!attribs || !attribs[ASPECT_RATIO_ATTR] || typeof attribs[ASPECT_RATIO_ATTR] !== 'string') {
128+
continue;
129+
}
130+
131+
const aspectRatio = attribs[ASPECT_RATIO_ATTR].replace(/:/g, '/');
132+
// We need to a `attribs['style'] || ''` in case there is no style attribute as we
133+
// don't want to coerce "undefined" or "null" into a string.
134+
attribs['style'] = `--${ASPECT_RATIO_ATTR}:${aspectRatio};${attribs['style'] || ''}`;
135+
}
136+
}
137+
138+
function appendAmpStoryCssLink(host, head) {
139+
const ampStoryCssLink = createElement('link', {
140+
'rel': 'stylesheet',
141+
'amp-extension': 'amp-story',
142+
'href': `${host}/v0/amp-story-1.0.css`,
143+
});
144+
appendChild(head, ampStoryCssLink);
145+
}
146+
147+
function appendAmpStoryDvhPolyfillScript(head) {
148+
const ampStoryDvhPolyfillScript = createElement('script', {
149+
[AMP_STORY_DVH_POLYFILL_ATTR]: '',
150+
});
151+
insertText(ampStoryDvhPolyfillScript, AMP_STORY_DVH_POLYFILL_CONTENT);
152+
appendChild(head, ampStoryDvhPolyfillScript);
153+
}
154+
155+
function isAmpStoryGridLayer(node) {
156+
return node.tagName === 'amp-story-grid-layer';
157+
}
158+
159+
function isAmpStoryScript(node) {
160+
return (
161+
node.tagName === 'script' && node.attribs && node.attribs['custom-element'] === 'amp-story'
162+
);
163+
}
164+
165+
function isStyleAmpCustom(node) {
166+
return node.tagName === 'style' && hasAttribute(node, 'amp-custom');
167+
}
168+
169+
/** @module AmpStoryCssTransformer */
170+
module.exports = AmpStoryCssTransformer;

packages/optimizer/lib/transformers/ReorderHeadTransformer.js

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,22 @@
1616
'use strict';
1717

1818
const {appendChild, appendAll, hasAttribute, firstChildByTag} = require('../NodeUtils');
19+
const {isAmpStoryDvhPolyfillScript} = require('../AmpConstants');
1920
const {isRenderDelayingExtension} = require('../Extensions.js');
2021

2122
class HeadNodes {
2223
constructor() {
2324
this._styleAmpRuntime = null;
2425
this._linkStyleAmpRuntime = null;
26+
this._linkStyleAmpStory = null;
2527
this._metaCharset = null;
2628
this._metaViewport = null;
2729
this._scriptAmpEngine = [];
2830
this._metaOther = [];
2931
this._resourceHintLinks = [];
3032
this._scriptRenderDelayingExtensions = new Map();
3133
this._scriptNonRenderDelayingExtensions = new Map();
34+
this._scriptAmpStoryDvhPollyfill = null;
3235
this._linkIcons = [];
3336
this._styleAmpCustom = null;
3437
this._linkStylesheetsBeforeAmpCustom = [];
@@ -63,7 +66,9 @@ class HeadNodes {
6366
appendAll(head, this._metaOther);
6467
appendChild(head, this._linkStyleAmpRuntime);
6568
appendChild(head, this._styleAmpRuntime);
69+
appendChild(head, this._linkStyleAmpStory);
6670
appendAll(head, this._scriptAmpEngine);
71+
appendChild(head, this._scriptAmpStoryDvhPollyfill);
6772
appendAll(head, this._scriptRenderDelayingExtensions);
6873
appendAll(head, this._scriptNonRenderDelayingExtensions);
6974
appendChild(head, this._styleAmpCustom);
@@ -127,6 +132,10 @@ class HeadNodes {
127132
this._registerExtension(this._scriptNonRenderDelayingExtensions, name, scriptIndex, node);
128133
return;
129134
}
135+
if (isAmpStoryDvhPolyfillScript(node)) {
136+
this._scriptAmpStoryDvhPollyfill = node;
137+
return;
138+
}
130139
this._others.push(node);
131140
}
132141

@@ -159,6 +168,10 @@ class HeadNodes {
159168
this._linkStyleAmpRuntime = node;
160169
return;
161170
}
171+
if (node.attribs.href.endsWith('/amp-story-1.0.css')) {
172+
this._linkStyleAmpStory = node;
173+
return;
174+
}
162175
if (!this._styleAmpCustom) {
163176
// We haven't seen amp-custom yet.
164177
this._linkStylesheetsBeforeAmpCustom.push(node);
@@ -195,16 +208,18 @@ class HeadNodes {
195208
* orders the <head> like so:
196209
* (0) <meta charset> tag
197210
* (1) <style amp-runtime> (inserted by ampruntimecss.go)
198-
* (2) remaining <meta> tags (those other than <meta charset>)
199-
* (3) AMP runtime .js <script> tag
200-
* (4) <script> tags that are render delaying
201-
* (5) <script> tags for remaining extensions
202-
* (6) <link> tag for favicons
203-
* (7) <link> tag for resource hints
204-
* (8) <link rel=stylesheet> tags before <style amp-custom>
205-
* (9) <style amp-custom>
206-
* (10) any other tags allowed in <head>
207-
* (11) AMP boilerplate (first style amp-boilerplate, then noscript)
211+
* (2) <link amp-extension=amp-story> (amp-story stylesheet)
212+
* (3) remaining <meta> tags (those other than <meta charset>)
213+
* (4) AMP runtime .js <script> tag
214+
* (5) <script amp-story-dvh-polyfill> inline script tag
215+
* (6) <script> tags that are render delaying
216+
* (7) <script> tags for remaining extensions
217+
* (8) <link> tag for favicons
218+
* (9) <link> tag for resource hints
219+
* (10) <link rel=stylesheet> tags before <style amp-custom>
220+
* (11) <style amp-custom>
221+
* (12) any other tags allowed in <head>
222+
* (13) AMP boilerplate (first style amp-boilerplate, then noscript)
208223
*/
209224
class ReorderHeadTransformer {
210225
transform(tree) {

packages/optimizer/lib/transformers/ServerSideRendering.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ class ServerSideRendering {
115115

116116
let customStyles;
117117
for (let node = head.firstChild; node; node = node.nextSibling) {
118-
// amp-experiment is a render delaying extension iff the tag is used in
118+
// amp-experiment is a render delaying extension if the tag is used in
119119
// the doc, which we checked for above.
120120
if (
121121
node.tagName === 'script' &&
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<!doctype html>
2+
<html >
3+
<head>
4+
<script async src="https://cdn.ampproject.org/v0.js"></script>
5+
<script async custom-element="amp-story" src="https://cdn.ampproject.org/v0/amp-story-1.0.js"></script>
6+
<link href="https://example.com/favicon.ico" rel="icon">
7+
<style amp-custom></style>
8+
<link rel="stylesheet" amp-extension="amp-story" href="https://cdn.ampproject.org/v0/amp-story-1.0.css"><script amp-story-dvh-polyfill>"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()})</script>
9+
</head>
10+
<body>
11+
<amp-story standalone poster-portrait-src="http://url.example/" publisher-logo-src="http://url.example/" publisher title>
12+
<amp-story-page id="my-first-page">
13+
<amp-story-grid-layer template="fill">
14+
<amp-img src="https://ampbyexample.com/img/image3.jpg" width="720" height="1280"></amp-img>
15+
</amp-story-grid-layer>
16+
</amp-story-page>
17+
</amp-story>
18+
</body>
19+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!doctype html>
2+
<html >
3+
<head>
4+
<script async src="https://cdn.ampproject.org/v0.js"></script>
5+
<script async custom-element="amp-story" src="https://cdn.ampproject.org/v0/amp-story-1.0.js"></script>
6+
<link href="https://example.com/favicon.ico" rel="icon">
7+
<style amp-custom></style>
8+
</head>
9+
<body>
10+
<amp-story standalone poster-portrait-src="http://url.example/" publisher-logo-src="http://url.example/" publisher title>
11+
<amp-story-page id="my-first-page">
12+
<amp-story-grid-layer template="fill">
13+
<amp-img src="https://ampbyexample.com/img/image3.jpg" width="720" height="1280"></amp-img>
14+
</amp-story-grid-layer>
15+
</amp-story-page>
16+
</amp-story>
17+
</body>
18+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!doctype html>
2+
<html >
3+
<head>
4+
<script async src="https://cdn.ampproject.org/v0.js"></script>
5+
<script async custom-element="amp-story" src="https://cdn.ampproject.org/v0/amp-story-1.0.js"></script>
6+
<link href="https://example.com/favicon.ico" rel="icon">
7+
<link rel="stylesheet" amp-extension="amp-story" href="https://cdn.ampproject.org/v0/amp-story-1.0.css">
8+
</head>
9+
<body>
10+
<amp-story standalone poster-portrait-src="http://url.example/" publisher-logo-src="http://url.example/" publisher title>
11+
<amp-story-page id="my-first-page">
12+
<amp-story-grid-layer template="fill">
13+
<amp-img src="https://ampbyexample.com/img/image3.jpg" width="720" height="1280"></amp-img>
14+
</amp-story-grid-layer>
15+
</amp-story-page>
16+
</amp-story>
17+
</body>
18+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!doctype html>
2+
<html >
3+
<head>
4+
<script async src="https://cdn.ampproject.org/v0.js"></script>
5+
<script async custom-element="amp-story" src="https://cdn.ampproject.org/v0/amp-story-1.0.js"></script>
6+
<link href=https://example.com/favicon.ico rel=icon>
7+
</head>
8+
<body>
9+
<amp-story standalone poster-portrait-src="http://url.example/" publisher-logo-src="http://url.example/" publisher title>
10+
<amp-story-page id="my-first-page">
11+
<amp-story-grid-layer template="fill">
12+
<amp-img src="https://ampbyexample.com/img/image3.jpg" width="720" height="1280"></amp-img>
13+
</amp-story-grid-layer>
14+
</amp-story-page>
15+
</amp-story>
16+
</body>
17+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!doctype html>
2+
<html >
3+
<head>
4+
<script async src="https://cdn.ampproject.org/lts/v0.js"></script>
5+
<script async custom-element="amp-story" src="https://cdn.ampproject.org/lts/v0/amp-story-1.0.js"></script>
6+
<link href="https://example.com/favicon.ico" rel="icon">
7+
<link rel="stylesheet" amp-extension="amp-story" href="https://cdn.ampproject.org/v0/amp-story-1.0.css">
8+
</head>
9+
<body>
10+
<amp-story standalone poster-portrait-src="http://url.example/" publisher-logo-src="http://url.example/" publisher title>
11+
<amp-story-page id="my-first-page">
12+
<amp-story-grid-layer template="fill">
13+
<amp-img src="https://ampbyexample.com/img/image3.jpg" width="720" height="1280"></amp-img>
14+
</amp-story-grid-layer>
15+
</amp-story-page>
16+
</amp-story>
17+
</body>
18+
</html>

0 commit comments

Comments
 (0)