Skip to content

Commit 646ff7c

Browse files
Add embed compiler (ampproject#769)
* Add embed compiler Provides an API to generate embeddable samples including source code and live preview. * fix test failures * Use css to resize iframe * consistent constant names * bump version
1 parent 70c93c5 commit 646ff7c

13 files changed

+916
-22
lines changed

.npmignore

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
api
2+
app.yaml
3+
backend
4+
bower.json
5+
client-secret.json.enc
6+
data
7+
deploy.sh
8+
dist
9+
gulpfile.js
10+
node_modules
11+
playground
12+
scripts
13+
server.go
14+
spec
15+
src
16+
static
17+
tasks

lib/FileName.js

+11-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
"use strict";
1818

19-
const gutil = require('gulp-util');
2019
const path = require('path');
2120

2221
/**
@@ -46,17 +45,26 @@ module.exports.toString = function(file) {
4645
if (typeof file === 'string') {
4746
string = file;
4847
} else if (typeof file.path === 'string') {
49-
string = path.basename(file.path);
48+
string = file.path;
5049
} else {
5150
return '';
5251
}
53-
string = gutil.replaceExtension(path.basename(string), '');
52+
string = path.basename(string);
53+
string = stripFileExtension(string);
5454
string = string.replace(/_/g, ' ');
5555
string = decodeURIComponent(string);
5656
string = string.replace(/%27/g,"'");
5757
return string;
5858
};
5959

60+
function stripFileExtension(string) {
61+
const index = string.lastIndexOf(".");
62+
if (index === -1) {
63+
return string;
64+
}
65+
return string.substring(0, index);
66+
}
67+
6068
function encode(string) {
6169
let fileName = string.replace(/\s+/g, '_');
6270
fileName = encodeURIComponent(fileName);

lib/Preview.js

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
/**
2+
* Copyright 2015 Google Inc. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS-IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
'use strict'
18+
19+
const Hogan = require('hogan.js');
20+
const fs = require('fs');
21+
const glob = require('glob');
22+
const slug = require('slug');
23+
const pygmentize = require('pygmentize-bundled');
24+
const minify = require('html-minifier').minify;
25+
const path = require('path');
26+
const mkdirp = require('mkdirp').sync;
27+
const phantom = require('phantom');
28+
29+
const DocumentParser = require('./DocumentParser');
30+
31+
const templatesDir = path.join(__dirname, '../templates/embed');
32+
33+
const templates = {
34+
embed: compileTemplate('embed.html'),
35+
template: compileTemplate('template.html'),
36+
preview: compileTemplate('preview.html'),
37+
styles: compileTemplate('styles.css'),
38+
embedJs: compileTemplate('embed.js'),
39+
};
40+
41+
/**
42+
* Reads all html files in a folder and generates the embeds.
43+
*/
44+
function generateEmbeds(config) {
45+
glob(config.src + '/**/*.html', {}, (err, files) => {
46+
files.forEach(file => generateEmbed(config, file));
47+
});
48+
}
49+
50+
/**
51+
* Generates embeds for a given file.
52+
*/
53+
function generateEmbed(config, file) {
54+
const targetPath = path.join(config.destDir, path.relative(config.src, file));
55+
const document = parseDocument(file);
56+
const sampleSections = document.sections.filter(
57+
s => s.inBody && !s.isEmptyCodeSection()
58+
);
59+
sampleSections.forEach((section, index) => {
60+
highlight(section.code).then(code => {
61+
const tag = section.doc ? slug(section.doc.toLowerCase()) : index;
62+
const context = {
63+
sample: {
64+
file: path.basename(targetPath),
65+
preview: addFlag(targetPath, tag, 'preview'),
66+
embed: addFlag(targetPath, tag, 'embed'),
67+
template: addFlag(targetPath, tag, 'template'),
68+
body: section.code,
69+
code: code,
70+
},
71+
config: config,
72+
document: document,
73+
};
74+
generate(context.sample.preview, templates.preview, context, /* minify */ true);
75+
generate(context.sample.embed, templates.embed, context, /* minify */ true);
76+
generateTemplate(context);
77+
});
78+
});
79+
}
80+
81+
/**
82+
* Syntax highlights a string.
83+
*/
84+
function highlight(code) {
85+
return new Promise((resolve, reject) => {
86+
pygmentize({ lang: 'html', format: 'html' }, code, function (err, result) {
87+
if (err) {
88+
console.log(err);
89+
reject(err);
90+
} else {
91+
resolve(result.toString());
92+
}
93+
});
94+
});
95+
}
96+
97+
/**
98+
* Renders the given template into a file.
99+
*/
100+
function generate(file, template, context, minifyResult) {
101+
let string = template.render(context, {
102+
'styles.css': templates.styles,
103+
'embed.js': templates.embedJs
104+
});
105+
if (minifyResult) {
106+
string = minify(string, {
107+
caseSensitive: true,
108+
collapseWhitespace: true,
109+
html5: true,
110+
minifyCSS: true,
111+
minifyJS: true,
112+
removeComments: true,
113+
removeAttributeQuotes: true
114+
});
115+
}
116+
writeFile(path.join(context.config.destRoot, file), string);
117+
}
118+
119+
/**
120+
* Appends a list of flags separated by a '.' to a filename.
121+
*/
122+
function addFlag() {
123+
const filename = arguments[0];
124+
const postfix = [].slice.call(arguments, 1).join('.');
125+
return filename.replace('.html', '.' + postfix + '.html');
126+
}
127+
128+
/**
129+
* Parses an ABE document from a file.
130+
*/
131+
function parseDocument(file) {
132+
const inputString = fs.readFileSync(file, 'utf-8');
133+
return DocumentParser.parse(inputString);
134+
}
135+
136+
/**
137+
* Opens the embed html file in a browser to determine the initial height.
138+
*/
139+
function generateTemplate(context) {
140+
let _page;
141+
let _phantom;
142+
phantom.create([], { logLevel: 'error' }).then(function (ph) {
143+
_phantom = ph;
144+
ph.createPage()
145+
.then(page => {
146+
_page = page;
147+
const url = path.join(context.config.destRoot, context.sample.embed);
148+
return _page.property('viewportSize', {width: 1024, height: 768})
149+
.then(() => _page.open(url) );
150+
})
151+
.then(status => {
152+
return _page.evaluate(function() {
153+
const element = document.querySelector('#source-panel');
154+
const height = element.getBoundingClientRect().top + element.offsetHeight;
155+
return height;
156+
});
157+
})
158+
.then(height => {
159+
context.sample.height = height;
160+
generate(context.sample.template, templates.template, context);
161+
})
162+
.then(() => {
163+
_page.close();
164+
_phantom.exit();
165+
})
166+
.catch(err => console.log(err));
167+
});
168+
}
169+
170+
function compileTemplate(filePath) {
171+
let string = fs.readFileSync(path.join(templatesDir, filePath), 'utf8');
172+
return Hogan.compile(string);
173+
}
174+
175+
function writeFile(filePath, content) {
176+
mkdirp(path.dirname(filePath));
177+
fs.writeFileSync(filePath, content);
178+
}
179+
180+
module.exports = {};
181+
module.exports.generatePreview = generateEmbeds;

lib/examples/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist

lib/examples/generate.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Copyright 2015 Google Inc. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS-IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
'use strict'
18+
19+
const path = require('path');
20+
const abe = require('../Preview.js');
21+
const config = {
22+
src: path.join(__dirname, 'src'), // root folder containing the samples
23+
destRoot: path.join(__dirname, 'dist'), // target folder for generated embeds
24+
destDir: '/embeds', // optional sub dir
25+
host: 'https://example.com' // this is from where the embeds are going to be served
26+
}
27+
abe.generatePreview(config);

lib/examples/src/responsive.html

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<!doctype html>
2+
<html >
3+
<head>
4+
<meta charset="utf-8">
5+
<script async src="https://cdn.ampproject.org/v0.js"></script>
6+
<script async custom-element="amp-youtube" src="https://cdn.ampproject.org/v0/amp-youtube-0.1.js"></script>
7+
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
8+
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
9+
</head>
10+
<body>
11+
<!-- Youtube -->
12+
<amp-youtube data-videoid="WrpkFROqR0Q"
13+
layout="responsive"
14+
width="560" height="315">
15+
</amp-youtube>
16+
<!-- resolution -->
17+
<amp-img alt="apple" src="apple.jpg"
18+
height="596" width="900"
19+
srcset="apple-900.jpg 900w,
20+
apple-800.jpg 800w,
21+
apple-700.jpg 700w,
22+
apple-600.jpg 600w,
23+
apple-500.jpg 500w,
24+
apple-400.jpg 400w"
25+
sizes="(max-width: 400px) 100vw,
26+
(max-width: 900px) 75vw, 600px">
27+
</amp-img>
28+
<!-- breakpoints -->
29+
<div>
30+
<amp-img alt="grey cat"
31+
media="(min-width: 670px)"
32+
width="650" height="340"
33+
src="cat-large.jpg"></amp-img>
34+
<amp-img alt="grey cat"
35+
media="(min-width: 470px) and (max-width: 669px)"
36+
width="450" height="340"
37+
src="cat-medium.jpg"></amp-img>
38+
<amp-img alt="grey cat"
39+
media="(max-width: 469px)"
40+
width="226" height="340"
41+
src="cat-small.jpg"></amp-img>
42+
</div>
43+
<!-- webp -->
44+
<amp-img alt="Mountains"
45+
width="550" height="368"
46+
layout="responsive"
47+
src="mountains.webp">
48+
<amp-img alt="Mountains"
49+
fallback
50+
width="550" height="368"
51+
layout="responsive"
52+
src="mountains.jpg"></amp-img>
53+
</amp-img>
54+
</body>
55+
</html>

package.json

+15-18
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
{
22
"name": "amp-by-example",
3-
"version": "1.0.0",
3+
"version": "0.1.0",
44
"description": "AMP by example",
55
"repository": {
66
"type": "git",
77
"url": "https://github.com/ampproject/amp-by-example.git"
88
},
9-
"private": true,
10-
"main": "app.js",
9+
"main": "lib/Preview.js",
1110
"engines": {
1211
"node": ">=4.0.0"
1312
},
@@ -17,15 +16,21 @@
1716
"start": "gulp serve"
1817
},
1918
"dependencies": {
20-
"basscss": "^8.0.3",
21-
"basscss-basic": "^1.0.0",
22-
"compression": "^1.6.1",
23-
"connect": "^3.4.0",
24-
"express": "^4.13.4"
19+
"phantom": "^4.0.1",
20+
"pygmentize-bundled": "^2.3.0",
21+
"hogan.js": "^3.0.2",
22+
"html-minifier": "^3.4.2",
23+
"mkdirp": "^0.5.1",
24+
"marked": "^0.3.5",
25+
"highlight.js": "^9.7.0",
26+
"string": "^3.3.1",
27+
"js-beautify": "^1.6.11",
28+
"glob": "^7.1.1",
29+
"clean-css": "^3.4.10",
30+
"slug": "^0.9.1"
2531
},
2632
"devDependencies": {
2733
"amphtml-validator": "^1.0.15",
28-
"clean-css": "^3.4.10",
2934
"del": "^2.2.0",
3035
"eslint": "^2.0.0",
3136
"eslint-plugin-google-camelcase": "0.0.2",
@@ -48,18 +53,10 @@
4853
"gulp-rename": "^1.2.2",
4954
"gulp-run": "^1.7.1",
5055
"gulp-util": "^3.0.7",
51-
"highlight.js": "^9.7.0",
52-
"hogan.js": "^3.0.2",
5356
"htmllint": "^0.6.0",
54-
"js-beautify": "^1.6.11",
55-
"marked": "^0.3.5",
56-
"mkdirp": "^0.5.1",
57-
"postcss-import": "^8.2.0",
5857
"run-sequence": "^1.1.5",
5958
"sitemap": "^1.5.0",
60-
"string": "^3.3.1",
6159
"sw-toolbox": "^3.5.1",
62-
"through2": "^2.0.1",
63-
"yargs": "^3.10.0"
60+
"through2": "^2.0.1"
6461
}
6562
}

tasks/compile-example.js

-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ const gutil = require('gulp-util');
2020
const path = require('path');
2121
const through = require('through2');
2222
const fs = require('fs');
23-
const mkdirp = require('mkdirp');
2423
const PluginError = gutil.PluginError;
2524
const DocumentParser = require('../lib/DocumentParser');
2625
const ExampleFile = require('../lib/ExampleFile');

0 commit comments

Comments
 (0)