File size: 5,829 Bytes
7f0d098 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | import JSZip from "jszip";
export interface ClonedPageAsset {
rawUrl: string;
localPath: string;
type: "css" | "js" | "image";
content?: string;
}
/**
* Clean HTML: Strip analytics, comments, tracking pixels, ads
*/
export function cleanHtmlString(html: string): string {
let cleaned = html;
// 1. Remove comments
cleaned = cleaned.replace(/<!--[\s\S]*?-->/g, "");
// 2. Locate and remove tracking script blocks
// e.g. Google Analytics, Facebook Pixel, doubleclick, hotjar
cleaned = cleaned.replace(/<script[^>]*>[\s\S]*?(googletagmanager|google-analytics|connect\.facebook\.net|hotjar|analytics\.js)[\s\S]*?<\/script>/gi, "");
// 3. Remove inline tracking pixels
cleaned = cleaned.replace(/<img[^>]*src="[^"]*(pixel|track|doubleclick|googleads)[^"]*"[^>]*>/gi, "");
return cleaned;
}
/**
* Resolve relative URLs with respect to base URL
*/
export function resolveRelativeUrl(baseUrl: string, relativePath: string): string {
try {
const base = new URL(baseUrl.startsWith("http") ? baseUrl : `https://${baseUrl}`);
const resolved = new URL(relativePath, base);
return resolved.toString();
} catch (e) {
return relativePath;
}
}
/**
* Extract styles, scripts and images from clean HTML to build localized project files
*/
export async function performWebsiteClone(targetUrl: string): Promise<{
success: boolean;
html: string;
css: string;
js: string;
size: string;
error?: string;
}> {
try {
// 1. Fetch main HTML via our Express proxy
const response = await fetch(`/api/proxy?url=${encodeURIComponent(targetUrl)}`);
if (!response.ok) {
throw new Error(`تعذر تحميل الموقع. رمز الخطأ: ${response.status}`);
}
const rawHtml = await response.text();
const cleanedHtml = cleanHtmlString(rawHtml);
// Parse the HTML using native DOMParser
const parser = new DOMParser();
const doc = parser.parseFromString(cleanedHtml, "text/html");
// 2. Discover stylesheets & inline CSS
const stylesheetLinks = Array.from(doc.querySelectorAll("link[rel='stylesheet']"));
let consolidatedColorsText = "/* SiteClone Pro Bundled CSS */\n";
// Attempt to pull external stylesheets (first 3 for performance)
for (let i = 0; i < Math.min(stylesheetLinks.length, 3); i++) {
const link = stylesheetLinks[i];
const href = link?.getAttribute("href");
if (href) {
const fullCssUrl = resolveRelativeUrl(targetUrl, href);
try {
const cssRes = await fetch(`/api/proxy?url=${encodeURIComponent(fullCssUrl)}`);
if (cssRes.ok) {
const cssText = await cssRes.text();
consolidatedColorsText += `\n/* Source: ${href} */\n${cssText}\n`;
// Modify link to point locally
link.setAttribute("href", "styles.css");
}
} catch {
// Fallback if resource fetch fails due to any reason
}
}
}
// 3. Discover script blocks (first 3 for performance)
const scriptTags = Array.from(doc.querySelectorAll("script[src]"));
let consolidatedJsText = "/* SiteClone Pro Bundled JS */\nconsole.log('SiteClone Pro V20 initialized.');\n";
for (let i = 0; i < Math.min(scriptTags.length, 3); i++) {
const script = scriptTags[i];
const src = script?.getAttribute("src");
if (src && !src.includes("analytics") && !src.includes("pixel")) {
const fullJsUrl = resolveRelativeUrl(targetUrl, src);
try {
const jsRes = await fetch(`/api/proxy?url=${encodeURIComponent(fullJsUrl)}`);
if (jsRes.ok) {
const jsText = await jsRes.text();
consolidatedJsText += `\n/* Source: ${src} */\n${jsText}\n`;
// Modify script tag to point locally
script.setAttribute("src", "app.js");
}
} catch {
// Fail gracefully
}
}
}
// Ensure we reference a local stylesheet/script if we harvested any
if (stylesheetLinks.length > 0 && !doc.querySelector("link[href='styles.css']")) {
const cssLink = doc.createElement("link");
cssLink.setAttribute("rel", "stylesheet");
cssLink.setAttribute("href", "styles.css");
doc.head.appendChild(cssLink);
}
if (scriptTags.length > 0 && !doc.querySelector("script[src='app.js']")) {
const jsScript = doc.createElement("script");
jsScript.setAttribute("src", "app.js");
doc.body.appendChild(jsScript);
}
const modifiedHtml = doc.documentElement.outerHTML;
// Estimate file size
const totalBytes = new Blob([modifiedHtml, consolidatedColorsText, consolidatedJsText]).size;
const sizeKb = (totalBytes / 1024).toFixed(1);
return {
success: true,
html: modifiedHtml,
css: consolidatedColorsText,
js: consolidatedJsText,
size: `${sizeKb} KB`
};
} catch (err: any) {
console.error("Cloning error:", err);
return {
success: false,
html: "",
css: "",
js: "",
size: "0 KB",
error: err?.message || String(err)
};
}
}
/**
* Trigger browser zip download using JSZip
*/
export async function downloadCloneAsZip(clone: { name: string; html: string; css: string; js: string }) {
const zip = new JSZip();
// Add files to the archive
zip.file("index.html", clone.html);
zip.file("styles.css", clone.css);
zip.file("app.js", clone.js);
// Generate ZIP file
const content = await zip.generateAsync({ type: "blob" });
// Custom anchor download trigger
const url = URL.createObjectURL(content);
const a = document.createElement("a");
a.href = url;
a.download = `${clone.name.replace(/\s+/g, "_")}_siteclone_pro.zip`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
|