diff --git a/csv_to_html.py b/csv_to_html.py index b0e9916..843e20a 100644 --- a/csv_to_html.py +++ b/csv_to_html.py @@ -2,9 +2,19 @@ import csv import html import sys +# language=css page_css = """ 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 { border-collapse: collapse; @@ -16,34 +26,106 @@ th, td { text-align: left; } th { - background-color: #f2f2f2; + 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: - """ - 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: - delimiter = "\t" if "\t" in csv_input else "," - reader = csv.reader(csv_input.splitlines(), delimiter=delimiter) + 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"
No data found in CSV input.
" - rendered = f""" + rendered = f""" @@ -59,26 +141,24 @@ def csv_to_html(csv_input: str) -> str: {rows_html} + - """ + """ - return rendered - - except csv.Error as e: - raise ValueError(f"Error parsing TSV: {e}") - except StopIteration: - return "No data found in TSV input.
" + return rendered def main(): try: - tsv_data = sys.stdin.read() - html_output = csv_to_html(tsv_data) + 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 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: raise Exception(f"An unexpected error occurred: {e}")