import { LabelTextElement, PlaceholderKey } from './label-element.types'; /** * Text as the label check compares it: NFKC, lower case, every character that * is not a letter or digit becomes a space, runs of spaces collapse. So * "Bühlweg 10, 86450 Altenmünster" also matches "Bühlweg 10 · 86450\nAltenmünster". */ export function normalizeForCheck(text: string): string { return ( text .normalize('NFKC') .toLowerCase() .replace(/[^\p{L}\p{N}]+/gu, ' ') // "250g" -> "250 g": a number glued to its unit .replace(/(\p{N})(?=\p{L})/gu, '$1 ') .trim() ); } // Elements whose tops are at most this far apart count as one row. export const ROW_TOLERANCE_MM = 1; export interface ResolvedText { element: LabelTextElement; text: string; } /** * The text each element prints for one bottling (placeholders filled in), * top first, then left. */ export function resolvedTexts( elements: readonly LabelTextElement[], values: Record, ): ResolvedText[] { const resolved = elements .map((element) => ({ element, text: element.placeholderKey ? values[element.placeholderKey] : element.text, })) .sort((a, b) => a.element.y - b.element.y); // Reading order comes from the geometry only, never from the order of the // elements array: rows (tops within the tolerance of the row's first top), // each row from left to right. const rows: ResolvedText[][] = []; let rowTop = Number.NEGATIVE_INFINITY; for (const entry of resolved) { if (entry.element.y - rowTop > ROW_TOLERANCE_MM) { rows.push([]); rowTop = entry.element.y; } rows[rows.length - 1].push(entry); } return rows.flatMap((row) => row.sort((a, b) => a.element.x - b.element.x)); } /** * Does the lot number already start with the lot marker "L" (as its own token * or glued to digits, "L-001", "L2026")? Then no further "L" is needed. */ export function lotStartsWithL(lotNumber: string): boolean { return /^l( |\d|$)/.test(normalizeForCheck(lotNumber)); }