Spreadsheet
inkling · 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 50,014 bytes · SHA-256 4f6c83f06b8f
<!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; margin: 0; padding: 0; }
html, body { height: 100%; overflow: hidden; }
#app-inner { display: flex; flex-direction: column; height: 100vh; font-family: 'Segoe UI', sans-serif; }
#toolbar { padding: 8px 12px; background: #f1f3f4; border-bottom: 1px solid #e0e0e0; display: flex; gap: 8px; align-items: center; }
#toolbar button { background: #fff; border: 1px solid #dadce0; padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 13px; }
#toolbar button:hover { background: #e8eaed; }
#formula-bar-area { padding: 6px 12px; background: #fff; border-bottom: 1px solid #e0e0e0; display: flex; align-items: center; gap: 8px; font-family: monospace; font-size: 14px; }
#name-box { font-weight: bold; color: #1a73e8; width: 60px; text-align: center; border-right: 1px solid #e0e0e0; padding-right: 8px; flex-shrink: 0; }
#formula-bar { flex: 1; border: 1px solid #d0d0d0; padding: 5px 8px; border-radius: 4px; font-family: monospace; font-size: 14px; outline: none; }
#formula-bar:focus { border-color: #1a73e8; }
#grid-container { flex: 1; overflow: auto; position: relative; background: #fff; }
.grid-table { border-collapse: collapse; table-layout: fixed; user-select: none; }
.grid-table th, .grid-table td { border: 1px solid #e0e0e0; height: 28px; padding: 0 6px; font-size: 13px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.grid-table th { background: #f8f9fa; font-weight: 600; color: #5f6368; text-align: center; position: sticky; top: 0; z-index: 10; height: 28px; }
.grid-table th:first-child, .grid-table td:first-child { position: sticky; left: 0; background: #f8f9fa; z-index: 11; width: 50px; text-align: center; }
.grid-table td:first-child { background: #f8f9fa; font-weight: 500; color: #5f6368; }
.grid-table td { text-align: right; color: #202124; cursor: cell; }
.grid-table td.text-left { text-align: left; }
.grid-table td.selected { border: 2px solid #1a73e8; background: #e8f0fe; outline: none; z-index: 5; }
.grid-table td.error { color: #d93025; }
.grid-table td[data-col="0"] { position: sticky; left: 0; z-index: 5; }
</style>
</head>
<body>
<div id="app-inner" style="display:flex;flex-direction:column;height:100vh;"></div><div id="toolbar" style="padding:8px;background:#f1f3f4;border-bottom:1px solid #e0e0e0;display:flex;gap:8px;align-items:center;"><button id="btn-insert-row">Insert Row</button><button id="btn-delete-row">Delete Row</button><button id="btn-insert-col">Insert Col</button><button id="btn-delete-col">Delete Col</button><button id="btn-save">Save</button></div><div id="formula-bar-area" style="padding:6px 12px;background:#fff;border-bottom:1px solid #e0e0e0;display:flex;align-items:center;gap:8px;font-family:monospace;font-size:14px;"><span id="name-box" style="font-weight:bold;color:#1a73e8;width:50px;text-align:center;border-right:1px solid #e0e0e0;padding-right:8px;">A1</span><input id="formula-bar" type="text" style="flex:1;border:1px solid #d0d0d0;padding:4px 8px;border-radius:4px;font-family:monospace;font-size:14px;outline:none;"></div><div id="grid-container" style="flex:1;overflow:auto;position:relative;background:#fff;"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./state":"src/state.ts","./parser":"src/parser.ts","./renderer":"src/renderer.ts","./config":"src/config.ts"},"src/state.ts":{"./config":"src/config.ts","./parser":"src/parser.ts"},"src/renderer.ts":{"./config":"src/config.ts","./state":"src/state.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 state_1 = require("./state");
const parser_1 = require("./parser");
const renderer_1 = require("./renderer");
const config_1 = require("./config");
function start() {
const state = new state_1.SpreadsheetState();
const saved = localStorage.getItem('spreadsheet_data');
if (saved) {
state.deserialize(saved);
}
else {
state.recalcAll();
}
const renderer = new renderer_1.Renderer('grid-container', state);
window.__sheetState = state;
window.__sheetRenderer = renderer;
window.__parseFormula = parser_1.parseFormula;
renderer.updateGrid();
// Toolbar buttons wire-up
document.getElementById('btn-insert-row')?.addEventListener('click', () => {
renderer.insertRow(renderer.selectedRow);
});
document.getElementById('btn-delete-row')?.addEventListener('click', () => {
renderer.deleteRow(renderer.selectedRow);
});
document.getElementById('btn-insert-col')?.addEventListener('click', () => {
renderer.insertCol(renderer.selectedCol);
});
document.getElementById('btn-delete-col')?.addEventListener('click', () => {
renderer.deleteCol(renderer.selectedCol);
});
document.getElementById('btn-save')?.addEventListener('click', () => {
localStorage.setItem('spreadsheet_data', state.serialize());
});
// Auto-save on change (throttled)
let saveTimer = null;
const scheduleSave = () => {
if (saveTimer)
clearTimeout(saveTimer);
saveTimer = window.setTimeout(() => {
localStorage.setItem('spreadsheet_data', state.serialize());
}, 500);
};
// Override state.setRaw to trigger save
const originalSetRaw = state.setRaw.bind(state);
state.setRaw = (c, r, raw) => {
originalSetRaw(c, r, raw);
scheduleSave();
};
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (renderer.editMode)
return; // Let input handle
if (e.target && e.target.tagName === 'INPUT')
return;
switch (e.key) {
case 'ArrowUp':
e.preventDefault();
renderer.selectCell(renderer.selectedCol, Math.max(0, renderer.selectedRow - 1));
break;
case 'ArrowDown':
e.preventDefault();
renderer.selectCell(renderer.selectedCol, Math.min(config_1.ROWS - 1, renderer.selectedRow + 1));
break;
case 'ArrowLeft':
e.preventDefault();
renderer.selectCell(Math.max(0, renderer.selectedCol - 1), renderer.selectedRow);
break;
case 'ArrowRight':
e.preventDefault();
renderer.selectCell(Math.min(config_1.COLS - 1, renderer.selectedCol + 1), renderer.selectedRow);
break;
case 'Enter':
e.preventDefault();
renderer.enterEdit(renderer.selectedCol, renderer.selectedRow);
break;
case 'Delete':
e.preventDefault();
renderer.state.clearCell(renderer.selectedCol, renderer.selectedRow);
renderer.updateCellDisplay(renderer.selectedCol, renderer.selectedRow);
renderer.state.processDirty();
renderer.updateGrid();
scheduleSave();
break;
case 'Tab':
e.preventDefault();
const newCol = Math.min(config_1.COLS - 1, renderer.selectedCol + 1);
renderer.selectCell(newCol, renderer.selectedRow);
break;
}
});
// Click outside to deselect edit
document.addEventListener('click', (e) => {
const target = e.target;
if (renderer.editMode && target !== renderer.editInput && !renderer.container.contains(target)) {
renderer.commitEdit(renderer.selectedCol, renderer.selectedRow);
}
});
}
start();
};
// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.COLORS = exports.COLS_BY_LABEL = exports.HEADER_WIDTH = exports.HEADER_HEIGHT = exports.CELL_HEIGHT = exports.CELL_WIDTH = exports.ROWS = exports.COLS = void 0;
exports.COLS = 26;
exports.ROWS = 100;
exports.CELL_WIDTH = 100;
exports.CELL_HEIGHT = 28;
exports.HEADER_HEIGHT = 28;
exports.HEADER_WIDTH = 50;
exports.COLS_BY_LABEL = [...Array(exports.COLS)].map((_, i) => String.fromCharCode(65 + i));
exports.COLORS = {
bg: '#ffffff',
headerBg: '#f8f9fa',
selectedBorder: '#1a73e8',
selectedFill: '#e8f0fe',
gridLine: '#e0e0e0',
errorText: '#d93025',
formulaBg: '#fff',
toolbarBg: '#f1f3f4',
text: '#202124',
muted: '#5f6368',
};
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SpreadsheetState = void 0;
const config_1 = require("./config");
const parser_1 = require("./parser");
class SpreadsheetState {
constructor() {
this.cells = new Map();
this.dependents = new Map();
this.dependencies = new Map();
this.dirty = new Set();
this.cycleCells = new Set();
}
getKey(col, row) {
return `${String.fromCharCode(65 + col)}${row + 1}`;
}
getCell(col, row) {
const k = this.getKey(col, row);
if (!this.cells.has(k)) {
this.cells.set(k, { raw: '', value: null, error: null });
}
return this.cells.get(k);
}
setRaw(col, row, raw) {
const k = this.getKey(col, row);
const data = this.getCell(col, row);
data.raw = raw;
data.value = null;
data.error = null;
data.parsed = undefined;
this.cells.set(k, data);
this.clearDependencies(k);
if (raw.startsWith('=')) {
try {
const parsed = (0, parser_1.parseFormula)(raw.slice(1));
data.parsed = parsed;
for (const r of parsed.refs) {
this.addDependency(k, r);
}
}
catch (e) {
data.error = '#ERR!';
data.value = '#ERR!';
}
}
this.markDirty(k);
}
clearCell(col, row) {
this.setRaw(col, row, '');
}
clearDependencies(key) {
const oldDeps = this.dependencies.get(key) || new Set();
for (const dep of oldDeps) {
const dset = this.dependents.get(dep);
if (dset) {
dset.delete(key);
if (dset.size === 0)
this.dependents.delete(dep);
}
}
this.dependencies.set(key, new Set());
}
addDependency(from, to) {
if (!this.dependencies.has(from))
this.dependencies.set(from, new Set());
this.dependencies.get(from).add(to);
if (!this.dependents.has(to))
this.dependents.set(to, new Set());
this.dependents.get(to).add(from);
}
markDirty(key) {
this.dirty.add(key);
// Also mark all dependents dirty
const toPropagate = new Set();
const stack = [key];
const visited = new Set();
visited.add(key);
while (stack.length > 0) {
const curr = stack.pop();
for (const d of (this.dependents.get(curr) || [])) {
if (!visited.has(d)) {
visited.add(d);
toPropagate.add(d);
stack.push(d);
}
}
}
for (const k of toPropagate) {
this.dirty.add(k);
}
}
processDirty() {
// Compute dirty cells in topological order (simplified iterative approach)
const computed = new Set();
let changed = true;
while (changed && this.dirty.size > 0) {
changed = false;
const currentDirty = Array.from(this.dirty);
for (const k of currentDirty) {
if (computed.has(k)) {
this.dirty.delete(k);
continue;
}
// Check if all dependencies are computed (not dirty and not computing cycle)
const deps = this.dependencies.get(k) || new Set();
const uncomputedDeps = Array.from(deps).filter(d => !computed.has(d));
if (uncomputedDeps.length === 0) {
computed.add(k);
this.dirty.delete(k);
this.computeCellInternal(k, new Set());
changed = true;
}
else {
// Some dependencies not yet computed, skip for now
}
}
}
// For remaining dirty cells that have cycles, compute them to detect cycles
for (const k of Array.from(this.dirty)) {
this.computeCellInternal(k, new Set());
this.dirty.delete(k);
}
}
computeCellInternal(key, computing) {
const data = this.cells.get(key);
if (!data)
return;
if (!data.raw) {
data.value = null;
data.error = null;
return;
}
if (!data.raw.startsWith('=')) {
data.error = null;
let val = data.raw;
const num = Number(data.raw);
if (!isNaN(num) && data.raw !== '')
val = num;
data.value = val;
return;
}
if (!data.parsed) {
try {
data.parsed = (0, parser_1.parseFormula)(data.raw.slice(1));
for (const r of data.parsed.refs) {
this.addDependency(key, r);
}
}
catch (e) {
data.error = '#ERR!';
data.value = '#ERR!';
return;
}
}
// Detect cycle
if (computing.has(key)) {
data.error = '#CYCLE!';
data.value = '#CYCLE!';
this.cycleCells.add(key);
return;
}
computing.add(key);
try {
const resolve = (ref) => {
if (computing.has(ref)) {
return { value: null, error: '#CYCLE!' };
}
const refData = this.cells.get(ref);
if (!refData) {
return { value: null, error: '#REF!' };
}
if (refData.error === '#CYCLE!') {
return { value: null, error: '#CYCLE!' };
}
// Ensure dependency computed
if (refData.raw && (refData.value === null && refData.error === null)) {
// It needs computation; trigger computation if not dirty
}
// We rely on processDirty to handle order; here just return current value or compute on demand
if (refData.raw.startsWith('=') && refData.value === null && refData.error === null) {
// Compute on demand for nested evaluation
this.computeCellInternal(ref, new Set(computing));
}
if (refData.error) {
return { value: null, error: refData.error };
}
if (refData.value === null || refData.value === undefined) {
return { value: null, error: null };
}
if (typeof refData.value === 'number')
return { value: refData.value, error: null };
const num = Number(refData.value);
if (isNaN(num))
return { value: null, error: null };
return { value: num, error: null };
};
const result = data.parsed.evaluate(resolve);
computing.delete(key);
if (typeof result === 'string' && (result === '#DIV/0!' || result === '#REF!' || result === '#CYCLE!' || result === '#ERR!')) {
data.error = result;
data.value = result;
}
else {
data.error = null;
data.value = result === null ? null : result;
}
}
catch (e) {
computing.delete(key);
data.error = '#ERR!';
data.value = '#ERR!';
}
}
recalcAll() {
for (let r = 0; r < config_1.ROWS; r++) {
for (let c = 0; c < config_1.COLS; c++) {
this.dirty.add(this.getKey(c, r));
}
}
this.processDirty();
}
insertRow(afterRow) {
// Shift all rows >= afterRow down by 1, update references
const newCells = new Map();
for (const [k, data] of this.cells) {
const match = k.match(/^([A-Z]+)([0-9]+)$/);
if (match) {
const col = match[1];
const row = parseInt(match[2], 10);
if (row > afterRow) {
const newRow = row + 1;
const newKey = col + newRow;
newCells.set(newKey, data);
}
else if (row === afterRow) {
// Keep as is? Actually inserting after a row means rows below shift.
// If inserting afterRow (0-indexed), then row afterRow+1 and above shift.
// Let's assume afterRow is 0-indexed row number. Insert after that row -> rows > afterRow shift.
newCells.set(k, data);
}
else {
newCells.set(k, data);
}
}
else {
newCells.set(k, data);
}
}
this.cells = newCells;
// Rewrite formulas: any reference to a row > afterRow needs +1
for (const [k, data] of this.cells) {
if (data.raw.startsWith('=')) {
// Parse and rewrite
const rewritten = this.rewriteFormula(data.raw, afterRow, 'row', 1);
data.raw = rewritten;
data.value = null;
data.error = null;
data.parsed = undefined;
this.clearDependencies(k);
try {
const parsed = (0, parser_1.parseFormula)(rewritten.slice(1));
data.parsed = parsed;
for (const r of parsed.refs) {
this.addDependency(k, r);
}
}
catch (e) {
data.error = '#ERR!';
}
}
}
this.recalcAll();
}
deleteRow(rowIdx) {
const newCells = new Map();
for (const [k, data] of this.cells) {
const match = k.match(/^([A-Z]+)([0-9]+)$/);
if (match) {
const col = match[1];
const r = parseInt(match[2], 10);
if (r === rowIdx + 1) {
// Delete this row
continue;
}
else if (r > rowIdx + 1) {
const newR = r - 1;
newCells.set(col + newR, data);
}
else {
newCells.set(k, data);
}
}
else {
newCells.set(k, data);
}
}
this.cells = newCells;
for (const [k, data] of this.cells) {
if (data.raw.startsWith('=')) {
const rewritten = this.rewriteFormula(data.raw, rowIdx, 'row', -1);
data.raw = rewritten;
data.value = null;
data.error = null;
data.parsed = undefined;
this.clearDependencies(k);
try {
const parsed = (0, parser_1.parseFormula)(rewritten.slice(1));
data.parsed = parsed;
for (const r of parsed.refs) {
this.addDependency(k, r);
}
}
catch (e) {
data.error = '#ERR!';
}
}
}
this.recalcAll();
}
insertCol(afterCol) {
const newCells = new Map();
for (const [k, data] of this.cells) {
const match = k.match(/^([A-Z]+)([0-9]+)$/);
if (match) {
const colChar = match[1];
const row = match[2];
const colIdx = colChar.charCodeAt(0) - 65;
if (colIdx > afterCol) {
const newColChar = String.fromCharCode(colChar.charCodeAt(0) + 1);
newCells.set(newColChar + row, data);
}
else {
newCells.set(k, data);
}
}
else {
newCells.set(k, data);
}
}
this.cells = newCells;
for (const [k, data] of this.cells) {
if (data.raw.startsWith('=')) {
const rewritten = this.rewriteFormula(data.raw, afterCol, 'col', 1);
data.raw = rewritten;
data.value = null;
data.error = null;
data.parsed = undefined;
this.clearDependencies(k);
try {
const parsed = (0, parser_1.parseFormula)(rewritten.slice(1));
data.parsed = parsed;
for (const r of parsed.refs) {
this.addDependency(k, r);
}
}
catch (e) {
data.error = '#ERR!';
}
}
}
this.recalcAll();
}
deleteCol(colIdx) {
const newCells = new Map();
for (const [k, data] of this.cells) {
const match = k.match(/^([A-Z]+)([0-9]+)$/);
if (match) {
const colChar = match[1];
const row = match[2];
const cIdx = colChar.charCodeAt(0) - 65;
if (cIdx === colIdx) {
continue;
}
else if (cIdx > colIdx) {
const newColChar = String.fromCharCode(colChar.charCodeAt(0) - 1);
newCells.set(newColChar + row, data);
}
else {
newCells.set(k, data);
}
}
else {
newCells.set(k, data);
}
}
this.cells = newCells;
for (const [k, data] of this.cells) {
if (data.raw.startsWith('=')) {
const rewritten = this.rewriteFormula(data.raw, colIdx, 'col', -1);
data.raw = rewritten;
data.value = null;
data.error = null;
data.parsed = undefined;
this.clearDependencies(k);
try {
const parsed = (0, parser_1.parseFormula)(rewritten.slice(1));
data.parsed = parsed;
for (const r of parsed.refs) {
this.addDependency(k, r);
}
}
catch (e) {
data.error = '#ERR!';
}
}
}
this.recalcAll();
}
rewriteFormula(raw, index, type, delta) {
// Rewrite cell references in formula string
// Simple regex-based approach
const regex = /([A-Z]+)([0-9]+)/g;
return raw.replace(regex, (match, letters, digits) => {
if (type === 'row') {
const rowNum = parseInt(digits, 10) - 1; // 0-indexed
if (rowNum === index)
return match;
if (delta > 0 && rowNum > index) {
return letters + (rowNum + delta + 1);
}
if (delta < 0 && rowNum > index) {
return letters + (rowNum + delta + 1);
}
return match;
}
else {
const colIdx = letters.charCodeAt(0) - 65;
if (colIdx === index)
return match;
if (delta > 0 && colIdx > index) {
return String.fromCharCode(letters.charCodeAt(0) + delta) + digits;
}
if (delta < 0 && colIdx > index) {
return String.fromCharCode(letters.charCodeAt(0) + delta) + digits;
}
return match;
}
});
}
serialize() {
const obj = {};
for (const [k, data] of this.cells) {
if (data.raw !== '' || data.value !== null) {
obj[k] = data.raw;
}
}
return JSON.stringify(obj);
}
deserialize(json) {
this.cells.clear();
this.dependencies.clear();
this.dependents.clear();
this.dirty.clear();
this.cycleCells.clear();
try {
const obj = JSON.parse(json);
for (const [k, raw] of Object.entries(obj)) {
const data = { raw: raw, value: null, error: null };
this.cells.set(k, data);
if (raw.startsWith('=')) {
try {
const parsed = (0, parser_1.parseFormula)(raw.slice(1));
data.parsed = parsed;
for (const r of parsed.refs) {
this.addDependency(k, r);
}
}
catch (e) {
data.error = '#ERR!';
}
}
}
}
catch (e) {
// ignore
}
this.recalcAll();
}
}
exports.SpreadsheetState = SpreadsheetState;
};
// ── module: src/parser.ts ──
__mods["src/parser.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.tokenize = tokenize;
exports.parseFormula = parseFormula;
function tokenize(input) {
const tokens = [];
let i = 0;
while (i < input.length) {
const ch = input[i];
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
i++;
continue;
}
if (ch === '+' || ch === '-' || ch === '*' || ch === '/') {
tokens.push({ type: 'OP', value: ch, raw: ch });
i++;
continue;
}
if (ch === '(') {
tokens.push({ type: 'LPAREN', value: '(', raw: '(' });
i++;
continue;
}
if (ch === ')') {
tokens.push({ type: 'RPAREN', value: ')', raw: ')' });
i++;
continue;
}
if (ch === ',') {
tokens.push({ type: 'COMMA', value: ',', raw: ',' });
i++;
continue;
}
if (/[A-Z]/.test(ch)) {
let j = i;
while (j < input.length && /[A-Z]/.test(input[j]))
j++;
const letters = input.slice(i, j);
let k = j;
while (k < input.length && /[0-9]/.test(input[k]))
k++;
const digits = input.slice(j, k);
if (digits.length > 0) {
tokens.push({ type: 'REF', value: letters + digits, raw: letters + digits });
}
else {
if (k < input.length && input[k] === '(') {
tokens.push({ type: 'FUNC', value: letters, raw: letters });
}
else {
tokens.push({ type: 'REF', value: letters, raw: letters });
}
}
i = k;
continue;
}
if (/[0-9]/.test(ch) || ch === '.') {
let j = i;
let hasDot = false;
if (ch === '.') {
hasDot = true;
j++;
}
while (j < input.length) {
if (/[0-9]/.test(input[j])) {
j++;
}
else if (input[j] === '.' && !hasDot) {
hasDot = true;
j++;
}
else {
break;
}
}
tokens.push({ type: 'NUMBER', value: input.slice(i, j), raw: input.slice(i, j) });
i = j;
continue;
}
if (ch === ':') {
tokens.push({ type: 'OP', value: ':', raw: ':' });
i++;
continue;
}
i++;
}
tokens.push({ type: 'EOF', value: '', raw: '' });
return tokens;
}
function current(s) {
return s.tokens[s.pos];
}
function advance(s) {
return s.tokens[s.pos++];
}
function expect(s, type) {
if (current(s).type !== type) {
throw new Error(`Expected ${type} but got ${current(s).type}`);
}
return advance(s);
}
function parseFormula(input) {
const tokens = tokenize(input);
const s = { tokens, pos: 0 };
const refs = new Set();
function parseExpression() {
return parseAddSub();
}
function parseAddSub() {
let left = parseMulDiv();
while (current(s).type === 'OP' && (current(s).value === '+' || current(s).value === '-')) {
const op = advance(s).value;
const right = parseMulDiv();
left = { type: 'binop', op, left, right, refs: combineRefs(left, right) };
}
return left;
}
function parseMulDiv() {
let left = parseUnary();
while (current(s).type === 'OP' && (current(s).value === '*' || current(s).value === '/')) {
const op = advance(s).value;
const right = parseUnary();
left = { type: 'binop', op, left, right, refs: combineRefs(left, right) };
}
return left;
}
function parseUnary() {
if (current(s).type === 'OP' && current(s).value === '-') {
advance(s);
const val = parseUnary();
return { type: 'unary', op: '-', val, refs: val.refs ? val.refs : new Set() };
}
return parsePrimary();
}
function parsePrimary() {
const tok = current(s);
if (tok.type === 'NUMBER') {
advance(s);
return { type: 'number', value: Number(tok.value), refs: new Set() };
}
if (tok.type === 'REF') {
advance(s);
refs.add(tok.value);
return { type: 'ref', ref: tok.value, refs: new Set([tok.value]) };
}
if (tok.type === 'FUNC') {
const name = advance(s).value;
expect(s, 'LPAREN');
const args = [];
if (current(s).type !== 'RPAREN') {
args.push(parseArgument());
while (current(s).type === 'COMMA') {
advance(s);
args.push(parseArgument());
}
}
expect(s, 'RPAREN');
const combinedRefs = new Set();
for (const a of args) {
for (const r of (a.refs || []))
combinedRefs.add(r);
}
return { type: 'func', name, args, refs: combinedRefs };
}
if (tok.type === 'LPAREN') {
advance(s);
const inner = parseExpression();
expect(s, 'RPAREN');
return inner;
}
throw new Error(`Unexpected token: ${tok.type}`);
}
function parseArgument() {
const tok = current(s);
if (tok.type === 'REF') {
const ref1 = tok.value;
advance(s);
if (current(s).type === 'OP' && current(s).value === ':') {
advance(s);
const tok2 = current(s);
if (tok2.type === 'REF') {
const ref2 = tok2.value;
advance(s);
const rangeRefs = expandRange(ref1, ref2);
const combinedRefs = new Set();
for (const r of rangeRefs)
combinedRefs.add(r);
return { type: 'range', refs: combinedRefs, start: ref1, end: ref2, rangeRefs };
}
else {
throw new Error('Invalid range');
}
}
else {
return { type: 'ref', ref: ref1, refs: new Set([ref1]) };
}
}
if (tok.type === 'NUMBER') {
advance(s);
return { type: 'number', value: Number(tok.value), refs: new Set() };
}
if (tok.type === 'FUNC') {
const name = advance(s).value;
expect(s, 'LPAREN');
const args = [];
if (current(s).type !== 'RPAREN') {
args.push(parseArgument());
while (current(s).type === 'COMMA') {
advance(s);
args.push(parseArgument());
}
}
expect(s, 'RPAREN');
const combinedRefs = new Set();
for (const a of args) {
for (const r of (a.refs || []))
combinedRefs.add(r);
}
return { type: 'func', name, args, refs: combinedRefs };
}
return parseExpression();
}
const ast = parseExpression();
expect(s, 'EOF');
// Collect refs from AST
function collectRefs(node) {
const s = new Set();
if (node.type === 'ref') {
s.add(node.ref);
}
else if (node.type === 'range') {
for (const r of node.rangeRefs || [])
s.add(r);
}
else if (node.type === 'func') {
for (const arg of node.args || []) {
for (const r of collectRefs(arg))
s.add(r);
}
}
else if (node.type === 'binop' || node.type === 'unary') {
for (const r of collectRefs(node.left || node.val))
s.add(r);
if (node.type === 'binop') {
for (const r of collectRefs(node.right))
s.add(r);
}
}
for (const r of node.refs || [])
s.add(r);
return s;
}
for (const r of collectRefs(ast))
refs.add(r);
return {
refs,
evaluate: (resolve) => {
return evaluateNode(ast, resolve);
}
};
}
function combineRefs(a, b) {
const s = new Set();
for (const r of (a.refs || []))
s.add(r);
for (const r of (b.refs || []))
s.add(r);
return s;
}
function expandRange(start, end) {
const match1 = start.match(/^([A-Z]+)([0-9]+)$/);
const match2 = end.match(/^([A-Z]+)([0-9]+)$/);
if (!match1 || !match2)
return [];
const c1 = match1[1];
const r1 = parseInt(match1[2], 10);
const c2 = match2[1];
const r2 = parseInt(match2[2], 10);
const refs = [];
const startCol = c1.charCodeAt(0) - 65;
const endCol = c2.charCodeAt(0) - 65;
for (let c = startCol; c <= endCol; c++) {
for (let r = r1; r <= r2; r++) {
refs.push(String.fromCharCode(65 + c) + r);
}
}
return refs;
}
function evaluateNode(node, resolve) {
switch (node.type) {
case 'number':
return node.value;
case 'ref': {
const res = resolve(node.ref);
if (res.error)
return res.error;
if (res.value === null || res.value === undefined)
return 0;
if (typeof res.value === 'number')
return res.value;
const num = Number(res.value);
if (isNaN(num))
return 0;
return num;
}
case 'binop': {
const left = evaluateNode(node.left, resolve);
const right = evaluateNode(node.right, resolve);
if (typeof left === 'string' || typeof right === 'string')
return '#ERR!';
if (typeof left === 'number' && typeof right === 'number') {
if (node.op === '+')
return left + right;
if (node.op === '-')
return left - right;
if (node.op === '*')
return left * right;
if (node.op === '/') {
if (right === 0)
return '#DIV/0!';
return left / right;
}
}
return '#ERR!';
}
case 'unary': {
const val = evaluateNode(node.val, resolve);
if (typeof val === 'string')
return '#ERR!';
if (typeof val !== 'number')
return '#ERR!';
return -val;
}
case 'func': {
const args = node.args;
const name = node.name.toUpperCase();
let values = [];
for (const arg of args) {
if (arg.type === 'range') {
for (const ref of arg.rangeRefs) {
const res = resolve(ref);
if (res.error)
return res.error;
if (res.value === null || res.value === undefined)
continue;
const num = Number(res.value);
if (!isNaN(num))
values.push(num);
}
}
else if (arg.type === 'ref') {
const res = resolve(arg.ref);
if (res.error)
return res.error;
if (res.value === null || res.value === undefined)
continue;
const num = Number(res.value);
if (!isNaN(num))
values.push(num);
}
else if (arg.type === 'number') {
values.push(arg.value);
}
else if (arg.type === 'func') {
const nested = evaluateNode(arg, resolve);
if (typeof nested === 'number')
values.push(nested);
else if (nested === '#DIV/0!' || nested === '#REF!' || nested === '#CYCLE!' || nested === '#ERR!')
return nested;
}
else {
const val = evaluateNode(arg, resolve);
if (typeof val === 'number')
values.push(val);
else if (typeof val === 'string')
return val;
}
}
if (name === 'SUM') {
return values.reduce((a, b) => a + b, 0);
}
if (name === 'AVG' || name === 'AVERAGE') {
if (values.length === 0)
return 0;
return values.reduce((a, b) => a + b, 0) / values.length;
}
if (name === 'MIN') {
if (values.length === 0)
return 0;
return Math.min(...values);
}
if (name === 'MAX') {
if (values.length === 0)
return 0;
return Math.max(...values);
}
if (name === 'COUNT') {
return values.length;
}
return '#ERR!';
}
default:
return '#ERR!';
}
}
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = void 0;
const config_1 = require("./config");
class Renderer {
constructor(containerId, state) {
this.selectedCol = 0;
this.selectedRow = 0;
this.editMode = false;
this.formulaKeyHandler = (e) => { };
this.container = document.getElementById(containerId);
this.state = state;
this.table = document.createElement('table');
this.table.className = 'grid-table';
this.container.appendChild(this.table);
this.renderHeaders();
this.renderCells();
this.selectCell(0, 0);
}
renderHeaders() {
const thead = document.createElement('thead');
const tr = document.createElement('tr');
// Empty corner
const thCorner = document.createElement('th');
thCorner.style.width = config_1.HEADER_WIDTH + 'px';
thCorner.textContent = '';
tr.appendChild(thCorner);
for (let c = 0; c < config_1.COLS; c++) {
const th = document.createElement('th');
th.textContent = config_1.COLS_BY_LABEL[c];
th.style.width = config_1.CELL_WIDTH + 'px';
th.addEventListener('contextmenu', (e) => {
e.preventDefault();
this.showHeaderMenu(c, 'col');
});
tr.appendChild(th);
}
thead.appendChild(tr);
this.table.appendChild(thead);
}
renderCells() {
const tbody = document.createElement('tbody');
for (let r = 0; r < config_1.ROWS; r++) {
const tr = document.createElement('tr');
// Row header
const thRow = document.createElement('th');
thRow.textContent = String(r + 1);
thRow.style.width = config_1.HEADER_WIDTH + 'px';
thRow.addEventListener('contextmenu', (e) => {
e.preventDefault();
this.showHeaderMenu(r, 'row');
});
tr.appendChild(thRow);
for (let c = 0; c < config_1.COLS; c++) {
const td = document.createElement('td');
td.setAttribute('data-col', String(c));
td.setAttribute('data-row', String(r));
td.style.height = config_1.CELL_HEIGHT + 'px';
td.style.width = config_1.CELL_WIDTH + 'px';
td.addEventListener('click', () => {
this.selectCell(c, r);
});
td.addEventListener('dblclick', () => {
this.enterEdit(c, r);
});
tr.appendChild(td);
}
tbody.appendChild(tr);
}
this.table.appendChild(tbody);
}
showHeaderMenu(index, type) {
// Simple context menu using a temporary element
const menu = document.createElement('div');
menu.style.position = 'fixed';
menu.style.background = '#fff';
menu.style.border = '1px solid #ccc';
menu.style.zIndex = '100';
menu.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
menu.style.padding = '4px 0';
menu.innerHTML = `<div style="padding:6px 16px;cursor:pointer;font-size:13px;hover:bg:#e8f0fe;" onclick="this.getRootNode().host.insertRow()">Insert ${type}</div>
<div style="padding:6px 16px;cursor:pointer;font-size:13px;hover:bg:#e8f0fe;" onclick="this.getRootNode().host.deleteRow()">Delete ${type}</div>`;
// Actually we'll wire events properly below
menu.style.left = '100px';
menu.style.top = '100px';
document.body.appendChild(menu);
const insertBtn = menu.querySelector('div:nth-child(1)');
const deleteBtn = menu.querySelector('div:nth-child(2)');
insertBtn.addEventListener('click', () => {
if (type === 'row')
this.insertRow(index);
else
this.insertCol(index);
document.body.removeChild(menu);
});
deleteBtn.addEventListener('click', () => {
if (type === 'row')
this.deleteRow(index);
else
this.deleteCol(index);
document.body.removeChild(menu);
});
setTimeout(() => {
document.addEventListener('click', () => {
if (document.body.contains(menu))
document.body.removeChild(menu);
}, { once: true });
}, 0);
}
insertRow(rowIdx) {
this.state.insertRow(rowIdx);
this.updateGrid();
}
deleteRow(rowIdx) {
this.state.deleteRow(rowIdx);
this.updateGrid();
}
insertCol(colIdx) {
this.state.insertCol(colIdx);
this.updateGrid();
}
deleteCol(colIdx) {
this.state.deleteCol(colIdx);
this.updateGrid();
}
selectCell(c, r) {
this.selectedCol = c;
this.selectedRow = r;
this.updateSelectionUI();
this.updateFormulaBar();
}
enterEdit(c, r) {
this.selectCell(c, r);
this.editMode = true;
const td = this.getTd(c, r);
if (!td)
return;
const currentText = this.state.getCell(c, r).raw;
const input = document.createElement('input');
input.type = 'text';
input.value = currentText;
input.style.width = '100%';
input.style.height = '100%';
input.style.border = 'none';
input.style.padding = '0 6px';
input.style.fontFamily = 'inherit';
input.style.fontSize = '13px';
input.style.background = '#e8f0fe';
td.innerHTML = '';
td.appendChild(input);
input.focus();
input.select();
this.editInput = input;
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
this.commitEdit(c, r);
}
else if (e.key === 'Escape') {
this.cancelEdit();
}
});
input.addEventListener('blur', () => {
this.commitEdit(c, r);
});
}
commitEdit(c, r) {
if (!this.editInput)
return;
const raw = this.editInput.value;
this.editMode = false;
this.editInput = undefined;
this.state.setRaw(c, r, raw);
this.updateCellDisplay(c, r);
this.state.processDirty();
this.updateGrid();
}
cancelEdit() {
this.editMode = false;
this.editInput = undefined;
this.updateGrid();
}
updateSelectionUI() {
const tds = this.table.querySelectorAll('td');
tds.forEach(td => {
td.classList.remove('selected');
});
const td = this.getTd(this.selectedCol, this.selectedRow);
if (td)
td.classList.add('selected');
}
updateFormulaBar() {
const nameBox = document.getElementById('name-box');
const formulaBar = document.getElementById('formula-bar');
const colLabel = config_1.COLS_BY_LABEL[this.selectedCol];
const rowNum = this.selectedRow + 1;
if (nameBox)
nameBox.textContent = colLabel + rowNum;
const data = this.state.getCell(this.selectedCol, this.selectedRow);
if (formulaBar) {
formulaBar.value = data.raw;
formulaBar.removeEventListener('keydown', this.formulaKeyHandler);
this.formulaKeyHandler = (e) => {
const ke = e;
if (ke.key === 'Enter') {
const raw = e.target.value;
this.state.setRaw(this.selectedCol, this.selectedRow, raw);
this.updateCellDisplay(this.selectedCol, this.selectedRow);
this.state.processDirty();
this.updateGrid();
// Move selection down
const newRow = Math.min(this.selectedRow + 1, config_1.ROWS - 1);
this.selectCell(this.selectedCol, newRow);
}
};
formulaBar.addEventListener('keydown', this.formulaKeyHandler);
}
}
getTd(c, r) {
return this.table.querySelector(`td[data-col="${c}"][data-row="${r}"]`);
}
updateCellDisplay(c, r) {
const td = this.getTd(c, r);
if (!td)
return;
const data = this.state.getCell(c, r);
td.innerHTML = '';
if (data.error) {
td.textContent = data.error;
td.classList.add('error');
td.classList.remove('text-left');
}
else {
td.classList.remove('error');
if (data.value === null || data.value === '') {
td.textContent = '';
}
else {
td.textContent = String(data.value);
if (typeof data.value === 'number' || (typeof data.value === 'string' && !isNaN(Number(data.value)))) {
td.style.textAlign = 'right';
td.classList.remove('text-left');
}
else {
td.style.textAlign = 'left';
td.classList.add('text-left');
}
}
}
}
updateGrid() {
for (let r = 0; r < config_1.ROWS; r++) {
for (let c = 0; c < config_1.COLS; c++) {
this.updateCellDisplay(c, r);
}
}
this.updateSelectionUI();
this.updateFormulaBar();
}
}
exports.Renderer = Renderer;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>