Spreadsheet
ProCreations grug 27b GGUF IQ4 · typescript

Browser check: Artifact runtime error. This is a technical check, not a quality review.
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 44,457 bytes · SHA-256 b40907eedc53
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Spreadsheet</title>
<style>
</style>
</head>
<body>
<div id="toolbar"></div>
<div id="formula-bar"></div>
<div id="grid-container"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./spreadsheet":"src/spreadsheet.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 spreadsheet_1 = require("./spreadsheet");
const spreadsheet = new spreadsheet_1.Spreadsheet(26, 100);
spreadsheet.render();
spreadsheet.loadFromLocalStorage();
spreadsheet.enableEditing();
};
// ── module: src/spreadsheet.ts ──
__mods["src/spreadsheet.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Spreadsheet = void 0;
/* Constants */
const COLUMNS = 26;
const ROWS = 100;
const CELL_WIDTH = 100;
const CELL_HEIGHT = 24;
const HEADER_HEIGHT = 24;
const HEADER_WIDTH = 40;
const COLORS = {
selection: '#2196F3',
gridline: '#e0e0e0',
header: '#f5f5f5',
error: '#d32f2f',
text: '#000',
background: '#fff',
};
/* Types */
var TokenType;
(function (TokenType) {
TokenType[TokenType["NUMBER"] = 0] = "NUMBER";
TokenType[TokenType["IDENT"] = 1] = "IDENT";
TokenType[TokenType["FUNC"] = 2] = "FUNC";
TokenType[TokenType["OPERATOR"] = 3] = "OPERATOR";
TokenType[TokenType["LPAREN"] = 4] = "LPAREN";
TokenType[TokenType["RPAREN"] = 5] = "RPAREN";
TokenType[TokenType["COMMA"] = 6] = "COMMA";
TokenType[TokenType["COLON"] = 7] = "COLON";
})(TokenType || (TokenType = {}));
var ExprType;
(function (ExprType) {
ExprType[ExprType["NUMBER"] = 0] = "NUMBER";
ExprType[ExprType["REFERENCE"] = 1] = "REFERENCE";
ExprType[ExprType["RANGE"] = 2] = "RANGE";
ExprType[ExprType["FUNCTION"] = 3] = "FUNCTION";
ExprType[ExprType["BINARY"] = 4] = "BINARY";
ExprType[ExprType["UNARY"] = 5] = "UNARY";
ExprType[ExprType["ERROR"] = 6] = "ERROR";
})(ExprType || (ExprType = {}));
class Spreadsheet {
constructor(cols, rows) {
this.cols = cols;
this.rows = rows;
this.selectedCell = null;
this.editingCell = null;
this.formulaBar = null;
this.refDisplay = null;
this.container = null;
this.table = null;
this.recalculating = false;
this.grid = [];
for (let row = 0; row < rows; row++) {
const rowCells = [];
for (let col = 0; col < cols; col++) {
rowCells.push({ raw: '', display: '', value: null, formula: null, isEditing: false });
}
this.grid.push(rowCells);
}
this.dependencies = new Map();
this.dependents = new Map();
}
colToLetter(col) {
let result = '';
let n = col;
while (n >= 0) {
result = String.fromCharCode(65 + (n % 26)) + result;
n = Math.floor(n / 26) - 1;
}
return result;
}
refToString(col, row) {
return this.colToLetter(col) + (row + 1);
}
/* Rendering */
render() {
this.container = document.getElementById('grid-container');
this.container.innerHTML = '';
this.table = document.createElement('table');
this.table.style.borderCollapse = 'collapse';
this.table.style.width = `${HEADER_WIDTH + this.cols * CELL_WIDTH}px`;
// Header row
const headerRow = document.createElement('tr');
const cornerHeader = document.createElement('th');
cornerHeader.style.width = `${HEADER_WIDTH}px`;
cornerHeader.style.minWidth = `${HEADER_WIDTH}px`;
cornerHeader.style.position = 'sticky';
cornerHeader.style.left = '0';
cornerHeader.style.top = '0';
cornerHeader.style.zIndex = '3';
cornerHeader.style.background = COLORS.header;
cornerHeader.style.borderRight = `1px solid ${COLORS.gridline}`;
cornerHeader.style.borderBottom = `1px solid ${COLORS.gridline}`;
headerRow.appendChild(cornerHeader);
for (let col = 0; col < this.cols; col++) {
const header = document.createElement('th');
header.textContent = this.colToLetter(col);
header.style.width = `${CELL_WIDTH}px`;
header.style.minWidth = `${CELL_WIDTH}px`;
header.style.position = 'sticky';
header.style.top = '0';
header.style.zIndex = '2';
header.style.background = COLORS.header;
header.style.textAlign = 'center';
header.style.borderRight = `1px solid ${COLORS.gridline}`;
header.style.borderBottom = `1px solid ${COLORS.gridline}`;
header.addEventListener('contextmenu', (event) => {
event.preventDefault();
this.showColumnMenu(event, col);
});
headerRow.appendChild(header);
}
this.table.appendChild(headerRow);
for (let row = 0; row < this.rows; row++) {
const tableRow = document.createElement('tr');
const rowHeader = document.createElement('th');
rowHeader.textContent = String(row + 1);
rowHeader.style.width = `${HEADER_WIDTH}px`;
rowHeader.style.minWidth = `${HEADER_WIDTH}px`;
rowHeader.style.position = 'sticky';
rowHeader.style.left = '0';
rowHeader.style.zIndex = '1';
rowHeader.style.background = COLORS.header;
rowHeader.style.textAlign = 'right';
rowHeader.style.paddingRight = '4px';
rowHeader.style.borderRight = `1px solid ${COLORS.gridline}`;
rowHeader.style.borderBottom = `1px solid ${COLORS.gridline}`;
rowHeader.addEventListener('contextmenu', (event) => {
event.preventDefault();
this.showRowMenu(event, row);
});
tableRow.appendChild(rowHeader);
for (let col = 0; col < this.cols; col++) {
const cell = document.createElement('td');
cell.id = `cell-${col}-${row}`;
cell.style.width = `${CELL_WIDTH}px`;
cell.style.height = `${CELL_HEIGHT}px`;
cell.style.borderRight = `1px solid ${COLORS.gridline}`;
cell.style.borderBottom = `1px solid ${COLORS.gridline}`;
cell.style.padding = '0 4px';
cell.style.cursor = 'pointer';
cell.style.whiteSpace = 'nowrap';
cell.style.overflow = 'hidden';
cell.style.textAlign = 'right';
const cellRef = this.refToString(col, row);
cell.dataset.ref = cellRef;
cell.addEventListener('click', () => {
this.selectCell({ col, row });
});
cell.addEventListener('dblclick', () => {
this.editCell({ col, row });
});
tableRow.appendChild(cell);
}
this.table.appendChild(tableRow);
}
this.container.appendChild(this.table);
this.updateFormulaBar();
}
/* Cell selection */
selectCell(ref) {
if (this.editingCell) {
this.commitEdit();
}
if (this.selectedCell) {
const oldCell = this.getDOMCell(this.selectedCell.col, this.selectedCell.row);
if (oldCell) {
oldCell.style.outline = 'none';
}
}
this.selectedCell = ref;
const newCell = this.getDOMCell(ref.col, ref.row);
if (newCell) {
newCell.style.outline = `2px solid ${COLORS.selection}`;
newCell.style.outlineOffset = '-2px';
}
this.updateFormulaBar();
}
/* Editing */
editCell(ref) {
this.selectCell(ref);
const cell = this.grid[ref.row][ref.col];
const domCell = this.getDOMCell(ref.col, ref.row);
if (!domCell)
return;
cell.isEditing = true;
this.editingCell = ref;
domCell.innerHTML = '';
const input = document.createElement('input');
input.value = cell.raw;
input.style.width = '100%';
input.style.height = '100%';
input.style.border = 'none';
input.style.outline = 'none';
input.style.padding = '0 4px';
input.style.fontFamily = 'inherit';
input.style.fontSize = 'inherit';
domCell.appendChild(input);
input.focus();
input.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.commitEdit(true);
this.selectCell({ col: ref.col, row: ref.row + 1 });
}
else if (event.key === 'Tab') {
event.preventDefault();
this.commitEdit(true);
this.selectCell({ col: ref.col + 1, row: ref.row });
}
else if (event.key === 'Escape') {
this.cancelEdit();
}
else {
this.formulaBar.value = input.value;
this.formulaBar.dispatchEvent(new Event('input', { bubbles: true }));
}
});
}
commitEdit(shouldRecalc = true) {
if (!this.editingCell)
return;
const { col, row } = this.editingCell;
const cell = this.grid[row][col];
const domCell = this.getDOMCell(col, row);
if (!domCell)
return;
const input = domCell.querySelector('input');
cell.raw = input.value;
cell.isEditing = false;
this.editingCell = null;
if (shouldRecalc) {
this.calculateCell({ col, row });
}
this.renderCell(col, row);
this.updateFormulaBar();
}
cancelEdit() {
if (!this.editingCell)
return;
const { col, row } = this.editingCell;
const cell = this.grid[row][col];
const domCell = this.getDOMCell(col, row);
if (!domCell)
return;
cell.isEditing = false;
this.editingCell = null;
this.renderCell(col, row);
this.updateFormulaBar();
}
/* Formula bar */
updateFormulaBar() {
if (this.selectedCell) {
const { col, row } = this.selectedCell;
const cell = this.grid[row][col];
if (!this.formulaBar) {
this.setupFormulaBar();
}
if (this.refDisplay) {
this.refDisplay.textContent = this.refToString(col, row);
}
this.formulaBar.value = cell.raw;
}
}
setupFormulaBar() {
const container = document.getElementById('formula-bar');
container.innerHTML = '';
container.style.padding = '4px';
container.style.background = '#f5f5f5';
container.style.borderBottom = `1px solid ${COLORS.gridline}`;
container.style.display = 'flex';
container.style.alignItems = 'center';
container.style.gap = '8px';
this.refDisplay = document.createElement('span');
this.refDisplay.style.fontWeight = 'bold';
container.appendChild(this.refDisplay);
this.formulaBar = document.createElement('input');
this.formulaBar.style.flex = '1';
this.formulaBar.style.padding = '4px';
this.formulaBar.style.fontFamily = 'monospace';
this.formulaBar.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.setCellRaw(this.formulaBar.value);
this.updateFormulaBar();
}
});
this.formulaBar.addEventListener('input', () => {
if (this.editingCell) {
const domCell = this.getDOMCell(this.editingCell.col, this.editingCell.row);
if (domCell) {
const input = domCell.querySelector('input');
if (input) {
input.value = this.formulaBar.value;
}
}
}
});
container.appendChild(this.formulaBar);
}
/* Toolbar */
setupToolbar() {
const container = document.getElementById('toolbar');
container.innerHTML = '';
container.style.padding = '8px';
container.style.background = '#fafafa';
container.style.borderBottom = `1px solid ${COLORS.gridline}`;
container.style.display = 'flex';
container.style.gap = '8px';
const insertRowBtn = document.createElement('button');
insertRowBtn.textContent = 'Insert Row';
insertRowBtn.addEventListener('click', () => {
if (this.selectedCell) {
this.insertRow(this.selectedCell.row);
}
});
container.appendChild(insertRowBtn);
const insertColBtn = document.createElement('button');
insertColBtn.textContent = 'Insert Column';
insertColBtn.addEventListener('click', () => {
if (this.selectedCell) {
this.insertColumn(this.selectedCell.col);
}
});
container.appendChild(insertColBtn);
const deleteRowBtn = document.createElement('button');
deleteRowBtn.textContent = 'Delete Row';
deleteRowBtn.addEventListener('click', () => {
if (this.selectedCell) {
this.deleteRow(this.selectedCell.row);
}
});
container.appendChild(deleteRowBtn);
const deleteColBtn = document.createElement('button');
deleteColBtn.textContent = 'Delete Column';
deleteColBtn.addEventListener('click', () => {
if (this.selectedCell) {
this.deleteColumn(this.selectedCell.col);
}
});
container.appendChild(deleteColBtn);
const clearCellBtn = document.createElement('button');
clearCellBtn.textContent = 'Clear Cell';
clearCellBtn.addEventListener('click', () => {
if (this.selectedCell) {
this.clearCell(this.selectedCell);
}
});
container.appendChild(clearCellBtn);
const saveBtn = document.createElement('button');
saveBtn.textContent = 'Save';
saveBtn.addEventListener('click', () => {
this.saveToLocalStorage();
});
container.appendChild(saveBtn);
}
/* Menu helpers */
showRowMenu(event, row) {
const menu = document.createElement('div');
menu.style.position = 'fixed';
menu.style.left = `${event.clientX}px`;
menu.style.top = `${event.clientY}px`;
menu.style.background = 'white';
menu.style.border = `1px solid ${COLORS.gridline}`;
menu.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)';
menu.style.padding = '4px 0';
menu.style.zIndex = '1000';
const insertOption = document.createElement('div');
insertOption.textContent = 'Insert Row Above';
insertOption.style.padding = '8px 16px';
insertOption.style.cursor = 'pointer';
insertOption.addEventListener('click', () => {
this.insertRow(row);
menu.remove();
});
menu.appendChild(insertOption);
const deleteOption = document.createElement('div');
deleteOption.textContent = 'Delete Row';
deleteOption.style.padding = '8px 16px';
deleteOption.style.cursor = 'pointer';
deleteOption.addEventListener('click', () => {
this.deleteRow(row);
menu.remove();
});
menu.appendChild(deleteOption);
document.body.appendChild(menu);
setTimeout(() => {
document.addEventListener('click', () => menu.remove(), { once: true });
}, 10);
}
showColumnMenu(event, col) {
const menu = document.createElement('div');
menu.style.position = 'fixed';
menu.style.left = `${event.clientX}px`;
menu.style.top = `${event.clientY}px`;
menu.style.background = 'white';
menu.style.border = `1px solid ${COLORS.gridline}`;
menu.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)';
menu.style.padding = '4px 0';
menu.style.zIndex = '1000';
const insertOption = document.createElement('div');
insertOption.textContent = 'Insert Column Left';
insertOption.style.padding = '8px 16px';
insertOption.style.cursor = 'pointer';
insertOption.addEventListener('click', () => {
this.insertColumn(col);
menu.remove();
});
menu.appendChild(insertOption);
const deleteOption = document.createElement('div');
deleteOption.textContent = 'Delete Column';
deleteOption.style.padding = '8px 16px';
deleteOption.style.cursor = 'pointer';
deleteOption.addEventListener('click', () => {
this.deleteColumn(col);
menu.remove();
});
menu.appendChild(deleteOption);
document.body.appendChild(menu);
setTimeout(() => {
document.addEventListener('click', () => menu.remove(), { once: true });
}, 10);
}
/* DOM helpers */
getDOMCell(col, row) {
return document.getElementById(`cell-${col}-${row}`);
}
setCellRaw(raw) {
if (!this.selectedCell)
return;
const { col, row } = this.selectedCell;
const cell = this.grid[row][col];
cell.raw = raw;
this.calculateCell({ col, row });
this.renderCell(col, row);
}
clearCell(ref) {
const cell = this.grid[ref.row][ref.col];
cell.raw = '';
cell.display = '';
cell.value = null;
cell.formula = null;
cell.error = undefined;
this.renderCell(ref.col, ref.row);
this.recalcDependents(ref);
}
/* Formula engine: tokenizer */
tokenize(text) {
const tokens = [];
let i = 0;
while (i < text.length) {
const ch = text[i];
if (ch === ' ' || ch === '\t') {
i++;
continue;
}
if (ch >= '0' && ch <= '9') {
let num = '';
while (i < text.length && (text[i] >= '0' && text[i] <= '9' || text[i] === '.')) {
num += text[i];
i++;
}
tokens.push({ type: TokenType.NUMBER, value: num });
continue;
}
if (ch >= 'A' && ch <= 'Z') {
let ident = '';
while (i < text.length && (text[i] >= 'A' && text[i] <= 'Z' || text[i] >= '0' && text[i] <= '9')) {
ident += text[i];
i++;
}
if (['SUM', 'AVG', 'AVERAGE', 'MIN', 'MAX', 'COUNT'].includes(ident.toUpperCase())) {
tokens.push({ type: TokenType.FUNC, value: ident.toUpperCase() });
}
else {
tokens.push({ type: TokenType.IDENT, value: ident });
}
continue;
}
if (ch === '(') {
tokens.push({ type: TokenType.LPAREN, value: '(' });
i++;
continue;
}
if (ch === ')') {
tokens.push({ type: TokenType.RPAREN, value: ')' });
i++;
continue;
}
if (ch === ',') {
tokens.push({ type: TokenType.COMMA, value: ',' });
i++;
continue;
}
if (ch === ':') {
tokens.push({ type: TokenType.COLON, value: ':' });
i++;
continue;
}
if (ch === '+' || ch === '-' || ch === '*' || ch === '/') {
tokens.push({ type: TokenType.OPERATOR, value: ch });
i++;
continue;
}
throw new Error(`Unexpected character: ${ch}`);
}
return tokens;
}
/* Formula engine: recursive-descent parser */
parse(expr) {
try {
const tokens = this.tokenize(expr);
const context = { tokens, pos: 0 };
const result = this.parseExpression(context);
if (context.pos < tokens.length) {
return { type: ExprType.ERROR, error: '#ERR!' };
}
return result;
}
catch (err) {
return { type: ExprType.ERROR, error: '#ERR!' };
}
}
parseExpression(ctx) {
let left = this.parseTerm(ctx);
while (ctx.pos < ctx.tokens.length &&
ctx.tokens[ctx.pos].type === TokenType.OPERATOR &&
(ctx.tokens[ctx.pos].value === '+' || ctx.tokens[ctx.pos].value === '-')) {
const op = ctx.tokens[ctx.pos].value;
ctx.pos++;
const right = this.parseTerm(ctx);
left = { type: ExprType.BINARY, op, left, right };
}
return left;
}
parseTerm(ctx) {
let left = this.parseUnary(ctx);
while (ctx.pos < ctx.tokens.length &&
ctx.tokens[ctx.pos].type === TokenType.OPERATOR &&
(ctx.tokens[ctx.pos].value === '*' || ctx.tokens[ctx.pos].value === '/')) {
const op = ctx.tokens[ctx.pos].value;
ctx.pos++;
const right = this.parseUnary(ctx);
left = { type: ExprType.BINARY, op, left, right };
}
return left;
}
parseUnary(ctx) {
if (ctx.pos < ctx.tokens.length &&
ctx.tokens[ctx.pos].type === TokenType.OPERATOR &&
ctx.tokens[ctx.pos].value === '-') {
ctx.pos++;
const operand = this.parseUnary(ctx);
return { type: ExprType.UNARY, op: '-', left: operand };
}
return this.parsePrimary(ctx);
}
parsePrimary(ctx) {
if (ctx.pos < ctx.tokens.length &&
ctx.tokens[ctx.pos].type === TokenType.NUMBER) {
const token = ctx.tokens[ctx.pos];
ctx.pos++;
return { type: ExprType.NUMBER, value: parseFloat(token.value) };
}
if (ctx.pos < ctx.tokens.length &&
ctx.tokens[ctx.pos].type === TokenType.IDENT) {
const ref = this.parseCellRef(ctx.tokens[ctx.pos].value);
if (!ref) {
return { type: ExprType.ERROR, error: '#REF!' };
}
ctx.pos++;
if (ctx.pos < ctx.tokens.length &&
ctx.tokens[ctx.pos].type === TokenType.COLON) {
ctx.pos++;
if (ctx.pos < ctx.tokens.length &&
ctx.tokens[ctx.pos].type === TokenType.IDENT) {
const endRef = this.parseCellRef(ctx.tokens[ctx.pos].value);
ctx.pos++;
if (!endRef) {
return { type: ExprType.ERROR, error: '#REF!' };
}
return { type: ExprType.RANGE, col: ref.col, row: ref.row, endCol: endRef.col, endRow: endRef.row };
}
return { type: ExprType.ERROR, error: '#REF!' };
}
return { type: ExprType.REFERENCE, col: ref.col, row: ref.row };
}
if (ctx.pos < ctx.tokens.length &&
ctx.tokens[ctx.pos].type === TokenType.FUNC) {
const func = ctx.tokens[ctx.pos].value;
ctx.pos++;
if (ctx.pos < ctx.tokens.length &&
ctx.tokens[ctx.pos].type === TokenType.LPAREN) {
ctx.pos++;
const args = [];
while (ctx.pos < ctx.tokens.length &&
ctx.tokens[ctx.pos].type !== TokenType.RPAREN) {
if (ctx.pos < ctx.tokens.length &&
ctx.tokens[ctx.pos].type === TokenType.COMMA) {
ctx.pos++;
continue;
}
args.push(this.parseExpression(ctx));
}
if (ctx.pos < ctx.tokens.length &&
ctx.tokens[ctx.pos].type === TokenType.RPAREN) {
ctx.pos++;
return { type: ExprType.FUNCTION, func, args };
}
}
return { type: ExprType.ERROR, error: '#ERR!' };
}
if (ctx.pos < ctx.tokens.length &&
ctx.tokens[ctx.pos].type === TokenType.LPAREN) {
ctx.pos++;
const expr = this.parseExpression(ctx);
if (ctx.pos < ctx.tokens.length &&
ctx.tokens[ctx.pos].type === TokenType.RPAREN) {
ctx.pos++;
return expr;
}
return { type: ExprType.ERROR, error: '#ERR!' };
}
return { type: ExprType.ERROR, error: '#ERR!' };
}
parseCellRef(ref) {
const match = ref.match(/^([A-Z]+)(\d+)$/);
if (!match)
return null;
const col = this.parseColumnName(match[1]);
const row = parseInt(match[2]) - 1;
if (col < 0 || col >= this.cols || row < 0 || row >= this.rows) {
return null;
}
return { col, row };
}
parseColumnName(name) {
let col = 0;
for (const ch of name) {
col = col * 26 + (ch.charCodeAt(0) - 65);
}
return col;
}
/* Formula engine: evaluator */
evaluateExpression(expr) {
switch (expr.type) {
case ExprType.NUMBER:
return expr.value;
case ExprType.REFERENCE: {
const cell = this.grid[expr.row][expr.col];
if (cell.error)
return null;
return cell.value;
}
case ExprType.RANGE: {
const values = [];
for (let row = Math.min(expr.row, expr.endRow); row <= Math.max(expr.row, expr.endRow); row++) {
for (let col = Math.min(expr.col, expr.endCol); col <= Math.max(expr.col, expr.endCol); col++) {
const cell = this.grid[row][col];
if (cell.value !== null && !cell.error) {
values.push(cell.value);
}
}
}
return values.length > 0 ? values[0] : null;
}
case ExprType.FUNCTION: {
const func = expr.func.toUpperCase();
const args = expr.args;
const values = [];
for (const arg of args) {
const argValues = this.collectValues(arg);
values.push(...argValues);
}
switch (func) {
case 'SUM':
return values.reduce((sum, val) => sum + val, 0);
case 'AVG':
case 'AVERAGE':
return values.length > 0 ? values.reduce((sum, val) => sum + val, 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:
return null;
}
}
case ExprType.BINARY: {
const left = this.evaluateExpression(expr.left);
if (left === null)
return null;
const right = this.evaluateExpression(expr.right);
if (right === null)
return null;
switch (expr.op) {
case '+':
return left + right;
case '-':
return left - right;
case '*':
return left * right;
case '/':
if (right === 0)
return null;
return left / right;
}
}
case ExprType.UNARY: {
const operand = this.evaluateExpression(expr.left);
if (operand === null)
return null;
return -operand;
}
case ExprType.ERROR:
return null;
}
return null;
}
collectValues(expr) {
const values = [];
switch (expr.type) {
case ExprType.NUMBER:
values.push(expr.value);
break;
case ExprType.REFERENCE: {
const cell = this.grid[expr.row][expr.col];
if (cell.value !== null && !cell.error) {
values.push(cell.value);
}
break;
}
case ExprType.RANGE: {
for (let row = Math.min(expr.row, expr.endRow); row <= Math.max(expr.row, expr.endRow); row++) {
for (let col = Math.min(expr.col, expr.endCol); col <= Math.max(expr.col, expr.endCol); col++) {
const cell = this.grid[row][col];
if (cell.value !== null && !cell.error) {
values.push(cell.value);
}
}
}
break;
}
case ExprType.FUNCTION:
for (const arg of expr.args) {
values.push(...this.collectValues(arg));
}
break;
}
return values;
}
/* Dependency graph */
getCellRefs(expr) {
const refs = [];
switch (expr.type) {
case ExprType.REFERENCE:
refs.push({ col: expr.col, row: expr.row });
break;
case ExprType.RANGE:
for (let row = Math.min(expr.row, expr.endRow); row <= Math.max(expr.row, expr.endRow); row++) {
for (let col = Math.min(expr.col, expr.endCol); col <= Math.max(expr.col, expr.endCol); col++) {
refs.push({ col, row });
}
}
break;
case ExprType.FUNCTION:
for (const arg of expr.args) {
refs.push(...this.getCellRefs(arg));
}
break;
case ExprType.BINARY:
refs.push(...this.getCellRefs(expr.left));
refs.push(...this.getCellRefs(expr.right));
break;
case ExprType.UNARY:
refs.push(...this.getCellRefs(expr));
break;
}
return refs;
}
addDependency(from, to) {
const fromRef = this.refToString(from.col, from.row);
const toRef = this.refToString(to.col, to.row);
if (!this.dependencies.has(fromRef)) {
this.dependencies.set(fromRef, new Set());
}
this.dependencies.get(fromRef).add(toRef);
if (!this.dependents.has(toRef)) {
this.dependents.set(toRef, new Set());
}
this.dependents.get(toRef).add(fromRef);
}
removeDependencies(ref) {
const refStr = this.refToString(ref.col, ref.row);
// Remove from dependents
for (const [key, dependents] of this.dependents) {
dependents.delete(refStr);
}
// Remove from dependencies
const dependencies = this.dependencies.get(refStr);
if (dependencies) {
for (const dep of dependencies) {
const dependents = this.dependents.get(dep);
if (dependents) {
dependents.delete(refStr);
}
}
this.dependencies.delete(refStr);
}
}
detectCycle(ref, visited, path) {
const refStr = this.refToString(ref.col, ref.row);
if (visited.has(refStr)) {
path.push(refStr);
return true;
}
visited.add(refStr);
path.push(refStr);
const deps = this.dependencies.get(refStr);
if (deps) {
for (const dep of deps) {
if (this.detectCycle(this.parseRef(dep), visited, path)) {
return true;
}
}
}
visited.delete(refStr);
path.pop();
return false;
}
parseRef(ref) {
const match = ref.match(/^([A-Z]+)(\d+)$/);
if (!match)
throw new Error(`Invalid cell reference: ${ref}`);
return {
col: this.parseColumnName(match[1]),
row: parseInt(match[2]) - 1,
};
}
/* Calculation */
calculateCell(ref) {
const cell = this.grid[ref.row][ref.col];
const raw = cell.raw.trim();
this.removeDependencies(ref);
if (raw === '') {
cell.display = '';
cell.value = null;
cell.formula = null;
cell.error = undefined;
this.recalcDependents(ref);
this.renderCell(ref.col, ref.row);
return;
}
if (raw.startsWith('=')) {
const formulaText = raw.substring(1);
const formula = this.parse(formulaText);
cell.formula = formula;
if (formula.type === ExprType.ERROR) {
cell.error = formula.error;
cell.display = formula.error;
cell.value = null;
this.recalcDependents(ref);
this.renderCell(ref.col, ref.row);
return;
}
// Build dependencies
const refs = this.getCellRefs(formula);
for (const r of refs) {
this.addDependency(ref, r);
}
// Check for cycles
if (this.detectCycle(ref, new Set(), [])) {
cell.error = '#CYCLE!';
cell.display = '#CYCLE!';
cell.value = null;
this.recalcDependents(ref);
this.renderCell(ref.col, ref.row);
return;
}
// Evaluate
const result = this.evaluateExpression(formula);
if (result === null) {
cell.error = '#REF!';
cell.display = '#REF!';
cell.value = null;
}
else if (isNaN(result)) {
cell.error = '#ERR!';
cell.display = '#ERR!';
cell.value = null;
}
else {
cell.error = '';
cell.display = result.toString();
cell.value = !!result ? result : null;
}
this.recalcDependents(ref);
this.renderCell(ref.col, ref.row);
}
else {
cell.display = raw;
cell.value = null;
cell.formula = null;
cell.error = undefined;
this.recalcDependents(ref);
this.renderCell(ref.col, ref.row);
}
}
recalcDependents(ref) {
const refStr = this.refToString(ref.col, ref.row);
const dependents = this.dependents.get(refStr);
if (!dependents)
return;
// Topological sort dependents
const sorted = [];
const visited = new Set();
const visit = (cellRef) => {
const refStr = this.refToString(cellRef.col, cellRef.row);
if (visited.has(refStr))
return;
visited.add(refStr);
const cellDeps = this.dependencies.get(refStr);
if (cellDeps) {
for (const dep of cellDeps) {
visit(this.parseRef(dep));
}
}
sorted.push(cellRef);
};
for (const dep of dependents) {
visit(this.parseRef(dep));
}
for (const cellRef of sorted) {
this.calculateCell(cellRef);
}
}
/* Rendering */
renderCell(col, row) {
const domCell = this.getDOMCell(col, row);
if (!domCell)
return;
const cell = this.grid[row][col];
if (cell.isEditing)
return;
domCell.textContent = cell.display;
domCell.style.textAlign = cell.value !== null ? 'right' : 'left';
if (cell.error) {
domCell.style.color = COLORS.error;
}
else {
domCell.style.color = COLORS.text;
}
}
/* Row/column operations */
insertRow(row) {
const newRow = [];
for (let col = 0; col < this.cols; col++) {
newRow.push({ raw: '', display: '', value: null, formula: null, isEditing: false });
}
this.grid.splice(row, 0, newRow);
// Rewrite formulas
this.rewriteFormulasForRowInsert(row);
this.render();
}
deleteRow(row) {
this.grid.splice(row, 1);
// Rewrite formulas
this.rewriteFormulasForRowDelete(row);
this.render();
}
insertColumn(col) {
for (let row = 0; row < this.rows; row++) {
this.grid[row].splice(col, 0, { raw: '', display: '', value: null, formula: null, isEditing: false });
}
// Rewrite formulas
this.rewriteFormulasForColInsert(col);
this.render();
}
deleteColumn(col) {
for (let row = 0; row < this.rows; row++) {
this.grid[row].splice(col, 1);
}
// Rewrite formulas
this.rewriteFormulasForColDelete(col);
this.render();
}
rewriteFormulasForRowInsert(row) {
for (let r = 0; r < this.rows; r++) {
for (let c = 0; c < this.cols; c++) {
const cell = this.grid[r][c];
if (cell.raw.startsWith('=')) {
cell.raw = cell.raw.replace(/([A-Z]+)(\d+)/g, (_, colLetter, rowNum) => {
const existingRow = parseInt(rowNum) - 1;
const newRow = existingRow >= row ? existingRow + 1 : existingRow;
return colLetter + (newRow + 1);
});
this.calculateCell({ col: c, row: r });
}
}
}
}
rewriteFormulasForRowDelete(row) {
for (let r = 0; r < this.rows; r++) {
for (let c = 0; c < this.cols; c++) {
const cell = this.grid[r][c];
if (cell.raw.startsWith('=')) {
cell.raw = cell.raw.replace(/([A-Z]+)(\d+)/g, (_, colLetter, rowNum) => {
const existingRow = parseInt(rowNum) - 1;
if (existingRow === row) {
return colLetter + '0';
}
const newRow = existingRow > row ? existingRow - 1 : existingRow;
return colLetter + (newRow + 1);
});
this.calculateCell({ col: c, row: r });
}
}
}
}
rewriteFormulasForColInsert(col) {
for (let r = 0; r < this.rows; r++) {
for (let c = 0; c < this.cols; c++) {
const cell = this.grid[r][c];
if (cell.raw.startsWith('=')) {
cell.raw = cell.raw.replace(/([A-Z]+)(\d+)/g, (_, colLetter, rowNum) => {
const existingCol = this.parseColumnName(colLetter);
const newCol = existingCol >= col ? existingCol + 1 : existingCol;
return this.colToLetter(newCol) + rowNum;
});
this.calculateCell({ col: c, row: r });
}
}
}
}
rewriteFormulasForColDelete(col) {
for (let r = 0; r < this.rows; r++) {
for (let c = 0; c < this.cols; c++) {
const cell = this.grid[r][c];
if (cell.raw.startsWith('=')) {
cell.raw = cell.raw.replace(/([A-Z]+)(\d+)/g, (_, colLetter, rowNum) => {
const existingCol = this.parseColumnName(colLetter);
if (existingCol === col) {
return '??' + rowNum;
}
const newCol = existingCol > col ? existingCol - 1 : existingCol;
return this.colToLetter(newCol) + rowNum;
});
this.calculateCell({ col: c, row: r });
}
}
}
}
/* Editing hooks */
enableEditing() {
this.setupToolbar();
document.addEventListener('keydown', (event) => {
if (!this.selectedCell)
return;
const { col, row } = this.selectedCell;
if (event.key === 'ArrowUp' && row > 0) {
event.preventDefault();
this.selectCell({ col, row: row - 1 });
}
else if (event.key === 'ArrowDown' && row < this.rows - 1) {
event.preventDefault();
this.selectCell({ col, row: row + 1 });
}
else if (event.key === 'ArrowLeft' && col > 0) {
event.preventDefault();
this.selectCell({ col: col - 1, row });
}
else if (event.key === 'ArrowRight' && col < this.cols - 1) {
event.preventDefault();
this.selectCell({ col: col + 1, row });
}
else if (event.key === 'Delete') {
event.preventDefault();
this.clearCell({ col, row });
}
else if (!this.editingCell && event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
this.editCell({ col, row });
const domCell = this.getDOMCell(col, row);
const input = domCell?.querySelector('input');
if (input) {
input.value = event.key;
}
}
});
document.addEventListener('paste', (event) => {
if (!this.editingCell) {
this.editCell(this.selectedCell);
}
});
}
/* localStorage */
saveToLocalStorage() {
const data = [];
for (let row = 0; row < this.rows; row++) {
const rowCells = [];
for (let col = 0; col < this.cols; col++) {
rowCells.push(this.grid[row][col].raw);
}
data.push(rowCells);
}
localStorage.setItem('spreadsheet', JSON.stringify(data));
}
loadFromLocalStorage() {
const data = localStorage.getItem('spreadsheet');
if (!data)
return;
const parsed = JSON.parse(data);
for (let row = 0; row < this.rows; row++) {
for (let col = 0; col < this.cols; col++) {
if (parsed[row] && parsed[row][col]) {
this.grid[row][col].raw = parsed[row][col];
this.calculateCell({ col, row });
this.renderCell(col, row);
}
}
}
}
}
exports.Spreadsheet = Spreadsheet;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"bartowski/ProCreations_grug-27b-GGUF-IQ4","provider":"brigid","persona":"typescript","sessionId":"d6a0f62f-36a2-484c-be8b-c9e6f2ba58ce","tokensIn":712889,"tokensOut":14159,"tokensTotal":727048,"cost":0,"turns":27,"toolCalls":27,"failedToolCalls":7,"timestamp":"2026-08-05T17:41:35.587Z"} -->