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

Unified Diff: telemetry/third_party/snap-it/HTMLSerializer.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/HTMLSerializer.js
diff --git a/telemetry/third_party/snap-it/HTMLSerializer.js b/telemetry/third_party/snap-it/HTMLSerializer.js
new file mode 100644
index 0000000000000000000000000000000000000000..7dcc3f69aa8ce4abcda1e65326c1459c4b23e84c
--- /dev/null
+++ b/telemetry/third_party/snap-it/HTMLSerializer.js
@@ -0,0 +1,755 @@
+/**
+ * HTML Serializer that takes a document and synchronously stores it as an array
+ * of strings, then asynchronously retrieves data URLs for same-origin images.
+ * It stores enough state to later be converted to an html text file.
+ */
+var HTMLSerializer = class {
+ constructor() {
+
+ /**
+ * @private {Set<string>} Contains the tag names that should be
+ * ignored while serializing a document.
+ * @const
+ */
+ this.FILTERED_TAGS = new Set(['SCRIPT', 'NOSCRIPT', 'STYLE', 'LINK']);
+
+ /**
+ * @private {Set<string>} Contains the tag names for elements
+ * that have no closing tags. List of tags taken from:
+ * https://html.spec.whatwg.org/multipage/syntax.html#void-elements.
+ * @const
+ */
+ this.NO_CLOSING_TAGS = new Set([
+ 'AREA',
+ 'BASE',
+ 'BR',
+ 'COL',
+ 'EMBED',
+ 'HR',
+ 'IMG',
+ 'INPUT',
+ 'KEYGEN',
+ 'LINK',
+ 'META',
+ 'PARAM',
+ 'SOURCE',
+ 'TRACK',
+ 'WBR'
+ ]);
+
+ /**
+ * @private {Array<string>} A list of the pseudo elements that will be
+ * processed.
+ * @const
+ */
+ this.PSEUDO_ELEMENTS = [':before', ':after'];
+
+ /**
+ * @private {Object<string, string>} The keys are all characters that need
+ * to be properly escaped when in a text node. The value is the
+ * properly escaped string.
+ * @const
+ */
+ this.CHARACTER_ESCAPING_MAP = {
+ '&' : '&amp;',
+ '<' : '&lt;',
+ '>' : '&gt;',
+ '"' : '&quot;',
+ "'" : '&#39;'
+ };
+
+ /**
+ * @public {Object<string, number>} An enum representing different types of
+ * text.
+ * @const
+ */
+ this.INPUT_TEXT_TYPE = {
+ HTML : 0,
+ CSS : 1
+ };
+
+ /**
+ * @public {Array<string>} This array represents the serialized html that
+ * makes up a node or document.
+ */
+ this.html = [];
+
+ /**
+ * @public {Object<number, string>} The keys represent an index in
+ * |this.html|. The value is a url at which the resource that belongs at
+ * that index can be retrieved. The resource will eventually be
+ * converted to a data url.
+ */
+ this.srcHoles = {};
+
+ /**
+ * @public {Object<number, string>} The keys represent an index in
+ * |this.html|. The value is a string that uniquely identifies an
+ * iframe, the serialized contents of which should be placed at that
+ * index of |this.html|.
+ */
+ this.frameHoles = {};
+
+ /**
+ * @private {Array<string>} Each element of |this.crossOriginStyleSheets|
+ * contains a url to a stylesheet that does not have the same origin
+ * as the webpage being serialized.
+ */
+ this.crossOriginStyleSheets = [];
+
+ /**
+ * @private {Array<string>} Each element of |this.fontCSS| will contain
+ * a CSS declaration of a different externally loaded font.
+ */
+ this.fontCSS = [];
+
+ /**
+ * @private {number} The index in |this.html| where the style element
+ * containing the fonts will go.
+ */
+ this.fontPlaceHolderIndex;
+
+ /**
+ * @private {number} The index in |this.html| where the style element
+ * containing the pseudo elements will go.
+ */
+ this.pseudoElementPlaceHolderIndex;
+
+ /**
+ * @private {number} The id of a style element that can be used to test
+ * minimized pseudo element declarations in popup.js.
+ */
+ this.pseudoElementTestingStyleId;
+
+ /**
+ * @private {number} The index in |this.html| where a style element will be
+ * placed to test minimized pseudo element declarations in popup.js.
+ */
+ this.pseudoElementTestingStyleIndex;
+
+ /**
+ * @private {Array<string>} Each element of this array is a string
+ * representing CSS that defines a single pseudo element.
+ */
+ this.pseudoElementCSS = [];
+
+ /**
+ * @private {Object<string, Object<string, string>>} The keys represent a
+ * pseudo element selector. The value is a map of that pseudo element's
+ * style property names to property values.
+ */
+ this.pseudoElementSelectorToCSSMap = {};
+
+ /**
+ * @private {Function} A funtion that generates a unique string each time it
+ * is called, which can be used as an element id.
+ */
+ this.generateId = this.generateIdGenerator();
+
+ /**
+ * @private {number} The window height of the Document being serialized.
+ */
+ this.windowHeight;
+
+ /**
+ * @private {number} The window width of the Document being serialized.
+ */
+ this.windowWidth;
+
+ /**
+ * @private {Object<string, number>} The keys represent the id of an
+ * Element. The value is the index in |this.html| where the
+ * value of the style attribute for that Element is specified.
+ */
+ this.idToStyleIndex = {};
+
+ /**
+ * @private {Object<string, Object<string, string>>} The keys represent the
+ * id of an Element. The value is a map of that Element's style
+ * attribute property names to property values.
+ */
+ this.idToStyleMap = {};
+
+ /**
+ * @private {number} The index in |this.html| at which the html element's
+ * style attribute is specified.
+ */
+ this.rootStyleIndex;
+
+ /**
+ * @private {string} The assigned id of the html element.
+ */
+ this.rootId;
+ }
+
+ /**
+ * Takes an html document, and populates this objects fields such that it can
+ * eventually be converted into an html file.
+ *
+ * @param {Document} doc The Document to serialize.
+ */
+ processDocument(doc) {
+ this.windowHeight = doc.defaultView.innerHeight;
+ this.windowWidth = doc.defaultView.innerWidth;
+
+ if (doc.doctype) {
+ this.html.push('<!DOCTYPE html>\n');
+ }
+
+ if (this.iframeFullyQualifiedName(doc.defaultView) == '0') {
+ this.html.push(
+ `<!-- Original window height: ${this.windowHeight}. -->\n`);
+ this.html.push(`<!-- Original window width: ${this.windowWidth}. -->\n`);
+ }
+
+ this.loadFonts(doc);
+ this.pseudoElementPlaceHolderIndex = this.html.length;
+ this.html.push(''); // Entry where pseudo element style tag will go.
+ this.pseudoElementTestingStyleIndex = this.html.length;
+ this.html.push(''); // Entry where minimized pseudo elements can be tested.
+
+ var nodes = doc.childNodes;
+ for (var i = 0, node; node = nodes[i]; i++) {
+ if (node.nodeType != Node.DOCUMENT_TYPE_NODE) {
+ this.processTree(node);
+ }
+ }
+ var pseudoElements = `<style>${this.pseudoElementCSS.join('')}</style>`;
+ this.html[this.pseudoElementPlaceHolderIndex] = pseudoElements;
+
+ this.pseudoElementTestingStyleId = this.generateId(doc);
+ var style = `<style id="${this.pseudoElementTestingStyleId}"></style>`;
+ var nestingDepth = this.windowDepth(doc.defaultView);
+ var escapedQuote = this.escapedCharacter('"', nestingDepth);
+ style = style.replace(/"/g, escapedQuote);
+ this.html[this.pseudoElementTestingStyleIndex] = style;
+ }
+
+ /**
+ * Takes an html node, and populates this object's fields such that it can
+ * eventually be converted into an html text file.
+ *
+ * @param {Node} node The Node to serialize.
+ * @private
+ */
+ processTree(node) {
+ var tagName = node.tagName;
+ if (!tagName && node.nodeType != Node.TEXT_NODE) {
+ // Ignore nodes that don't have tags and are not text.
+ } else if (tagName && this.FILTERED_TAGS.has(tagName)) {
+ // Filter out nodes that are in filteredTags.
+ } else if (node.nodeType == Node.TEXT_NODE) {
+ this.processText(node);
+ } else {
+ this.html.push(`<${tagName.toLowerCase()} `);
+ var id;
+ if (node.attributes.id) {
+ id = node.attributes.id.value;
+ } else {
+ id = this.generateId(node.ownerDocument);
+ }
+ this.processAttributes(node, id);
+ this.processPseudoElements(node, id);
+ this.html.push('>');
+
+ if (tagName == 'HEAD') {
+ this.fontPlaceHolderIndex = this.html.length;
+ this.html.push('');
+ this.pseudoElementPlaceHolderIndex = this.html.length;
+ this.html.push('');
+ this.pseudoElementTestingStyleIndex = this.html.length;
+ this.html.push('');
+ }
+
+ var children = node.childNodes;
+ if (children) {
+ for (var i = 0, child; child = children[i]; i++) {
+ this.processTree(child);
+ }
+ }
+
+ if (!this.NO_CLOSING_TAGS.has(tagName)) {
+ this.html.push(`</${tagName.toLowerCase()}>`);
+ }
+ }
+ }
+
+ /**
+ * Takes an HTML element, and if it has pseudo elements listed in
+ * |this.PSEUDO_ELEMENTS| they will be added to |this.pseudoElementCSS| and
+ * |this.pseudoElementSelectorToCSSMap|.
+ *
+ * @param {Element} element The Element whose pseudo elements will be
+ * processed.
+ * @param {string} id The id of the Element whose pseudo elements will be
+ * processed.
+ * @private
+ */
+ processPseudoElements(element, id) {
+ var win = element.ownerDocument.defaultView;
+ for (var i = 0, pseudo; pseudo = this.PSEUDO_ELEMENTS[i]; i++) {
+ var style = win.getComputedStyle(element, pseudo);
+ if (style.content) {
+ var nestingDepth = this.windowDepth(win);
+ var escapedQuote = this.escapedCharacter('"', nestingDepth);
+ var styleText = style.cssText.replace(/"/g, escapedQuote);
+ styleText = this.escapedUnicodeString(
+ styleText,
+ this.INPUT_TEXT_TYPE.CSS);
+ this.pseudoElementCSS.push(
+ '#' + id + ':' + pseudo + '{' + styleText + '} ');
+
+ var styleMap = {};
+ for (var i = 0; i < style.length; i++) {
+ var propertyName = style.item(i);
+ var propertyValue = style.getPropertyValue(propertyName);
+ propertyValue = this.escapedUnicodeString(
+ propertyValue,
+ this.INPUT_TEXT_TYPE.CSS);
+ styleMap[propertyName] = propertyValue;
+ }
+ this.pseudoElementSelectorToCSSMap['#' + id + ':' + pseudo] = styleMap;
+ }
+ }
+ }
+
+ /**
+ * Takes an html node of type Node.TEXT_NODE, and add its text content with
+ * all characters properly escaped to |this.html|.
+ * @param {Node} node The text node.
+ */
+ // TODO(sfine): Take care of attribute value normalization:
+ // https://developers.whatwg.org/the-iframe-element.html#the-iframe-element
+ processText(node) {
+ var win = node.ownerDocument.defaultView;
+ var nestingDepth = this.windowDepth(win);
+ var text = node.textContent;
+ text = this.escapedCharacterString(text, nestingDepth+1);
+ text = this.escapedUnicodeString(text, this.INPUT_TEXT_TYPE.HTML);
+ this.html.push(text);
+ }
+
+ /**
+ * Takes an html element, and populates this object's fields with the
+ * appropriate attribute names and values.
+ *
+ * @param {Element} element The Element to serialize.
+ * @param {string} id The id of the Element being serialized.
+ * @private
+ */
+ processAttributes(element, id) {
+ var win = element.ownerDocument.defaultView;
+ var style = win.getComputedStyle(element, null);
+ var styleMap = {};
+ for (var i = 0; i < style.length; i++) {
+ var propertyName = style.item(i);
+ styleMap[propertyName] = style.getPropertyValue(propertyName);
+ }
+ this.idToStyleMap[id] = styleMap;
+ this.idToStyleIndex[id] = this.html.length;
+ if (element.tagName == 'HTML') {
+ this.rootStyleIndex = this.html.length;
+ this.rootId = id;
+ }
+ this.processSimpleAttribute(win, 'style', style.cssText);
+ this.processSimpleAttribute(win, 'id', id);
+
+ var attributes = element.attributes;
+ if (attributes) {
+ for (var i = 0, attribute; attribute = attributes[i]; i++) {
+ switch (attribute.name.toLowerCase()) {
+ case 'src':
+ this.processSrcAttribute(element);
+ break;
+ case 'style':
+ case 'id':
+ break;
+ default:
+ var name = attribute.name;
+ var value = attribute.value;
+ this.processSimpleAttribute(win, name, value);
+ }
+ }
+ // TODO(sfine): Ensure this is working by making sure that an iframe
+ // will always have attributes.
+ if (element.tagName == 'IFRAME' && element.attributes.src) {
+ var valueIndex = this.processHoleAttribute(win, 'srcdoc');
+ var iframeName = this.iframeFullyQualifiedName(element.contentWindow);
+ this.frameHoles[valueIndex] = iframeName;
+ }
+ }
+ }
+
+ /**
+ * Process the src attribute of a given element.
+ *
+ * @param {Element} element The Element being processed, which has the src
+ * attribute.
+ * @private
+ */
+ processSrcAttribute(element) {
+ var win = element.ownerDocument.defaultView;
+ var url = this.fullyQualifiedURL(element);
+ var sameOrigin = window.location.host == url.host;
+ switch (element.tagName) {
+ case 'IFRAME':
+ break; // Do nothing.
+ case 'SOURCE':
+ var parent = element.parent;
+ if (parent && parent.tagName == 'PICTURE' && sameOrigin) {
+ this.processSrcHole(element);
+ } else {
+ this.processSimpleAttribute(win, 'src', url.href);
+ }
+ break;
+ case 'INPUT':
+ var type = element.attributes.type;
+ if (type && type.value.toLowerCase() == 'image') {
+ this.processSrcHole(element);
+ }
+ break;
+ case 'IMG':
+ if (sameOrigin) {
+ this.processSrcHole(element);
+ } else {
+ this.processSimpleAttribute(win, 'src', url.href);
+ }
+ break;
+ default:
+ this.processSimpleAttribute(win, 'src', url.href);
+ }
+ }
+
+ /**
+ * Get a URL object for the value of the |element|'s src attribute.
+ *
+ * @param {Element} element The element for which to retrieve the URL.
+ * @return {URL} The URL object.
+ */
+ fullyQualifiedURL(element) {
+ var url = element.attributes.src.value;
+ var a = document.createElement('a');
+ a.href = url;
+ url = a.href; // Retrieve fully qualified URL.
+ return new URL(url);
+ }
+
+ /**
+ * Add an entry to |this.srcHoles| so it can be processed asynchronously.
+ *
+ * @param {Element} element The element being processed, which has the src
+ * attribute.
+ * @private
+ */
+ processSrcHole(element) {
+ var win = element.ownerDocument.defaultView;
+ var valueIndex = this.processHoleAttribute(win, 'src');
+ this.srcHoles[valueIndex] = this.fullyQualifiedURL(element).href;
+ }
+
+ /**
+ * Add an attribute with name |name| to |this.html| with an empty index for
+ * its value that can later be filled in.
+ *
+ * @param {Window} win The window of the Element that is being processed.
+ * @param {string} name The name of the attribute.
+ * @return {number} The index in |this.html| where the value will be placed.
+ */
+ processHoleAttribute(win, name) {
+ var quote = this.escapedCharacter('"', this.windowDepth(win));
+ this.html.push(`${name}=${quote}`);
+ var valueIndex = this.html.length;
+ this.html.push(''); // Entry where value will go.
+ this.html.push(quote + ' '); // Add a space before the next attribute.
+ return valueIndex;
+ }
+
+ /**
+ * Add a name and value pair to the list of attributes in |this.html|.
+ *
+ * @param {Window} win The window of the Element that is being processed.
+ * @param {string} name The name of the attribute.
+ * @param {string} value The value of the attribute.
+ */
+ processSimpleAttribute(win, name, value) {
+ var nestingDepth = this.windowDepth(win);
+ var quote = this.escapedCharacter('"', nestingDepth);
+ value = this.escapedCharacterString(value, nestingDepth+1);
+ this.html.push(`${name}=${quote}${value}${quote} `);
+ }
+
+ /**
+ * Load all external fonts, and add an entry to |this.html| at index
+ * |this.fontPlaceHolderIndex|.
+ *
+ * @param {Document} doc The Document being serialized.
+ */
+ loadFonts(doc) {
+ this.fontPlaceHolderIndex = this.html.length;
+ this.html.push(''); // Entry where the font style tag will go.
+ for (var i = 0, styleSheet; styleSheet = doc.styleSheets[i]; i++) {
+ if (styleSheet.cssRules) {
+ for (var j = 0, rule; rule = styleSheet.cssRules[j]; j++) {
+ this.processCSSFonts(doc.defaultView, styleSheet.href, rule.cssText);
+ }
+ } else {
+ this.crossOriginStyleSheets.push(styleSheet.href);
+ }
+ }
+ }
+
+ /**
+ * Takes a string representing CSS and parses it to find any fonts that are
+ * declared. If any fonts are declared, it processes them so that they
+ * can be used in the serialized document and adds them to |this.fontCSS|.
+ *
+ * @param {Window} win The Window of the Document being serialized.
+ * @param {string} href The url at which the CSS stylesheet is located.
+ * @param {string} css The CSS text.
+ */
+ processCSSFonts(win, href, css) {
+ var serializer = this;
+ var fonts = css.match(/@font-face *?{[\s\S]*?}/g);
+ if (fonts) {
+ var nestingDepth = this.windowDepth(win);
+ var escapedQuote = this.escapedCharacter('"', nestingDepth);
+ for (var i = 0; i < fonts.length; i++) {
+ // Convert url specified in font to fully qualified url.
+ var font = fonts[i].replace(/url\("(.*?)"\)/g, function(match, url) {
+ // If href is null the url must be a fully qualified url.
+ url = href ? serializer.fullyQualifiedFontURL(href, url) : url;
+ return 'url("' + url + '")';
+ }).
+ replace(/"/g, escapedQuote);
+ this.fontCSS.push(font);
+ }
+ }
+ }
+
+ /**
+ * Computes the fully qualified url at which a font can be loaded.
+ * TODO(sfine): Make this method sufficiently robust, so that it can replace
+ * the current implementation of fullyQualifiedURL.
+ *
+ * @param {string} href The url at which the CSS stylesheet containing the
+ * font is located.
+ * @param {string} url The url listed in the font declaration.
+ */
+ fullyQualifiedFontURL(href, url) {
+ if (href.charAt(href.length-1) == '/') {
+ href = href.slice(0, href.length-1);
+ }
+ var hrefURL = new URL(href);
+ if (url.includes('://')) {
+ return url;
+ } else if (url.startsWith('//')) {
+ return hrefURL.protocol + url;
+ } else if (url.startsWith('/')) {
+ return hrefURL.origin + url;
+ } else {
+ href = href.slice(0, href.lastIndexOf('/'));
+ return href + '/' + url;
+ }
+ }
+
+ /**
+ * Computes the index of the window in its parent's array of frames.
+ *
+ * @param {Window} childWindow The window to use in the calculation.
+ * @return {number} the frames index.
+ */
+ iframeIndex(childWindow) {
+ if (childWindow.parent != childWindow) {
+ for (var i = 0; i < childWindow.parent.frames.length; i++) {
+ if (childWindow.parent.frames[i] == childWindow) {
+ return i;
+ }
+ }
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Computes the full path of the frame in the root document. Nested layers
+ * are seperated by '.'.
+ *
+ * @param {Window} win The window to use in the calculation.
+ * @return {string} The full path.
+ */
+ iframeFullyQualifiedName(win) {
+ if (this.iframeIndex(win) < 0) {
+ return '0';
+ } else {
+ var fullyQualifiedName = this.iframeFullyQualifiedName(win.parent);
+ var index = this.iframeIndex(win);
+ return fullyQualifiedName + '.' + index;
+ }
+ }
+
+ /**
+ * Calculate the correct encoding of a character that should be used given the
+ * nesting depth of the window in the frame tree.
+ *
+ * @param {string} char The character that should be escaped.
+ * @param {number} depth The nesting depth of the appropriate window in the
+ * frame tree.
+ * @return {string} The correctly escaped string.
+ */
+ escapedCharacter(char, depth) {
+ if (depth == 0) {
+ return char;
+ } else {
+ var arr = 'amp;'.repeat(depth-1);
+ return '&' + arr + this.CHARACTER_ESCAPING_MAP[char].slice(1);
+ }
+ }
+
+ /**
+ * Returns the string that is passed as an argument with all characters in
+ * |this.ESCAPED_CHARACTER_MAP| replaced with the correct character encoding
+ * that should be used, given the nesting depth of the window in the frame
+ * tree.
+ *
+ * @param {string} str The string that should have its characters escaped.
+ * @param {number} depth The nesting depth of the appropriate window in the
+ * frame tree.
+ * @return {string} The correctly escaped string.
+ */
+ escapedCharacterString(str, depth) {
+ // Some escaping introduces '&' characters so we escape '&' first to prevent
+ // escaping the '&' added by other escape substitutions.
+ str = str.replace(/&/g, this.escapedCharacter('&', depth));
+ for (var char in this.CHARACTER_ESCAPING_MAP) {
+ if (char != '&') {
+ var regExp = new RegExp(char, 'g');
+ str = str.replace(regExp, this.escapedCharacter(char, depth));
+ }
+ }
+ return str;
+ }
+
+ /**
+ * Returns the string that is passed as an argument with all non ascii unicode
+ * characters escaped.
+ *
+ * @param {string} str The string that should have its characters escaped.
+ * @param {number} textType A possible value of |this.INPUT_TEXT_TYPE| which
+ * represents the type of text being escaped.
+ * @return {string} The correctly escaped string.
+ */
+ escapedUnicodeString(str, textType) {
+ var serializer = this;
+ return str.replace(/[\s\S]/g, function(char) {
+ var unicode = char.codePointAt();
+ if (unicode < 128) {
+ return char;
+ } else if (textType == serializer.INPUT_TEXT_TYPE.HTML) {
+ return '&#' + unicode + ';';
+ } else {
+ return '\\' + unicode.toString(16);
+ }
+ });
+ }
+
+ /**
+ * Calculate the nesting depth of a window in the frame tree.
+ *
+ * @param {Window} win The window to use in the calculation.
+ * @return {number} The nesting depth of the window in the frame trees.
+ */
+ windowDepth(win) {
+ return this.iframeFullyQualifiedName(win).split('.').length - 1;
+ }
+
+ /**
+ * Create a function that will generate strings which can be used as
+ * ids.
+ *
+ * @return {Function<Document>} A funtion that generates a valid id each time
+ * it is called.
+ */
+ generateIdGenerator() {
+ var counter = 0;
+ function idGenerator(doc) {
+ var id;
+ do {
+ id = 'snap-it' + counter++;
+ } while (doc.getElementById(id));
+ return id;
+ }
+ return idGenerator;
+ }
+
+ /**
+ * Asynchronously fill in any holes in |this.html|.
+ *
+ * @param {Document} doc The Document being serialized.
+ * @param {Function} callback The callback function, which will be called when
+ * all asynchronous processing is finished.
+ */
+ fillHolesAsync(doc, callback) {
+ var serializer = this;
+ this.fillFontHoles(doc, function() {
+ serializer.fillSrcHoles(callback);
+ });
+ }
+
+ /**
+ * Takes all of the cross origin stylesheets, processes their font
+ * declarations, and adds them to |this.html|. Calls the callback when
+ * complete.
+ *
+ * @param {Document} doc The Document being serialized.
+ * @param {Function} callback The callback function.
+ */
+ fillFontHoles(doc, callback) {
+ if (this.crossOriginStyleSheets.length == 0) {
+ var fonts = `<style>${this.fontCSS.join('')}</style>`;
+ this.html[this.fontPlaceHolderIndex] = fonts;
+ callback();
+ } else {
+ var styleSheetSrc = this.crossOriginStyleSheets.shift();
+ var serializer = this;
+ fetch(styleSheetSrc).then(function(response) {
+ return response.text();
+ }).then(function(css) {
+ serializer.processCSSFonts(doc.defaultView, styleSheetSrc, css);
+ serializer.fillFontHoles(doc, callback);
+ }).catch(function(error) {
+ console.log(error);
+ serializer.fillFontHoles(doc, callback);
+ });
+ }
+ }
+
+ /**
+ * Take all of the srcHoles and create data urls for the resources, placing
+ * them in |this.html|. Calls the callback when complete.
+ *
+ * @param {Function} callback The callback function.
+ */
+ fillSrcHoles(callback) {
+ if (Object.keys(this.srcHoles).length == 0) {
+ callback(this);
+ } else {
+ var index = Object.keys(this.srcHoles)[0];
+ var src = this.srcHoles[index];
+ delete this.srcHoles[index];
+ var serializer = this;
+ fetch(src).then(function(response) {
+ return response.blob();
+ }).then(function(blob) {
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ serializer.html[index] = e.target.result;
+ serializer.fillSrcHoles(callback);
+ }
+ reader.readAsDataURL(blob);
+ }).catch(function(error) {
+ console.log(error);
+ serializer.fillSrcHoles(callback);
+ });
+ }
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698