Spreadsheet
ThinkingCap Qwen3.6 27B GGUF · 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 45,586 bytes · SHA-256 fbffd1d317c8
<!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 ── */
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
font-size: 13px; color: #202124; overflow: hidden;
height: 100vh; display: flex; flex-direction: column;
}
#toolbar {
display: flex; align-items: center; gap: 4px;
padding: 4px 8px; background: #f8f9fa;
border-bottom: 1px solid #d4d4d4;
min-height: 32px; flex-shrink: 0;
}
#toolbar button {
background: #fff; border: 1px solid #d4d4d4;
border-radius: 3px; padding: 3px 10px;
font-size: 12px; cursor: pointer; color: #444;
}
#toolbar button:hover { background: #e8eaed; }
#formula-bar {
display: flex; align-items: center;
padding: 4px 8px; background: #f8f9fa;
border-bottom: 1px solid #d4d4d4; flex-shrink: 0;
}
#cell-ref {
min-width: 50px; text-align: center; font-weight: 600;
color: #666; border-right: 1px solid #d4d4d4;
padding-right: 8px; flex-shrink: 0;
}
#formula-input {
flex: 1; border: 1px solid #d4d4d4;
padding: 4px 8px; font-size: 13px;
font-family: 'Segoe UI', system-ui, sans-serif;
border-radius: 2px; outline: none;
}
#formula-input:focus {
border-color: #1a73e8; box-shadow: 0 0 0 1px #1a73e8;
}
#grid-container { flex: 1; overflow: auto; }
#grid { border-collapse: collapse; table-layout: fixed; }
#grid th, #grid td {
border: 1px solid #d4d4d4; padding: 0;
font-size: 13px; white-space: nowrap;
overflow: hidden; text-overflow: ellipsis;
}
.col-header {
background: #f8f9fa; color: #666; font-weight: 500;
text-align: center; position: sticky; top: 0;
z-index: 2; height: 25px; min-width: 40px; user-select: none;
}
.col-header-active { background: #d2e3fc; color: #1a73e8; font-weight: 600; }
.row-header {
background: #f8f9fa; color: #666; font-weight: 500;
text-align: center; position: sticky; left: 0;
z-index: 1; min-width: 40px; width: 40px; user-select: none;
}
.row-header-active { background: #d2e3fc; color: #1a73e8; font-weight: 600; }
.corner {
position: sticky; top: 0; left: 0; z-index: 3;
background: #f8f9fa; width: 40px; min-width: 40px;
}
.cell {
padding: 2px 4px; height: 25px;
min-height: 25px; cursor: cell;
}
.cell-number { text-align: right; }
.cell-error { color: #c0392b; font-weight: 500; }
.cell-selected {
outline: 2px solid #1a73e8;
outline-offset: -1px; position: relative; z-index: 1;
}
.cell-input {
width: 100%; height: 100%; border: none; outline: none;
font-size: 13px; font-family: 'Segoe UI', system-ui, sans-serif;
padding: 2px 4px;
}
.context-menu {
position: fixed; background: #fff;
border: 1px solid #d4d4d4; border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
z-index: 100; min-width: 160px; padding: 4px 0;
}
.context-menu-item {
padding: 6px 16px; cursor: pointer;
font-size: 13px; color: #333;
}
.context-menu-item:hover { background: #e8eaed; }
</style>
</head>
<body>
<div id="toolbar"></div>
<div id="formula-bar">
<span id="cell-ref"></span>
<input id="formula-input" type="text" spellcheck="false" autocomplete="off">
</div>
<div id="grid-container">
<table id="grid"></table>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./config":"src/config.ts","./state":"src/state.ts","./formula":"src/formula.ts","./renderer":"src/renderer.ts"},"src/state.ts":{"./config":"src/config.ts"},"src/formula.ts":{"./config":"src/config.ts","./state":"src/state.ts"},"src/renderer.ts":{"./config":"src/config.ts","./state":"src/state.ts","./formula":"src/formula.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 formula_1 = require("./formula");
const renderer_1 = require("./renderer");
// ---- Init ----
const sheet = new state_1.Sheet(config_1.CONFIG.COLS, config_1.CONFIG.ROWS);
const depGraph = new formula_1.DepGraph();
(0, renderer_1.setSheet)(sheet, depGraph);
(0, renderer_1.setSheetSize)(sheet.cols, sheet.rows);
sheet.load();
(0, formula_1.fullRecalc)(sheet, depGraph);
(0, renderer_1.buildGrid)(sheet);
(0, renderer_1.renderAll)(sheet);
// Expose for testing
window.__sheet = sheet;
window.__depGraph = depGraph;
window.__finishEdit = renderer_1.finishEdit;
window.__renderAll = renderer_1.renderAll;
window.__recalcAndCascade = formula_1.recalcAndCascade;
window.__rebuildDeps = formula_1.rebuildDeps;
window.__cellKey = state_1.cellKey;
window.__fullRecalc = formula_1.fullRecalc;
// Select A1 initially
(0, renderer_1.setSelected)(0, 0);
(0, renderer_1.updateCellRefDisplay)();
// ---- Toolbar buttons ----
const toolbar = document.getElementById('toolbar');
function makeToolbarButton(label, action) {
const btn = document.createElement('button');
btn.textContent = label;
btn.addEventListener('click', () => { action(); });
return btn;
}
toolbar.appendChild(makeToolbarButton('Insert Row', () => insertRowAt((0, renderer_1.getSelected)()?.row ?? 0)));
toolbar.appendChild(makeToolbarButton('Delete Row', () => deleteRowAt((0, renderer_1.getSelected)()?.row ?? 0)));
toolbar.appendChild(makeToolbarButton('Insert Col', () => insertColAt((0, renderer_1.getSelected)()?.col ?? 0)));
toolbar.appendChild(makeToolbarButton('Delete Col', () => deleteColAt((0, renderer_1.getSelected)()?.col ?? 0)));
// ---- Grid events ----
const grid = document.getElementById('grid');
grid.addEventListener('click', (e) => {
const td = e.target.closest('td.cell');
if (!td)
return;
const c = parseInt(td.dataset.col || '0', 10);
const r = parseInt(td.dataset.row || '0', 10);
(0, renderer_1.setSelected)(c, r);
(0, renderer_1.updateCellRefDisplay)();
const cellData = sheet.get(c, r);
const formulaInput = document.getElementById('formula-input');
if (formulaInput)
formulaInput.value = cellData.raw;
});
grid.addEventListener('dblclick', (e) => {
const td = e.target.closest('td.cell');
if (!td)
return;
const c = parseInt(td.dataset.col || '0', 10);
const r = parseInt(td.dataset.row || '0', 10);
(0, renderer_1.setSelected)(c, r);
(0, renderer_1.updateCellRefDisplay)();
(0, renderer_1.startEdit)(sheet);
});
// Header right-click → context menu
grid.addEventListener('contextmenu', (e) => {
e.preventDefault();
const th = e.target.closest('th.col-header');
const rowHeader = e.target.closest('td.row-header');
if (th) {
const col = parseInt(th.dataset.col || '0', 10);
showContextMenu(e.clientX, e.clientY, [
{ label: 'Insert Column', action: () => insertColAt(col) },
{ label: 'Delete Column', action: () => deleteColAt(col) },
]);
}
else if (rowHeader) {
const row = parseInt(rowHeader.dataset.row || '0', 10);
showContextMenu(e.clientX, e.clientY, [
{ label: 'Insert Row', action: () => insertRowAt(row) },
{ label: 'Delete Row', action: () => deleteRowAt(row) },
]);
}
});
function showContextMenu(x, y, items) {
removeContextMenu();
const menu = document.createElement('div');
menu.className = 'context-menu';
menu.style.left = x + 'px';
menu.style.top = y + 'px';
for (const item of items) {
const div = document.createElement('div');
div.className = 'context-menu-item';
div.textContent = item.label;
div.addEventListener('click', () => { item.action(); removeContextMenu(); });
menu.appendChild(div);
}
document.body.appendChild(menu);
}
function removeContextMenu() {
const existing = document.querySelector('.context-menu');
if (existing)
existing.remove();
}
document.addEventListener('click', (e) => {
if (!e.target.closest('.context-menu'))
removeContextMenu();
});
// ---- Formula bar ----
const formulaInputEl = document.getElementById('formula-input');
formulaInputEl.addEventListener('keydown', (e) => {
const sel = (0, renderer_1.getSelected)();
if (!sel)
return;
if (e.key === 'Enter') {
e.preventDefault();
const { col: c, row: r } = sel;
const raw = formulaInputEl.value;
(0, renderer_1.finishEdit)(sheet, c, r, raw, depGraph);
(0, renderer_1.moveSelection)(0, 1);
}
});
formulaInputEl.addEventListener('input', () => {
const input = grid.querySelector('.cell-input');
if (input)
input.value = formulaInputEl.value;
});
// ---- Keyboard shortcuts ----
document.addEventListener('keydown', (e) => {
if (document.activeElement === formulaInputEl)
return;
if (document.activeElement?.classList.contains('cell-input'))
return;
const sel = (0, renderer_1.getSelected)();
if (!sel)
return;
if (e.key === 'ArrowDown') {
e.preventDefault();
(0, renderer_1.moveSelection)(0, 1);
}
else if (e.key === 'ArrowUp') {
e.preventDefault();
(0, renderer_1.moveSelection)(0, -1);
}
else if (e.key === 'ArrowRight') {
e.preventDefault();
(0, renderer_1.moveSelection)(1, 0);
}
else if (e.key === 'ArrowLeft') {
e.preventDefault();
(0, renderer_1.moveSelection)(-1, 0);
}
else if (e.key === 'Tab') {
e.preventDefault();
(0, renderer_1.moveSelection)(e.shiftKey ? -1 : 1, 0);
}
else if (e.key === 'Enter') {
e.preventDefault();
if (grid.querySelector('.cell-input')) {
(0, renderer_1.moveSelection)(0, 1);
}
else {
(0, renderer_1.startEdit)(sheet);
}
}
else if (e.key === 'Delete' || e.key === 'Backspace') {
e.preventDefault();
clearCell(sel.col, sel.row);
}
else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
(0, renderer_1.startEdit)(sheet);
const input = grid.querySelector('.cell-input');
if (input) {
input.value = e.key;
formulaInputEl.value = e.key;
}
}
});
function clearCell(c, r) {
sheet.clear(c, r);
(0, formula_1.fullRecalc)(sheet, depGraph);
(0, renderer_1.renderAll)(sheet);
sheet.save();
(0, renderer_1.updateCellRefDisplay)();
formulaInputEl.value = '';
}
// ---- Formula rewriting for row/col shifts ----
function rewriteRaw(raw, colShift, rowShift) {
if (!raw.startsWith('='))
return raw;
return raw.replace(/[A-Z]\d+/g, (match) => {
const parsed = (0, state_1.parseKey)(match);
if (!parsed)
return match;
let nc = parsed[0] + colShift;
let nr = parsed[1] + rowShift;
if (nc < 0 || nc >= sheet.cols || nr < 0 || nr >= sheet.rows) {
return '#REF!';
}
return (0, state_1.cellKey)(nc, nr);
});
}
function rebuildGrid() {
(0, renderer_1.buildGrid)(sheet);
(0, renderer_1.renderAll)(sheet);
}
// ---- Row/Column insert/delete ----
function insertRowAt(rowIdx) {
const entries = new Map();
sheet.forEach((c, r, data) => {
if (r < rowIdx) {
entries.set((0, state_1.cellKey)(c, r), data);
}
else {
const newKey = (0, state_1.cellKey)(c, r + 1);
const rewritten = rewriteRaw(data.raw, 0, 1);
entries.set(newKey, { ...data, raw: rewritten });
}
});
sheet.rows++;
(0, renderer_1.setSheetSize)(sheet.cols, sheet.rows);
sheet.grid = entries;
(0, formula_1.fullRecalc)(sheet, depGraph);
rebuildGrid();
sheet.save();
}
function deleteRowAt(rowIdx) {
if (sheet.rows <= 1)
return;
const entries = new Map();
sheet.forEach((c, r, data) => {
if (r < rowIdx) {
entries.set((0, state_1.cellKey)(c, r), data);
}
else if (r > rowIdx) {
const newKey = (0, state_1.cellKey)(c, r - 1);
const rewritten = rewriteRaw(data.raw, 0, -1);
entries.set(newKey, { ...data, raw: rewritten });
}
// skip r === rowIdx
});
sheet.rows--;
(0, renderer_1.setSheetSize)(sheet.cols, sheet.rows);
sheet.grid = entries;
(0, formula_1.fullRecalc)(sheet, depGraph);
rebuildGrid();
sheet.save();
}
function insertColAt(colIdx) {
const entries = new Map();
sheet.forEach((c, r, data) => {
if (c < colIdx) {
entries.set((0, state_1.cellKey)(c, r), data);
}
else {
const newKey = (0, state_1.cellKey)(c + 1, r);
const rewritten = rewriteRaw(data.raw, 1, 0);
entries.set(newKey, { ...data, raw: rewritten });
}
});
sheet.cols++;
(0, renderer_1.setSheetSize)(sheet.cols, sheet.rows);
sheet.grid = entries;
(0, formula_1.fullRecalc)(sheet, depGraph);
rebuildGrid();
sheet.save();
}
function deleteColAt(colIdx) {
if (sheet.cols <= 1)
return;
const entries = new Map();
sheet.forEach((c, r, data) => {
if (c < colIdx) {
entries.set((0, state_1.cellKey)(c, r), data);
}
else if (c > colIdx) {
const newKey = (0, state_1.cellKey)(c - 1, r);
const rewritten = rewriteRaw(data.raw, -1, 0);
entries.set(newKey, { ...data, raw: rewritten });
}
// skip c === colIdx
});
sheet.cols--;
(0, renderer_1.setSheetSize)(sheet.cols, sheet.rows);
sheet.grid = entries;
(0, formula_1.fullRecalc)(sheet, depGraph);
rebuildGrid();
sheet.save();
}
};
// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ERROR_MSGS = exports.CONFIG = void 0;
// Tunable constants
exports.CONFIG = {
// Grid dimensions
COLS: 26,
ROWS: 100,
// Cell dimensions
CELL_WIDTH: 100,
CELL_HEIGHT: 25,
HEADER_WIDTH: 40,
HEADER_HEIGHT: 25,
// Colors
COLOR_GRID_LINE: '#d4d4d4',
COLOR_HEADER_BG: '#f8f9fa',
COLOR_HEADER_TEXT: '#666',
COLOR_SELECTION_BORDER: '#1a73e8',
COLOR_TEXT: '#202124',
COLOR_ERROR: '#c0392b',
COLOR_NUMBER: '#202124',
COLOR_ACTIVE_HEADER: '#e8eaed',
COLOR_ROW_HEADER: '#f1f3f4',
COLOR_COL_HEADER: '#f1f3f4',
};
exports.ERROR_MSGS = {
CYCLE: '#CYCLE!',
DIV0: '#DIV/0!',
REF: '#REF!',
ERR: '#ERR!',
};
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Sheet = void 0;
exports.colName = colName;
exports.colIndex = colIndex;
exports.cellKey = cellKey;
exports.parseKey = parseKey;
exports.makeEmptyCell = makeEmptyCell;
exports.makeErrorCell = makeErrorCell;
function colName(c) {
return String.fromCharCode(65 + c);
}
function colIndex(name) {
return name.charCodeAt(0) - 65;
}
function cellKey(c, r) {
return colName(c) + (r + 1);
}
function parseKey(key) {
const m = key.match(/^([A-Z])(\d+)$/);
if (!m)
return null;
const c = colIndex(m[1]);
const r = parseInt(m[2], 10) - 1;
if (r < 0)
return null;
return [c, r];
}
function makeEmptyCell() {
return { raw: '', value: '', isError: false };
}
function makeErrorCell(msg) {
return { raw: '', value: msg, isError: true };
}
class Sheet {
constructor(cols, rows) {
this.grid = new Map();
this.cols = cols;
this.rows = rows;
}
get(c, r) {
const key = cellKey(c, r);
return this.grid.get(key) || makeEmptyCell();
}
set(c, r, data) {
const key = cellKey(c, r);
this.grid.set(key, data);
}
clear(c, r) {
const key = cellKey(c, r);
this.grid.delete(key);
}
forEach(fn) {
for (const [key, data] of this.grid) {
const parsed = parseKey(key);
if (parsed)
fn(parsed[0], parsed[1], data);
}
}
save() {
const obj = {};
for (const [key, data] of this.grid) {
obj[key] = data;
}
localStorage.setItem('spreadsheet_data', JSON.stringify(obj));
localStorage.setItem('spreadsheet_cols', String(this.cols));
localStorage.setItem('spreadsheet_rows', String(this.rows));
}
load() {
const colsStr = localStorage.getItem('spreadsheet_cols');
const rowsStr = localStorage.getItem('spreadsheet_rows');
if (colsStr)
this.cols = parseInt(colsStr, 10);
if (rowsStr)
this.rows = parseInt(rowsStr, 10);
const raw = localStorage.getItem('spreadsheet_data');
if (!raw)
return;
try {
const obj = JSON.parse(raw);
this.grid = new Map(Object.entries(obj));
}
catch {
this.grid = new Map();
}
}
}
exports.Sheet = Sheet;
};
// ── module: src/formula.ts ──
__mods["src/formula.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DepGraph = void 0;
exports.tokenize = tokenize;
exports.parseFormula = parseFormula;
exports.collectRefs = collectRefs;
exports.evaluate = evaluate;
exports.recalcCell = recalcCell;
exports.recalcAndCascade = recalcAndCascade;
exports.rebuildDeps = rebuildDeps;
exports.fullRecalc = fullRecalc;
const config_1 = require("./config");
const state_1 = require("./state");
function tokenize(input) {
const toks = [];
let i = 0;
const s = input.toUpperCase();
while (i < s.length) {
if (s[i] === ' ') {
i++;
continue;
}
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++];
toks.push({ type: 'NUMBER', val: num });
}
else if (s[i] >= 'A' && s[i] <= 'Z') {
let id = '';
while (i < s.length && ((s[i] >= 'A' && s[i] <= 'Z') || (s[i] >= '0' && s[i] <= '9')))
id += s[i++];
toks.push({ type: 'IDENT', val: id });
}
else {
switch (s[i]) {
case '+':
toks.push({ type: 'PLUS', val: '+' });
i++;
break;
case '-':
toks.push({ type: 'MINUS', val: '-' });
i++;
break;
case '*':
toks.push({ type: 'STAR', val: '*' });
i++;
break;
case '/':
toks.push({ type: 'SLASH', val: '/' });
i++;
break;
case '(':
toks.push({ type: 'LPAREN', val: '(' });
i++;
break;
case ')':
toks.push({ type: 'RPAREN', val: ')' });
i++;
break;
case ',':
toks.push({ type: 'COMMA', val: ',' });
i++;
break;
case ':':
toks.push({ type: 'COLON', val: ':' });
i++;
break;
default: throw new Error(config_1.ERROR_MSGS.ERR);
}
}
}
toks.push({ type: 'EOF', val: '' });
return toks;
}
// ---- Parser (recursive descent) ----
class Parser {
constructor(toks) {
this.toks = toks;
this.pos = 0;
}
peek() {
return this.toks[this.pos];
}
consume() {
return this.toks[this.pos++];
}
expect(type) {
const t = this.consume();
if (t.type !== type)
throw new Error(config_1.ERROR_MSGS.ERR);
return t;
}
parse() {
try {
const node = this.expr();
if (this.peek().type !== 'EOF')
throw new Error(config_1.ERROR_MSGS.ERR);
return node;
}
catch {
return { tag: 'err', msg: config_1.ERROR_MSGS.ERR };
}
}
// expr → addSub
expr() {
return this.addSub();
}
// addSub → mulDiv (('+' | '-') mulDiv)*
addSub() {
let left = this.mulDiv();
while (this.peek().type === 'PLUS' || this.peek().type === 'MINUS') {
const op = this.consume().val;
const right = this.mulDiv();
left = { tag: 'binop', op, left, right };
}
return left;
}
// mulDiv → unary (('*' | '/') unary)*
mulDiv() {
let left = this.unary();
while (this.peek().type === 'STAR' || this.peek().type === 'SLASH') {
const op = this.consume().val;
const right = this.unary();
left = { tag: 'binop', op, left, right };
}
return left;
}
// unary → '-' unary | primary
unary() {
if (this.peek().type === 'MINUS') {
this.consume();
const operand = this.unary();
return { tag: 'unary', op: '-', operand };
}
return this.primary();
}
// primary → NUMBER | cellRef (':' cellRef)? | func '(' args ')' | '(' expr ')'
primary() {
const t = this.peek();
if (t.type === 'NUMBER') {
this.consume();
const v = parseFloat(t.val);
return { tag: 'num', val: isNaN(v) ? NaN : v };
}
if (t.type === 'LPAREN') {
this.consume();
const node = this.expr();
this.expect('RPAREN');
return node;
}
if (t.type === 'IDENT') {
this.consume();
// Check if it's a function name
if (['SUM', 'AVG', 'AVERAGE', 'MIN', 'MAX', 'COUNT'].includes(t.val)) {
this.expect('LPAREN');
const args = [];
if (this.peek().type !== 'RPAREN') {
args.push(this.expr());
while (this.peek().type === 'COMMA') {
this.consume();
args.push(this.expr());
}
}
this.expect('RPAREN');
return { tag: 'func', name: t.val, args };
}
// Must be a cell reference like A1
const parsed = (0, state_1.parseKey)(t.val);
if (!parsed)
throw new Error(config_1.ERROR_MSGS.ERR);
const refNode = { tag: 'ref', col: parsed[0], row: parsed[1] };
// Check for range operator ':'
if (this.peek().type === 'COLON') {
this.consume();
const toNode = this.primary();
return { tag: 'range', from: refNode, to: toNode };
}
return refNode;
}
throw new Error(config_1.ERROR_MSGS.ERR);
}
}
function parseFormula(raw) {
if (!raw.startsWith('=')) {
// Not a formula
const n = parseFloat(raw);
if (!isNaN(n))
return { tag: 'num', val: n };
return { tag: 'num', val: NaN }; // text
}
const expr = raw.substring(1);
try {
const toks = tokenize(expr);
const parser = new Parser(toks);
return parser.parse();
}
catch {
return { tag: 'err', msg: config_1.ERROR_MSGS.ERR };
}
}
// ---- Dependency Extraction ----
function collectRefs(node) {
const refs = new Set();
collectRefsImpl(node, refs);
return [...refs];
}
function collectRefsImpl(node, set) {
if (!node)
return;
switch (node.tag) {
case 'ref':
set.add(node.col + ',' + node.row);
break;
case 'binop':
collectRefsImpl(node.left, set);
collectRefsImpl(node.right, set);
break;
case 'unary':
collectRefsImpl(node.operand, set);
break;
case 'func':
for (const a of node.args)
collectRefsImpl(a, set);
break;
case 'range':
collectRefsImpl(node.from, set);
collectRefsImpl(node.to, set);
break;
}
}
function isEvalError(v) {
return typeof v === 'object' && v !== null && '_error' in v;
}
function makeEvalError(msg) {
return { _error: msg };
}
function numVal(v) {
if (isEvalError(v))
return v;
if (typeof v === 'number')
return v;
if (typeof v === 'string') {
const n = parseFloat(v);
return isNaN(n) ? 0 : n;
}
return 0;
}
function evaluate(sheet, node, computed, visiting) {
if (!computed)
computed = new Set();
if (!visiting)
visiting = new Set();
switch (node.tag) {
case 'num':
return node.val;
case 'err':
return makeEvalError(node.msg);
case 'ref': {
const key = node.col + ',' + node.row;
if (visiting.has(key))
return makeEvalError(config_1.ERROR_MSGS.CYCLE);
visiting.add(key);
const cell = sheet.get(node.col, node.row);
if (cell.raw === '' || cell.raw === undefined) {
visiting.delete(key);
computed.add(key);
return 0;
}
if (cell.isError) {
visiting.delete(key);
return makeEvalError(cell.value);
}
if (!cell.raw.startsWith('=')) {
const n = parseFloat(cell.raw);
visiting.delete(key);
computed.add(key);
return isNaN(n) ? cell.raw : n;
}
// For formula cells, check if already computed this pass
if (computed.has(key)) {
visiting.delete(key);
return cell.value; // use already-computed value (non-formula short-circuit above)
}
// Evaluate this formula cell
const ast = parseFormula(cell.raw);
const result = evaluate(sheet, ast, computed, visiting);
visiting.delete(key);
computed.add(key);
// Update cell value in sheet
if (isEvalError(result)) {
sheet.set(node.col, node.row, { raw: cell.raw, value: result._error, isError: true });
}
else {
const display = typeof result === 'number' ? formatNumber(result) : String(result);
sheet.set(node.col, node.row, { raw: cell.raw, value: display, isError: false });
}
return result;
}
case 'unary': {
const v = evaluate(sheet, node.operand, computed, visiting);
if (isEvalError(v))
return v;
return -numVal(v);
}
case 'binop': {
const left = evaluate(sheet, node.left, computed, visiting);
if (isEvalError(left))
return left;
const right = evaluate(sheet, node.right, computed, visiting);
if (isEvalError(right))
return right;
const ln = numVal(left);
if (isEvalError(ln))
return ln;
const rn = numVal(right);
if (isEvalError(rn))
return rn;
switch (node.op) {
case '+': return ln + rn;
case '-': return ln - rn;
case '*': return ln * rn;
case '/': return rn === 0 ? makeEvalError(config_1.ERROR_MSGS.DIV0) : ln / rn;
default: return makeEvalError(config_1.ERROR_MSGS.ERR);
}
}
case 'func': {
const values = [];
for (const arg of node.args) {
const expanded = expandArg(sheet, arg, computed, visiting);
if (isEvalError(expanded))
return expanded;
if (Array.isArray(expanded))
values.push(...expanded);
else
values.push(expanded);
}
return applyFunc(node.name, values);
}
case 'range': {
return expandRange(sheet, node, computed, visiting);
}
default:
return makeEvalError(config_1.ERROR_MSGS.ERR);
}
}
function expandArg(sheet, node, visited, visiting) {
if (node.tag === 'range') {
return expandRange(sheet, node, visited, visiting);
}
return evaluate(sheet, node, visited, visiting);
}
function expandRange(sheet, node, visited, visiting) {
if (node.tag !== 'range')
return makeEvalError(config_1.ERROR_MSGS.ERR);
// Extract col/row from range endpoints. They can be refs or other expressions.
// For refs, get coordinates directly. For evaluated expressions, try to parse as cell key.
const fCoords = resolveRangeEndpoint(node.from, sheet, visited, visiting);
if (isEvalError(fCoords))
return fCoords;
const tCoords = resolveRangeEndpoint(node.to, sheet, visited, visiting);
if (isEvalError(tCoords))
return tCoords;
const vals = [];
const c1 = Math.min(fCoords[0], tCoords[0]), c2 = Math.max(fCoords[0], tCoords[0]);
const r1 = Math.min(fCoords[1], tCoords[1]), r2 = Math.max(fCoords[1], tCoords[1]);
for (let c = c1; c <= c2; c++) {
for (let r = r1; r <= r2; r++) {
const key = c + ',' + r;
if (visiting.has(key))
return makeEvalError(config_1.ERROR_MSGS.CYCLE);
visiting.add(key);
const cell = sheet.get(c, r);
visiting.delete(key);
if (visited.has(key)) {
vals.push(cell.isError ? makeEvalError(cell.value) : (isNaN(parseFloat(cell.value)) ? cell.value : parseFloat(cell.value)));
continue;
}
visited.add(key);
if (cell.raw === '' || cell.raw === undefined)
vals.push(0);
else if (cell.isError)
vals.push(makeEvalError(cell.value));
else if (!cell.raw.startsWith('=')) {
const n = parseFloat(cell.raw);
vals.push(isNaN(n) ? cell.raw : n);
}
else {
const ast = parseFormula(cell.raw);
vals.push(evaluate(sheet, ast, visited, visiting));
}
}
}
return vals;
}
// Resolve a range endpoint to [col, row] coordinates
function resolveRangeEndpoint(node, sheet, visited, visiting) {
if (node.tag === 'ref')
return [node.col, node.row];
// Evaluate the expression and try to parse as cell key
const val = evaluate(sheet, node, visited, visiting);
if (isEvalError(val))
return val;
const parsed = (0, state_1.parseKey)(String(val));
if (!parsed)
return makeEvalError(config_1.ERROR_MSGS.REF);
return parsed;
}
function tryParseCellRef(v) {
if (typeof v === 'string') {
const parsed = (0, state_1.parseKey)(v.toUpperCase());
return parsed;
}
return null;
}
function applyFunc(name, values) {
const nums = values.filter(v => !isEvalError(v) && typeof v === 'number');
if (name === 'SUM')
return nums.reduce((a, b) => a + b, 0);
if (name === 'AVG' || name === 'AVERAGE')
return nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : 0;
if (name === 'MIN')
return nums.length ? Math.min(...nums) : 0;
if (name === 'MAX')
return nums.length ? Math.max(...nums) : 0;
if (name === 'COUNT')
return nums.length;
return makeEvalError(config_1.ERROR_MSGS.ERR);
}
// ---- Dependency Graph & Recalculation ----
// deps: cellKey -> Set of cellKeys that depend on this cell
class DepGraph {
constructor() {
this.deps = new Map();
}
add(from, to) {
if (!this.deps.has(from))
this.deps.set(from, new Set());
this.deps.get(from).add(to);
}
getDependents(key) {
return [...(this.deps.get(key) || [])];
}
removeCell(key) {
// Remove this cell's dependencies (it doesn't depend on anything anymore)
// Remove this cell from other cells' dependent lists
for (const [, set] of this.deps) {
set.delete(key);
}
}
clear() {
this.deps = new Map();
}
}
exports.DepGraph = DepGraph;
// Recompute a cell and cascade to dependents
// recalcing tracks which cells are currently being recalculated to prevent infinite recursion
function recalcCell(sheet, key, depGraph, recalcing) {
if (!recalcing)
recalcing = new Set();
if (recalcing.has(key))
return;
recalcing.add(key);
const parsed = (0, state_1.parseKey)(key);
if (!parsed)
return;
const [c, r] = parsed;
const cell = sheet.get(c, r);
if (!cell.raw || !cell.raw.startsWith('=')) {
sheet.set(c, r, { raw: cell.raw || '', value: cell.raw || '', isError: false });
}
else {
const ast = parseFormula(cell.raw);
let result;
try {
result = evaluate(sheet, ast, new Set(), new Set());
}
catch {
result = makeEvalError(config_1.ERROR_MSGS.ERR);
}
if (isEvalError(result)) {
sheet.set(c, r, { raw: cell.raw, value: result._error, isError: true });
}
else {
const display = typeof result === 'number' ? formatNumber(result) : String(result);
sheet.set(c, r, { raw: cell.raw, value: display, isError: false });
}
}
// Cascade to dependents
const dependents = depGraph.getDependents(key);
for (const dep of dependents) {
recalcCell(sheet, dep, depGraph, recalcing);
}
}
// For full recalc, detect cycles by walking the dependency graph
function detectCycles(depGraph, keys) {
const cycleCells = new Set();
const WHITE = 0, GRAY = 1, BLACK = 2;
const color = new Map();
for (const k of keys)
color.set(k, WHITE);
function dfs(k, path) {
color.set(k, GRAY);
path.push(k);
for (const dep of depGraph.getDependents(k)) {
if (!color.has(dep)) {
color.set(dep, WHITE);
}
if (color.get(dep) === GRAY) {
// Found cycle - mark all cells in the cycle
const idx = path.indexOf(dep);
for (let i = idx; i < path.length; i++) {
cycleCells.add(path[i]);
}
}
else if (color.get(dep) === WHITE) {
dfs(dep, path);
}
}
path.pop();
color.set(k, BLACK);
}
for (const k of keys) {
if (color.get(k) === WHITE) {
dfs(k, []);
}
}
return cycleCells;
}
// Recompute a cell's dependencies and recalc it
function recalcAndCascade(sheet, key, depGraph) {
recalcCell(sheet, key, depGraph, null);
}
// Rebuild dependency graph from scratch
function rebuildDeps(sheet, depGraph) {
depGraph.clear();
sheet.forEach((c, r, cell) => {
if (!cell.raw || !cell.raw.startsWith('='))
return;
const ast = parseFormula(cell.raw);
const refs = collectRefs(ast);
const key = (0, state_1.cellKey)(c, r);
for (const ref of refs) {
const [rc, rr] = ref.split(',').map(Number);
const refKey = (0, state_1.cellKey)(rc, rr);
depGraph.add(refKey, key);
}
});
}
// Full recalculation
function fullRecalc(sheet, depGraph) {
rebuildDeps(sheet, depGraph);
sheet.forEach((c, r, cell) => {
if (cell.raw && cell.raw.startsWith('=')) {
recalcCell(sheet, (0, state_1.cellKey)(c, r), depGraph, null);
}
else {
sheet.set(c, r, { raw: cell.raw || '', value: cell.raw || '', isError: false });
}
});
}
function formatNumber(n) {
if (Number.isInteger(n))
return String(n);
return parseFloat(n.toFixed(10)).toString();
}
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getSelected = getSelected;
exports.setSelected = setSelected;
exports.buildGrid = buildGrid;
exports.renderCell = renderCell;
exports.renderAll = renderAll;
exports.getCellElement = getCellElement;
exports.updateCellRefDisplay = updateCellRefDisplay;
exports.startEdit = startEdit;
exports.finishEdit = finishEdit;
exports.moveSelection = moveSelection;
exports.setSheet = setSheet;
exports.setSheetSize = setSheetSize;
const state_1 = require("./state");
const formula_1 = require("./formula");
let table;
let selectedCell = null;
let editingCell = null;
let editingInput = null;
function getSelected() { return selectedCell; }
function setSelected(c, r) { selectedCell = { col: c, row: r }; }
function buildGrid(sheet) {
table = document.getElementById('grid');
table.innerHTML = '';
// Build header row
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
const corner = document.createElement('th');
corner.className = 'corner';
headerRow.appendChild(corner);
for (let c = 0; c < sheet.cols; c++) {
const th = document.createElement('th');
th.className = 'col-header';
th.textContent = (0, state_1.colName)(c);
th.dataset.col = String(c);
headerRow.appendChild(th);
}
thead.appendChild(headerRow);
table.appendChild(thead);
// Build body
const tbody = document.createElement('tbody');
for (let r = 0; r < sheet.rows; r++) {
const tr = document.createElement('tr');
const rowHeader = document.createElement('td');
rowHeader.className = 'row-header';
rowHeader.textContent = String(r + 1);
tr.appendChild(rowHeader);
for (let c = 0; c < sheet.cols; c++) {
const td = document.createElement('td');
td.dataset.col = String(c);
td.dataset.row = String(r);
td.className = 'cell';
tr.appendChild(td);
}
tbody.appendChild(tr);
}
table.appendChild(tbody);
}
function renderCell(td, data) {
td.textContent = data.value;
td.className = 'cell';
if (data.isError)
td.classList.add('cell-error');
const n = parseFloat(data.value);
if (!isNaN(n) && data.value !== '' && !data.isError) {
td.classList.add('cell-number');
}
}
function renderAll(sheet) {
const tbody = table.querySelector('tbody');
if (!tbody)
return;
const rows = tbody.querySelectorAll('tr');
for (let r = 0; r < rows.length; r++) {
const tds = rows[r].querySelectorAll('td:not(.row-header)');
for (let c = 0; c < tds.length; c++) {
const data = sheet.get(c, r);
renderCell(tds[c], data);
}
}
if (selectedCell)
highlightCell(selectedCell.col, selectedCell.row);
}
function highlightCell(c, r) {
// Remove previous selection
const prev = table.querySelector('.cell-selected');
if (prev)
prev.classList.remove('cell-selected');
// Remove header highlights
table.querySelectorAll('.col-header-active').forEach(el => el.classList.remove('col-header-active'));
table.querySelectorAll('.row-header-active').forEach(el => el.classList.remove('row-header-active'));
// Add selection
const td = table.querySelector(`td[data-col="${c}"][data-row="${r}"]:not(.row-header)`);
if (td)
td.classList.add('cell-selected');
// Highlight headers
const colHeader = table.querySelector(`th.col-header[data-col="${c}"]`);
if (colHeader)
colHeader.classList.add('col-header-active');
const rowHeader = table.querySelector(`td.row-header[data-row="${r}"]`);
if (rowHeader)
rowHeader.classList.add('row-header-active');
}
function getCellElement(c, r) {
return table.querySelector(`td[data-col="${c}"][data-row="${r}"]:not(.row-header)`);
}
function updateCellRefDisplay() {
const el = document.getElementById('cell-ref');
if (!el)
return;
if (selectedCell) {
el.textContent = (0, state_1.cellKey)(selectedCell.col, selectedCell.row);
}
else {
el.textContent = '';
}
}
function startEdit(sheet) {
if (!selectedCell || editingCell)
return;
const c = selectedCell.col;
const r = selectedCell.row;
editingCell = { col: c, row: r };
const td = getCellElement(c, r);
if (!td)
return;
const cellData = sheet.get(c, r);
td.textContent = '';
const input = document.createElement('input');
input.type = 'text';
input.className = 'cell-input';
input.value = cellData.raw;
input.spellcheck = false;
input.autocomplete = 'off';
td.appendChild(input);
input.focus();
input.select();
editingInput = input;
// Sync formula bar
const formulaInput = document.getElementById('formula-input');
if (formulaInput)
formulaInput.value = cellData.raw;
input.addEventListener('blur', () => commitEdit(sheet, c, r));
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
commitEdit(sheet, c, r);
moveSelection(0, 1);
}
else if (e.key === 'Tab') {
e.preventDefault();
commitEdit(sheet, c, r);
moveSelection(1, 0);
}
else if (e.key === 'Escape') {
cancelEdit(sheet, c, r);
}
});
// Sync formula bar while typing
input.addEventListener('input', () => {
const fb = document.getElementById('formula-input');
if (fb)
fb.value = input.value;
});
}
function commitEdit(sheet, c, r) {
if (editingInput) {
const raw = editingInput.value;
finishEdit(sheet, c, r, raw, currentDepGraph);
}
else {
finishEdit(sheet, c, r, '', currentDepGraph);
}
}
function finishEdit(sheet, c, r, raw, depGraph) {
editingCell = null;
editingInput = null;
// Remove input from td
const td = getCellElement(c, r);
if (td) {
const inp = td.querySelector('input');
if (inp)
inp.remove();
}
// Store raw value
const cellData = sheet.get(c, r);
const wasFormula = cellData.raw && cellData.raw.startsWith('=');
sheet.set(c, r, { raw, value: '', isError: false });
// Recalculate
(0, formula_1.rebuildDeps)(sheet, depGraph);
(0, formula_1.recalcAndCascade)(sheet, (0, state_1.cellKey)(c, r), depGraph);
// Re-render
const newData = sheet.get(c, r);
const td2 = getCellElement(c, r);
if (td2)
renderCell(td2, newData);
if (selectedCell)
highlightCell(selectedCell.col, selectedCell.row);
// Save
sheet.save();
// Sync formula bar
const formulaInput = document.getElementById('formula-input');
if (formulaInput && selectedCell) {
const sd = sheet.get(selectedCell.col, selectedCell.row);
formulaInput.value = sd.raw;
}
}
function cancelEdit(sheet, c, r) {
editingCell = null;
editingInput = null;
const td = getCellElement(c, r);
if (td) {
const inp = td.querySelector('input');
if (inp)
inp.remove();
const data = sheet.get(c, r);
renderCell(td, data);
}
if (selectedCell)
highlightCell(selectedCell.col, selectedCell.row);
}
function moveSelection(dc, dr) {
if (!selectedCell)
return;
let nc = Math.max(0, Math.min(sheet_cols - 1, selectedCell.col + dc));
let nr = Math.max(0, Math.min(sheet_rows - 1, selectedCell.row + dr));
selectedCell = { col: nc, row: nr };
highlightCell(nc, nr);
updateCellRefDisplay();
const data = currentSheet.get(nc, nr);
const formulaInput = document.getElementById('formula-input');
if (formulaInput)
formulaInput.value = data.raw;
// Scroll into view
const td = getCellElement(nc, nr);
if (td)
td.scrollIntoView({ block: 'nearest', inline: 'nearest' });
}
let sheet_cols = 26;
let sheet_rows = 100;
let currentSheet = null;
let currentDepGraph = null;
function setSheet(sheet, depGraph) {
currentSheet = sheet;
currentDepGraph = depGraph;
}
// Public API
function setSheetSize(cols, rows) {
sheet_cols = cols;
sheet_rows = rows;
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"ThinkingCap-Qwen3.6-27B-GGUF","provider":"brigid","persona":"typescript","sessionId":"c87a7375-990a-48b7-b1e1-c4cab5458d74","tokensIn":1251558,"tokensOut":35590,"tokensTotal":1287148,"cost":0,"turns":44,"toolCalls":44,"failedToolCalls":1,"timestamp":"2026-08-03T20:01:45.305Z"} -->