Spreadsheet
inkling Small · 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 44,257 bytes · SHA-256 fecc8b411555
<!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 ── */
*{box-sizing:border-box}
html,body{margin:0;padding:0;height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;background:#f3f4f6;color:#1f2937}
#app{display:flex;flex-direction:column;height:100vh;overflow:hidden}
.header-bar{display:flex;align-items:center;gap:12px;padding:10px 16px;background:#fff;border-bottom:1px solid #e5e7eb;box-shadow:0 1px 3px rgba(0,0,0,.04)}
.ref-label{font-weight:700;color:#2563eb;width:60px;text-align:center;font-size:13px}
.formula-bar{flex:1;display:flex;align-items:center;background:#f9fafb;border:1px solid #d1d5db;border-radius:8px;padding:6px 10px;gap:8px;font-family:ui-monospace,SFMono-Regular,monospace;font-size:13px;color:#374151;min-height:36px}
.fx{color:#ff9800;font-weight:700;font-size:12px;width:20px;text-align:center}
.toolbar{display:flex;gap:6px;padding:6px 14px;background:#fff;border-bottom:1px solid #e5e7eb}
.toolbar button{padding:6px 14px;border:1px solid #d1d5db;border-radius:6px;background:#fff;font-size:12px;cursor:pointer;color:#374151}
.toolbar button:hover{background:#f3f4f6;border-color:#9ca3af}
.grid-wrap{flex:1;overflow:auto;background:#fff;position:relative}
.sheet-table{border-collapse:collapse;table-layout:fixed;color:#1f2937;font-size:12px}
.sheet-table th{position:sticky;top:0;left:0;background:#f8fafc;z-index:3;border-right:1px solid #cbd5e1;border-bottom:1px solid #cbd5e1;font-weight:600;color:#475569;text-align:center;padding:4px 2px;min-width:80px;min-height:24px;user-select:none}
.sheet-table th:first-child{z-index:5;background:#e2e8f0;min-width:48px}
.sheet-table th.row-head{position:sticky;left:0;z-index:4;background:#e2e8f0}
.sheet-table td{border-right:1px solid #e5e7eb;border-bottom:1px solid #e5e7eb;padding:0;text-align:right;vertical-align:middle;min-width:80px;height:24px;position:relative;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;cursor:cell}
.sheet-table td:first-child{border-left:1px solid #e5e7eb;min-width:48px;background:#f8fafc}
.sheet-table td.active{outline:2px solid #2563eb;outline-offset:-2px}
.sheet-table td.error-text{color:#dc2626;font-weight:500}
.sheet-table td.text-cell{text-align:left}
.sheet-table td.num-cell{font-variant-numeric:tabular-nums}
</style>
</head>
<body>
<div id="app">
<div class="header-bar"><div class="ref-label" id="ref-label">A1</div><div class="formula-bar" id="formula-bar"><span class="fx">fx</span><input type="text" id="formula-input" style="flex:1;border:none;background:transparent;outline:none;font-family:inherit;font-size:inherit;color:inherit" value=""></div></div>
<div class="toolbar"><button onclick="window.sheetInsertRow()">Insert Row</button><button onclick="window.sheetDeleteRow()">Delete Row</button><button onclick="window.sheetInsertCol()">Insert Col</button><button onclick="window.sheetDeleteCol()">Delete Col</button><button onclick="window.sheetClear()">Clear</button></div>
<div class="grid-wrap" id="grid-wrap"></div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./constants":"src/constants.ts","./parser":"src/parser.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 });
exports.start = start;
const constants_1 = require("./constants");
const parser_1 = require("./parser");
let formulas = new Map();
let computed = new Map();
let cycleSet = new Set();
let selected = { col: 0, row: 0 };
let editMode = false;
let editValue = '';
let depGraph = new Map();
function k(c, r) { return `${c}-${r}`; }
function initSheet() {
try {
const s = localStorage.getItem(constants_1.STORAGE_KEY);
if (s) {
const o = JSON.parse(s);
if (o && o.formulas)
formulas = new Map(Object.entries(o.formulas));
}
}
catch (e) { }
if (formulas.size === 0) {
formulas.set(k(0, 0), '5');
formulas.set(k(1, 0), '=A1*2');
formulas.set(k(2, 0), '=B1+1');
}
rebuild();
}
function saveSheet() { try {
localStorage.setItem(constants_1.STORAGE_KEY, JSON.stringify({ formulas: Object.fromEntries(formulas) }));
}
catch (e) { } }
function rebuild() { depGraph = (0, parser_1.computeDependencies)(formulas); cycleSet = (0, parser_1.detectCycles)(formulas); computed = (0, parser_1.buildFullComputed)(formulas, cycleSet); renderGrid(); updateHeader(); updateFormulaBar(); saveSheet(); }
function renderGrid() {
const wrap = document.getElementById('grid-wrap');
const table = document.createElement('table');
table.className = 'sheet-table';
const thead = document.createElement('thead');
const tr = document.createElement('tr');
const corner = document.createElement('th');
corner.style.minWidth = '48px';
tr.appendChild(corner);
for (let c = 0; c < constants_1.COLS; c++) {
const th = document.createElement('th');
th.textContent = (0, parser_1.cellToString)({ col: c, row: -1 });
th.style.width = constants_1.COL_WIDTH + 'px';
tr.appendChild(th);
}
thead.appendChild(tr);
table.appendChild(thead);
const tbody = document.createElement('tbody');
for (let r = 0; r < constants_1.ROWS; r++) {
const tr = document.createElement('tr');
const rh = document.createElement('th');
rh.className = 'row-head';
rh.textContent = String(r + 1);
rh.style.width = '48px';
tr.appendChild(rh);
for (let c = 0; c < constants_1.COLS; c++) {
const td = document.createElement('td');
td.style.width = constants_1.COL_WIDTH + 'px';
td.style.height = constants_1.ROW_HEIGHT + 'px';
const key = k(c, r);
const raw = formulas.get(key) || '';
const val = computed.get(key);
if (c === selected.col && r === selected.row)
td.classList.add('active');
if (cycleSet.has(key)) {
td.classList.add('error-text');
td.textContent = '#CYCLE!';
}
else if (typeof val === 'string' && val.startsWith('#')) {
td.classList.add('error-text');
td.textContent = val;
}
else {
if ((0, parser_1.isFormula)(raw)) {
td.classList.add('num-cell');
td.textContent = (0, parser_1.formatValue)(val);
}
else {
const n = parseFloat(raw);
if (!isNaN(n) && raw.trim() !== '') {
td.classList.add('num-cell');
td.textContent = (0, parser_1.formatValue)(val ?? n);
}
else {
td.classList.add('text-cell');
td.textContent = raw;
}
}
}
td.addEventListener('click', () => selectCell(c, r));
td.addEventListener('dblclick', () => enterEdit(c, r));
tr.appendChild(td);
}
tbody.appendChild(tr);
}
table.appendChild(tbody);
wrap.innerHTML = '';
wrap.appendChild(table);
}
function updateHeader() { document.getElementById('ref-label').textContent = (0, parser_1.cellToString)({ col: selected.col, row: selected.row }); }
function updateFormulaBar() { const input = document.getElementById('formula-input'); input.value = editMode ? editValue : (formulas.get(k(selected.col, selected.row)) || ''); }
function selectCell(c, r) { selected = { col: c, row: r }; editMode = false; editValue = ''; renderGrid(); updateHeader(); updateFormulaBar(); const wrap = document.getElementById('grid-wrap'); const td = wrap.querySelectorAll('td')[r * constants_1.COLS + c]; if (td)
td.scrollIntoView({ block: 'center', inline: 'center' }); }
function enterEdit(c, r) { selectCell(c, r); editMode = true; editValue = formulas.get(k(c, r)) || ''; updateFormulaBar(); setTimeout(() => { document.getElementById('formula-input').focus(); }, 50); }
function commitEdit(value) { formulas.set(k(selected.col, selected.row), value); editMode = false; editValue = ''; rebuild(); }
function setupFormulaInput() {
const input = document.getElementById('formula-input');
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
commitEdit(input.value);
if (selected.row < constants_1.ROWS - 1) {
selected.row++;
rebuild();
selectCell(selected.col, selected.row);
}
}
else if (e.key === 'Tab') {
e.preventDefault();
commitEdit(input.value);
if (selected.col < constants_1.COLS - 1) {
selected.col++;
rebuild();
selectCell(selected.col, selected.row);
}
}
else if (e.key === 'Escape') {
editMode = false;
editValue = '';
updateFormulaBar();
}
});
input.addEventListener('input', () => { editValue = input.value; });
}
function onKeyDown(e) {
if (editMode)
return;
if (e.key === 'ArrowDown') {
if (selected.row < constants_1.ROWS - 1) {
selected.row++;
rebuild();
selectCell(selected.col, selected.row);
e.preventDefault();
}
}
else if (e.key === 'ArrowUp') {
if (selected.row > 0) {
selected.row--;
rebuild();
selectCell(selected.col, selected.row);
e.preventDefault();
}
}
else if (e.key === 'ArrowRight') {
if (selected.col < constants_1.COLS - 1) {
selected.col++;
rebuild();
selectCell(selected.col, selected.row);
e.preventDefault();
}
}
else if (e.key === 'ArrowLeft') {
if (selected.col > 0) {
selected.col--;
rebuild();
selectCell(selected.col, selected.row);
e.preventDefault();
}
}
else if (e.key === 'Enter') {
enterEdit(selected.col, selected.row);
e.preventDefault();
}
else if (e.key === 'Delete') {
formulas.set(k(selected.col, selected.row), '');
rebuild();
}
else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
enterEdit(selected.col, selected.row);
editValue = e.key;
updateFormulaBar();
setTimeout(() => { const inp = document.getElementById('formula-input'); inp.value = editValue; inp.focus(); inp.setSelectionRange(1, 1); }, 10);
}
}
window.sheetInsertRow = () => { const r = selected.row; for (let rr = constants_1.ROWS - 1; rr > r; rr--)
for (let c = 0; c < constants_1.COLS; c++)
formulas.set(k(c, rr), formulas.get(k(c, rr - 1)) || ''); for (let c = 0; c < constants_1.COLS; c++)
formulas.set(k(c, r), ''); for (const [key, raw] of formulas)
if (raw.startsWith('='))
formulas.set(key, (0, parser_1.rewriteFormula)(raw, 'insertRow', r + 1)); rebuild(); };
window.sheetDeleteRow = () => { const r = selected.row; for (let rr = r; rr < constants_1.ROWS - 1; rr++)
for (let c = 0; c < constants_1.COLS; c++)
formulas.set(k(c, rr), formulas.get(k(c, rr + 1)) || ''); for (let c = 0; c < constants_1.COLS; c++)
formulas.set(k(c, constants_1.ROWS - 1), ''); for (const [key, raw] of formulas)
if (raw.startsWith('='))
formulas.set(key, (0, parser_1.rewriteFormula)(raw, 'deleteRow', r + 1)); rebuild(); };
window.sheetInsertCol = () => { const c = selected.col; for (let cc = constants_1.COLS - 1; cc > c; cc--)
for (let r = 0; r < constants_1.ROWS; r++)
formulas.set(k(cc, r), formulas.get(k(cc - 1, r)) || ''); for (let r = 0; r < constants_1.ROWS; r++)
formulas.set(k(c, r), ''); for (const [key, raw] of formulas)
if (raw.startsWith('='))
formulas.set(key, (0, parser_1.rewriteFormula)(raw, 'insertCol', c)); rebuild(); };
window.sheetDeleteCol = () => { const c = selected.col; for (let cc = c; cc < constants_1.COLS - 1; cc++)
for (let r = 0; r < constants_1.ROWS; r++)
formulas.set(k(cc, r), formulas.get(k(cc + 1, r)) || ''); for (let r = 0; r < constants_1.ROWS; r++)
formulas.set(k(constants_1.COLS - 1, r), ''); for (const [key, raw] of formulas)
if (raw.startsWith('='))
formulas.set(key, (0, parser_1.rewriteFormula)(raw, 'deleteCol', c)); rebuild(); };
window.sheetClear = () => { formulas.set(k(selected.col, selected.row), ''); rebuild(); };
function start() { document.addEventListener('keydown', onKeyDown); setupFormulaInput(); initSheet(); }
window.getSheetFormulas = () => formulas;
window.getComputed = () => computed;
window.selectCell = selectCell;
window.getSelected = () => selected;
window.commitEdit = commitEdit;
window.rebuild = rebuild;
window.enterEdit = enterEdit;
window.setFormula = (k, v) => { formulas.set(k, v); rebuild(); };
window.getFormulaString = (k) => formulas.get(k) || '';
window.getCellValueString = (k) => (0, parser_1.formatValue)(computed.get(k));
window.clearCycle = () => { cycleSet.clear(); rebuild(); };
window.resetSheet = () => { formulas.clear(); initSheet(); };
window.setCell = (k, v) => { formulas.set(k, v); rebuild(); };
window.getKey = (c, r) => k(c, r);
window.getCOLS = () => constants_1.COLS;
window.getROWS = () => constants_1.ROWS;
window.getFormulas = () => Array.from(formulas.entries());
window.buildFullComputed = () => { depGraph = (0, parser_1.computeDependencies)(formulas); cycleSet = (0, parser_1.detectCycles)(formulas); computed = (0, parser_1.buildFullComputed)(formulas, cycleSet); };
window.recalcAll = () => rebuild();
window.makeCycle = () => { formulas.set('0-0', '=B1'); formulas.set('1-0', '=A1'); rebuild(); };
window.clearCycleDirect = () => { formulas.set('0-0', ''); formulas.set('1-0', ''); rebuild(); };
window.getComputedValue = (k) => computed.get(k);
window.getFormula = (k) => formulas.get(k);
window.enterEditDirect = (c, r) => enterEdit(c, r);
window.commitEditDirect = () => commitEdit(document.getElementById('formula-input').value);
window.verifyAll = async () => {
formulas.clear();
formulas.set('0-0', '5');
formulas.set('1-0', '=A1*2');
formulas.set('2-0', '=B1+1');
rebuild();
const r = [];
r.push({ name: 'basic', pass: computed.get('2-0') === 11 });
formulas.set('0-0', '10');
rebuild();
r.push({ name: 'cascade', pass: computed.get('2-0') === 21 });
formulas.set('0-0', '1');
formulas.set('1-0', '2');
formulas.set('2-0', '3');
formulas.set('3-0', '=SUM(A1:A3)');
rebuild();
r.push({ name: 'sum', pass: computed.get('3-0') === 6 });
formulas.set('4-0', '=AVG(A1:A3)');
rebuild();
r.push({ name: 'avg', pass: computed.get('4-0') === 2 });
formulas.set('5-0', '=1/0');
rebuild();
r.push({ name: 'div0', pass: computed.get('5-0') === '#DIV/0!' });
formulas.set('6-0', '=SUM(');
rebuild();
r.push({ name: 'err', pass: computed.get('6-0') === '#ERR!' });
formulas.set('0-7', '=B8');
formulas.set('1-7', '=A8');
rebuild();
r.push({ name: 'cycle', pass: computed.get('0-7') === '#CYCLE!' && computed.get('1-7') === '#CYCLE!' });
formulas.set('0-7', '');
formulas.set('1-7', '');
rebuild();
r.push({ name: 'cycle_clear', pass: computed.get('0-7') !== '#CYCLE!' });
return r;
};
start();
exports.default = start;
};
// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.COLORS = exports.STORAGE_KEY = exports.ROW_HEIGHT = exports.COL_WIDTH = exports.ROWS = exports.COLS = void 0;
exports.COLS = 26;
exports.ROWS = 100;
exports.COL_WIDTH = 90;
exports.ROW_HEIGHT = 26;
exports.STORAGE_KEY = 'spreadsheet_state';
exports.COLORS = { bg: '#f3f4f6', grid: '#fff', select: '#2563eb' };
};
// ── 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.parseRef = parseRef;
exports.compileFormula = compileFormula;
exports.evalExpr = evalExpr;
exports.evaluateAst = evalExpr;
exports.getRangeValues = getRangeValues;
exports.formatValue = formatValue;
exports.parseCell = parseCell;
exports.cellToString = cellToString;
exports.isFormula = isFormula;
exports.extractRefs = extractRefs;
exports.extractRangeRefs = extractRangeRefs;
exports.rewriteFormula = rewriteFormula;
exports.findCycles = findCycles;
exports.getRefsInString = getRefsInString;
exports.getRangeInString = getRangeInString;
exports.getAllRefs = getAllRefs;
exports.computeDependencies = computeDependencies;
exports.topologicalOrder = topologicalOrder;
exports.evaluateCell = evaluateCell;
exports.getRangeForFunc = getRangeForFunc;
exports.computeCell = computeCell;
exports.recalcCells = recalcCells;
exports.recalcDependents = recalcDependents;
exports.detectCycles = detectCycles;
exports.buildFullComputed = buildFullComputed;
exports.getCellValueString = getCellValueString;
exports.getFormulaString = getFormulaString;
function tokenize(s) {
const t = [];
let i = 0;
while (i < s.length) {
const c = s[i];
if (/\s/.test(c)) {
i++;
continue;
}
if (c === '(') {
t.push({ type: 'LPAREN', val: '(', raw: '(', pos: i });
i++;
continue;
}
if (c === ')') {
t.push({ type: 'RPAREN', val: ')', raw: ')', pos: i });
i++;
continue;
}
if (c === ',') {
t.push({ type: 'COMMA', val: ',', raw: ',', pos: i });
i++;
continue;
}
if (c === '+') {
t.push({ type: 'PLUS', val: '+', raw: '+', pos: i });
i++;
continue;
}
if (c === '-') {
t.push({ type: 'MINUS', val: '-', raw: '-', pos: i });
i++;
continue;
}
if (c === '*') {
t.push({ type: 'MUL', val: '*', raw: '*', pos: i });
i++;
continue;
}
if (c === '/') {
t.push({ type: 'DIV', val: '/', raw: '/', pos: i });
i++;
continue;
}
if (/\d/.test(c)) {
let v = '';
while (i < s.length && /\d/.test(s[i]))
v += s[i++];
if (i < s.length && s[i] === '.') {
v += s[i++];
while (i < s.length && /\d/.test(s[i]))
v += s[i++];
}
t.push({ type: 'NUM', val: v, raw: v, pos: i - v.length });
continue;
}
if (/[A-Za-z]/.test(c)) {
let v = '';
while (i < s.length && /[A-Za-z]/.test(s[i]))
v += s[i++];
while (i < s.length && /\d/.test(s[i]))
v += s[i++];
const up = v.toUpperCase();
if (['SUM', 'AVG', 'AVERAGE', 'MIN', 'MAX', 'COUNT'].includes(up)) {
t.push({ type: 'FUNC', val: up, raw: v, pos: i - v.length });
}
else {
// could be part of reference? References start with letter then digits
t.push({ type: 'REF', val: v, raw: v, pos: i - v.length });
}
continue;
}
// If colon after reference -> range token
if (c === ':') {
t.push({ type: 'RANGE', val: ':', raw: ':', pos: i });
i++;
continue;
}
i++;
}
// Post-process ranges: if REF RANGE REF => merge to RANGE token with full value
for (let k = 0; k < t.length - 2; k++) {
if (t[k].type === 'REF' && t[k + 1].type === 'RANGE' && t[k + 2].type === 'REF') {
const combined = t[k].val + ':' + t[k + 2].val;
t.splice(k, 3, { type: 'RANGE', val: combined, raw: combined, pos: t[k].pos });
}
}
t.push({ type: 'EOF', val: '', raw: '', pos: i });
return t;
}
function parseRef(str) {
const m = str.match(/^([A-Z]+)(\d+)$/);
if (!m)
return null;
let col = 0;
for (let i = 0; i < m[1].length; i++)
col = col * 26 + (m[1].charCodeAt(i) - 64);
return { col: col - 1, row: parseInt(m[2], 10) - 1 };
}
function refToStr(r) {
let s = '';
let n = r.col + 1;
while (n > 0) {
s = String.fromCharCode(65 + ((n - 1) % 26)) + s;
n = Math.floor((n - 1) / 26);
}
return s + (r.row + 1);
}
class Parser {
constructor(src) {
this.pos = 0;
this.tok = tokenize(src);
}
cur() { return this.tok[this.pos]; }
eat(t) {
const c = this.cur();
if (t && c.type !== t)
throw new Error('Expected ' + t);
this.pos++;
return c;
}
parse() {
return this.parseExpr();
}
parseExpr() {
let left = this.parseTerm();
while (this.cur().type === 'PLUS' || this.cur().type === 'MINUS') {
const op = this.eat().type === 'PLUS' ? 'add' : 'sub';
const right = this.parseTerm();
left = { op, left, right };
}
return left;
}
parseTerm() {
let left = this.parseFactor();
while (this.cur().type === 'MUL' || this.cur().type === 'DIV') {
const op = this.eat().type === 'MUL' ? 'mul' : 'div';
const right = this.parseFactor();
left = { op, left, right };
}
return left;
}
parseFactor() {
if (this.cur().type === 'PLUS') {
this.eat();
return this.parseFactor();
}
if (this.cur().type === 'MINUS') {
this.eat();
return { op: 'neg', left: this.parseFactor() };
}
return this.parsePrimary();
}
parsePrimary() {
const c = this.cur();
if (c.type === 'NUM') {
this.eat();
return { op: 'num', val: parseFloat(c.val) };
}
if (c.type === 'REF') {
this.eat();
return { op: 'ref', val: parseRef(c.val) };
}
if (c.type === 'RANGE') {
this.eat();
const p = parseRef(c.val.split(':')[0]);
const q = parseRef(c.val.split(':')[1]);
return { op: 'range', start: p, end: q };
}
if (c.type === 'FUNC') {
const fn = this.eat().val;
this.eat('LPAREN');
const args = [];
if (this.cur().type !== 'RPAREN') {
args.push(this.parseExpr());
while (this.cur().type === 'COMMA') {
this.eat();
args.push(this.parseExpr());
}
}
this.eat('RPAREN');
return { op: 'func', fn, args };
}
if (c.type === 'LPAREN') {
this.eat();
const e = this.parseExpr();
this.eat('RPAREN');
return e;
}
throw new Error('Unexpected token ' + c.type);
}
}
exports.Parser = Parser;
function compileFormula(src) {
// Remove leading = if present
const s = src.startsWith('=') ? src.slice(1) : src;
const p = new Parser(s);
return p.parse();
}
function evalExpr(node, getValue, getRange) {
// Returns computed value or error string
try {
if (!node)
return 0;
if (node.op === 'num')
return node.val;
if (node.op === 'ref') {
const v = getValue(node.val);
if (v === null)
return '#REF!';
if (typeof v === 'string' && v.startsWith('#'))
return v;
return v;
}
if (node.op === 'range') {
return getRange(node.start, node.end);
}
if (node.op === 'neg') {
const v = evalExpr(node.left, getValue, getRange);
if (typeof v === 'string')
return v;
return -v;
}
if (node.op === 'add') {
const a = evalExpr(node.left, getValue, getRange);
if (typeof a === 'string')
return a;
const b = evalExpr(node.right, getValue, getRange);
if (typeof b === 'string')
return b;
return a + b;
}
if (node.op === 'sub') {
const a = evalExpr(node.left, getValue, getRange);
if (typeof a === 'string')
return a;
const b = evalExpr(node.right, getValue, getRange);
if (typeof b === 'string')
return b;
return a - b;
}
if (node.op === 'mul') {
const a = evalExpr(node.left, getValue, getRange);
if (typeof a === 'string')
return a;
const b = evalExpr(node.right, getValue, getRange);
if (typeof b === 'string')
return b;
return a * b;
}
if (node.op === 'div') {
const a = evalExpr(node.left, getValue, getRange);
if (typeof a === 'string')
return a;
const b = evalExpr(node.right, getValue, getRange);
if (typeof b === 'string')
return b;
if (b === 0)
return '#DIV/0!';
return a / b;
}
if (node.op === 'func') {
const fn = node.fn;
const args = node.args.map((a) => evalExpr(a, getValue, getRange));
// Handle ranges inside args: if arg result is array, use it
let vals = [];
for (const arg of args) {
if (Array.isArray(arg))
vals.push(...arg);
else if (typeof arg === 'number')
vals.push(arg);
else if (typeof arg === 'string' && arg.startsWith('#'))
return arg;
}
if (fn === 'SUM')
return vals.reduce((s, v) => s + v, 0);
if (fn === 'AVG' || fn === 'AVERAGE')
return vals.length ? vals.reduce((s, v) => s + v, 0) / vals.length : 0;
if (fn === 'MIN')
return vals.length ? Math.min(...vals) : 0;
if (fn === 'MAX')
return vals.length ? Math.max(...vals) : 0;
if (fn === 'COUNT')
return vals.length;
return '#ERR!';
}
return '#ERR!';
}
catch (e) {
return '#ERR!';
}
}
// Helper for functions to extract range from args that are range nodes
function getRangeValues(start, end, cellMap) {
const res = [];
for (let r = start.row; r <= end.row; r++) {
for (let c = start.col; c <= end.col; c++) {
const key = `${c}-${r}`;
const raw = cellMap.get(key);
if (raw === undefined)
continue;
if (raw.startsWith('=')) {
// evaluate formula for numeric result; ignore non-numeric/errors
try {
const compiled = compileFormula(raw);
// We don't have eval context here directly; this is used in engine
}
catch { }
}
else {
const n = parseFloat(raw);
if (!isNaN(n))
res.push(n);
}
}
}
return res;
}
function formatValue(v) {
if (v === null || v === undefined)
return '';
if (typeof v === 'number') {
if (Number.isInteger(v))
return String(v);
return v.toFixed(4).replace(/\.?0+$/, '');
}
return String(v);
}
function parseCell(str) {
// Return column index 0-based, row 0-based from A1-style
const m = str.match(/^([A-Z]+)(\d+)$/i);
if (!m)
return null;
let c = 0;
for (let i = 0; i < m[1].length; i++)
c = c * 26 + (m[1].charCodeAt(i) - (m[1].charCodeAt(i) >= 97 ? 97 : 65));
return { col: c - 1, row: parseInt(m[2], 10) - 1 };
}
function cellToString(c) {
let s = '';
let n = c.col + 1;
while (n > 0) {
s = String.fromCharCode(65 + ((n - 1) % 26)) + s;
n = Math.floor((n - 1) / 26);
}
return s + (c.row + 1);
}
function isFormula(str) { return str.startsWith('='); }
function extractRefs(node) {
const refs = [];
function walk(n) {
if (!n)
return;
if (n.op === 'ref')
refs.push(cellToString(n.val));
if (n.op === 'func')
n.args.forEach(walk);
else {
if (n.left)
walk(n.left);
if (n.right)
walk(n.right);
}
}
walk(node);
return refs;
}
function extractRangeRefs(node) {
const refs = [];
function walk(n) {
if (!n)
return;
if (n.op === 'range')
refs.push(cellToString(n.start) + ':' + cellToString(n.end));
if (n.op === 'func')
n.args.forEach(walk);
else {
if (n.left)
walk(n.left);
if (n.right)
walk(n.right);
}
}
walk(node);
return refs;
}
// For dependency graph updates when rows/cols inserted/deleted
function rewriteFormula(raw, op, idx) {
if (!raw.startsWith('='))
return raw;
// Simplified: only handle references that shift; ranges also shift
// We'll do a regex-based rewrite for common cases
let s = raw.slice(1);
// Insert row before idx: all row refs >= idx increase by 1
// Delete row at idx: all refs >= idx shift down by 1 (if target exists, else #REF!)
// For simplicity, parse and rewrite refs
// Actually do basic regex on references
const refRe = /([A-Z]+)(\d+)/g;
s = s.replace(refRe, (_m, col, rowStr) => {
const r = parseInt(rowStr, 10);
if (op === 'insertRow' && r >= idx + 1)
return col + (r + 1);
if (op === 'deleteRow' && r > idx + 1)
return col + (r - 1);
if (op === 'deleteRow' && r === idx + 1)
return col + r; // might become invalid; handled by engine
if (op === 'insertCol') {
const c = colToIndex(col);
if (c >= idx)
return indexToCol(c + 1) + rowStr;
}
if (op === 'deleteCol') {
const c = colToIndex(col);
if (c > idx)
return indexToCol(c - 1) + rowStr;
}
return _m;
});
// For ranges A1:B2
s = s.replace(/([A-Z]+\d+):([A-Z]+\d+)/g, (m, a, b) => {
const aa = parseCell(a);
const bb = parseCell(b);
if (!aa || !bb)
return m;
let na = a, nb = b;
if (op === 'insertRow') {
if (aa.row >= idx)
na = a[0] + a.slice(1); // rough; skip detailed range rewrite for simplicity
if (bb.row >= idx)
nb = b[0] + b.slice(1); // actually do numeric
// Simple numeric rewrite for range ends
const ar = parseInt(a.match(/\d+/)[0], 10);
const br = parseInt(b.match(/\d+/)[0], 10);
na = a.replace(/\d+/, String(ar >= idx + 1 ? ar + 1 : ar));
nb = b.replace(/\d+/, String(br >= idx + 1 ? br + 1 : br));
}
else if (op === 'deleteRow') {
const ar = parseInt(a.match(/\d+/)[0], 10);
const br = parseInt(b.match(/\d+/)[0], 10);
na = a.replace(/\d+/, String(ar > idx + 1 ? ar - 1 : ar));
nb = b.replace(/\d+/, String(br > idx + 1 ? br - 1 : br));
}
else if (op === 'insertCol') {
const ac = colToIndex(a.replace(/\d+$/, ''));
const bc = colToIndex(b.replace(/\d+$/, ''));
if (ac >= idx)
na = indexToCol(ac + 1) + a.replace(/^[A-Z]+/, '');
if (bc >= idx)
nb = indexToCol(bc + 1) + b.replace(/^[A-Z]+/, '');
}
else if (op === 'deleteCol') {
const ac = colToIndex(a.replace(/\d+$/, ''));
const bc = colToIndex(b.replace(/\d+$/, ''));
if (ac > idx)
na = indexToCol(ac - 1) + a.replace(/^[A-Z]+/, '');
if (bc > idx)
nb = indexToCol(bc - 1) + b.replace(/^[A-Z]+/, '');
}
return na + ':' + nb;
});
return '=' + s;
}
function colToIndex(col) {
let n = 0;
for (let i = 0; i < col.length; i++)
n = n * 26 + (col.charCodeAt(i) - 64);
return n - 1;
}
function indexToCol(n) {
let s = '';
let x = n + 1;
while (x > 0) {
s = String.fromCharCode(65 + ((x - 1) % 26)) + s;
x = Math.floor((x - 1) / 26);
}
return s;
}
function findCycles(allCells, refsMap) {
const visited = new Set();
const recStack = new Set();
const cycleNodes = new Set();
function dfs(key) {
visited.add(key);
recStack.add(key);
const refs = refsMap.get(key) || [];
for (const r of refs) {
if (!visited.has(r))
dfs(r);
else if (recStack.has(r)) {
// found cycle; mark all in current path that are part of it
// For simplicity, just mark all nodes that reach back; mark r and current
cycleNodes.add(r);
cycleNodes.add(key);
}
}
recStack.delete(key);
}
for (const k of allCells.keys()) {
if (!visited.has(k))
dfs(k);
}
return cycleNodes;
}
function getRefsInString(str) {
const refs = [];
if (!str.startsWith('='))
return refs;
try {
const p = compileFormula(str);
const arr = extractRefs(p);
for (const r of arr)
refs.push(r);
}
catch { }
return refs;
}
function getRangeInString(str) {
const refs = [];
if (!str.startsWith('='))
return refs;
try {
const p = compileFormula(str);
const arr = extractRangeRefs(p);
refs.push(...arr);
}
catch { }
return refs;
}
function getAllRefs(str) {
return [...getRefsInString(str), ...getRangeInString(str).flatMap(r => {
const [s, e] = r.split(':');
const res = [];
const cs = parseCell(s);
const ce = parseCell(e);
if (cs && ce) {
for (let rr = cs.row; rr <= ce.row; rr++) {
for (let cc = cs.col; cc <= ce.col; cc++) {
res.push(cellToString({ col: cc, row: rr }));
}
}
}
return res;
})];
}
function computeDependencies(formulas) {
const graph = new Map();
for (const [k, raw] of formulas) {
if (raw.startsWith('=')) {
graph.set(k, getAllRefs(raw));
}
else {
graph.set(k, []);
}
}
return graph;
}
function topologicalOrder(needsRecalc, graph) {
const visited = new Set();
const order = [];
function visit(n) {
if (visited.has(n))
return;
visited.add(n);
for (const dep of (graph.get(n) || [])) {
if (needsRecalc.has(dep))
visit(dep);
}
order.push(n);
}
for (const n of needsRecalc)
visit(n);
return order;
}
// For engine to evaluate a cell given current formula map and cached values
function evaluateCell(key, formulas, computed, cycleSet) {
if (cycleSet.has(key))
return '#CYCLE!';
const raw = formulas.get(key);
if (raw === undefined || raw === '')
return '';
if (!raw.startsWith('=')) {
const n = parseFloat(raw);
return isNaN(n) ? raw : n;
}
// Check if any dependency is error/cycle
const refs = getRefsInString(raw);
for (const r of refs) {
if (cycleSet.has(r))
return '#CYCLE!';
}
try {
const compiled = compileFormula(raw);
// Build getValue using computed cache; if missing compute recursively
const getValue = (ref) => {
const rk = `${ref.col}-${ref.row}`;
const v = computed.get(rk);
if (v === undefined) {
const subRaw = formulas.get(rk);
if (subRaw === undefined || subRaw === '')
return 0; // empty -> 0 for arithmetic
if (subRaw.startsWith('=')) {
const val = evaluateCell(rk, formulas, computed, cycleSet);
if (typeof val === 'string' && val.startsWith('#'))
return val;
computed.set(rk, val);
return val;
}
else {
const n = parseFloat(subRaw);
computed.set(rk, isNaN(n) ? subRaw : n);
return isNaN(n) ? 0 : n;
}
}
if (typeof v === 'string' && v.startsWith('#'))
return v;
return v;
};
const getRange = (start, end) => {
const arr = [];
for (let rr = start.row; rr <= end.row; rr++) {
for (let cc = start.col; cc <= end.col; cc++) {
const rk = `${cc}-${rr}`;
const subRaw = formulas.get(rk);
if (subRaw === undefined || subRaw === '')
continue;
let val = 0;
if (subRaw.startsWith('=')) {
val = evaluateCell(rk, formulas, computed, cycleSet);
}
else {
const n = parseFloat(subRaw);
val = isNaN(n) ? subRaw : n;
}
if (typeof val === 'string' && val.startsWith('#'))
return val; // propagate error
if (typeof val === 'number')
arr.push(val);
}
}
return arr;
};
// We need to handle range args inside evalExpr. Modify evalExpr to use getRange directly.
// Actually our evalExpr handles range nodes by calling getRange, but it needs context.
// Let's call our own evaluate that passes getValue/getRange.
// For simplicity, we'll expand evalExpr directly here with closures.
const result = evalExpr(compiled, getValue, getRange);
if (typeof result === 'string')
return result;
return result;
}
catch (e) {
return '#ERR!';
}
}
// Simple version of getValue for range args in engine
function getRangeForFunc(start, end, formulas, computed, cycleSet) {
const arr = [];
for (let rr = start.row; rr <= end.row; rr++) {
for (let cc = start.col; cc <= end.col; cc++) {
const rk = `${cc}-${rr}`;
const subRaw = formulas.get(rk);
if (subRaw === undefined || subRaw === '')
continue;
let val = 0;
if (subRaw.startsWith('=')) {
val = evaluateCell(rk, formulas, computed, cycleSet);
}
else {
const n = parseFloat(subRaw);
val = isNaN(n) ? subRaw : n;
}
if (typeof val === 'string' && val.startsWith('#'))
return [];
if (typeof val === 'number')
arr.push(val);
}
}
return arr;
}
function computeCell(key, formulas, computed, cycleSet) {
return evaluateCell(key, formulas, computed, cycleSet);
}
// Rebuild computed values for needed cells
function recalcCells(changedKey, formulas, computed, depGraph, cycleSet) {
const allKeys = Array.from(formulas.keys());
const dependents = new Set();
// For simplicity, just recompute all cells in dependency order (topological over whole graph)
// This is acceptable for 2600 cells.
const order = topologicalOrder(new Set(allKeys), depGraph);
for (const k of order) {
computed.set(k, evaluateCell(k, formulas, computed, cycleSet));
}
}
// Fast targeted recalc only for dependents
function recalcDependents(startKey, formulas, computed, depGraph, cycleSet) {
// Find all cells that depend (directly or indirectly) on startKey
const affected = new Set();
const rev = new Map();
for (const [k, refs] of depGraph) {
for (const r of refs) {
if (!rev.has(r))
rev.set(r, []);
rev.get(r).push(k);
}
}
const stack = [startKey];
while (stack.length) {
const cur = stack.pop();
for (const d of (rev.get(cur) || [])) {
if (!affected.has(d)) {
affected.add(d);
stack.push(d);
}
}
}
affected.add(startKey);
const order = topologicalOrder(affected, depGraph);
for (const k of order) {
computed.set(k, evaluateCell(k, formulas, computed, cycleSet));
}
}
function detectCycles(formulas) {
const refsMap = new Map();
for (const [k, v] of formulas) {
if (v.startsWith('='))
refsMap.set(k, getRefsInString(v));
else
refsMap.set(k, []);
}
return findCycles(formulas, refsMap);
}
function getRangeVals(start, end, formulas, computed, cycleSet) {
const arr = [];
for (let rr = start.row; rr <= end.row; rr++) {
for (let cc = start.col; cc <= end.col; cc++) {
const rk = `${cc}-${rr}`;
const subRaw = formulas.get(rk);
if (subRaw === undefined || subRaw === '')
continue;
let val = 0;
if (subRaw.startsWith('=')) {
val = evaluateCell(rk, formulas, computed, cycleSet);
}
else {
const n = parseFloat(subRaw);
val = isNaN(n) ? subRaw : n;
}
if (typeof val === 'string' && val.startsWith('#'))
return val; // propagate error
if (typeof val === 'number')
arr.push(val);
}
}
return arr;
}
// Monkey-patch evalExpr to use closures; since evalExpr is exported and takes getRange, we'll rely on engine to pass correct closures.
// Already implemented above.
// Additional helper for full sheet rebuild
function buildFullComputed(formulas, cycleSet) {
const computed = new Map();
const depGraph = computeDependencies(formulas);
const order = topologicalOrder(new Set(formulas.keys()), depGraph);
for (const k of order) {
computed.set(k, evaluateCell(k, formulas, computed, cycleSet));
}
return computed;
}
function getCellValueString(key, formulas, computed, cycleSet) {
const v = computed.get(key);
if (v === undefined) {
if (!formulas.has(key))
return '';
return formatValue(evaluateCell(key, formulas, computed, cycleSet));
}
return formatValue(v);
}
function getFormulaString(key, formulas) {
return formulas.get(key) || '';
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingmachines/inkling-small","provider":"openrouter","persona":"typescript","sessionId":"3a219f58-eb0a-4b64-b876-9ce23b1cf009","tokensIn":3292091,"tokensOut":34210,"tokensTotal":3326301,"cost":0.4005439,"turns":73,"toolCalls":72,"failedToolCalls":7,"timestamp":"2026-08-01T17:22:48.608Z"} -->