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

Unified 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 side-by-side diff with in-line comments
Download patch
Index: telemetry/third_party/snap-it/popup.js
diff --git a/telemetry/third_party/snap-it/popup.js b/telemetry/third_party/snap-it/popup.js
new file mode 100644
index 0000000000000000000000000000000000000000..17a92234ae74ca48479c9c83f36b85b7e40103e9
--- /dev/null
+++ b/telemetry/third_party/snap-it/popup.js
@@ -0,0 +1,340 @@
+document.addEventListener('DOMContentLoaded', function() {
+ if (document.getElementById('button')) {
+ document.getElementById('button').addEventListener('click', click);
+ }
+});
+
+function click() {
+ var messages = []
+ var results;
+ chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
+ messages.push(message);
+ if (results && messages.length == results.length) {
+ completeProcess(messages);
+ }
+ });
+
+ var serializer = {file: 'HTMLSerializer.js', allFrames: true};
+ chrome.tabs.executeScript(null, serializer, function() {
+ var contentScript = {file: 'content_script.js', allFrames: true};
+ chrome.tabs.executeScript(null, contentScript, function(response) {
+ results = response;
+ if (messages.length == results.length)
+ completeProcess(messages);
+ });
+ });
+
+ var a = document.getElementById('button');
+ a.className = 'serialize';
+ a.innerHTML = 'Serializing...';
+ a.removeEventListener('click', click);
+}
+
+/**
+ * Takes all the responses from the injected content scripts and creates the
+ * HTML file for download.
+ *
+ * @param {Array<Object>} messages The response from all of the injected content
+ * scripts.
+ */
+function completeProcess(messages) {
+ var html = outputHTMLString(messages);
+ var file = new Blob([html], {type: 'text/html'});
+ var url = URL.createObjectURL(file);
+
+ var a = document.getElementById('button');
+ a.className = 'download';
+ a.innerHTML = 'Download';
+ a.href = url;
+ a.download = "snap-it.html";
+}
+
+/**
+ * Converts the responses from the injected content scripts into a string
+ * representing the HTML.
+ *
+ * @param {Array<Object>} messages The response from all of the injected content
+ * scripts.
+ * @return {string} The resulting HTML.
+ */
+function outputHTMLString(messages) {
+ var rootIndex = 0;
+ for (var i = 1; i < messages.length; i++) {
+ rootIndex = messages[i].frameIndex === '0' ? i : rootIndex;
+ }
+ fillRemainingHolesAndMinimizeStyles(messages, rootIndex);
+ return messages[rootIndex].html.join('');
+}
+
+/**
+ * Fills all of the gaps in |messages[i].html|.
+ *
+ * @param {Array<Object>} messages The response from all of the injected content
+ * scripts.
+ * @param {number} i The index of messages to use.
+ */
+function fillRemainingHolesAndMinimizeStyles(messages, i) {
+ var html = messages[i].html;
+ var frameHoles = messages[i].frameHoles;
+ for (var index in frameHoles) {
+ if (frameHoles.hasOwnProperty(index)) {
+ var frameIndex = frameHoles[index];
+ for (var j = 0; j < messages.length; j++) {
+ if (messages[j].frameIndex == frameIndex) {
+ fillRemainingHolesAndMinimizeStyles(messages, j);
+ html[index] = messages[j].html.join('');
+ }
+ }
+ }
+ }
+ minimizeStyles(messages[i]);
+}
+
+/**
+ * Removes all style attribute properties that are unneeded.
+ *
+ * @param {Object} message The message Object whose style attributes should be
+ * minimized.
+ */
+function minimizeStyles(message) {
+ var nestingDepth = message.frameIndex.split('.').length - 1;
+ var iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ iframe.setAttribute(
+ 'style',
+ `height: ${message.windowHeight}px;` +
+ `width: ${message.windowWidth}px;`);
+
+ var html = message.html.join('');
+ html = unescapeHTML(html, nestingDepth);
+ iframe.contentDocument.documentElement.innerHTML = html;
+ var doc = iframe.contentDocument;
+
+ // Remove entry in |message.html| where extra style element was specified.
+ message.html[message.pseudoElementTestingStyleIndex] = '';
+ var finalPseudoElements = [];
+ for (var selector in message.pseudoElementSelectorToCSSMap) {
+ minimizePseudoElementStyle(message, doc, selector, finalPseudoElements);
+ }
+
+ message.html[message.pseudoElementPlaceHolderIndex] =
+ `<style>${finalPseudoElements.join(' ')}</style>`;
+
+ if (message.rootStyleIndex) {
+ minimizeStyle(
+ message,
+ doc,
+ doc.documentElement,
+ message.rootId,
+ message.rootStyleIndex);
+ }
+
+ for (var id in message.idToStyleIndex) {
+ var index = message.idToStyleIndex[id];
+ var element = doc.getElementById(id);
+ if (element) {
+ minimizeStyle(message, doc, element, id, index);
+ }
+ }
+ iframe.remove();
+}
+
+/**
+ * Removes all style attribute properties that are unneeded for a single
+ * pseudo element.
+ *
+ * @param {Object} message The message Object that contains the pseudo element
+ * whose style attributes should be minimized.
+ * @param {Document} doc The Document that contains the rendered HTML.
+ * @param {string} selector The CSS selector for the pseudo element.
+ * @param {Array<string>} finalPseudoElements An array to contain the final
+ * declaration of pseudo elements. It will be updated to reflect the pseudo
+ * element that is being processed.
+ */
+function minimizePseudoElementStyle(
+ message,
+ doc,
+ selector,
+ finalPseudoElements) {
+ var maxNumberOfIterations = 5;
+ var match = selector.match(/^#(.*):(:.*)$/);
+ var id = match[1];
+ var type = match[2];
+ var element = doc.getElementById(id);
+ if (element) {
+ var originalStyleMap = message.pseudoElementSelectorToCSSMap[selector];
+ var requiredStyleMap = {};
+ // We compare the computed style before and after removing the pseudo
+ // element and accumulate the differences in |requiredStyleMap|. The pseudo
+ // element is removed by changing the element id. Because some properties
+ // affect other properties, such as border-style: solid causing a change in
+ // border-width, we do this iteratively until a fixed-point is reached (or
+ // |maxNumberOfIterations| is hit).
+ // TODO(sfine): Unify this logic with minimizeStyles.
+ for (var i = 0; i < maxNumberOfIterations; i++) {
+ var currentPseudoElement = ['#' + message.unusedId + ':' + type + '{'];
+ currentPseudoElement.push(buildStyleAttribute(requiredStyleMap));
+ currentPseudoElement.push('}');
+ element.setAttribute('id', message.unusedId);
+ var style = doc.getElementById(message.pseudoElementTestingStyleId);
+ style.innerHTML = currentPseudoElement.join(' ');
+ var foundNewRequiredStyle = updateMinimizedStyleMap(
+ doc,
+ element,
+ originalStyleMap,
+ requiredStyleMap,
+ type);
+ if (!foundNewRequiredStyle) {
+ break;
+ }
+ }
+ element.setAttribute('id', id);
+ finalPseudoElements.push('#' + id + ':' + type + '{');
+ var finalPseudoElement = buildStyleAttribute(requiredStyleMap);
+ var nestingDepth = message.frameIndex.split('.').length - 1;
+ finalPseudoElement = finalPseudoElement.replace(
+ /"/g,
+ escapedQuote(nestingDepth));
+ finalPseudoElements.push(finalPseudoElement);
+ finalPseudoElements.push('}');
+ }
+}
+
+
+/**
+ * Removes all style attribute properties that are unneeded for a single
+ * element.
+ *
+ * @param {Object} message The message Object that contains the element whose
+ * style attributes should be minimized.
+ * @param {Document} doc The Document that contains the rendered HTML.
+ * @param {Element} element The Element whose style attributes should be
+ * minimized.
+ * @param {string} id The id of the Element in the final page.
+ * @param {number} index The index in |message.html| where the Element's style
+ * attribute is specified.
+ */
+function minimizeStyle(message, doc, element, id, index) {
+ var originalStyleAttribute = element.getAttribute('style');
+ var originalStyleMap = message.idToStyleMap[id];
+ var requiredStyleMap = {};
+ var maxNumberOfIterations = 5;
+
+ // We compare the computed style before and after removing the style attribute
+ // and accumulate the differences in |requiredStyleMap|. Because some
+ // properties affect other properties, such as boder-style: solid causing a
+ // change in border-width, we do this iteratively until a fixed-point is
+ // reached (or |maxNumberOfIterations| is hit).
+ for (var i = 0; i < maxNumberOfIterations; i++) {
+ element.setAttribute('style', buildStyleAttribute(requiredStyleMap));
+ var foundNewRequiredStyle = updateMinimizedStyleMap(
+ doc,
+ element,
+ originalStyleMap,
+ requiredStyleMap,
+ null);
+ element.setAttribute('style', buildStyleAttribute(originalStyleMap));
+ if (!foundNewRequiredStyle) {
+ break;
+ }
+ }
+
+ var finalStyleAttribute = buildStyleAttribute(requiredStyleMap);
+ if (finalStyleAttribute) {
+ var nestingDepth = message.frameIndex.split('.').length - 1;
+ finalStyleAttribute = finalStyleAttribute.replace(
+ /"/g,
+ escapedQuote(nestingDepth + 1));
+ var quote = escapedQuote(nestingDepth);
+ message.html[index] = `style=${quote}${finalStyleAttribute}${quote} `;
+ } else {
+ message.html[index] = '';
+ }
+}
+
+/**
+ * We compare the original computed style with the minimized computed style
+ * and update |minimizedStyleMap| based on any differences.
+ *
+ * @param {Document} doc The Document that contains the rendered HTML.
+ * @param {Element} element The Element whose style attributes should be
+ * minimized.
+ * @param {Object<string, string>} originalStyleMap A map representing the
+ * original computed style values. The keys are style attribute property
+ * names. The values are the corresponding property values.
+ * @param {Object<string, string>} minimizedStyleMap A map representing the
+ * minimized style values. The keys are style attribute property names. The
+ * values are the corresponding property values.
+ * @param {string} pseudo If the style describes an ordinary
+ * Element, then |pseudo| will be set to null. If the style describes a
+ * pseudo element, then |pseudo| will be the string that represents that
+ * pseudo element.
+ * @return {boolean} Returns true if minimizedStyleMap was changed. Returns false
+ * otherwise.
+ */
+function updateMinimizedStyleMap(
+ doc,
+ element,
+ originalStyleMap,
+ minimizedStyleMap,
+ pseudo) {
+ var currentComputedStyle = doc.defaultView.getComputedStyle(element, pseudo);
+ var foundNewRequiredStyle = false;
+ for (var property in originalStyleMap) {
+ var originalValue = originalStyleMap[property];
+ if (originalValue != currentComputedStyle.getPropertyValue(property)) {
+ minimizedStyleMap[property] = originalValue;
+ foundNewRequiredStyle = true;
+ }
+ }
+ return foundNewRequiredStyle;
+}
+
+/**
+ * Build a style attribute from a map of property names to property values.
+ *
+ * @param {Object<string, string} styleMap The keys are style attribute property
+ * names. The values are the corresponding property values.
+ * @return {string} The correct style attribute.
+ */
+function buildStyleAttribute(styleMap) {
+ var styleAttribute = [];
+ for (var property in styleMap) {
+ styleAttribute.push(property + ': ' + styleMap[property] + ';');
+ }
+ return styleAttribute.join(' ');
+}
+
+/**
+ * Take a string that represents valid HTML and unescape it so that it can be
+ * rendered.
+ *
+ * @param {string} html The HTML to unescape.
+ * @param {number} nestingDepth The number of times the HTML must be unescaped.
+ * @return {string} The unescaped HTML.
+ */
+function unescapeHTML(html, nestingDepth) {
+ var div = document.createElement('div');
+ for (var i = 0; i < nestingDepth; i++) {
+ div.innerHTML = `<iframe srcdoc="${html}"></iframe>`;
+ html = div.childNodes[0].attributes.srcdoc.value;
+ }
+ return html;
+}
+
+/**
+ * Calculate the correct encoding of a quotation mark that should be used given
+ * the nesting depth of the window in the frame tree.
+ *
+ * @param {number} depth The nesting depth of the appropriate window in the
+ * frame tree.
+ * @return {string} The correctly escaped string.
+ */
+function escapedQuote(depth) {
+ if (depth == 0) {
+ return '"';
+ } else {
+ var arr = 'amp;'.repeat(depth-1);
+ return '&' + arr + 'quot;';
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698