1 | /**
|
---|
2 | * This is a PhantomJS script that renders a running page and captures both a screenshot
|
---|
3 | * of the rendered page and a data snapshot of the ComponentManager's set of rendered
|
---|
4 | * widgets. This data is then used to extract theme images.
|
---|
5 | */
|
---|
6 | var page = require('webpage').create(),
|
---|
7 | system = require('system'),
|
---|
8 | fs = require('fs');
|
---|
9 |
|
---|
10 | /**
|
---|
11 | * error handler logic, updates should be below this section
|
---|
12 | */
|
---|
13 | page.onConsoleMessage = function (msg) {
|
---|
14 | console.log(msg);
|
---|
15 | };
|
---|
16 |
|
---|
17 | function handleError (err, stack) {
|
---|
18 | console.log("== Unhandled Error ==");
|
---|
19 | phantom.defaultErrorHandler(err, stack);
|
---|
20 | phantom.exit(2);
|
---|
21 | }
|
---|
22 |
|
---|
23 | page.onError = phantom.onError = handleError;
|
---|
24 |
|
---|
25 | /* end error handler setup */
|
---|
26 |
|
---|
27 | if (system.args.length < 2) {
|
---|
28 | console.log("usage:");
|
---|
29 | console.log(" <path to html file> <image name> <widget data file>]:");
|
---|
30 | phantom.exit(1);
|
---|
31 | }
|
---|
32 |
|
---|
33 | /**
|
---|
34 | * args:
|
---|
35 | * 0 => this script's file name
|
---|
36 | * 1 => the html file to render (on windows, be mindful of '\\' chars)
|
---|
37 | * 2 => the name of the screen shot image (default: screenshot.png)
|
---|
38 | * 3 => the name of the widget data file (default: widgetdata.json)
|
---|
39 | */
|
---|
40 | var url = system.args[1].replace("\\", "/"),
|
---|
41 | screenCapFileName = system.args[2],
|
---|
42 | widgetDataFile = system.args[3],
|
---|
43 | configFiles = Array.prototype.slice.call(system.args, [4]);
|
---|
44 |
|
---|
45 |
|
---|
46 | console.log("loading page " + url);
|
---|
47 |
|
---|
48 | function waitFor (test, ready, timeout) {
|
---|
49 | var maxtimeOutMillis = timeout ? timeout : 30 * 1000,
|
---|
50 | start = new Date().getTime(),
|
---|
51 | condition = false,
|
---|
52 | interval = setInterval(function() {
|
---|
53 | if ((new Date().getTime() - start < maxtimeOutMillis) && !condition) {
|
---|
54 | condition = test();
|
---|
55 | } else {
|
---|
56 | if (!condition) {
|
---|
57 | console.log('failed to render widgets within 30 sec.');
|
---|
58 | phantom.exit(1);
|
---|
59 | } else {
|
---|
60 | clearInterval(interval);
|
---|
61 | ready();
|
---|
62 | }
|
---|
63 | }
|
---|
64 | }, 100);
|
---|
65 | }
|
---|
66 |
|
---|
67 | page.open(url, function (status) {
|
---|
68 | if (status === 'success') {
|
---|
69 | page.evaluate(function() {
|
---|
70 | if (document.addEventListener) {
|
---|
71 | document.addEventListener('DOMContentLoaded', function () {
|
---|
72 | // This is very important for getting transparency on corners.
|
---|
73 | document.body.style.backgroundColor = 'transparent';
|
---|
74 | });
|
---|
75 | }
|
---|
76 | document.body.style.backgroundColor = 'transparent';
|
---|
77 |
|
---|
78 |
|
---|
79 | window.generateSlicerManifest = function() {
|
---|
80 | var elements = document.body.querySelectorAll('.x-slicer-target');
|
---|
81 | var widgets = [];
|
---|
82 | var slicesRe = /^'x-slicer\:(.+)'$/;
|
---|
83 | var transparentRe = /^rgba\(.*[,]\s*0\)$/i;
|
---|
84 | var urlRe = /url[(]([^)]+)[)]/;
|
---|
85 |
|
---|
86 | function getData (el) {
|
---|
87 | var data = el.getAttribute('data-slicer');
|
---|
88 | if (data) {
|
---|
89 | return JSON.parse(data);
|
---|
90 | }
|
---|
91 | return null;
|
---|
92 | }
|
---|
93 |
|
---|
94 | function getSlices (entry, src) {
|
---|
95 | var content = src && src.content;
|
---|
96 | var slices = entry.slices;
|
---|
97 | if (content) {
|
---|
98 | var m = slicesRe.exec(content);
|
---|
99 | if (m && m[1]) {
|
---|
100 | var sliceStrings = m[1].split(', ');
|
---|
101 | forEach(sliceStrings, function(str){
|
---|
102 | // Each string looks like a url, with a schema, followed by some 'other' string, either a path or
|
---|
103 | // some other token
|
---|
104 | var colon = str.indexOf(':');
|
---|
105 | if (colon == -1) return;
|
---|
106 | var schema = str.slice(0, colon);
|
---|
107 | var path = str.slice(colon + 1);
|
---|
108 | if (schema == "stretch") {
|
---|
109 | // The stretch property is used to modify other slices for this widget, store it on its own
|
---|
110 | entry.stretch = path;
|
---|
111 | } else {
|
---|
112 | // The path indicates the desired output file to create for this type of slice operation
|
---|
113 | if (!!slices[schema] && 'url(' + slices[schema] + ')' != path) {
|
---|
114 | err("The widget " + entry.id + " declares two " + schema + " with two different urls");
|
---|
115 | }
|
---|
116 | // From SASS, this path is in the form of url(path), whereas we only want to pass along the inner
|
---|
117 | // part of the path
|
---|
118 | var urlMatch = urlRe.exec(path);
|
---|
119 | if (urlMatch && urlMatch[1]) {
|
---|
120 | slices[schema.replace(/-/g,'_')] = urlMatch[1];
|
---|
121 | } else {
|
---|
122 | err("The widget " + entry.id + "'s " + schema + " slice's url cannot be parsed: " + path);
|
---|
123 | }
|
---|
124 | }
|
---|
125 | });
|
---|
126 | }
|
---|
127 | }
|
---|
128 | }
|
---|
129 |
|
---|
130 | function err (str) {
|
---|
131 | console.error(str);
|
---|
132 | throw str;
|
---|
133 | }
|
---|
134 |
|
---|
135 | function forEach (it, fn) {
|
---|
136 | for (var i = 0; i < it.length; ++i) {
|
---|
137 | fn(it[i]);
|
---|
138 | }
|
---|
139 | }
|
---|
140 |
|
---|
141 | function copyProps (dest, src) {
|
---|
142 | var out = dest || {};
|
---|
143 | if (!!src) {
|
---|
144 | for (var key in src) {
|
---|
145 | var val = src[key];
|
---|
146 | if (typeof(val) == "object") {
|
---|
147 | out[key] = copyProps(out[key], val);
|
---|
148 | } else {
|
---|
149 | out[key] = val;
|
---|
150 | }
|
---|
151 | }
|
---|
152 | }
|
---|
153 |
|
---|
154 | return out;
|
---|
155 | }
|
---|
156 |
|
---|
157 | forEach(elements, function (el) {
|
---|
158 | var view = el.ownerDocument.defaultView;
|
---|
159 | var style = view.getComputedStyle(el, null);
|
---|
160 | var bg = style['background-image'];
|
---|
161 | var box = el.getBoundingClientRect();
|
---|
162 |
|
---|
163 | var entry = {
|
---|
164 | box: {
|
---|
165 | x: window.scrollX + box.left,
|
---|
166 | y: window.scrollY + box.top,
|
---|
167 | w: box.right - box.left,
|
---|
168 | h: box.bottom - box.top
|
---|
169 | },
|
---|
170 | radius: {
|
---|
171 | tl: parseInt(style['border-top-left-radius'], 10) || 0,
|
---|
172 | tr: parseInt(style['border-top-right-radius'], 10) || 0,
|
---|
173 | br: parseInt(style['border-bottom-right-radius'], 10) || 0,
|
---|
174 | bl: parseInt(style['border-bottom-left-radius'], 10) || 0
|
---|
175 | },
|
---|
176 | border: {
|
---|
177 | t: parseInt(style['border-top-width'], 10) || 0,
|
---|
178 | r: parseInt(style['border-right-width'], 10) || 0,
|
---|
179 | b: parseInt(style['border-bottom-width'], 10) || 0,
|
---|
180 | l: parseInt(style['border-left-width'], 10) || 0
|
---|
181 | }
|
---|
182 | };
|
---|
183 |
|
---|
184 | if (bg.indexOf('-gradient') !== -1) {
|
---|
185 | if (bg.indexOf('50% 0') !== -1 || bg.indexOf('top') !== -1 ||
|
---|
186 | bg.indexOf('bottom') !== -1) {
|
---|
187 | entry.gradient = 'top';
|
---|
188 | } else {
|
---|
189 | entry.gradient = 'left';
|
---|
190 | }
|
---|
191 | }
|
---|
192 |
|
---|
193 | // Reads from sass to get data
|
---|
194 | entry.slices = {};
|
---|
195 | getSlices(entry, view.getComputedStyle(el, ':after'));
|
---|
196 |
|
---|
197 | if (!!el.id) {
|
---|
198 | entry.id = el.id;
|
---|
199 |
|
---|
200 | // Merge with existing properties in global widgetSlices array, favoring widgetSlices
|
---|
201 | if (!!window.widgetSlices) {
|
---|
202 | entry = copyProps((window.widgetSlices && window.widgetSlices[el.id]), entry);
|
---|
203 | delete window.widgetSlices[el.id];
|
---|
204 | }
|
---|
205 | }
|
---|
206 |
|
---|
207 | if (!entry.gradient && !entry.slices.length &&
|
---|
208 | transparentRe.test(style['background-color']) &&
|
---|
209 | transparentRe.test(style['border-top-color']) &&
|
---|
210 | transparentRe.test(style['border-right-color']) &&
|
---|
211 | transparentRe.test(style['border-bottom-color']) &&
|
---|
212 | transparentRe.test(style['border-left-color'])) {
|
---|
213 | // If we have no gradient and the background and border are both
|
---|
214 | // transparent, the Sass is allowed to have no slices. If we do
|
---|
215 | // the push on this entry, the slicer core will generate warnings
|
---|
216 | // about it. This is a Good Thing and we don't want to blindly
|
---|
217 | // mask legitimate issues such as not sending in the proper slice
|
---|
218 | // requests.
|
---|
219 | return;
|
---|
220 | }
|
---|
221 |
|
---|
222 | widgets.push(entry);
|
---|
223 | });
|
---|
224 |
|
---|
225 | if (!!window.widgetSlices) {
|
---|
226 | for (var id in window.widgetSlices) {
|
---|
227 | // widgets.push(window.widgetSlices[id]);
|
---|
228 | console.error("Widget Slice detected without corresponding element : " + id);
|
---|
229 | }
|
---|
230 | }
|
---|
231 |
|
---|
232 | var slicerManifest = window.slicerManifest = getData(document.body) || {};
|
---|
233 | slicerManifest.widgets = widgets;
|
---|
234 | if (!slicerManifest.format) {
|
---|
235 | // legacy support sets format to "1.0"
|
---|
236 | slicerManifest.format = '2.0';
|
---|
237 | }
|
---|
238 | window['widgetsReady'] = true;
|
---|
239 | return slicerManifest;
|
---|
240 | };
|
---|
241 |
|
---|
242 | });
|
---|
243 |
|
---|
244 | waitFor(function(){
|
---|
245 | return page.evaluate(function() {
|
---|
246 | return !!(window['readyForConfig']);
|
---|
247 | });
|
---|
248 | }, function() {
|
---|
249 | var cfgs = [];
|
---|
250 | for (var i = 0; i < configFiles.length; i++) {
|
---|
251 | cfgs.push(fs.read(configFiles[i]));
|
---|
252 | };
|
---|
253 | page.evaluate(function(cfgs) {
|
---|
254 | window.readyForConfig.apply(null, cfgs);
|
---|
255 | }, cfgs);
|
---|
256 | });
|
---|
257 |
|
---|
258 | waitFor(function() {
|
---|
259 | return page.evaluate(function() {
|
---|
260 | return !!(window['widgetsReady']);
|
---|
261 | });
|
---|
262 | }, function() {
|
---|
263 | try {
|
---|
264 | console.log('Capturing screenshot');
|
---|
265 | page.render(screenCapFileName);
|
---|
266 | data = page.evaluate(function() {
|
---|
267 | if (!window["slicerManifest"]) {
|
---|
268 | window.generateSlicerManifest()
|
---|
269 | }
|
---|
270 | return window.slicerManifest;
|
---|
271 | });
|
---|
272 |
|
---|
273 | if (data) {
|
---|
274 | console.log('Saving slicer widget manifest');
|
---|
275 | fs.write(widgetDataFile, JSON.stringify(data, null, ' '), 'w');
|
---|
276 | }
|
---|
277 |
|
---|
278 | console.log('Capture complete');
|
---|
279 | phantom.exit();
|
---|
280 | } catch (e) {
|
---|
281 | console.log("Error capturing page : " + e);
|
---|
282 | phantom.exit(100);
|
---|
283 | }
|
---|
284 | });
|
---|
285 | } else {
|
---|
286 | console.log('Failed to load page');
|
---|
287 | phantom.exit(100);
|
---|
288 | }
|
---|
289 | });
|
---|