| OLD | NEW |
| (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 } |
| OLD | NEW |