Files
playground/csv_to_html.py
2024-12-25 23:01:34 +03:00

168 lines
4.0 KiB
Python

import csv
import html
import sys
# language=css
page_css = """
body {
margin: 0;
padding: 0.5rem;
font-family: menlo, consolas, monospace;
font-size: 14px;
}
input {
font-size: inherit;
font-family: inherit;
background-color: transparent;
padding: 0.25em 0.5em;
}
table {
border-collapse: collapse;
width: 100%;
}
th, td {
border: 1px solid black;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
tr:nth-child(even) {
background-color: #fdfdfd;
}
"""
# language=js
page_script = """
function sortableTable(table) {
const sortStates = new Map();
table.querySelectorAll('th').forEach((header, colIndex) => {
header.style.cursor = 'pointer';
header.onclick = () => {
const isAsc = !sortStates.get(header);
const tbody = table.querySelector('tbody');
const rows = [...tbody.rows];
rows.sort((a, b) => {
const [valA, valB] = [a, b].map(row =>
row.cells[colIndex].textContent.trim());
return /^\d+$/.test(valA)
? (valA - valB) * (isAsc ? 1 : -1)
: valA.localeCompare(valB) * (isAsc ? 1 : -1);
});
sortStates.set(header, isAsc);
tbody.replaceChildren(...rows); // Better than innerHTML = ''
};
});
}
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
function addFilterInput(tableElement) {
const filterInput = document.createElement('input');
filterInput.type = 'text';
filterInput.placeholder = "Search...";
tableElement.parentNode.insertBefore(filterInput, tableElement);
const debouncedFilter = debounce(filter, 100);
filterInput.addEventListener('input', () => {
debouncedFilter();
});
function filter() {
const filterValue = filterInput.value.trim();
const rows = tableElement.querySelectorAll('tbody tr');
rows.forEach(row => {
const rowData = row.textContent.toLowerCase();
if (filterValue.startsWith('/') && filterValue.endsWith('/')) {
const regex = new RegExp(filterValue.slice(1, -1), 'ig');
row.style.display = regex.test(rowData) ? '' : 'none';
} else {
row.style.display = rowData.includes(filterValue.toLowerCase()) ? '' : 'none';
}
});
}
}
sortableTable(document.querySelector('table'));
addFilterInput(document.querySelector('table'));
"""
def csv_to_html(csv_input: str) -> str:
try:
lines = csv_input.splitlines()
delimiter = "\t" if "\t" in lines[0] else ","
reader = csv.reader(lines, delimiter=delimiter)
header = next(reader) # Get the header row
header_html = f'{"".join(f"<th>{html.escape(col)}</th>" for col in header)}'
rows = [
f"<tr>{''.join(f'<td>{html.escape(cell)}</td>' for cell in row)}</tr>"
for row in reader
]
rows_html = "\n".join(rows)
except csv.Error as e:
raise ValueError(f"Error parsing CSV: {e}")
except StopIteration:
return "<p>No data found in CSV input.</p>"
rendered = f"""
<!DOCTYPE html>
<html>
<head>
<title>Query Results</title>
<style>{page_css}</style>
</head>
<body>
<table>
<thead>
<tr>{header_html}</tr>
</thead>
<tbody>
{rows_html}
</tbody>
</table>
<script>{page_script}</script>
</body>
</html>
"""
return rendered
def main():
try:
csv_data = sys.stdin.read()
html_output = csv_to_html(csv_data)
if html_output:
print(html_output)
except UnicodeDecodeError as e:
raise ValueError(
f"Error decoding input: {e}. Ensure your CSV data is encoded correctly (e.g., UTF-8)."
)
except Exception as e:
raise Exception(f"An unexpected error occurred: {e}")
if __name__ == "__main__":
main()