Skip to content

Instantly share code, notes, and snippets.

@kalda341
Created March 1, 2020 04:08
Show Gist options
  • Select an option

  • Save kalda341/e671c35d4a9b49fa9accf2f6ab0f75fd to your computer and use it in GitHub Desktop.

Select an option

Save kalda341/e671c35d4a9b49fa9accf2f6ab0f75fd to your computer and use it in GitHub Desktop.
(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