Spreadsheet
Qwen 3.6 27B MTP GGUF IQ4 NL · typescript

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 62,889 bytes · SHA-256 52e3d8bf4488
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Spreadsheet</title>
<style>
/* ── css: spreadsheet ── */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
font-family: 'Google Sans', 'Roboto', 'Segoe UI', Arial, sans-serif;
font-size: 13px;
color: #202124;
background: #ffffff;
overflow: hidden;
}
#spreadsheet-container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
}
/* Toolbar */
#toolbar {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: #f1f3f4;
border-bottom: 1px solid #dadce0;
min-height: 36px;
flex-shrink: 0;
}
#toolbar button {
background: #ffffff;
border: 1px solid #dadce0;
border-radius: 4px;
padding: 4px 10px;
font-size: 12px;
color: #3c4043;
cursor: pointer;
white-space: nowrap;
}
#toolbar button:hover {
background: #e8eaed;
border-color: #c0c0c0;
}
.toolbar-separator {
width: 1px;
height: 20px;
background: #dadce0;
margin: 0 8px;
}
.toolbar-hint {
font-size: 11px;
color: #5f6368;
margin-left: auto;
}
/* Formula bar */
#formula-bar {
display: flex;
align-items: center;
padding: 4px 8px;
border-bottom: 1px solid #dadce0;
background: #ffffff;
flex-shrink: 0;
height: 36px;
}
#cell-ref {
display: flex;
align-items: center;
justify-content: center;
width: 64px;
height: 28px;
border: 1px solid #dadce0;
border-right: none;
border-radius: 4px 0 0 4px;
background: #f1f3f4;
font-size: 12px;
font-weight: 600;
color: #3c4043;
flex-shrink: 0;
cursor: default;
}
#formula-input {
flex: 1;
height: 28px;
border: 1px solid #dadce0;
border-left: none;
border-radius: 0 4px 4px 0;
padding: 0 8px;
font-size: 13px;
font-family: inherit;
color: #202124;
outline: none;
}
#formula-input:focus {
border-color: #1a73e8;
}
/* Grid area */
#grid-area {
flex: 1;
overflow: auto;
position: relative;
}
/* Table styles */
.sheet-table {
border-collapse: collapse;
table-layout: fixed;
width: max-content;
}
.sheet-table th,
.sheet-table td {
border-right: 1px solid #e0e0e0;
border-bottom: 1px solid #e0e0e0;
padding: 0 6px;
font-size: 13px;
line-height: 22px;
height: 24px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
}
/* Column headers */
.col-header {
background: #f5f5f5;
color: #333;
font-weight: 600;
font-size: 12px;
text-align: center;
position: sticky;
top: 0;
z-index: 3;
border-bottom: 2px solid #c0c0c0;
border-right: 1px solid #e0e0e0;
user-select: none;
cursor: default;
}
/* Row headers */
.row-header {
background: #f5f5f5;
color: #333;
font-weight: 600;
font-size: 12px;
text-align: center;
position: sticky;
left: 0;
z-index: 2;
border-right: 2px solid #c0c0c0;
user-select: none;
cursor: default;
}
/* Corner header */
.corner-header {
background: #f5f5f5;
position: sticky;
top: 0;
left: 0;
z-index: 4;
border-bottom: 2px solid #c0c0c0;
border-right: 2px solid #c0c0c0;
user-select: none;
}
/* Cell styles */
.cell {
background: #ffffff;
color: #202124;
cursor: cell;
position: relative;
}
.cell:hover {
outline: 1px solid #1a73e8;
outline-offset: -1px;
}
.cell.selected {
outline: 2px solid #1a73e8;
outline-offset: -1px;
z-index: 1;
}
.cell.cell-number {
text-align: right;
}
.cell.cell-text {
text-align: left;
}
.cell.cell-error {
color: #d32f2f;
font-size: 11px;
font-style: italic;
}
.cell.editing {
padding: 0;
}
.cell-edit-input {
width: 100%;
height: 100%;
border: none;
outline: 2px solid #1a73e8;
padding: 0 6px;
font-size: 13px;
font-family: inherit;
color: #202124;
background: #fffde7;
}
/* Context menu */
.context-menu {
position: fixed;
background: #ffffff;
border: 1px solid #dadce0;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 1000;
min-width: 160px;
padding: 4px 0;
}
.context-menu-item {
padding: 8px 16px;
cursor: pointer;
font-size: 13px;
color: #202124;
}
.context-menu-item:hover {
background: #f1f3f4;
}
</style>
</head>
<body>
<div id="spreadsheet-container">
<!-- Toolbar -->
<div id="toolbar">
<button id="btn-insert-row" title="Insert row above selection">+ Row</button>
<button id="btn-delete-row" title="Delete selected row">- Row</button>
<button id="btn-insert-col" title="Insert column before selection">+ Col</button>
<button id="btn-delete-col" title="Delete selected column">- Col</button>
<div class="toolbar-separator"></div>
<span class="toolbar-hint">Right-click headers for more options • Delete to clear • Enter/Tab to navigate</span>
</div>
<!-- Formula bar -->
<div id="formula-bar">
<span id="cell-ref">A1</span>
<input id="formula-input" type="text" spellcheck="false" autocomplete="off">
</div>
<!-- Grid area -->
<div id="grid-area"></div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./sheet":"src/sheet.ts","./renderer":"src/renderer.ts","./types":"src/types.ts"},"src/parser.ts":{"./types":"src/types.ts"},"src/sheet.ts":{"./types":"src/types.ts","./parser":"src/parser.ts"},"src/renderer.ts":{"./sheet":"src/sheet.ts","./types":"src/types.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const sheet_1 = require("./sheet");
const renderer_1 = require("./renderer");
const types_1 = require("./types");
function main() {
const sheet = new sheet_1.Sheet();
const depGraph = new sheet_1.DependencyGraph();
// Load saved data
const renderer = new renderer_1.Renderer(sheet, depGraph);
const loaded = renderer.load();
if (loaded) {
depGraph.updateAll(sheet);
renderer.fullRecalculate();
}
// Wire up grid keyboard events
const gridArea = document.getElementById('grid-area');
gridArea.addEventListener('keydown', (e) => renderer.handleGridKeydown(e));
// Wire up formula bar
const formulaInput = document.getElementById('formula-input');
formulaInput.addEventListener('keydown', (e) => renderer.handleFormulaBarKeydown(e));
formulaInput.addEventListener('input', () => renderer.handleFormulaBarInput());
// Focus formula bar on click
const cellRef = document.getElementById('cell-ref');
cellRef.addEventListener('click', () => {
formulaInput.focus();
});
// Wire up toolbar buttons
const btnInsertRow = document.getElementById('btn-insert-row');
const btnDeleteRow = document.getElementById('btn-delete-row');
const btnInsertCol = document.getElementById('btn-insert-col');
const btnDeleteCol = document.getElementById('btn-delete-col');
btnInsertRow.addEventListener('click', () => {
renderer.insertRow(renderer.selection.row);
// Adjust selection if needed
if (renderer.selection.row >= renderer.sheet.numRows) {
renderer.selection.row = renderer.sheet.numRows - 1;
}
renderer.highlightCell();
renderer.updateFormulaBar();
});
btnDeleteRow.addEventListener('click', () => {
renderer.deleteRow(renderer.selection.row);
if (renderer.selection.row >= renderer.sheet.numRows) {
renderer.selection.row = Math.max(0, renderer.sheet.numRows - 1);
}
renderer.highlightCell();
renderer.updateFormulaBar();
});
btnInsertCol.addEventListener('click', () => {
renderer.insertCol(renderer.selection.col);
if (renderer.selection.col >= renderer.sheet.numCols) {
renderer.selection.col = renderer.sheet.numCols - 1;
}
renderer.highlightCell();
renderer.updateFormulaBar();
});
btnDeleteCol.addEventListener('click', () => {
renderer.deleteCol(renderer.selection.col);
if (renderer.selection.col >= renderer.sheet.numCols) {
renderer.selection.col = Math.max(0, renderer.sheet.numCols - 1);
}
renderer.highlightCell();
renderer.updateFormulaBar();
});
// Initial cell selection highlight
renderer.highlightCell();
renderer.updateFormulaBar();
// Save on page unload
window.addEventListener('beforeunload', () => renderer.save());
// Expose internals for testing
const expose = window;
expose.__sheet = sheet;
expose.__renderer = renderer;
expose.__depGraph = depGraph;
expose.__recalculate = sheet_1.recalculate;
expose.__evaluateCell = sheet_1.evaluateCell;
expose.__addrKey = types_1.addrKey;
}
main();
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.addrKey = addrKey;
exports.parseAddrKey = parseAddrKey;
exports.colName = colName;
exports.parseColName = parseColName;
exports.cellLabel = cellLabel;
function addrKey(col, row) {
return col + ',' + row;
}
function parseAddrKey(key) {
const idx = key.indexOf(',');
return { col: Number(key.slice(0, idx)), row: Number(key.slice(idx + 1)) };
}
function colName(col) {
return String.fromCharCode(65 + col);
}
function parseColName(s) {
const c = s.toUpperCase();
if (c.length === 1 && c >= 'A' && c <= 'Z')
return c.charCodeAt(0) - 65;
return null;
}
function cellLabel(col, row) {
return colName(col) + (row + 1);
}
};
// ── module: src/parser.ts ──
__mods["src/parser.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.tokenize = tokenize;
exports.parseFormula = parseFormula;
const types_1 = require("./types");
function tokenize(input) {
const tokens = [];
let i = 0;
while (i < input.length) {
const c = input[i];
// Skip whitespace
if (c === ' ' || c === '\t' || c === '\n') {
i++;
continue;
}
// Numbers (including decimal point)
if (c >= '0' && c <= '9' || (c === '.' && i + 1 < input.length && input[i + 1] >= '0' && input[i + 1] <= '9')) {
let s = '';
while (i < input.length && (input[i] >= '0' && input[i] <= '9' || input[i] === '.')) {
s += input[i];
i++;
}
tokens.push({ type: 'NUMBER', value: s, pos: tokens.length });
continue;
}
// Cell reference or function name (letters followed by optional digits for cell refs)
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) {
let s = '';
while (i < input.length && ((input[i] >= 'A' && input[i] <= 'Z') || (input[i] >= 'a' && input[i] <= 'z') || (input[i] >= '0' && input[i] <= '9'))) {
s += input[i];
i++;
}
// Check if it's a cell reference (e.g. A1, B7)
const upper = s.toUpperCase();
const lastChar = upper[upper.length - 1];
// Could be a cell ref: letter(s) then digits
if (upper.length >= 2 && lastChar >= '0' && lastChar <= '9') {
tokens.push({ type: 'CELL', value: upper, pos: tokens.length });
}
else {
tokens.push({ type: 'FUNC', value: upper, pos: tokens.length });
}
continue;
}
// Operators and punctuation
if (c === '+') {
tokens.push({ type: 'PLUS', value: '+', pos: tokens.length });
i++;
continue;
}
if (c === '-') {
tokens.push({ type: 'MINUS', value: '-', pos: tokens.length });
i++;
continue;
}
if (c === '*') {
tokens.push({ type: 'STAR', value: '*', pos: tokens.length });
i++;
continue;
}
if (c === '/') {
tokens.push({ type: 'SLASH', value: '/', pos: tokens.length });
i++;
continue;
}
if (c === '(') {
tokens.push({ type: 'LPAREN', value: '(', pos: tokens.length });
i++;
continue;
}
if (c === ')') {
tokens.push({ type: 'RPAREN', value: ')', pos: tokens.length });
i++;
continue;
}
if (c === ',') {
tokens.push({ type: 'COMMA', value: ',', pos: tokens.length });
i++;
continue;
}
if (c === ':') {
tokens.push({ type: 'RANGE', value: ':', pos: tokens.length });
i++;
continue;
}
// Unknown character - skip it
i++;
}
tokens.push({ type: 'EOF', value: '', pos: tokens.length });
return tokens;
}
function peek(p) { return p.tokens[p.pos]; }
function consume(p, type) {
const t = p.tokens[p.pos];
if (t.type !== type)
throw new Error(`Unexpected token '${t.value}' (expected ${type})`);
p.pos++;
return t;
}
function parseCellRef(token) {
const s = token.value;
// Find where letters end and digits begin
let ci = 0;
while (ci < s.length && s[ci] >= 'A' && s[ci] <= 'Z')
ci++;
const letters = s.slice(0, ci);
const digits = s.slice(ci);
const col = (0, types_1.parseColName)(letters);
const row = Number(digits) - 1;
if (col === null || isNaN(row) || row < 0)
return null;
return { col, row };
}
function parseExpr(p) {
let left = parseTerm(p);
while (peek(p).type === 'PLUS' || peek(p).type === 'MINUS') {
const op = consume(p, peek(p).type);
const right = parseTerm(p);
left = { type: 'binary', op: op.value, left, right };
}
return left;
}
function parseTerm(p) {
let left = parseUnary(p);
while (peek(p).type === 'STAR' || peek(p).type === 'SLASH') {
const op = consume(p, peek(p).type);
const right = parseUnary(p);
left = { type: 'binary', op: op.value, left, right };
}
return left;
}
function parseUnary(p) {
if (peek(p).type === 'MINUS') {
consume(p, 'MINUS');
const operand = parseUnary(p);
return { type: 'unary', op: '-', operand };
}
if (peek(p).type === 'PLUS') {
consume(p, 'PLUS');
return parseUnary(p);
}
return parsePrimary(p);
}
function parsePrimary(p) {
const t = peek(p);
// Number literal
if (t.type === 'NUMBER') {
consume(p, 'NUMBER');
return { type: 'number', value: Number(t.value) };
}
// Parenthesized expression
if (t.type === 'LPAREN') {
consume(p, 'LPAREN');
const expr = parseExpr(p);
consume(p, 'RPAREN');
return expr;
}
// Function call
if (t.type === 'FUNC') {
const name = consume(p, 'FUNC').value;
consume(p, 'LPAREN');
const args = [];
if (peek(p).type !== 'RPAREN') {
args.push(parseExpr(p));
while (peek(p).type === 'COMMA') {
consume(p, 'COMMA');
args.push(parseExpr(p));
}
}
consume(p, 'RPAREN');
return { type: 'func', name, args };
}
// Cell reference (possibly with range)
if (t.type === 'CELL') {
const addr = parseCellRef(consume(p, 'CELL'));
if (!addr)
throw new Error(`Invalid cell reference '${t.value}'`);
if (addr.col > p.col || addr.row > p.row || addr.row < 0)
throw new Error(`Invalid cell reference '${t.value}'`);
// Check for range operator
if (peek(p).type === 'RANGE') {
consume(p, 'RANGE');
const endToken = consume(p, 'CELL');
const end = parseCellRef(endToken);
if (!end)
throw new Error(`Invalid cell reference '${endToken.value}'`);
if (end.col > p.col || end.row > p.row || end.row < 0)
throw new Error(`Invalid cell reference '${endToken.value}'`);
// Normalize range so start <= end
const startCol = Math.min(addr.col, end.col);
const startRow = Math.min(addr.row, end.row);
const endCol = Math.max(addr.col, end.col);
const endRow = Math.max(addr.row, end.row);
return { type: 'range', start: { col: startCol, row: startRow }, end: { col: endCol, row: endRow } };
}
return { type: 'cell', col: addr.col, row: addr.row };
}
throw new Error(`Unexpected token '${t.value}'`);
}
function parseFormula(input, maxCol, maxRow) {
const tokens = tokenize(input);
const p = { tokens, pos: 0, col: maxCol, row: maxRow };
const expr = parseExpr(p);
if (peek(p).type !== 'EOF')
throw new Error(`Unexpected token '${peek(p).value}'`);
return expr;
}
};
// ── module: src/sheet.ts ──
__mods["src/sheet.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DependencyGraph = exports.Sheet = exports.COLORS = exports.HEADER_HEIGHT = exports.HEADER_WIDTH = exports.CELL_HEIGHT = exports.CELL_WIDTH = exports.NUM_ROWS = exports.NUM_COLS = void 0;
exports.evaluateCell = evaluateCell;
exports.recalculate = recalculate;
const types_1 = require("./types");
const parser_1 = require("./parser");
// ---- Tunable Constants ----
exports.NUM_COLS = 26;
exports.NUM_ROWS = 100;
exports.CELL_WIDTH = 100;
exports.CELL_HEIGHT = 24;
exports.HEADER_WIDTH = 48;
exports.HEADER_HEIGHT = 26;
exports.COLORS = {
bg: '#ffffff',
gridLine: '#e0e0e0',
headerBg: '#f5f5f5',
headerText: '#333333',
headerBorder: '#c0c0c0',
selection: '#1a73e8',
cellText: '#202124',
errorColor: '#d32f2f',
formulaBarBg: '#ffffff',
formulaBarBorder: '#dadce0',
toolbarBg: '#f1f3f4',
toolbarBorder: '#dadce0',
};
// ---- Sheet State ----
class Sheet {
constructor() {
this.cells = new Map();
this.numCols = exports.NUM_COLS;
this.numRows = exports.NUM_ROWS;
}
getCell(col, row) {
if (col < 0 || col >= this.numCols || row < 0 || row >= this.numRows)
return undefined;
return this.cells.get((0, types_1.addrKey)(col, row));
}
setCellRaw(col, row, raw) {
const key = (0, types_1.addrKey)(col, row);
if (raw === '') {
this.cells.delete(key);
}
else {
this.cells.set(key, { raw, computed: { type: 'null' } });
}
}
// ---- Row/Column Insert/Delete with formula rewriting ----
rewriteCellRaw(cell, colShift, rowShift) {
const raw = cell.raw;
if (!raw.startsWith('='))
return raw;
// We need to rewrite cell references in the raw formula
// Strategy: tokenize, then rebuild the formula string with shifted references
const tokens = (0, parser_1.tokenize)(raw.slice(1)); // skip '='
let result = '=';
for (const t of tokens) {
if (t.type === 'CELL') {
const addr = this.parseCellFromToken(t.value);
if (addr) {
const newCol = colShift(addr.col);
const newRow = rowShift(addr.row);
if (newCol >= 0 && newRow >= 0) {
result += (0, types_1.cellLabel)(newCol, newRow);
}
else {
// Reference would be out of bounds - keep original (will be #REF!)
result += t.value;
}
}
else {
result += t.value;
}
}
else {
result += t.value;
}
}
return result;
}
parseCellFromToken(s) {
let ci = 0;
while (ci < s.length && s[ci] >= 'A' && s[ci] <= 'Z')
ci++;
if (ci === 0)
return null;
const letters = s.slice(0, ci);
const digits = s.slice(ci);
if (digits.length === 0)
return null;
const col = letters.charCodeAt(0) - 65;
const row = Number(digits) - 1;
if (isNaN(row) || row < 0)
return null;
return { col, row };
}
insertRow(before) {
if (before < 0 || before > this.numRows)
return;
// Rewrite all formulas to shift row references
const newCells = new Map();
for (const [key, cell] of this.cells) {
const parts = key.split(',');
const c = Number(parts[0]);
const r = Number(parts[1]);
const newRow = r >= before ? r + 1 : r;
const newRaw = cell.raw.startsWith('=')
? this.rewriteCellRaw(cell, (col) => col, (row) => row >= before ? row + 1 : row)
: cell.raw;
newCells.set((0, types_1.addrKey)(c, newRow), { raw: newRaw, computed: { type: 'null' } });
}
this.cells = newCells;
this.numRows++;
}
deleteRow(row) {
if (row < 0 || row >= this.numRows)
return;
// Rewrite formulas first, then remove cells in that row
const newCells = new Map();
for (const [key, cell] of this.cells) {
const parts = key.split(',');
const c = Number(parts[0]);
const r = Number(parts[1]);
if (r === row)
continue; // remove this cell
const newRow = r > row ? r - 1 : r;
const newRaw = cell.raw.startsWith('=')
? this.rewriteCellRaw(cell, (col) => col, (rowIdx) => rowIdx > row ? rowIdx - 1 : rowIdx)
: cell.raw;
newCells.set((0, types_1.addrKey)(c, newRow), { raw: newRaw, computed: { type: 'null' } });
}
this.cells = newCells;
this.numRows--;
}
insertCol(before) {
if (before < 0 || before > this.numCols)
return;
const newCells = new Map();
for (const [key, cell] of this.cells) {
const parts = key.split(',');
const c = Number(parts[0]);
const r = Number(parts[1]);
const newCol = c >= before ? c + 1 : c;
const newRaw = cell.raw.startsWith('=')
? this.rewriteCellRaw(cell, (col) => col >= before ? col + 1 : col, (row) => row)
: cell.raw;
newCells.set((0, types_1.addrKey)(newCol, r), { raw: newRaw, computed: { type: 'null' } });
}
this.cells = newCells;
this.numCols++;
}
deleteCol(col) {
if (col < 0 || col >= this.numCols)
return;
const newCells = new Map();
for (const [key, cell] of this.cells) {
const parts = key.split(',');
const c = Number(parts[0]);
const r = Number(parts[1]);
if (c === col)
continue;
const newCol = c > col ? c - 1 : c;
const newRaw = cell.raw.startsWith('=')
? this.rewriteCellRaw(cell, (colIdx) => colIdx > col ? colIdx - 1 : colIdx, (row) => row)
: cell.raw;
newCells.set((0, types_1.addrKey)(newCol, r), { raw: newRaw, computed: { type: 'null' } });
}
this.cells = newCells;
this.numCols--;
}
}
exports.Sheet = Sheet;
// ---- Formula Evaluator ----
function evalExpr(expr, sheet, visiting) {
switch (expr.type) {
case 'number':
return { type: 'number', value: expr.value };
case 'cell': {
const key = (0, types_1.addrKey)(expr.col, expr.row);
if (visiting.has(key))
return { type: 'error', value: '#CYCLE!' };
const cell = sheet.getCell(expr.col, expr.row);
if (!cell)
return { type: 'null' };
return cell.computed;
}
case 'range': {
// Collect values in range for use by functions
const values = [];
for (let r = expr.start.row; r <= expr.end.row; r++) {
for (let c = expr.start.col; c <= expr.end.col; c++) {
const cell = sheet.getCell(c, r);
if (cell)
values.push(cell.computed);
}
}
return { type: 'text', value: '' }; // ranges are only valid inside functions
}
case 'unary': {
if (expr.op === '-') {
const v = evalExpr(expr.operand, sheet, visiting);
if (v.type === 'number')
return { type: 'number', value: -v.value };
if (v.type === 'error')
return v;
return { type: 'error', value: '#ERR!' };
}
return evalExpr(expr.operand, sheet, visiting);
}
case 'binary': {
const left = evalExpr(expr.left, sheet, visiting);
if (left.type === 'error')
return left;
const right = evalExpr(expr.right, sheet, visiting);
if (right.type === 'error')
return right;
if (left.type !== 'number' || right.type !== 'number')
return { type: 'error', value: '#ERR!' };
switch (expr.op) {
case '+': return { type: 'number', value: left.value + right.value };
case '-': return { type: 'number', value: left.value - right.value };
case '*': return { type: 'number', value: left.value * right.value };
case '/':
if (right.value === 0)
return { type: 'error', value: '#DIV/0!' };
return { type: 'number', value: left.value / right.value };
}
return { type: 'error', value: '#ERR!' };
}
case 'func': {
return evalFunc(expr.name, expr.args, sheet, visiting);
}
}
}
function evalFunc(name, args, sheet, visiting) {
const upperName = name.toUpperCase();
// Collect all numeric values and cell values from args
const allValues = [];
const numbers = [];
for (const arg of args) {
if (arg.type === 'number') {
numbers.push(arg.value);
}
else if (arg.type === 'cell') {
const key = (0, types_1.addrKey)(arg.col, arg.row);
if (visiting.has(key))
return { type: 'error', value: '#CYCLE!' };
const cell = sheet.getCell(arg.col, arg.row);
if (cell) {
allValues.push(cell.computed);
if (cell.computed.type === 'number')
numbers.push(cell.computed.value);
}
}
else if (arg.type === 'range') {
for (let r = arg.start.row; r <= arg.end.row; r++) {
for (let c = arg.start.col; c <= arg.end.col; c++) {
const key = (0, types_1.addrKey)(c, r);
if (visiting.has(key))
return { type: 'error', value: '#CYCLE!' };
const cell = sheet.getCell(c, r);
if (cell) {
allValues.push(cell.computed);
if (cell.computed.type === 'number')
numbers.push(cell.computed.value);
}
}
}
}
else if (arg.type === 'binary' || arg.type === 'unary') {
const v = evalExpr(arg, sheet, visiting);
if (v.type === 'error')
return v;
if (v.type === 'number')
numbers.push(v.value);
}
}
const allNums = numbers;
switch (upperName) {
case 'SUM':
return allNums.length === 0 ? { type: 'number', value: 0 } : { type: 'number', value: allNums.reduce((a, b) => a + b, 0) };
case 'AVG':
case 'AVERAGE':
return allNums.length === 0 ? { type: 'error', value: '#DIV/0!' } : { type: 'number', value: allNums.reduce((a, b) => a + b, 0) / allNums.length };
case 'MIN':
return allNums.length === 0 ? { type: 'number', value: 0 } : { type: 'number', value: Math.min(...allNums) };
case 'MAX':
return allNums.length === 0 ? { type: 'number', value: 0 } : { type: 'number', value: Math.max(...allNums) };
case 'COUNT':
return { type: 'number', value: allNums.length };
default:
return { type: 'error', value: '#ERR!' };
}
}
function evaluateCell(col, row, sheet, visiting) {
const cell = sheet.getCell(col, row);
if (!cell)
return { type: 'null' };
const raw = cell.raw;
if (!raw.startsWith('=')) {
// Check if it's a number
const num = Number(raw);
if (!isNaN(num) && raw.trim() !== '') {
return { type: 'number', value: num };
}
return { type: 'text', value: raw };
}
// Parse and evaluate formula
try {
const expr = (0, parser_1.parseFormula)(raw.slice(1), sheet.numCols - 1, sheet.numRows - 1);
return evalExpr(expr, sheet, visiting);
}
catch (e) {
return { type: 'error', value: '#ERR!' };
}
}
// ---- Dependency Graph & Recalculation ----
function collectReferences(raw, maxCol, maxRow) {
if (!raw.startsWith('='))
return [];
const refs = new Set();
try {
const tokens = (0, parser_1.tokenize)(raw.slice(1));
for (const t of tokens) {
if (t.type === 'CELL') {
const addr = parseCellRefFromToken(t.value);
if (addr)
refs.add((0, types_1.addrKey)(addr.col, addr.row));
}
}
}
catch (e) {
// parse error - no valid references
}
return Array.from(refs);
}
function parseCellRefFromToken(s) {
let ci = 0;
while (ci < s.length && s[ci] >= 'A' && s[ci] <= 'Z')
ci++;
if (ci === 0)
return null;
const digits = s.slice(ci);
if (digits.length === 0)
return null;
const col = s.slice(0, ci).charCodeAt(0) - 65;
const row = Number(digits) - 1;
if (isNaN(row) || row < 0 || col < 0)
return null;
return { col, row };
}
class DependencyGraph {
constructor() {
// cell -> set of cells it depends on
this.dependsOn = new Map();
// cell -> set of cells that depend on it
this.dependents = new Map();
}
clear() {
this.dependsOn.clear();
this.dependents.clear();
}
updateCell(col, row, sheet) {
const key = (0, types_1.addrKey)(col, row);
const raw = sheet.getCell(col, row)?.raw || '';
const refs = collectReferences(raw, sheet.numCols - 1, sheet.numRows - 1);
// Remove old edges
const oldRefs = this.dependsOn.get(key);
if (oldRefs) {
for (const ref of oldRefs) {
const deps = this.dependents.get(ref);
if (deps)
deps.delete(key);
}
this.dependsOn.delete(key);
}
// Add new edges
if (refs.length > 0) {
this.dependsOn.set(key, new Set(refs));
for (const ref of refs) {
if (!this.dependents.has(ref))
this.dependents.set(ref, new Set());
this.dependents.get(ref).add(key);
}
}
}
updateAll(sheet) {
this.clear();
for (const [key, cell] of sheet.cells) {
const parts = key.split(',');
const col = Number(parts[0]);
const row = Number(parts[1]);
this.updateCell(col, row, sheet);
}
}
// Find all cells that need recalculation, in topological order (dependencies first)
// Starting from a set of changed cells
recalcOrder(changed) {
// BFS from changed cells through dependents, collecting all affected cells
// Then do topological sort on the subgraph
const affected = new Set();
const queue = [...changed];
const visited = new Set();
while (queue.length > 0) {
const current = queue.shift();
if (visited.has(current))
continue;
visited.add(current);
affected.add(current);
const deps = this.dependents.get(current);
if (deps) {
for (const dep of deps) {
if (!visited.has(dep))
queue.push(dep);
}
}
}
// Topological sort using Kahn's algorithm
// Count in-degree (only from within affected set)
const inDegree = new Map();
for (const cell of affected) {
const deps = this.dependsOn.get(cell);
let count = 0;
if (deps) {
for (const d of deps) {
if (affected.has(d))
count++;
}
}
inDegree.set(cell, count);
}
// Start with cells that have no dependencies in the affected set
const q = [];
for (const [cell, deg] of inDegree) {
if (deg === 0)
q.push(cell);
}
const result = [];
while (q.length > 0) {
const current = q.shift();
result.push(current);
const deps = this.dependents.get(current);
if (deps) {
for (const dep of deps) {
if (affected.has(dep)) {
const newDeg = (inDegree.get(dep) || 0) - 1;
inDegree.set(dep, newDeg);
if (newDeg === 0)
q.push(dep);
}
}
}
}
// If not all affected cells are in result, there's a cycle
if (result.length < affected.size) {
return null; // cycle detected
}
return result;
}
}
exports.DependencyGraph = DependencyGraph;
function recalculate(sheet, depGraph, changedKey) {
const changed = new Set();
changed.add(changedKey);
// Find all affected cells
const affected = new Set();
const q = [...changed];
const visited = new Set();
while (q.length > 0) {
const cur = q.shift();
if (visited.has(cur))
continue;
visited.add(cur);
affected.add(cur);
const deps = depGraph.dependents.get(cur);
if (deps)
for (const d of deps)
if (!visited.has(d))
q.push(d);
}
const recalcSet = new Set();
// Find cycle cells
const cycleCells = findCycleCells(depGraph, changed);
if (cycleCells.size > 0) {
// Mark cycle cells
for (const key of cycleCells) {
const { col, row } = (0, types_1.parseAddrKey)(key);
const cell = sheet.getCell(col, row);
if (cell) {
cell.computed = { type: 'error', value: '#CYCLE!' };
recalcSet.add(key);
}
}
// Recalculate non-cycle cells in topological order
const nonCycle = new Set();
for (const k of affected)
if (!cycleCells.has(k))
nonCycle.add(k);
const order = topologicalSort(depGraph, nonCycle);
for (const key of order) {
const { col, row } = (0, types_1.parseAddrKey)(key);
const cell = sheet.getCell(col, row);
if (!cell)
continue;
const visiting = new Set();
visiting.add(key);
cell.computed = evaluateCell(col, row, sheet, visiting);
recalcSet.add(key);
}
}
else {
// No cycle - recalculate all affected in topological order
const order = depGraph.recalcOrder(changed) || [];
for (const key of order) {
const { col, row } = (0, types_1.parseAddrKey)(key);
const cell = sheet.getCell(col, row);
if (!cell)
continue;
const visiting = new Set();
visiting.add(key);
cell.computed = evaluateCell(col, row, sheet, visiting);
recalcSet.add(key);
}
}
return recalcSet;
}
function topologicalSort(depGraph, nodes) {
const inDegree = new Map();
for (const cell of nodes) {
const deps = depGraph.dependsOn.get(cell);
let count = 0;
if (deps)
for (const d of deps)
if (nodes.has(d))
count++;
inDegree.set(cell, count);
}
const q = [];
for (const [cell, deg] of inDegree)
if (deg === 0)
q.push(cell);
const result = [];
while (q.length > 0) {
const cur = q.shift();
result.push(cur);
const deps = depGraph.dependents.get(cur);
if (deps) {
for (const dep of deps) {
if (nodes.has(dep)) {
const newDeg = (inDegree.get(dep) || 0) - 1;
inDegree.set(dep, newDeg);
if (newDeg === 0)
q.push(dep);
}
}
}
}
return result;
}
function findCycleCells(depGraph, changed) {
// Find all cells involved in cycles via DFS
const cycleCells = new Set();
// Collect all affected cells first
const affected = new Set();
const queue = [...changed];
const visited = new Set();
while (queue.length > 0) {
const current = queue.shift();
if (visited.has(current))
continue;
visited.add(current);
affected.add(current);
const deps = depGraph.dependents.get(current);
if (deps)
for (const dep of deps)
if (!visited.has(dep))
queue.push(dep);
}
// DFS from each affected cell to find cycles
for (const start of affected) {
const path = new Map(); // cell -> depth
const stack = [{ cell: start, isReturn: false }];
while (stack.length > 0) {
const top = stack[stack.length - 1];
const current = top.cell;
const isReturn = top.isReturn;
if (!isReturn) {
top.isReturn = true;
if (path.has(current)) {
// Found a cycle - mark all cells in the cycle
const cycleStart = path.get(current);
for (const [cell, depth] of path) {
if (depth >= cycleStart)
cycleCells.add(cell);
}
continue;
}
path.set(current, path.size);
const deps = depGraph.dependents.get(current);
if (deps) {
for (const dep of deps) {
if (affected.has(dep)) {
stack.push({ cell: dep, isReturn: false });
}
}
}
}
else {
path.delete(current);
stack.pop();
}
}
}
return cycleCells;
}
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = void 0;
const sheet_1 = require("./sheet");
const types_1 = require("./types");
class Renderer {
constructor(sheet, depGraph) {
this.selection = { col: 0, row: 0 };
this.editing = false;
this.editCell = null;
this.editInput = null;
// Cell DOM cache: Map<key, td>
this.cellElements = new Map();
this.sheet = sheet;
this.depGraph = depGraph;
this.container = document.getElementById('spreadsheet-container');
this.grid = document.getElementById('grid-area');
this.formulaInput = document.getElementById('formula-input');
this.cellRefDisplay = document.getElementById('cell-ref');
this.buildTable();
}
buildTable() {
this.grid.innerHTML = '';
this.cellElements.clear();
const table = document.createElement('table');
table.className = 'sheet-table';
this.table = table;
// Thead with corner cell + column headers
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
// Corner cell
const corner = document.createElement('th');
corner.className = 'corner-header';
corner.style.width = sheet_1.HEADER_WIDTH + 'px';
corner.style.height = sheet_1.HEADER_HEIGHT + 'px';
headerRow.appendChild(corner);
// Column headers
for (let c = 0; c < this.sheet.numCols; c++) {
const th = document.createElement('th');
th.className = 'col-header';
th.textContent = (0, types_1.colName)(c);
th.style.width = sheet_1.CELL_WIDTH + 'px';
th.style.height = sheet_1.HEADER_HEIGHT + 'px';
th.dataset.col = String(c);
th.addEventListener('contextmenu', (e) => {
e.preventDefault();
this.showColumnMenu(e, c);
});
headerRow.appendChild(th);
}
thead.appendChild(headerRow);
table.appendChild(thead);
// Tbody with all cells
const tbody = document.createElement('tbody');
for (let r = 0; r < this.sheet.numRows; r++) {
const tr = document.createElement('tr');
// Row header
const rowHeader = document.createElement('th');
rowHeader.className = 'row-header';
rowHeader.textContent = String(r + 1);
rowHeader.style.width = sheet_1.HEADER_WIDTH + 'px';
rowHeader.style.height = sheet_1.CELL_HEIGHT + 'px';
rowHeader.dataset.row = String(r);
rowHeader.addEventListener('contextmenu', (e) => {
e.preventDefault();
this.showRowMenu(e, r);
});
tr.appendChild(rowHeader);
// Data cells
for (let c = 0; c < this.sheet.numCols; c++) {
const td = document.createElement('td');
td.className = 'cell';
td.style.width = sheet_1.CELL_WIDTH + 'px';
td.style.height = sheet_1.CELL_HEIGHT + 'px';
td.dataset.col = String(c);
td.dataset.row = String(r);
const key = (0, types_1.addrKey)(c, r);
this.cellElements.set(key, td);
// Click to select
td.addEventListener('mousedown', (e) => {
if (this.editing) {
this.commitEdit();
}
if (e.detail === 2) {
// Double-click - enter edit mode
this.selection = { col: c, row: r };
this.selectCell();
this.startEdit();
}
else if (e.detail === 1) {
this.selection = { col: c, row: r };
this.selectCell();
}
});
tr.appendChild(td);
}
tbody.appendChild(tr);
}
table.appendChild(tbody);
this.grid.appendChild(table);
}
selectCell() {
// Remove old selection
const oldTd = this.cellElements.get((0, types_1.addrKey)(this.selection.col, this.selection.row));
// We'll re-select below, but first update the formula bar
this.updateFormulaBar();
this.highlightCell();
}
highlightCell() {
// Remove selection from all cells
for (const td of this.cellElements.values()) {
td.classList.remove('selected');
}
// Add selection to current cell
const key = (0, types_1.addrKey)(this.selection.col, this.selection.row);
const td = this.cellElements.get(key);
if (td)
td.classList.add('selected');
// Scroll into view
td?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
}
updateFormulaBar() {
const { col, row } = this.selection;
this.cellRefDisplay.textContent = (0, types_1.cellLabel)(col, row);
const cell = this.sheet.getCell(col, row);
if (cell) {
this.formulaInput.value = cell.raw;
}
else {
this.formulaInput.value = '';
}
}
updateCellDisplay(col, row) {
const key = (0, types_1.addrKey)(col, row);
const td = this.cellElements.get(key);
if (!td)
return;
const cell = this.sheet.getCell(col, row);
if (!cell) {
td.textContent = '';
td.classList.remove('cell-number', 'cell-text', 'cell-error');
return;
}
const computed = cell.computed;
switch (computed.type) {
case 'null':
td.textContent = '';
td.classList.remove('cell-number', 'cell-text', 'cell-error');
break;
case 'text':
td.textContent = computed.value;
td.classList.add('cell-text');
td.classList.remove('cell-number', 'cell-error');
break;
case 'number': {
// Format number - avoid floating point display issues
const n = computed.value;
const text = Number.isInteger(n) ? String(n) : parseFloat(n.toPrecision(12)).toString();
td.textContent = text;
td.classList.add('cell-number');
td.classList.remove('cell-text', 'cell-error');
break;
}
case 'error':
td.textContent = computed.value;
td.classList.add('cell-error');
td.classList.remove('cell-number', 'cell-text');
break;
}
}
startEdit() {
if (this.editing)
return;
this.editing = true;
const { col, row } = this.selection;
const key = (0, types_1.addrKey)(col, row);
const td = this.cellElements.get(key);
if (!td)
return;
const cell = this.sheet.getCell(col, row);
const raw = cell ? cell.raw : '';
// Replace cell content with input
td.classList.add('editing');
const input = document.createElement('input');
input.type = 'text';
input.value = raw;
input.className = 'cell-edit-input';
td.textContent = '';
td.appendChild(input);
input.focus();
input.select();
this.editCell = td;
this.editInput = input;
// Sync formula bar
this.formulaInput.value = raw;
// Handle input events
input.addEventListener('keydown', (e) => this.handleEditKeydown(e));
input.addEventListener('input', () => {
this.formulaInput.value = input.value;
});
}
commitEdit() {
if (!this.editing || !this.editInput || !this.editCell)
return;
const { col, row } = this.selection;
const raw = this.editInput.value;
const td = this.editCell;
// Restore cell display
td.classList.remove('editing');
this.editing = false;
this.editCell = null;
this.editInput = null;
// Update cell data
this.sheet.setCellRaw(col, row, raw);
this.depGraph.updateCell(col, row, this.sheet);
// Recalculate
const changed = (0, sheet_1.recalculate)(this.sheet, this.depGraph, (0, types_1.addrKey)(col, row));
// Update displays
for (const key of changed) {
const { col: c, row: r } = (0, types_1.parseAddrKey)(key);
this.updateCellDisplay(c, r);
}
// Save to localStorage
this.save();
// Update formula bar
this.updateFormulaBar();
}
handleEditKeydown(e) {
if (e.key === 'Enter') {
e.preventDefault();
this.commitEdit();
this.moveSelection(0, 1); // Move down
}
else if (e.key === 'Tab') {
e.preventDefault();
this.commitEdit();
this.moveSelection(1, 0); // Move right
}
else if (e.key === 'Escape') {
e.preventDefault();
// Cancel edit - restore original value
if (this.editCell) {
this.editCell.classList.remove('editing');
const { col, row } = this.selection;
this.updateCellDisplay(col, row);
}
this.editing = false;
this.editCell = null;
this.editInput = null;
this.updateFormulaBar();
}
}
moveSelection(dCol, dRow) {
let newCol = this.selection.col + dCol;
let newRow = this.selection.row + dRow;
if (newCol < 0)
newCol = 0;
if (newCol >= this.sheet.numCols)
newCol = this.sheet.numCols - 1;
if (newRow < 0)
newRow = 0;
if (newRow >= this.sheet.numRows)
newRow = this.sheet.numRows - 1;
this.selection = { col: newCol, row: newRow };
this.highlightCell();
this.updateFormulaBar();
}
handleGridKeydown(e) {
if (this.editing)
return; // Edit mode handles its own keys
const { col, row } = this.selection;
switch (e.key) {
case 'ArrowUp':
e.preventDefault();
this.moveSelection(0, -1);
break;
case 'ArrowDown':
e.preventDefault();
this.moveSelection(0, 1);
break;
case 'ArrowLeft':
e.preventDefault();
this.moveSelection(-1, 0);
break;
case 'ArrowRight':
e.preventDefault();
this.moveSelection(1, 0);
break;
case 'Enter':
e.preventDefault();
this.startEdit();
break;
case 'Tab':
e.preventDefault();
this.moveSelection(e.shiftKey ? -1 : 1, 0);
break;
case 'Delete':
case 'Backspace':
e.preventDefault();
this.sheet.setCellRaw(col, row, '');
this.depGraph.updateCell(col, row, this.sheet);
const changed = (0, sheet_1.recalculate)(this.sheet, this.depGraph, (0, types_1.addrKey)(col, row));
for (const key of changed) {
const { col: c, row: r } = (0, types_1.parseAddrKey)(key);
this.updateCellDisplay(c, r);
}
this.updateFormulaBar();
this.save();
break;
default:
// If it's a printable character, start editing
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
this.startEdit();
if (this.editInput) {
this.editInput.value = e.key;
this.formulaInput.value = e.key;
}
}
}
}
handleFormulaBarInput() {
if (this.editing && this.editInput) {
this.editInput.value = this.formulaInput.value;
}
}
handleFormulaBarKeydown(e) {
if (e.key === 'Enter') {
e.preventDefault();
const { col, row } = this.selection;
const raw = this.formulaInput.value;
// Update the cell
this.sheet.setCellRaw(col, row, raw);
this.depGraph.updateCell(col, row, this.sheet);
// If we were editing, commit
if (this.editing && this.editInput) {
this.editInput.value = raw;
}
this.commitEdit();
// Move down
this.moveSelection(0, 1);
}
else if (e.key === 'Escape') {
e.preventDefault();
this.updateFormulaBar(); // Reset to cell value
if (this.editing) {
this.editing = false;
this.editCell = null;
this.editInput = null;
const { col, row } = this.selection;
this.updateCellDisplay(col, row);
}
}
}
// ---- Context menus for insert/delete row/col ----
showColumnMenu(e, col) {
// Create a context menu
const menu = document.createElement('div');
menu.className = 'context-menu';
menu.style.left = e.pageX + 'px';
menu.style.top = e.pageY + 'px';
const insertBefore = document.createElement('div');
insertBefore.className = 'context-menu-item';
insertBefore.textContent = 'Insert column before';
insertBefore.addEventListener('click', () => {
document.body.removeChild(menu);
this.insertCol(col);
});
menu.appendChild(insertBefore);
const insertAfter = document.createElement('div');
insertAfter.className = 'context-menu-item';
insertAfter.textContent = 'Insert column after';
insertAfter.addEventListener('click', () => {
document.body.removeChild(menu);
this.insertCol(col + 1);
});
menu.appendChild(insertAfter);
const deleteCol = document.createElement('div');
deleteCol.className = 'context-menu-item';
deleteCol.textContent = 'Delete column';
deleteCol.addEventListener('click', () => {
document.body.removeChild(menu);
this.deleteCol(col);
});
menu.appendChild(deleteCol);
document.body.appendChild(menu);
// Close menu on outside click
const close = () => {
if (document.body.contains(menu)) {
document.body.removeChild(menu);
}
document.removeEventListener('click', close);
};
setTimeout(() => document.addEventListener('click', close), 0);
}
showRowMenu(e, row) {
const menu = document.createElement('div');
menu.className = 'context-menu';
menu.style.left = e.pageX + 'px';
menu.style.top = e.pageY + 'px';
const insertBefore = document.createElement('div');
insertBefore.className = 'context-menu-item';
insertBefore.textContent = 'Insert row above';
insertBefore.addEventListener('click', () => {
document.body.removeChild(menu);
this.insertRow(row);
});
menu.appendChild(insertBefore);
const insertAfter = document.createElement('div');
insertAfter.className = 'context-menu-item';
insertAfter.textContent = 'Insert row below';
insertAfter.addEventListener('click', () => {
document.body.removeChild(menu);
this.insertRow(row + 1);
});
menu.appendChild(insertAfter);
const deleteRow = document.createElement('div');
deleteRow.className = 'context-menu-item';
deleteRow.textContent = 'Delete row';
deleteRow.addEventListener('click', () => {
document.body.removeChild(menu);
this.deleteRow(row);
});
menu.appendChild(deleteRow);
document.body.appendChild(menu);
const close = () => {
if (document.body.contains(menu)) {
document.body.removeChild(menu);
}
document.removeEventListener('click', close);
};
setTimeout(() => document.addEventListener('click', close), 0);
}
insertCol(col) {
if (this.editing)
this.commitEdit();
this.sheet.insertCol(col);
this.depGraph.updateAll(this.sheet);
this.fullRecalculate();
this.buildTable();
this.save();
}
deleteCol(col) {
if (this.editing)
this.commitEdit();
this.sheet.deleteCol(col);
this.depGraph.updateAll(this.sheet);
this.fullRecalculate();
this.buildTable();
this.save();
}
insertRow(row) {
if (this.editing)
this.commitEdit();
this.sheet.insertRow(row);
this.depGraph.updateAll(this.sheet);
this.fullRecalculate();
this.buildTable();
this.save();
}
deleteRow(row) {
if (this.editing)
this.commitEdit();
this.sheet.deleteRow(row);
this.depGraph.updateAll(this.sheet);
this.fullRecalculate();
this.buildTable();
this.save();
}
// Recalculate all cells (used on load, after insert/delete row/col)
fullRecalculate() {
this.depGraph.updateAll(this.sheet);
// Collect all cells (including non-formula cells that may have computed values)
const allCellKeys = new Set(this.sheet.cells.keys());
// Find cycle cells first
const cycleCells = findCycleCellsForRecalc(this.depGraph, allCellKeys);
// Build topological order for non-cycle cells
const nonCycle = new Set();
for (const k of allCellKeys)
if (!cycleCells.has(k))
nonCycle.add(k);
const order = topoSortGraph(this.depGraph, nonCycle);
// Mark cycle cells
for (const key of cycleCells) {
const { col: c, row: r } = (0, types_1.parseAddrKey)(key);
const cell = this.sheet.getCell(c, r);
if (cell) {
cell.computed = { type: 'error', value: '#CYCLE!' };
this.updateCellDisplay(c, r);
}
}
// Recalculate non-cycle cells in order
for (const key of order) {
const { col: c, row: r } = (0, types_1.parseAddrKey)(key);
const cell = this.sheet.getCell(c, r);
if (!cell)
continue;
cell.computed = (0, sheet_1.evaluateCell)(c, r, this.sheet, new Set([key]));
this.updateCellDisplay(c, r);
}
}
// ---- localStorage persistence ----
save() {
const data = {};
for (const [key, cell] of this.sheet.cells) {
data[key] = cell.raw;
}
const meta = {
numCols: this.sheet.numCols,
numRows: this.sheet.numRows,
cells: data,
};
try {
localStorage.setItem('spreadsheet-data', JSON.stringify(meta));
}
catch (e) {
// Storage full or unavailable
}
}
load() {
try {
const raw = localStorage.getItem('spreadsheet-data');
if (!raw)
return false;
const meta = JSON.parse(raw);
this.sheet.numCols = meta.numCols || sheet_1.NUM_COLS;
this.sheet.numRows = meta.numRows || sheet_1.NUM_ROWS;
this.sheet.cells = new Map();
for (const [key, val] of Object.entries(meta.cells)) {
this.sheet.cells.set(key, { raw: val, computed: { type: 'null' } });
}
return true;
}
catch (e) {
return false;
}
}
}
exports.Renderer = Renderer;
function topoSortGraph(depGraph, nodes) {
const inDegree = new Map();
for (const cell of nodes) {
const deps = depGraph.dependsOn.get(cell);
let count = 0;
if (deps)
for (const d of deps)
if (nodes.has(d))
count++;
inDegree.set(cell, count);
}
const q = [];
for (const [cell, deg] of inDegree)
if (deg === 0)
q.push(cell);
const result = [];
while (q.length > 0) {
const cur = q.shift();
result.push(cur);
const deps = depGraph.dependents.get(cur);
if (deps) {
for (const dep of deps) {
if (nodes.has(dep)) {
const newDeg = (inDegree.get(dep) || 0) - 1;
inDegree.set(dep, newDeg);
if (newDeg === 0)
q.push(dep);
}
}
}
}
return result;
}
// Helper function used both in sheet.ts recalculation and here
function findCycleCellsForRecalc(depGraph, affected) {
const cycleCells = new Set();
for (const start of affected) {
const path = new Map();
const stack = [{ cell: start, isReturn: false }];
while (stack.length > 0) {
const top = stack[stack.length - 1];
const current = top.cell;
const isReturn = top.isReturn;
if (!isReturn) {
top.isReturn = true;
if (path.has(current)) {
const cycleStart = path.get(current);
for (const [cell, depth] of path) {
if (depth >= cycleStart)
cycleCells.add(cell);
}
continue;
}
path.set(current, path.size);
const deps = depGraph.dependents.get(current);
if (deps) {
for (const dep of deps) {
if (affected.has(dep)) {
stack.push({ cell: dep, isReturn: false });
}
}
}
}
else {
path.delete(current);
stack.pop();
}
}
}
return cycleCells;
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.6-27B-MTP-GGUF-IQ4_NL","provider":"brigid","persona":"typescript","sessionId":"2dceb99a-b5ea-43a6-905b-dd125de372b9","tokensIn":1690771,"tokensOut":59650,"tokensTotal":1750421,"cost":0,"turns":52,"toolCalls":51,"failedToolCalls":1,"timestamp":"2026-08-04T01:07:30.267Z"} -->