String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
+const letter = (n) => { let s = ""; for (let i = n + 1; i > 0; i = Math.floor((i - 1) / 26)) s = String.fromCharCode(65 + ((i - 1) % 26)) + s; return s; };
+
+/**
+ * One sheet as a full HTML page: title bar, formula bar, grid with column
+ * letters and row numbers, and the sheet tabs with the active one selected.
+ */
+export function sheetPage(book, fileName, active, { maxRows = 24, width = 1280 } = {}) {
+ const sh = book.sheets[active];
+ const ncols = Math.max(sh.widths.length, ...sh.rows.map((r) => r.cells.length ? r.cells[r.cells.length - 1].col + 1 : 0));
+ const px = (i) => Math.round((sh.widths[i] || 8.43) * 7 + 5);
+ // Fill the window with empty columns the way Excel does.
+ const colsPx = [];
+ let used = 42;
+ for (let i = 0; i < ncols || used < width; i += 1) { const w = i < ncols ? px(i) : 69; colsPx.push(w); used += w; }
+ const rows = sh.rows.slice(0, maxRows);
+ const blank = 3;
+
+ const td = (c, r, i) => {
+ const st = c?.style || {};
+ const css = [];
+ let fill = st.fill;
+ if (!fill && sh.table && r > 0 && r % 2 === 1) fill = "#DDEBF7";
+ if (fill) css.push(`background:${fill}`);
+ if (st.color) css.push(`color:${st.color}`);
+ if (st.bold) css.push("font-weight:700");
+ if (st.center) css.push("text-align:center");
+ else if (c && c.kind !== "s") css.push(`text-align:${c.kind === "b" ? "center" : "right"}`);
+ if (st.border) css.push("border-color:#bfbfbf");
+ const filter = r === 0 && sh.filter && i < ncols ? '▾' : "";
+ const cls = [r === 0 && st.wrap ? "wr" : "", filter ? "flt" : ""].filter(Boolean).join(" ");
+ return `| ${c ? esc(c.text) : ""}${filter} | `;
+ };
+
+ const body = rows.map((row, r) => {
+ const byCol = new Map(row.cells.map((c) => [c.col, c]));
+ return `| ${r + 1} | `
+ + colsPx.map((_, i) => td(byCol.get(i), r, i)).join("") + "
";
+ }).join("") + Array.from({ length: blank }, (_, k) =>
+ `| ${rows.length + k + 1} | ${colsPx.map(() => " | ").join("")}
`).join("");
+
+ const first = rows[0]?.cells[0]?.text ?? "";
+ const tabs = book.sheets.map((s, i) =>
+ `${esc(s.name)}`).join("");
+
+ return `
+X${esc(fileName)}— Excel
+A1fx${esc(first)}
+${colsPx.map((w) => ``).join("")}
+ | ${colsPx.map((_, i) => `${letter(i)} | `).join("")}
+${body}
+${tabs}
+`;
+}