"""Body rendering as pure functions (ingest-spec §5). Byte-compatible with the reference implementation: golden conformance (§11) freezes the markdown-table shape, the escape order, and the fenced-block form. """ from __future__ import annotations from collections.abc import Sequence from .errors import RenderError def sql_value_to_text(value: object) -> str: """Convert one SQL cell value to its §5 text form (BEFORE table escaping). SQL NULL → empty string; integers plain decimal; non-integral numbers in shortest round-trip decimal form (repr); text verbatim; any other type MUST fail — never silent coercion. bool is an int subclass sqlite never emits, guarded explicitly so a stray one can never render as str(True). """ if value is None: return "" if isinstance(value, bool): raise RenderError( f"unsupported SQL cell type bool ({value!r}) — never silent coercion", code="unsupported_cell_type", ) if isinstance(value, int): return str(value) if isinstance(value, float): return repr(value) if isinstance(value, str): return value raise RenderError( f"unsupported SQL cell type {type(value).__name__} — spec §5 allows " "integer/float/text/NULL only; never silent coercion", code="unsupported_cell_type", ) def _escape_cell(value: str) -> str: # Escape order is load-bearing: `\` FIRST (else the backslash introduced # by pipe-escaping gets double-escaped), then `|`, then newlines → single # space with CRLF treated as ONE unit. value = value.replace("\\", "\\\\").replace("|", "\\|") return value.replace("\r\n", " ").replace("\r", " ").replace("\n", " ") def render_table(header: Sequence[str], rows: Sequence[Sequence[str]]) -> str: """Render extraction rows as the §5 markdown-table body. Cells are strings by the time they reach here (sql values via sql_value_to_text). Header cells are escaped identically to data cells. LF-only, exactly one trailing newline. """ def line(cells: Sequence[str]) -> str: return "| " + " | ".join(_escape_cell(cell) for cell in cells) + " |" separator = "| " + " | ".join("---" for _ in header) + " |" return "\n".join([line(header), separator, *(line(row) for row in rows)]) + "\n" def render_fenced_block(body: str) -> str: """Render an http response body verbatim inside a fenced code block (§5). NOT table-escaped — pipe/backslash survive verbatim. The body's own trailing newlines are stripped so the closing fence sits flush; LF-only, exactly one trailing newline. """ return f"```\n{body.rstrip(chr(10))}\n```\n"