/* ObsiGate — XLSX A1 reference helpers (#154-A5). * * Pure functions (no DOM, no i18n): safe to unit-test in Node and shared by the * viewer. Extracted from frontend/js/viewer.js without behaviour change. */ /** Parse an A1 reference ("B12") into its 1-based (row, col) parts. */ export function parseRef(ref) { const m = /^([A-Z]+)(\d+)$/.exec(ref || ""); if (!m) return null; let col = 0; for (const ch of m[1]) col = col * 26 + (ch.charCodeAt(0) - 64); return { row: Number(m[2]), col }; } /** Column number (1-based) → letters ("A", "Z", "AA"). */ export function columnName(col) { let name = ""; while (col > 0) { const rem = (col - 1) % 26; name = String.fromCharCode(65 + rem) + name; col = Math.floor((col - 1) / 26); } return name; } // #184 — cell lookup cache. `findTd` used to run a `querySelector` per call; // painting the grid out to A → Z × 1000 rows means a single "select column" // asks for 26 000 cells, which was quadratic on the DOM. The index is built // once per panel and rebuilt on demand whenever an entry no longer matches its // own `data-cell` (the signature of a renumbered grid after a structure op). const _cellIndex = new WeakMap(); function buildCellIndex(panel) { const map = new Map(); panel.querySelectorAll("td[data-cell]").forEach((td) => { map.set(td.dataset.cell, td); }); _cellIndex.set(panel, map); return map; } /** Drop a panel's lookup cache (bulk DOM changes). */ export function invalidateCellIndex(panel) { if (panel) _cellIndex.delete(panel); } /** Find a rendered cell inside a sheet panel by (row, col). */ export function findTd(panel, row, col) { const ref = `${columnName(col)}${row}`; const cached = _cellIndex.get(panel); const hit = cached && cached.get(ref); if (hit && hit.dataset.cell === ref) return hit; // Miss, or a stale entry (the row was renumbered): rebuild once and retry. const fresh = buildCellIndex(panel).get(ref); return fresh && fresh.dataset.cell === ref ? fresh : null; } /** Sheet prefix of a range ref ("'Mon onglet'!$A$1" → "Mon onglet"), or null. */ export function sheetOfRef(ref) { const s = String(ref || ""); if (!s.includes("!")) return null; return s.split("!")[0].replace(/^'|'$/g, ""); } /** First cell of a range ref ("Data!$A$1:$B$5" → "A1"), or null. */ export function firstCellOfRange(ref) { const s = String(ref || ""); const body = s.includes("!") ? s.split("!").pop() : s; const m = /([A-Za-z]+)\$?(\d+)/.exec(body); return m ? `${m[1].toUpperCase()}${m[2]}` : null; }