Rebuild the Classic template on the engine with A4/Letter and a template harness
Classic now renders through buildRenderModel, computeFrame, the template and renderCore, with S.No and HSN/SAC columns, GST slots only through checked Slot components, taxable value, reverse-charge line, cancelled tag, continuation band and Page n of N. Letter absorbs its height deficit in elastic gaps. A harness runs 15 fixtures on A4 and Letter plus an item sweep and asserts zero audit errors, repeated headers, no split blocks, rupee extraction and deterministic output. The legacy InvoiceDocument is removed.
This commit is contained in:
@@ -10,6 +10,11 @@ import { pdfFlags } from "./pdfProps";
|
||||
import { Cell, Row } from "./Row";
|
||||
import { Txt } from "./Txt";
|
||||
|
||||
/** A cell: one string, or several (one Txt each, stacked), for multi-line values. */
|
||||
export type TableCellValue = string | string[];
|
||||
|
||||
const cellParts = (v: TableCellValue): string[] => (Array.isArray(v) ? (v.length ? v : [""]) : [v]);
|
||||
|
||||
export interface TableRoles {
|
||||
head: RoleName | TypeToken;
|
||||
body: RoleName | TypeToken;
|
||||
@@ -22,6 +27,8 @@ export interface TableLook {
|
||||
padY?: number;
|
||||
/** Bottom border of each row in pt (counted in the row height). Default 0. */
|
||||
border?: number;
|
||||
/** Extra right padding per column, pt (e.g. keeps description text off the next column). Counted in widths. */
|
||||
padRight?: number[];
|
||||
faceOverrides?: Readonly<Record<string, string>>;
|
||||
}
|
||||
|
||||
@@ -30,8 +37,8 @@ export interface ItemsTablePlanInput extends TableLook {
|
||||
gap: number;
|
||||
/** Table width in pt (the content width of the page, or of the cell holding the table). */
|
||||
width: number;
|
||||
header: string[];
|
||||
rows: string[][];
|
||||
header: TableCellValue[];
|
||||
rows: TableCellValue[][];
|
||||
measurer: Measurer;
|
||||
roles: TableRoles;
|
||||
/** Height of whatever follows the last row and must stay with it (the totals). */
|
||||
@@ -49,20 +56,23 @@ export interface ItemsTablePlan {
|
||||
rowLines: number[];
|
||||
}
|
||||
|
||||
function cellHeight(m: Measurer, role: RoleName | TypeToken, text: string, inner: number, look: TableLook): { lines: number; height: number } {
|
||||
function cellHeight(m: Measurer, role: RoleName | TypeToken, value: TableCellValue, inner: number, look: TableLook): { lines: number; height: number } {
|
||||
const t = roleToken(role);
|
||||
if (t.leading === undefined) throw new PlanError("table roles need a leading", "invalid-plan");
|
||||
if (/[\n\t]/.test(text)) throw new PlanError("table cells must not contain \\n or \\t", "invalid-plan");
|
||||
const lines = Math.max(
|
||||
1,
|
||||
m.wrapLines(drawnText(t, text), inner, {
|
||||
face: resolveFace(t.face, look.faceOverrides),
|
||||
weight: t.weight,
|
||||
italic: t.italic ?? false,
|
||||
size: t.size,
|
||||
letterSpacing: (t.tracking ?? 0) * t.size,
|
||||
}).length,
|
||||
);
|
||||
let lines = 0;
|
||||
for (const text of cellParts(value)) {
|
||||
if (/[\n\t]/.test(text)) throw new PlanError("table cells must not contain \\n or \\t", "invalid-plan");
|
||||
lines += Math.max(
|
||||
1,
|
||||
m.wrapLines(drawnText(t, text), inner, {
|
||||
face: resolveFace(t.face, look.faceOverrides),
|
||||
weight: t.weight,
|
||||
italic: t.italic ?? false,
|
||||
size: t.size,
|
||||
letterSpacing: (t.tracking ?? 0) * t.size,
|
||||
}).length,
|
||||
);
|
||||
}
|
||||
return { lines, height: lines * t.leading };
|
||||
}
|
||||
|
||||
@@ -84,8 +94,8 @@ export function planItemsTable(input: ItemsTablePlanInput): ItemsTablePlan {
|
||||
for (const r of rows) if (r.length !== columns.length) throw new PlanError(`row has ${r.length} cells for ${columns.length} columns`, "invalid-plan");
|
||||
|
||||
const { widths } = colsDetailed(width, columns, gap);
|
||||
const inner = widths.map((w) => Math.max(0, w - 2 * padX));
|
||||
const heightOf = (role: RoleName | TypeToken, cells: string[]) => {
|
||||
const inner = widths.map((w, i) => Math.max(0, w - 2 * padX - (input.padRight?.[i] ?? 0)));
|
||||
const heightOf = (role: RoleName | TypeToken, cells: TableCellValue[]) => {
|
||||
const hs = cells.map((c, i) => cellHeight(measurer, role, c, inner[i], input));
|
||||
return { lines: Math.max(...hs.map((h) => h.lines)), height: Math.max(...hs.map((h) => h.height)) + 2 * padY + border };
|
||||
};
|
||||
@@ -110,8 +120,8 @@ export interface TableRowsProps extends TableLook {
|
||||
columns: ColSpec[];
|
||||
gap: number;
|
||||
width: number;
|
||||
header: string[];
|
||||
rows: string[][];
|
||||
header: TableCellValue[];
|
||||
rows: TableCellValue[][];
|
||||
roles: TableRoles;
|
||||
/** Per column; default left. */
|
||||
align?: Array<"left" | "right" | "center">;
|
||||
@@ -122,6 +132,8 @@ export interface TableRowsProps extends TableLook {
|
||||
headerStyle?: Record<string, unknown>;
|
||||
rowStyle?: Record<string, unknown>;
|
||||
borderColor?: string;
|
||||
/** Text colour of the header cells. */
|
||||
headColor?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,7 +141,7 @@ export interface TableRowsProps extends TableLook {
|
||||
* `wrap={false}` row per item. The container has no padding or border; those go on the rows, because
|
||||
* the repeated header is positioned against the container's top.
|
||||
*/
|
||||
export function TableRows({ columns, gap, width, header, rows, roles, align = [], tailH, tail, headerStyle, rowStyle, borderColor, ...look }: TableRowsProps) {
|
||||
export function TableRows({ columns, gap, width, header, rows, roles, align = [], tailH, tail, headerStyle, rowStyle, borderColor, headColor, ...look }: TableRowsProps) {
|
||||
const padX = look.padX ?? 0;
|
||||
const padY = look.padY ?? 4;
|
||||
const border = look.border ?? 0;
|
||||
@@ -138,12 +150,14 @@ export function TableRows({ columns, gap, width, header, rows, roles, align = []
|
||||
...(border ? { borderBottomWidth: border, borderBottomColor: borderColor ?? "#000000", borderBottomStyle: "solid" } : {}),
|
||||
...extra,
|
||||
});
|
||||
const cells = (role: RoleName | TypeToken, values: string[]) =>
|
||||
const cells = (role: RoleName | TypeToken, values: TableCellValue[], color?: string) =>
|
||||
values.map((v, i) => (
|
||||
<Cell key={i} padding={{ top: padY, bottom: padY, left: padX, right: padX }}>
|
||||
<Txt role={role} width={Math.max(0, widths[i] - 2 * padX)} align={align[i]} faceOverrides={look.faceOverrides}>
|
||||
{v}
|
||||
</Txt>
|
||||
<Cell key={i} padding={{ top: padY, bottom: padY, left: padX, right: padX + (look.padRight?.[i] ?? 0) }}>
|
||||
{cellParts(v).map((part, li) => (
|
||||
<Txt key={li} role={role} color={color} width={Math.max(0, widths[i] - 2 * padX - (look.padRight?.[i] ?? 0))} align={align[i]} faceOverrides={look.faceOverrides}>
|
||||
{part}
|
||||
</Txt>
|
||||
))}
|
||||
</Cell>
|
||||
));
|
||||
|
||||
@@ -151,7 +165,7 @@ export function TableRows({ columns, gap, width, header, rows, roles, align = []
|
||||
<AvailableWidthProvider width={width}>
|
||||
<View>
|
||||
<Row id={TABLE_HEAD_ID} widths={columns} gap={gap} width={width} fixed style={cellStyle(headerStyle)}>
|
||||
{cells(roles.head, header)}
|
||||
{cells(roles.head, header, headColor)}
|
||||
</Row>
|
||||
{rows.map((r, i) => (
|
||||
<Row
|
||||
|
||||
Reference in New Issue
Block a user