Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(57)

Side by Side Diff: telemetry/third_party/snap-it/popup.js

Issue 3010063002: [Telemetry] Add script to snapshot page's HTML (Closed)
Patch Set: fix --interactive flag spec Created 3 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 document.addEventListener('DOMContentLoaded', function() {
2 if (document.getElementById('button')) {
3 document.getElementById('button').addEventListener('click', click);
4 }
5 });
6
7 function click() {
8 var messages = []
9 var results;
10 chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
11 messages.push(message);
12 if (results && messages.length == results.length) {
13 completeProcess(messages);
14 }
15 });
16
17 var serializer = {file: 'HTMLSerializer.js', allFrames: true};
18 chrome.tabs.executeScript(null, serializer, function() {
19 var contentScript = {file: 'content_script.js', allFrames: true};
20 chrome.tabs.executeScript(null, contentScript, function(response) {
21 results = response;
22 if (messages.length == results.length)
23 completeProcess(messages);
24 });
25 });
26
27 var a = document.getElementById('button');
28 a.className = 'serialize';
29 a.innerHTML = 'Serializing...';
30 a.removeEventListener('click', click);
31 }
32
33 /**
34 * Takes all the responses from the injected content scripts and creates the
35 * HTML file for download.
36 *
37 * @param {Array<Object>} messages The response from all of the injected content
38 * scripts.
39 */
40 function completeProcess(messages) {
41 var html = outputHTMLString(messages);
42 var file = new Blob([html], {type: 'text/html'});
43 var url = URL.createObjectURL(file);
44
45 var a = document.getElementById('button');
46 a.className = 'download';
47 a.innerHTML = 'Download';
48 a.href = url;
49 a.download = "snap-it.html";
50 }
51
52 /**
53 * Converts the responses from the injected content scripts into a string
54 * representing the HTML.
55 *
56 * @param {Array<Object>} messages The response from all of the injected content
57 * scripts.
58 * @return {string} The resulting HTML.
59 */
60 function outputHTMLString(messages) {
61 var rootIndex = 0;
62 for (var i = 1; i < messages.length; i++) {
63 rootIndex = messages[i].frameIndex === '0' ? i : rootIndex;
64 }
65 fillRemainingHolesAndMinimizeStyles(messages, rootIndex);
66 return messages[rootIndex].html.join('');
67 }
68
69 /**
70 * Fills all of the gaps in |messages[i].html|.
71 *
72 * @param {Array<Object>} messages The response from all of the injected content
73 * scripts.
74 * @param {number} i The index of messages to use.
75 */
76 function fillRemainingHolesAndMinimizeStyles(messages, i) {
77 var html = messages[i].html;
78 var frameHoles = messages[i].frameHoles;
79 for (var index in frameHoles) {
80 if (frameHoles.hasOwnProperty(index)) {
81 var frameIndex = frameHoles[index];
82 for (var j = 0; j < messages.length; j++) {
83 if (messages[j].frameIndex == frameIndex) {
84 fillRemainingHolesAndMinimizeStyles(messages, j);
85 html[index] = messages[j].html.join('');
86 }
87 }
88 }
89 }
90 minimizeStyles(messages[i]);
91 }
92
93 /**
94 * Removes all style attribute properties that are unneeded.
95 *
96 * @param {Object} message The message Object whose style attributes should be
97 * minimized.
98 */
99 function minimizeStyles(message) {
100 var nestingDepth = message.frameIndex.split('.').length - 1;
101 var iframe = document.createElement('iframe');
102 document.body.appendChild(iframe);
103 iframe.setAttribute(
104 'style',
105 `height: ${message.windowHeight}px;` +
106 `width: ${message.windowWidth}px;`);
107
108 var html = message.html.join('');
109 html = unescapeHTML(html, nestingDepth);
110 iframe.contentDocument.documentElement.innerHTML = html;
111 var doc = iframe.contentDocument;
112
113 // Remove entry in |message.html| where extra style element was specified.
114 message.html[message.pseudoElementTestingStyleIndex] = '';
115 var finalPseudoElements = [];
116 for (var selector in message.pseudoElementSelectorToCSSMap) {
117 minimizePseudoElementStyle(message, doc, selector, finalPseudoElements);
118 }
119
120 message.html[message.pseudoElementPlaceHolderIndex] =
121 `<style>${finalPseudoElements.join(' ')}</style>`;
122
123 if (message.rootStyleIndex) {
124 minimizeStyle(
125 message,
126 doc,
127 doc.documentElement,
128 message.rootId,
129 message.rootStyleIndex);
130 }
131
132 for (var id in message.idToStyleIndex) {
133 var index = message.idToStyleIndex[id];
134 var element = doc.getElementById(id);
135 if (element) {
136 minimizeStyle(message, doc, element, id, index);
137 }
138 }
139 iframe.remove();
140 }
141
142 /**
143 * Removes all style attribute properties that are unneeded for a single
144 * pseudo element.
145 *
146 * @param {Object} message The message Object that contains the pseudo element
147 * whose style attributes should be minimized.
148 * @param {Document} doc The Document that contains the rendered HTML.
149 * @param {string} selector The CSS selector for the pseudo element.
150 * @param {Array<string>} finalPseudoElements An array to contain the final
151 * declaration of pseudo elements. It will be updated to reflect the pseudo
152 * element that is being processed.
153 */
154 function minimizePseudoElementStyle(
155 message,
156 doc,
157 selector,
158 finalPseudoElements) {
159 var maxNumberOfIterations = 5;
160 var match = selector.match(/^#(.*):(:.*)$/);
161 var id = match[1];
162 var type = match[2];
163 var element = doc.getElementById(id);
164 if (element) {
165 var originalStyleMap = message.pseudoElementSelectorToCSSMap[selector];
166 var requiredStyleMap = {};
167 // We compare the computed style before and after removing the pseudo
168 // element and accumulate the differences in |requiredStyleMap|. The pseudo
169 // element is removed by changing the element id. Because some properties
170 // affect other properties, such as border-style: solid causing a change in
171 // border-width, we do this iteratively until a fixed-point is reached (or
172 // |maxNumberOfIterations| is hit).
173 // TODO(sfine): Unify this logic with minimizeStyles.
174 for (var i = 0; i < maxNumberOfIterations; i++) {
175 var currentPseudoElement = ['#' + message.unusedId + ':' + type + '{'];
176 currentPseudoElement.push(buildStyleAttribute(requiredStyleMap));
177 currentPseudoElement.push('}');
178 element.setAttribute('id', message.unusedId);
179 var style = doc.getElementById(message.pseudoElementTestingStyleId);
180 style.innerHTML = currentPseudoElement.join(' ');
181 var foundNewRequiredStyle = updateMinimizedStyleMap(
182 doc,
183 element,
184 originalStyleMap,
185 requiredStyleMap,
186 type);
187 if (!foundNewRequiredStyle) {
188 break;
189 }
190 }
191 element.setAttribute('id', id);
192 finalPseudoElements.push('#' + id + ':' + type + '{');
193 var finalPseudoElement = buildStyleAttribute(requiredStyleMap);
194 var nestingDepth = message.frameIndex.split('.').length - 1;
195 finalPseudoElement = finalPseudoElement.replace(
196 /"/g,
197 escapedQuote(nestingDepth));
198 finalPseudoElements.push(finalPseudoElement);
199 finalPseudoElements.push('}');
200 }
201 }
202
203
204 /**
205 * Removes all style attribute properties that are unneeded for a single
206 * element.
207 *
208 * @param {Object} message The message Object that contains the element whose
209 * style attributes should be minimized.
210 * @param {Document} doc The Document that contains the rendered HTML.
211 * @param {Element} element The Element whose style attributes should be
212 * minimized.
213 * @param {string} id The id of the Element in the final page.
214 * @param {number} index The index in |message.html| where the Element's style
215 * attribute is specified.
216 */
217 function minimizeStyle(message, doc, element, id, index) {
218 var originalStyleAttribute = element.getAttribute('style');
219 var originalStyleMap = message.idToStyleMap[id];
220 var requiredStyleMap = {};
221 var maxNumberOfIterations = 5;
222
223 // We compare the computed style before and after removing the style attribute
224 // and accumulate the differences in |requiredStyleMap|. Because some
225 // properties affect other properties, such as boder-style: solid causing a
226 // change in border-width, we do this iteratively until a fixed-point is
227 // reached (or |maxNumberOfIterations| is hit).
228 for (var i = 0; i < maxNumberOfIterations; i++) {
229 element.setAttribute('style', buildStyleAttribute(requiredStyleMap));
230 var foundNewRequiredStyle = updateMinimizedStyleMap(
231 doc,
232 element,
233 originalStyleMap,
234 requiredStyleMap,
235 null);
236 element.setAttribute('style', buildStyleAttribute(originalStyleMap));
237 if (!foundNewRequiredStyle) {
238 break;
239 }
240 }
241
242 var finalStyleAttribute = buildStyleAttribute(requiredStyleMap);
243 if (finalStyleAttribute) {
244 var nestingDepth = message.frameIndex.split('.').length - 1;
245 finalStyleAttribute = finalStyleAttribute.replace(
246 /"/g,
247 escapedQuote(nestingDepth + 1));
248 var quote = escapedQuote(nestingDepth);
249 message.html[index] = `style=${quote}${finalStyleAttribute}${quote} `;
250 } else {
251 message.html[index] = '';
252 }
253 }
254
255 /**
256 * We compare the original computed style with the minimized computed style
257 * and update |minimizedStyleMap| based on any differences.
258 *
259 * @param {Document} doc The Document that contains the rendered HTML.
260 * @param {Element} element The Element whose style attributes should be
261 * minimized.
262 * @param {Object<string, string>} originalStyleMap A map representing the
263 * original computed style values. The keys are style attribute property
264 * names. The values are the corresponding property values.
265 * @param {Object<string, string>} minimizedStyleMap A map representing the
266 * minimized style values. The keys are style attribute property names. The
267 * values are the corresponding property values.
268 * @param {string} pseudo If the style describes an ordinary
269 * Element, then |pseudo| will be set to null. If the style describes a
270 * pseudo element, then |pseudo| will be the string that represents that
271 * pseudo element.
272 * @return {boolean} Returns true if minimizedStyleMap was changed. Returns fals e
273 * otherwise.
274 */
275 function updateMinimizedStyleMap(
276 doc,
277 element,
278 originalStyleMap,
279 minimizedStyleMap,
280 pseudo) {
281 var currentComputedStyle = doc.defaultView.getComputedStyle(element, pseudo);
282 var foundNewRequiredStyle = false;
283 for (var property in originalStyleMap) {
284 var originalValue = originalStyleMap[property];
285 if (originalValue != currentComputedStyle.getPropertyValue(property)) {
286 minimizedStyleMap[property] = originalValue;
287 foundNewRequiredStyle = true;
288 }
289 }
290 return foundNewRequiredStyle;
291 }
292
293 /**
294 * Build a style attribute from a map of property names to property values.
295 *
296 * @param {Object<string, string} styleMap The keys are style attribute property
297 * names. The values are the corresponding property values.
298 * @return {string} The correct style attribute.
299 */
300 function buildStyleAttribute(styleMap) {
301 var styleAttribute = [];
302 for (var property in styleMap) {
303 styleAttribute.push(property + ': ' + styleMap[property] + ';');
304 }
305 return styleAttribute.join(' ');
306 }
307
308 /**
309 * Take a string that represents valid HTML and unescape it so that it can be
310 * rendered.
311 *
312 * @param {string} html The HTML to unescape.
313 * @param {number} nestingDepth The number of times the HTML must be unescaped.
314 * @return {string} The unescaped HTML.
315 */
316 function unescapeHTML(html, nestingDepth) {
317 var div = document.createElement('div');
318 for (var i = 0; i < nestingDepth; i++) {
319 div.innerHTML = `<iframe srcdoc="${html}"></iframe>`;
320 html = div.childNodes[0].attributes.srcdoc.value;
321 }
322 return html;
323 }
324
325 /**
326 * Calculate the correct encoding of a quotation mark that should be used given
327 * the nesting depth of the window in the frame tree.
328 *
329 * @param {number} depth The nesting depth of the appropriate window in the
330 * frame tree.
331 * @return {string} The correctly escaped string.
332 */
333 function escapedQuote(depth) {
334 if (depth == 0) {
335 return '"';
336 } else {
337 var arr = 'amp;'.repeat(depth-1);
338 return '&' + arr + 'quot;';
339 }
340 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698