Created
March 14, 2025 15:20
-
-
Save xavetar/79a379d4955a1c6d5609d6e1f1dae94e to your computer and use it in GitHub Desktop.
Replicate Web-Page
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() { | |
| 'use strict'; | |
| // Функция для инициализации слушателя клавиш (зависим от раскладки) | |
| function initializeKeyListener() { | |
| console.log('Initializing key listener...'); | |
| document.addEventListener('keydown', async function(event) { | |
| console.log(`Key pressed: ${event.key}, Ctrl: ${event.ctrlKey}, Meta: ${event.metaKey}, Alt: ${event.altKey}`); | |
| // Фиксированное сочетание: Ctrl + Command (Meta) + S | |
| const isSaveShortcut = event.ctrlKey && event.metaKey && event.key.toLowerCase() === 's'; | |
| // Альтернативное сочетание (закомментировано): Command + Option + S | |
| // const isSaveShortcut = event.metaKey && event.altKey && event.key.toLowerCase() === 's'; | |
| // Если хотите оба варианта, используйте: | |
| // const isSaveShortcut = (event.ctrlKey && event.metaKey && event.key.toLowerCase() === 's') || | |
| // (event.metaKey && event.altKey && event.key.toLowerCase() === 's'); | |
| if (isSaveShortcut) { | |
| event.preventDefault(); | |
| console.log('Save shortcut detected (Ctrl+Command+S). Starting download...'); | |
| try { | |
| await downloadCompleteHTML(); | |
| console.log('Page saved successfully'); | |
| } catch (err) { | |
| console.error('Failed to save page:', err); | |
| alert(`Error saving page: ${err.message}`); | |
| } | |
| } else { | |
| console.log('Not a save shortcut'); | |
| } | |
| }, { passive: false, capture: true }); | |
| } | |
| // Основная функция сохранения страницы | |
| async function downloadCompleteHTML() { | |
| console.log('Starting downloadCompleteHTML...'); | |
| async function fetchResource(url, isBinary = false) { | |
| try { | |
| const controller = new AbortController(); | |
| const timeout = setTimeout(() => controller.abort(), 10000); | |
| const response = await fetch(url, { | |
| signal: controller.signal, | |
| cache: 'no-store' | |
| }); | |
| clearTimeout(timeout); | |
| if (!response.ok) throw new Error(`HTTP ${response.status}`); | |
| console.log(`Fetched resource: ${url}`); | |
| if (isBinary) { | |
| const blob = await response.blob(); | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onloadend = () => resolve(reader.result); | |
| reader.onerror = reject; | |
| reader.readAsDataURL(blob); | |
| }); | |
| } | |
| return await response.text(); | |
| } catch (error) { | |
| console.warn(`Failed to fetch ${url}: ${error.message}`); | |
| return isBinary ? '' : `/* Failed to load: ${url} */`; | |
| } | |
| } | |
| async function inlineCSS(linkElement) { | |
| const href = linkElement.href; | |
| console.log(`Inlining CSS: ${href}`); | |
| const cssContent = await fetchResource(href); | |
| const resolvedCSS = cssContent.replace( | |
| /url\((?!['"]?(?:data|https?|ftp):)['"]?([^'")]+)['"]?\)/g, | |
| (match, relativeUrl) => { | |
| try { | |
| const absoluteUrl = new URL(relativeUrl.trim(), href).href; | |
| return `url("${absoluteUrl}")`; | |
| } catch (err) { | |
| console.warn(`Failed to resolve URL: ${relativeUrl}`); | |
| return match; | |
| } | |
| } | |
| ); | |
| const styleElement = document.createElement('style'); | |
| styleElement.textContent = resolvedCSS; | |
| return styleElement; | |
| } | |
| async function inlineMedia(element) { | |
| const mediaElements = element.querySelectorAll('img, video, audio, source'); | |
| console.log(`Found ${mediaElements.length} media elements to inline`); | |
| for (const media of mediaElements) { | |
| const srcAttr = media.src ? 'src' : 'data-src'; | |
| if (media[srcAttr]?.startsWith('http')) { | |
| const dataUri = await fetchResource(media[srcAttr], true); | |
| if (dataUri) media[srcAttr] = dataUri; | |
| } | |
| } | |
| } | |
| console.log('Cloning document...'); | |
| const parser = new DOMParser(); | |
| const doc = parser.parseFromString(document.documentElement.outerHTML, 'text/html'); | |
| const linkElements = [...doc.querySelectorAll('link[rel="stylesheet"]')]; | |
| console.log(`Processing ${linkElements.length} CSS links...`); | |
| await Promise.all(linkElements.map(async link => { | |
| const styleElement = await inlineCSS(link); | |
| link.replaceWith(styleElement); | |
| })); | |
| await inlineMedia(doc); | |
| const scripts = doc.querySelectorAll('script[src]'); | |
| console.log(`Processing ${scripts.length} scripts...`); | |
| await Promise.all([...scripts].map(async script => { | |
| if (script.src.startsWith('http')) { | |
| const scriptContent = await fetchResource(script.src); | |
| script.textContent = scriptContent; | |
| script.removeAttribute('src'); | |
| } | |
| })); | |
| const finalHTML = `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`; | |
| console.log('HTML prepared for download'); | |
| function downloadHTML(content, fileName) { | |
| const blob = new Blob([content], { type: 'text/html' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = fileName || `${document.title || 'page'}_${new Date().toISOString().split('T')[0]}.html`; | |
| document.body.appendChild(a); | |
| console.log(`Triggering download: ${a.download}`); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } | |
| downloadHTML(finalHTML); | |
| } | |
| // Немедленный запуск | |
| console.log('Script loaded. Running initial save...'); | |
| try { | |
| await downloadCompleteHTML(); | |
| console.log('Initial save completed'); | |
| } catch (err) { | |
| console.error('Initial save failed:', err); | |
| } | |
| // Активируем слушатель клавиш | |
| initializeKeyListener(); | |
| console.log('Key listener activated. Press Ctrl+Command+S to save'); | |
| })(); |
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() { | |
| 'use strict'; | |
| // Функция для инициализации слушателя клавиш | |
| function initializeKeyListener() { | |
| console.log('Initializing key listener...'); | |
| document.addEventListener('keydown', async function(event) { | |
| console.log(`Key pressed: ${event.key}, Ctrl: ${event.ctrlKey}, Shift: ${event.shiftKey}, Alt: ${event.altKey}`); | |
| // Универсальное сочетание: Ctrl + Shift + S (работает на Linux, Windows и macOS) | |
| const isSaveShortcut = event.ctrlKey && event.shiftKey && event.key.toLowerCase() === 's'; | |
| if (isSaveShortcut) { | |
| event.preventDefault(); | |
| console.log('Save shortcut detected (Ctrl+Shift+S). Starting download...'); | |
| try { | |
| await downloadCompleteHTML(); | |
| console.log('Page saved successfully'); | |
| } catch (err) { | |
| console.error('Failed to save page:', err); | |
| alert(`Error saving page: ${err.message}`); | |
| } | |
| } else { | |
| console.log('Not a save shortcut'); | |
| } | |
| }, { passive: false, capture: true }); | |
| } | |
| // Основная функция сохранения страницы | |
| async function downloadCompleteHTML() { | |
| console.log('Starting downloadCompleteHTML...'); | |
| async function fetchResource(url, isBinary = false) { | |
| try { | |
| const controller = new AbortController(); | |
| const timeout = setTimeout(() => controller.abort(), 10000); | |
| const response = await fetch(url, { | |
| signal: controller.signal, | |
| cache: 'no-store' | |
| }); | |
| clearTimeout(timeout); | |
| if (!response.ok) throw new Error(`HTTP ${response.status}`); | |
| console.log(`Fetched resource: ${url}`); | |
| if (isBinary) { | |
| const blob = await response.blob(); | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onloadend = () => resolve(reader.result); | |
| reader.onerror = reject; | |
| reader.readAsDataURL(blob); | |
| }); | |
| } | |
| return await response.text(); | |
| } catch (error) { | |
| console.warn(`Failed to fetch ${url}: ${error.message}`); | |
| return isBinary ? '' : `/* Failed to load: ${url} */`; | |
| } | |
| } | |
| async function inlineCSS(linkElement) { | |
| const href = linkElement.href; | |
| console.log(`Inlining CSS: ${href}`); | |
| const cssContent = await fetchResource(href); | |
| const resolvedCSS = cssContent.replace( | |
| /url\((?!['"]?(?:data|https?|ftp):)['"]?([^'")]+)['"]?\)/g, | |
| (match, relativeUrl) => { | |
| try { | |
| const absoluteUrl = new URL(relativeUrl.trim(), href).href; | |
| return `url("${absoluteUrl}")`; | |
| } catch (err) { | |
| console.warn(`Failed to resolve URL: ${relativeUrl}`); | |
| return match; | |
| } | |
| } | |
| ); | |
| const styleElement = document.createElement('style'); | |
| styleElement.textContent = resolvedCSS; | |
| return styleElement; | |
| } | |
| async function inlineMedia(element) { | |
| const mediaElements = element.querySelectorAll('img, video, audio, source'); | |
| console.log(`Found ${mediaElements.length} media elements to inline`); | |
| for (const media of mediaElements) { | |
| const srcAttr = media.src ? 'src' : 'data-src'; | |
| if (media[srcAttr]?.startsWith('http')) { | |
| const dataUri = await fetchResource(media[srcAttr], true); | |
| if (dataUri) media[srcAttr] = dataUri; | |
| } | |
| } | |
| } | |
| console.log('Cloning document...'); | |
| const parser = new DOMParser(); | |
| const doc = parser.parseFromString(document.documentElement.outerHTML, 'text/html'); | |
| const linkElements = [...doc.querySelectorAll('link[rel="stylesheet"]')]; | |
| console.log(`Processing ${linkElements.length} CSS links...`); | |
| await Promise.all(linkElements.map(async link => { | |
| const styleElement = await inlineCSS(link); | |
| link.replaceWith(styleElement); | |
| })); | |
| await inlineMedia(doc); | |
| const scripts = doc.querySelectorAll('script[src]'); | |
| console.log(`Processing ${scripts.length} scripts...`); | |
| await Promise.all([...scripts].map(async script => { | |
| if (script.src.startsWith('http')) { | |
| const scriptContent = await fetchResource(script.src); | |
| script.textContent = scriptContent; | |
| script.removeAttribute('src'); | |
| } | |
| })); | |
| const finalHTML = `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`; | |
| console.log('HTML prepared for download'); | |
| function downloadHTML(content, fileName) { | |
| const blob = new Blob([content], { type: 'text/html' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = fileName || `${document.title || 'page'}_${new Date().toISOString().split('T')[0]}.html`; | |
| document.body.appendChild(a); | |
| console.log(`Triggering download: ${a.download}`); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } | |
| downloadHTML(finalHTML); | |
| } | |
| // Немедленный запуск | |
| console.log('Script loaded. Running initial save...'); | |
| try { | |
| await downloadCompleteHTML(); | |
| console.log('Initial save completed'); | |
| } catch (err) { | |
| console.error('Initial save failed:', err); | |
| } | |
| // Активируем слушатель клавиш | |
| initializeKeyListener(); | |
| console.log('Key listener activated. Press Ctrl+Shift+S to save'); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment