Spreadsheet
thinkingcap qwen3.6 27b · 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 53,872 bytes · SHA-256 4c058ddbcf5a
<!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: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; overflow: hidden; font-family: 'Segoe UI', Roboto, sans-serif; background: #ffffff; color: #202124; }
#app { display: flex; flex-direction: column; height: 100vh; }
#toolbar button { padding: 4px 10px; font-size: 12px; border: 1px solid #dadce0; background: #ffffff; cursor: pointer; border-radius: 3px; color: #5f6368; }
#toolbar button:hover { background: #f1f3f4; }
</style>
</head>
<body>
<div id="toolbar" style="display:flex;align-items:center;padding:4px 8px;background:#edf2fa;border-bottom:1px solid #e2e8f0;gap:6px;">
<button id="btn-insert-row">Insert Row</button>
<button id="btn-delete-row">Delete Row</button>
<button id="btn-insert-col">Insert Col</button>
<button id="btn-delete-col">Delete Col</button>
<span style="flex:1"></span>
<button id="btn-clear-all" title="Clear all cells">Clear All</button>
</div>
<div id="formula-bar-row" style="display:flex;align-items:center;padding:4px 8px;background:#ffffff;border-bottom:2px solid #e2e8f0;gap:4px;">
<span id="cell-ref" style="font-weight:600;font-size:13px;color:#5f6368;width:40px;text-align:center;font-family:'Segoe UI',Roboto,sans-serif;">A1</span>
<input id="formula-bar" type="text" placeholder="Formula bar" style="flex:1;height:26px;border:1px solid #dadce0;padding:0 8px;font-size:13px;font-family:'Segoe UI',Roboto,sans-serif;outline:none;">
</div>
<div id="grid-container" style="overflow:auto;flex:1;"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./config":"src/config.ts","./state":"src/state.ts","./renderer":"src/renderer.ts","./input":"src/input.ts"},"src/types.ts":{"./config":"src/config.ts"},"src/parser.ts":{"./types":"src/types.ts","./config":"src/config.ts"},"src/state.ts":{"./config":"src/config.ts","./types":"src/types.ts","./parser":"src/parser.ts"},"src/renderer.ts":{"./config":"src/config.ts","./state":"src/state.ts","./types":"src/types.ts"},"src/input.ts":{"./config":"src/config.ts","./state":"src/state.ts","./renderer":"src/renderer.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 config_1 = require("./config");
const state_1 = require("./state");
const renderer_1 = require("./renderer");
const input_1 = require("./input");
function main() {
const state = new state_1.SheetState();
const container = document.getElementById('grid-container');
const renderer = new renderer_1.Renderer(container, state);
const input = new input_1.InputHandler(state, renderer);
// Formula bar setup
const formulaBarInput = document.getElementById('formula-bar');
input.setFormulaBar(formulaBarInput);
// 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');
const btnClearAll = document.getElementById('btn-clear-all');
if (btnInsertRow) {
btnInsertRow.addEventListener('click', () => {
const row = renderer.getSelectedRow();
state.insertRow(row + 1); // insert before current row (1-indexed for the function)
renderer.updateAllCells();
renderer.selectCell(renderer.getSelectedCol(), Math.min(row, config_1.ROWS - 1));
});
}
if (btnDeleteRow) {
btnDeleteRow.addEventListener('click', () => {
const row = renderer.getSelectedRow();
state.deleteRow(row);
renderer.updateAllCells();
renderer.selectCell(renderer.getSelectedCol(), Math.min(row, config_1.ROWS - 1));
});
}
if (btnInsertCol) {
btnInsertCol.addEventListener('click', () => {
const col = renderer.getSelectedCol();
state.insertCol(col + 1);
renderer.updateAllCells();
renderer.selectCell(Math.min(col, config_1.COLS - 1), renderer.getSelectedRow());
});
}
if (btnDeleteCol) {
btnDeleteCol.addEventListener('click', () => {
const col = renderer.getSelectedCol();
state.deleteCol(col);
renderer.updateAllCells();
renderer.selectCell(Math.min(col, config_1.COLS - 1), renderer.getSelectedRow());
});
}
if (btnClearAll) {
btnClearAll.addEventListener('click', () => {
state.clear();
renderer.updateAllCells();
formulaBarInput.value = '';
});
}
// Load saved state
try {
const saved = localStorage.getItem('spreadsheet-data');
if (saved) {
state.deserialize(saved);
renderer.updateAllCells();
}
}
catch {
// ignore
}
// Select A1 initially
renderer.selectCell(0, 0);
input.init();
}
main();
};
// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ERR_PARSE = exports.ERR_DIV0 = exports.ERR_REF = exports.ERR_CYCLE = exports.COLORS = exports.HEADER_HEIGHT = exports.HEADER_WIDTH = exports.CELL_HEIGHT = exports.CELL_WIDTH = exports.ROWS = exports.COLS = void 0;
// Tunable constants
exports.COLS = 26;
exports.ROWS = 100;
exports.CELL_WIDTH = 100;
exports.CELL_HEIGHT = 28;
exports.HEADER_WIDTH = 45;
exports.HEADER_HEIGHT = 25;
// Colors
exports.COLORS = {
bg: '#ffffff',
headerBg: '#f8fafc',
headerBorder: '#e2e8f0',
gridLine: '#e2e8f0',
selectionBorder: '#1a73e8',
selectedCellBg: '#e8f0fe',
errorColor: '#d32f2f',
textPrimary: '#202124',
textSecondary: '#5f6368',
formulaBarBg: '#ffffff',
toolbarBg: '#edf2fa',
};
// Error strings
exports.ERR_CYCLE = '#CYCLE!';
exports.ERR_REF = '#REF!';
exports.ERR_DIV0 = '#DIV/0!';
exports.ERR_PARSE = '#ERR!';
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.isError = isError;
const config_1 = require("./config");
function isError(v) {
return typeof v === 'string' && (v === config_1.ERR_CYCLE || v === config_1.ERR_REF || v === config_1.ERR_DIV0 || v === config_1.ERR_PARSE);
}
};
// ── module: src/parser.ts ──
__mods["src/parser.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Parser = void 0;
exports.parseFormula = parseFormula;
const config_1 = require("./config");
function tokenize(input) {
const tokens = [];
let i = 0;
const s = input.trim();
while (i < s.length) {
// Skip whitespace
if (s[i] === ' ') {
i++;
continue;
}
// Number (including decimals and leading minus handled by parser)
if ((s[i] >= '0' && s[i] <= '9') || s[i] === '.') {
let num = '';
while (i < s.length && ((s[i] >= '0' && s[i] <= '9') || s[i] === '.')) {
num += s[i++];
}
tokens.push({ type: 'NUMBER', value: num });
continue;
}
// Cell reference (A1, Z100) or function name
if (s[i] >= 'A' && s[i] <= 'Z') {
let word = '';
while (i < s.length && ((s[i] >= 'A' && s[i] <= 'Z') || (s[i] >= 'a' && s[i] <= 'z'))) {
word += s[i++];
}
// Check if followed by digits → cell ref
if (i < s.length && s[i] >= '0' && s[i] <= '9') {
let num = '';
while (i < s.length && s[i] >= '0' && s[i] <= '9') {
num += s[i++];
}
tokens.push({ type: 'CELLREF', value: word.toUpperCase() + num });
}
else {
// Function name
const upper = word.toUpperCase();
if (['SUM', 'AVG', 'AVERAGE', 'MIN', 'MAX', 'COUNT'].includes(upper)) {
tokens.push({ type: 'FUNC', value: upper === 'AVG' ? 'AVERAGE' : upper });
}
else {
// Unknown identifier → treat as text error
throw new Error('Unknown identifier');
}
}
continue;
}
// Operators and punctuation
switch (s[i]) {
case '+':
tokens.push({ type: 'PLUS', value: '+' });
i++;
break;
case '-':
tokens.push({ type: 'MINUS', value: '-' });
i++;
break;
case '*':
tokens.push({ type: 'STAR', value: '*' });
i++;
break;
case '/':
tokens.push({ type: 'SLASH', value: '/' });
i++;
break;
case '(':
tokens.push({ type: 'LPAREN', value: '(' });
i++;
break;
case ')':
tokens.push({ type: 'RPAREN', value: ')' });
i++;
break;
case ',':
tokens.push({ type: 'COMMA', value: ',' });
i++;
break;
case ':':
tokens.push({ type: 'RANGE_OP', value: ':' });
i++;
break;
default: throw new Error('Unexpected character');
}
}
return tokens;
}
// --- Recursive Descent Parser ---
class Parser {
constructor(tokens) {
this.pos = 0;
this.refs = []; // collected cell references
this.tokens = tokens;
}
peek() { return this.tokens[this.pos]; }
consume(expected) {
const t = this.tokens[this.pos];
if (t && (expected === undefined || t.type === expected)) {
this.pos++;
return t;
}
throw new Error(`Expected ${expected ?? 'token'}, got ${t?.type}`);
}
parse() {
const node = this.parseExpression();
if (this.pos < this.tokens.length) {
throw new Error('Unexpected token after expression');
}
return node;
}
// Expression → Additive
parseExpression() {
return this.parseAdditive();
}
// Additive → Multiplicative (('+' | '-') Multiplicative)*
parseAdditive() {
let left = this.parseMultiplicative();
while (this.peek() && (this.peek().type === 'PLUS' || this.peek().type === 'MINUS')) {
const op = this.consume().value;
const right = this.parseMultiplicative();
left = { kind: 'binary', op, left, right };
}
return left;
}
// Multiplicative → Unary (('*' | '/') Unary)*
parseMultiplicative() {
let left = this.parseUnary();
while (this.peek() && (this.peek().type === 'STAR' || this.peek().type === 'SLASH')) {
const op = this.consume().value;
const right = this.parseUnary();
left = { kind: 'binary', op, left, right };
}
return left;
}
// Unary → '-' Unary | Primary
parseUnary() {
if (this.peek() && this.peek().type === 'MINUS') {
this.consume();
const operand = this.parseUnary();
return { kind: 'unaryMinus', operand };
}
return this.parsePrimary();
}
// Primary → NUMBER | CELLREF [':' CELLREF] | FUNC '(' args ')' | '(' Expression ')'
parsePrimary() {
const t = this.peek();
if (!t)
throw new Error('Unexpected end of expression');
switch (t.type) {
case 'NUMBER': {
this.consume();
return { kind: 'literal', value: parseFloat(t.value) };
}
case 'CELLREF': {
const ref = t.value;
this.consume();
if (this.peek() && this.peek().type === 'RANGE_OP') {
this.consume(); // consume ':'
const endRef = this.consume('CELLREF').value;
return this.parseRange(ref, endRef);
}
return this.parseCellRefNode(ref);
}
case 'FUNC': {
const name = t.value;
this.consume();
this.consume('LPAREN');
const args = [];
if (this.peek() && this.peek().type !== 'RPAREN') {
args.push(this.parseExpression());
while (this.peek() && this.peek().type === 'COMMA') {
this.consume(); // consume ','
args.push(this.parseExpression());
}
}
this.consume('RPAREN');
return { kind: 'func', name, args };
}
case 'LPAREN': {
this.consume();
const expr = this.parseExpression();
this.consume('RPAREN');
return expr;
}
default:
throw new Error(`Unexpected token: ${t.type}`);
}
}
parseCellRefNode(ref) {
const match = ref.match(/^([A-Z]+)(\d+)$/);
if (!match)
throw new Error('Invalid cell reference');
const col = this.colNameToNum(match[1]);
const row = parseInt(match[2], 10) - 1; // 0-indexed
if (col < 0 || col >= config_1.COLS || row < 0 || row >= config_1.ROWS)
throw new Error('Cell out of range');
this.refs.push(ref);
return { kind: 'cellRef', col, row };
}
parseRange(startRef, endRef) {
const sm = startRef.match(/^([A-Z]+)(\d+)$/);
const em = endRef.match(/^([A-Z]+)(\d+)$/);
if (!sm || !em)
throw new Error('Invalid range');
const sc = this.colNameToNum(sm[1]);
const sr = parseInt(sm[2], 10) - 1;
const ec = this.colNameToNum(em[1]);
const er = parseInt(em[2], 10) - 1;
if (sc < 0 || sc >= config_1.COLS || sr < 0 || sr >= config_1.ROWS || ec < 0 || ec >= config_1.COLS || er < 0 || er >= config_1.ROWS) {
throw new Error('Range out of range');
}
this.refs.push(startRef);
this.refs.push(endRef);
return { kind: 'range', startCol: sc, startRow: sr, endCol: ec, endRow: er };
}
colNameToNum(name) {
let n = 0;
for (let i = 0; i < name.length; i++) {
n = n * 26 + (name.charCodeAt(i) - 64);
}
return n - 1; // A=0, B=1, ...
}
static colNumToName(n) {
let name = '';
while (n >= 0) {
name = String.fromCharCode(65 + (n % 26)) + name;
n = Math.floor(n / 26) - 1;
}
return name || 'A';
}
static rowIdxToNum(idx) {
return idx + 1; // 0-indexed → 1-indexed
}
static cellId(col, row) {
return Parser.colNumToName(col) + Parser.rowIdxToNum(row);
}
}
exports.Parser = Parser;
// Public API
function parseFormula(input) {
try {
const tokens = tokenize(input);
if (tokens.length === 0)
return null;
const parser = new Parser(tokens);
const ast = parser.parse();
return { ast, refs: parser.refs };
}
catch {
return null;
}
}
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SheetState = void 0;
const config_1 = require("./config");
const types_1 = require("./types");
const parser_1 = require("./parser");
// --- Sheet State ---
class SheetState {
constructor() {
this.cells = [];
for (let c = 0; c < config_1.COLS; c++) {
const col = [];
for (let r = 0; r < config_1.ROWS; r++)
col.push(undefined);
this.cells.push(col);
}
this.astCache = new Map();
this.dependents = new Map();
this.dependencies = new Map();
}
id(c, r) {
return parser_1.Parser.cellId(c, r);
}
getCell(col, row) {
if (col < 0 || col >= config_1.COLS || row < 0 || row >= config_1.ROWS)
return undefined;
return this.cells[col][row];
}
hasRaw(col, row) {
const cell = this.getCell(col, row);
return !!cell && cell.raw !== '';
}
// Set a cell's raw value and recalculate dependents
// Returns list of [col, row] pairs that were updated (including the original cell)
setCell(col, row, raw) {
const id = this.id(col, row);
const oldRaw = this.getCell(col, row)?.raw ?? '';
if (oldRaw === raw)
return []; // no change
const updated = [[col, row]];
// Remove old dependencies for this cell
this.clearDeps(id);
// Store raw value
const cellData = { raw, value: '' };
this.cells[col][row] = cellData;
// Parse formula
const isFormula = raw.startsWith('=');
let parseResult = null;
if (isFormula) {
parseResult = (0, parser_1.parseFormula)(raw.substring(1));
if (!parseResult) {
cellData.value = config_1.ERR_PARSE;
return updated;
}
// Register dependencies
this.astCache.set(id, parseResult);
for (const ref of parseResult.refs) {
const depSet = this.dependents.get(ref) || new Set();
depSet.add(id);
this.dependents.set(ref, depSet);
const myDeps = this.dependencies.get(id) || new Set();
myDeps.add(ref);
this.dependencies.set(id, myDeps);
}
}
else {
this.astCache.set(id, null);
if (raw === '') {
delete this.cells[col][row];
}
else {
// Try to parse as number
const num = parseFloat(raw);
cellData.value = isNaN(num) ? raw : num;
}
}
// Check for cycles using DFS from this cell through its dependencies
const cycleCells = this.detectCycles(id);
// Evaluate this cell first (unless it's part of a cycle)
if (!cycleCells.has(id)) {
const evalResult = this.evaluateCell(id);
if (this.cells[col]?.[row]) {
this.cells[col][row].value = evalResult.value;
}
}
else {
if (this.cells[col]?.[row]) {
this.cells[col][row].value = config_1.ERR_CYCLE;
}
}
// Mark all cycle cells as #CYCLE!
for (const cycleId of cycleCells) {
const [cc, cr] = this.parseId(cycleId);
if (this.cells[cc]?.[cr]) {
this.cells[cc][cr].value = config_1.ERR_CYCLE;
updated.push([cc, cr]);
}
}
// Propagate change to all dependents of this cell, collecting updated cells
this.propagateChange(id, new Set(), updated);
return updated;
}
clearDeps(cellId) {
// Remove from dependents of cells we reference
const myDeps = this.dependencies.get(cellId);
if (myDeps) {
for (const dep of myDeps) {
const dSet = this.dependents.get(dep);
if (dSet) {
dSet.delete(cellId);
if (dSet.size === 0)
this.dependents.delete(dep);
}
}
}
this.dependencies.delete(cellId);
}
// Recalculate a cell and propagate to its dependents
recalcDependents(cellId, visited) {
if (visited.has(cellId))
return;
visited.add(cellId);
const evalResult = this.evaluateCell(cellId);
const [col, row] = this.parseId(cellId);
if (this.cells[col]?.[row]) {
this.cells[col][row].value = evalResult.value;
}
// Propagate to dependents
const deps = this.dependents.get(cellId);
if (deps) {
for (const dep of deps) {
this.recalcDependents(dep, visited);
}
}
}
// Detect cycles by doing DFS through dependencies from the given cell
detectCycles(startId) {
const cycleCells = new Set();
// DFS to find if we can reach startId again through dependency chain
const dfs = (cellId, path) => {
if (path.has(cellId))
return false; // already in current path
path.add(cellId);
const deps = this.dependencies.get(cellId);
if (!deps) {
path.delete(cellId);
return false;
}
for (const dep of deps) {
if (dep === startId) {
// Found a cycle - mark all cells in the path
for (const p of path)
cycleCells.add(p);
cycleCells.add(startId);
path.delete(cellId);
return true;
}
if (dfs(dep, path)) {
cycleCells.add(cellId);
path.delete(cellId);
return true;
}
}
path.delete(cellId);
return false;
};
dfs(startId, new Set());
return cycleCells;
}
propagateChange(cellId, visited, updated) {
const deps = this.dependents.get(cellId);
if (!deps)
return;
for (const dep of deps) {
if (visited.has(dep))
continue;
visited.add(dep);
const [dc, dr] = this.parseId(dep);
const cellData = this.cells[dc]?.[dr];
if (!cellData || !this.astCache.get(dep))
continue;
// Check for cycle during propagation
const evalResult = this.evaluateCell(dep);
cellData.value = evalResult.value;
if (updated)
updated.push([dc, dr]);
this.propagateChange(dep, visited, updated);
}
}
evaluateCell(cellId) {
const [col, row] = this.parseId(cellId);
const cellData = this.cells[col]?.[row];
if (!cellData)
return { value: '' };
const parseResult = this.astCache.get(cellId);
if (!parseResult) {
// Non-formula cell — already has its value set
return { value: cellData.value };
}
try {
const val = this.evalAst(parseResult.ast, new Set());
return { value: val };
}
catch (e) {
if (e.message === 'CYCLE')
return { value: config_1.ERR_CYCLE };
if (e.message === 'DIV0')
return { value: config_1.ERR_DIV0 };
return { value: config_1.ERR_PARSE };
}
}
evalAst(node, visiting) {
switch (node.kind) {
case 'literal': return node.value;
case 'text': return node.value;
case 'cellRef': {
const refId = this.id(node.col, node.row);
if (visiting.has(refId))
throw new Error('CYCLE');
visiting.add(refId);
const cellData = this.cells[node.col]?.[node.row];
if (!cellData)
return 0; // empty cell → 0 in arithmetic context
// If the referenced cell is still being computed (has formula but not yet evaluated), recurse
const refParseResult = this.astCache.get(refId);
if (refParseResult && !(0, types_1.isError)(cellData.value)) {
// Already has a valid value from prior computation, use it
const v = cellData.value;
visiting.delete(refId);
return typeof v === 'number' ? v : 0;
}
const v = cellData.value;
visiting.delete(refId);
if ((0, types_1.isError)(v))
return v; // propagate errors
return typeof v === 'number' ? v : parseFloat(String(v)) || 0;
}
case 'range': {
const values = [];
for (let c = Math.min(node.startCol, node.endCol); c <= Math.max(node.startCol, node.endCol); c++) {
for (let r = Math.min(node.startRow, node.endRow); r <= Math.max(node.startRow, node.endRow); r++) {
const cellData = this.cells[c]?.[r];
if (!cellData)
continue;
const v = cellData.value;
if ((0, types_1.isError)(v))
return v;
const num = typeof v === 'number' ? v : parseFloat(String(v));
if (!isNaN(num))
values.push(num);
}
}
// Return as array-like for functions to consume — we handle this in func evaluation
return NaN; // sentinel: range should be consumed by a function, not directly evaluated
}
case 'unaryMinus': {
const v = this.evalAst(node.operand, visiting);
if (typeof v === 'string')
return v;
return -v;
}
case 'binary': {
const left = this.evalAst(node.left, visiting);
const right = this.evalAst(node.right, visiting);
if (typeof left === 'string' || typeof right === 'string') {
// If either is an error string, propagate it
return typeof left === 'string' ? left : right;
}
switch (node.op) {
case '+': return left + right;
case '-': return left - right;
case '*': return left * right;
case '/':
if (right === 0)
throw new Error('DIV0');
return left / right;
}
}
case 'func': {
const values = [];
for (const arg of node.args) {
if (arg.kind === 'range') {
// Evaluate range directly to collect all cell values
for (let c = Math.min(arg.startCol, arg.endCol); c <= Math.max(arg.startCol, arg.endCol); c++) {
for (let r = Math.min(arg.startRow, arg.endRow); r <= Math.max(arg.startRow, arg.endRow); r++) {
const cellData = this.cells[c]?.[r];
if (!cellData)
continue;
const v = cellData.value;
if ((0, types_1.isError)(v))
return v;
const num = typeof v === 'number' ? v : parseFloat(String(v));
if (!isNaN(num))
values.push(num);
}
}
}
else {
const v = this.evalAst(arg, visiting);
if (typeof v === 'string')
return v; // error propagation
if (!isNaN(v))
values.push(v);
}
}
return this.applyFunc(node.name, values);
}
}
}
applyFunc(name, values) {
switch (name) {
case 'SUM': return values.reduce((a, b) => a + b, 0);
case 'AVERAGE': return values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0;
case 'MIN': return values.length > 0 ? Math.min(...values) : 0;
case 'MAX': return values.length > 0 ? Math.max(...values) : 0;
case 'COUNT': return values.length;
default: throw new Error('Unknown function');
}
}
parseId(id) {
const match = id.match(/^([A-Z]+)(\d+)$/);
if (!match)
throw new Error('Invalid cell ID');
let col = 0;
for (let i = 0; i < match[1].length; i++) {
col = col * 26 + (match[1].charCodeAt(i) - 64);
}
col -= 1;
const row = parseInt(match[2], 10) - 1;
return [col, row];
}
// --- Row/Column operations ---
insertRow(before) {
if (before <= 0 || before > config_1.ROWS)
return;
// Shift all rows down from 'before' onwards
for (let c = 0; c < config_1.COLS; c++) {
const newRowData = undefined;
// We need to shift the column array — but we can't grow beyond ROWS
// So we drop the last row if needed
this.cells[c].splice(before, 0, newRowData);
this.cells[c].pop(); // keep size at ROWS
}
this.rebuildAll();
}
deleteRow(at) {
if (at < 0 || at >= config_1.ROWS)
return;
for (let c = 0; c < config_1.COLS; c++) {
this.cells[c].splice(at, 1);
this.cells[c].push(undefined); // keep size at ROWS
}
this.rebuildAll();
}
insertCol(before) {
if (before <= 0 || before > config_1.COLS)
return;
for (let r = 0; r < config_1.ROWS; r++) {
const cols = [];
// Collect all cell data at this row across columns
const rowData = this.cells.map(c => c[r]);
rowData.splice(before, 0, undefined);
rowData.pop();
for (let c = 0; c < config_1.COLS; c++) {
this.cells[c][r] = rowData[c];
}
}
this.rebuildAll();
}
deleteCol(at) {
if (at < 0 || at >= config_1.COLS)
return;
for (let r = 0; r < config_1.ROWS; r++) {
const rowData = this.cells.map(c => c[r]);
rowData.splice(at, 1);
rowData.push(undefined);
for (let c = 0; c < config_1.COLS; c++) {
this.cells[c][r] = rowData[c];
}
}
this.rebuildAll();
}
// Full rebuild after structural changes — re-parses all formulas and recalculates
rebuildAll() {
this.astCache.clear();
this.dependents.clear();
this.dependencies.clear();
// First pass: parse all formulas and build dependency graph
for (let c = 0; c < config_1.COLS; c++) {
for (let r = 0; r < config_1.ROWS; r++) {
const cellData = this.cells[c]?.[r];
if (!cellData)
continue;
const id = this.id(c, r);
const isFormula = cellData.raw.startsWith('=');
if (isFormula) {
const parseResult = (0, parser_1.parseFormula)(cellData.raw.substring(1));
this.astCache.set(id, parseResult);
if (parseResult) {
for (const ref of parseResult.refs) {
const depSet = this.dependents.get(ref) || new Set();
depSet.add(id);
this.dependents.set(ref, depSet);
const myDeps = this.dependencies.get(id) || new Set();
myDeps.add(ref);
this.dependencies.set(id, myDeps);
}
}
else {
cellData.value = config_1.ERR_PARSE;
}
}
else {
if (cellData.raw === '') {
delete this.cells[c][r];
}
else {
const num = parseFloat(cellData.raw);
cellData.value = isNaN(num) ? cellData.raw : num;
}
}
}
}
// Second pass: topological sort and evaluate all formula cells
this.topoEvaluate();
}
topoEvaluate() {
const inDegree = new Map();
const formulaCells = new Set();
for (let c = 0; c < config_1.COLS; c++) {
for (let r = 0; r < config_1.ROWS; r++) {
const cellData = this.cells[c]?.[r];
if (!cellData)
continue;
const id = this.id(c, r);
if (!this.astCache.get(id))
continue;
formulaCells.add(id);
const deps = this.dependencies.get(id);
inDegree.set(id, deps ? deps.size : 0);
}
}
// Kahn's algorithm
const queue = [];
for (const id of formulaCells) {
if ((inDegree.get(id) ?? 0) === 0)
queue.push(id);
}
const evaluated = new Set();
while (queue.length > 0) {
const cellId = queue.shift();
evaluated.add(cellId);
const [col, row] = this.parseId(cellId);
const evalResult = this.evaluateCell(cellId);
if (this.cells[col]?.[row]) {
this.cells[col][row].value = evalResult.value;
}
const deps = this.dependents.get(cellId);
if (deps) {
for (const dep of deps) {
inDegree.set(dep, (inDegree.get(dep) ?? 0) - 1);
if ((inDegree.get(dep) ?? 0) === 0)
queue.push(dep);
}
}
}
// Any formula cells not evaluated are part of cycles
for (const id of formulaCells) {
if (!evaluated.has(id)) {
const [col, row] = this.parseId(id);
if (this.cells[col]?.[row]) {
this.cells[col][row].value = config_1.ERR_CYCLE;
}
}
}
}
// --- Serialization ---
serialize() {
const data = {};
for (let c = 0; c < config_1.COLS; c++) {
for (let r = 0; r < config_1.ROWS; r++) {
const cellData = this.cells[c]?.[r];
if (cellData && cellData.raw !== '') {
data[this.id(c, r)] = cellData.raw;
}
}
}
return JSON.stringify(data);
}
deserialize(json) {
try {
const data = JSON.parse(json);
for (const [id, raw] of Object.entries(data)) {
const match = id.match(/^([A-Z]+)(\d+)$/);
if (!match)
continue;
let col = 0;
for (let i = 0; i < match[1].length; i++) {
col = col * 26 + (match[1].charCodeAt(i) - 64);
}
col -= 1;
const row = parseInt(match[2], 10) - 1;
if (col < 0 || col >= config_1.COLS || row < 0 || row >= config_1.ROWS)
continue;
this.setCell(col, row, raw);
}
}
catch {
// ignore bad data
}
}
clear() {
for (let c = 0; c < config_1.COLS; c++) {
for (let r = 0; r < config_1.ROWS; r++) {
delete this.cells[c][r];
}
}
this.astCache.clear();
this.dependents.clear();
this.dependencies.clear();
}
}
exports.SheetState = SheetState;
};
// ── 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 config_1 = require("./config");
const types_1 = require("./types");
class Renderer {
constructor(container, state) {
this.selectedCol = -1;
this.selectedRow = -1;
this.state = state;
this.cellMap = new Map();
this.table = document.createElement('table');
this.table.style.borderCollapse = 'collapse';
this.table.style.tableLayout = 'fixed';
container.appendChild(this.table);
this.buildTable();
}
buildTable() {
// Create thead with column headers
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
// Corner cell (top-left)
const corner = document.createElement('th');
corner.style.width = config_1.HEADER_WIDTH + 'px';
corner.style.height = config_1.HEADER_HEIGHT + 'px';
corner.style.minWidth = config_1.HEADER_WIDTH + 'px';
corner.style.background = config_1.COLORS.headerBg;
corner.style.borderBottom = `2px solid ${config_1.COLORS.gridLine}`;
corner.style.borderRight = `2px solid ${config_1.COLORS.gridLine}`;
headerRow.appendChild(corner);
for (let c = 0; c < config_1.COLS; c++) {
const th = document.createElement('th');
const colName = String.fromCharCode(65 + c);
th.textContent = colName;
th.style.width = config_1.CELL_WIDTH + 'px';
th.style.minWidth = config_1.CELL_WIDTH + 'px';
th.style.height = config_1.HEADER_HEIGHT + 'px';
th.style.background = config_1.COLORS.headerBg;
th.style.borderBottom = `2px solid ${config_1.COLORS.gridLine}`;
th.style.color = config_1.COLORS.textSecondary;
th.style.fontSize = '12px';
th.style.fontWeight = '500';
th.setAttribute('data-col', String(c));
headerRow.appendChild(th);
}
thead.appendChild(headerRow);
this.table.appendChild(thead);
// Create tbody with row data
const tbody = document.createElement('tbody');
for (let r = 0; r < config_1.ROWS; r++) {
const tr = document.createElement('tr');
// Row header
const th = document.createElement('th');
th.textContent = String(r + 1);
th.style.width = config_1.HEADER_WIDTH + 'px';
th.style.minWidth = config_1.HEADER_WIDTH + 'px';
th.style.height = config_1.CELL_HEIGHT + 'px';
th.style.background = config_1.COLORS.headerBg;
th.style.borderRight = `2px solid ${config_1.COLORS.gridLine}`;
th.style.color = config_1.COLORS.textSecondary;
th.style.fontSize = '12px';
th.style.fontWeight = '500';
tr.appendChild(th);
for (let c = 0; c < config_1.COLS; c++) {
const td = document.createElement('td');
td.style.width = config_1.CELL_WIDTH + 'px';
td.style.height = config_1.CELL_HEIGHT + 'px';
td.style.borderBottom = `1px solid ${config_1.COLORS.gridLine}`;
td.style.borderRight = `1px solid ${config_1.COLORS.gridLine}`;
td.style.padding = '2px 4px';
td.style.fontSize = '13px';
td.style.fontFamily = '"Segoe UI", Roboto, sans-serif';
td.style.overflow = 'hidden';
td.style.whiteSpace = 'nowrap';
td.style.textOverflow = 'ellipsis';
td.setAttribute('data-col', String(c));
td.setAttribute('data-row', String(r));
tr.appendChild(td);
const key = `${c},${r}`;
this.cellMap.set(key, td);
}
tbody.appendChild(tr);
}
this.table.appendChild(tbody);
}
selectCell(col, row) {
// Deselect previous
if (this.selectedCol >= 0 && this.selectedRow >= 0) {
const prevKey = `${this.selectedCol},${this.selectedRow}`;
const prevTd = this.cellMap.get(prevKey);
if (prevTd) {
prevTd.style.outline = 'none';
prevTd.style.background = '';
}
}
this.selectedCol = col;
this.selectedRow = row;
// Select new cell
const key = `${col},${row}`;
const td = this.cellMap.get(key);
if (td) {
td.style.outline = `2px solid ${config_1.COLORS.selectionBorder}`;
td.style.outlineOffset = '-1px';
td.style.background = config_1.COLORS.selectedCellBg;
}
// Scroll cell into view
if (td)
td.scrollIntoView({ block: 'nearest', inline: 'nearest' });
}
updateCell(col, row) {
const key = `${col},${row}`;
const td = this.cellMap.get(key);
if (!td)
return;
const isSelected = col === this.selectedCol && row === this.selectedRow;
const cellData = this.state.getCell(col, row);
if (!cellData || cellData.raw === '') {
td.textContent = '';
td.style.textAlign = 'left';
td.style.color = config_1.COLORS.textPrimary;
if (isSelected) {
td.style.outline = `2px solid ${config_1.COLORS.selectionBorder}`;
td.style.outlineOffset = '-1px';
td.style.background = config_1.COLORS.selectedCellBg;
}
else {
td.style.outline = 'none';
td.style.background = '';
}
return;
}
const val = cellData.value;
if ((0, types_1.isError)(val)) {
td.textContent = String(val);
td.style.color = config_1.COLORS.errorColor;
td.style.fontWeight = '600';
td.style.textAlign = 'left';
}
else if (typeof val === 'number') {
// Format number: avoid long decimals
const formatted = Number.isInteger(val) ? String(val) : parseFloat(val.toFixed(10)).toString();
td.textContent = formatted;
td.style.color = config_1.COLORS.textPrimary;
td.style.textAlign = 'right';
}
else {
td.textContent = String(val);
td.style.color = config_1.COLORS.textPrimary;
td.style.textAlign = 'left';
}
if (isSelected) {
td.style.outline = `2px solid ${config_1.COLORS.selectionBorder}`;
td.style.outlineOffset = '-1px';
td.style.background = config_1.COLORS.selectedCellBg;
}
else {
td.style.outline = 'none';
td.style.background = '';
}
}
updateAllCells() {
for (let c = 0; c < config_1.COLS; c++) {
for (let r = 0; r < config_1.ROWS; r++) {
this.updateCell(c, r);
}
}
}
getSelectedCol() { return this.selectedCol; }
getSelectedRow() { return this.selectedRow; }
// Get the TD element for a cell
getTd(col, row) {
return this.cellMap.get(`${col},${row}`);
}
// Start editing mode on selected cell
startEdit() {
const td = this.cellMap.get(`${this.selectedCol},${this.selectedRow}`);
if (!td)
return null;
const input = document.createElement('input');
input.type = 'text';
input.style.width = '100%';
input.style.height = '100%';
input.style.border = 'none';
input.style.outline = 'none';
input.style.padding = '0';
input.style.fontSize = '13px';
input.style.fontFamily = '"Segoe UI", Roboto, sans-serif';
input.style.background = 'transparent';
const cellData = this.state.getCell(this.selectedCol, this.selectedRow);
input.value = cellData?.raw ?? '';
td.textContent = '';
td.appendChild(input);
input.focus();
input.select();
return input;
}
// Cancel edit mode (remove input from cell)
cancelEdit() {
const td = this.cellMap.get(`${this.selectedCol},${this.selectedRow}`);
if (!td)
return;
const input = td.querySelector('input');
if (input)
input.remove();
this.updateCell(this.selectedCol, this.selectedRow);
}
// Commit edit mode
commitEdit() {
const td = this.cellMap.get(`${this.selectedCol},${this.selectedRow}`);
let value = '';
if (td) {
const input = td.querySelector('input');
if (input) {
value = input.value;
input.remove();
}
}
return value;
}
getTable() { return this.table; }
}
exports.Renderer = Renderer;
};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputHandler = void 0;
const config_1 = require("./config");
class InputHandler {
constructor(state, renderer) {
this.editing = false;
this.formulaBarInput = null;
this.cellEditInput = null;
this.state = state;
this.renderer = renderer;
}
setFormulaBar(input) {
this.formulaBarInput = input;
}
init() {
const table = this.renderer.getTable();
// Click on cell to select it
table.addEventListener('mousedown', (e) => {
const target = e.target;
const td = target.closest('td');
if (!td)
return;
const col = parseInt(td.getAttribute('data-col') ?? '-1', 10);
const row = parseInt(td.getAttribute('data-row') ?? '-1', 10);
if (col < 0 || row < 0)
return;
// If we're editing and click elsewhere, commit first
if (this.editing && !(td === this.renderer.getTd(this.renderer.getSelectedCol(), this.renderer.getSelectedRow()))) {
this.commitEdit();
}
this.renderer.selectCell(col, row);
this.updateFormulaBar();
});
// Double-click to enter edit mode
table.addEventListener('dblclick', (e) => {
const target = e.target;
const td = target.closest('td');
if (!td)
return;
const col = parseInt(td.getAttribute('data-col') ?? '-1', 10);
const row = parseInt(td.getAttribute('data-row') ?? '-1', 10);
if (col < 0 || row < 0)
return;
this.renderer.selectCell(col, row);
this.startEdit();
});
// Keyboard handling
document.addEventListener('keydown', (e) => {
if (this.editing) {
this.handleEditingKey(e);
}
else {
this.handleNormalKey(e);
}
});
// Formula bar input
if (this.formulaBarInput) {
this.formulaBarInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.commitFormulaBarEdit();
}
else if (e.key === 'Escape') {
this.cancelFormulaBarEdit();
}
});
// Sync formula bar changes to cell display
this.formulaBarInput.addEventListener('input', () => {
// We don't update the cell live during typing in formula bar,
// only on commit. But we do want to show what's being typed.
});
}
}
handleNormalKey(e) {
const col = this.renderer.getSelectedCol();
const row = this.renderer.getSelectedRow();
if (col < 0 || row < 0)
return;
// Arrow keys move selection
if (e.key === 'ArrowUp' && row > 0) {
e.preventDefault();
this.renderer.selectCell(col, row - 1);
this.updateFormulaBar();
return;
}
if (e.key === 'ArrowDown' && row < config_1.ROWS - 1) {
e.preventDefault();
this.renderer.selectCell(col, row + 1);
this.updateFormulaBar();
return;
}
if (e.key === 'ArrowLeft' && col > 0) {
e.preventDefault();
this.renderer.selectCell(col - 1, row);
this.updateFormulaBar();
return;
}
if (e.key === 'ArrowRight' && col < config_1.COLS - 1) {
e.preventDefault();
this.renderer.selectCell(col + 1, row);
this.updateFormulaBar();
return;
}
// Delete key clears cell
if (e.key === 'Delete') {
e.preventDefault();
const updated = this.state.setCell(col, row, '');
for (const [c, r] of updated) {
this.renderer.updateCell(c, r);
}
this.updateFormulaBar();
this.saveState();
return;
}
// Tab moves right and enters edit mode
if (e.key === 'Tab') {
e.preventDefault();
const newCol = Math.min(col + 1, config_1.COLS - 1);
this.renderer.selectCell(newCol, row);
this.updateFormulaBar();
return;
}
// If key is a printable character, start editing
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
this.startEdit();
// Type the character into the edit input
if (this.cellEditInput) {
this.cellEditInput.value = e.key;
}
}
}
handleEditingKey(e) {
if (!this.cellEditInput)
return;
if (e.key === 'Enter') {
e.preventDefault();
const value = this.commitEdit();
// Move down
const newRow = Math.min(this.renderer.getSelectedRow() + 1, config_1.ROWS - 1);
this.renderer.selectCell(this.renderer.getSelectedCol(), newRow);
this.updateFormulaBar();
}
else if (e.key === 'Tab') {
e.preventDefault();
const value = this.commitEdit();
// Move right
const newCol = Math.min(this.renderer.getSelectedCol() + 1, config_1.COLS - 1);
this.renderer.selectCell(newCol, this.renderer.getSelectedRow());
this.updateFormulaBar();
}
else if (e.key === 'Escape') {
e.preventDefault();
this.cancelEdit();
}
}
startEdit() {
this.editing = true;
this.cellEditInput = this.renderer.startEdit();
// Sync formula bar to show raw value
this.updateFormulaBar();
}
commitEdit() {
const value = this.renderer.commitEdit();
this.editing = false;
this.cellEditInput = null;
const col = this.renderer.getSelectedCol();
const row = this.renderer.getSelectedRow();
const updated = this.state.setCell(col, row, value);
for (const [c, r] of updated) {
this.renderer.updateCell(c, r);
}
this.saveState();
return value;
}
cancelEdit() {
this.editing = false;
this.cellEditInput = null;
this.renderer.cancelEdit();
}
commitFormulaBarEdit() {
if (!this.formulaBarInput)
return;
const value = this.formulaBarInput.value;
const col = this.renderer.getSelectedCol();
const row = this.renderer.getSelectedRow();
const updated = this.state.setCell(col, row, value);
for (const [c, r] of updated) {
this.renderer.updateCell(c, r);
}
this.saveState();
}
cancelFormulaBarEdit() {
if (!this.formulaBarInput)
return;
// Restore formula bar to current cell's raw value
const col = this.renderer.getSelectedCol();
const row = this.renderer.getSelectedRow();
const cellData = this.state.getCell(col, row);
this.formulaBarInput.value = cellData?.raw ?? '';
}
updateFormulaBar() {
if (!this.formulaBarInput)
return;
const col = this.renderer.getSelectedCol();
const row = this.renderer.getSelectedRow();
const cellData = this.state.getCell(col, row);
this.formulaBarInput.value = cellData?.raw ?? '';
// Update the cell reference display (e.g. "B4")
const refDisplay = document.getElementById('cell-ref');
if (refDisplay) {
const colName = String.fromCharCode(65 + col);
refDisplay.textContent = `${colName}${row + 1}`;
}
}
saveState() {
try {
localStorage.setItem('spreadsheet-data', this.state.serialize());
}
catch {
// ignore storage errors
}
}
}
exports.InputHandler = InputHandler;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>