Lưu trang web giữ nguyên layout

Lưu trang web thành html giữ nguyên layout

// ==UserScript==
// @name          Lưu trang web giữ nguyên layout
// @name:en       Save to html with original layout
// @description   Lưu trang web thành html giữ nguyên layout
// @description:en  Save webpage with original layout
// @namespace     Violentmonkey Scripts
// @match         *://*.*/*
// @grant         GM_xmlhttpRequest
// @version       0.0.1
// @author        teeen
// @license       MIT
// ==/UserScript==

const sleep=ms=>new Promise(r=>setTimeout(r,ms));

function toDataURL(data) {
  return new Promise((rs,rj)=>{
    const fs=new FileReader();
    fs.onload=()=> rs(fs.result);
    fs.onerror=()=>rj(fs.error);
    fs.readAsDataURL(new Blob([data]));
  });
}

async function zip(data) {  // return Blob
  let blob=new Blob([data]);
  const cs = new CompressionStream("gzip");
  const compressedStream = blob.stream().pipeThrough(cs);
  return await new Response(compressedStream).blob();
}

function xhr(url, detail) {
  const nurl=new URL(url);
  const option={'url':url, origin:nurl.origin}

  if (typeof detail =='string' && /^(?:blob|text|json|arraybuffer|document)$/.test(detail))  option['responseType']=detail;
  if (typeof detail =='object') option=detail;

  return new Promise(rs=>{
    option['onloadend']=res=> (res.status==200) ? rs(res.response) : rs(false);
    const c = GM_xmlhttpRequest(option);
  })
}


(async function main () {

  document.addEventListener('keydown',async (e)=>{
    if (e.ctrlKey && (e.key=='S'||e.key=='s')) {
      e.preventDefault();
      
      //scrol down and up to load full page
      scrollTo({left:document.body.clientWidth,top:document.body.clientHeight,behavior:'smooth'});
      await sleep(500);
      scrollTo({left:0,top:0,behavior:'smooth'});
      await sleep(1500);

      await save();}
    return false;
  });
})();

async function save() {
  document.querySelectorAll('[href]').forEach(el=> {
    if (typeof el.href !=='string') return;
    if (el.tagName=='USE') return; //USE tag in svg
    if (el.href.startsWith('http')) return;
    if (el.href.startsWith('/')) {el.href=location.origin+el.href; return;}
    el.href=location.origin+location.pathname.slice(0,location.pathname.lastIndexOf('/'))+'/'+el.href;
  })

  document.querySelectorAll('script').forEach(el=>el.remove());

  document.querySelectorAll('[src]').forEach(el=> {
    if (el.src.startsWith('http')) return;
    if (el.src.startsWith('/')) {el.src=location.origin+el.src; return;}
    el.src=location.origin+location.pathname.slice(0,location.pathname.lastIndexOf('/'))+'/'+el.src;
    return;
  })

  await Promise.all([...document.querySelectorAll('link[rel="stylesheet"]')].map(async (el)=>{
    const cssContent=await xhr(el.href,'text');
    console.log(cssContent);
    el.href=(await toDataURL(cssContent)).replace('data:application/octet-stream;base64,','data:text/css;base64,');//correct mime
  }));

  let cssText='';
  for (ss of document.styleSheets) {
    try {
      for(rules of ss.cssRules) cssText += rules.cssText +"\n"; //sometimes cors happens
    } catch (e) {
      console.error(e);
    }
  }

  document.querySelectorAll('style').forEach(el=>el.remove());
  document.querySelectorAll('link[rel="stylesheet"]').forEach(el=>el.remove());

  let newStyle= document.createElement('style');
  newStyle.textContent= cssText;
  document.querySelector('head').appendChild(newStyle);

  const images=document.querySelectorAll('img');
  for (let i=0; i<images.length; i++) {
    const a=await xhr(images[i].src,'blob');
    images[i].src=(await toDataURL(a)).replace('data:application/octet-stream;base64,','data:image;base64,');//correct mime;
  }

  let s = new XMLSerializer().serializeToString(document);
  s=new Blob([s]);

  let title=document.querySelector('title').textContent.trim();
  title=title??(location.pathname.split('/').at(-1)?location.pathname.split('/').at(-1):location.pathname.split('/').at(-2));
  const a=document.createElement('a');
  a.href=URL.createObjectURL(s);
  a.download=title+'.html'
  a.click();
}