|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|
|
<title>HTML Table to Word Converter</title> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
|
<style> |
|
|
:root { |
|
|
--primary: #6366f1; |
|
|
--primary-dark: #4f46e5; |
|
|
--bg: #f9fafb; |
|
|
--surface: #ffffff; |
|
|
--text: #111827; |
|
|
--muted: #6b7280; |
|
|
--border: #e5e7eb; |
|
|
--error: #ef4444; |
|
|
--success: #10b981; |
|
|
--radius: 12px; |
|
|
--shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); |
|
|
} |
|
|
|
|
|
* { |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
body { |
|
|
margin: 0; |
|
|
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; |
|
|
background: var(--bg); |
|
|
color: var(--text); |
|
|
line-height: 1.6; |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
padding: 2rem 1rem; |
|
|
} |
|
|
|
|
|
.app { |
|
|
width: 100%; |
|
|
max-width: 640px; |
|
|
} |
|
|
|
|
|
header { |
|
|
text-align: center; |
|
|
margin-bottom: 2rem; |
|
|
} |
|
|
|
|
|
h1 { |
|
|
font-size: 2.25rem; |
|
|
font-weight: 800; |
|
|
margin: 0; |
|
|
} |
|
|
|
|
|
.card { |
|
|
background: var(--surface); |
|
|
border-radius: var(--radius); |
|
|
box-shadow: var(--shadow); |
|
|
padding: 2rem; |
|
|
} |
|
|
|
|
|
.upload-zone { |
|
|
border: 2px dashed var(--border); |
|
|
border-radius: var(--radius); |
|
|
padding: 3rem 2rem; |
|
|
text-align: center; |
|
|
transition: border-color 0.3s, background-color 0.3s; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.upload-zone:hover { |
|
|
border-color: var(--primary); |
|
|
background-color: #f3f4ff; |
|
|
} |
|
|
|
|
|
.upload-zone.dragover { |
|
|
border-color: var(--primary); |
|
|
background-color: #e0e7ff; |
|
|
} |
|
|
|
|
|
.upload-icon { |
|
|
font-size: 3rem; |
|
|
color: var(--muted); |
|
|
margin-bottom: 0.5rem; |
|
|
} |
|
|
|
|
|
.upload-text { |
|
|
font-size: 1.125rem; |
|
|
font-weight: 600; |
|
|
margin: 0; |
|
|
} |
|
|
|
|
|
.upload-hint { |
|
|
color: var(--muted); |
|
|
margin-top: 0.5rem; |
|
|
} |
|
|
|
|
|
.file-input { |
|
|
display: none; |
|
|
} |
|
|
|
|
|
.success { |
|
|
color: var(--success); |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.error { |
|
|
color: var(--error); |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.info { |
|
|
color: var(--muted); |
|
|
margin-top: 0.5rem; |
|
|
} |
|
|
|
|
|
.controls { |
|
|
margin-top: 2rem; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 1rem; |
|
|
} |
|
|
|
|
|
label { |
|
|
font-weight: 600; |
|
|
margin-bottom: 0.5rem; |
|
|
display: block; |
|
|
} |
|
|
|
|
|
input[type="number"] { |
|
|
width: 100%; |
|
|
padding: 0.75rem; |
|
|
border: 1px solid var(--border); |
|
|
border-radius: 8px; |
|
|
font-size: 1rem; |
|
|
} |
|
|
|
|
|
input[type="number"]:focus { |
|
|
border-color: var(--primary); |
|
|
outline: none; |
|
|
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2); |
|
|
} |
|
|
|
|
|
.preview { |
|
|
overflow-x: auto; |
|
|
margin-top: 2rem; |
|
|
} |
|
|
|
|
|
table { |
|
|
width: 100%; |
|
|
border-collapse: collapse; |
|
|
font-size: 0.875rem; |
|
|
} |
|
|
|
|
|
th, td { |
|
|
padding: 0.75rem; |
|
|
border-bottom: 1px solid var(--border); |
|
|
text-align: left; |
|
|
} |
|
|
|
|
|
th { |
|
|
background-color: #f3f4f6; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.btn { |
|
|
background: var(--primary); |
|
|
color: #fff; |
|
|
border: none; |
|
|
padding: 0.75rem 1.5rem; |
|
|
border-radius: 8px; |
|
|
font-size: 1rem; |
|
|
font-weight: 600; |
|
|
cursor: pointer; |
|
|
transition: background-color 0.3s; |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
gap: 0.5rem; |
|
|
} |
|
|
|
|
|
.btn:hover { |
|
|
background: var(--primary-dark); |
|
|
} |
|
|
|
|
|
.status { |
|
|
margin-top: 1rem; |
|
|
padding: 1rem; |
|
|
border-radius: var(--radius); |
|
|
} |
|
|
|
|
|
.status.success { |
|
|
background: #ecfdf5; |
|
|
color: var(--success); |
|
|
} |
|
|
|
|
|
.status.error { |
|
|
background: #fef2f2; |
|
|
color: var(--error); |
|
|
} |
|
|
|
|
|
.status.info { |
|
|
background: #f3f4f6; |
|
|
color: var(--muted); |
|
|
} |
|
|
|
|
|
@media (max-width: 600px) { |
|
|
h1 { |
|
|
font-size: 1.75rem; |
|
|
} |
|
|
|
|
|
.card { |
|
|
padding: 1.5rem; |
|
|
} |
|
|
|
|
|
.upload-zone { |
|
|
padding: 2rem 1rem; |
|
|
} |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="app"> |
|
|
<header> |
|
|
<h1>HTML Table to Word Converter</h1> |
|
|
</header> |
|
|
|
|
|
<div class="card"> |
|
|
<form id="uploadForm"> |
|
|
<div class="upload-zone" id="dropZone"> |
|
|
<i class="fas fa-cloud-upload-alt upload-icon"></i> |
|
|
<p class="upload-text">Upload HTML File</p> |
|
|
<p class="upload-hint">Drag & drop or click to select</p> |
|
|
<input type="file" id="fileInput" class="file-input" accept=".html,.htm" /> |
|
|
</div> |
|
|
|
|
|
<div id="status" class="status" style="display: none;"></div> |
|
|
|
|
|
<div id="controls" class="controls" style="display: none;"> |
|
|
<div> |
|
|
<label for="skipRows">Rows to skip (from top)</label> |
|
|
<input type="number" id="skipRows" min="0" value="1" /> |
|
|
</div> |
|
|
<div> |
|
|
<label for="numColumns">Number of columns to import</label> |
|
|
<input type="number" id="numColumns" min="1" max="50" value="8" /> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div id="preview" class="preview" style="display: none;"> |
|
|
<h2>Extracted Table Preview</h2> |
|
|
<table id="previewTable"></table> |
|
|
</div> |
|
|
|
|
|
<button type="button" id="downloadBtn" class="btn" style="display: none;"> |
|
|
<i class="fas fa-download"></i>Download as Word (.docx) |
|
|
</button> |
|
|
</form> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
const fileInput = document.getElementById('fileInput'); |
|
|
const dropZone = document.getElementById('dropZone'); |
|
|
const statusDiv = document.getElementById('status'); |
|
|
const controlsDiv = document.getElementById('controls'); |
|
|
const previewDiv = document.getElementById('preview'); |
|
|
const previewTable = document.getElementById('previewTable'); |
|
|
const downloadBtn = document.getElementById('downloadBtn'); |
|
|
const skipRowsInput = document.getElementById('skipRows'); |
|
|
const numColumnsInput = document.getElementById('numColumns'); |
|
|
|
|
|
let extractedTable = null; |
|
|
|
|
|
dropZone.addEventListener('click', () => fileInput.click()); |
|
|
|
|
|
dropZone.addEventListener('dragover', (e) => { |
|
|
e.preventDefault(); |
|
|
dropZone.classList.add('dragover'); |
|
|
}); |
|
|
|
|
|
dropZone.addEventListener('dragleave', () => { |
|
|
dropZone.classList.remove('dragover'); |
|
|
}); |
|
|
|
|
|
dropZone.addEventListener('drop', (e) => { |
|
|
e.preventDefault(); |
|
|
dropZone.classList.remove('dragover'); |
|
|
const files = e.dataTransfer.files; |
|
|
if (files.length) { |
|
|
handleFile(files[0]); |
|
|
} |
|
|
}); |
|
|
|
|
|
fileInput.addEventListener('change', () => { |
|
|
const file = fileInput.files[0]; |
|
|
if (file) { |
|
|
handleFile(file); |
|
|
} |
|
|
}); |
|
|
|
|
|
function handleFile(file) { |
|
|
const reader = new FileReader(); |
|
|
reader.onload = (e) => { |
|
|
try { |
|
|
const html = e.target.result; |
|
|
const tables = extractAllTables(html); |
|
|
const validTables = tables.filter(t => t && t.length && t[0].length >= 5); |
|
|
if (!validTables.length) { |
|
|
showStatus("No valid tables found in the uploaded HTML file.", 'error'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const largest = validTables.reduce((max, cur) => (cur.length > max.length ? cur : max), validTables[0]); |
|
|
extractedTable = largest; |
|
|
|
|
|
const maxCols = largest[0].length; |
|
|
const maxRows = largest.length; |
|
|
|
|
|
showStatus(`Found ${validTables.length} table(s). Using the largest one with ${maxRows} rows and ${maxCols} columns.`, 'success'); |
|
|
|
|
|
skipRowsInput.max = Math.max(maxRows - 1, 0); |
|
|
skipRowsInput.value = maxRows <= 1 ? 0 : 1; |
|
|
|
|
|
numColumnsInput.max = maxCols; |
|
|
numColumnsInput.value = Math.min(8, maxCols); |
|
|
|
|
|
controlsDiv.style.display = 'block'; |
|
|
previewTable.innerHTML = ''; |
|
|
updatePreview(); |
|
|
} catch (err) { |
|
|
showStatus("An error occurred while processing the file.", 'error'); |
|
|
console.error(err); |
|
|
} |
|
|
}; |
|
|
reader.readAsText(file); |
|
|
} |
|
|
|
|
|
function extractAllTables(html) { |
|
|
const parser = new DOMParser(); |
|
|
const doc = parser.parseFromString(html, 'text/html'); |
|
|
const tables = doc.querySelectorAll('table'); |
|
|
return Array.from(tables).map(table => { |
|
|
const rows = Array.from(table.querySelectorAll('tr')); |
|
|
return rows.map(row => { |
|
|
const cells = Array.from(row.querySelectorAll('td, th')); |
|
|
return cells.map(cell => cell.textContent.trim()); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
function updatePreview() { |
|
|
if (!extractedTable) return; |
|
|
const skip = parseInt(skipRowsInput.value, 10); |
|
|
const cols = parseInt(numColumnsInput.value, 10); |
|
|
const sliced = extractedTable.slice(skip).map(row => row.slice(0, cols)); |
|
|
|
|
|
previewTable.innerHTML = ''; |
|
|
if (sliced.length > 0) { |
|
|
const thead = document.createElement('thead'); |
|
|
const tr = document.createElement('tr'); |
|
|
sliced[0].forEach(cell => { |
|
|
const th = document.createElement('th'); |
|
|
th.textContent = cell; |
|
|
tr.appendChild(th); |
|
|
}); |
|
|
thead.appendChild(tr); |
|
|
previewTable.appendChild(thead); |
|
|
|
|
|
const tbody = document.createElement('tbody'); |
|
|
for (let i = 1; i < sliced.length; i++) { |
|
|
if (sliced[i].every(c => !c)) continue; |
|
|
const tr = document.createElement('tr'); |
|
|
sliced[i].forEach(cell => { |
|
|
const td = document.createElement('td'); |
|
|
td.textContent = cell; |
|
|
tr.appendChild(td); |
|
|
}); |
|
|
tbody.appendChild(tr); |
|
|
} |
|
|
previewTable.appendChild(tbody); |
|
|
} |
|
|
|
|
|
previewDiv.style.display = 'block'; |
|
|
downloadBtn.style.display = 'inline-flex'; |
|
|
} |
|
|
|
|
|
skipRowsInput.addEventListener('input', updatePreview); |
|
|
numColumnsInput.addEventListener('input', updatePreview); |
|
|
|
|
|
downloadBtn.addEventListener('click', () => { |
|
|
if (!extractedTable) return; |
|
|
const skip = parseInt(skipRowsInput.value, 10); |
|
|
const cols = parseInt(numColumnsInput.value, 10); |
|
|
const sliced = extractedTable.slice(skip).map(row => row.slice(0, cols)); |
|
|
|
|
|
downloadTableAsDocx(sliced, 'extracted_table.docx'); |
|
|
}); |
|
|
|
|
|
function downloadTableAsDocx(table, filename) { |
|
|
let html = '<html><head><meta charset="UTF-8"></head><body><table>'; |
|
|
table.forEach(row => { |
|
|
html += '<tr>'; |
|
|
row.forEach(cell => { |
|
|
html += `<td>${cell}</td>`; |
|
|
}); |
|
|
html += '</tr>'; |
|
|
}); |
|
|
html += '</table></body></html>'; |
|
|
|
|
|
const blob = new Blob([html], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }); |
|
|
const url = URL.createObjectURL(blob); |
|
|
const a = document.createElement('a'); |
|
|
a.href = url; |
|
|
a.download = filename; |
|
|
document.body.appendChild(a); |
|
|
a.click(); |
|
|
document.body.removeChild(a); |
|
|
URL.revokeObjectURL(url); |
|
|
} |
|
|
|
|
|
function showStatus(message, type) { |
|
|
statusDiv.textContent = message; |
|
|
statusDiv.className = `status ${type}`; |
|
|
statusDiv.style.display = 'block'; |
|
|
} |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
|