"""CSV / TSV -> PDF (typed table).
The first row is rendered as the header (
); every row becomes a
row — the table rules from app.BASE_CSS are injected at the top of
so they under-layer any user CSS. Delimiter: forced tab for .tsv;
for .csv, csv.Sniffer on a sample with a comma fallback (single-line files
that the sniffer rejects still render).
Empty / whitespace-only input is a job error ("empty file: ...").
"""
from __future__ import annotations
import csv
import io
from pathlib import Path
from . import common
from .text_converter import decode_bytes
def _rows(text: str, delimiter: str) -> list[list[str]]:
rows = list(csv.reader(io.StringIO(text), delimiter=delimiter))
return [row for row in rows if any(cell.strip() for cell in row)]
def _table_html(rows: list[list[str]]) -> str:
if not rows:
return ""
header, body = rows[0], rows[1:]
head = "".join(f"| {common.escape(c)} | " for c in header)
out = [f"{head} "]
for row in body:
# pad ragged rows to header width so borders line up
cells = row + [""] * (len(header) - len(row))
out.append("" + "".join(f"| {common.escape(c)} | " for c in cells) + " ")
out.append(" ")
return "\n".join(out)
def convert(path: Path, job_id: str, progress_cb=None, output_dir=None,
source_name=None) -> str:
"""Render a CSV or TSV file to a tabular PDF."""
import app # lazy: BASE_CSS lives with the EPUB pipeline
raw = path.read_bytes()
if not raw.strip():
raise ValueError("empty file: nothing to convert")
text = decode_bytes(raw)
if path.suffix.lower() == ".tsv":
delimiter = "\t"
else:
try:
delimiter = csv.Sniffer().sniff(text[:8192], delimiters=",;\t|").delimiter
except csv.Error:
delimiter = ","
rows = _rows(text, delimiter)
table = _table_html(rows)
html_doc = (
""
f"{table}"
)
def _build():
return html_doc, path.name
return common.convert_single(
path, job_id, build_html=_build, css="",
progress_cb=progress_cb, output_dir=output_dir,
name_src=source_name or path.name,
)
|