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

Side by Side 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 unified diff | Download patch
OLDNEW
(Empty)
1 /**
2 * HTML Serializer that takes a document and synchronously stores it as an array
3 * of strings, then asynchronously retrieves data URLs for same-origin images.
4 * It stores enough state to later be converted to an html text file.
5 */
6 var HTMLSerializer = class {
7 constructor() {
8
9 /**
10 * @private {Set<string>} Contains the tag names that should be
11 * ignored while serializing a document.
12 * @const
13 */
14 this.FILTERED_TAGS = new Set(['SCRIPT', 'NOSCRIPT', 'STYLE', 'LINK']);
15
16 /**
17 * @private {Set<string>} Contains the tag names for elements
18 * that have no closing tags. List of tags taken from:
19 * https://html.spec.whatwg.org/multipage/syntax.html#void-elements.
20 * @const
21 */
22 this.NO_CLOSING_TAGS = new Set([
23 'AREA',
24 'BASE',
25 'BR',
26 'COL',
27 'EMBED',
28 'HR',
29 'IMG',
30 'INPUT',
31 'KEYGEN',
32 'LINK',
33 'META',
34 'PARAM',
35 'SOURCE',
36 'TRACK',
37 'WBR'
38 ]);
39
40 /**
41 * @private {Array<string>} A list of the pseudo elements that will be
42 * processed.
43 * @const
44 */
45 this.PSEUDO_ELEMENTS = [':before', ':after'];
46
47 /**
48 * @private {Object<string, string>} The keys are all characters that need
49 * to be properly escaped when in a text node. The value is the
50 * properly escaped string.
51 * @const
52 */
53 this.CHARACTER_ESCAPING_MAP = {
54 '&' : '&amp;',
55 '<' : '&lt;',
56 '>' : '&gt;',
57 '"' : '&quot;',
58 "'" : '&#39;'
59 };
60
61 /**
62 * @public {Object<string, number>} An enum representing different types of
63 * text.
64 * @const
65 */
66 this.INPUT_TEXT_TYPE = {
67 HTML : 0,
68 CSS : 1
69 };
70
71 /**
72 * @public {Array<string>} This array represents the serialized html that
73 * makes up a node or document.
74 */
75 this.html = [];
76
77 /**
78 * @public {Object<number, string>} The keys represent an index in
79 * |this.html|. The value is a url at which the resource that belongs at
80 * that index can be retrieved. The resource will eventually be
81 * converted to a data url.
82 */
83 this.srcHoles = {};
84
85 /**
86 * @public {Object<number, string>} The keys represent an index in
87 * |this.html|. The value is a string that uniquely identifies an
88 * iframe, the serialized contents of which should be placed at that
89 * index of |this.html|.
90 */
91 this.frameHoles = {};
92
93 /**
94 * @private {Array<string>} Each element of |this.crossOriginStyleSheets|
95 * contains a url to a stylesheet that does not have the same origin
96 * as the webpage being serialized.
97 */
98 this.crossOriginStyleSheets = [];
99
100 /**
101 * @private {Array<string>} Each element of |this.fontCSS| will contain
102 * a CSS declaration of a different externally loaded font.
103 */
104 this.fontCSS = [];
105
106 /**
107 * @private {number} The index in |this.html| where the style element
108 * containing the fonts will go.
109 */
110 this.fontPlaceHolderIndex;
111
112 /**
113 * @private {number} The index in |this.html| where the style element
114 * containing the pseudo elements will go.
115 */
116 this.pseudoElementPlaceHolderIndex;
117
118 /**
119 * @private {number} The id of a style element that can be used to test
120 * minimized pseudo element declarations in popup.js.
121 */
122 this.pseudoElementTestingStyleId;
123
124 /**
125 * @private {number} The index in |this.html| where a style element will be
126 * placed to test minimized pseudo element declarations in popup.js.
127 */
128 this.pseudoElementTestingStyleIndex;
129
130 /**
131 * @private {Array<string>} Each element of this array is a string
132 * representing CSS that defines a single pseudo element.
133 */
134 this.pseudoElementCSS = [];
135
136 /**
137 * @private {Object<string, Object<string, string>>} The keys represent a
138 * pseudo element selector. The value is a map of that pseudo element's
139 * style property names to property values.
140 */
141 this.pseudoElementSelectorToCSSMap = {};
142
143 /**
144 * @private {Function} A funtion that generates a unique string each time it
145 * is called, which can be used as an element id.
146 */
147 this.generateId = this.generateIdGenerator();
148
149 /**
150 * @private {number} The window height of the Document being serialized.
151 */
152 this.windowHeight;
153
154 /**
155 * @private {number} The window width of the Document being serialized.
156 */
157 this.windowWidth;
158
159 /**
160 * @private {Object<string, number>} The keys represent the id of an
161 * Element. The value is the index in |this.html| where the
162 * value of the style attribute for that Element is specified.
163 */
164 this.idToStyleIndex = {};
165
166 /**
167 * @private {Object<string, Object<string, string>>} The keys represent the
168 * id of an Element. The value is a map of that Element's style
169 * attribute property names to property values.
170 */
171 this.idToStyleMap = {};
172
173 /**
174 * @private {number} The index in |this.html| at which the html element's
175 * style attribute is specified.
176 */
177 this.rootStyleIndex;
178
179 /**
180 * @private {string} The assigned id of the html element.
181 */
182 this.rootId;
183 }
184
185 /**
186 * Takes an html document, and populates this objects fields such that it can
187 * eventually be converted into an html file.
188 *
189 * @param {Document} doc The Document to serialize.
190 */
191 processDocument(doc) {
192 this.windowHeight = doc.defaultView.innerHeight;
193 this.windowWidth = doc.defaultView.innerWidth;
194
195 if (doc.doctype) {
196 this.html.push('<!DOCTYPE html>\n');
197 }
198
199 if (this.iframeFullyQualifiedName(doc.defaultView) == '0') {
200 this.html.push(
201 `<!-- Original window height: ${this.windowHeight}. -->\n`);
202 this.html.push(`<!-- Original window width: ${this.windowWidth}. -->\n`);
203 }
204
205 this.loadFonts(doc);
206 this.pseudoElementPlaceHolderIndex = this.html.length;
207 this.html.push(''); // Entry where pseudo element style tag will go.
208 this.pseudoElementTestingStyleIndex = this.html.length;
209 this.html.push(''); // Entry where minimized pseudo elements can be tested.
210
211 var nodes = doc.childNodes;
212 for (var i = 0, node; node = nodes[i]; i++) {
213 if (node.nodeType != Node.DOCUMENT_TYPE_NODE) {
214 this.processTree(node);
215 }
216 }
217 var pseudoElements = `<style>${this.pseudoElementCSS.join('')}</style>`;
218 this.html[this.pseudoElementPlaceHolderIndex] = pseudoElements;
219
220 this.pseudoElementTestingStyleId = this.generateId(doc);
221 var style = `<style id="${this.pseudoElementTestingStyleId}"></style>`;
222 var nestingDepth = this.windowDepth(doc.defaultView);
223 var escapedQuote = this.escapedCharacter('"', nestingDepth);
224 style = style.replace(/"/g, escapedQuote);
225 this.html[this.pseudoElementTestingStyleIndex] = style;
226 }
227
228 /**
229 * Takes an html node, and populates this object's fields such that it can
230 * eventually be converted into an html text file.
231 *
232 * @param {Node} node The Node to serialize.
233 * @private
234 */
235 processTree(node) {
236 var tagName = node.tagName;
237 if (!tagName && node.nodeType != Node.TEXT_NODE) {
238 // Ignore nodes that don't have tags and are not text.
239 } else if (tagName && this.FILTERED_TAGS.has(tagName)) {
240 // Filter out nodes that are in filteredTags.
241 } else if (node.nodeType == Node.TEXT_NODE) {
242 this.processText(node);
243 } else {
244 this.html.push(`<${tagName.toLowerCase()} `);
245 var id;
246 if (node.attributes.id) {
247 id = node.attributes.id.value;
248 } else {
249 id = this.generateId(node.ownerDocument);
250 }
251 this.processAttributes(node, id);
252 this.processPseudoElements(node, id);
253 this.html.push('>');
254
255 if (tagName == 'HEAD') {
256 this.fontPlaceHolderIndex = this.html.length;
257 this.html.push('');
258 this.pseudoElementPlaceHolderIndex = this.html.length;
259 this.html.push('');
260 this.pseudoElementTestingStyleIndex = this.html.length;
261 this.html.push('');
262 }
263
264 var children = node.childNodes;
265 if (children) {
266 for (var i = 0, child; child = children[i]; i++) {
267 this.processTree(child);
268 }
269 }
270
271 if (!this.NO_CLOSING_TAGS.has(tagName)) {
272 this.html.push(`</${tagName.toLowerCase()}>`);
273 }
274 }
275 }
276
277 /**
278 * Takes an HTML element, and if it has pseudo elements listed in
279 * |this.PSEUDO_ELEMENTS| they will be added to |this.pseudoElementCSS| and
280 * |this.pseudoElementSelectorToCSSMap|.
281 *
282 * @param {Element} element The Element whose pseudo elements will be
283 * processed.
284 * @param {string} id The id of the Element whose pseudo elements will be
285 * processed.
286 * @private
287 */
288 processPseudoElements(element, id) {
289 var win = element.ownerDocument.defaultView;
290 for (var i = 0, pseudo; pseudo = this.PSEUDO_ELEMENTS[i]; i++) {
291 var style = win.getComputedStyle(element, pseudo);
292 if (style.content) {
293 var nestingDepth = this.windowDepth(win);
294 var escapedQuote = this.escapedCharacter('"', nestingDepth);
295 var styleText = style.cssText.replace(/"/g, escapedQuote);
296 styleText = this.escapedUnicodeString(
297 styleText,
298 this.INPUT_TEXT_TYPE.CSS);
299 this.pseudoElementCSS.push(
300 '#' + id + ':' + pseudo + '{' + styleText + '} ');
301
302 var styleMap = {};
303 for (var i = 0; i < style.length; i++) {
304 var propertyName = style.item(i);
305 var propertyValue = style.getPropertyValue(propertyName);
306 propertyValue = this.escapedUnicodeString(
307 propertyValue,
308 this.INPUT_TEXT_TYPE.CSS);
309 styleMap[propertyName] = propertyValue;
310 }
311 this.pseudoElementSelectorToCSSMap['#' + id + ':' + pseudo] = styleMap;
312 }
313 }
314 }
315
316 /**
317 * Takes an html node of type Node.TEXT_NODE, and add its text content with
318 * all characters properly escaped to |this.html|.
319 * @param {Node} node The text node.
320 */
321 // TODO(sfine): Take care of attribute value normalization:
322 // https://developers.whatwg.org/the-iframe-element.html#the-iframe-element
323 processText(node) {
324 var win = node.ownerDocument.defaultView;
325 var nestingDepth = this.windowDepth(win);
326 var text = node.textContent;
327 text = this.escapedCharacterString(text, nestingDepth+1);
328 text = this.escapedUnicodeString(text, this.INPUT_TEXT_TYPE.HTML);
329 this.html.push(text);
330 }
331
332 /**
333 * Takes an html element, and populates this object's fields with the
334 * appropriate attribute names and values.
335 *
336 * @param {Element} element The Element to serialize.
337 * @param {string} id The id of the Element being serialized.
338 * @private
339 */
340 processAttributes(element, id) {
341 var win = element.ownerDocument.defaultView;
342 var style = win.getComputedStyle(element, null);
343 var styleMap = {};
344 for (var i = 0; i < style.length; i++) {
345 var propertyName = style.item(i);
346 styleMap[propertyName] = style.getPropertyValue(propertyName);
347 }
348 this.idToStyleMap[id] = styleMap;
349 this.idToStyleIndex[id] = this.html.length;
350 if (element.tagName == 'HTML') {
351 this.rootStyleIndex = this.html.length;
352 this.rootId = id;
353 }
354 this.processSimpleAttribute(win, 'style', style.cssText);
355 this.processSimpleAttribute(win, 'id', id);
356
357 var attributes = element.attributes;
358 if (attributes) {
359 for (var i = 0, attribute; attribute = attributes[i]; i++) {
360 switch (attribute.name.toLowerCase()) {
361 case 'src':
362 this.processSrcAttribute(element);
363 break;
364 case 'style':
365 case 'id':
366 break;
367 default:
368 var name = attribute.name;
369 var value = attribute.value;
370 this.processSimpleAttribute(win, name, value);
371 }
372 }
373 // TODO(sfine): Ensure this is working by making sure that an iframe
374 // will always have attributes.
375 if (element.tagName == 'IFRAME' && element.attributes.src) {
376 var valueIndex = this.processHoleAttribute(win, 'srcdoc');
377 var iframeName = this.iframeFullyQualifiedName(element.contentWindow);
378 this.frameHoles[valueIndex] = iframeName;
379 }
380 }
381 }
382
383 /**
384 * Process the src attribute of a given element.
385 *
386 * @param {Element} element The Element being processed, which has the src
387 * attribute.
388 * @private
389 */
390 processSrcAttribute(element) {
391 var win = element.ownerDocument.defaultView;
392 var url = this.fullyQualifiedURL(element);
393 var sameOrigin = window.location.host == url.host;
394 switch (element.tagName) {
395 case 'IFRAME':
396 break; // Do nothing.
397 case 'SOURCE':
398 var parent = element.parent;
399 if (parent && parent.tagName == 'PICTURE' && sameOrigin) {
400 this.processSrcHole(element);
401 } else {
402 this.processSimpleAttribute(win, 'src', url.href);
403 }
404 break;
405 case 'INPUT':
406 var type = element.attributes.type;
407 if (type && type.value.toLowerCase() == 'image') {
408 this.processSrcHole(element);
409 }
410 break;
411 case 'IMG':
412 if (sameOrigin) {
413 this.processSrcHole(element);
414 } else {
415 this.processSimpleAttribute(win, 'src', url.href);
416 }
417 break;
418 default:
419 this.processSimpleAttribute(win, 'src', url.href);
420 }
421 }
422
423 /**
424 * Get a URL object for the value of the |element|'s src attribute.
425 *
426 * @param {Element} element The element for which to retrieve the URL.
427 * @return {URL} The URL object.
428 */
429 fullyQualifiedURL(element) {
430 var url = element.attributes.src.value;
431 var a = document.createElement('a');
432 a.href = url;
433 url = a.href; // Retrieve fully qualified URL.
434 return new URL(url);
435 }
436
437 /**
438 * Add an entry to |this.srcHoles| so it can be processed asynchronously.
439 *
440 * @param {Element} element The element being processed, which has the src
441 * attribute.
442 * @private
443 */
444 processSrcHole(element) {
445 var win = element.ownerDocument.defaultView;
446 var valueIndex = this.processHoleAttribute(win, 'src');
447 this.srcHoles[valueIndex] = this.fullyQualifiedURL(element).href;
448 }
449
450 /**
451 * Add an attribute with name |name| to |this.html| with an empty index for
452 * its value that can later be filled in.
453 *
454 * @param {Window} win The window of the Element that is being processed.
455 * @param {string} name The name of the attribute.
456 * @return {number} The index in |this.html| where the value will be placed.
457 */
458 processHoleAttribute(win, name) {
459 var quote = this.escapedCharacter('"', this.windowDepth(win));
460 this.html.push(`${name}=${quote}`);
461 var valueIndex = this.html.length;
462 this.html.push(''); // Entry where value will go.
463 this.html.push(quote + ' '); // Add a space before the next attribute.
464 return valueIndex;
465 }
466
467 /**
468 * Add a name and value pair to the list of attributes in |this.html|.
469 *
470 * @param {Window} win The window of the Element that is being processed.
471 * @param {string} name The name of the attribute.
472 * @param {string} value The value of the attribute.
473 */
474 processSimpleAttribute(win, name, value) {
475 var nestingDepth = this.windowDepth(win);
476 var quote = this.escapedCharacter('"', nestingDepth);
477 value = this.escapedCharacterString(value, nestingDepth+1);
478 this.html.push(`${name}=${quote}${value}${quote} `);
479 }
480
481 /**
482 * Load all external fonts, and add an entry to |this.html| at index
483 * |this.fontPlaceHolderIndex|.
484 *
485 * @param {Document} doc The Document being serialized.
486 */
487 loadFonts(doc) {
488 this.fontPlaceHolderIndex = this.html.length;
489 this.html.push(''); // Entry where the font style tag will go.
490 for (var i = 0, styleSheet; styleSheet = doc.styleSheets[i]; i++) {
491 if (styleSheet.cssRules) {
492 for (var j = 0, rule; rule = styleSheet.cssRules[j]; j++) {
493 this.processCSSFonts(doc.defaultView, styleSheet.href, rule.cssText);
494 }
495 } else {
496 this.crossOriginStyleSheets.push(styleSheet.href);
497 }
498 }
499 }
500
501 /**
502 * Takes a string representing CSS and parses it to find any fonts that are
503 * declared. If any fonts are declared, it processes them so that they
504 * can be used in the serialized document and adds them to |this.fontCSS|.
505 *
506 * @param {Window} win The Window of the Document being serialized.
507 * @param {string} href The url at which the CSS stylesheet is located.
508 * @param {string} css The CSS text.
509 */
510 processCSSFonts(win, href, css) {
511 var serializer = this;
512 var fonts = css.match(/@font-face *?{[\s\S]*?}/g);
513 if (fonts) {
514 var nestingDepth = this.windowDepth(win);
515 var escapedQuote = this.escapedCharacter('"', nestingDepth);
516 for (var i = 0; i < fonts.length; i++) {
517 // Convert url specified in font to fully qualified url.
518 var font = fonts[i].replace(/url\("(.*?)"\)/g, function(match, url) {
519 // If href is null the url must be a fully qualified url.
520 url = href ? serializer.fullyQualifiedFontURL(href, url) : url;
521 return 'url("' + url + '")';
522 }).
523 replace(/"/g, escapedQuote);
524 this.fontCSS.push(font);
525 }
526 }
527 }
528
529 /**
530 * Computes the fully qualified url at which a font can be loaded.
531 * TODO(sfine): Make this method sufficiently robust, so that it can replace
532 * the current implementation of fullyQualifiedURL.
533 *
534 * @param {string} href The url at which the CSS stylesheet containing the
535 * font is located.
536 * @param {string} url The url listed in the font declaration.
537 */
538 fullyQualifiedFontURL(href, url) {
539 if (href.charAt(href.length-1) == '/') {
540 href = href.slice(0, href.length-1);
541 }
542 var hrefURL = new URL(href);
543 if (url.includes('://')) {
544 return url;
545 } else if (url.startsWith('//')) {
546 return hrefURL.protocol + url;
547 } else if (url.startsWith('/')) {
548 return hrefURL.origin + url;
549 } else {
550 href = href.slice(0, href.lastIndexOf('/'));
551 return href + '/' + url;
552 }
553 }
554
555 /**
556 * Computes the index of the window in its parent's array of frames.
557 *
558 * @param {Window} childWindow The window to use in the calculation.
559 * @return {number} the frames index.
560 */
561 iframeIndex(childWindow) {
562 if (childWindow.parent != childWindow) {
563 for (var i = 0; i < childWindow.parent.frames.length; i++) {
564 if (childWindow.parent.frames[i] == childWindow) {
565 return i;
566 }
567 }
568 } else {
569 return -1;
570 }
571 }
572
573 /**
574 * Computes the full path of the frame in the root document. Nested layers
575 * are seperated by '.'.
576 *
577 * @param {Window} win The window to use in the calculation.
578 * @return {string} The full path.
579 */
580 iframeFullyQualifiedName(win) {
581 if (this.iframeIndex(win) < 0) {
582 return '0';
583 } else {
584 var fullyQualifiedName = this.iframeFullyQualifiedName(win.parent);
585 var index = this.iframeIndex(win);
586 return fullyQualifiedName + '.' + index;
587 }
588 }
589
590 /**
591 * Calculate the correct encoding of a character that should be used given the
592 * nesting depth of the window in the frame tree.
593 *
594 * @param {string} char The character that should be escaped.
595 * @param {number} depth The nesting depth of the appropriate window in the
596 * frame tree.
597 * @return {string} The correctly escaped string.
598 */
599 escapedCharacter(char, depth) {
600 if (depth == 0) {
601 return char;
602 } else {
603 var arr = 'amp;'.repeat(depth-1);
604 return '&' + arr + this.CHARACTER_ESCAPING_MAP[char].slice(1);
605 }
606 }
607
608 /**
609 * Returns the string that is passed as an argument with all characters in
610 * |this.ESCAPED_CHARACTER_MAP| replaced with the correct character encoding
611 * that should be used, given the nesting depth of the window in the frame
612 * tree.
613 *
614 * @param {string} str The string that should have its characters escaped.
615 * @param {number} depth The nesting depth of the appropriate window in the
616 * frame tree.
617 * @return {string} The correctly escaped string.
618 */
619 escapedCharacterString(str, depth) {
620 // Some escaping introduces '&' characters so we escape '&' first to prevent
621 // escaping the '&' added by other escape substitutions.
622 str = str.replace(/&/g, this.escapedCharacter('&', depth));
623 for (var char in this.CHARACTER_ESCAPING_MAP) {
624 if (char != '&') {
625 var regExp = new RegExp(char, 'g');
626 str = str.replace(regExp, this.escapedCharacter(char, depth));
627 }
628 }
629 return str;
630 }
631
632 /**
633 * Returns the string that is passed as an argument with all non ascii unicode
634 * characters escaped.
635 *
636 * @param {string} str The string that should have its characters escaped.
637 * @param {number} textType A possible value of |this.INPUT_TEXT_TYPE| which
638 * represents the type of text being escaped.
639 * @return {string} The correctly escaped string.
640 */
641 escapedUnicodeString(str, textType) {
642 var serializer = this;
643 return str.replace(/[\s\S]/g, function(char) {
644 var unicode = char.codePointAt();
645 if (unicode < 128) {
646 return char;
647 } else if (textType == serializer.INPUT_TEXT_TYPE.HTML) {
648 return '&#' + unicode + ';';
649 } else {
650 return '\\' + unicode.toString(16);
651 }
652 });
653 }
654
655 /**
656 * Calculate the nesting depth of a window in the frame tree.
657 *
658 * @param {Window} win The window to use in the calculation.
659 * @return {number} The nesting depth of the window in the frame trees.
660 */
661 windowDepth(win) {
662 return this.iframeFullyQualifiedName(win).split('.').length - 1;
663 }
664
665 /**
666 * Create a function that will generate strings which can be used as
667 * ids.
668 *
669 * @return {Function<Document>} A funtion that generates a valid id each time
670 * it is called.
671 */
672 generateIdGenerator() {
673 var counter = 0;
674 function idGenerator(doc) {
675 var id;
676 do {
677 id = 'snap-it' + counter++;
678 } while (doc.getElementById(id));
679 return id;
680 }
681 return idGenerator;
682 }
683
684 /**
685 * Asynchronously fill in any holes in |this.html|.
686 *
687 * @param {Document} doc The Document being serialized.
688 * @param {Function} callback The callback function, which will be called when
689 * all asynchronous processing is finished.
690 */
691 fillHolesAsync(doc, callback) {
692 var serializer = this;
693 this.fillFontHoles(doc, function() {
694 serializer.fillSrcHoles(callback);
695 });
696 }
697
698 /**
699 * Takes all of the cross origin stylesheets, processes their font
700 * declarations, and adds them to |this.html|. Calls the callback when
701 * complete.
702 *
703 * @param {Document} doc The Document being serialized.
704 * @param {Function} callback The callback function.
705 */
706 fillFontHoles(doc, callback) {
707 if (this.crossOriginStyleSheets.length == 0) {
708 var fonts = `<style>${this.fontCSS.join('')}</style>`;
709 this.html[this.fontPlaceHolderIndex] = fonts;
710 callback();
711 } else {
712 var styleSheetSrc = this.crossOriginStyleSheets.shift();
713 var serializer = this;
714 fetch(styleSheetSrc).then(function(response) {
715 return response.text();
716 }).then(function(css) {
717 serializer.processCSSFonts(doc.defaultView, styleSheetSrc, css);
718 serializer.fillFontHoles(doc, callback);
719 }).catch(function(error) {
720 console.log(error);
721 serializer.fillFontHoles(doc, callback);
722 });
723 }
724 }
725
726 /**
727 * Take all of the srcHoles and create data urls for the resources, placing
728 * them in |this.html|. Calls the callback when complete.
729 *
730 * @param {Function} callback The callback function.
731 */
732 fillSrcHoles(callback) {
733 if (Object.keys(this.srcHoles).length == 0) {
734 callback(this);
735 } else {
736 var index = Object.keys(this.srcHoles)[0];
737 var src = this.srcHoles[index];
738 delete this.srcHoles[index];
739 var serializer = this;
740 fetch(src).then(function(response) {
741 return response.blob();
742 }).then(function(blob) {
743 var reader = new FileReader();
744 reader.onload = function(e) {
745 serializer.html[index] = e.target.result;
746 serializer.fillSrcHoles(callback);
747 }
748 reader.readAsDataURL(blob);
749 }).catch(function(error) {
750 console.log(error);
751 serializer.fillSrcHoles(callback);
752 });
753 }
754 }
755 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698