vibe-coding-cn/assets/repo/html-tools-main/clean_epub_css.html

424 lines
19 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EPUB CSS 清理工具 (Web版)</title>
<style>
body {
font-family: sans-serif;
line-height: 1.6;
padding: 20px;
max-width: 600px;
margin: 20px auto;
background-color: #f4f4f4;
border: 1px solid #ccc;
border-radius: 8px;
}
h1 {
text-align: center;
color: #333;
}
#drop-area {
border: 2px dashed #ccc;
border-radius: 5px;
padding: 30px;
text-align: center;
background-color: #fff;
margin-bottom: 20px;
cursor: pointer;
}
#drop-area.highlight {
border-color: dodgerblue;
}
#drop-area p {
margin: 0;
color: #555;
}
#fileInput {
display: none; /* Hide default input, use drop area or label */
}
label.file-label {
display: inline-block;
padding: 10px 15px;
background-color: dodgerblue;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
transition: background-color 0.2s;
}
label.file-label:hover {
background-color: #007ae5;
}
#status {
margin-top: 15px;
padding: 10px;
background-color: #eee;
border-radius: 4px;
min-height: 40px; /* Ensure space for messages */
text-align: center;
font-weight: bold;
}
.status-details {
font-size: 0.9em;
color: #555;
margin-top: 5px;
}
.warning {
color: #a00;
font-weight: bold;
text-align: center;
margin-top: 20px;
padding: 10px;
background-color: #fdd;
border: 1px solid #fbb;
border-radius: 4px;
}
progress {
width: 100%;
margin-top: 10px;
display: none; /* Hidden by default */
}
</style>
<!-- Include JSZip library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<!-- Include FileSaver.js library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
</head>
<body>
<h1>EPUB CSS 清理工具 (Web版)</h1>
<p>将单个 EPUB 文件或包含多个 EPUB 的 ZIP 文件拖拽到下方区域,或点击按钮选择文件,即可移除指定的 CSS 样式。</p>
<div id="drop-area">
<!-- Accept both .epub and .zip -->
<input type="file" id="fileInput" accept=".epub,.zip">
<p>将 EPUB 或 ZIP 文件拖拽到这里</p>
<label for="fileInput" class="file-label">或者选择文件</label>
</div>
<progress id="progressBar" value="0" max="100"></progress>
<div id="status">请选择或拖拽一个 EPUB 或 ZIP 文件。</div>
<div id="statusDetails" class="status-details"></div>
<div class="warning">
<strong>重要提示:</strong> 此工具在你的浏览器中处理文件,不会上传到服务器。处理后的文件需要你手动下载。建议在处理前备份原始文件!处理 ZIP 文件时会生成一个包含所有处理后或原始EPUB 的新 ZIP 文件。
</div>
<script>
// --- Check if libraries loaded ---
if (typeof JSZip === 'undefined' || typeof saveAs === 'undefined') {
const errorMsg = '错误:无法加载必需的库 (JSZip 或 FileSaver)。请检查您的网络连接,并确保没有浏览器插件(如广告拦截器)阻止从 cdnjs.cloudflare.com 加载脚本。然后请刷新页面重试。';
document.getElementById('status').textContent = errorMsg;
document.getElementById('status').style.color = '#a00';
document.getElementById('drop-area').style.display = 'none';
document.querySelector('.warning').textContent = errorMsg;
} else {
// --- Global Variables & Constants ---
const dropArea = document.getElementById('drop-area');
const fileInput = document.getElementById('fileInput');
const statusDiv = document.getElementById('status');
const statusDetailsDiv = document.getElementById('statusDetails');
const progressBar = document.getElementById('progressBar');
const CSS_REMOVE_PATTERN = /((text-indent|line-height|font-size|height|font-family|color)\s*:\s*[^;]*;|display\s*:\s*block\s*;)/ig;
// --- Event Handlers ---
setupDragDropHandlers();
fileInput.addEventListener('change', handleFileSelect, false);
function setupDragDropHandlers() {
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, () => dropArea.classList.add('highlight'), false);
});
['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, () => dropArea.classList.remove('highlight'), false);
});
dropArea.addEventListener('drop', handleDrop, false);
}
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
function handleDrop(e) {
handleFiles(e.dataTransfer.files);
}
function handleFileSelect(e) {
handleFiles(e.target.files);
}
// --- Main File Handling Logic ---
function handleFiles(files) {
if (files.length === 0) return;
const file = files[0]; // Process only the first file
// Check file type
const isEpub = file.name.toLowerCase().endsWith('.epub');
const isZip = file.name.toLowerCase().endsWith('.zip');
if (!isEpub && !isZip) {
updateStatus('错误:请选择一个 .epub 或 .zip 文件。', true);
resetInput();
return;
}
updateStatus(`正在读取文件: ${file.name}...`);
clearStatusDetails();
progressBar.style.display = 'block';
progressBar.value = 0;
const reader = new FileReader();
reader.onload = async (e) => {
const fileContent = e.target.result; // ArrayBuffer
try {
if (isEpub) {
updateStatus(`正在处理 EPUB 文件: ${file.name}...`);
const result = await processEpub(fileContent, file.name);
if (result.blob) {
saveAs(result.blob, result.filename);
updateStatus(`处理完成!已生成 ${result.filename}`);
} else if (result.modified === false) {
updateStatus('信息:文件无需修改。');
}
resetInput();
} else if (isZip) {
updateStatus(`正在处理 ZIP 压缩包: ${file.name}...`);
await processZipArchive(fileContent, file.name);
}
} catch (error) {
console.error("处理失败:", error);
updateStatus(`处理失败:${error.message}`, true);
resetInput();
}
};
reader.onprogress = updateReadProgress;
reader.onerror = handleReadError;
reader.readAsArrayBuffer(file);
}
function updateReadProgress(e) {
if (e.lengthComputable) {
progressBar.value = Math.round((e.loaded / e.total) * 10); // Reading is ~10%
}
}
function handleReadError() {
updateStatus('错误:读取文件时出错。', true);
resetInput();
}
// --- ZIP Archive Processing ---
async function processZipArchive(zipFileContent, zipFilename) {
let outputZip = new JSZip();
let processedCount = 0;
let failedCount = 0;
let foundCount = 0;
let modifiedCount = 0;
let filePromises = [];
updateStatus(`正在打开 ZIP 压缩包...`);
progressBar.value = 15;
const inputZip = await JSZip.loadAsync(zipFileContent);
const totalFilesInZip = Object.keys(inputZip.files).length;
updateStatus(`在 ZIP 中查找 EPUB 文件...`);
inputZip.forEach((relativePath, zipEntry) => {
if (!zipEntry.dir && relativePath.toLowerCase().endsWith('.epub')) {
foundCount++;
updateStatusDetails(`找到 EPUB: ${relativePath}`);
// Add a promise to process this EPUB entry
const promise = zipEntry.async('arraybuffer')
.then(async (innerEpubContent) => {
try {
// Process the inner EPUB
const result = await processEpub(innerEpubContent, relativePath);
if (result.blob) {
// Add modified EPUB to output ZIP
outputZip.file(result.filename, result.blob);
modifiedCount++;
console.log(`Added cleaned ${result.filename} to output zip.`);
} else {
// Add original EPUB back if not modified
outputZip.file(relativePath, innerEpubContent);
console.log(`Added original ${relativePath} (no changes needed) to output zip.`);
}
processedCount++;
} catch (epubError) {
// Add original EPUB back if processing failed
console.error(`Error processing inner EPUB ${relativePath}:`, epubError);
updateStatusDetails(`处理 ${relativePath} 时出错,将保留原文件。`);
outputZip.file(relativePath, innerEpubContent); // Add original back
failedCount++;
}
})
.catch(extractError => {
// Handle error extracting the inner EPUB content
console.error(`Error extracting inner EPUB ${relativePath} from zip:`, extractError);
updateStatusDetails(`无法从 ZIP 中提取 ${relativePath},已跳过。`);
failedCount++;
})
.finally(() => {
// Update progress based on entries processed (success or fail)
const progress = 15 + Math.round(((processedCount + failedCount) / foundCount) * 60); // Processing EPUBs is ~60%
progressBar.value = Math.min(progress, 75); // Cap at 75 before final packing
});
filePromises.push(promise);
}
// We are currently NOT adding non-EPUB files from the source ZIP to the output ZIP.
// To do so, handle the 'else' case here and add zipEntry data to outputZip.
});
// Wait for all inner EPUB processing promises to complete
await Promise.all(filePromises);
progressBar.value = 80; // Done processing files inside zip
if (foundCount === 0) {
updateStatus('错误:在 ZIP 文件中未找到任何 EPUB 文件。', true);
resetInput();
return;
}
updateStatus(`处理了 ${foundCount} 个 EPUB 文件。正在生成输出 ZIP...`);
statusDetailsDiv.innerHTML += `<br>成功: ${processedCount}, 修改: ${modifiedCount}, 失败/跳过: ${failedCount}`;
progressBar.value = 90;
const outputZipBlob = await outputZip.generateAsync({
type: "blob",
compression: "DEFLATE",
platform: "browser"
}, (metadata) => {
progressBar.value = 90 + Math.round(metadata.percent * 0.1);
});
progressBar.value = 100;
const baseName = zipFilename.substring(0, zipFilename.lastIndexOf('.')) || zipFilename;
const outputZipFilename = `${baseName}_cleaned.zip`;
saveAs(outputZipBlob, outputZipFilename);
updateStatus(`处理完成!已生成包含处理后 EPUB 的 ${outputZipFilename}`);
resetInput();
}
// --- Single EPUB Processing (Modified to return result) ---
async function processEpub(fileContent, originalFilename) {
// This function now returns an object:
// { blob: Blob, filename: string } if modified
// { modified: false } if no modification needed
// Throws error if processing fails critically
console.log(`Processing EPUB: ${originalFilename}`);
// Note: Progress bar updates inside this function are less meaningful now,
// as the overall progress is handled by the caller (handleFiles or processZipArchive).
// We could pass a progress callback if detailed inner progress is needed.
const zip = await JSZip.loadAsync(fileContent);
const newZip = new JSZip();
let cssFilesFound = false;
let modified = false;
let filePromises = [];
zip.forEach((relativePath, zipEntry) => {
if (zipEntry.dir) { newZip.folder(relativePath); return; }
const options = {
date: zipEntry.date,
unixPermissions: zipEntry.unixPermissions,
dosPermissions: zipEntry.dosPermissions,
comment: zipEntry.comment,
dir: zipEntry.dir,
compression: zipEntry.options?.compression === "STORE" ? "STORE" : "DEFLATE"
};
if (relativePath.toLowerCase() === 'mimetype') {
const promise = zipEntry.async('uint8array').then(data => {
newZip.file(relativePath, data, { ...options, compression: "STORE" });
});
filePromises.push(promise);
} else if (relativePath.toLowerCase().endsWith('.css')) {
cssFilesFound = true;
const promise = zipEntry.async('string').then(originalCss => {
const cleanedCss = originalCss.replace(CSS_REMOVE_PATTERN, '');
let contentToAdd = originalCss;
if (cleanedCss !== originalCss) {
modified = true;
contentToAdd = cleanedCss;
console.log(` Cleaned CSS: ${relativePath}`);
}
newZip.file(relativePath, contentToAdd, options);
}).catch(err => {
console.warn(` Could not process CSS ${relativePath} as text, keeping original. Error: ${err}`);
return zipEntry.async('uint8array').then(originalData => {
newZip.file(relativePath, originalData, options);
});
});
filePromises.push(promise);
} else {
const promise = zipEntry.async('uint8array').then(data => {
newZip.file(relativePath, data, options);
});
filePromises.push(promise);
}
});
await Promise.all(filePromises);
if (!modified) {
console.log(` EPUB ${originalFilename} - No modifications needed.`);
return { modified: false }; // Indicate no changes were made
}
console.log(` Repacking modified EPUB: ${originalFilename}`);
const newEpubBlob = await newZip.generateAsync({
type: "blob",
mimeType: "application/epub+zip",
compression: "DEFLATE",
platform: "browser"
});
const baseName = originalFilename.substring(0, originalFilename.lastIndexOf('.')) || originalFilename;
const newFilename = `${baseName}_cleaned.epub`;
return { blob: newEpubBlob, filename: newFilename }; // Return data for the caller
} // end of processEpub function
// --- UI Update Functions ---
function updateStatus(message, isError = false) {
statusDiv.textContent = message;
statusDiv.style.color = isError ? '#a00' : '#000';
if (isError) {
progressBar.style.display = 'none';
clearStatusDetails();
}
}
function updateStatusDetails(message) {
statusDetailsDiv.innerHTML += message + "<br>";
}
function clearStatusDetails() {
statusDetailsDiv.innerHTML = "";
}
function resetInput() {
fileInput.value = '';
// Hide progress bar after a delay
setTimeout(() => { progressBar.style.display = 'none'; progressBar.value = 0; }, 5000);
}
} // End of the 'else' block for library check
</script>
</body>
</html>