@pixerate/editor 0.2.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -809,6 +809,7 @@ var FontSize = Extension5.create({
809
809
  });
810
810
 
811
811
  // src/extensions/rich-text-preset.ts
812
+ import { wrappingInputRule, markInputRule } from "@tiptap/core";
812
813
  import StarterKit from "@tiptap/starter-kit";
813
814
  import Color from "@tiptap/extension-color";
814
815
  import Highlight from "@tiptap/extension-highlight";
@@ -821,6 +822,52 @@ import Typography from "@tiptap/extension-typography";
821
822
  import Underline from "@tiptap/extension-underline";
822
823
  import Placeholder from "@tiptap/extension-placeholder";
823
824
  import CharacterCount from "@tiptap/extension-character-count";
825
+
826
+ // src/extensions/mention.ts
827
+ import MentionExtension from "@tiptap/extension-mention";
828
+ var Mention = MentionExtension.extend({
829
+ addAttributes() {
830
+ return {
831
+ ...this.parent?.(),
832
+ type: {
833
+ default: null,
834
+ parseHTML: (element) => element.getAttribute("data-type-name") || element.getAttribute("data-entity-type"),
835
+ renderHTML: (attributes) => {
836
+ if (!attributes.type) return {};
837
+ return { "data-type-name": attributes.type };
838
+ }
839
+ },
840
+ avatarUrl: {
841
+ default: null,
842
+ parseHTML: (element) => element.getAttribute("data-avatar-url"),
843
+ renderHTML: (attributes) => {
844
+ if (!attributes.avatarUrl) return {};
845
+ return { "data-avatar-url": attributes.avatarUrl };
846
+ }
847
+ },
848
+ color: {
849
+ default: null,
850
+ parseHTML: (element) => element.getAttribute("data-color"),
851
+ renderHTML: (attributes) => {
852
+ if (!attributes.color) return {};
853
+ return { "data-color": attributes.color };
854
+ }
855
+ }
856
+ };
857
+ }
858
+ });
859
+ function createMentionExtension(options = {}) {
860
+ const { HTMLAttributes = {}, ...rest } = options;
861
+ return Mention.configure({
862
+ HTMLAttributes: {
863
+ class: "pixerate-mention-node",
864
+ ...HTMLAttributes
865
+ },
866
+ ...rest
867
+ });
868
+ }
869
+
870
+ // src/extensions/rich-text-preset.ts
824
871
  var TextStyle2 = _TextStyle.TextStyle || _TextStyle.default || _TextStyle;
825
872
  function createRichTextPreset(options = {}) {
826
873
  const {
@@ -858,7 +905,19 @@ function createRichTextPreset(options = {}) {
858
905
  TextAlign.configure({
859
906
  types: ["heading", "paragraph"]
860
907
  }),
861
- Link.configure({
908
+ Link.extend({
909
+ addInputRules() {
910
+ return [
911
+ markInputRule({
912
+ find: /(?:^|\s)\[([^\]]+)\]\(([^)]+)\)$/,
913
+ type: this.type,
914
+ getAttributes: (match) => ({
915
+ href: match[2]
916
+ })
917
+ })
918
+ ];
919
+ }
920
+ }).configure({
862
921
  openOnClick: openLinksOnClick,
863
922
  autolink: true,
864
923
  defaultProtocol: "https",
@@ -869,7 +928,19 @@ function createRichTextPreset(options = {}) {
869
928
  }
870
929
  }),
871
930
  TaskList,
872
- TaskItem.configure({
931
+ TaskItem.extend({
932
+ addInputRules() {
933
+ return [
934
+ wrappingInputRule({
935
+ find: /^\s*(\[([ |x])?\])\s$/i,
936
+ type: this.type,
937
+ getAttributes: (match) => ({
938
+ checked: match[2]?.toLowerCase() === "x"
939
+ })
940
+ })
941
+ ];
942
+ }
943
+ }).configure({
873
944
  nested: true
874
945
  }),
875
946
  FontSize,
@@ -891,6 +962,18 @@ function createRichTextPreset(options = {}) {
891
962
  if (enableColorHighlighter) {
892
963
  extensions.push(ColorHighlighter);
893
964
  }
965
+ if (options.mention) {
966
+ const mentionConfig = typeof options.mention === "object" ? options.mention : {};
967
+ extensions.push(
968
+ Mention.configure({
969
+ HTMLAttributes: {
970
+ class: "pixerate-mention-node",
971
+ ...mentionConfig.HTMLAttributes || {}
972
+ },
973
+ ...mentionConfig
974
+ })
975
+ );
976
+ }
894
977
  return extensions;
895
978
  }
896
979
 
@@ -908,9 +991,16 @@ function plainTextToTipTapHtml(text) {
908
991
  }
909
992
  function htmlToPlainText(html) {
910
993
  if (!html) return "";
994
+ const mentionNormalized = html.replace(
995
+ /<span[^>]*data-type=["']mention["'][^>]*data-label=["']([^"']+)["'][^>]*>.*?<\/span>/gi,
996
+ "@$1"
997
+ ).replace(
998
+ /<span[^>]*data-type=["']mention["'][^>]*data-id=["']([^"']+)["'][^>]*>.*?<\/span>/gi,
999
+ "@$1"
1000
+ );
911
1001
  if (typeof document !== "undefined") {
912
1002
  const tempDiv = document.createElement("div");
913
- tempDiv.innerHTML = html;
1003
+ tempDiv.innerHTML = mentionNormalized;
914
1004
  tempDiv.querySelectorAll("br.ProseMirror-trailingBreak").forEach((br) => br.remove());
915
1005
  tempDiv.querySelectorAll("br").forEach((br) => br.replaceWith("\n"));
916
1006
  const paragraphs = tempDiv.querySelectorAll("p, div, li, h1, h2, h3, h4, h5, h6");
@@ -923,7 +1013,7 @@ function htmlToPlainText(html) {
923
1013
  }
924
1014
  return tempDiv.textContent || "";
925
1015
  }
926
- return html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<\/div>/gi, "\n").replace(/<\/li>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#039;/g, "'").trim();
1016
+ return mentionNormalized.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<\/div>/gi, "\n").replace(/<\/li>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#039;/g, "'").trim();
927
1017
  }
928
1018
  function getEditorText(editor) {
929
1019
  if (!editor) return "";
@@ -940,6 +1030,80 @@ function getEditorText(editor) {
940
1030
  }
941
1031
  return "";
942
1032
  }
1033
+ function extractMentionsFromDoc(docOrEditor) {
1034
+ if (!docOrEditor) return [];
1035
+ const mentions = [];
1036
+ const doc = docOrEditor?.state?.doc || (typeof docOrEditor?.descendants === "function" ? docOrEditor : null);
1037
+ if (doc && typeof doc.descendants === "function") {
1038
+ doc.descendants((node) => {
1039
+ if (node.type?.name === "mention" && node.attrs) {
1040
+ mentions.push({
1041
+ id: node.attrs.id || "",
1042
+ label: node.attrs.label || "",
1043
+ type: node.attrs.type,
1044
+ avatarUrl: node.attrs.avatarUrl,
1045
+ color: node.attrs.color,
1046
+ ...node.attrs
1047
+ });
1048
+ }
1049
+ });
1050
+ return mentions;
1051
+ }
1052
+ if (typeof docOrEditor === "object" && docOrEditor.content) {
1053
+ const walk = (nodes) => {
1054
+ for (const node of nodes) {
1055
+ if (node.type === "mention" && node.attrs) {
1056
+ mentions.push({
1057
+ id: node.attrs.id || "",
1058
+ label: node.attrs.label || "",
1059
+ type: node.attrs.type,
1060
+ avatarUrl: node.attrs.avatarUrl,
1061
+ color: node.attrs.color,
1062
+ ...node.attrs
1063
+ });
1064
+ }
1065
+ if (Array.isArray(node.content)) {
1066
+ walk(node.content);
1067
+ }
1068
+ }
1069
+ };
1070
+ walk(docOrEditor.content);
1071
+ return mentions;
1072
+ }
1073
+ if (typeof docOrEditor === "string") {
1074
+ if (typeof document !== "undefined") {
1075
+ const div = document.createElement("div");
1076
+ div.innerHTML = docOrEditor;
1077
+ div.querySelectorAll('span[data-type="mention"]').forEach((el) => {
1078
+ mentions.push({
1079
+ id: el.getAttribute("data-id") || "",
1080
+ label: el.getAttribute("data-label") || "",
1081
+ type: el.getAttribute("data-type-name") || void 0,
1082
+ avatarUrl: el.getAttribute("data-avatar-url") || void 0,
1083
+ color: el.getAttribute("data-color") || void 0
1084
+ });
1085
+ });
1086
+ } else {
1087
+ const spanRegex = /<span([^>]*data-type=["']mention["'][^>]*)>/gi;
1088
+ let match;
1089
+ while ((match = spanRegex.exec(docOrEditor)) !== null) {
1090
+ const attrStr = match[1];
1091
+ const getAttr = (name) => {
1092
+ const m = new RegExp(`${name}=["']([^"']*)["']`, "i").exec(attrStr);
1093
+ return m ? m[1] : void 0;
1094
+ };
1095
+ mentions.push({
1096
+ id: getAttr("data-id") || "",
1097
+ label: getAttr("data-label") || "",
1098
+ type: getAttr("data-type-name") || getAttr("data-entity-type"),
1099
+ avatarUrl: getAttr("data-avatar-url"),
1100
+ color: getAttr("data-color")
1101
+ });
1102
+ }
1103
+ }
1104
+ }
1105
+ return mentions;
1106
+ }
943
1107
 
944
1108
  // src/controller.ts
945
1109
  import { Editor } from "@tiptap/core";
@@ -1048,36 +1212,1296 @@ var EditorController = class {
1048
1212
  function createEditor(options = {}) {
1049
1213
  return new EditorController(options);
1050
1214
  }
1215
+
1216
+ // src/spreadsheet/formula/lexer.ts
1217
+ function tokenizeFormula(formula) {
1218
+ const tokens = [];
1219
+ let input = formula.trim();
1220
+ let offset = 0;
1221
+ if (input.startsWith("=")) {
1222
+ input = input.slice(1);
1223
+ offset = 1;
1224
+ }
1225
+ let i = 0;
1226
+ while (i < input.length) {
1227
+ const char = input[i];
1228
+ if (/\s/.test(char)) {
1229
+ i++;
1230
+ continue;
1231
+ }
1232
+ if (/\d/.test(char) || char === "." && i + 1 < input.length && /\d/.test(input[i + 1])) {
1233
+ const start = i;
1234
+ let hasDot = char === ".";
1235
+ i++;
1236
+ while (i < input.length && (/\d/.test(input[i]) || input[i] === "." && !hasDot)) {
1237
+ if (input[i] === ".") hasDot = true;
1238
+ i++;
1239
+ }
1240
+ tokens.push({
1241
+ type: "NUMBER",
1242
+ value: input.slice(start, i),
1243
+ position: start + offset
1244
+ });
1245
+ continue;
1246
+ }
1247
+ if (char === '"' || char === "'") {
1248
+ const quote = char;
1249
+ const start = i;
1250
+ i++;
1251
+ let str = "";
1252
+ while (i < input.length && input[i] !== quote) {
1253
+ if (input[i] === "\\" && i + 1 < input.length) {
1254
+ i++;
1255
+ }
1256
+ str += input[i];
1257
+ i++;
1258
+ }
1259
+ if (i < input.length && input[i] === quote) {
1260
+ i++;
1261
+ }
1262
+ tokens.push({
1263
+ type: "STRING",
1264
+ value: str,
1265
+ position: start + offset
1266
+ });
1267
+ continue;
1268
+ }
1269
+ if (char === "[") {
1270
+ const start = i;
1271
+ i++;
1272
+ let prop = "";
1273
+ while (i < input.length && input[i] !== "]") {
1274
+ prop += input[i];
1275
+ i++;
1276
+ }
1277
+ if (i < input.length && input[i] === "]") {
1278
+ i++;
1279
+ }
1280
+ tokens.push({
1281
+ type: "PROPERTY_REF",
1282
+ value: prop.trim(),
1283
+ position: start + offset
1284
+ });
1285
+ continue;
1286
+ }
1287
+ if (char === "(") {
1288
+ tokens.push({ type: "LPAREN", value: "(", position: i + offset });
1289
+ i++;
1290
+ continue;
1291
+ }
1292
+ if (char === ")") {
1293
+ tokens.push({ type: "RPAREN", value: ")", position: i + offset });
1294
+ i++;
1295
+ continue;
1296
+ }
1297
+ if (char === ",") {
1298
+ tokens.push({ type: "COMMA", value: ",", position: i + offset });
1299
+ i++;
1300
+ continue;
1301
+ }
1302
+ const twoChars = input.slice(i, i + 2);
1303
+ if (["<=", ">=", "<>", "!="].includes(twoChars)) {
1304
+ tokens.push({
1305
+ type: "OPERATOR",
1306
+ value: twoChars === "!=" ? "<>" : twoChars,
1307
+ position: i + offset
1308
+ });
1309
+ i += 2;
1310
+ continue;
1311
+ }
1312
+ if (["+", "-", "*", "/", "^", "&", "=", "<", ">"].includes(char)) {
1313
+ tokens.push({ type: "OPERATOR", value: char, position: i + offset });
1314
+ i++;
1315
+ continue;
1316
+ }
1317
+ if (/[a-zA-Z_$]/.test(char)) {
1318
+ const start = i;
1319
+ while (i < input.length && /[a-zA-Z0-9_$.]/.test(input[i])) {
1320
+ i++;
1321
+ }
1322
+ const rawWord = input.slice(start, i);
1323
+ if (i < input.length && input[i] === ":") {
1324
+ const colonPos = i;
1325
+ i++;
1326
+ const secondStart = i;
1327
+ while (i < input.length && /[a-zA-Z0-9_$.]/.test(input[i])) {
1328
+ i++;
1329
+ }
1330
+ const secondWord = input.slice(secondStart, i);
1331
+ if (secondWord.length > 0) {
1332
+ tokens.push({
1333
+ type: "RANGE",
1334
+ value: `${rawWord}:${secondWord}`.toUpperCase(),
1335
+ position: start + offset
1336
+ });
1337
+ continue;
1338
+ } else {
1339
+ i = colonPos;
1340
+ }
1341
+ }
1342
+ const upper = rawWord.toUpperCase();
1343
+ if (upper === "TRUE" || upper === "FALSE") {
1344
+ tokens.push({
1345
+ type: "BOOLEAN",
1346
+ value: upper,
1347
+ position: start + offset
1348
+ });
1349
+ continue;
1350
+ }
1351
+ if (/^\$?[A-Za-z]+\$?[0-9]+$/.test(rawWord)) {
1352
+ tokens.push({
1353
+ type: "CELL_REF",
1354
+ value: upper.replace(/\$/g, ""),
1355
+ position: start + offset
1356
+ });
1357
+ continue;
1358
+ }
1359
+ tokens.push({
1360
+ type: "IDENTIFIER",
1361
+ value: upper,
1362
+ position: start + offset
1363
+ });
1364
+ continue;
1365
+ }
1366
+ i++;
1367
+ }
1368
+ tokens.push({ type: "EOF", value: "", position: input.length + offset });
1369
+ return tokens;
1370
+ }
1371
+
1372
+ // src/spreadsheet/formula/parser.ts
1373
+ function parseCellReference(ref) {
1374
+ const match = ref.match(/^([A-Za-z]+)(\d+)$/);
1375
+ if (!match) {
1376
+ throw new Error(`Invalid cell reference: ${ref}`);
1377
+ }
1378
+ return {
1379
+ col: match[1].toUpperCase(),
1380
+ row: parseInt(match[2], 10)
1381
+ };
1382
+ }
1383
+ function parseRange(rangeStr) {
1384
+ const [start, end] = rangeStr.split(":");
1385
+ const startRef = parseCellReference(start);
1386
+ const endRef = parseCellReference(end);
1387
+ return {
1388
+ type: "Range",
1389
+ raw: rangeStr.toUpperCase(),
1390
+ startCol: startRef.col,
1391
+ startRow: startRef.row,
1392
+ endCol: endRef.col,
1393
+ endRow: endRef.row
1394
+ };
1395
+ }
1396
+ var FormulaParser = class {
1397
+ tokens = [];
1398
+ current = 0;
1399
+ constructor(tokens) {
1400
+ this.tokens = tokens;
1401
+ }
1402
+ parse() {
1403
+ if (this.peek().type === "EOF") {
1404
+ return { type: "Literal", value: "" };
1405
+ }
1406
+ const expr = this.expression();
1407
+ if (this.peek().type !== "EOF") {
1408
+ throw new Error(`Unexpected token at position ${this.peek().position}: ${this.peek().value}`);
1409
+ }
1410
+ return expr;
1411
+ }
1412
+ expression() {
1413
+ return this.comparison();
1414
+ }
1415
+ comparison() {
1416
+ let expr = this.concatenation();
1417
+ while (this.matchOperator("=", "<>", "<", ">", "<=", ">=")) {
1418
+ const op = this.previous().value;
1419
+ const right = this.concatenation();
1420
+ expr = { type: "BinaryOp", op, left: expr, right };
1421
+ }
1422
+ return expr;
1423
+ }
1424
+ concatenation() {
1425
+ let expr = this.addition();
1426
+ while (this.matchOperator("&")) {
1427
+ const op = this.previous().value;
1428
+ const right = this.addition();
1429
+ expr = { type: "BinaryOp", op, left: expr, right };
1430
+ }
1431
+ return expr;
1432
+ }
1433
+ addition() {
1434
+ let expr = this.multiplication();
1435
+ while (this.matchOperator("+", "-")) {
1436
+ const op = this.previous().value;
1437
+ const right = this.multiplication();
1438
+ expr = { type: "BinaryOp", op, left: expr, right };
1439
+ }
1440
+ return expr;
1441
+ }
1442
+ multiplication() {
1443
+ let expr = this.exponentiation();
1444
+ while (this.matchOperator("*", "/")) {
1445
+ const op = this.previous().value;
1446
+ const right = this.exponentiation();
1447
+ expr = { type: "BinaryOp", op, left: expr, right };
1448
+ }
1449
+ return expr;
1450
+ }
1451
+ exponentiation() {
1452
+ let expr = this.unary();
1453
+ while (this.matchOperator("^")) {
1454
+ const op = this.previous().value;
1455
+ const right = this.unary();
1456
+ expr = { type: "BinaryOp", op, left: expr, right };
1457
+ }
1458
+ return expr;
1459
+ }
1460
+ unary() {
1461
+ if (this.matchOperator("-", "+")) {
1462
+ const op = this.previous().value;
1463
+ const argument = this.unary();
1464
+ return { type: "UnaryOp", op, argument };
1465
+ }
1466
+ return this.primary();
1467
+ }
1468
+ primary() {
1469
+ const token = this.peek();
1470
+ if (token.type === "NUMBER") {
1471
+ this.advance();
1472
+ return { type: "Literal", value: parseFloat(token.value) };
1473
+ }
1474
+ if (token.type === "STRING") {
1475
+ this.advance();
1476
+ return { type: "Literal", value: token.value };
1477
+ }
1478
+ if (token.type === "BOOLEAN") {
1479
+ this.advance();
1480
+ return { type: "Literal", value: token.value === "TRUE" };
1481
+ }
1482
+ if (token.type === "CELL_REF") {
1483
+ this.advance();
1484
+ const parsed = parseCellReference(token.value);
1485
+ return {
1486
+ type: "CellRef",
1487
+ ref: token.value,
1488
+ col: parsed.col,
1489
+ row: parsed.row
1490
+ };
1491
+ }
1492
+ if (token.type === "RANGE") {
1493
+ this.advance();
1494
+ return parseRange(token.value);
1495
+ }
1496
+ if (token.type === "PROPERTY_REF") {
1497
+ this.advance();
1498
+ return {
1499
+ type: "PropertyRef",
1500
+ property: token.value
1501
+ };
1502
+ }
1503
+ if (token.type === "IDENTIFIER") {
1504
+ const name = token.value;
1505
+ this.advance();
1506
+ if (this.check("LPAREN")) {
1507
+ this.consume("LPAREN", `Expected '(' after function ${name}`);
1508
+ const args = [];
1509
+ if (!this.check("RPAREN")) {
1510
+ do {
1511
+ args.push(this.expression());
1512
+ } while (this.match("COMMA"));
1513
+ }
1514
+ this.consume("RPAREN", `Expected ')' after function arguments`);
1515
+ return {
1516
+ type: "FunctionCall",
1517
+ name,
1518
+ args
1519
+ };
1520
+ }
1521
+ return { type: "Literal", value: name };
1522
+ }
1523
+ if (this.match("LPAREN")) {
1524
+ const expr = this.expression();
1525
+ this.consume("RPAREN", `Expected ')' after expression`);
1526
+ return expr;
1527
+ }
1528
+ throw new Error(`Unexpected token at position ${token.position}: ${token.value || token.type}`);
1529
+ }
1530
+ match(...types) {
1531
+ for (const type of types) {
1532
+ if (this.check(type)) {
1533
+ this.advance();
1534
+ return true;
1535
+ }
1536
+ }
1537
+ return false;
1538
+ }
1539
+ matchOperator(...ops) {
1540
+ if (this.check("OPERATOR") && ops.includes(this.peek().value)) {
1541
+ this.advance();
1542
+ return true;
1543
+ }
1544
+ return false;
1545
+ }
1546
+ check(type) {
1547
+ if (this.isAtEnd()) return false;
1548
+ return this.peek().type === type;
1549
+ }
1550
+ advance() {
1551
+ if (!this.isAtEnd()) this.current++;
1552
+ return this.previous();
1553
+ }
1554
+ isAtEnd() {
1555
+ return this.peek().type === "EOF";
1556
+ }
1557
+ peek() {
1558
+ return this.tokens[this.current];
1559
+ }
1560
+ previous() {
1561
+ return this.tokens[this.current - 1];
1562
+ }
1563
+ consume(type, message) {
1564
+ if (this.check(type)) return this.advance();
1565
+ throw new Error(message);
1566
+ }
1567
+ };
1568
+ function parseFormula(formula) {
1569
+ const tokens = tokenizeFormula(formula);
1570
+ const parser = new FormulaParser(tokens);
1571
+ return parser.parse();
1572
+ }
1573
+
1574
+ // src/spreadsheet/formula/evaluator.ts
1575
+ function colNameToIndex(col) {
1576
+ let index = 0;
1577
+ const upper = col.toUpperCase();
1578
+ for (let i = 0; i < upper.length; i++) {
1579
+ index = index * 26 + (upper.charCodeAt(i) - 65 + 1);
1580
+ }
1581
+ return index - 1;
1582
+ }
1583
+ function indexToColName(index) {
1584
+ let num = index + 1;
1585
+ let name = "";
1586
+ while (num > 0) {
1587
+ const mod = (num - 1) % 26;
1588
+ name = String.fromCharCode(65 + mod) + name;
1589
+ num = Math.floor((num - mod) / 26);
1590
+ }
1591
+ return name;
1592
+ }
1593
+ var FormulaEvaluator = class {
1594
+ context;
1595
+ constructor(context = {}) {
1596
+ this.context = context;
1597
+ }
1598
+ evaluate(astOrFormula) {
1599
+ const ast = typeof astOrFormula === "string" ? parseFormula(astOrFormula) : astOrFormula;
1600
+ return this.evaluateNode(ast);
1601
+ }
1602
+ evaluateNode(node) {
1603
+ switch (node.type) {
1604
+ case "Literal":
1605
+ return node.value;
1606
+ case "CellRef": {
1607
+ if (!this.context.resolveCell) {
1608
+ throw new Error(`Cannot resolve cell reference ${node.ref}: no cell resolver provided`);
1609
+ }
1610
+ return this.context.resolveCell(node.col, node.row);
1611
+ }
1612
+ case "Range": {
1613
+ if (!this.context.resolveRange) {
1614
+ throw new Error(`Cannot resolve range ${node.raw}: no range resolver provided`);
1615
+ }
1616
+ return this.context.resolveRange(node.startCol, node.startRow, node.endCol, node.endRow);
1617
+ }
1618
+ case "PropertyRef": {
1619
+ if (!this.context.resolveProperty) {
1620
+ throw new Error(`Cannot resolve property [${node.property}]: no property resolver provided`);
1621
+ }
1622
+ return this.context.resolveProperty(node.property);
1623
+ }
1624
+ case "UnaryOp": {
1625
+ const val = this.evaluateNode(node.argument);
1626
+ if (node.op === "-") return -Number(val);
1627
+ if (node.op === "+") return +Number(val);
1628
+ throw new Error(`Unsupported unary operator: ${node.op}`);
1629
+ }
1630
+ case "BinaryOp": {
1631
+ const left = this.evaluateNode(node.left);
1632
+ const right = this.evaluateNode(node.right);
1633
+ switch (node.op) {
1634
+ case "+":
1635
+ return Number(left) + Number(right);
1636
+ case "-":
1637
+ return Number(left) - Number(right);
1638
+ case "*":
1639
+ return Number(left) * Number(right);
1640
+ case "/": {
1641
+ const denom = Number(right);
1642
+ if (denom === 0) return "#DIV/0!";
1643
+ return Number(left) / denom;
1644
+ }
1645
+ case "^":
1646
+ return Math.pow(Number(left), Number(right));
1647
+ case "&":
1648
+ return `${left ?? ""}${right ?? ""}`;
1649
+ case "=":
1650
+ return String(left).toLowerCase() === String(right).toLowerCase();
1651
+ case "<>":
1652
+ return String(left).toLowerCase() !== String(right).toLowerCase();
1653
+ case "<":
1654
+ return Number(left) < Number(right);
1655
+ case "<=":
1656
+ return Number(left) <= Number(right);
1657
+ case ">":
1658
+ return Number(left) > Number(right);
1659
+ case ">=":
1660
+ return Number(left) >= Number(right);
1661
+ default:
1662
+ throw new Error(`Unsupported binary operator: ${node.op}`);
1663
+ }
1664
+ }
1665
+ case "FunctionCall": {
1666
+ return this.callFunction(node.name, node.args);
1667
+ }
1668
+ default:
1669
+ throw new Error(`Unknown AST node: ${node.type}`);
1670
+ }
1671
+ }
1672
+ callFunction(name, args) {
1673
+ const fnName = name.toUpperCase();
1674
+ const evaluateAndFlatten = () => {
1675
+ const items = [];
1676
+ for (const arg of args) {
1677
+ const val = this.evaluateNode(arg);
1678
+ if (Array.isArray(val)) {
1679
+ items.push(...val);
1680
+ } else {
1681
+ items.push(val);
1682
+ }
1683
+ }
1684
+ return items;
1685
+ };
1686
+ switch (fnName) {
1687
+ case "SUM": {
1688
+ const items = evaluateAndFlatten();
1689
+ let total = 0;
1690
+ for (const item of items) {
1691
+ const num = Number(item);
1692
+ if (!isNaN(num)) total += num;
1693
+ }
1694
+ return total;
1695
+ }
1696
+ case "AVERAGE": {
1697
+ const items = evaluateAndFlatten();
1698
+ let total = 0;
1699
+ let count = 0;
1700
+ for (const item of items) {
1701
+ const num = Number(item);
1702
+ if (!isNaN(num) && item !== "" && item !== null && item !== void 0) {
1703
+ total += num;
1704
+ count++;
1705
+ }
1706
+ }
1707
+ return count === 0 ? 0 : total / count;
1708
+ }
1709
+ case "COUNT": {
1710
+ const items = evaluateAndFlatten();
1711
+ let count = 0;
1712
+ for (const item of items) {
1713
+ const num = Number(item);
1714
+ if (!isNaN(num) && item !== "" && item !== null && item !== void 0) {
1715
+ count++;
1716
+ }
1717
+ }
1718
+ return count;
1719
+ }
1720
+ case "COUNTA": {
1721
+ const items = evaluateAndFlatten();
1722
+ let count = 0;
1723
+ for (const item of items) {
1724
+ if (item !== "" && item !== null && item !== void 0) {
1725
+ count++;
1726
+ }
1727
+ }
1728
+ return count;
1729
+ }
1730
+ case "MIN": {
1731
+ const items = evaluateAndFlatten().map(Number).filter((n) => !isNaN(n));
1732
+ return items.length === 0 ? 0 : Math.min(...items);
1733
+ }
1734
+ case "MAX": {
1735
+ const items = evaluateAndFlatten().map(Number).filter((n) => !isNaN(n));
1736
+ return items.length === 0 ? 0 : Math.max(...items);
1737
+ }
1738
+ case "IF": {
1739
+ if (args.length < 2) throw new Error("IF requires at least 2 arguments");
1740
+ const condition = Boolean(this.evaluateNode(args[0]));
1741
+ if (condition) {
1742
+ return this.evaluateNode(args[1]);
1743
+ } else {
1744
+ return args.length > 2 ? this.evaluateNode(args[2]) : false;
1745
+ }
1746
+ }
1747
+ case "CONCAT": {
1748
+ const items = evaluateAndFlatten();
1749
+ return items.map((i) => i === null || i === void 0 ? "" : String(i)).join("");
1750
+ }
1751
+ case "UPPER": {
1752
+ if (args.length !== 1) throw new Error("UPPER requires 1 argument");
1753
+ const val = this.evaluateNode(args[0]);
1754
+ return String(val ?? "").toUpperCase();
1755
+ }
1756
+ case "LOWER": {
1757
+ if (args.length !== 1) throw new Error("LOWER requires 1 argument");
1758
+ const val = this.evaluateNode(args[0]);
1759
+ return String(val ?? "").toLowerCase();
1760
+ }
1761
+ case "TRIM": {
1762
+ if (args.length !== 1) throw new Error("TRIM requires 1 argument");
1763
+ const val = this.evaluateNode(args[0]);
1764
+ return String(val ?? "").trim();
1765
+ }
1766
+ case "LEN": {
1767
+ if (args.length !== 1) throw new Error("LEN requires 1 argument");
1768
+ const val = this.evaluateNode(args[0]);
1769
+ return String(val ?? "").length;
1770
+ }
1771
+ case "ROUND": {
1772
+ if (args.length < 1) throw new Error("ROUND requires at least 1 argument");
1773
+ const num = Number(this.evaluateNode(args[0]));
1774
+ const decimals = args.length > 1 ? Number(this.evaluateNode(args[1])) : 0;
1775
+ const factor = Math.pow(10, decimals);
1776
+ return Math.round(num * factor) / factor;
1777
+ }
1778
+ case "ABS": {
1779
+ if (args.length !== 1) throw new Error("ABS requires 1 argument");
1780
+ return Math.abs(Number(this.evaluateNode(args[0])));
1781
+ }
1782
+ case "TODAY": {
1783
+ return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1784
+ }
1785
+ case "DAYS": {
1786
+ if (args.length !== 2) throw new Error("DAYS requires 2 arguments (end_date, start_date)");
1787
+ const d1 = new Date(this.evaluateNode(args[0]));
1788
+ const d2 = new Date(this.evaluateNode(args[1]));
1789
+ const diffTime = d1.getTime() - d2.getTime();
1790
+ return Math.round(diffTime / (1e3 * 60 * 60 * 24));
1791
+ }
1792
+ default:
1793
+ throw new Error(`Unknown function: ${fnName}`);
1794
+ }
1795
+ }
1796
+ };
1797
+
1798
+ // src/spreadsheet/formula/dependency-graph.ts
1799
+ function extractCellDependencies(node) {
1800
+ const deps = [];
1801
+ const visit = (curr) => {
1802
+ if (curr.type === "CellRef") {
1803
+ deps.push({ col: curr.col, row: curr.row });
1804
+ } else if (curr.type === "Range") {
1805
+ const startC = colNameToIndex(curr.startCol);
1806
+ const endC = colNameToIndex(curr.endCol);
1807
+ const minC = Math.min(startC, endC);
1808
+ const maxC = Math.max(startC, endC);
1809
+ const minR = Math.min(curr.startRow, curr.endRow);
1810
+ const maxR = Math.max(curr.startRow, curr.endRow);
1811
+ for (let c = minC; c <= maxC; c++) {
1812
+ for (let r = minR; r <= maxR; r++) {
1813
+ deps.push({ col: indexToColName(c), row: r });
1814
+ }
1815
+ }
1816
+ } else if (curr.type === "BinaryOp") {
1817
+ visit(curr.left);
1818
+ visit(curr.right);
1819
+ } else if (curr.type === "UnaryOp") {
1820
+ visit(curr.argument);
1821
+ } else if (curr.type === "FunctionCall") {
1822
+ curr.args.forEach(visit);
1823
+ }
1824
+ };
1825
+ visit(node);
1826
+ return deps;
1827
+ }
1828
+ var DependencyGraph = class {
1829
+ // Map of cellKey (e.g. "B2") -> Set of cellKeys that this cell depends ON
1830
+ dependencies = /* @__PURE__ */ new Map();
1831
+ // Map of cellKey (e.g. "A1") -> Set of cellKeys that depend ON this cell (dependents)
1832
+ dependents = /* @__PURE__ */ new Map();
1833
+ setCellFormula(cellKey, formulaStr) {
1834
+ const key = cellKey.toUpperCase();
1835
+ const oldDeps = this.dependencies.get(key) || /* @__PURE__ */ new Set();
1836
+ for (const dep of oldDeps) {
1837
+ this.dependents.get(dep)?.delete(key);
1838
+ }
1839
+ this.dependencies.delete(key);
1840
+ if (!formulaStr || !formulaStr.startsWith("=")) {
1841
+ return;
1842
+ }
1843
+ try {
1844
+ const ast = parseFormula(formulaStr);
1845
+ const deps = extractCellDependencies(ast);
1846
+ const newDeps = /* @__PURE__ */ new Set();
1847
+ for (const d of deps) {
1848
+ const depKey = `${d.col}${d.row}`.toUpperCase();
1849
+ newDeps.add(depKey);
1850
+ if (!this.dependents.has(depKey)) {
1851
+ this.dependents.set(depKey, /* @__PURE__ */ new Set());
1852
+ }
1853
+ this.dependents.get(depKey).add(key);
1854
+ }
1855
+ this.dependencies.set(key, newDeps);
1856
+ } catch {
1857
+ }
1858
+ }
1859
+ getDependencies(cellKey) {
1860
+ return Array.from(this.dependencies.get(cellKey.toUpperCase()) || []);
1861
+ }
1862
+ getDependents(cellKey) {
1863
+ return Array.from(this.dependents.get(cellKey.toUpperCase()) || []);
1864
+ }
1865
+ /**
1866
+ * Detects if there is a cycle containing cellKey.
1867
+ */
1868
+ hasCycle(cellKey) {
1869
+ const key = cellKey.toUpperCase();
1870
+ const visited = /* @__PURE__ */ new Set();
1871
+ const recStack = /* @__PURE__ */ new Set();
1872
+ const dfs = (node) => {
1873
+ visited.add(node);
1874
+ recStack.add(node);
1875
+ const deps = this.dependencies.get(node) || /* @__PURE__ */ new Set();
1876
+ for (const neighbor of deps) {
1877
+ if (!visited.has(neighbor)) {
1878
+ if (dfs(neighbor)) return true;
1879
+ } else if (recStack.has(neighbor)) {
1880
+ return true;
1881
+ }
1882
+ }
1883
+ recStack.delete(node);
1884
+ return false;
1885
+ };
1886
+ return dfs(key);
1887
+ }
1888
+ /**
1889
+ * Returns cells affected by updating cellKey in topological order.
1890
+ */
1891
+ getEvaluationOrder(startCellKey) {
1892
+ const start = startCellKey.toUpperCase();
1893
+ const visited = /* @__PURE__ */ new Set();
1894
+ const recStack = /* @__PURE__ */ new Set();
1895
+ const order = [];
1896
+ let cycleDetected = false;
1897
+ const affected = /* @__PURE__ */ new Set();
1898
+ const collectAffected = (node) => {
1899
+ affected.add(node);
1900
+ const directDependents = this.dependents.get(node) || /* @__PURE__ */ new Set();
1901
+ for (const dep of directDependents) {
1902
+ if (!affected.has(dep)) {
1903
+ collectAffected(dep);
1904
+ }
1905
+ }
1906
+ };
1907
+ collectAffected(start);
1908
+ const visit = (node) => {
1909
+ if (recStack.has(node)) {
1910
+ cycleDetected = true;
1911
+ return;
1912
+ }
1913
+ if (visited.has(node)) return;
1914
+ recStack.add(node);
1915
+ visited.add(node);
1916
+ const deps = this.dependencies.get(node) || /* @__PURE__ */ new Set();
1917
+ for (const neighbor of deps) {
1918
+ if (affected.has(neighbor)) {
1919
+ visit(neighbor);
1920
+ }
1921
+ }
1922
+ recStack.delete(node);
1923
+ order.push(node);
1924
+ };
1925
+ for (const node of affected) {
1926
+ if (!visited.has(node)) {
1927
+ visit(node);
1928
+ }
1929
+ }
1930
+ return { order, hasCycle: cycleDetected };
1931
+ }
1932
+ };
1933
+
1934
+ // src/spreadsheet/datasource/index.ts
1935
+ var DataSourceBinder = class {
1936
+ dataSources = /* @__PURE__ */ new Map();
1937
+ register(dataSource) {
1938
+ this.dataSources.set(dataSource.id, dataSource);
1939
+ }
1940
+ unregister(dataSourceId) {
1941
+ this.dataSources.delete(dataSourceId);
1942
+ }
1943
+ getDataSource(id) {
1944
+ return this.dataSources.get(id);
1945
+ }
1946
+ /**
1947
+ * Generates or reconciles spreadsheet rows from a primary data source.
1948
+ */
1949
+ syncRows(currentRows, dataSourceId) {
1950
+ const ds = this.dataSources.get(dataSourceId);
1951
+ if (!ds) return currentRows;
1952
+ const records = ds.getRecords();
1953
+ const existingRecordRowMap = /* @__PURE__ */ new Map();
1954
+ const freeformRows = [];
1955
+ for (const row of currentRows) {
1956
+ if (row.type === "record" && row.recordId) {
1957
+ existingRecordRowMap.set(row.recordId, row);
1958
+ } else {
1959
+ freeformRows.push(row);
1960
+ }
1961
+ }
1962
+ const nextRows = [];
1963
+ let rowIndex = 0;
1964
+ for (const record of records) {
1965
+ const existing = existingRecordRowMap.get(record.id);
1966
+ nextRows.push({
1967
+ id: existing?.id || `row_${record.id}`,
1968
+ index: rowIndex++,
1969
+ height: existing?.height || 32,
1970
+ recordId: record.id,
1971
+ type: "record"
1972
+ });
1973
+ existingRecordRowMap.delete(record.id);
1974
+ }
1975
+ for (const freeform of freeformRows) {
1976
+ nextRows.push({
1977
+ ...freeform,
1978
+ index: rowIndex++
1979
+ });
1980
+ }
1981
+ return nextRows;
1982
+ }
1983
+ /**
1984
+ * Resolves a record's bound field value.
1985
+ */
1986
+ resolveBoundValue(dataSourceId, recordId, field) {
1987
+ const ds = this.dataSources.get(dataSourceId);
1988
+ if (!ds) return void 0;
1989
+ const records = ds.getRecords();
1990
+ const record = records.find((r) => r.id === recordId);
1991
+ return record ? record[field] : void 0;
1992
+ }
1993
+ /**
1994
+ * Dispatches an update to the underlying data source.
1995
+ */
1996
+ async commitBoundValue(dataSourceId, recordId, field, value) {
1997
+ const ds = this.dataSources.get(dataSourceId);
1998
+ if (!ds) return;
1999
+ await ds.updateRecord(recordId, field, value);
2000
+ }
2001
+ };
2002
+
2003
+ // src/spreadsheet/controller.ts
2004
+ var SpreadsheetController = class {
2005
+ document;
2006
+ activeCell = null;
2007
+ selectedRange = null;
2008
+ editingCell = null;
2009
+ draftValue = "";
2010
+ binder = new DataSourceBinder();
2011
+ depGraph = new DependencyGraph();
2012
+ evaluator;
2013
+ primaryDataSourceId;
2014
+ onDocumentChange;
2015
+ onSelectionChange;
2016
+ onCellCommit;
2017
+ // History stack for undo/redo
2018
+ undoStack = [];
2019
+ redoStack = [];
2020
+ isApplyingHistory = false;
2021
+ constructor(options = {}) {
2022
+ const defaultCols = [
2023
+ { id: "col_a", key: "A", title: "A", width: 120, type: "freeform" },
2024
+ { id: "col_b", key: "B", title: "B", width: 120, type: "freeform" },
2025
+ { id: "col_c", key: "C", title: "C", width: 120, type: "freeform" },
2026
+ { id: "col_d", key: "D", title: "D", width: 120, type: "freeform" }
2027
+ ];
2028
+ const defaultRows = options.primaryDataSourceId ? [] : Array.from({ length: 10 }, (_, i) => ({
2029
+ id: `row_${i + 1}`,
2030
+ index: i,
2031
+ height: 32,
2032
+ type: "freeform"
2033
+ }));
2034
+ this.document = {
2035
+ id: options.document?.id || `sheet_${Date.now()}`,
2036
+ name: options.document?.name || "Sheet 1",
2037
+ columns: options.document?.columns || defaultCols,
2038
+ rows: options.document?.rows || defaultRows,
2039
+ cells: options.document?.cells || {},
2040
+ createdAt: options.document?.createdAt || Date.now(),
2041
+ updatedAt: options.document?.updatedAt || Date.now()
2042
+ };
2043
+ this.primaryDataSourceId = options.primaryDataSourceId;
2044
+ this.onDocumentChange = options.onDocumentChange;
2045
+ this.onSelectionChange = options.onSelectionChange;
2046
+ this.onCellCommit = options.onCellCommit;
2047
+ if (options.dataSources) {
2048
+ options.dataSources.forEach((ds) => this.binder.register(ds));
2049
+ }
2050
+ this.evaluator = new FormulaEvaluator({
2051
+ resolveCell: (colStr, rowNum) => this.resolveCellByCoord(colStr, rowNum),
2052
+ resolveRange: (sCol, sRow, eCol, eRow) => this.resolveRangeByCoords(sCol, sRow, eCol, eRow),
2053
+ resolveProperty: (prop) => this.resolveCurrentRowProperty(prop)
2054
+ });
2055
+ this.syncWithDataSource();
2056
+ this.recalculateAll();
2057
+ }
2058
+ registerDataSource(ds) {
2059
+ this.binder.register(ds);
2060
+ this.syncWithDataSource();
2061
+ this.recalculateAll();
2062
+ }
2063
+ setPrimaryDataSource(dataSourceId) {
2064
+ this.primaryDataSourceId = dataSourceId;
2065
+ this.syncWithDataSource();
2066
+ this.recalculateAll();
2067
+ }
2068
+ syncWithDataSource() {
2069
+ if (!this.primaryDataSourceId) return;
2070
+ this.document.rows = this.binder.syncRows(this.document.rows, this.primaryDataSourceId);
2071
+ this.recomputeRowIndices();
2072
+ }
2073
+ recomputeRowIndices() {
2074
+ this.document.rows.forEach((r, idx) => {
2075
+ r.index = idx;
2076
+ });
2077
+ this.document.columns.forEach((c, idx) => {
2078
+ c.key = indexToColName(idx);
2079
+ });
2080
+ }
2081
+ getColumn(colIdOrKey) {
2082
+ return this.document.columns.find(
2083
+ (c) => c.id === colIdOrKey || c.key.toUpperCase() === colIdOrKey.toUpperCase()
2084
+ );
2085
+ }
2086
+ getRow(rowIdOrIndex) {
2087
+ if (typeof rowIdOrIndex === "number") {
2088
+ return this.document.rows[rowIdOrIndex];
2089
+ }
2090
+ return this.document.rows.find((r) => r.id === rowIdOrIndex);
2091
+ }
2092
+ getCell(rowId, colId) {
2093
+ const existing = this.document.cells[rowId]?.[colId];
2094
+ if (existing) return existing;
2095
+ const col = this.document.columns.find((c) => c.id === colId);
2096
+ const row = this.document.rows.find((r) => r.id === rowId);
2097
+ if (col && row && col.type === "bound" && col.binding && row.recordId) {
2098
+ const boundVal = this.binder.resolveBoundValue(
2099
+ col.binding.dataSource,
2100
+ row.recordId,
2101
+ col.binding.field
2102
+ );
2103
+ return {
2104
+ raw: boundVal !== void 0 ? String(boundVal) : "",
2105
+ value: boundVal ?? "",
2106
+ format: col.format
2107
+ };
2108
+ }
2109
+ if (col && col.type === "formula" && col.formula) {
2110
+ return {
2111
+ raw: col.formula,
2112
+ value: "",
2113
+ format: col.format
2114
+ };
2115
+ }
2116
+ return { raw: "", value: "", format: col?.format };
2117
+ }
2118
+ setCellValue(rowId, colId, raw) {
2119
+ this.recordSnapshot();
2120
+ if (!this.document.cells[rowId]) {
2121
+ this.document.cells[rowId] = {};
2122
+ }
2123
+ const col = this.document.columns.find((c) => c.id === colId);
2124
+ const row = this.document.rows.find((r) => r.id === rowId);
2125
+ if (col?.type === "bound" && col.binding && row?.recordId) {
2126
+ this.binder.commitBoundValue(
2127
+ col.binding.dataSource,
2128
+ row.recordId,
2129
+ col.binding.field,
2130
+ raw
2131
+ );
2132
+ }
2133
+ const cellKey = this.getCellKey(rowId, colId);
2134
+ this.depGraph.setCellFormula(cellKey, raw.startsWith("=") ? raw : null);
2135
+ this.document.cells[rowId][colId] = {
2136
+ raw,
2137
+ value: raw,
2138
+ format: col?.format
2139
+ };
2140
+ this.document.updatedAt = Date.now();
2141
+ this.recalculateCell(rowId, colId);
2142
+ this.notifyChange();
2143
+ const evaluatedVal = this.document.cells[rowId][colId]?.value;
2144
+ this.onCellCommit?.(rowId, colId, raw, evaluatedVal);
2145
+ }
2146
+ getCellKey(rowId, colId) {
2147
+ const row = this.document.rows.find((r) => r.id === rowId);
2148
+ const col = this.document.columns.find((c) => c.id === colId);
2149
+ if (!row || !col) return `${colId}_${rowId}`;
2150
+ return `${col.key}${row.index + 1}`.toUpperCase();
2151
+ }
2152
+ recalculateCell(rowId, colId) {
2153
+ const cellKey = this.getCellKey(rowId, colId);
2154
+ const { order, hasCycle } = this.depGraph.getEvaluationOrder(cellKey);
2155
+ if (hasCycle) {
2156
+ if (this.document.cells[rowId]?.[colId]) {
2157
+ this.document.cells[rowId][colId].error = "#CYCLE!";
2158
+ this.document.cells[rowId][colId].value = "#CYCLE!";
2159
+ }
2160
+ return;
2161
+ }
2162
+ for (const key of order) {
2163
+ this.evalSingleCellByKey(key);
2164
+ }
2165
+ }
2166
+ recalculateAll() {
2167
+ for (const row of this.document.rows) {
2168
+ for (const col of this.document.columns) {
2169
+ const cell = this.getCell(row.id, col.id);
2170
+ const cellKey = `${col.key}${row.index + 1}`.toUpperCase();
2171
+ if (cell.raw.startsWith("=")) {
2172
+ this.depGraph.setCellFormula(cellKey, cell.raw);
2173
+ }
2174
+ }
2175
+ }
2176
+ for (const row of this.document.rows) {
2177
+ for (const col of this.document.columns) {
2178
+ const cellKey = `${col.key}${row.index + 1}`.toUpperCase();
2179
+ this.evalSingleCellByKey(cellKey);
2180
+ }
2181
+ }
2182
+ }
2183
+ evalSingleCellByKey(cellKey) {
2184
+ const match = cellKey.match(/^([A-Za-z]+)(\d+)$/);
2185
+ if (!match) return;
2186
+ const colName = match[1].toUpperCase();
2187
+ const rowNum = parseInt(match[2], 10);
2188
+ const col = this.document.columns.find((c) => c.key.toUpperCase() === colName);
2189
+ const row = this.document.rows[rowNum - 1];
2190
+ if (!col || !row) return;
2191
+ const cell = this.getCell(row.id, col.id);
2192
+ if (!cell.raw.startsWith("=")) {
2193
+ if (cell.error === "#CYCLE!") {
2194
+ cell.error = void 0;
2195
+ }
2196
+ return;
2197
+ }
2198
+ try {
2199
+ const computed = this.evaluator.evaluate(cell.raw);
2200
+ if (!this.document.cells[row.id]) {
2201
+ this.document.cells[row.id] = {};
2202
+ }
2203
+ this.document.cells[row.id][col.id] = {
2204
+ ...cell,
2205
+ value: computed,
2206
+ error: void 0
2207
+ };
2208
+ } catch (err) {
2209
+ if (!this.document.cells[row.id]) {
2210
+ this.document.cells[row.id] = {};
2211
+ }
2212
+ this.document.cells[row.id][col.id] = {
2213
+ ...cell,
2214
+ value: "#ERROR!",
2215
+ error: err?.message || "Formula error"
2216
+ };
2217
+ }
2218
+ }
2219
+ resolveCellByCoord(colStr, rowNum) {
2220
+ const col = this.document.columns.find((c) => c.key.toUpperCase() === colStr.toUpperCase());
2221
+ const row = this.document.rows[rowNum - 1];
2222
+ if (!col || !row) return 0;
2223
+ const cell = this.getCell(row.id, col.id);
2224
+ return cell.value !== void 0 && cell.value !== "" ? cell.value : 0;
2225
+ }
2226
+ resolveRangeByCoords(sCol, sRow, eCol, eRow) {
2227
+ const startC = colNameToIndex(sCol);
2228
+ const endC = colNameToIndex(eCol);
2229
+ const minC = Math.min(startC, endC);
2230
+ const maxC = Math.max(startC, endC);
2231
+ const minR = Math.min(sRow, eRow);
2232
+ const maxR = Math.max(sRow, eRow);
2233
+ const results = [];
2234
+ for (let c = minC; c <= maxC; c++) {
2235
+ const colName = indexToColName(c);
2236
+ const col = this.document.columns.find((colDef) => colDef.key.toUpperCase() === colName);
2237
+ if (!col) continue;
2238
+ for (let r = minR; r <= maxR; r++) {
2239
+ const row = this.document.rows[r - 1];
2240
+ if (!row) continue;
2241
+ const cell = this.getCell(row.id, col.id);
2242
+ results.push(cell.value !== void 0 && cell.value !== "" ? cell.value : 0);
2243
+ }
2244
+ }
2245
+ return results;
2246
+ }
2247
+ resolveCurrentRowProperty(prop) {
2248
+ if (!this.activeCell) return 0;
2249
+ const row = this.document.rows[this.activeCell.row];
2250
+ if (!row || !row.recordId || !this.primaryDataSourceId) return 0;
2251
+ return this.binder.resolveBoundValue(this.primaryDataSourceId, row.recordId, prop) ?? 0;
2252
+ }
2253
+ // --- Selection and Navigation ---
2254
+ selectCell(row, col, extendSelection = false) {
2255
+ const clampedRow = Math.max(0, Math.min(row, this.document.rows.length - 1));
2256
+ const clampedCol = Math.max(0, Math.min(col, this.document.columns.length - 1));
2257
+ const rowObj = this.document.rows[clampedRow];
2258
+ const colObj = this.document.columns[clampedCol];
2259
+ this.activeCell = {
2260
+ row: clampedRow,
2261
+ col: clampedCol,
2262
+ rowId: rowObj?.id,
2263
+ colId: colObj?.id
2264
+ };
2265
+ if (extendSelection && this.selectedRange) {
2266
+ this.selectedRange = {
2267
+ startRow: this.selectedRange.startRow,
2268
+ startCol: this.selectedRange.startCol,
2269
+ endRow: clampedRow,
2270
+ endCol: clampedCol
2271
+ };
2272
+ } else {
2273
+ this.selectedRange = {
2274
+ startRow: clampedRow,
2275
+ startCol: clampedCol,
2276
+ endRow: clampedRow,
2277
+ endCol: clampedCol
2278
+ };
2279
+ }
2280
+ this.onSelectionChange?.(this.activeCell, this.selectedRange);
2281
+ }
2282
+ selectRange(range) {
2283
+ this.selectedRange = range;
2284
+ this.onSelectionChange?.(this.activeCell, this.selectedRange);
2285
+ }
2286
+ startEditing(row, col, initialValue) {
2287
+ const targetRow = row ?? this.activeCell?.row ?? 0;
2288
+ const targetCol = col ?? this.activeCell?.col ?? 0;
2289
+ this.selectCell(targetRow, targetCol);
2290
+ const rowObj = this.document.rows[targetRow];
2291
+ const colObj = this.document.columns[targetCol];
2292
+ if (!rowObj || !colObj) return;
2293
+ this.editingCell = {
2294
+ row: targetRow,
2295
+ col: targetCol,
2296
+ rowId: rowObj.id,
2297
+ colId: colObj.id
2298
+ };
2299
+ const cell = this.getCell(rowObj.id, colObj.id);
2300
+ this.draftValue = initialValue !== void 0 ? initialValue : cell.raw;
2301
+ }
2302
+ commitEditing(valueToCommit) {
2303
+ if (!this.editingCell) return;
2304
+ const val = valueToCommit !== void 0 ? valueToCommit : this.draftValue;
2305
+ if (this.editingCell.rowId && this.editingCell.colId) {
2306
+ this.setCellValue(this.editingCell.rowId, this.editingCell.colId, val);
2307
+ }
2308
+ this.editingCell = null;
2309
+ this.draftValue = "";
2310
+ }
2311
+ cancelEditing() {
2312
+ this.editingCell = null;
2313
+ this.draftValue = "";
2314
+ }
2315
+ moveCursor(direction, extendSelection = false) {
2316
+ if (!this.activeCell) {
2317
+ this.selectCell(0, 0);
2318
+ return;
2319
+ }
2320
+ let nextRow = this.activeCell.row;
2321
+ let nextCol = this.activeCell.col;
2322
+ switch (direction) {
2323
+ case "up":
2324
+ nextRow = Math.max(0, nextRow - 1);
2325
+ break;
2326
+ case "down":
2327
+ nextRow = Math.min(this.document.rows.length - 1, nextRow + 1);
2328
+ break;
2329
+ case "left":
2330
+ nextCol = Math.max(0, nextCol - 1);
2331
+ break;
2332
+ case "right":
2333
+ nextCol = Math.min(this.document.columns.length - 1, nextCol + 1);
2334
+ break;
2335
+ }
2336
+ this.selectCell(nextRow, nextCol, extendSelection);
2337
+ }
2338
+ // --- Grid Structure Mutations ---
2339
+ insertColumn(index, config) {
2340
+ this.recordSnapshot();
2341
+ const newId = config?.id || `col_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
2342
+ const newCol = {
2343
+ id: newId,
2344
+ key: indexToColName(index),
2345
+ title: config?.title || indexToColName(index),
2346
+ width: config?.width || 130,
2347
+ type: config?.type || "freeform",
2348
+ binding: config?.binding,
2349
+ formula: config?.formula,
2350
+ format: config?.format || "text",
2351
+ readOnly: config?.readOnly
2352
+ };
2353
+ this.document.columns.splice(index, 0, newCol);
2354
+ this.recomputeRowIndices();
2355
+ this.recalculateAll();
2356
+ this.notifyChange();
2357
+ }
2358
+ deleteColumn(colId) {
2359
+ this.recordSnapshot();
2360
+ this.document.columns = this.document.columns.filter((c) => c.id !== colId);
2361
+ for (const rowId of Object.keys(this.document.cells)) {
2362
+ delete this.document.cells[rowId][colId];
2363
+ }
2364
+ this.recomputeRowIndices();
2365
+ this.recalculateAll();
2366
+ this.notifyChange();
2367
+ }
2368
+ insertRow(index, config) {
2369
+ this.recordSnapshot();
2370
+ const newId = config?.id || `row_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
2371
+ const newRow = {
2372
+ id: newId,
2373
+ index,
2374
+ height: config?.height || 32,
2375
+ recordId: config?.recordId,
2376
+ type: config?.type || "freeform"
2377
+ };
2378
+ this.document.rows.splice(index, 0, newRow);
2379
+ this.recomputeRowIndices();
2380
+ this.recalculateAll();
2381
+ this.notifyChange();
2382
+ }
2383
+ deleteRow(rowId) {
2384
+ this.recordSnapshot();
2385
+ this.document.rows = this.document.rows.filter((r) => r.id !== rowId);
2386
+ delete this.document.cells[rowId];
2387
+ this.recomputeRowIndices();
2388
+ this.recalculateAll();
2389
+ this.notifyChange();
2390
+ }
2391
+ setColumnWidth(colId, width) {
2392
+ const col = this.document.columns.find((c) => c.id === colId);
2393
+ if (col) {
2394
+ col.width = Math.max(50, width);
2395
+ this.notifyChange();
2396
+ }
2397
+ }
2398
+ // --- Clipboard TSV/CSV ---
2399
+ exportToTsv() {
2400
+ return this.document.rows.map(
2401
+ (row) => this.document.columns.map((col) => {
2402
+ const cell = this.getCell(row.id, col.id);
2403
+ return String(cell.value ?? "").replace(/\t/g, " ");
2404
+ }).join(" ")
2405
+ ).join("\n");
2406
+ }
2407
+ importFromTsv(tsvData, startRow = 0, startCol = 0) {
2408
+ this.recordSnapshot();
2409
+ const lines = tsvData.trim().split("\n");
2410
+ lines.forEach((line, rIdx) => {
2411
+ const cells = line.split(" ");
2412
+ const targetR = startRow + rIdx;
2413
+ while (targetR >= this.document.rows.length) {
2414
+ this.insertRow(this.document.rows.length);
2415
+ }
2416
+ const row = this.document.rows[targetR];
2417
+ cells.forEach((val, cIdx) => {
2418
+ const targetC = startCol + cIdx;
2419
+ while (targetC >= this.document.columns.length) {
2420
+ this.insertColumn(this.document.columns.length);
2421
+ }
2422
+ const col = this.document.columns[targetC];
2423
+ this.setCellValue(row.id, col.id, val);
2424
+ });
2425
+ });
2426
+ this.recalculateAll();
2427
+ this.notifyChange();
2428
+ }
2429
+ // --- Undo/Redo Snapshots ---
2430
+ recordSnapshot() {
2431
+ if (this.isApplyingHistory) return;
2432
+ this.undoStack.push(JSON.stringify(this.document));
2433
+ if (this.undoStack.length > 50) this.undoStack.shift();
2434
+ this.redoStack = [];
2435
+ }
2436
+ undo() {
2437
+ if (this.undoStack.length === 0) return;
2438
+ this.isApplyingHistory = true;
2439
+ this.redoStack.push(JSON.stringify(this.document));
2440
+ const previous = JSON.parse(this.undoStack.pop());
2441
+ this.document = previous;
2442
+ this.recalculateAll();
2443
+ this.notifyChange();
2444
+ this.isApplyingHistory = false;
2445
+ }
2446
+ redo() {
2447
+ if (this.redoStack.length === 0) return;
2448
+ this.isApplyingHistory = true;
2449
+ this.undoStack.push(JSON.stringify(this.document));
2450
+ const next = JSON.parse(this.redoStack.pop());
2451
+ this.document = next;
2452
+ this.recalculateAll();
2453
+ this.notifyChange();
2454
+ this.isApplyingHistory = false;
2455
+ }
2456
+ notifyChange() {
2457
+ this.onDocumentChange?.(this.document);
2458
+ }
2459
+ };
1051
2460
  export {
1052
2461
  ColorHighlighter,
2462
+ DataSourceBinder,
2463
+ DependencyGraph,
1053
2464
  EditorController,
1054
2465
  FontSize,
2466
+ FormulaEvaluator,
2467
+ FormulaParser,
1055
2468
  GradientText,
1056
2469
  INSTRUCTION_REGEX,
1057
2470
  LoadingNode,
2471
+ Mention,
1058
2472
  SlashCommands,
1059
2473
  SmilieReplacer,
2474
+ SpreadsheetController,
1060
2475
  TEMPLATE_REGEX,
1061
2476
  TemplateSuggestions,
1062
2477
  VARIABLE_REGEX,
2478
+ colNameToIndex,
1063
2479
  createEditor,
2480
+ createMentionExtension,
1064
2481
  createRichTextPreset,
1065
2482
  escapeHtml,
2483
+ extractCellDependencies,
1066
2484
  extractInstructionNames,
2485
+ extractMentionsFromDoc,
1067
2486
  extractTemplateNames,
1068
2487
  extractVariableNames,
1069
2488
  findHexColors,
1070
2489
  getEditorText,
1071
2490
  getLatestVersion,
1072
2491
  htmlToPlainText,
2492
+ indexToColName,
1073
2493
  instructionRegex,
1074
2494
  mapRawOffsetToResolved,
1075
2495
  mapResolvedOffsetToRaw,
2496
+ parseCellReference,
2497
+ parseFormula,
2498
+ parseRange,
1076
2499
  plainTextToTipTapHtml,
1077
2500
  resolveTemplates,
1078
2501
  resolveTemplatesWithMapping,
1079
2502
  resolveVariables,
1080
2503
  templateRegex,
2504
+ tokenizeFormula,
1081
2505
  tokenizePrompt,
1082
2506
  variableRegex
1083
2507
  };