Spreadsheet
Qwen 3.6 27B MTP GGUF UD Q5 K XL · 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 65,899 bytes · SHA-256 8cf310c30f09
<!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: base ── */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 13px;
color: #1d1d1f;
background: #f9f9f9;
overflow: hidden;
height: 100vh;
display: flex;
flex-direction: column;
}
/* ── css: toolbar ── */
.toolbar {
display: flex;
align-items: center;
padding: 4px 8px;
background: #fff;
border-bottom: 1px solid #dadce0;
gap: 2px;
flex-shrink: 0;
}
.toolbar button {
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
padding: 4px 10px;
font-size: 12px;
color: #1d1d1f;
cursor: pointer;
white-space: nowrap;
}
.toolbar button:hover {
background: #e8eaed;
border-color: #dadce0;
}
.toolbar-sep {
width: 1px;
height: 20px;
background: #dadce0;
margin: 0 6px;
}
/* ── css: formula-bar ── */
#formula-bar {
display: flex;
align-items: center;
background: #fff;
border-bottom: 1px solid #dadce0;
padding: 0 4px;
height: 32px;
flex-shrink: 0;
}
.cell-ref {
display: flex;
align-items: center;
justify-content: center;
width: 56px;
min-width: 56px;
height: 28px;
border-right: 1px solid #dadce0;
font-size: 12px;
font-weight: 500;
color: #5f6368;
user-select: none;
}
#formula-input {
flex: 1;
border: none;
outline: none;
padding: 0 8px;
font-size: 13px;
font-family: inherit;
color: #1d1d1f;
height: 28px;
margin: 2px 0;
}
#formula-input:focus {
box-shadow: inset 0 0 0 2px #1a73e8;
}
/* ── css: grid ── */
#grid-container {
flex: 1;
overflow: hidden;
position: relative;
background: #fff;
}
#grid-scroll {
width: 100%;
height: 100%;
overflow: auto;
position: relative;
}
#grid {
border-collapse: collapse;
table-layout: fixed;
/* Width/height set by JS */
}
#grid th, #grid td {
border: 1px solid #dadce0;
padding: 0;
margin: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* Column headers (top row) */
#grid thead th {
position: sticky;
top: 0;
z-index: 2;
background: #f8f9fa;
color: #5f6368;
font-weight: 500;
font-size: 12px;
text-align: center;
user-select: none;
border-bottom: 1px solid #c0c0c0;
}
/* Row headers (first column) */
#grid tbody th.row-header {
position: sticky;
left: 0;
z-index: 1;
background: #f8f9fa;
color: #5f6368;
font-weight: 400;
font-size: 12px;
text-align: center;
user-select: none;
border-right: 1px solid #c0c0c0;
}
/* Top-left corner cell */
#grid thead th.corner {
position: sticky;
top: 0;
left: 0;
z-index: 3;
background: #f8f9fa;
border-bottom: 1px solid #c0c0c0;
border-right: 1px solid #c0c0c0;
}
/* Data cells */
#grid td.cell {
cursor: cell;
font-size: 13px;
padding: 2px 4px;
min-height: 24px;
box-sizing: border-box;
height: 24px;
vertical-align: middle;
}
#grid td.cell.selected {
outline: 2px solid #1a73e8;
outline-offset: -1px;
z-index: 1;
position: relative;
}
#grid td.cell.text-val { text-align: left; }
#grid td.cell.number-val { text-align: right; }
#grid td.cell.error-val { text-align: right; color: #d93025; font-size: 11px; }
/* Context menu */
.context-menu {
position: fixed;
background: #fff;
border: 1px solid #dadce0;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
z-index: 100;
min-width: 160px;
padding: 4px 0;
}
.context-menu.hidden { display: none; }
.context-menu-item {
padding: 6px 16px;
cursor: pointer;
font-size: 13px;
color: #1d1d1f;
}
.context-menu-item:hover {
background: #e8eaed;
}
.context-menu-sep {
height: 1px;
background: #dadce0;
margin: 4px 0;
}
/* ── css: selection ── */
#grid td.cell.selected {
outline: 2px solid #1a73e8;
outline-offset: -1px;
z-index: 1;
position: relative;
}
#grid td.cell.editing {
outline: 2px solid #1a73e8;
outline-offset: -1px;
z-index: 1;
position: relative;
}
/* Cell editing input (for in-cell editing) */
.cell-input {
border: 2px solid #1a73e8;
outline: none;
width: 100%;
height: 100%;
font-size: 13px;
font-family: inherit;
padding: 1px 3px;
margin: -2px -1px;
box-sizing: border-box;
background: #fff;
}
/* ── css: scrollbar ── */
#grid-scroll::-webkit-scrollbar {
width: 10px;
height: 10px;
}
#grid-scroll::-webkit-scrollbar-track {
background: #f1f1f1;
}
#grid-scroll::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 5px;
}
#grid-scroll::-webkit-scrollbar-thumb:hover {
background: #a1a1a1;
}
#grid-scroll::-webkit-scrollbar-corner {
background: #f1f1f1;
}
</style>
</head>
<body>
<div id="toolbar" class="toolbar">
<button id="btn-insert-row" title="Insert Row Above">Insert Row</button>
<button id="btn-insert-col" title="Insert Column Left">Insert Column</button>
<button id="btn-delete-row" title="Delete Selected Row">Delete Row</button>
<button id="btn-delete-col" title="Delete Selected Column">Delete Column</button>
<button id="btn-clear" title="Clear Selected Cell (Delete key)">Clear</button>
<span class="toolbar-sep"></span>
<button id="btn-save" title="Save to localStorage">Save</button>
</div>
<div id="formula-bar">
<span id="cell-ref" class="cell-ref"></span>
<input id="formula-input" type="text" spellcheck="false" autocomplete="off">
</div>
<div id="grid-container">
<div id="grid-scroll">
<table id="grid"></table>
</div>
</div>
<div id="context-menu" class="context-menu hidden"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./render":"src/render.ts","./editing":"src/editing.ts","./state":"src/state.ts","./types":"src/types.ts","./engine":"src/engine.ts","./utils":"src/utils.ts"},"src/state.ts":{"./types":"src/types.ts","./utils":"src/utils.ts"},"src/utils.ts":{"./types":"src/types.ts"},"src/tokenizer.ts":{"./types":"src/types.ts"},"src/parser.ts":{"./types":"src/types.ts","./tokenizer":"src/tokenizer.ts","./utils":"src/utils.ts"},"src/evaluator.ts":{"./types":"src/types.ts","./parser":"src/parser.ts","./utils":"src/utils.ts","./state":"src/state.ts"},"src/engine.ts":{"./state":"src/state.ts","./evaluator":"src/evaluator.ts","./types":"src/types.ts","./utils":"src/utils.ts"},"src/render.ts":{"./state":"src/state.ts","./types":"src/types.ts","./utils":"src/utils.ts"},"src/editing.ts":{"./state":"src/state.ts","./engine":"src/engine.ts","./render":"src/render.ts","./utils":"src/utils.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.showContextMenu = showContextMenu;
const render_1 = require("./render");
const editing_1 = require("./editing");
const state_1 = require("./state");
const engine_1 = require("./engine");
const utils_1 = require("./utils");
function refreshCellDom(addr) {
if (!addr)
return;
const ref = (0, utils_1.parseCellRef)(addr);
if (ref && (0, state_1.inBounds)(ref.row, ref.col)) {
(0, render_1.updateCellAt)(ref.row, ref.col);
}
}
const types_1 = require("./types");
const STORAGE_KEY = 'spreadsheet_data_v2';
// ─── Init ──────────────────────────────────────────────────────────────────────
function init() {
loadFromStorage();
(0, render_1.initGrid)();
wireEvents();
(0, engine_1.recalcAll)();
(0, render_1.rerenderAll)();
}
// ─── Events ────────────────────────────────────────────────────────────────────
function wireEvents() {
// Grid click → select cell
const grid = document.getElementById('grid');
grid.addEventListener('click', onGridClick);
grid.addEventListener('dblclick', onGridDblClick);
// Keyboard
document.addEventListener('keydown', onKeyDown);
// Formula bar
const formulaInput = document.getElementById('formula-input');
formulaInput.addEventListener('keydown', onFormulaBarKey);
formulaInput.addEventListener('input', onFormulaBarInput);
// Toolbar buttons
document.getElementById('btn-insert-row')?.addEventListener('click', () => insertRowAt(selRow()));
document.getElementById('btn-insert-col')?.addEventListener('click', () => insertColAt(selCol()));
document.getElementById('btn-delete-row')?.addEventListener('click', () => deleteRowAt(selRow()));
document.getElementById('btn-delete-col')?.addEventListener('click', () => deleteColAt(selCol()));
document.getElementById('btn-clear')?.addEventListener('click', () => { (0, editing_1.clearCell)(); saveToStorage(); });
document.getElementById('btn-save')?.addEventListener('click', () => { saveToStorage(); flashSave(); });
// Close context menu on outside click
document.addEventListener('click', () => hideContextMenu());
// Save before unload
window.addEventListener('beforeunload', saveToStorage);
}
function selCol() {
const s = (0, state_1.getSelection)();
return s.col;
}
function selRow() {
const s = (0, state_1.getSelection)();
return s.row;
}
function onGridClick(e) {
const td = e.target.closest('td.cell');
if (!td)
return;
const col = parseInt(td.dataset.col || '0', 10);
const row = parseInt(td.dataset.row || '0', 10);
(0, editing_1.selectCell)(col, row);
}
function onGridDblClick(e) {
const td = e.target.closest('td.cell');
if (!td)
return;
const col = parseInt(td.dataset.col || '0', 10);
const row = parseInt(td.dataset.row || '0', 10);
(0, editing_1.selectCell)(col, row);
(0, editing_1.startEdit)();
}
function onKeyDown(e) {
const formulaInput = document.getElementById('formula-input');
// If formula bar is focused or is the event target, handle formula bar editing
if (e.target === formulaInput) {
if (e.key === 'Escape') {
(0, editing_1.cancelEdit)();
e.preventDefault();
e.stopPropagation();
return;
}
if (e.key === 'Enter') {
const updated = (0, editing_1.commitFromFormulaBar)();
for (const addr of updated)
refreshCellDom(addr);
e.preventDefault();
e.stopPropagation();
(0, editing_1.moveSelection)(0, 1);
saveToStorage();
return;
}
if (e.key === 'Tab') {
const updated = (0, editing_1.commitFromFormulaBar)();
for (const addr of updated)
refreshCellDom(addr);
e.preventDefault();
e.stopPropagation();
(0, editing_1.moveSelection)(e.shiftKey ? -1 : 1, 0);
saveToStorage();
return;
}
// Allow normal typing in formula bar
return;
}
// Grid navigation and editing (formula bar not focused)
if ((0, editing_1.isEditing)()) {
if (e.key === 'Escape') {
(0, editing_1.cancelEdit)();
e.preventDefault();
return;
}
return;
}
switch (e.key) {
case 'ArrowUp':
e.preventDefault();
(0, editing_1.moveSelection)(0, -1);
break;
case 'ArrowDown':
e.preventDefault();
(0, editing_1.moveSelection)(0, 1);
break;
case 'ArrowLeft':
e.preventDefault();
(0, editing_1.moveSelection)(-1, 0);
break;
case 'ArrowRight':
e.preventDefault();
(0, editing_1.moveSelection)(1, 0);
break;
case 'Tab':
e.preventDefault();
(0, editing_1.moveSelection)(e.shiftKey ? -1 : 1, 0);
break;
case 'Enter':
e.preventDefault();
(0, editing_1.startEdit)();
formulaInput.focus();
break;
case 'Delete':
case 'Backspace':
e.preventDefault();
(0, editing_1.clearCell)();
saveToStorage();
break;
case 'F2':
e.preventDefault();
(0, editing_1.startEdit)();
formulaInput.focus();
break;
default:
// If printable character, start editing with that character
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
e.preventDefault();
(0, editing_1.startEdit)();
formulaInput.focus();
formulaInput.value = e.key;
}
break;
}
}
function onFormulaBarKey(e) {
// Handled by onKeyDown since e.target === formulaInput matches there
}
function onFormulaBarInput() {
// No live preview needed — value commits on Enter
}
function showContextMenu(x, y, type, index) {
const menu = document.getElementById('context-menu');
menu.innerHTML = '';
menu.classList.remove('hidden');
if (type === 'row') {
addItem(menu, `Insert Row Above (Row ${index + 1})`, () => {
insertRowAt(index);
hideContextMenu();
});
addItem(menu, `Delete Row ${index + 1}`, () => {
deleteRowAt(index);
hideContextMenu();
});
}
else {
addItem(menu, `Insert Column Left of ${(0, utils_1.colLabel)(index)}`, () => {
insertColAt(index);
hideContextMenu();
});
addItem(menu, `Delete Column ${(0, utils_1.colLabel)(index)}`, () => {
deleteColAt(index);
hideContextMenu();
});
}
// Position
const menuRect = menu.getBoundingClientRect();
let menuX = x;
let menuY = y;
if (x + menuRect.width > window.innerWidth)
menuX = window.innerWidth - menuRect.width - 4;
if (y + menuRect.height > window.innerHeight)
menuY = window.innerHeight - menuRect.height - 4;
menu.style.left = menuX + 'px';
menu.style.top = menuY + 'px';
}
function hideContextMenu() {
const menu = document.getElementById('context-menu');
menu.classList.add('hidden');
}
function addItem(menu, text, onClick) {
const item = document.createElement('div');
item.className = 'context-menu-item';
item.textContent = text;
item.addEventListener('click', onClick);
menu.appendChild(item);
}
window.__spreadshowContextMenu = showContextMenu;
// ─── Row/Column Operations ─────────────────────────────────────────────────────
function insertRowAt(index) {
if (state_1.cellGrid.length >= types_1.ROW_COUNT)
return; // Max rows
// Create a new empty row
const newRow = [];
for (let c = 0; c < state_1.cellGrid[0].length; c++) {
newRow.push((0, state_1.emptyCell)());
}
// Insert at index
state_1.cellGrid.splice(index, 0, newRow);
// Shift all references in formulas that point to rows >= index
rewriteReferencesForRowInsert(index);
(0, engine_1.recalcAll)();
(0, render_1.rerenderAll)();
saveToStorage();
}
function deleteRowAt(index) {
if (state_1.cellGrid.length <= 1)
return;
// Remove row
state_1.cellGrid.splice(index, 1);
// Shift all references in formulas
rewriteReferencesForRowDelete(index);
(0, engine_1.recalcAll)();
(0, render_1.rerenderAll)();
saveToStorage();
}
function insertColAt(index) {
if (state_1.cellGrid[0].length >= types_1.COL_COUNT)
return;
// Create new column (empty cells)
for (let r = 0; r < state_1.cellGrid.length; r++) {
state_1.cellGrid[r].splice(index, 0, (0, state_1.emptyCell)());
}
rewriteReferencesForColInsert(index);
(0, engine_1.recalcAll)();
(0, render_1.rerenderAll)();
saveToStorage();
}
function deleteColAt(index) {
if (state_1.cellGrid[0].length <= 1)
return;
for (let r = 0; r < state_1.cellGrid.length; r++) {
state_1.cellGrid[r].splice(index, 1);
}
rewriteReferencesForColDelete(index);
(0, engine_1.recalcAll)();
(0, render_1.rerenderAll)();
saveToStorage();
}
// ─── Formula Rewriting ─────────────────────────────────────────────────────────
// When a row/col is inserted/deleted, rewrite cell references in all formulas
function rewriteReferencesForRowInsert(rowIndex) {
for (let r = 0; r < state_1.cellGrid.length; r++) {
for (let c = 0; c < state_1.cellGrid[r].length; c++) {
const cell = state_1.cellGrid[r][c];
if (!cell.raw.startsWith('='))
continue;
cell.raw = rewriteFormulaForRowShift(cell.raw, rowIndex, 1);
}
}
}
function rewriteReferencesForRowDelete(deleteRow) {
for (let r = 0; r < state_1.cellGrid.length; r++) {
for (let c = 0; c < state_1.cellGrid[r].length; c++) {
const cell = state_1.cellGrid[r][c];
if (!cell.raw.startsWith('='))
continue;
const rewritten = rewriteFormulaForRowShift(cell.raw, deleteRow, -1);
if (rewritten === '#REF!') {
// Mark as error cell
cell.raw = '=REF';
cell.kind = 'error';
cell.value = '#REF!';
cell.error = '#REF!';
}
else {
cell.raw = rewritten;
}
}
}
}
function rewriteReferencesForColInsert(colIndex) {
for (let r = 0; r < state_1.cellGrid.length; r++) {
for (let c = 0; c < state_1.cellGrid[r].length; c++) {
const cell = state_1.cellGrid[r][c];
if (!cell.raw.startsWith('='))
continue;
cell.raw = rewriteFormulaForColShift(cell.raw, colIndex, 1);
}
}
}
function rewriteReferencesForColDelete(deleteCol) {
for (let r = 0; r < state_1.cellGrid.length; r++) {
for (let c = 0; c < state_1.cellGrid[r].length; c++) {
const cell = state_1.cellGrid[r][c];
if (!cell.raw.startsWith('='))
continue;
const rewritten = rewriteFormulaForColShift(cell.raw, deleteCol, -1);
if (rewritten === '#REF!') {
cell.raw = '=REF';
cell.kind = 'error';
cell.value = '#REF!';
cell.error = '#REF!';
}
else {
cell.raw = rewritten;
}
}
}
}
// Rewrite cell references in a formula string when rows shift
// Returns rewritten formula, or '#REF!' if any reference is invalid
function rewriteFormulaForRowShift(formula, pivotRow, delta) {
let invalid = false;
const rewritten = formula.replace(/[A-Z]\d+/gi, (match) => {
const ref = (0, utils_1.parseCellRef)(match);
if (!ref)
return match;
if (delta > 0) {
// Insert row: refs to rows >= pivot shift +1
if (ref.row >= pivotRow) {
const newRow = ref.row + delta;
if (newRow < state_1.cellGrid.length) {
return (0, utils_1.colLabel)(ref.col) + (newRow + 1);
}
}
}
else {
// Delete row: refs to rows > pivot shift -1
if (ref.row > pivotRow) {
const newRow = ref.row + delta;
if (newRow >= 0) {
return (0, utils_1.colLabel)(ref.col) + (newRow + 1);
}
else {
invalid = true;
return match;
}
}
// Reference exactly to the deleted row → invalid
if (ref.row === pivotRow) {
invalid = true;
return match;
}
}
return match;
});
return invalid ? '#REF!' : rewritten;
}
// Rewrite cell references in a formula string when columns shift
// Returns rewritten formula, or '#REF!' if any reference is invalid
function rewriteFormulaForColShift(formula, pivotCol, delta) {
let invalid = false;
const rewritten = formula.replace(/[A-Z]\d+/gi, (match) => {
const ref = (0, utils_1.parseCellRef)(match);
if (!ref)
return match;
if (delta > 0) {
if (ref.col >= pivotCol) {
const newCol = ref.col + delta;
if (newCol < state_1.cellGrid[0].length) {
return (0, utils_1.colLabel)(newCol) + (ref.row + 1);
}
}
}
else {
if (ref.col > pivotCol) {
const newCol = ref.col + delta;
if (newCol >= 0) {
return (0, utils_1.colLabel)(newCol) + (ref.row + 1);
}
else {
invalid = true;
return match;
}
}
if (ref.col === pivotCol) {
invalid = true;
return match;
}
}
return match;
});
return invalid ? '#REF!' : rewritten;
}
// ─── Persistence ───────────────────────────────────────────────────────────────
function saveToStorage() {
try {
const data = {};
for (let r = 0; r < (0, state_1.getGridRowCount)(); r++) {
for (let c = 0; c < (0, state_1.getGridColCount)(); c++) {
const cell = state_1.cellGrid[r][c];
if (cell && cell.raw !== '') {
data[(0, utils_1.cellAddress)(c, r)] = cell.raw;
}
}
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
catch {
// Storage full or unavailable
}
}
function loadFromStorage() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw)
return;
const data = JSON.parse(raw);
for (const [addr, value] of Object.entries(data)) {
const ref = (0, utils_1.parseCellRef)(addr);
if (ref && (0, state_1.inBounds)(ref.row, ref.col)) {
(0, state_1.setCellRaw)(ref.row, ref.col, value);
}
}
}
catch {
// Corrupted data
}
}
function flashSave() {
const btn = document.getElementById('btn-save');
const orig = btn.textContent;
btn.textContent = '✓ Saved!';
setTimeout(() => { btn.textContent = orig; }, 1200);
}
// ─── Start ─────────────────────────────────────────────────────────────────────
init();
};
// ── module: src/types.ts ──
__mods["src/types.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.COL_HEADER_HEIGHT = exports.ROW_HEADER_WIDTH = exports.ROW_HEIGHT = exports.COL_WIDTH = exports.ROW_COUNT = exports.COL_COUNT = void 0;
// Tunable constants
exports.COL_COUNT = 26; // A-Z
exports.ROW_COUNT = 100;
exports.COL_WIDTH = 100;
exports.ROW_HEIGHT = 24;
exports.ROW_HEADER_WIDTH = 40;
exports.COL_HEADER_HEIGHT = 24;
// Error strings
exports.ERR_CYCLE = '#CYCLE!';
exports.ERR_DIV0 = '#DIV/0!';
exports.ERR_REF = '#REF!';
exports.ERR_PARSE = '#ERR!';
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.selRow = exports.selCol = exports.rowOrder = exports.colOrder = exports.cellGrid = void 0;
exports.emptyCell = emptyCell;
exports.setCellRaw = setCellRaw;
exports.getCellRaw = getCellRaw;
exports.setCellValue = setCellValue;
exports.getCell = getCell;
exports.getGridColCount = getGridColCount;
exports.getGridRowCount = getGridRowCount;
exports.initOrder = initOrder;
exports.inBounds = inBounds;
exports.getSelection = getSelection;
exports.setSelection = setSelection;
const types_1 = require("./types");
const utils_1 = require("./utils");
// Grid is a 2D array of Cell objects.
// colOrder / rowOrder track insertion order (may differ from default after inserts/deletes)
exports.cellGrid = createEmptyGrid();
exports.colOrder = [];
exports.rowOrder = [];
exports.selCol = 0;
exports.selRow = 0;
function createEmptyGrid() {
const grid = [];
for (let r = 0; r < types_1.ROW_COUNT; r++) {
const row = [];
for (let c = 0; c < types_1.COL_COUNT; c++) {
row.push(emptyCell());
}
grid.push(row);
}
return grid;
}
function emptyCell() {
return { raw: '', kind: 'text', value: '' };
}
function setCellRaw(r, c, raw) {
if (r < 0 || r >= exports.cellGrid.length || c < 0 || c >= exports.cellGrid[0].length)
return;
exports.cellGrid[r][c] = { raw, kind: 'text', value: raw };
}
function getCellRaw(r, c) {
if (r < 0 || r >= exports.cellGrid.length || c < 0 || c >= exports.cellGrid[0].length)
return '';
return exports.cellGrid[r][c].raw;
}
function setCellValue(r, c, kind, value, error) {
if (r < 0 || r >= exports.cellGrid.length || c < 0 || c >= exports.cellGrid[0].length)
return;
const cell = exports.cellGrid[r][c];
cell.kind = kind;
cell.value = value;
cell.error = error;
}
function getCell(r, c) {
if (r < 0 || r >= exports.cellGrid.length || c < 0 || c >= exports.cellGrid[0].length)
return null;
return exports.cellGrid[r][c];
}
function getGridColCount() {
return exports.cellGrid[0]?.length ?? 0;
}
function getGridRowCount() {
return exports.cellGrid.length;
}
// Initialize column and row order arrays
function initOrder() {
exports.colOrder = [];
for (let c = 0; c < types_1.COL_COUNT; c++) {
exports.colOrder.push((0, utils_1.colLabel)(c));
}
exports.rowOrder = [];
for (let r = 0; r < types_1.ROW_COUNT; r++) {
exports.rowOrder.push(r);
}
}
// Check if a position is within grid bounds
function inBounds(r, c) {
return r >= 0 && r < exports.cellGrid.length && c >= 0 && c < exports.cellGrid[0]?.length;
}
function getSelection() {
return { col: exports.selCol, row: exports.selRow };
}
function setSelection(col, row) {
exports.selCol = col;
exports.selRow = row;
}
};
// ── module: src/utils.ts ──
__mods["src/utils.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.colLabel = colLabel;
exports.colIndex = colIndex;
exports.parseCellRef = parseCellRef;
exports.cellAddress = cellAddress;
exports.formatNumber = formatNumber;
exports.isValidCellRef = isValidCellRef;
exports.isValidRangeRef = isValidRangeRef;
const types_1 = require("./types");
// Convert column index (0-based) to letter label (A, B, ..., Z)
function colLabel(c) {
if (c < 0 || c >= types_1.COL_COUNT)
return '';
return String.fromCharCode(65 + c);
}
// Convert column label to 0-based index
function colIndex(label) {
const upper = label.toUpperCase();
if (upper.length === 1) {
const idx = upper.charCodeAt(0) - 65;
return idx >= 0 && idx < types_1.COL_COUNT ? idx : -1;
}
return -1;
}
// Parse cell reference "A1" into { col, row }
function parseCellRef(ref) {
if (!ref || typeof ref !== 'string')
return null;
const match = ref.match(/^([A-Z])(\d+)$/i);
if (!match)
return null;
const col = colIndex(match[1]);
const row = parseInt(match[2], 10) - 1;
if (col < 0 || col >= types_1.COL_COUNT || row < 0 || row >= 1000)
return null; // generous row bound
return { col, row };
}
// Cell address string from col/row indices
function cellAddress(c, r) {
return colLabel(c) + (r + 1);
}
// Format a number for display: up to 10 significant digits, no trailing zeros
function formatNumber(n) {
if (isNaN(n))
return '#ERR!';
if (!isFinite(n))
return '#ERR!';
if (Number.isInteger(n) && Math.abs(n) < 1e15) {
return n.toString();
}
const s = n.toPrecision(10);
return parseFloat(s).toString();
}
// Check if a string is a valid cell reference (e.g. "A1", "Z100")
function isValidCellRef(s) {
return /^([A-Z])(\d+)$/i.test(s);
}
// Check if a string is a valid range reference (e.g. "A1:A5")
function isValidRangeRef(s) {
return /^[A-Z]\d+:[A-Z]\d+$/i.test(s);
}
};
// ── module: src/tokenizer.ts ──
__mods["src/tokenizer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.tokenize = tokenize;
function tokenize(source) {
const tokens = [];
let i = 0;
while (i < source.length) {
const ch = source[i];
// Skip whitespace
if (ch === ' ' || ch === '\t') {
i++;
continue;
}
// Number (integer or decimal)
if ((ch >= '0' && ch <= '9') || (ch === '.')) {
let numStr = '';
const start = i;
while (i < source.length && ((source[i] >= '0' && source[i] <= '9') || source[i] === '.')) {
numStr += source[i];
i++;
}
tokens.push({ type: 'NUMBER', value: parseFloat(numStr), col: start });
continue;
}
// Letter → identifier (cell ref or function name)
if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')) {
let ident = '';
const start = i;
while (i < source.length && ((source[i] >= 'A' && source[i] <= 'Z') || (source[i] >= 'a' && source[i] <= 'z') || (source[i] >= '0' && source[i] <= '9'))) {
ident += source[i];
i++;
}
// Check if next char is ':' → it's a range start
if (i < source.length && source[i] === ':') {
tokens.push({ type: 'IDENT', value: ident, col: start });
continue;
}
tokens.push({ type: 'IDENT', value: ident, col: start });
continue;
}
// Operators and punctuation
switch (ch) {
case '+':
tokens.push({ type: 'PLUS', value: '+', col: i });
break;
case '-':
tokens.push({ type: 'MINUS', value: '-', col: i });
break;
case '*':
tokens.push({ type: 'STAR', value: '*', col: i });
break;
case '/':
tokens.push({ type: 'SLASH', value: '/', col: i });
break;
case '(':
tokens.push({ type: 'LPAREN', value: '(', col: i });
break;
case ')':
tokens.push({ type: 'RPAREN', value: ')', col: i });
break;
case ',':
tokens.push({ type: 'COMMA', value: ',', col: i });
break;
case ':':
tokens.push({ type: 'COLON', value: ':', col: i });
break;
default:
throw new Error(`Unexpected character '${ch}' at position ${i}`);
}
i++;
}
tokens.push({ type: 'EOF', value: '', col: i });
return tokens;
}
};
// ── module: src/parser.ts ──
__mods["src/parser.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Parser = void 0;
exports.parseFormula = parseFormula;
const tokenizer_1 = require("./tokenizer");
const utils_1 = require("./utils");
class Parser {
constructor(source) {
this.pos = 0;
this.tokens = (0, tokenizer_1.tokenize)(source);
}
parse() {
const expr = this.parseExpression();
if (this.current().type !== 'EOF') {
throw new Error(`Unexpected token '${this.current().value}' at position ${this.current().col}`);
}
return expr;
}
current() {
return this.tokens[this.pos] || this.tokens[this.tokens.length - 1];
}
eat(type) {
const tok = this.current();
if (tok.type === 'EOF' && type !== 'EOF') {
throw new Error(`Unexpected end of expression`);
}
this.pos++;
return tok;
}
match(type) {
return this.current().type === type;
}
// Expression = Term (('+' | '-') Term)*
parseExpression() {
let left = this.parseTerm();
while (this.match('PLUS') || this.match('MINUS')) {
const op = this.current().value;
this.eat(op);
const right = this.parseTerm();
left = { kind: 'binop', op, left, right };
}
return left;
}
// Term = Unary (('*' | '/') Unary)*
parseTerm() {
let left = this.parseUnary();
while (this.match('STAR') || this.match('SLASH')) {
const op = this.current().value;
this.eat(op);
const right = this.parseUnary();
left = { kind: 'binop', op, left, right };
}
return left;
}
// Unary = '-' Unary | Primary
parseUnary() {
if (this.match('MINUS')) {
this.eat('MINUS');
const operand = this.parsePrimary();
return { kind: 'unary', op: '-', operand };
}
if (this.match('PLUS')) {
this.eat('PLUS');
return this.parsePrimary();
}
return this.parsePrimary();
}
// Primary = NUMBER | CELL_COLON_CELL | CELL '(' Args ')' | '(' Expression ')'
parsePrimary() {
const tok = this.current();
// Number literal
if (tok.type === 'NUMBER') {
this.eat('NUMBER');
return { kind: 'number', value: tok.value };
}
// Identifier: could be cell ref, range, or function call
if (tok.type === 'IDENT') {
const identStr = tok.value;
this.eat('IDENT');
// Function call: IDENT '(' ...
if (this.match('LPAREN')) {
this.eat('LPAREN');
const args = [];
if (!this.match('RPAREN')) {
args.push(this.parseExpression());
while (this.match('COMMA')) {
this.eat('COMMA');
args.push(this.parseExpression());
}
}
this.eat('RPAREN');
return { kind: 'call', name: identStr.toUpperCase(), args };
}
// Range: IDENT ':' IDENT (e.g. A1:B5)
if (this.match('COLON')) {
this.eat('COLON');
const endTok = this.eat('IDENT');
const startRef = (0, utils_1.parseCellRef)(identStr);
const endRef = (0, utils_1.parseCellRef)(endTok.value);
if (!startRef || !endRef) {
throw new Error(`Invalid range '${identStr}:${endTok.value}'`);
}
return { kind: 'range', col1: startRef.col, row1: startRef.row, col2: endRef.col, row2: endRef.row };
}
// Cell reference: A1, B7
const ref = (0, utils_1.parseCellRef)(identStr);
if (!ref) {
throw new Error(`Invalid cell reference '${identStr}'`);
}
return { kind: 'cell', col: ref.col, row: ref.row };
}
// Parenthesized expression
if (tok.type === 'LPAREN') {
this.eat('LPAREN');
const expr = this.parseExpression();
this.eat('RPAREN');
return expr;
}
throw new Error(`Unexpected token '${tok.value}' at position ${tok.col}`);
}
}
exports.Parser = Parser;
// Top-level parse function
function parseFormula(source) {
const parser = new Parser(source);
return parser.parse();
}
};
// ── module: src/evaluator.ts ──
__mods["src/evaluator.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseRaw = parseRaw;
exports.evaluate = evaluate;
exports.setCell = setCell;
exports.collectRefs = collectRefs;
const types_1 = require("./types");
const parser_1 = require("./parser");
const utils_1 = require("./utils");
const state_1 = require("./state");
// Parse a formula string (after the =) into an AST, or throw on error
function parseRaw(raw) {
if (raw === '') {
return { isFormula: false };
}
if (!raw.startsWith('=')) {
// Plain text or number
const num = Number(raw);
if (!isNaN(num) && raw.trim() !== '') {
return { isFormula: false, error: undefined };
}
return { isFormula: false, error: undefined };
}
try {
const expr = (0, parser_1.parseFormula)(raw.substring(1));
return { isFormula: true, expr };
}
catch (e) {
return { isFormula: true, error: types_1.ERR_PARSE };
}
}
// Evaluate an AST node at position (col, row)
function evaluate(expr, col, row) {
try {
const result = evalExpr(expr, col, row);
if (result.error) {
return result;
}
return { value: result.value, kind: 'number' };
}
catch (e) {
const msg = e?.message ?? '';
if (msg.includes('#CYCLE!')) {
return { value: types_1.ERR_CYCLE, kind: 'error', error: types_1.ERR_CYCLE };
}
if (msg.includes('#DIV/0!')) {
return { value: types_1.ERR_DIV0, kind: 'error', error: types_1.ERR_DIV0 };
}
if (msg.includes('#REF!')) {
return { value: types_1.ERR_REF, kind: 'error', error: types_1.ERR_REF };
}
return { value: types_1.ERR_PARSE, kind: 'error', error: types_1.ERR_PARSE };
}
}
// Set a cell's raw value and classify it (don't evaluate formulas here)
function setCell(r, c, raw) {
const cell = { raw, kind: 'text', value: raw };
if (raw === '') {
cell.kind = 'text';
cell.value = '';
}
else if (!raw.startsWith('=')) {
const num = Number(raw);
if (!isNaN(num) && raw.trim() !== '') {
cell.kind = 'number';
cell.value = num;
}
else {
cell.kind = 'text';
cell.value = raw;
}
}
else {
cell.kind = 'formula';
cell.value = raw; // placeholder until evaluated
}
state_1.cellGrid[r][c] = cell;
return cell;
}
function evalExpr(expr, _col, _row) {
switch (expr.kind) {
case 'number':
return { value: expr.value, kind: 'number' };
case 'cell': {
const cell = (0, state_1.getCell)(expr.row, expr.col);
if (!cell || !(0, state_1.inBounds)(expr.row, expr.col)) {
return { value: types_1.ERR_REF, kind: 'error', error: types_1.ERR_REF };
}
if (cell.kind === 'error') {
return { value: cell.value, kind: 'error', error: cell.error };
}
if (cell.raw.startsWith('=') && (cell.value === '' || (typeof cell.value === 'string' && cell.value.startsWith('=')))) {
// Formula not yet evaluated or in a cycle
if (cell.value === '')
return { value: 0, kind: 'number' };
return { value: types_1.ERR_CYCLE, kind: 'error', error: types_1.ERR_CYCLE };
}
const v = cell.kind === 'number' ? cell.value : Number(cell.value);
if (isNaN(v) && cell.kind === 'text' && cell.value !== '') {
// Text that can't be a number in a numeric context → 0
return { value: 0, kind: 'number' };
}
return { value: isNaN(v) ? 0 : v, kind: cell.kind };
}
case 'range': {
// Range evaluates to an array of values — but in arithmetic context, this doesn't make sense alone.
// Range is primarily used as a function argument.
// When used directly in arithmetic, treat as #ERR!
return { value: types_1.ERR_PARSE, kind: 'error', error: types_1.ERR_PARSE };
}
case 'binop': {
const left = evalExpr(expr.left, _col, _row);
if (left.error)
return left;
const right = evalExpr(expr.right, _col, _row);
if (right.error)
return right;
const lNum = Number(left.value);
const rNum = Number(right.value);
switch (expr.op) {
case '+': return { value: lNum + rNum, kind: 'number' };
case '-': return { value: lNum - rNum, kind: 'number' };
case '*': return { value: lNum * rNum, kind: 'number' };
case '/':
if (rNum === 0)
return { value: types_1.ERR_DIV0, kind: 'error', error: types_1.ERR_DIV0 };
return { value: lNum / rNum, kind: 'number' };
default: return { value: types_1.ERR_PARSE, kind: 'error', error: types_1.ERR_PARSE };
}
}
case 'unary': {
const operand = evalExpr(expr.operand, _col, _row);
if (operand.error)
return operand;
if (expr.op === '-')
return { value: -Number(operand.value), kind: 'number' };
return operand;
}
case 'call': {
return evalFunction(expr.name, expr.args, _col, _row);
}
}
}
// Get a list of numeric values from an expression (handles cells, ranges, numbers)
function resolveArgsToValues(args, col, row) {
const values = [];
for (const arg of args) {
switch (arg.kind) {
case 'number':
values.push(arg.value);
break;
case 'cell': {
const cell = (0, state_1.getCell)(arg.row, arg.col);
if (!cell || !(0, state_1.inBounds)(arg.row, arg.col))
continue;
if (cell.kind === 'error') {
return { values: [], hasError: true, error: cell.error };
}
const v = cell.kind === 'number' ? cell.value : Number(cell.value);
if (!isNaN(v))
values.push(v);
break;
}
case 'range': {
const { col1, row1, col2, row2 } = arg;
const minC = Math.min(col1, col2);
const maxC = Math.max(col1, col2);
const minR = Math.min(row1, row2);
const maxR = Math.max(row1, row2);
for (let r = minR; r <= maxR; r++) {
for (let c = minC; c <= maxC; c++) {
const cell = (0, state_1.getCell)(r, c);
if (!cell || !(0, state_1.inBounds)(r, c))
continue;
if (cell.kind === 'error') {
return { values: [], hasError: true, error: cell.error };
}
const v = cell.kind === 'number' ? cell.value : Number(cell.value);
if (!isNaN(v))
values.push(v);
}
}
break;
}
case 'binop':
case 'unary': {
const result = evalExpr(arg, col, row);
if (result.error)
return { values: [], hasError: true, error: result.error };
values.push(Number(result.value));
break;
}
case 'call': {
const result = evalExpr(arg, col, row);
if (result.error)
return { values: [], hasError: true, error: result.error };
values.push(Number(result.value));
break;
}
}
}
return { values, hasError: false };
}
function evalFunction(name, args, col, row) {
const { values, hasError, error } = resolveArgsToValues(args, col, row);
if (hasError)
return { value: error || types_1.ERR_PARSE, kind: 'error', error: error };
switch (name) {
case 'SUM':
return { value: values.reduce((a, b) => a + b, 0), kind: 'number' };
case 'AVG':
case 'AVERAGE':
if (values.length === 0)
return { value: 0, kind: 'number' };
return { value: values.reduce((a, b) => a + b, 0) / values.length, kind: 'number' };
case 'MIN':
if (values.length === 0)
return { value: 0, kind: 'number' };
return { value: Math.min(...values), kind: 'number' };
case 'MAX':
if (values.length === 0)
return { value: 0, kind: 'number' };
return { value: Math.max(...values), kind: 'number' };
case 'COUNT':
return { value: values.length, kind: 'number' };
default:
return { value: types_1.ERR_PARSE, kind: 'error', error: types_1.ERR_PARSE };
}
}
// Extract cell references from an AST node (for dependency graph)
function collectRefs(expr) {
const refs = new Set();
collectRefsFrom(expr, refs);
return refs;
}
function collectRefsFrom(expr, refs) {
switch (expr.kind) {
case 'cell':
refs.add(`${(0, utils_1.colLabel)(expr.col)}${expr.row + 1}`);
break;
case 'range': {
const minC = Math.min(expr.col1, expr.col2);
const maxC = Math.max(expr.col1, expr.col2);
const minR = Math.min(expr.row1, expr.row2);
const maxR = Math.max(expr.row1, expr.row2);
for (let r = minR; r <= maxR; r++) {
for (let c = minC; c <= maxC; c++) {
refs.add(`${(0, utils_1.colLabel)(c)}${r + 1}`);
}
}
break;
}
case 'binop':
collectRefsFrom(expr.left, refs);
collectRefsFrom(expr.right, refs);
break;
case 'unary':
collectRefsFrom(expr.operand, refs);
break;
case 'call':
for (const arg of expr.args) {
collectRefsFrom(arg, refs);
}
break;
}
}
};
// ── module: src/engine.ts ──
__mods["src/engine.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateCell = updateCell;
exports.recalcAll = recalcAll;
const state_1 = require("./state");
const evaluator_1 = require("./evaluator");
const types_1 = require("./types");
const utils_1 = require("./utils");
function buildDepGraph() {
const dependsOn = new Map();
const dependents = new Map();
for (let r = 0; r < (0, state_1.getGridRowCount)(); r++) {
for (let c = 0; c < (0, state_1.getGridColCount)(); c++) {
const cell = state_1.cellGrid[r][c];
if (!cell || !cell.raw.startsWith('='))
continue;
const addr = (0, utils_1.cellAddress)(c, r);
const parsed = (0, evaluator_1.parseRaw)(cell.raw);
if (parsed.expr) {
const refs = (0, evaluator_1.collectRefs)(parsed.expr);
dependsOn.set(addr, refs);
for (const ref of refs) {
if (!dependents.has(ref))
dependents.set(ref, new Set());
dependents.get(ref).add(addr);
}
}
}
}
return { dependsOn, dependents };
}
function collectDownstream(addr, dependents) {
const result = new Set();
const queue = [addr];
const visited = new Set();
while (queue.length > 0) {
const current = queue.shift();
if (visited.has(current))
continue;
visited.add(current);
const deps = dependents.get(current);
if (deps) {
for (const dep of deps) {
if (!result.has(dep)) {
result.add(dep);
queue.push(dep);
}
}
}
}
return result;
}
function topoSort(addresses, dependsOn) {
const result = [];
const inDegree = new Map();
const subDeps = new Map();
for (const addr of addresses) {
const deps = dependsOn.get(addr) || new Set();
const filtered = new Set();
for (const d of deps) {
if (addresses.has(d)) {
filtered.add(d);
}
}
subDeps.set(addr, filtered);
inDegree.set(addr, filtered.size);
}
const queue = [];
for (const [addr, deg] of inDegree) {
if (deg === 0)
queue.push(addr);
}
while (queue.length > 0) {
const current = queue.shift();
result.push(current);
for (const addr of addresses) {
const deps = subDeps.get(addr);
if (deps && deps.has(current)) {
deps.delete(current);
inDegree.set(addr, inDegree.get(addr) - 1);
if (inDegree.get(addr) === 0) {
queue.push(addr);
}
}
}
}
for (const addr of addresses) {
if (!result.includes(addr)) {
result.push(addr);
}
}
return result;
}
function detectCycles(addresses, dependsOn) {
const cycleCells = new Set();
const WHITE = 0, GRAY = 1, BLACK = 2;
const color = new Map();
for (const addr of addresses) {
color.set(addr, WHITE);
}
const dfs = (node, path) => {
color.set(node, GRAY);
path.push(node);
const deps = dependsOn.get(node);
if (deps) {
for (const dep of deps) {
if (!addresses.has(dep))
continue;
const c = color.get(dep);
if (c === GRAY) {
const cycleStart = path.indexOf(dep);
for (let i = cycleStart; i < path.length; i++) {
cycleCells.add(path[i]);
}
return;
}
if (c === WHITE) {
dfs(dep, path);
}
}
}
path.pop();
color.set(node, BLACK);
};
for (const addr of addresses) {
if (color.get(addr) === WHITE) {
dfs(addr, []);
}
}
return cycleCells;
}
function recalcCell(r, c) {
const cell = state_1.cellGrid[r][c];
if (!cell || !cell.raw.startsWith('='))
return false;
// Skip already-marked #REF! errors (from row/col deletion)
if (cell.kind === 'error' && cell.error === '#REF!')
return false;
const parsed = (0, evaluator_1.parseRaw)(cell.raw);
if (parsed.error) {
cell.kind = 'error';
cell.value = parsed.error;
cell.error = parsed.error;
return true;
}
if (!parsed.expr) {
cell.kind = 'text';
cell.value = cell.raw;
return true;
}
const result = (0, evaluator_1.evaluate)(parsed.expr, c, r);
const changed = cell.value !== result.value || cell.kind !== result.kind;
cell.kind = result.kind;
cell.value = result.value;
cell.error = result.error;
return changed;
}
// Main recalculation: update a cell and propagate changes to dependents
// Returns list of addresses that were updated (including the source)
function updateCell(r, c, raw) {
if (!(0, state_1.inBounds)(r, c))
return { updated: [] };
const updated = [];
const addr = (0, utils_1.cellAddress)(c, r);
// Set the cell
(0, evaluator_1.setCell)(r, c, raw);
updated.push(addr);
// Evaluate this cell if it's a formula
const cell = state_1.cellGrid[r][c];
if (cell.kind === 'formula' && cell.raw.startsWith('=')) {
const parsed = (0, evaluator_1.parseRaw)(cell.raw);
if (parsed.error) {
cell.kind = 'error';
cell.value = parsed.error;
cell.error = parsed.error;
}
else if (parsed.expr) {
const result = (0, evaluator_1.evaluate)(parsed.expr, c, r);
cell.kind = result.kind;
cell.value = result.value;
cell.error = result.error;
}
}
// Rebuild dependency graph
const { dependsOn, dependents } = buildDepGraph();
// Collect all downstream dependents
const downstream = collectDownstream(addr, dependents);
if (downstream.size === 0)
return { updated };
// Detect cycles among affected cells
const allAffected = new Set(downstream);
allAffected.add(addr);
const cycles = detectCycles(allAffected, dependsOn);
// Topo sort non-cycle cells
const toSort = new Set();
for (const a of allAffected) {
if (!cycles.has(a) && a !== addr)
toSort.add(a); // skip source (already evaluated)
}
const order = topoSort(toSort, dependsOn);
// Recalculate in topological order
for (const a of order) {
if (typeof a !== 'string' || !a)
continue;
const ref = (0, utils_1.parseCellRef)(a);
if (ref && (0, state_1.inBounds)(ref.row, ref.col)) {
recalcCell(ref.row, ref.col);
updated.push(a);
}
}
// Mark cycle cells
for (const a of cycles) {
if (typeof a !== 'string' || !a)
continue;
const ref = (0, utils_1.parseCellRef)(a);
if (ref && (0, state_1.inBounds)(ref.row, ref.col)) {
const cell = state_1.cellGrid[ref.row][ref.col];
if (cell && cell.raw.startsWith('=')) {
cell.kind = 'error';
cell.value = types_1.ERR_CYCLE;
cell.error = types_1.ERR_CYCLE;
}
updated.push(a);
}
}
return { updated };
}
// Full recalculation of all formulas (used on load or full re-render)
function recalcAll() {
const { dependsOn, dependents } = buildDepGraph();
const updated = [];
// Collect all formula cells
const formulaCells = new Set();
for (let r = 0; r < (0, state_1.getGridRowCount)(); r++) {
for (let c = 0; c < (0, state_1.getGridColCount)(); c++) {
const cell = state_1.cellGrid[r][c];
if (cell && cell.raw.startsWith('=')) {
formulaCells.add((0, utils_1.cellAddress)(c, r));
}
}
}
if (formulaCells.size === 0)
return updated;
const cycles = detectCycles(formulaCells, dependsOn);
const toSort = new Set();
for (const a of formulaCells) {
if (!cycles.has(a))
toSort.add(a);
}
const order = topoSort(toSort, dependsOn);
for (const a of order) {
const ref = (0, utils_1.parseCellRef)(a);
if (ref && (0, state_1.inBounds)(ref.row, ref.col)) {
recalcCell(ref.row, ref.col);
updated.push(a);
}
}
for (const a of cycles) {
const ref = (0, utils_1.parseCellRef)(a);
if (ref && (0, state_1.inBounds)(ref.row, ref.col)) {
const cell = state_1.cellGrid[ref.row][ref.col];
if (cell && cell.raw.startsWith('=')) {
cell.kind = 'error';
cell.value = types_1.ERR_CYCLE;
cell.error = types_1.ERR_CYCLE;
}
updated.push(a);
}
}
return updated;
}
};
// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initGrid = initGrid;
exports.updateCellAt = updateCellAt;
exports.updateSelection = updateSelection;
exports.setFormulaBarValue = setFormulaBarValue;
exports.getFormulaBarValue = getFormulaBarValue;
exports.rerenderAll = rerenderAll;
exports.rerenderHeaders = rerenderHeaders;
const state_1 = require("./state");
const types_1 = require("./types");
const utils_1 = require("./utils");
let table = null;
let tbody = null;
let thead = null;
let cellMap = new Map();
function initGrid() {
(0, state_1.initOrder)();
table = document.getElementById('grid');
if (!table)
return;
table.innerHTML = '';
thead = document.createElement('thead');
tbody = document.createElement('tbody');
table.appendChild(thead);
table.appendChild(tbody);
cellMap.clear();
renderHeaders();
renderRows();
updateSelection();
}
function renderHeaders() {
if (!thead)
return;
thead.innerHTML = '';
const tr = document.createElement('tr');
// Corner cell
const cornerTh = document.createElement('th');
cornerTh.className = 'corner';
cornerTh.style.width = `${types_1.ROW_HEADER_WIDTH}px`;
cornerTh.style.height = `${types_1.COL_HEADER_HEIGHT}px`;
tr.appendChild(cornerTh);
// Column headers
const cCount = (0, state_1.getGridColCount)();
for (let c = 0; c < cCount; c++) {
const th = document.createElement('th');
th.textContent = (0, utils_1.colLabel)(c);
th.style.width = `${types_1.COL_WIDTH}px`;
th.style.height = `${types_1.COL_HEADER_HEIGHT}px`;
th.dataset.col = c.toString();
th.addEventListener('contextmenu', (e) => {
e.preventDefault();
window.__spreadshowContextMenu(e.clientX, e.clientY, 'col', c);
});
tr.appendChild(th);
}
thead.appendChild(tr);
}
function renderRows() {
if (!tbody)
return;
tbody.innerHTML = '';
cellMap.clear();
const rCount = (0, state_1.getGridRowCount)();
const cCount = (0, state_1.getGridColCount)();
for (let r = 0; r < rCount; r++) {
const tr = document.createElement('tr');
// Row header
const th = document.createElement('th');
th.className = 'row-header';
th.textContent = (r + 1).toString();
th.style.width = `${types_1.ROW_HEADER_WIDTH}px`;
th.style.height = `${types_1.ROW_HEIGHT}px`;
th.style.minWidth = `${types_1.ROW_HEADER_WIDTH}px`;
th.dataset.row = r.toString();
th.addEventListener('contextmenu', (e) => {
e.preventDefault();
window.__spreadshowContextMenu(e.clientX, e.clientY, 'row', r);
});
tr.appendChild(th);
// Data cells
for (let c = 0; c < cCount; c++) {
const td = document.createElement('td');
td.className = 'cell';
td.dataset.row = r.toString();
td.dataset.col = c.toString();
td.style.width = `${types_1.COL_WIDTH}px`;
td.style.height = `${types_1.ROW_HEIGHT}px`;
updateCellDisplay(td, r, c);
tr.appendChild(td);
cellMap.set(`${c},${r}`, td);
}
tbody.appendChild(tr);
}
}
function updateCellDisplay(td, r, c) {
const cell = state_1.cellGrid[r]?.[c];
if (!cell) {
td.textContent = '';
td.className = 'cell';
return;
}
td.className = 'cell';
if (cell.raw === '') {
td.textContent = '';
return;
}
if (cell.kind === 'error') {
td.textContent = cell.value;
td.classList.add('error-val');
return;
}
if (cell.kind === 'number') {
td.textContent = (0, utils_1.formatNumber)(cell.value);
td.classList.add('number-val');
return;
}
// text
td.textContent = cell.value;
td.classList.add('text-val');
}
function updateCellAt(r, c) {
const td = cellMap.get(`${c},${r}`);
if (td) {
updateCellDisplay(td, r, c);
}
}
function updateSelection() {
// Remove old selection
const prev = table?.querySelector('.selected');
if (prev)
prev.classList.remove('selected');
// Add new selection
const td = cellMap.get(`${state_1.selCol},${state_1.selRow}`);
if (td) {
td.classList.add('selected');
// Scroll into view
td.scrollIntoView({ block: 'nearest', inline: 'nearest' });
}
// Update formula bar cell ref
const refEl = document.getElementById('cell-ref');
if (refEl) {
refEl.textContent = (0, utils_1.colLabel)(state_1.selCol) + (state_1.selRow + 1);
}
// Update formula bar content
updateFormulaBar();
}
function updateFormulaBar() {
const input = document.getElementById('formula-input');
if (!input)
return;
const cell = state_1.cellGrid[state_1.selRow]?.[state_1.selCol];
if (!cell) {
input.value = '';
return;
}
// Formula bar always shows raw content
input.value = cell.raw;
}
function setFormulaBarValue(val) {
const input = document.getElementById('formula-input');
if (input)
input.value = val;
}
function getFormulaBarValue() {
const input = document.getElementById('formula-input');
return input ? input.value : '';
}
function rerenderAll() {
renderRows();
updateSelection();
}
function rerenderHeaders() {
renderHeaders();
renderRows();
updateSelection();
}
};
// ── module: src/editing.ts ──
__mods["src/editing.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.isEditing = isEditing;
exports.getEditState = getEditState;
exports.startEdit = startEdit;
exports.commitEdit = commitEdit;
exports.commitFromFormulaBar = commitFromFormulaBar;
exports.cancelEdit = cancelEdit;
exports.moveSelection = moveSelection;
exports.selectCell = selectCell;
exports.clearCell = clearCell;
const state_1 = require("./state");
const engine_1 = require("./engine");
const render_1 = require("./render");
let editingCell = false;
let editStartValue = '';
let _selCol = 0;
let _selRow = 0;
// Mirror selection from state
function isEditing() {
return editingCell;
}
function getEditState() {
return { col: _selCol, row: _selRow, startValue: editStartValue };
}
// Start editing the current cell (either in-cell or formula bar)
function startEdit() {
if (editingCell)
return;
editingCell = true;
const s = (0, state_1.getSelection)();
_selCol = s.col;
_selRow = s.row;
editStartValue = (0, state_1.getCellRaw)(_selRow, _selCol);
const input = document.getElementById('formula-input');
if (input) {
input.value = editStartValue;
input.select();
input.focus();
}
}
// Commit the current edit; returns updated addresses
function commitEdit() {
if (!editingCell)
return [];
editingCell = false;
const value = (0, render_1.getFormulaBarValue)();
(0, state_1.setCellRaw)(_selRow, _selCol, value);
const result = (0, engine_1.updateCell)(_selRow, _selCol, value);
(0, render_1.updateSelection)();
return result.updated;
}
// Commit edit from formula bar (even if not in explicit edit mode); returns updated addresses
function commitFromFormulaBar() {
const value = (0, render_1.getFormulaBarValue)();
const s = (0, state_1.getSelection)();
const col = s.col;
const row = s.row;
(0, state_1.setCellRaw)(row, col, value);
const result = (0, engine_1.updateCell)(row, col, value);
(0, render_1.updateSelection)();
return result.updated;
}
// Cancel the current edit (restore previous value)
function cancelEdit() {
if (!editingCell)
return;
editingCell = false;
const input = document.getElementById('formula-input');
if (input) {
input.value = editStartValue;
}
}
// Move selection
function moveSelection(dcol, drow) {
const { col, row } = (0, state_1.getSelection)();
const newCol = Math.max(0, Math.min((0, state_1.getGridColCount)() - 1, col + dcol));
const newRow = Math.max(0, Math.min((0, state_1.getGridRowCount)() - 1, row + drow));
(0, state_1.setSelection)(newCol, newRow);
(0, render_1.updateSelection)();
}
function selectCell(col, row) {
if (col >= 0 && col < (0, state_1.getGridColCount)() && row >= 0 && row < (0, state_1.getGridRowCount)()) {
(0, state_1.setSelection)(col, row);
(0, render_1.updateSelection)();
}
}
// Clear the current cell
function clearCell() {
const s = (0, state_1.getSelection)();
(0, state_1.setCellRaw)(s.row, s.col, '');
(0, engine_1.updateCell)(s.row, s.col, '');
(0, render_1.updateCellAt)(s.row, s.col);
(0, render_1.updateSelection)();
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.6-27B-MTP-GGUF-UD-Q5_K_XL","provider":"brigid","persona":"typescript","sessionId":"ed3f868c-ab17-421e-b89d-f44458df4295","tokensIn":8575979,"tokensOut":96487,"tokensTotal":8672466,"cost":0,"turns":155,"toolCalls":154,"failedToolCalls":6,"timestamp":"2026-08-04T09:59:24.958Z"} -->