Spreadsheet
Deepseek v4 Flash 0731 · 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 48,144 bytes · SHA-256 5a2817169e0e
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sheets — Spreadsheet</title>
<style>
/* ── css: layout ── */
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 13px; color: #202124; background: #fff;
}
#app { display: flex; flex-direction: column; height: 100vh; }
#toolbar {
display: flex; align-items: center; gap: 14px;
padding: 6px 10px; background: #f8f9fa; border-bottom: 1px solid #dadce0;
flex-wrap: wrap; flex: 0 0 auto;
}
.tb-group { display: flex; align-items: center; gap: 4px; }
#toolbar button {
font-size: 12.5px; padding: 5px 10px; border: 1px solid #dadce0; border-radius: 4px;
background: #fff; color: #202124; cursor: pointer; line-height: 1;
}
#toolbar button:hover { background: #f1f3f4; }
#toolbar button:active { background: #e8eaed; }
.tb-hint { color: #5f6368; font-size: 12px; }
.tb-hint code { background: #e8eaed; border-radius: 3px; padding: 0 4px; font-size: 11.5px; }
#fbar {
display: flex; align-items: center; flex: 0 0 auto;
border-bottom: 1px solid #dadce0; background: #fff;
height: 30px;
}
#cell-ref {
min-width: 64px; padding: 0 8px; text-align: center;
font-size: 13px; color: #202124; border-right: 1px solid #e1e1e1;
font-weight: 500; user-select: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
#fx {
padding: 0 7px; color: #5f6368; font-style: italic; font-size: 13px;
border-right: 1px solid #e1e1e1; user-select: none;
}
#formula-input {
flex: 1; border: none; outline: none; height: 100%; padding: 0 8px;
font-size: 13px; color: #202124;
}
#formula-input.editing { background: #fff; }
#grid-wrap {
flex: 1 1 auto; overflow: auto; position: relative;
scrollbar-width: thin; scrollbar-color: #c1c1c1 transparent;
}
#grid { border-collapse: collapse; border-spacing: 0; table-layout: fixed; }
/* ── css: cells ── */
#grid th, #grid td {
border-right: 1px solid #e1e1e1; border-bottom: 1px solid #e1e1e1;
padding: 0; height: 24px; white-space: nowrap; overflow: hidden;
}
#grid tbody td.cell { position: relative; cursor: cell; text-align: left; background: #fff; }
#grid tbody td.cell.num { text-align: right; }
#grid tbody td.cell.err { color: #d93025; }
#grid tbody td.cell.selected { box-shadow: inset 0 0 0 2px #1a73e8; z-index: 1; }
#grid tbody td.cell.editing { box-shadow: inset 0 0 0 2px #1a73e8; background: #fff; }
#grid tbody td.cell.editing input.ov {
position: absolute; left: 0; top: 0; width: 100%; height: 100%;
border: none; outline: none; padding: 0 5px; margin: 0;
font: inherit; background: #fff; color: #202124;
}
#grid tbody td.cell input.ov.err { color: #d93025; }
#grid thead th {
background: #f8f9fa; color: #5f6368; font-weight: 500; font-size: 11px;
text-align: center; cursor: pointer; user-select: none; position: sticky; top: 0; z-index: 3;
}
#grid thead th:hover { background: #e8f0fe; color: #1a73e8; }
#grid tbody tr td.rowhead {
background: #f8f9fa; color: #5f6368; font-size: 11px; text-align: center;
user-select: none; cursor: pointer; position: sticky; left: 0; z-index: 2; width: 46px;
}
#grid .corner {
background: #f8f9fa; position: sticky; top: 0; left: 0; z-index: 4; width: 46px; cursor: default;
}
#grid tbody td.cell .txt { padding: 0 5px; line-height: 24px; overflow: hidden; text-overflow: ellipsis; white-space: pre; }
#grid tbody td.rowhead.hl { background: #e8f0fe; color: #1a73e8; }
#grid thead th.hl { background: #e8f0fe; color: #1a73e8; }
#grid-wrap:focus { outline: none; }
.menu {
position: fixed; background: #fff; border: 1px solid #dadce0; border-radius: 6px;
box-shadow: 0 4px 16px rgba(0,0,0,.18); z-index: 100; min-width: 180px; padding: 4px 0;
}
.menu .mi { padding: 6px 14px; cursor: pointer; font-size: 13px; }
.menu .mi:hover { background: #f1f3f4; }
</style>
</head>
<body>
<div id="app">
<div id="toolbar">
<div class="tb-group" id="tb-file">
<button id="btn-insert-row" title="Insert row above the selected one">Insert row</button>
<button id="btn-delete-row" title="Delete the selected row">Delete row</button>
<button id="btn-insert-col" title="Insert column left of the selected one">Insert column</button>
<button id="btn-delete-col" title="Delete the selected column">Delete column</button>
</div>
<div class="tb-group" id="tb-help">
<span class="tb-hint">Formulas: <code>=SUM(A1:A5, 10, B2)</code> · functions: SUM, AVG/AVERAGE, MIN, MAX, COUNT · right-click a header for row/column ops</span>
</div>
</div>
<div id="fbar">
<span id="cell-ref" title="Selected cell">A1</span>
<span id="fx" title="Formula">fx</span>
<input id="formula-input" spellcheck="false" autocomplete="off" placeholder="= expression">
</div>
<div id="grid-wrap">
<table id="grid" cellspacing="0" cellpadding="0"></table>
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./render":"src/render.ts"},"src/parser.ts":{"./consts":"src/consts.ts"},"src/model.ts":{"./parser":"src/parser.ts","./consts":"src/consts.ts"},"src/render.ts":{"./consts":"src/consts.ts","./parser":"src/parser.ts","./model":"src/model.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";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
// Entry point: boot the spreadsheet.
const render = __importStar(require("./render"));
function start() {
render.init();
}
start();
};
// ── module: src/consts.ts ──
__mods["src/consts.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ERR = exports.LAST_COL = exports.FIRST_COL = exports.SAVE_KEY = exports.ROW_HEIGHT = exports.COL_WIDTH = exports.GRID_ROWS = exports.GRID_COLS = void 0;
exports.colLetter = colLetter;
// =============================================================
// Tunable constants
// =============================================================
exports.GRID_COLS = 26; // A..Z
exports.GRID_ROWS = 100; // rows 1..100
exports.COL_WIDTH = 90; // px
exports.ROW_HEIGHT = 24; // px
exports.SAVE_KEY = "sheets.data.v1";
exports.FIRST_COL = "A";
exports.LAST_COL = String.fromCharCode(65 + exports.GRID_COLS - 1); // Z
exports.ERR = Object.freeze({
REF: "#REF!",
DIV0: "#DIV/0!",
PARSE: "#ERR!",
CYCLE: "#CYCLE!",
});
function colLetter(i) {
return String.fromCharCode(65 + i);
}
};
// ── module: src/parser.ts ──
__mods["src/parser.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.isCellRef = isCellRef;
exports.refToRC = refToRC;
exports.rcToRef = rcToRef;
exports.isLiveRef = isLiveRef;
exports.isDeadRef = isDeadRef;
exports.parseFormula = parseFormula;
exports.collectRefs = collectRefs;
exports.astToString = astToString;
// =============================================================
// Formula parser: tokenizer + recursive-descent. No eval().
// =============================================================
const consts_1 = require("./consts");
// ---- cell reference helpers ----
function isCellRef(s) {
return /^[A-Za-z]\d+$/.test(s);
}
// Return {col,row} or null if invalid. col in 0..25, row in 1..GRID_ROWS.
function refToRC(ref) {
const m = /^([A-Za-z])(\d+)$/.exec(ref);
if (!m)
return null;
const col = m[1].toUpperCase().charCodeAt(0) - 65;
const row = parseInt(m[2], 10);
if (col < 0 || col > 25)
return null;
return { col, row };
}
function rcToRef(col, row) {
if (col < 0 || col > 25)
return (0, consts_1.colLetter)(0) + "0";
return (0, consts_1.colLetter)(col) + row;
}
// Is this ref a live, in-bounds cell?
function isLiveRef(ref) {
const rc = refToRC(ref);
return rc !== null && rc.row >= 1 && rc.row <= consts_1.GRID_ROWS;
}
// Row 0 is the reserved "dead" ref that evaluates to #REF!.
function isDeadRef(ref) {
const rc = refToRC(ref);
return rc !== null && rc.row === 0;
}
function tokenize(src) {
const toks = [];
let i = 0;
while (i < src.length) {
const c = src[i];
if (c === " " || c === "\t") {
i++;
continue;
}
if (c >= "0" && c <= "9") {
let j = i;
while (j < src.length && src[j] >= "0" && src[j] <= "9")
j++;
if (j < src.length && src[j] === ".") {
j++;
while (j < src.length && src[j] >= "0" && src[j] <= "9")
j++;
}
const num = parseFloat(src.slice(i, j));
if (isNaN(num))
return null;
toks.push({ t: "num", v: num });
i = j;
continue;
}
if (/[A-Za-z]/.test(c)) {
let j = i;
while (j < src.length && /[A-Za-z0-9]/.test(src[j]))
j++;
toks.push({ t: "id", v: src.slice(i, j) });
i = j;
continue;
}
if (c === "(") {
toks.push({ t: "lp" });
i++;
continue;
}
if (c === ")") {
toks.push({ t: "rp" });
i++;
continue;
}
if (c === ",") {
toks.push({ t: "comma" });
i++;
continue;
}
if (c === ":") {
toks.push({ t: "colon" });
i++;
continue;
}
if (c === "+" || c === "-" || c === "*" || c === "/") {
toks.push({ t: "op", v: c });
i++;
continue;
}
return null; // invalid char
}
toks.push({ t: "eof" });
return toks;
}
// ---- recursive descent ----
class Parser {
constructor(toks) {
this.pos = 0;
this.toks = toks;
}
peek() { return this.toks[this.pos]; }
next() { return this.toks[this.pos++]; }
atEnd() { return this.peek().t === "eof"; }
match(t) {
if (this.peek().t === t) {
this.pos++;
return true;
}
return false;
}
opVal() {
const t = this.peek();
return t.t === "op" ? t.v : "";
}
consumeOp() {
const t = this.next();
return t.v;
}
consumeId() {
const t = this.next();
return t.v;
}
parse() {
const n = this.expr();
if (!n)
return null;
if (!this.atEnd())
return null;
return n;
}
expr() {
let left = this.term();
if (!left)
return null;
while (this.peek().t === "op" && (this.opVal() === "+" || this.opVal() === "-")) {
const op = this.consumeOp();
const right = this.term();
if (!right)
return null;
left = { type: "bin", op, left, right };
}
return left;
}
term() {
let left = this.factor();
if (!left)
return null;
while (this.peek().t === "op" && (this.opVal() === "*" || this.opVal() === "/")) {
const op = this.consumeOp();
const right = this.factor();
if (!right)
return null;
left = { type: "bin", op, left, right };
}
return left;
}
factor() {
if (this.peek().t === "op" && (this.opVal() === "-" || this.opVal() === "+")) {
const op = this.consumeOp();
const operand = this.factor();
if (!operand)
return null;
return { type: "un", op, operand };
}
return this.primary();
}
primary() {
const tk = this.peek();
if (tk.t === "num") {
this.next();
return { type: "num", value: tk.v };
}
if (tk.t === "id") {
this.next(); // consume the id token
const name = (tk.v);
if (this.peek().t === "lp") {
this.next();
const args = [];
if (this.peek().t !== "rp") {
const first = this.expr();
if (!first)
return null;
args.push(first);
while (this.match("comma")) {
const a = this.expr();
if (!a)
return null;
args.push(a);
}
}
if (!this.match("rp"))
return null;
return { type: "func", name: name.toUpperCase(), args };
}
else {
if (!isCellRef(name))
return null;
const from = name.toUpperCase();
if (this.peek().t === "colon") {
this.next();
const nxt = this.next();
if (nxt.t !== "id")
return null;
const idTok = nxt;
if (!isCellRef(idTok.v))
return null;
const to = idTok.v.toUpperCase();
const a = refToRC(from);
const b = refToRC(to);
if (!a || !b)
return { type: "range", from, to };
const c1 = Math.min(a.col, b.col), r1 = Math.min(a.row, b.row);
const c2 = Math.max(a.col, b.col), r2 = Math.max(a.row, b.row);
return { type: "range", from: rcToRef(c1, r1), to: rcToRef(c2, r2) };
}
return { type: "cell", ref: from };
}
}
if (tk.t === "lp") {
this.next();
const inner = this.expr();
if (!inner)
return null;
if (!this.match("rp"))
return null;
return inner;
}
return null;
}
}
// Parse a full formula body (without the leading '='). Returns AST or null on failure.
function parseFormula(body) {
if (body.trim() === "")
return null;
const toks = tokenize(body);
if (!toks)
return null;
const p = new Parser(toks);
return p.parse();
}
// ---- collect every cell ref referenced by an AST (for dependency graph) ----
function collectRefs(node, out) {
switch (node.type) {
case "cell":
out.add(node.ref);
break;
case "range":
out.add(node.from);
out.add(node.to);
break;
case "func":
for (const a of node.args)
collectRefs(a, out);
break;
case "bin":
collectRefs(node.left, out);
collectRefs(node.right, out);
break;
case "un":
collectRefs(node.operand, out);
break;
case "num":
break;
}
}
// ---- print an AST back to a formula string (with safe parens) ----
function astToString(node) {
switch (node.type) {
case "num": return String(node.value);
case "cell": return node.ref;
case "range": return node.from + ":" + node.to;
case "func": return node.name + "(" + node.args.map(astToString).join(",") + ")";
case "bin": return "(" + astToString(node.left) + node.op + astToString(node.right) + ")";
case "un": return node.op + "(" + astToString(node.operand) + ")";
}
}
};
// ── module: src/model.ts ──
__mods["src/model.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.inputs = void 0;
exports.setCell = setCell;
exports.clearCell = clearCell;
exports.rawOf = rawOf;
exports.getCellDisplay = getCellDisplay;
exports.deleteRow = deleteRow;
exports.insertRow = insertRow;
exports.deleteCol = deleteCol;
exports.insertCol = insertCol;
exports.serialize = serialize;
exports.load = load;
exports.loadFromStorage = loadFromStorage;
exports.saveToStorage = saveToStorage;
exports.testDisplay = testDisplay;
// =============================================================
// Data model: raw inputs, dependency graph, recalculation,
// cycle detection, structural ops (insert/delete) and
// serialization. No DOM code here.
// =============================================================
const parser_1 = require("./parser");
const consts_1 = require("./consts");
// ---- raw inputs: cellId ("A1") -> raw string the user typed ----
exports.inputs = {};
// formula AST cache
const astCache = new Map();
// refs[c] = cell ids referenced by c's formula
const refs = new Map();
// dependents[c] = set of cells whose formulas reference c
const dependents = new Map();
// computed[c] = cached computed value for formula cells
const computed = new Map();
// ---- literals ----
function literalValue(raw) {
const t = raw.trim();
if (/^-?\d+(\.\d+)?$/.test(t))
return { kind: "num", num: parseFloat(t) };
return { kind: "text", text: raw };
}
function fmtNum(n) {
if (!isFinite(n))
return "∞";
const r = parseFloat(n.toPrecision(12));
return String(r);
}
// valueOf: current cell value used during evaluation. null => empty cell.
function valueOf(cell) {
const raw = exports.inputs[cell];
if (raw === undefined)
return null;
if (raw.startsWith("=")) {
return computed.get(cell) ?? null;
}
return literalValue(raw);
}
// ---- evaluation (walks AST; refs read through valueOf) ----
function numOf(v) {
return v.kind === "num" ? v.num : NaN;
}
function collect(node) {
if (node.type === "range") {
const arr = [];
const a = (0, parser_1.refToRC)(node.from);
const b = (0, parser_1.refToRC)(node.to);
if (a && b && b.row >= 1) {
const r0 = Math.max(a.row, 1);
const c0 = Math.max(a.col, 0);
for (let r = r0; r <= b.row; r++) {
for (let c = c0; c <= b.col; c++) {
const v = valueOf((0, parser_1.rcToRef)(c, r));
if (v)
arr.push(v);
}
}
}
return arr;
}
const v = evalNode(node);
return v === null ? [] : [v];
}
function applyFunc(name, args) {
const flat = [];
for (const a of args)
flat.push(...collect(a));
for (const v of flat)
if (v.kind === "err")
return v;
const nums = flat.filter((v) => v.kind === "num").map(numOf);
const sum = nums.reduce((x, y) => x + y, 0);
switch (name) {
case "SUM": return { kind: "num", num: sum };
case "AVG":
case "AVERAGE": return { kind: "num", num: nums.length ? sum / nums.length : 0 };
case "MIN": return { kind: "num", num: nums.length ? Math.min(...nums) : 0 };
case "MAX": return { kind: "num", num: nums.length ? Math.max(...nums) : 0 };
case "COUNT": return { kind: "num", num: nums.length };
default: return { kind: "err", code: consts_1.ERR.PARSE };
}
}
function evalNode(node) {
switch (node.type) {
case "num":
return { kind: "num", num: node.value };
case "cell": {
if (!(0, parser_1.isLiveRef)(node.ref))
return { kind: "err", code: consts_1.ERR.REF };
const v = valueOf(node.ref);
return v ?? { kind: "num", num: 0 };
}
case "range":
return { kind: "err", code: consts_1.ERR.PARSE };
case "func":
return applyFunc(node.name, node.args);
case "un": {
const v = evalNode(node.operand);
if (v === null)
return { kind: "num", num: 0 };
if (v.kind === "err")
return v;
if (v.kind === "text")
return { kind: "err", code: consts_1.ERR.PARSE };
return { kind: "num", num: node.op === "-" ? -v.num : v.num };
}
case "bin": {
const l = evalNode(node.left) ?? { kind: "num", num: 0 };
const r = evalNode(node.right) ?? { kind: "num", num: 0 };
if (l.kind === "err")
return l;
if (r.kind === "err")
return r;
const a = numOf(l), b = numOf(r);
if (isNaN(a) || isNaN(b))
return { kind: "err", code: consts_1.ERR.PARSE };
let res;
switch (node.op) {
case "+":
res = a + b;
break;
case "-":
res = a - b;
break;
case "*":
res = a * b;
break;
case "/":
if (b === 0)
return { kind: "err", code: consts_1.ERR.DIV0 };
res = a / b;
break;
default: return { kind: "err", code: consts_1.ERR.PARSE };
}
return { kind: "num", num: res };
}
}
}
function computeCell(cell) {
const raw = exports.inputs[cell];
if (raw === undefined)
return { kind: "num", num: 0 };
if (!raw.startsWith("="))
return literalValue(raw);
let ast = astCache.get(cell);
if (!ast) {
ast = (0, parser_1.parseFormula)(raw.slice(1));
astCache.set(cell, ast);
}
if (!ast)
return { kind: "err", code: consts_1.ERR.PARSE };
const v = evalNode(ast);
return v ?? { kind: "num", num: 0 };
}
// ---- recalculation ----
function recompute(roots) {
// collect affected cells: roots + all transitive dependents
const affected = new Set();
const stack = [];
for (const r of roots) {
if (!affected.has(r)) {
affected.add(r);
stack.push(r);
}
}
while (stack.length) {
const c = stack.pop();
const d = dependents.get(c);
if (d)
for (const x of d) {
if (!affected.has(x)) {
affected.add(x);
stack.push(x);
}
}
}
// topological sort over refs (restricted to affected) + cycle detection
const color = new Map(); // 0 none, 1 visiting, 2 done
const inCycle = new Set();
const order = [];
const pathStack = [];
const pathIdx = new Map();
const dfs = (c) => {
color.set(c, 1);
pathIdx.set(c, pathStack.length);
pathStack.push(c);
const rs = refs.get(c);
if (rs) {
for (const r of rs) {
if (!affected.has(r))
continue;
const rc = color.get(r);
if (rc === 1) {
const start = pathIdx.get(r);
for (let i = start; i < pathStack.length; i++)
inCycle.add(pathStack[i]);
}
else if (rc === undefined) {
dfs(r);
}
}
}
pathStack.pop();
pathIdx.delete(c);
color.set(c, 2);
order.push(c);
};
for (const c of affected) {
if (color.get(c) === undefined)
dfs(c);
}
// evaluate in topological order
for (const c of order) {
if (inCycle.has(c)) {
computed.set(c, { kind: "err", code: consts_1.ERR.CYCLE });
}
else {
computed.set(c, computeCell(c));
}
}
return affected;
}
function recomputeAll() {
recompute(Object.keys(exports.inputs));
}
// ---- graph rebuild helpers ----
function resetGraph() {
astCache.clear();
refs.clear();
dependents.clear();
computed.clear();
}
function registerFormula(cell, raw) {
const ast = (0, parser_1.parseFormula)(raw.slice(1));
astCache.set(cell, ast);
const s = new Set();
if (ast)
(0, parser_1.collectRefs)(ast, s);
const list = [...s];
refs.set(cell, list);
for (const r of list) {
if (!dependents.has(r))
dependents.set(r, new Set());
dependents.get(r).add(cell);
}
}
function registerAll() {
for (const cell in exports.inputs) {
const raw = exports.inputs[cell];
if (raw.startsWith("="))
registerFormula(cell, raw);
else {
astCache.set(cell, null);
refs.set(cell, []);
}
}
}
// ---- public API ----
// Set a cell's raw content. Returns affected cell ids (for rendering).
function setCell(cell, raw) {
// unregister old refs
const oldRefs = refs.get(cell) || [];
for (const r of oldRefs)
dependents.get(r)?.delete(cell);
if (raw === "") {
delete exports.inputs[cell];
astCache.delete(cell);
refs.delete(cell);
computed.delete(cell);
return recompute([cell]);
}
exports.inputs[cell] = raw;
if (raw.startsWith("=")) {
registerFormula(cell, raw);
}
else {
astCache.set(cell, null);
refs.set(cell, []);
}
return recompute([cell]);
}
function clearCell(cell) {
return setCell(cell, "");
}
function rawOf(cell) {
return exports.inputs[cell];
}
function getCellDisplay(cell) {
const raw = exports.inputs[cell];
if (raw === undefined)
return { text: "", align: "left", isError: false };
if (raw.startsWith("=")) {
const cv = computed.get(cell);
if (cv?.kind === "err")
return { text: cv.code, align: "left", isError: true };
if (cv?.kind === "num")
return { text: fmtNum(cv.num), align: "right", isError: false };
if (cv?.kind === "text")
return { text: cv.text, align: "left", isError: false };
return { text: "", align: "left", isError: false };
}
const lit = literalValue(raw);
if (lit.kind === "num")
return { text: fmtNum(lit.num), align: "right", isError: false };
return { text: raw, align: "left", isError: false };
}
function transformRef(ref, colMap, rowMap) {
const rc = (0, parser_1.refToRC)(ref);
if (!rc)
return ref;
const nc = colMap(rc.col);
const nr = rowMap(rc.row);
if (nc === null || nr === null || nc < 0 || nc > 25 || nr < 1)
return (0, parser_1.rcToRef)(0, 0); // dead -> A0 -> #REF!
return (0, parser_1.rcToRef)(nc, nr);
}
function transformNode(node, colMap, rowMap) {
switch (node.type) {
case "cell":
return { type: "cell", ref: transformRef(node.ref, colMap, rowMap) };
case "range":
return {
type: "range",
from: transformRef(node.from, colMap, rowMap),
to: transformRef(node.to, colMap, rowMap),
};
case "func":
return { type: "func", name: node.name, args: node.args.map((a) => transformNode(a, colMap, rowMap)) };
case "bin":
return {
type: "bin", op: node.op,
left: transformNode(node.left, colMap, rowMap),
right: transformNode(node.right, colMap, rowMap),
};
case "un":
return { type: "un", op: node.op, operand: transformNode(node.operand, colMap, rowMap) };
case "num":
return node;
}
}
// Build the moved raw-input table after a structural op.
function moveData(colMap, rowMap) {
const ni = {};
for (const cell in exports.inputs) {
const raw = exports.inputs[cell];
const rc = (0, parser_1.refToRC)(cell);
if (!rc)
continue;
const nc = colMap(rc.col);
const nr = rowMap(rc.row);
if (nc === null || nr === null || nc < 0 || nc > 25 || nr < 1)
continue; // deleted
const nid = (0, parser_1.rcToRef)(nc, nr);
let newRaw = raw;
if (raw.startsWith("=")) {
const ast = astCache.get(cell);
if (ast)
newRaw = "=" + (0, parser_1.astToString)(transformNode(ast, colMap, rowMap));
}
ni[nid] = newRaw;
}
return ni;
}
function applyStructure(colMap, rowMap) {
exports.inputs = moveData(colMap, rowMap);
resetGraph();
registerAll();
recomputeAll();
}
const idMap = (i) => i;
function deleteRow(R) {
const rowMap = (r) => (r < R ? r : r === R ? null : r - 1);
applyStructure(idMap, rowMap);
}
function insertRow(R) {
const rowMap = (r) => (r >= R ? r + 1 : r);
applyStructure(idMap, rowMap);
}
function deleteCol(C) {
const colMap = (c) => (c < C ? c : c === C ? null : c - 1);
applyStructure(colMap, idMap);
}
function insertCol(C) {
const colMap = (c) => (c >= C ? c + 1 : c);
applyStructure(colMap, idMap);
}
// ---- serialization to localStorage (raw formulas, not values) ----
function serialize() {
const cells = Object.keys(exports.inputs)
.sort((a, b) => (0, parser_1.refToRC)(a).row - (0, parser_1.refToRC)(b).row || (0, parser_1.refToRC)(a).col - (0, parser_1.refToRC)(b).col)
.map((cell) => [cell, exports.inputs[cell]]);
return JSON.stringify({ v: 1, cells });
}
function load(json) {
try {
const data = JSON.parse(json);
if (!data || data.v !== 1 || !Array.isArray(data.cells))
return false;
const ni = {};
for (const entry of data.cells) {
if (!Array.isArray(entry) || entry.length !== 2)
continue;
const [cell, raw] = entry;
if (typeof cell !== "string" || typeof raw !== "string")
continue;
if (!(0, parser_1.isCellRef)(cell))
continue;
ni[cell] = raw;
}
exports.inputs = ni;
resetGraph();
registerAll();
recomputeAll();
return true;
}
catch {
return false;
}
}
function loadFromStorage() {
try {
const s = localStorage.getItem(consts_1.SAVE_KEY);
if (!s)
return false;
return load(s);
}
catch {
return false;
}
}
function saveToStorage() {
try {
localStorage.setItem(consts_1.SAVE_KEY, serialize());
}
catch {
/* storage may be unavailable */
}
}
// return a human key used by tests: computed value text of a cell
function testDisplay(cell) {
return getCellDisplay(cell).text;
}
};
// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.cellEl = cellEl;
exports.getSelected = getSelected;
exports.buildGrid = buildGrid;
exports.refreshCell = refreshCell;
exports.refreshAll = refreshAll;
exports.refreshCellAndDeps = refreshCellAndDeps;
exports.select = select;
exports.commit = commit;
exports.cancelEdit = cancelEdit;
exports.clearSelected = clearSelected;
exports.init = init;
// =============================================================
// Rendering + interaction: DOM grid, selection, editing,
// formula bar, header context menus.
// =============================================================
const consts_1 = require("./consts");
const parser_1 = require("./parser");
const model = __importStar(require("./model"));
let selected = "A1";
let editing = false;
let editText = "";
let overlayInput = null;
let syncing = false;
let menuEl = null;
const fbar = document.getElementById("formula-input");
const cellRefEl = document.getElementById("cell-ref");
const gridWrap = document.getElementById("grid-wrap");
const grid = document.getElementById("grid");
function cellEl(cell) {
return document.getElementById("c-" + cell);
}
function getSelected() {
return selected;
}
// ---- build the grid DOM ---------------------------------------
function buildGrid() {
const colgroup = document.createElement("colgroup");
const none = document.createElement("col");
none.style.width = "46px";
colgroup.appendChild(none);
for (let c = 0; c < consts_1.GRID_COLS; c++) {
const col = document.createElement("col");
col.style.width = consts_1.COL_WIDTH + "px";
colgroup.appendChild(col);
}
grid.appendChild(colgroup);
const thead = document.createElement("thead");
const htr = document.createElement("tr");
const corner = document.createElement("th");
corner.className = "corner";
htr.appendChild(corner);
for (let c = 0; c < consts_1.GRID_COLS; c++) {
const th = document.createElement("th");
th.textContent = (0, consts_1.colLetter)(c);
th.dataset.col = String(c);
th.addEventListener("click", () => { if (editing)
commit(); select("A" + ((0, parser_1.refToRC)(selected).row)); });
th.addEventListener("contextmenu", (e) => headerMenu(e, "col", c));
htr.appendChild(th);
}
thead.appendChild(htr);
grid.appendChild(thead);
const tbody = document.createElement("tbody");
for (let r = 1; r <= consts_1.GRID_ROWS; r++) {
const tr = document.createElement("tr");
const rh = document.createElement("td");
rh.className = "rowhead";
rh.textContent = String(r);
rh.dataset.row = String(r);
rh.addEventListener("click", () => { if (editing)
commit(); select((0, consts_1.colLetter)(0) + r); });
rh.addEventListener("contextmenu", (e) => headerMenu(e, "row", r));
tr.appendChild(rh);
for (let c = 0; c < consts_1.GRID_COLS; c++) {
const id = (0, consts_1.colLetter)(c) + r;
const td = document.createElement("td");
td.className = "cell";
td.id = "c-" + id;
const txt = document.createElement("div");
txt.className = "txt";
td.appendChild(txt);
td.dataset.cell = id;
td.addEventListener("mousedown", (e) => {
if (e.button === 2)
return;
if (editing)
commit();
select(id);
});
td.addEventListener("dblclick", () => { beginEdit("overlay", rawForSel()); });
tr.appendChild(td);
}
tbody.appendChild(tr);
}
grid.appendChild(tbody);
gridWrap.tabIndex = 0;
}
function rawForSel() {
return model.rawOf(selected) ?? "";
}
// ---- display refresh ------------------------------------------
function refreshCell(cell) {
const el = cellEl(cell);
if (!el)
return;
const d = model.getCellDisplay(cell);
const txt = el.querySelector(".txt");
if (txt)
txt.textContent = d.text;
el.classList.toggle("num", d.align === "right");
el.classList.toggle("err", d.isError);
}
function refreshAll() {
for (let r = 1; r <= consts_1.GRID_ROWS; r++) {
for (let c = 0; c < consts_1.GRID_COLS; c++) {
refreshCell((0, consts_1.colLetter)(c) + r);
}
}
updateHeaders();
}
function refreshCellAndDeps(cells) {
for (const id of cells)
refreshCell(id);
}
function updateHeaders() {
const rc = (0, parser_1.refToRC)(selected);
// clear highlights
document.querySelectorAll("th.hl").forEach((el) => el.classList.remove("hl"));
document.querySelectorAll("td.rowhead.hl").forEach((el) => el.classList.remove("hl"));
const cth = document.querySelector(`th[data-col="${rc.col}"]`);
const rth = document.querySelector(`td.rowhead[data-row="${rc.row}"]`);
if (cth)
cth.classList.add("hl");
if (rth)
rth.classList.add("hl");
}
// ---- selection ------------------------------------------------
function select(cell) {
const old = cellEl(selected);
if (old)
old.classList.remove("selected");
selected = cell;
const el = cellEl(cell);
if (el) {
el.classList.add("selected");
el.scrollIntoView({ block: "nearest", inline: "nearest" });
}
cellRefEl.textContent = cell;
updateHeaders();
if (!editing) {
fbar.value = rawForSel();
}
gridWrap.focus();
}
function moveSel(dr, dc) {
const rc = (0, parser_1.refToRC)(selected);
const nr = Math.max(1, Math.min(consts_1.GRID_ROWS, rc.row + dr));
const nc = Math.max(0, Math.min(consts_1.GRID_COLS - 1, rc.col + dc));
select((0, parser_1.rcToRef)(nc, nr));
}
// ---- editing --------------------------------------------------
function beginEdit(focus, initial) {
commit(); // commit any prior edit first
editing = true;
editText = initial;
syncing = true;
fbar.value = editText;
syncing = false;
fbar.classList.add("editing");
const td = cellEl(selected);
if (td) {
td.classList.add("editing");
const txt = td.querySelector(".txt");
if (txt)
txt.style.visibility = "hidden";
overlayInput = document.createElement("input");
overlayInput.className = "ov";
overlayInput.spellcheck = false;
overlayInput.autocomplete = "off";
overlayInput.value = editText;
overlayInput.addEventListener("input", () => {
if (syncing)
return;
syncing = true;
editText = overlayInput.value;
if (fbar.value !== editText)
fbar.value = editText;
syncing = false;
});
td.appendChild(overlayInput);
}
if (focus === "overlay" && overlayInput) {
overlayInput.focus();
try {
overlayInput.setSelectionRange(editText.length, editText.length);
}
catch { /* noop */ }
}
else {
fbar.focus();
}
}
function endEdit() {
editing = false;
fbar.classList.remove("editing");
const td = cellEl(selected);
if (td) {
td.classList.remove("editing");
const txt = td.querySelector(".txt");
if (txt)
txt.style.visibility = "";
if (overlayInput && overlayInput.parentNode === td)
td.removeChild(overlayInput);
}
overlayInput = null;
}
function commit(moveDr = 0, moveDc = 0) {
if (!editing) {
if (moveDr || moveDc)
moveSel(moveDr, moveDc);
return false;
}
const text = editText;
endEdit();
const affected = model.setCell(selected, text);
refreshCell(selected);
refreshCellAndDeps(affected);
if (moveDr || moveDc)
moveSel(moveDr, moveDc);
else
gridWrap.focus();
return true;
}
function cancelEdit() {
if (!editing)
return;
endEdit();
refreshCell(selected);
gridWrap.focus();
}
// ---- clear selected cell --------------------------------------
function clearSelected() {
if (editing) {
endEdit();
}
const affected = model.clearCell(selected);
refreshCell(selected);
refreshCellAndDeps(affected);
gridWrap.focus();
}
// ---- formula bar wiring ---------------------------------------
fbar.addEventListener("focus", () => {
if (!editing)
beginEdit("bar", rawForSel());
});
fbar.addEventListener("input", () => {
if (syncing)
return;
if (!editing)
return;
syncing = true;
editText = fbar.value;
if (overlayInput && overlayInput.value !== editText)
overlayInput.value = editText;
syncing = false;
});
// ---- header context menu --------------------------------------
function headerMenu(e, kind, index) {
e.preventDefault();
const items = [];
if (kind === "row") {
items.push({
label: "Insert row above",
action: () => { endEdit(); model.insertRow(index); afterStructure(); select((0, parser_1.rcToRef)(0, index)); },
});
items.push({
label: "Delete row " + index,
action: () => { endEdit(); model.deleteRow(index); afterStructure(); },
});
}
else {
items.push({
label: "Insert column left",
action: () => { endEdit(); model.insertCol(index); afterStructure(); select((0, parser_1.rcToRef)(index, 1)); },
});
items.push({
label: "Delete column " + (0, consts_1.colLetter)(index),
action: () => { endEdit(); model.deleteCol(index); afterStructure(); },
});
}
openMenu(e.clientX, e.clientY, items);
}
function afterStructure() {
refreshAll();
updateHeaders();
gridWrap.focus();
// selection cell may have moved; re-highlight selected
select(selected);
}
function openMenu(x, y, items) {
closeMenu();
menuEl = document.createElement("div");
menuEl.className = "menu";
for (const it of items) {
const mi = document.createElement("div");
mi.className = "mi";
mi.textContent = it.label;
mi.addEventListener("click", () => { closeMenu(); it.action(); });
menuEl.appendChild(mi);
}
document.body.appendChild(menuEl);
const r = menuEl.getBoundingClientRect();
menuEl.style.left = Math.min(x, window.innerWidth - r.width - 8) + "px";
menuEl.style.top = Math.min(y, window.innerHeight - r.height - 8) + "px";
}
function closeMenu() {
if (menuEl && menuEl.parentNode)
menuEl.parentNode.removeChild(menuEl);
menuEl = null;
}
// close menu & commit when clicking elsewhere
document.addEventListener("mousedown", (e) => {
if (menuEl && !e.target.closest(".menu"))
closeMenu();
});
// ---- keyboard ---------------------------------------------
function isPrintable(e) {
if (e.ctrlKey || e.metaKey || e.altKey)
return false;
if (e.key.length !== 1)
return false;
return true;
}
document.addEventListener("keydown", (e) => {
const tgt = e.target;
const inInput = tgt.tagName === "INPUT";
if (inInput && tgt !== fbar && tgt !== overlayInput)
return; // other inputs (none)
if (e.key === "Escape") {
e.preventDefault();
closeMenu();
if (editing)
cancelEdit();
else
gridWrap.focus();
return;
}
if (e.key === "Enter") {
e.preventDefault();
commit(1, 0);
return;
}
if (e.key === "Tab") {
e.preventDefault();
commit(0, 1);
return;
}
if (editing || inInput) {
// while editing, let other keys type normally
return;
}
if (e.key === "ArrowDown") {
e.preventDefault();
moveSel(1, 0);
return;
}
if (e.key === "ArrowUp") {
e.preventDefault();
moveSel(-1, 0);
return;
}
if (e.key === "ArrowRight") {
e.preventDefault();
moveSel(0, 1);
return;
}
if (e.key === "ArrowLeft") {
e.preventDefault();
moveSel(0, -1);
return;
}
if (e.key === "Delete") {
e.preventDefault();
clearSelected();
return;
}
if (e.key === "Backspace") {
e.preventDefault();
beginEdit("overlay", "");
return;
}
if (isPrintable(e)) {
e.preventDefault();
beginEdit("overlay", e.key);
return;
}
});
// ---- toolbar --------------------------------------------------
function bindToolbar() {
document.getElementById("btn-insert-row").onclick = () => {
endEdit();
const r = (0, parser_1.refToRC)(selected).row;
model.insertRow(r);
afterStructure();
select((0, parser_1.rcToRef)(0, r));
};
document.getElementById("btn-delete-row").onclick = () => {
endEdit();
const r = (0, parser_1.refToRC)(selected).row;
model.deleteRow(r);
afterStructure();
};
document.getElementById("btn-insert-col").onclick = () => {
endEdit();
const c = (0, parser_1.refToRC)(selected).col;
model.insertCol(c);
afterStructure();
select((0, parser_1.rcToRef)(c, 1));
};
document.getElementById("btn-delete-col").onclick = () => {
endEdit();
const c = (0, parser_1.refToRC)(selected).col;
model.deleteCol(c);
afterStructure();
};
}
function init() {
buildGrid();
bindToolbar();
// load persisted
const loaded = model.loadFromStorage();
refreshAll();
select("A1");
// save on unload
window.addEventListener("beforeunload", () => model.saveToStorage());
// autosave after edits
setInterval(() => model.saveToStorage(), 2000);
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"deepseek/deepseek-v4-flash-0731","provider":"openrouter","persona":"typescript","sessionId":"dbc1a959-e2b7-4711-ae6d-60276e04ba69","tokensIn":3410981,"tokensOut":61705,"tokensTotal":3472686,"cost":0.08716394799999998,"turns":66,"toolCalls":69,"failedToolCalls":0,"timestamp":"2026-08-01T16:10:05.284Z"} -->