Created
March 1, 2020 04:08
-
-
Save kalda341/e671c35d4a9b49fa9accf2f6ab0f75fd to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (async function() { | |
| const chrome = window.chrome; | |
| async function readBlob(blob, readerFun) { | |
| return new Promise((fulfill, reject) => { | |
| const reader = new FileReader(); | |
| reader.onerror = reject; | |
| reader.onload = (e) => fulfill(reader.result); | |
| readerFun(reader, blob); | |
| }); | |
| } | |
| async function readBlobAsText(blob) { | |
| return readBlob(blob, function(reader, blob) { | |
| reader.readAsText(blob); | |
| }); | |
| } | |
| async function blobToDataURL(blob) { | |
| return readBlob(blob, function(reader, blob) { | |
| reader.readAsDataURL(blob); | |
| }); | |
| } | |
| async function loadBinaryURL(url) { | |
| const response = await fetch(url); | |
| if (response.status !== 200) { | |
| throw new Error(`Dammit! Could't load the URL: ${url}`); | |
| } | |
| const blob = await response.blob(); | |
| return blob; | |
| } | |
| async function replaceURLWithData(element, URLAttribute = 'href', transformData) { | |
| // We're passing this URL to the transform function, so make sure that it's absolute | |
| const url = new URL(element.getAttribute(URLAttribute), window.location).href; | |
| const newUrl = await loadBinaryURL(url).then(blob => { | |
| // We need to provide the URL so that we can load relative URLS properly in the | |
| // transform | |
| return transformData ? transformData(blob, url) : blob; | |
| }).then(blob => { | |
| return blobToDataURL(blob); | |
| }).catch(error => { | |
| console.error('Failed to replace URL with data URL:'); | |
| console.error(error); | |
| // Fake data URL | |
| return 'data:,'; | |
| }); | |
| element.setAttribute(URLAttribute, newUrl); | |
| } | |
| function replaceRelativeURLWithAbsolute(element, URLAttribute = 'href') { | |
| const value = element.getAttribute(URLAttribute); | |
| // TODO: We may want to handle anchor links differently one day | |
| const url = new URL(value, window.location); | |
| element.setAttribute(URLAttribute, url.href); | |
| } | |
| async function transformInlineStyle(element) { | |
| const style = element.getAttribute('style'); | |
| if (style) { | |
| element.setAttribute('style', await transformCSSText(style, window.location)); | |
| } | |
| } | |
| async function transformCSSText(cssText, cssUrl) { | |
| const urlRegex = /url\(\s*(?:'|")?([^)'"]+)(?:'|")?\s*\)/g; | |
| // Load all the URLs in parallel | |
| const urlMatches = Array.from(cssText.matchAll(urlRegex)); | |
| const base64Urls = await Promise.all(urlMatches.map(async function(match) { | |
| // Ensure that relative URLS are correct | |
| const url = new URL(match[1], cssUrl).href; | |
| try { | |
| const blob = await loadBinaryURL(url); | |
| return await blobToDataURL(blob); | |
| } catch (error) { | |
| console.error('Error loading URL in CSS:'); | |
| console.error(error); | |
| // Not really valid, but the best that we can do without knowing the | |
| // context of the URL | |
| return 'data:,'; | |
| } | |
| })); | |
| // And then replace sequentially | |
| return cssText.replace(urlRegex, function() { | |
| const base64URL = base64Urls.shift(); | |
| return `url("${base64URL}")`; | |
| }); | |
| } | |
| async function transformCSSBlob(cssBlob, cssUrl) { | |
| const cssText = await readBlobAsText(cssBlob); | |
| const transformedText = await transformCSSText(cssText, cssUrl); | |
| return new Blob([transformedText], {type: cssBlob.type}); | |
| } | |
| async function getTransformedElement(inputNode) { | |
| // Filter out tags we're not interested in | |
| if (![1, 3].includes(inputNode.nodeType)) { | |
| return null; | |
| } else if (inputNode.nodeType === 1) { | |
| if (['SCRIPT', 'IFRAME', 'FRAME', 'FRAMESET'].includes(inputNode.tagName)) { | |
| return null; | |
| } else if (inputNode.tagName === 'LINK' && inputNode.getAttribute('rel') !== 'stylesheet') { | |
| return null; | |
| } | |
| } | |
| // If we made it this far then we want to return the cloned inputNode, after potentially | |
| // performing some trasformations | |
| let outputNode = inputNode.cloneNode(false); | |
| if (inputNode.nodeType === 1) { | |
| // TODO: Transform style tags | |
| if (inputNode.matches('link[rel=stylesheet]')) { | |
| await replaceURLWithData(outputNode, 'href', transformCSSBlob); | |
| } else if (inputNode.matches('img[src]')) { | |
| await replaceURLWithData(outputNode, 'src'); | |
| } else if (inputNode.matches('source[srcset]')) { | |
| // We'll gain a decent performance boost by not including all the different | |
| // versions of all the different images. | |
| return null; | |
| // // This may not be a good idea - just due to the sheer number of | |
| // // potential images | |
| // await replaceURLWithData(outputNode, 'srcset'); | |
| } else if (inputNode.matches('a[href]')) { | |
| // Relative URLs won't work anymore! | |
| replaceRelativeURLWithAbsolute(outputNode); | |
| } | |
| // Ensure that we perform the same transformation to inline CSS as we do for | |
| // stylesheets | |
| if (inputNode.getAttribute('style')) { | |
| await transformInlineStyle(outputNode); | |
| } | |
| // Make sure we add the child nodes seeing we haven't transformed them! | |
| if (inputNode.matches('noscript')) { | |
| // We shouldn't need noscript as we will have scripting to produce the | |
| // initial DOM state that we're snapshotting. | |
| return null; | |
| // // The contents of noscript won't be in the DOM | |
| // const throwawayElement = document.createElement('div'); | |
| // throwawayElement.innerHTML = inputNode.innerHTML; | |
| // const newThrowawayElement = await getTransformedElement(throwawayElement); | |
| // outputNode.innerHTML = newThrowawayElement.innerHTML; | |
| } else { | |
| const children = await Promise.all(Array.from(inputNode.childNodes).map(getTransformedElement)); | |
| children.forEach(child => { | |
| child && outputNode.appendChild(child); | |
| }); | |
| } | |
| } | |
| return outputNode; | |
| } | |
| function sendResponse(result, status = 'success') { | |
| chrome.runtime.sendMessage({ | |
| result, | |
| status, | |
| }); | |
| } | |
| try { | |
| const htmlTag = document.getElementsByTagName('html')[0]; | |
| const result = await getTransformedElement(htmlTag); | |
| sendResponse(result.outerHTML); | |
| } catch (error) { | |
| console.error(error); | |
| sendResponse(error, 'error'); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment