Spreadsheet
Qwen 3.6 27B MTP GGUF · 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 55,080 bytes · SHA-256 17690696e7ef
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Spreadsheet</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Roboto, Arial, sans-serif;
font-size: 13px;
color: #202124;
background: #fff;
overflow: hidden;
}
#app {
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
}
/* Toolbar */
#toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: #f8f9fa;
border-bottom: 1px solid #dadce0;
min-height: 44px;
flex-shrink: 0;
}
#toolbar .logo { font-size: 20px; }
#toolbar .title { font-size: 14px; font-weight: 500; color: #202124; }
#toolbar .hint { margin-left: auto; color: #5f6368; font-size: 11px; }
/* Formula Bar */
#fbar-row {
display: flex;
align-items: center;
padding: 4px 8px;
background: #fff;
border-bottom: 1px solid #dadce0;
flex-shrink: 0;
}
#fbar-ref {
display: inline-flex;
align-items: center;
justify-content: center;
width: 56px;
height: 28px;
font-size: 12px;
font-weight: 600;
color: #5f6368;
background: #f1f3f4;
border: 1px solid #dadce0;
border-radius: 4px 0 0 4px;
text-align: center;
flex-shrink: 0;
user-select: none;
}
#fbar-input {
flex: 1;
height: 28px;
border: 1px solid #dadce0;
border-left: none;
border-radius: 0 4px 4px 0;
padding: 0 10px;
font-size: 13px;
font-family: 'Roboto Mono', 'Courier New', monospace;
outline: none;
background: #fff;
}
#fbar-input:focus { border-color: #1a73e8; }
/* Grid Container */
#grid-container {
flex: 1;
overflow: auto;
background: #fff;
position: relative;
}
/* Grid table */
.spreadsheet-grid { border-collapse: collapse; }
/* Column headers: sticky top */
.col-header {
position: sticky;
top: 0;
z-index: 5;
user-select: none;
}
/* Row headers: sticky left */
.row-header {
position: sticky;
left: 0;
z-index: 5;
user-select: none;
}
/* Corner cell: sticky both */
.corner {
position: sticky;
top: 0;
left: 0;
z-index: 10;
user-select: none;
}
</style>
</head>
<body>
<div id="toolbar"></div>
<div id="fbar-row">
<span id="fbar-ref"></span>
<span style="color:#bbb;padding:0 4px;">❯</span>
<input id="fbar-input" type="text" placeholder="Formula">
</div>
<div id="grid-container"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./model":"src/model.ts","./engine":"src/engine.ts","./view":"src/view.ts"},"src/types.ts":{"./config":"src/config.ts"},"src/model.ts":{"./config":"src/config.ts","./types":"src/types.ts"},"src/parser.ts":{"./config":"src/config.ts","./types":"src/types.ts"},"src/engine.ts":{"./config":"src/config.ts","./model":"src/model.ts","./parser":"src/parser.ts","./types":"src/types.ts"},"src/view.ts":{"./config":"src/config.ts","./model":"src/model.ts","./engine":"src/engine.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";
// ─── Entry Point ──────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
const model_1 = require("./model");
const engine_1 = require("./engine");
const view_1 = require("./view");
function start() {
const model = (0, model_1.loadModel)();
const graph = (0, engine_1.rebuildGraph)(model);
(0, engine_1.recalcAll)(model, graph);
const gridEl = document.getElementById('grid-container');
const fbarInput = document.getElementById('fbar-input');
const fbarRef = document.getElementById('fbar-ref');
const app = {
model,
selCol: 0,
selRow: 0,
editing: false,
editValue: '',
gridEl,
tableEl: document.createElement('table'),
fbarRef,
fbarInput,
cells: new Map(),
rowHeaders: new Map(),
colHeaders: new Map(),
};
// Expose for context menu
// Expose engine functions for testing/debugging
window.__spreadsheetApp = app;
window.rebuildGraph = engine_1.rebuildGraph;
window.recalcAll = engine_1.recalcAll;
window.recalcFrom = engine_1.recalcFrom;
window.commitEdit = () => (0, view_1.commitEdit)(app);
window.renderAll = () => (0, view_1.renderAll)(app);
window.selectCell = (c, r) => (0, view_1.selectCell)(app, c, r);
(0, view_1.buildGrid)(app);
(0, view_1.renderAll)(app);
(0, view_1.selectCell)(app, 0, 0);
(0, view_1.setupKeyboard)(app);
(0, view_1.setupFormulaBar)(app);
// Toolbar
const toolbar = document.getElementById('toolbar');
if (toolbar) {
toolbar.innerHTML = `
<span class="logo">📊</span>
<span class="title">Spreadsheet</span>
<span style="margin-left:auto;color:#5f6368;font-size:12px;">Right-click headers to insert/delete rows & columns</span>
`;
}
}
start();
};
// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ERR_PARSE = exports.ERR_REF = exports.ERR_DIV0 = exports.ERR_CYCLE = exports.COLOR_TOOLBAR_BG = exports.COLOR_MENU_HOVER = exports.COLOR_MENU_BG = exports.COLOR_FBAR_REF = exports.COLOR_FBAR_BORDER = exports.COLOR_FBAR_BG = exports.COLOR_NUM = exports.COLOR_TEXT = exports.COLOR_ERROR = exports.COLOR_SELECTION = exports.COLOR_HEADER_TEXT = exports.COLOR_HEADER_BG = exports.COLOR_GRIDLINE = exports.COLOR_BG = exports.CELL_H = exports.CELL_W = exports.HEADER_H = exports.HEADER_W = exports.ROWS = exports.COLS = void 0;
// ─── Tunable Constants ────────────────────────────────────────────────
exports.COLS = 26; // A–Z
exports.ROWS = 100;
exports.HEADER_W = 42; // row header column width
exports.HEADER_H = 24; // column header row height
exports.CELL_W = 96; // default cell width
exports.CELL_H = 26; // default cell height
// ─── Colors ───────────────────────────────────────────────────────────
exports.COLOR_BG = '#ffffff';
exports.COLOR_GRIDLINE = '#e0e0e0';
exports.COLOR_HEADER_BG = '#f8f9fa';
exports.COLOR_HEADER_TEXT = '#333';
exports.COLOR_SELECTION = '#1a73e8';
exports.COLOR_ERROR = '#d32f2f';
exports.COLOR_TEXT = '#202124';
exports.COLOR_NUM = '#202124';
exports.COLOR_FBAR_BG = '#fff';
exports.COLOR_FBAR_BORDER = '#dadce0';
exports.COLOR_FBAR_REF = '#5f6368';
exports.COLOR_MENU_BG = '#fff';
exports.COLOR_MENU_HOVER = '#f1f3f4';
exports.COLOR_TOOLBAR_BG = '#f1f3f4';
// ─── Special Values ──────────────────────────────────────────────────
exports.ERR_CYCLE = '#CYCLE!';
exports.ERR_DIV0 = '#DIV/0!';
exports.ERR_REF = '#REF!';
exports.ERR_PARSE = '#ERR!';
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ─── Type Definitions ─────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.isError = isError;
const config_1 = require("./config");
function isError(tag) {
return tag === config_1.ERR_CYCLE || tag === config_1.ERR_DIV0 || tag === config_1.ERR_REF || tag === config_1.ERR_PARSE;
}
};
// ── module: src/model.ts ──
__mods["src/model.ts"] = function (exports, require, module) {
"use strict";
// ─── Model: Grid data, serialization, cell address utilities ───────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.key = key;
exports.parseKey = parseKey;
exports.colToLetter = colToLetter;
exports.letterToCol = letterToCol;
exports.addrToLabel = addrToLabel;
exports.createModel = createModel;
exports.getCell = getCell;
exports.setCell = setCell;
exports.clearCell = clearCell;
exports.hasCell = hasCell;
exports.insertRow = insertRow;
exports.deleteRow = deleteRow;
exports.insertCol = insertCol;
exports.deleteCol = deleteCol;
exports.saveModel = saveModel;
exports.loadModel = loadModel;
const config_1 = require("./config");
function key(col, row) {
return `${col},${row}`;
}
function parseKey(k) {
const idx = k.indexOf(',');
return { col: parseInt(k.slice(0, idx), 10), row: parseInt(k.slice(idx + 1), 10) };
}
function colToLetter(col) {
return String.fromCharCode(65 + col);
}
function letterToCol(letter) {
return letter.toUpperCase().charCodeAt(0) - 65;
}
function addrToLabel(addr) {
return `${colToLetter(addr.col)}${addr.row + 1}`;
}
function createModel() {
return { cells: new Map(), cols: config_1.COLS, rows: config_1.ROWS };
}
function getCell(model, col, row) {
return model.cells.get(key(col, row));
}
function setCell(model, col, row, raw) {
model.cells.set(key(col, row), { raw });
}
function clearCell(model, col, row) {
model.cells.delete(key(col, row));
}
function hasCell(model, col, row) {
return model.cells.has(key(col, row));
}
// Insert row at given index, shift rows down, rewrite all cell references
function insertRow(model, row) {
model.rows++;
// Shift cells below the inserted row
const newCells = new Map();
for (const [k, data] of model.cells) {
const { col, row: r2 } = parseKey(k);
if (r2 >= row) {
const newKey = key(col, r2 + 1);
// Rewrite raw formula to point to shifted references
newCells.set(newKey, { raw: shiftRowRefs(data.raw, row, 1) });
}
else {
newCells.set(k, data);
}
}
model.cells = newCells;
}
// Delete row at given index, shift rows up
function deleteRow(model, row) {
if (model.rows <= 1)
return;
model.rows--;
const newCells = new Map();
for (const [k, data] of model.cells) {
const { col, row: r } = parseKey(k);
if (r === row) {
continue;
}
else if (r > row) {
const newKey = key(col, r - 1);
newCells.set(newKey, { raw: shiftRowRefs(data.raw, row, -1) });
}
else {
newCells.set(k, data);
}
}
model.cells = newCells;
for (const [k] of newCells) {
const { row: rr } = parseKey(k);
if (rr >= model.rows)
newCells.delete(k);
}
}
// Insert column at given index
function insertCol(model, col) {
model.cols++;
const newCells = new Map();
for (const [k, data] of model.cells) {
const { col: c2, row: r } = parseKey(k);
if (c2 >= col) {
const newKey = key(c2 + 1, r);
newCells.set(newKey, { raw: shiftColRefs(data.raw, col, 1) });
}
else {
newCells.set(k, data);
}
}
model.cells = newCells;
}
// Delete column
function deleteCol(model, col) {
if (model.cols <= 1)
return;
model.cols--;
const newCells = new Map();
for (const [k, data] of model.cells) {
const { col: c2, row: r } = parseKey(k);
if (c2 === col) {
continue;
}
else if (c2 > col) {
const newKey = key(c2 - 1, r);
newCells.set(newKey, { raw: shiftColRefs(data.raw, col, -1) });
}
else {
newCells.set(k, data);
}
}
model.cells = newCells;
for (const [k] of newCells) {
const { col: c2 } = parseKey(k);
if (c2 >= model.cols)
newCells.delete(k);
}
}
// Rewrite row references in a formula string
function shiftRowRefs(raw, pivotRow, delta) {
return raw.replace(/([A-Z])(\d+)/g, (_, letter, rowNum) => {
const row = parseInt(rowNum, 10) - 1; // 0-based
if (row >= pivotRow) {
return letter + (row + delta + 1);
}
return letter + rowNum;
});
}
// Rewrite column references in a formula string
function shiftColRefs(raw, pivotCol, delta) {
return raw.replace(/([A-Z])(\d+)/g, (_, letter, rowNum) => {
const col = letter.charCodeAt(0) - 65;
if (col >= pivotCol) {
return String.fromCharCode(65 + col + delta) + rowNum;
}
return letter + rowNum;
});
}
// ─── Serialization ────────────────────────────────────────────────────
const STORAGE_KEY = 'spreadsheet_data_v2';
function saveModel(model) {
const data = {};
data.cols = model.cols;
data.rows = model.rows;
for (const [k, cell] of model.cells) {
data[k] = cell.raw;
}
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
catch { }
}
function loadModel() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw)
return createModel();
const data = JSON.parse(raw);
const model = createModel();
model.cols = data.cols ?? config_1.COLS;
model.rows = data.rows ?? config_1.ROWS;
for (const [k, rawVal] of Object.entries(data)) {
if (k === 'cols' || k === 'rows')
continue;
if (typeof rawVal === 'string') {
model.cells.set(k, { raw: rawVal });
}
}
return model;
}
catch {
return createModel();
}
}
};
// ── module: src/parser.ts ──
__mods["src/parser.ts"] = function (exports, require, module) {
"use strict";
// ─── Formula Parser: Tokenizer + Recursive-Descent ─────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.tokenize = tokenize;
exports.parse = parse;
const config_1 = require("./config");
// ─── Tokenizer ────────────────────────────────────────────────────────
function tokenize(input) {
const tokens = [];
let i = 0;
const src = input.trim();
while (i < src.length) {
// Skip whitespace
if (src[i] === ' ') {
i++;
continue;
}
// Number (possibly with decimal point)
if (src[i] >= '0' && src[i] <= '9') {
let start = i;
while (i < src.length && ((src[i] >= '0' && src[i] <= '9') || src[i] === '.'))
i++;
tokens.push({ type: 'number', value: parseFloat(src.slice(start, i)) });
continue;
}
// Letter → name (cell ref or function)
if ((src[i] >= 'A' && src[i] <= 'Z') || (src[i] >= 'a' && src[i] <= 'z')) {
let start = i;
while (i < src.length && ((src[i] >= 'A' && src[i] <= 'Z') || (src[i] >= 'a' && src[i] <= 'z')))
i++;
tokens.push({ type: 'name', value: src.slice(start, i) });
continue;
}
// Operators and punctuation
switch (src[i]) {
case '+':
tokens.push({ type: 'op', value: '+' });
i++;
break;
case '-':
tokens.push({ type: 'op', value: '-' });
i++;
break;
case '*':
tokens.push({ type: 'op', value: '*' });
i++;
break;
case '/':
tokens.push({ type: 'op', value: '/' });
i++;
break;
case ',':
tokens.push({ type: 'comma' });
i++;
break;
case ':':
tokens.push({ type: 'colon' });
i++;
break;
case '(':
tokens.push({ type: 'lparen' });
i++;
break;
case ')':
tokens.push({ type: 'rparen' });
i++;
break;
default:
i++;
break; // skip unknown chars
}
}
tokens.push({ type: 'eof' });
return tokens;
}
function parse(input) {
const tokens = tokenize(input);
const p = { tokens, pos: 0, error: false };
const node = parseExpr(p);
if (p.error || p.pos >= p.tokens.length - 1) {
if (p.error)
return { kind: 'error', value: config_1.ERR_PARSE };
// Not all tokens consumed
const remaining = p.tokens[p.pos];
if (remaining && remaining.type !== 'eof') {
return { kind: 'error', value: config_1.ERR_PARSE };
}
}
return node;
}
function parseExpr(p) {
return parseAddExpr(p);
}
function isOpToken(tok, op) {
return tok.type === 'op' && tok.value === op;
}
function parseAddExpr(p) {
let left = parseMulExpr(p);
if (left.kind === 'error')
return left;
while (p.pos < p.tokens.length && (isOpToken(p.tokens[p.pos], '+') || isOpToken(p.tokens[p.pos], '-'))) {
const tok = p.tokens[p.pos];
const op = tok.type === 'op' ? tok.value : '+';
p.pos++;
const right = parseMulExpr(p);
if (right.kind === 'error')
return right;
left = { kind: 'binop', op, left, right };
}
return left;
}
function parseMulExpr(p) {
let left = parseUnary(p);
if (left.kind === 'error')
return left;
while (p.pos < p.tokens.length && (isOpToken(p.tokens[p.pos], '*') || isOpToken(p.tokens[p.pos], '/'))) {
const tok = p.tokens[p.pos];
const op = tok.type === 'op' ? tok.value : '*';
p.pos++;
const right = parseUnary(p);
if (right.kind === 'error')
return right;
left = { kind: 'binop', op, left, right };
}
return left;
}
function parseUnary(p) {
if (p.pos < p.tokens.length && isOpToken(p.tokens[p.pos], '-')) {
p.pos++;
const operand = parseUnary(p);
if (operand.kind === 'error')
return operand;
return { kind: 'unop', op: '-', operand };
}
return parsePrimary(p);
}
function parsePrimary(p) {
if (p.pos >= p.tokens.length) {
return { kind: 'error', value: config_1.ERR_PARSE };
}
const tok = p.tokens[p.pos];
// Number literal
if (tok.type === 'number') {
p.pos++;
return { kind: 'number', value: tok.value };
}
// Parenthesized expression
if (tok.type === 'lparen') {
p.pos++; // consume '('
const expr = parseExpr(p);
if (expr.kind === 'error')
return expr;
if (p.pos < p.tokens.length && p.tokens[p.pos].type === 'rparen') {
p.pos++;
}
else {
return { kind: 'error', value: config_1.ERR_PARSE };
}
return expr;
}
// Name → could be cell ref, range, or function
if (tok.type === 'name') {
const name = tok.value;
p.pos++;
// Check if it looks like a cell reference (letter(s) followed by digits)
// We need to look ahead for digits since we consumed the letter part
// Actually, cell refs like A1 are tokenized as NAME since digits follow letters
// Let's check: our tokenizer eats all letters, so "A1" → {type:'name', value:'A'}, then {type:'number', value:1}
// So a cell ref is actually NAME followed by NUMBER. Let me handle that.
// Check for function call: name followed by '('
if (p.pos < p.tokens.length && p.tokens[p.pos].type === 'lparen') {
// Function call
p.pos++; // consume '('
const args = [];
// Parse first argument
if (p.pos < p.tokens.length && p.tokens[p.pos].type !== 'rparen') {
args.push(parseExpr(p));
}
while (p.pos < p.tokens.length && p.tokens[p.pos].type === 'comma') {
p.pos++;
args.push(parseExpr(p));
}
if (p.pos < p.tokens.length && p.tokens[p.pos].type === 'rparen') {
p.pos++;
}
else {
return { kind: 'error', value: config_1.ERR_PARSE };
}
return { kind: 'function', name: name.toUpperCase(), args };
}
// Check if next token is a number → cell reference
if (p.pos < p.tokens.length && p.tokens[p.pos].type === 'number') {
const numTok = p.tokens[p.pos];
const rowVal = numTok.value;
p.pos++;
const col = parseColumnName(name);
if (col < 0)
return { kind: 'error', value: config_1.ERR_REF };
const row = Math.floor(rowVal) - 1; // 1-based to 0-based
if (row < 0)
return { kind: 'error', value: config_1.ERR_REF };
// Check for range: cellRef ':' cellRef
if (p.pos < p.tokens.length && p.tokens[p.pos].type === 'colon') {
p.pos++; // consume ':'
const rangeNode = parseCellRefOrRangeEnd(p);
if (rangeNode.kind === 'error')
return rangeNode;
if (rangeNode.kind === 'cell') {
return {
kind: 'range',
colStart: col, rowStart: row,
colEnd: rangeNode.col, rowEnd: rangeNode.row
};
}
return { kind: 'error', value: config_1.ERR_PARSE };
}
return { kind: 'cell', col, row };
}
// Just a name that's not a cell ref or function → treat as string
return { kind: 'string', value: name };
}
return { kind: 'error', value: config_1.ERR_PARSE };
}
function parseCellRefOrRangeEnd(p) {
if (p.pos >= p.tokens.length) {
return { kind: 'error', value: config_1.ERR_PARSE };
}
const tok = p.tokens[p.pos];
if (tok.type === 'name') {
const name = tok.value;
p.pos++;
if (p.pos < p.tokens.length && p.tokens[p.pos].type === 'number') {
const numTok = p.tokens[p.pos];
const rowVal = numTok.value;
p.pos++;
const col = parseColumnName(name);
if (col < 0)
return { kind: 'error', value: config_1.ERR_REF };
const row = Math.floor(rowVal) - 1;
return { kind: 'cell', col, row };
}
}
return { kind: 'error', value: config_1.ERR_PARSE };
}
function parseColumnName(name) {
// Support A-Z (single letter for now; could extend to AA, AB, etc.)
if (name.length === 1) {
return name.charCodeAt(0) - 65;
}
// Multi-letter column names (AA, AB, ..., AZ, BA, ...)
let col = 0;
for (let i = 0; i < name.length; i++) {
col = col * 26 + (name.charCodeAt(i) - 64);
}
return col - 1; // 0-based
}
};
// ── module: src/engine.ts ──
__mods["src/engine.ts"] = function (exports, require, module) {
"use strict";
// ─── Formula Evaluator & Dependency Graph ─────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.createDepGraph = createDepGraph;
exports.extractRefs = extractRefs;
exports.rebuildGraph = rebuildGraph;
exports.detectCycles = detectCycles;
exports.evaluateNode = evaluateNode;
exports.recalcFrom = recalcFrom;
exports.recalcAll = recalcAll;
const config_1 = require("./config");
const model_1 = require("./model");
const parser_1 = require("./parser");
function createDepGraph() {
return { deps: new Map(), reverseDeps: new Map() };
}
function ensureSet(map, k) {
if (!map.has(k))
map.set(k, new Set());
return map.get(k);
}
function extractRefs(raw) {
const refs = new Set();
if (!raw.startsWith('='))
return [];
const formula = raw.slice(1);
const node = (0, parser_1.parse)(formula);
if (node.kind === 'error')
return [];
collectRefs(node, refs);
return [...refs];
}
function collectRefs(node, refs) {
switch (node.kind) {
case 'cell':
refs.add((0, model_1.key)(node.col, node.row));
break;
case 'range': {
for (let r = Math.min(node.rowStart, node.rowEnd); r <= Math.max(node.rowStart, node.rowEnd); r++) {
for (let c = Math.min(node.colStart, node.colEnd); c <= Math.max(node.colStart, node.colEnd); c++) {
refs.add((0, model_1.key)(c, r));
}
}
break;
}
case 'binop':
collectRefs(node.left, refs);
collectRefs(node.right, refs);
break;
case 'unop':
collectRefs(node.operand, refs);
break;
case 'function':
for (const arg of node.args)
collectRefs(arg, refs);
break;
}
}
function rebuildGraph(model) {
const graph = createDepGraph();
for (const [cellKey, cell] of model.cells) {
if (!cell.raw.startsWith('='))
continue;
const refs = extractRefs(cell.raw).filter(r => r !== cellKey);
if (refs.length > 0) {
graph.deps.set(cellKey, new Set(refs));
for (const ref of refs) {
ensureSet(graph.reverseDeps, ref).add(cellKey);
}
}
}
return graph;
}
// ─── Cycle Detection ──────────────────────────────────────────────────
function detectCycles(model) {
const cycleCells = new Set();
// For each formula cell, do a DFS through deps looking for cycles
for (const [cellKey, cell] of model.cells) {
if (!cell.raw.startsWith('='))
continue;
// DFS from this cell through its deps
const path = new Set();
const allVisited = new Set();
if (findCycle(cellKey, model, path, allVisited, cycleCells)) {
// All cells remaining on the path are part of the cycle
for (const s of path)
cycleCells.add(s);
}
}
return cycleCells;
}
function findCycle(cellKey, model, path, allVisited, cycleCells) {
if (path.has(cellKey))
return true;
if (allVisited.has(cellKey))
return false;
allVisited.add(cellKey);
path.add(cellKey);
const cell = model.cells.get(cellKey);
if (cell?.raw.startsWith('=')) {
const refs = extractRefs(cell.raw).filter(r => r !== cellKey);
for (const ref of refs) {
if (findCycle(ref, model, path, allVisited, cycleCells)) {
cycleCells.add(cellKey);
path.delete(cellKey);
return true;
}
}
}
path.delete(cellKey);
return false;
}
function evaluateNode(node, model, evalStack) {
switch (node.kind) {
case 'number': return { value: node.value };
case 'string': return { value: node.value };
case 'error': return { error: node.value };
case 'cell': {
const cellKey = (0, model_1.key)(node.col, node.row);
if (node.col < 0 || node.col >= model.cols || node.row < 0 || node.row >= model.rows)
return { error: config_1.ERR_REF };
if (evalStack.has(cellKey))
return { error: config_1.ERR_CYCLE };
const cell = model.cells.get(cellKey);
if (!cell)
return { value: 0 };
if (cell.error)
return { error: cell.error };
const val = cell.value;
if (val === undefined || val === null || val === '')
return { value: 0 };
return { value: val };
}
case 'range': {
const values = [];
const cs = Math.min(node.colStart, node.colEnd);
const ce = Math.max(node.colStart, node.colEnd);
const rs = Math.min(node.rowStart, node.rowEnd);
const re = Math.max(node.rowStart, node.rowEnd);
for (let r = rs; r <= re; r++) {
for (let c = cs; c <= ce; c++) {
const cell = model.cells.get((0, model_1.key)(c, r));
if (cell) {
if (cell.error)
return { error: cell.error };
const val = cell.value;
if (typeof val === 'number')
values.push(val);
else if (typeof val === 'string' && val !== '') {
const parsed = parseFloat(val);
if (!isNaN(parsed))
values.push(parsed);
}
}
}
}
return { value: values };
}
case 'unop': {
const result = evaluateNode(node.operand, model, evalStack);
if (result.error)
return result;
if (typeof result.value === 'number')
return { value: -result.value };
return { error: config_1.ERR_PARSE };
}
case 'binop': {
const left = evaluateNode(node.left, model, evalStack);
if (left.error)
return left;
if (Array.isArray(left.value))
return { error: config_1.ERR_PARSE };
const right = evaluateNode(node.right, model, evalStack);
if (right.error)
return right;
if (Array.isArray(right.value))
return { error: config_1.ERR_PARSE };
const lv = typeof left.value === 'number' ? left.value : parseNum(left.value);
const rv = typeof right.value === 'number' ? right.value : parseNum(right.value);
if (isNaN(lv) || isNaN(rv))
return { error: config_1.ERR_PARSE };
switch (node.op) {
case '+': return { value: lv + rv };
case '-': return { value: lv - rv };
case '*': return { value: lv * rv };
case '/': return rv === 0 ? { error: config_1.ERR_DIV0 } : { value: lv / rv };
default: return { error: config_1.ERR_PARSE };
}
}
case 'function': return evaluateFunction(node.name, node.args, model, evalStack);
default: return { error: config_1.ERR_PARSE };
}
}
function parseNum(v) {
if (v === undefined || v === null || v === '')
return 0;
if (typeof v === 'number')
return v;
if (typeof v === 'string') {
const n = parseFloat(v);
return isNaN(n) ? 0 : n;
}
return 0;
}
function evaluateFunction(name, args, model, evalStack) {
const numbers = [];
for (const arg of args) {
const result = evaluateNode(arg, model, evalStack);
if (result.error)
return result;
if (Array.isArray(result.value)) {
for (const n of result.value)
numbers.push(n);
}
else if (typeof result.value === 'number')
numbers.push(result.value);
else if (typeof result.value === 'string') {
const n = parseFloat(result.value);
if (!isNaN(n))
numbers.push(n);
}
}
switch (name) {
case 'SUM': return { value: numbers.reduce((a, b) => a + b, 0) };
case 'AVG':
case 'AVERAGE':
return numbers.length === 0 ? { value: 0 } : { value: numbers.reduce((a, b) => a + b, 0) / numbers.length };
case 'MIN': return numbers.length > 0 ? { value: Math.min(...numbers) } : { value: 0 };
case 'MAX': return numbers.length > 0 ? { value: Math.max(...numbers) } : { value: 0 };
case 'COUNT': return { value: numbers.length };
default: return { error: config_1.ERR_PARSE };
}
}
// ─── Recalculation ────────────────────────────────────────────────────
function computeCell(model, cellKey) {
const cell = model.cells.get(cellKey);
if (!cell)
return { raw: '' };
if (!cell.raw || !cell.raw.startsWith('=')) {
const n = parseFloat(cell.raw);
return { raw: cell.raw, value: (isNaN(n) || cell.raw === '') ? cell.raw : n };
}
const node = (0, parser_1.parse)(cell.raw.slice(1));
if (node.kind === 'error')
return { raw: cell.raw, error: node.value };
const evalStack = new Set();
evalStack.add(cellKey);
const result = evaluateNode(node, model, evalStack);
evalStack.delete(cellKey);
if (result.error)
return { raw: cell.raw, error: result.error };
const val = result.value;
let displayVal;
if (Array.isArray(val))
displayVal = val.join(',');
else if (typeof val === 'number')
displayVal = Math.round(val * 1e10) / 1e10;
else
displayVal = String(val ?? '');
return { raw: cell.raw, value: displayVal };
}
// Recompute starting from a changed cell, cascading to dependents
function recalcFrom(model, graph, cellKey) {
const cycleCells = detectCycles(model);
// Collect all cells that need recalculation using BFS through reverse deps
const toRecalc = new Set();
const queue = [cellKey];
while (queue.length > 0) {
const current = queue.shift();
if (toRecalc.has(current))
continue;
toRecalc.add(current);
const reverse = graph.reverseDeps.get(current);
if (reverse) {
for (const dep of reverse) {
if (!toRecalc.has(dep))
queue.push(dep);
}
}
}
// Sort in dependency order: cells with no deps in toRecalc first
const sorted = topoSortSet(graph, toRecalc, cycleCells);
// Mark cycle cells
for (const ck of cycleCells) {
const cell = model.cells.get(ck);
if (cell)
model.cells.set(ck, { raw: cell.raw, value: '#CYCLE!', error: config_1.ERR_CYCLE });
}
// Evaluate in order
for (const ck of sorted) {
if (cycleCells.has(ck))
continue;
model.cells.set(ck, computeCell(model, ck));
}
}
// Recompute ALL formula cells in the correct dependency order
function recalcAll(model, graph) {
const cycleCells = detectCycles(model);
// Step 1: Set values for non-formula cells first (so formulas can read them)
for (const [cellKey, cell] of model.cells) {
if (!cell.raw || !cell.raw.startsWith('=')) {
const n = parseFloat(cell.raw);
model.cells.set(cellKey, { raw: cell.raw, value: (isNaN(n) || cell.raw === '') ? cell.raw : n });
}
}
// Step 2: Collect all formula cell keys
const formulaKeys = new Set();
for (const [cellKey, cell] of model.cells) {
if (cell.raw.startsWith('='))
formulaKeys.add(cellKey);
}
const sorted = topoSortSet(graph, formulaKeys, cycleCells);
// Step 3: Mark cycle cells
for (const ck of cycleCells) {
const cell = model.cells.get(ck);
if (cell)
model.cells.set(ck, { raw: cell.raw, value: '#CYCLE!', error: config_1.ERR_CYCLE });
}
// Step 4: Evaluate formula cells in topological order
for (const ck of sorted) {
if (cycleCells.has(ck))
continue;
model.cells.set(ck, computeCell(model, ck));
}
}
// Topological sort of a set of cells in dependency order
// Returns cells in order: dependencies before dependents
function topoSortSet(graph, cellSet, cycleCells) {
const sorted = [];
const visited = new Set();
const visiting = new Set(); // for cycle detection during topo sort
for (const ck of cellSet) {
if (!visited.has(ck)) {
topoVisit(ck, graph, cellSet, visited, visiting, sorted, cycleCells);
}
}
return sorted;
}
function topoVisit(cellKey, graph, cellSet, visited, visiting, sorted, cycleCells) {
if (visited.has(cellKey))
return;
if (visiting.has(cellKey))
return; // already being processed
if (cycleCells.has(cellKey)) {
visited.add(cellKey);
return;
}
visiting.add(cellKey);
// Visit dependencies first
const deps = graph.deps.get(cellKey);
if (deps) {
for (const dep of deps) {
// Visit even if not in cellSet (they might be external deps)
if (!visited.has(dep) && !cycleCells.has(dep)) {
topoVisit(dep, graph, cellSet, visited, visiting, sorted, cycleCells);
}
}
}
visiting.delete(cellKey);
visited.add(cellKey);
if (cellSet.has(cellKey) && !cycleCells.has(cellKey)) {
sorted.push(cellKey);
}
}
};
// ── module: src/view.ts ──
__mods["src/view.ts"] = function (exports, require, module) {
"use strict";
// ─── View: Grid rendering, input handling, context menus ──────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildGrid = buildGrid;
exports.renderCellDisplay = renderCellDisplay;
exports.renderAll = renderAll;
exports.selectCell = selectCell;
exports.startEdit = startEdit;
exports.commitEdit = commitEdit;
exports.cancelEdit = cancelEdit;
exports.setupKeyboard = setupKeyboard;
exports.setupFormulaBar = setupFormulaBar;
const config_1 = require("./config");
const model_1 = require("./model");
const engine_1 = require("./engine");
// ─── Build Grid DOM ───────────────────────────────────────────────────
function buildGrid(app) {
const { model, gridEl } = app;
gridEl.innerHTML = '';
const table = document.createElement('table');
table.style.borderCollapse = 'collapse';
table.style.width = `${config_1.HEADER_W + model.cols * config_1.CELL_W}px`;
table.style.tableLayout = 'fixed';
app.tableEl = table;
app.cells = new Map();
app.rowHeaders = new Map();
app.colHeaders = new Map();
// Header row
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
const corner = document.createElement('th');
corner.className = 'corner';
corner.style.cssText = `width:${config_1.HEADER_W}px;height:${config_1.HEADER_H}px;min-width:${config_1.HEADER_W}px;background:${config_1.COLOR_MENU_BG};border-bottom:2px solid ${config_1.COLOR_GRIDLINE};border-right:1px solid ${config_1.COLOR_GRIDLINE};`;
headerRow.appendChild(corner);
for (let c = 0; c < model.cols; c++) {
const th = document.createElement('th');
th.textContent = (0, model_1.colToLetter)(c);
th.className = 'col-header';
th.style.cssText = `width:${config_1.CELL_W}px;min-width:${config_1.CELL_W}px;height:${config_1.HEADER_H}px;background:${config_1.COLOR_MENU_BG};text-align:center;font-weight:600;color:#333;border-bottom:2px solid ${config_1.COLOR_GRIDLINE};border-right:1px solid ${config_1.COLOR_GRIDLINE};`;
th.dataset.col = String(c);
app.colHeaders.set(c, th);
th.addEventListener('contextmenu', (e) => { e.preventDefault(); showHeaderMenu(e, c, 'col'); });
headerRow.appendChild(th);
}
thead.appendChild(headerRow);
table.appendChild(thead);
// Body rows
const tbody = document.createElement('tbody');
for (let r = 0; r < model.rows; r++) {
const tr = document.createElement('tr');
const rh = document.createElement('th');
rh.textContent = String(r + 1);
rh.className = 'row-header';
rh.style.cssText = `width:${config_1.HEADER_W}px;min-width:${config_1.HEADER_W}px;height:${config_1.CELL_H}px;background:${config_1.COLOR_MENU_BG};text-align:center;font-weight:600;color:#333;border-bottom:1px solid ${config_1.COLOR_GRIDLINE};border-right:2px solid ${config_1.COLOR_GRIDLINE};`;
rh.dataset.row = String(r);
app.rowHeaders.set(r, rh);
rh.addEventListener('contextmenu', (e) => { e.preventDefault(); showHeaderMenu(e, r, 'row'); });
tr.appendChild(rh);
for (let c = 0; c < model.cols; c++) {
const td = document.createElement('td');
td.style.cssText = `width:${config_1.CELL_W}px;height:${config_1.CELL_H}px;border-left:1px solid ${config_1.COLOR_GRIDLINE};border-top:1px solid ${config_1.COLOR_GRIDLINE};padding:2px 4px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;cursor:cell;box-sizing:border-box;background:#fff;`;
td.dataset.col = String(c);
td.dataset.row = String(r);
const cellKey = (0, model_1.key)(c, r);
app.cells.set(cellKey, td);
td.addEventListener('mousedown', (e) => {
if (e.button === 0) {
if (app.editing)
commitEdit(app);
selectCell(app, c, r);
}
});
td.addEventListener('dblclick', () => { if (!app.editing)
startEdit(app); });
tr.appendChild(td);
}
tbody.appendChild(tr);
}
table.appendChild(tbody);
gridEl.appendChild(table);
}
// ─── Render ───────────────────────────────────────────────────────────
function renderCellDisplay(app, col, row) {
const td = app.cells.get((0, model_1.key)(col, row));
if (!td)
return;
const cell = app.model.cells.get((0, model_1.key)(col, row));
if (!cell || cell.raw === '') {
td.textContent = '';
td.style.color = '';
td.style.textAlign = '';
return;
}
if (cell.error) {
td.textContent = cell.error;
td.style.color = config_1.COLOR_ERROR;
td.style.textAlign = 'left';
return;
}
const val = cell.value;
td.textContent = val === undefined || val === null ? '' : String(val);
td.style.color = config_1.COLOR_TEXT;
td.style.textAlign = typeof val === 'number' ? 'right' : 'left';
}
function renderAll(app) {
const { model } = app;
for (const cellKey of model.cells.keys()) {
const parts = cellKey.split(',');
renderCellDisplay(app, parseInt(parts[0], 10), parseInt(parts[1], 10));
}
for (const [cellKey, td] of app.cells) {
if (!model.cells.has(cellKey)) {
td.textContent = '';
td.style.color = '';
td.style.textAlign = '';
}
}
}
// ─── Selection ────────────────────────────────────────────────────────
function selectCell(app, col, row) {
const prevTd = app.cells.get((0, model_1.key)(app.selCol, app.selRow));
if (prevTd) {
prevTd.style.outline = '';
}
app.selCol = col;
app.selRow = row;
const curTd = app.cells.get((0, model_1.key)(col, row));
if (curTd) {
curTd.style.outline = `2px solid ${config_1.COLOR_SELECTION}`;
curTd.style.outlineOffset = '-2px';
curTd.scrollIntoView({ block: 'nearest', inline: 'nearest' });
}
const cell = app.model.cells.get((0, model_1.key)(col, row));
app.fbarInput.value = cell ? cell.raw : '';
app.fbarRef.textContent = (0, model_1.addrToLabel)({ col, row });
for (const [r, rh] of app.rowHeaders)
rh.style.background = r === row ? config_1.COLOR_SELECTION : '';
for (const [c, ch] of app.colHeaders)
ch.style.background = c === col ? config_1.COLOR_SELECTION : '';
// Selection headers: text white when selected
const selRh = app.rowHeaders.get(row);
if (selRh)
selRh.style.color = '#fff';
const selCh = app.colHeaders.get(col);
if (selCh)
selCh.style.color = '#fff';
for (const [r, rh] of app.rowHeaders)
if (r !== row)
rh.style.color = '#333';
for (const [c, ch] of app.colHeaders)
if (c !== col)
ch.style.color = '#333';
}
// ─── Editing ──────────────────────────────────────────────────────────
function startEdit(app) {
const { selCol, selRow } = app;
const cellKey = (0, model_1.key)(selCol, selRow);
const cell = app.model.cells.get(cellKey);
const td = app.cells.get(cellKey);
if (!td)
return;
app.editing = true;
app.editValue = cell ? cell.raw : '';
app.fbarInput.value = app.editValue;
td.innerHTML = '';
const input = document.createElement('input');
input.type = 'text';
input.value = app.editValue;
input.style.cssText = `width:100%;height:100%;border:none;outline:none;padding:2px 4px;font:inherit;font-family:monospace;font-size:13px;background:#fff;box-sizing:border-box;`;
td.appendChild(input);
input.focus();
input.select();
input.addEventListener('input', () => { app.fbarInput.value = input.value; app.editValue = input.value; });
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
commitEdit(app);
selectCell(app, selCol, Math.min(selRow + 1, app.model.rows - 1));
}
else if (e.key === 'Tab') {
e.preventDefault();
commitEdit(app);
selectCell(app, Math.min(selCol + 1, app.model.cols - 1), selRow);
}
else if (e.key === 'Escape') {
cancelEdit(app);
}
});
}
function commitEdit(app) {
if (!app.editing)
return;
const { selCol, selRow } = app;
const raw = app.editValue;
const cellKey = (0, model_1.key)(selCol, selRow);
const td = app.cells.get(cellKey);
if (td) {
const inp = td.querySelector('input');
if (inp)
inp.remove();
}
app.editing = false;
if (raw === '')
(0, model_1.clearCell)(app.model, selCol, selRow);
else
(0, model_1.setCell)(app.model, selCol, selRow, raw);
const graph = (0, engine_1.rebuildGraph)(app.model);
(0, engine_1.recalcFrom)(app.model, graph, cellKey);
renderAll(app);
(0, model_1.saveModel)(app.model);
const cell = app.model.cells.get(cellKey);
app.fbarInput.value = cell ? cell.raw : '';
}
function cancelEdit(app) {
if (!app.editing)
return;
const { selCol, selRow } = app;
const td = app.cells.get((0, model_1.key)(selCol, selRow));
if (td) {
const inp = td.querySelector('input');
if (inp)
inp.remove();
}
app.editing = false;
renderCellDisplay(app, selCol, selRow);
const cell = app.model.cells.get((0, model_1.key)(selCol, selRow));
app.fbarInput.value = cell ? cell.raw : '';
}
// ─── Context Menu ─────────────────────────────────────────────────────
let menuTimer = null;
let activeMenu = null;
function showHeaderMenu(e, index, type) {
removeMenu();
const app = window.__spreadsheetApp;
const menu = document.createElement('div');
menu.style.cssText = `position:fixed;left:${e.clientX}px;top:${e.clientY}px;background:${config_1.COLOR_MENU_BG};border:1px solid #dadce0;border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,.2);z-index:1000;padding:4px 0;min-width:180px;`;
const labels = type === 'row'
? [`Insert row above ${index + 1}`, `Delete row ${index + 1}`]
: [`Insert col left of ${(0, model_1.colToLetter)(index)}`, `Delete col ${(0, model_1.colToLetter)(index)}`];
for (const label of labels) {
const item = document.createElement('div');
item.textContent = label;
item.style.cssText = `padding:6px 16px;cursor:pointer;font-size:13px;color:${config_1.COLOR_TEXT};font-family:inherit;`;
item.addEventListener('mouseenter', () => item.style.background = config_1.COLOR_MENU_HOVER);
item.addEventListener('mouseleave', () => item.style.background = '');
item.addEventListener('click', () => {
if (label.startsWith('Insert')) {
if (type === 'row')
(0, model_1.insertRow)(app.model, index);
else
(0, model_1.insertCol)(app.model, index);
}
else {
if (type === 'row')
(0, model_1.deleteRow)(app.model, index);
else
(0, model_1.deleteCol)(app.model, index);
}
buildGrid(app);
const graph = (0, engine_1.rebuildGraph)(app.model);
(0, engine_1.recalcAll)(app.model, graph);
renderAll(app);
selectCell(app, app.selCol, app.selRow);
(0, model_1.saveModel)(app.model);
removeMenu();
});
menu.appendChild(item);
}
document.body.appendChild(menu);
activeMenu = menu;
menuTimer = setTimeout(() => removeMenu(), 3000);
}
function removeMenu() {
if (menuTimer) {
clearTimeout(menuTimer);
menuTimer = null;
}
if (activeMenu) {
activeMenu.remove();
activeMenu = null;
}
}
// Click outside to close menu
document.addEventListener('click', () => removeMenu());
// ─── Keyboard Handler ─────────────────────────────────────────────────
function setupKeyboard(app) {
document.addEventListener('keydown', (e) => {
if (app.editing)
return;
const { selCol, selRow } = app;
switch (e.key) {
case 'ArrowUp':
e.preventDefault();
selectCell(app, selCol, Math.max(0, selRow - 1));
break;
case 'ArrowDown':
e.preventDefault();
selectCell(app, selCol, Math.min(app.model.rows - 1, selRow + 1));
break;
case 'ArrowLeft':
e.preventDefault();
selectCell(app, Math.max(0, selCol - 1), selRow);
break;
case 'ArrowRight':
e.preventDefault();
selectCell(app, Math.min(app.model.cols - 1, selCol + 1), selRow);
break;
case 'Tab':
e.preventDefault();
selectCell(app, Math.min(app.model.cols - 1, selCol + 1), selRow);
break;
case 'Enter': {
e.preventDefault();
const cell = app.model.cells.get((0, model_1.key)(app.selCol, app.selRow));
if (cell && cell.raw !== '') {
commitEdit(app);
selectCell(app, selCol, Math.min(selRow + 1, app.model.rows - 1));
}
else {
startEdit(app);
}
break;
}
case 'Delete':
case 'Backspace':
e.preventDefault();
(0, model_1.clearCell)(app.model, selCol, selRow);
const g1 = (0, engine_1.rebuildGraph)(app.model);
(0, engine_1.recalcFrom)(app.model, g1, (0, model_1.key)(selCol, selRow));
renderAll(app);
app.fbarInput.value = '';
(0, model_1.saveModel)(app.model);
break;
default:
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
app.editValue = e.key;
startEdit(app);
const td = app.cells.get((0, model_1.key)(selCol, selRow));
if (td) {
const inp = td.querySelector('input');
if (inp) {
inp.value = e.key;
app.fbarInput.value = e.key;
}
}
}
break;
}
});
}
// ─── Formula Bar ──────────────────────────────────────────────────────
function setupFormulaBar(app) {
const input = app.fbarInput;
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
app.editValue = input.value;
const { selCol, selRow } = app;
const cellKey = (0, model_1.key)(selCol, selRow);
if (input.value === '')
(0, model_1.clearCell)(app.model, selCol, selRow);
else
(0, model_1.setCell)(app.model, selCol, selRow, input.value);
app.editing = false;
const graph = (0, engine_1.rebuildGraph)(app.model);
(0, engine_1.recalcFrom)(app.model, graph, cellKey);
renderAll(app);
(0, model_1.saveModel)(app.model);
}
else if (e.key === 'Escape') {
const cell = app.model.cells.get((0, model_1.key)(app.selCol, app.selRow));
input.value = cell ? cell.raw : '';
}
});
input.addEventListener('focus', () => {
if (!app.editing) {
const cell = app.model.cells.get((0, model_1.key)(app.selCol, app.selRow));
input.value = cell ? cell.raw : '';
}
});
input.addEventListener('input', () => {
app.editValue = input.value;
if (app.editing) {
const td = app.cells.get((0, model_1.key)(app.selCol, app.selRow));
if (td) {
const ci = td.querySelector('input');
if (ci)
ci.value = input.value;
}
}
});
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.6-27B-MTP-GGUF","provider":"brigid","persona":"typescript","sessionId":"60f28adf-ced3-497f-a00b-5e36c3daba0c","tokensIn":5402740,"tokensOut":75106,"tokensTotal":5477846,"cost":0,"turns":95,"toolCalls":99,"failedToolCalls":1,"timestamp":"2026-08-02T04:31:45.162Z"} -->