csv_to_html: Add column sort
This commit is contained in:
+105
-25
@@ -2,9 +2,19 @@ import csv
|
|||||||
import html
|
import html
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
# language=css
|
||||||
page_css = """
|
page_css = """
|
||||||
body {
|
body {
|
||||||
font-family: consolas, monospace;
|
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 {
|
table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
@@ -16,34 +26,106 @@ th, td {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
th {
|
th {
|
||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
}
|
}
|
||||||
tr:nth-child(even) {
|
tr:nth-child(even) {
|
||||||
background-color: #fdfdfd;
|
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:
|
def csv_to_html(csv_input: str) -> str:
|
||||||
"""
|
|
||||||
Converts TSV input from stdin to an HTML table.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
csv_input: The TSV input as a string.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A string containing the HTML table, or None if an error occurs.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
delimiter = "\t" if "\t" in csv_input else ","
|
lines = csv_input.splitlines()
|
||||||
reader = csv.reader(csv_input.splitlines(), delimiter=delimiter)
|
delimiter = "\t" if "\t" in lines[0] else ","
|
||||||
|
reader = csv.reader(lines, delimiter=delimiter)
|
||||||
header = next(reader) # Get the header row
|
header = next(reader) # Get the header row
|
||||||
|
|
||||||
header_html = f'{"".join(f"<th>{html.escape(col)}</th>" for col in header)}'
|
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 = [
|
||||||
|
f"<tr>{''.join(f'<td>{html.escape(cell)}</td>' for cell in row)}</tr>"
|
||||||
|
for row in reader
|
||||||
|
]
|
||||||
rows_html = "\n".join(rows)
|
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"""
|
rendered = f"""
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -59,26 +141,24 @@ def csv_to_html(csv_input: str) -> str:
|
|||||||
{rows_html}
|
{rows_html}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<script>{page_script}</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return rendered
|
return rendered
|
||||||
|
|
||||||
except csv.Error as e:
|
|
||||||
raise ValueError(f"Error parsing TSV: {e}")
|
|
||||||
except StopIteration:
|
|
||||||
return "<p>No data found in TSV input.</p>"
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
try:
|
try:
|
||||||
tsv_data = sys.stdin.read()
|
csv_data = sys.stdin.read()
|
||||||
html_output = csv_to_html(tsv_data)
|
html_output = csv_to_html(csv_data)
|
||||||
if html_output:
|
if html_output:
|
||||||
print(html_output)
|
print(html_output)
|
||||||
except UnicodeDecodeError as e:
|
except UnicodeDecodeError as e:
|
||||||
raise ValueError(f"Error decoding input: {e}. Ensure your TSV data is encoded correctly (e.g., UTF-8).")
|
raise ValueError(
|
||||||
|
f"Error decoding input: {e}. Ensure your CSV data is encoded correctly (e.g., UTF-8)."
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"An unexpected error occurred: {e}")
|
raise Exception(f"An unexpected error occurred: {e}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user