@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/extensions/index.cjs +89 -2
- package/dist/extensions/index.cjs.map +1 -1
- package/dist/extensions/index.d.cts +28 -1
- package/dist/extensions/index.d.ts +28 -1
- package/dist/extensions/index.js +87 -2
- package/dist/extensions/index.js.map +1 -1
- package/dist/index.cjs +1445 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +1428 -4
- package/dist/index.js.map +1 -1
- package/dist/serializers/index.cjs +85 -2
- package/dist/serializers/index.cjs.map +1 -1
- package/dist/serializers/index.d.cts +15 -2
- package/dist/serializers/index.d.ts +15 -2
- package/dist/serializers/index.js +84 -2
- package/dist/serializers/index.js.map +1 -1
- package/dist/spreadsheet/index.cjs +1297 -0
- package/dist/spreadsheet/index.cjs.map +1 -0
- package/dist/spreadsheet/index.d.cts +268 -0
- package/dist/spreadsheet/index.d.ts +268 -0
- package/dist/spreadsheet/index.js +1259 -0
- package/dist/spreadsheet/index.js.map +1 -0
- package/package.json +7 -1
package/dist/index.cjs
CHANGED
|
@@ -31,34 +31,49 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
33
|
ColorHighlighter: () => ColorHighlighter,
|
|
34
|
+
DataSourceBinder: () => DataSourceBinder,
|
|
35
|
+
DependencyGraph: () => DependencyGraph,
|
|
34
36
|
EditorController: () => EditorController,
|
|
35
37
|
FontSize: () => FontSize,
|
|
38
|
+
FormulaEvaluator: () => FormulaEvaluator,
|
|
39
|
+
FormulaParser: () => FormulaParser,
|
|
36
40
|
GradientText: () => GradientText,
|
|
37
41
|
INSTRUCTION_REGEX: () => INSTRUCTION_REGEX,
|
|
38
42
|
LoadingNode: () => LoadingNode,
|
|
43
|
+
Mention: () => Mention,
|
|
39
44
|
SlashCommands: () => SlashCommands,
|
|
40
45
|
SmilieReplacer: () => SmilieReplacer,
|
|
46
|
+
SpreadsheetController: () => SpreadsheetController,
|
|
41
47
|
TEMPLATE_REGEX: () => TEMPLATE_REGEX,
|
|
42
48
|
TemplateSuggestions: () => TemplateSuggestions,
|
|
43
49
|
VARIABLE_REGEX: () => VARIABLE_REGEX,
|
|
50
|
+
colNameToIndex: () => colNameToIndex,
|
|
44
51
|
createEditor: () => createEditor,
|
|
52
|
+
createMentionExtension: () => createMentionExtension,
|
|
45
53
|
createRichTextPreset: () => createRichTextPreset,
|
|
46
54
|
escapeHtml: () => escapeHtml,
|
|
55
|
+
extractCellDependencies: () => extractCellDependencies,
|
|
47
56
|
extractInstructionNames: () => extractInstructionNames,
|
|
57
|
+
extractMentionsFromDoc: () => extractMentionsFromDoc,
|
|
48
58
|
extractTemplateNames: () => extractTemplateNames,
|
|
49
59
|
extractVariableNames: () => extractVariableNames,
|
|
50
60
|
findHexColors: () => findHexColors,
|
|
51
61
|
getEditorText: () => getEditorText,
|
|
52
62
|
getLatestVersion: () => getLatestVersion,
|
|
53
63
|
htmlToPlainText: () => htmlToPlainText,
|
|
64
|
+
indexToColName: () => indexToColName,
|
|
54
65
|
instructionRegex: () => instructionRegex,
|
|
55
66
|
mapRawOffsetToResolved: () => mapRawOffsetToResolved,
|
|
56
67
|
mapResolvedOffsetToRaw: () => mapResolvedOffsetToRaw,
|
|
68
|
+
parseCellReference: () => parseCellReference,
|
|
69
|
+
parseFormula: () => parseFormula,
|
|
70
|
+
parseRange: () => parseRange,
|
|
57
71
|
plainTextToTipTapHtml: () => plainTextToTipTapHtml,
|
|
58
72
|
resolveTemplates: () => resolveTemplates,
|
|
59
73
|
resolveTemplatesWithMapping: () => resolveTemplatesWithMapping,
|
|
60
74
|
resolveVariables: () => resolveVariables,
|
|
61
75
|
templateRegex: () => templateRegex,
|
|
76
|
+
tokenizeFormula: () => tokenizeFormula,
|
|
62
77
|
tokenizePrompt: () => tokenizePrompt,
|
|
63
78
|
variableRegex: () => variableRegex
|
|
64
79
|
});
|
|
@@ -875,6 +890,7 @@ var FontSize = import_core7.Extension.create({
|
|
|
875
890
|
});
|
|
876
891
|
|
|
877
892
|
// src/extensions/rich-text-preset.ts
|
|
893
|
+
var import_core8 = require("@tiptap/core");
|
|
878
894
|
var import_starter_kit = __toESM(require("@tiptap/starter-kit"), 1);
|
|
879
895
|
var import_extension_color = __toESM(require("@tiptap/extension-color"), 1);
|
|
880
896
|
var import_extension_highlight = __toESM(require("@tiptap/extension-highlight"), 1);
|
|
@@ -887,6 +903,52 @@ var import_extension_typography = __toESM(require("@tiptap/extension-typography"
|
|
|
887
903
|
var import_extension_underline = __toESM(require("@tiptap/extension-underline"), 1);
|
|
888
904
|
var import_extension_placeholder = __toESM(require("@tiptap/extension-placeholder"), 1);
|
|
889
905
|
var import_extension_character_count = __toESM(require("@tiptap/extension-character-count"), 1);
|
|
906
|
+
|
|
907
|
+
// src/extensions/mention.ts
|
|
908
|
+
var import_extension_mention = __toESM(require("@tiptap/extension-mention"), 1);
|
|
909
|
+
var Mention = import_extension_mention.default.extend({
|
|
910
|
+
addAttributes() {
|
|
911
|
+
return {
|
|
912
|
+
...this.parent?.(),
|
|
913
|
+
type: {
|
|
914
|
+
default: null,
|
|
915
|
+
parseHTML: (element) => element.getAttribute("data-type-name") || element.getAttribute("data-entity-type"),
|
|
916
|
+
renderHTML: (attributes) => {
|
|
917
|
+
if (!attributes.type) return {};
|
|
918
|
+
return { "data-type-name": attributes.type };
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
avatarUrl: {
|
|
922
|
+
default: null,
|
|
923
|
+
parseHTML: (element) => element.getAttribute("data-avatar-url"),
|
|
924
|
+
renderHTML: (attributes) => {
|
|
925
|
+
if (!attributes.avatarUrl) return {};
|
|
926
|
+
return { "data-avatar-url": attributes.avatarUrl };
|
|
927
|
+
}
|
|
928
|
+
},
|
|
929
|
+
color: {
|
|
930
|
+
default: null,
|
|
931
|
+
parseHTML: (element) => element.getAttribute("data-color"),
|
|
932
|
+
renderHTML: (attributes) => {
|
|
933
|
+
if (!attributes.color) return {};
|
|
934
|
+
return { "data-color": attributes.color };
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
function createMentionExtension(options = {}) {
|
|
941
|
+
const { HTMLAttributes = {}, ...rest } = options;
|
|
942
|
+
return Mention.configure({
|
|
943
|
+
HTMLAttributes: {
|
|
944
|
+
class: "pixerate-mention-node",
|
|
945
|
+
...HTMLAttributes
|
|
946
|
+
},
|
|
947
|
+
...rest
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// src/extensions/rich-text-preset.ts
|
|
890
952
|
var TextStyle2 = _TextStyle.TextStyle || _TextStyle.default || _TextStyle;
|
|
891
953
|
function createRichTextPreset(options = {}) {
|
|
892
954
|
const {
|
|
@@ -924,7 +986,19 @@ function createRichTextPreset(options = {}) {
|
|
|
924
986
|
import_extension_text_align.default.configure({
|
|
925
987
|
types: ["heading", "paragraph"]
|
|
926
988
|
}),
|
|
927
|
-
import_extension_link.default.
|
|
989
|
+
import_extension_link.default.extend({
|
|
990
|
+
addInputRules() {
|
|
991
|
+
return [
|
|
992
|
+
(0, import_core8.markInputRule)({
|
|
993
|
+
find: /(?:^|\s)\[([^\]]+)\]\(([^)]+)\)$/,
|
|
994
|
+
type: this.type,
|
|
995
|
+
getAttributes: (match) => ({
|
|
996
|
+
href: match[2]
|
|
997
|
+
})
|
|
998
|
+
})
|
|
999
|
+
];
|
|
1000
|
+
}
|
|
1001
|
+
}).configure({
|
|
928
1002
|
openOnClick: openLinksOnClick,
|
|
929
1003
|
autolink: true,
|
|
930
1004
|
defaultProtocol: "https",
|
|
@@ -935,7 +1009,19 @@ function createRichTextPreset(options = {}) {
|
|
|
935
1009
|
}
|
|
936
1010
|
}),
|
|
937
1011
|
import_extension_task_list.default,
|
|
938
|
-
import_extension_task_item.default.
|
|
1012
|
+
import_extension_task_item.default.extend({
|
|
1013
|
+
addInputRules() {
|
|
1014
|
+
return [
|
|
1015
|
+
(0, import_core8.wrappingInputRule)({
|
|
1016
|
+
find: /^\s*(\[([ |x])?\])\s$/i,
|
|
1017
|
+
type: this.type,
|
|
1018
|
+
getAttributes: (match) => ({
|
|
1019
|
+
checked: match[2]?.toLowerCase() === "x"
|
|
1020
|
+
})
|
|
1021
|
+
})
|
|
1022
|
+
];
|
|
1023
|
+
}
|
|
1024
|
+
}).configure({
|
|
939
1025
|
nested: true
|
|
940
1026
|
}),
|
|
941
1027
|
FontSize,
|
|
@@ -957,6 +1043,18 @@ function createRichTextPreset(options = {}) {
|
|
|
957
1043
|
if (enableColorHighlighter) {
|
|
958
1044
|
extensions.push(ColorHighlighter);
|
|
959
1045
|
}
|
|
1046
|
+
if (options.mention) {
|
|
1047
|
+
const mentionConfig = typeof options.mention === "object" ? options.mention : {};
|
|
1048
|
+
extensions.push(
|
|
1049
|
+
Mention.configure({
|
|
1050
|
+
HTMLAttributes: {
|
|
1051
|
+
class: "pixerate-mention-node",
|
|
1052
|
+
...mentionConfig.HTMLAttributes || {}
|
|
1053
|
+
},
|
|
1054
|
+
...mentionConfig
|
|
1055
|
+
})
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
960
1058
|
return extensions;
|
|
961
1059
|
}
|
|
962
1060
|
|
|
@@ -974,9 +1072,16 @@ function plainTextToTipTapHtml(text) {
|
|
|
974
1072
|
}
|
|
975
1073
|
function htmlToPlainText(html) {
|
|
976
1074
|
if (!html) return "";
|
|
1075
|
+
const mentionNormalized = html.replace(
|
|
1076
|
+
/<span[^>]*data-type=["']mention["'][^>]*data-label=["']([^"']+)["'][^>]*>.*?<\/span>/gi,
|
|
1077
|
+
"@$1"
|
|
1078
|
+
).replace(
|
|
1079
|
+
/<span[^>]*data-type=["']mention["'][^>]*data-id=["']([^"']+)["'][^>]*>.*?<\/span>/gi,
|
|
1080
|
+
"@$1"
|
|
1081
|
+
);
|
|
977
1082
|
if (typeof document !== "undefined") {
|
|
978
1083
|
const tempDiv = document.createElement("div");
|
|
979
|
-
tempDiv.innerHTML =
|
|
1084
|
+
tempDiv.innerHTML = mentionNormalized;
|
|
980
1085
|
tempDiv.querySelectorAll("br.ProseMirror-trailingBreak").forEach((br) => br.remove());
|
|
981
1086
|
tempDiv.querySelectorAll("br").forEach((br) => br.replaceWith("\n"));
|
|
982
1087
|
const paragraphs = tempDiv.querySelectorAll("p, div, li, h1, h2, h3, h4, h5, h6");
|
|
@@ -989,7 +1094,7 @@ function htmlToPlainText(html) {
|
|
|
989
1094
|
}
|
|
990
1095
|
return tempDiv.textContent || "";
|
|
991
1096
|
}
|
|
992
|
-
return
|
|
1097
|
+
return mentionNormalized.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<\/div>/gi, "\n").replace(/<\/li>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").trim();
|
|
993
1098
|
}
|
|
994
1099
|
function getEditorText(editor) {
|
|
995
1100
|
if (!editor) return "";
|
|
@@ -1006,9 +1111,83 @@ function getEditorText(editor) {
|
|
|
1006
1111
|
}
|
|
1007
1112
|
return "";
|
|
1008
1113
|
}
|
|
1114
|
+
function extractMentionsFromDoc(docOrEditor) {
|
|
1115
|
+
if (!docOrEditor) return [];
|
|
1116
|
+
const mentions = [];
|
|
1117
|
+
const doc = docOrEditor?.state?.doc || (typeof docOrEditor?.descendants === "function" ? docOrEditor : null);
|
|
1118
|
+
if (doc && typeof doc.descendants === "function") {
|
|
1119
|
+
doc.descendants((node) => {
|
|
1120
|
+
if (node.type?.name === "mention" && node.attrs) {
|
|
1121
|
+
mentions.push({
|
|
1122
|
+
id: node.attrs.id || "",
|
|
1123
|
+
label: node.attrs.label || "",
|
|
1124
|
+
type: node.attrs.type,
|
|
1125
|
+
avatarUrl: node.attrs.avatarUrl,
|
|
1126
|
+
color: node.attrs.color,
|
|
1127
|
+
...node.attrs
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
return mentions;
|
|
1132
|
+
}
|
|
1133
|
+
if (typeof docOrEditor === "object" && docOrEditor.content) {
|
|
1134
|
+
const walk = (nodes) => {
|
|
1135
|
+
for (const node of nodes) {
|
|
1136
|
+
if (node.type === "mention" && node.attrs) {
|
|
1137
|
+
mentions.push({
|
|
1138
|
+
id: node.attrs.id || "",
|
|
1139
|
+
label: node.attrs.label || "",
|
|
1140
|
+
type: node.attrs.type,
|
|
1141
|
+
avatarUrl: node.attrs.avatarUrl,
|
|
1142
|
+
color: node.attrs.color,
|
|
1143
|
+
...node.attrs
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
if (Array.isArray(node.content)) {
|
|
1147
|
+
walk(node.content);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
walk(docOrEditor.content);
|
|
1152
|
+
return mentions;
|
|
1153
|
+
}
|
|
1154
|
+
if (typeof docOrEditor === "string") {
|
|
1155
|
+
if (typeof document !== "undefined") {
|
|
1156
|
+
const div = document.createElement("div");
|
|
1157
|
+
div.innerHTML = docOrEditor;
|
|
1158
|
+
div.querySelectorAll('span[data-type="mention"]').forEach((el) => {
|
|
1159
|
+
mentions.push({
|
|
1160
|
+
id: el.getAttribute("data-id") || "",
|
|
1161
|
+
label: el.getAttribute("data-label") || "",
|
|
1162
|
+
type: el.getAttribute("data-type-name") || void 0,
|
|
1163
|
+
avatarUrl: el.getAttribute("data-avatar-url") || void 0,
|
|
1164
|
+
color: el.getAttribute("data-color") || void 0
|
|
1165
|
+
});
|
|
1166
|
+
});
|
|
1167
|
+
} else {
|
|
1168
|
+
const spanRegex = /<span([^>]*data-type=["']mention["'][^>]*)>/gi;
|
|
1169
|
+
let match;
|
|
1170
|
+
while ((match = spanRegex.exec(docOrEditor)) !== null) {
|
|
1171
|
+
const attrStr = match[1];
|
|
1172
|
+
const getAttr = (name) => {
|
|
1173
|
+
const m = new RegExp(`${name}=["']([^"']*)["']`, "i").exec(attrStr);
|
|
1174
|
+
return m ? m[1] : void 0;
|
|
1175
|
+
};
|
|
1176
|
+
mentions.push({
|
|
1177
|
+
id: getAttr("data-id") || "",
|
|
1178
|
+
label: getAttr("data-label") || "",
|
|
1179
|
+
type: getAttr("data-type-name") || getAttr("data-entity-type"),
|
|
1180
|
+
avatarUrl: getAttr("data-avatar-url"),
|
|
1181
|
+
color: getAttr("data-color")
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
return mentions;
|
|
1187
|
+
}
|
|
1009
1188
|
|
|
1010
1189
|
// src/controller.ts
|
|
1011
|
-
var
|
|
1190
|
+
var import_core9 = require("@tiptap/core");
|
|
1012
1191
|
var import_model = require("@tiptap/pm/model");
|
|
1013
1192
|
var EditorController = class {
|
|
1014
1193
|
editor;
|
|
@@ -1032,7 +1211,7 @@ var EditorController = class {
|
|
|
1032
1211
|
this.onTokensChange = onTokensChange;
|
|
1033
1212
|
this.onSelectionChange = onSelectionChange;
|
|
1034
1213
|
const initialContent = plainTextMode && typeof content === "string" ? plainTextToTipTapHtml(content) : content;
|
|
1035
|
-
this.editor = new
|
|
1214
|
+
this.editor = new import_core9.Editor({
|
|
1036
1215
|
element: element || void 0,
|
|
1037
1216
|
content: initialContent,
|
|
1038
1217
|
extensions,
|
|
@@ -1114,37 +1293,1297 @@ var EditorController = class {
|
|
|
1114
1293
|
function createEditor(options = {}) {
|
|
1115
1294
|
return new EditorController(options);
|
|
1116
1295
|
}
|
|
1296
|
+
|
|
1297
|
+
// src/spreadsheet/formula/lexer.ts
|
|
1298
|
+
function tokenizeFormula(formula) {
|
|
1299
|
+
const tokens = [];
|
|
1300
|
+
let input = formula.trim();
|
|
1301
|
+
let offset = 0;
|
|
1302
|
+
if (input.startsWith("=")) {
|
|
1303
|
+
input = input.slice(1);
|
|
1304
|
+
offset = 1;
|
|
1305
|
+
}
|
|
1306
|
+
let i = 0;
|
|
1307
|
+
while (i < input.length) {
|
|
1308
|
+
const char = input[i];
|
|
1309
|
+
if (/\s/.test(char)) {
|
|
1310
|
+
i++;
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
if (/\d/.test(char) || char === "." && i + 1 < input.length && /\d/.test(input[i + 1])) {
|
|
1314
|
+
const start = i;
|
|
1315
|
+
let hasDot = char === ".";
|
|
1316
|
+
i++;
|
|
1317
|
+
while (i < input.length && (/\d/.test(input[i]) || input[i] === "." && !hasDot)) {
|
|
1318
|
+
if (input[i] === ".") hasDot = true;
|
|
1319
|
+
i++;
|
|
1320
|
+
}
|
|
1321
|
+
tokens.push({
|
|
1322
|
+
type: "NUMBER",
|
|
1323
|
+
value: input.slice(start, i),
|
|
1324
|
+
position: start + offset
|
|
1325
|
+
});
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
if (char === '"' || char === "'") {
|
|
1329
|
+
const quote = char;
|
|
1330
|
+
const start = i;
|
|
1331
|
+
i++;
|
|
1332
|
+
let str = "";
|
|
1333
|
+
while (i < input.length && input[i] !== quote) {
|
|
1334
|
+
if (input[i] === "\\" && i + 1 < input.length) {
|
|
1335
|
+
i++;
|
|
1336
|
+
}
|
|
1337
|
+
str += input[i];
|
|
1338
|
+
i++;
|
|
1339
|
+
}
|
|
1340
|
+
if (i < input.length && input[i] === quote) {
|
|
1341
|
+
i++;
|
|
1342
|
+
}
|
|
1343
|
+
tokens.push({
|
|
1344
|
+
type: "STRING",
|
|
1345
|
+
value: str,
|
|
1346
|
+
position: start + offset
|
|
1347
|
+
});
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
if (char === "[") {
|
|
1351
|
+
const start = i;
|
|
1352
|
+
i++;
|
|
1353
|
+
let prop = "";
|
|
1354
|
+
while (i < input.length && input[i] !== "]") {
|
|
1355
|
+
prop += input[i];
|
|
1356
|
+
i++;
|
|
1357
|
+
}
|
|
1358
|
+
if (i < input.length && input[i] === "]") {
|
|
1359
|
+
i++;
|
|
1360
|
+
}
|
|
1361
|
+
tokens.push({
|
|
1362
|
+
type: "PROPERTY_REF",
|
|
1363
|
+
value: prop.trim(),
|
|
1364
|
+
position: start + offset
|
|
1365
|
+
});
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
if (char === "(") {
|
|
1369
|
+
tokens.push({ type: "LPAREN", value: "(", position: i + offset });
|
|
1370
|
+
i++;
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
if (char === ")") {
|
|
1374
|
+
tokens.push({ type: "RPAREN", value: ")", position: i + offset });
|
|
1375
|
+
i++;
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
if (char === ",") {
|
|
1379
|
+
tokens.push({ type: "COMMA", value: ",", position: i + offset });
|
|
1380
|
+
i++;
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
const twoChars = input.slice(i, i + 2);
|
|
1384
|
+
if (["<=", ">=", "<>", "!="].includes(twoChars)) {
|
|
1385
|
+
tokens.push({
|
|
1386
|
+
type: "OPERATOR",
|
|
1387
|
+
value: twoChars === "!=" ? "<>" : twoChars,
|
|
1388
|
+
position: i + offset
|
|
1389
|
+
});
|
|
1390
|
+
i += 2;
|
|
1391
|
+
continue;
|
|
1392
|
+
}
|
|
1393
|
+
if (["+", "-", "*", "/", "^", "&", "=", "<", ">"].includes(char)) {
|
|
1394
|
+
tokens.push({ type: "OPERATOR", value: char, position: i + offset });
|
|
1395
|
+
i++;
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
if (/[a-zA-Z_$]/.test(char)) {
|
|
1399
|
+
const start = i;
|
|
1400
|
+
while (i < input.length && /[a-zA-Z0-9_$.]/.test(input[i])) {
|
|
1401
|
+
i++;
|
|
1402
|
+
}
|
|
1403
|
+
const rawWord = input.slice(start, i);
|
|
1404
|
+
if (i < input.length && input[i] === ":") {
|
|
1405
|
+
const colonPos = i;
|
|
1406
|
+
i++;
|
|
1407
|
+
const secondStart = i;
|
|
1408
|
+
while (i < input.length && /[a-zA-Z0-9_$.]/.test(input[i])) {
|
|
1409
|
+
i++;
|
|
1410
|
+
}
|
|
1411
|
+
const secondWord = input.slice(secondStart, i);
|
|
1412
|
+
if (secondWord.length > 0) {
|
|
1413
|
+
tokens.push({
|
|
1414
|
+
type: "RANGE",
|
|
1415
|
+
value: `${rawWord}:${secondWord}`.toUpperCase(),
|
|
1416
|
+
position: start + offset
|
|
1417
|
+
});
|
|
1418
|
+
continue;
|
|
1419
|
+
} else {
|
|
1420
|
+
i = colonPos;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
const upper = rawWord.toUpperCase();
|
|
1424
|
+
if (upper === "TRUE" || upper === "FALSE") {
|
|
1425
|
+
tokens.push({
|
|
1426
|
+
type: "BOOLEAN",
|
|
1427
|
+
value: upper,
|
|
1428
|
+
position: start + offset
|
|
1429
|
+
});
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
if (/^\$?[A-Za-z]+\$?[0-9]+$/.test(rawWord)) {
|
|
1433
|
+
tokens.push({
|
|
1434
|
+
type: "CELL_REF",
|
|
1435
|
+
value: upper.replace(/\$/g, ""),
|
|
1436
|
+
position: start + offset
|
|
1437
|
+
});
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
tokens.push({
|
|
1441
|
+
type: "IDENTIFIER",
|
|
1442
|
+
value: upper,
|
|
1443
|
+
position: start + offset
|
|
1444
|
+
});
|
|
1445
|
+
continue;
|
|
1446
|
+
}
|
|
1447
|
+
i++;
|
|
1448
|
+
}
|
|
1449
|
+
tokens.push({ type: "EOF", value: "", position: input.length + offset });
|
|
1450
|
+
return tokens;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// src/spreadsheet/formula/parser.ts
|
|
1454
|
+
function parseCellReference(ref) {
|
|
1455
|
+
const match = ref.match(/^([A-Za-z]+)(\d+)$/);
|
|
1456
|
+
if (!match) {
|
|
1457
|
+
throw new Error(`Invalid cell reference: ${ref}`);
|
|
1458
|
+
}
|
|
1459
|
+
return {
|
|
1460
|
+
col: match[1].toUpperCase(),
|
|
1461
|
+
row: parseInt(match[2], 10)
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
function parseRange(rangeStr) {
|
|
1465
|
+
const [start, end] = rangeStr.split(":");
|
|
1466
|
+
const startRef = parseCellReference(start);
|
|
1467
|
+
const endRef = parseCellReference(end);
|
|
1468
|
+
return {
|
|
1469
|
+
type: "Range",
|
|
1470
|
+
raw: rangeStr.toUpperCase(),
|
|
1471
|
+
startCol: startRef.col,
|
|
1472
|
+
startRow: startRef.row,
|
|
1473
|
+
endCol: endRef.col,
|
|
1474
|
+
endRow: endRef.row
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
var FormulaParser = class {
|
|
1478
|
+
tokens = [];
|
|
1479
|
+
current = 0;
|
|
1480
|
+
constructor(tokens) {
|
|
1481
|
+
this.tokens = tokens;
|
|
1482
|
+
}
|
|
1483
|
+
parse() {
|
|
1484
|
+
if (this.peek().type === "EOF") {
|
|
1485
|
+
return { type: "Literal", value: "" };
|
|
1486
|
+
}
|
|
1487
|
+
const expr = this.expression();
|
|
1488
|
+
if (this.peek().type !== "EOF") {
|
|
1489
|
+
throw new Error(`Unexpected token at position ${this.peek().position}: ${this.peek().value}`);
|
|
1490
|
+
}
|
|
1491
|
+
return expr;
|
|
1492
|
+
}
|
|
1493
|
+
expression() {
|
|
1494
|
+
return this.comparison();
|
|
1495
|
+
}
|
|
1496
|
+
comparison() {
|
|
1497
|
+
let expr = this.concatenation();
|
|
1498
|
+
while (this.matchOperator("=", "<>", "<", ">", "<=", ">=")) {
|
|
1499
|
+
const op = this.previous().value;
|
|
1500
|
+
const right = this.concatenation();
|
|
1501
|
+
expr = { type: "BinaryOp", op, left: expr, right };
|
|
1502
|
+
}
|
|
1503
|
+
return expr;
|
|
1504
|
+
}
|
|
1505
|
+
concatenation() {
|
|
1506
|
+
let expr = this.addition();
|
|
1507
|
+
while (this.matchOperator("&")) {
|
|
1508
|
+
const op = this.previous().value;
|
|
1509
|
+
const right = this.addition();
|
|
1510
|
+
expr = { type: "BinaryOp", op, left: expr, right };
|
|
1511
|
+
}
|
|
1512
|
+
return expr;
|
|
1513
|
+
}
|
|
1514
|
+
addition() {
|
|
1515
|
+
let expr = this.multiplication();
|
|
1516
|
+
while (this.matchOperator("+", "-")) {
|
|
1517
|
+
const op = this.previous().value;
|
|
1518
|
+
const right = this.multiplication();
|
|
1519
|
+
expr = { type: "BinaryOp", op, left: expr, right };
|
|
1520
|
+
}
|
|
1521
|
+
return expr;
|
|
1522
|
+
}
|
|
1523
|
+
multiplication() {
|
|
1524
|
+
let expr = this.exponentiation();
|
|
1525
|
+
while (this.matchOperator("*", "/")) {
|
|
1526
|
+
const op = this.previous().value;
|
|
1527
|
+
const right = this.exponentiation();
|
|
1528
|
+
expr = { type: "BinaryOp", op, left: expr, right };
|
|
1529
|
+
}
|
|
1530
|
+
return expr;
|
|
1531
|
+
}
|
|
1532
|
+
exponentiation() {
|
|
1533
|
+
let expr = this.unary();
|
|
1534
|
+
while (this.matchOperator("^")) {
|
|
1535
|
+
const op = this.previous().value;
|
|
1536
|
+
const right = this.unary();
|
|
1537
|
+
expr = { type: "BinaryOp", op, left: expr, right };
|
|
1538
|
+
}
|
|
1539
|
+
return expr;
|
|
1540
|
+
}
|
|
1541
|
+
unary() {
|
|
1542
|
+
if (this.matchOperator("-", "+")) {
|
|
1543
|
+
const op = this.previous().value;
|
|
1544
|
+
const argument = this.unary();
|
|
1545
|
+
return { type: "UnaryOp", op, argument };
|
|
1546
|
+
}
|
|
1547
|
+
return this.primary();
|
|
1548
|
+
}
|
|
1549
|
+
primary() {
|
|
1550
|
+
const token = this.peek();
|
|
1551
|
+
if (token.type === "NUMBER") {
|
|
1552
|
+
this.advance();
|
|
1553
|
+
return { type: "Literal", value: parseFloat(token.value) };
|
|
1554
|
+
}
|
|
1555
|
+
if (token.type === "STRING") {
|
|
1556
|
+
this.advance();
|
|
1557
|
+
return { type: "Literal", value: token.value };
|
|
1558
|
+
}
|
|
1559
|
+
if (token.type === "BOOLEAN") {
|
|
1560
|
+
this.advance();
|
|
1561
|
+
return { type: "Literal", value: token.value === "TRUE" };
|
|
1562
|
+
}
|
|
1563
|
+
if (token.type === "CELL_REF") {
|
|
1564
|
+
this.advance();
|
|
1565
|
+
const parsed = parseCellReference(token.value);
|
|
1566
|
+
return {
|
|
1567
|
+
type: "CellRef",
|
|
1568
|
+
ref: token.value,
|
|
1569
|
+
col: parsed.col,
|
|
1570
|
+
row: parsed.row
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
if (token.type === "RANGE") {
|
|
1574
|
+
this.advance();
|
|
1575
|
+
return parseRange(token.value);
|
|
1576
|
+
}
|
|
1577
|
+
if (token.type === "PROPERTY_REF") {
|
|
1578
|
+
this.advance();
|
|
1579
|
+
return {
|
|
1580
|
+
type: "PropertyRef",
|
|
1581
|
+
property: token.value
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
if (token.type === "IDENTIFIER") {
|
|
1585
|
+
const name = token.value;
|
|
1586
|
+
this.advance();
|
|
1587
|
+
if (this.check("LPAREN")) {
|
|
1588
|
+
this.consume("LPAREN", `Expected '(' after function ${name}`);
|
|
1589
|
+
const args = [];
|
|
1590
|
+
if (!this.check("RPAREN")) {
|
|
1591
|
+
do {
|
|
1592
|
+
args.push(this.expression());
|
|
1593
|
+
} while (this.match("COMMA"));
|
|
1594
|
+
}
|
|
1595
|
+
this.consume("RPAREN", `Expected ')' after function arguments`);
|
|
1596
|
+
return {
|
|
1597
|
+
type: "FunctionCall",
|
|
1598
|
+
name,
|
|
1599
|
+
args
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
return { type: "Literal", value: name };
|
|
1603
|
+
}
|
|
1604
|
+
if (this.match("LPAREN")) {
|
|
1605
|
+
const expr = this.expression();
|
|
1606
|
+
this.consume("RPAREN", `Expected ')' after expression`);
|
|
1607
|
+
return expr;
|
|
1608
|
+
}
|
|
1609
|
+
throw new Error(`Unexpected token at position ${token.position}: ${token.value || token.type}`);
|
|
1610
|
+
}
|
|
1611
|
+
match(...types) {
|
|
1612
|
+
for (const type of types) {
|
|
1613
|
+
if (this.check(type)) {
|
|
1614
|
+
this.advance();
|
|
1615
|
+
return true;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
return false;
|
|
1619
|
+
}
|
|
1620
|
+
matchOperator(...ops) {
|
|
1621
|
+
if (this.check("OPERATOR") && ops.includes(this.peek().value)) {
|
|
1622
|
+
this.advance();
|
|
1623
|
+
return true;
|
|
1624
|
+
}
|
|
1625
|
+
return false;
|
|
1626
|
+
}
|
|
1627
|
+
check(type) {
|
|
1628
|
+
if (this.isAtEnd()) return false;
|
|
1629
|
+
return this.peek().type === type;
|
|
1630
|
+
}
|
|
1631
|
+
advance() {
|
|
1632
|
+
if (!this.isAtEnd()) this.current++;
|
|
1633
|
+
return this.previous();
|
|
1634
|
+
}
|
|
1635
|
+
isAtEnd() {
|
|
1636
|
+
return this.peek().type === "EOF";
|
|
1637
|
+
}
|
|
1638
|
+
peek() {
|
|
1639
|
+
return this.tokens[this.current];
|
|
1640
|
+
}
|
|
1641
|
+
previous() {
|
|
1642
|
+
return this.tokens[this.current - 1];
|
|
1643
|
+
}
|
|
1644
|
+
consume(type, message) {
|
|
1645
|
+
if (this.check(type)) return this.advance();
|
|
1646
|
+
throw new Error(message);
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
function parseFormula(formula) {
|
|
1650
|
+
const tokens = tokenizeFormula(formula);
|
|
1651
|
+
const parser = new FormulaParser(tokens);
|
|
1652
|
+
return parser.parse();
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// src/spreadsheet/formula/evaluator.ts
|
|
1656
|
+
function colNameToIndex(col) {
|
|
1657
|
+
let index = 0;
|
|
1658
|
+
const upper = col.toUpperCase();
|
|
1659
|
+
for (let i = 0; i < upper.length; i++) {
|
|
1660
|
+
index = index * 26 + (upper.charCodeAt(i) - 65 + 1);
|
|
1661
|
+
}
|
|
1662
|
+
return index - 1;
|
|
1663
|
+
}
|
|
1664
|
+
function indexToColName(index) {
|
|
1665
|
+
let num = index + 1;
|
|
1666
|
+
let name = "";
|
|
1667
|
+
while (num > 0) {
|
|
1668
|
+
const mod = (num - 1) % 26;
|
|
1669
|
+
name = String.fromCharCode(65 + mod) + name;
|
|
1670
|
+
num = Math.floor((num - mod) / 26);
|
|
1671
|
+
}
|
|
1672
|
+
return name;
|
|
1673
|
+
}
|
|
1674
|
+
var FormulaEvaluator = class {
|
|
1675
|
+
context;
|
|
1676
|
+
constructor(context = {}) {
|
|
1677
|
+
this.context = context;
|
|
1678
|
+
}
|
|
1679
|
+
evaluate(astOrFormula) {
|
|
1680
|
+
const ast = typeof astOrFormula === "string" ? parseFormula(astOrFormula) : astOrFormula;
|
|
1681
|
+
return this.evaluateNode(ast);
|
|
1682
|
+
}
|
|
1683
|
+
evaluateNode(node) {
|
|
1684
|
+
switch (node.type) {
|
|
1685
|
+
case "Literal":
|
|
1686
|
+
return node.value;
|
|
1687
|
+
case "CellRef": {
|
|
1688
|
+
if (!this.context.resolveCell) {
|
|
1689
|
+
throw new Error(`Cannot resolve cell reference ${node.ref}: no cell resolver provided`);
|
|
1690
|
+
}
|
|
1691
|
+
return this.context.resolveCell(node.col, node.row);
|
|
1692
|
+
}
|
|
1693
|
+
case "Range": {
|
|
1694
|
+
if (!this.context.resolveRange) {
|
|
1695
|
+
throw new Error(`Cannot resolve range ${node.raw}: no range resolver provided`);
|
|
1696
|
+
}
|
|
1697
|
+
return this.context.resolveRange(node.startCol, node.startRow, node.endCol, node.endRow);
|
|
1698
|
+
}
|
|
1699
|
+
case "PropertyRef": {
|
|
1700
|
+
if (!this.context.resolveProperty) {
|
|
1701
|
+
throw new Error(`Cannot resolve property [${node.property}]: no property resolver provided`);
|
|
1702
|
+
}
|
|
1703
|
+
return this.context.resolveProperty(node.property);
|
|
1704
|
+
}
|
|
1705
|
+
case "UnaryOp": {
|
|
1706
|
+
const val = this.evaluateNode(node.argument);
|
|
1707
|
+
if (node.op === "-") return -Number(val);
|
|
1708
|
+
if (node.op === "+") return +Number(val);
|
|
1709
|
+
throw new Error(`Unsupported unary operator: ${node.op}`);
|
|
1710
|
+
}
|
|
1711
|
+
case "BinaryOp": {
|
|
1712
|
+
const left = this.evaluateNode(node.left);
|
|
1713
|
+
const right = this.evaluateNode(node.right);
|
|
1714
|
+
switch (node.op) {
|
|
1715
|
+
case "+":
|
|
1716
|
+
return Number(left) + Number(right);
|
|
1717
|
+
case "-":
|
|
1718
|
+
return Number(left) - Number(right);
|
|
1719
|
+
case "*":
|
|
1720
|
+
return Number(left) * Number(right);
|
|
1721
|
+
case "/": {
|
|
1722
|
+
const denom = Number(right);
|
|
1723
|
+
if (denom === 0) return "#DIV/0!";
|
|
1724
|
+
return Number(left) / denom;
|
|
1725
|
+
}
|
|
1726
|
+
case "^":
|
|
1727
|
+
return Math.pow(Number(left), Number(right));
|
|
1728
|
+
case "&":
|
|
1729
|
+
return `${left ?? ""}${right ?? ""}`;
|
|
1730
|
+
case "=":
|
|
1731
|
+
return String(left).toLowerCase() === String(right).toLowerCase();
|
|
1732
|
+
case "<>":
|
|
1733
|
+
return String(left).toLowerCase() !== String(right).toLowerCase();
|
|
1734
|
+
case "<":
|
|
1735
|
+
return Number(left) < Number(right);
|
|
1736
|
+
case "<=":
|
|
1737
|
+
return Number(left) <= Number(right);
|
|
1738
|
+
case ">":
|
|
1739
|
+
return Number(left) > Number(right);
|
|
1740
|
+
case ">=":
|
|
1741
|
+
return Number(left) >= Number(right);
|
|
1742
|
+
default:
|
|
1743
|
+
throw new Error(`Unsupported binary operator: ${node.op}`);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
case "FunctionCall": {
|
|
1747
|
+
return this.callFunction(node.name, node.args);
|
|
1748
|
+
}
|
|
1749
|
+
default:
|
|
1750
|
+
throw new Error(`Unknown AST node: ${node.type}`);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
callFunction(name, args) {
|
|
1754
|
+
const fnName = name.toUpperCase();
|
|
1755
|
+
const evaluateAndFlatten = () => {
|
|
1756
|
+
const items = [];
|
|
1757
|
+
for (const arg of args) {
|
|
1758
|
+
const val = this.evaluateNode(arg);
|
|
1759
|
+
if (Array.isArray(val)) {
|
|
1760
|
+
items.push(...val);
|
|
1761
|
+
} else {
|
|
1762
|
+
items.push(val);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
return items;
|
|
1766
|
+
};
|
|
1767
|
+
switch (fnName) {
|
|
1768
|
+
case "SUM": {
|
|
1769
|
+
const items = evaluateAndFlatten();
|
|
1770
|
+
let total = 0;
|
|
1771
|
+
for (const item of items) {
|
|
1772
|
+
const num = Number(item);
|
|
1773
|
+
if (!isNaN(num)) total += num;
|
|
1774
|
+
}
|
|
1775
|
+
return total;
|
|
1776
|
+
}
|
|
1777
|
+
case "AVERAGE": {
|
|
1778
|
+
const items = evaluateAndFlatten();
|
|
1779
|
+
let total = 0;
|
|
1780
|
+
let count = 0;
|
|
1781
|
+
for (const item of items) {
|
|
1782
|
+
const num = Number(item);
|
|
1783
|
+
if (!isNaN(num) && item !== "" && item !== null && item !== void 0) {
|
|
1784
|
+
total += num;
|
|
1785
|
+
count++;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
return count === 0 ? 0 : total / count;
|
|
1789
|
+
}
|
|
1790
|
+
case "COUNT": {
|
|
1791
|
+
const items = evaluateAndFlatten();
|
|
1792
|
+
let count = 0;
|
|
1793
|
+
for (const item of items) {
|
|
1794
|
+
const num = Number(item);
|
|
1795
|
+
if (!isNaN(num) && item !== "" && item !== null && item !== void 0) {
|
|
1796
|
+
count++;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
return count;
|
|
1800
|
+
}
|
|
1801
|
+
case "COUNTA": {
|
|
1802
|
+
const items = evaluateAndFlatten();
|
|
1803
|
+
let count = 0;
|
|
1804
|
+
for (const item of items) {
|
|
1805
|
+
if (item !== "" && item !== null && item !== void 0) {
|
|
1806
|
+
count++;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
return count;
|
|
1810
|
+
}
|
|
1811
|
+
case "MIN": {
|
|
1812
|
+
const items = evaluateAndFlatten().map(Number).filter((n) => !isNaN(n));
|
|
1813
|
+
return items.length === 0 ? 0 : Math.min(...items);
|
|
1814
|
+
}
|
|
1815
|
+
case "MAX": {
|
|
1816
|
+
const items = evaluateAndFlatten().map(Number).filter((n) => !isNaN(n));
|
|
1817
|
+
return items.length === 0 ? 0 : Math.max(...items);
|
|
1818
|
+
}
|
|
1819
|
+
case "IF": {
|
|
1820
|
+
if (args.length < 2) throw new Error("IF requires at least 2 arguments");
|
|
1821
|
+
const condition = Boolean(this.evaluateNode(args[0]));
|
|
1822
|
+
if (condition) {
|
|
1823
|
+
return this.evaluateNode(args[1]);
|
|
1824
|
+
} else {
|
|
1825
|
+
return args.length > 2 ? this.evaluateNode(args[2]) : false;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
case "CONCAT": {
|
|
1829
|
+
const items = evaluateAndFlatten();
|
|
1830
|
+
return items.map((i) => i === null || i === void 0 ? "" : String(i)).join("");
|
|
1831
|
+
}
|
|
1832
|
+
case "UPPER": {
|
|
1833
|
+
if (args.length !== 1) throw new Error("UPPER requires 1 argument");
|
|
1834
|
+
const val = this.evaluateNode(args[0]);
|
|
1835
|
+
return String(val ?? "").toUpperCase();
|
|
1836
|
+
}
|
|
1837
|
+
case "LOWER": {
|
|
1838
|
+
if (args.length !== 1) throw new Error("LOWER requires 1 argument");
|
|
1839
|
+
const val = this.evaluateNode(args[0]);
|
|
1840
|
+
return String(val ?? "").toLowerCase();
|
|
1841
|
+
}
|
|
1842
|
+
case "TRIM": {
|
|
1843
|
+
if (args.length !== 1) throw new Error("TRIM requires 1 argument");
|
|
1844
|
+
const val = this.evaluateNode(args[0]);
|
|
1845
|
+
return String(val ?? "").trim();
|
|
1846
|
+
}
|
|
1847
|
+
case "LEN": {
|
|
1848
|
+
if (args.length !== 1) throw new Error("LEN requires 1 argument");
|
|
1849
|
+
const val = this.evaluateNode(args[0]);
|
|
1850
|
+
return String(val ?? "").length;
|
|
1851
|
+
}
|
|
1852
|
+
case "ROUND": {
|
|
1853
|
+
if (args.length < 1) throw new Error("ROUND requires at least 1 argument");
|
|
1854
|
+
const num = Number(this.evaluateNode(args[0]));
|
|
1855
|
+
const decimals = args.length > 1 ? Number(this.evaluateNode(args[1])) : 0;
|
|
1856
|
+
const factor = Math.pow(10, decimals);
|
|
1857
|
+
return Math.round(num * factor) / factor;
|
|
1858
|
+
}
|
|
1859
|
+
case "ABS": {
|
|
1860
|
+
if (args.length !== 1) throw new Error("ABS requires 1 argument");
|
|
1861
|
+
return Math.abs(Number(this.evaluateNode(args[0])));
|
|
1862
|
+
}
|
|
1863
|
+
case "TODAY": {
|
|
1864
|
+
return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1865
|
+
}
|
|
1866
|
+
case "DAYS": {
|
|
1867
|
+
if (args.length !== 2) throw new Error("DAYS requires 2 arguments (end_date, start_date)");
|
|
1868
|
+
const d1 = new Date(this.evaluateNode(args[0]));
|
|
1869
|
+
const d2 = new Date(this.evaluateNode(args[1]));
|
|
1870
|
+
const diffTime = d1.getTime() - d2.getTime();
|
|
1871
|
+
return Math.round(diffTime / (1e3 * 60 * 60 * 24));
|
|
1872
|
+
}
|
|
1873
|
+
default:
|
|
1874
|
+
throw new Error(`Unknown function: ${fnName}`);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1878
|
+
|
|
1879
|
+
// src/spreadsheet/formula/dependency-graph.ts
|
|
1880
|
+
function extractCellDependencies(node) {
|
|
1881
|
+
const deps = [];
|
|
1882
|
+
const visit = (curr) => {
|
|
1883
|
+
if (curr.type === "CellRef") {
|
|
1884
|
+
deps.push({ col: curr.col, row: curr.row });
|
|
1885
|
+
} else if (curr.type === "Range") {
|
|
1886
|
+
const startC = colNameToIndex(curr.startCol);
|
|
1887
|
+
const endC = colNameToIndex(curr.endCol);
|
|
1888
|
+
const minC = Math.min(startC, endC);
|
|
1889
|
+
const maxC = Math.max(startC, endC);
|
|
1890
|
+
const minR = Math.min(curr.startRow, curr.endRow);
|
|
1891
|
+
const maxR = Math.max(curr.startRow, curr.endRow);
|
|
1892
|
+
for (let c = minC; c <= maxC; c++) {
|
|
1893
|
+
for (let r = minR; r <= maxR; r++) {
|
|
1894
|
+
deps.push({ col: indexToColName(c), row: r });
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
} else if (curr.type === "BinaryOp") {
|
|
1898
|
+
visit(curr.left);
|
|
1899
|
+
visit(curr.right);
|
|
1900
|
+
} else if (curr.type === "UnaryOp") {
|
|
1901
|
+
visit(curr.argument);
|
|
1902
|
+
} else if (curr.type === "FunctionCall") {
|
|
1903
|
+
curr.args.forEach(visit);
|
|
1904
|
+
}
|
|
1905
|
+
};
|
|
1906
|
+
visit(node);
|
|
1907
|
+
return deps;
|
|
1908
|
+
}
|
|
1909
|
+
var DependencyGraph = class {
|
|
1910
|
+
// Map of cellKey (e.g. "B2") -> Set of cellKeys that this cell depends ON
|
|
1911
|
+
dependencies = /* @__PURE__ */ new Map();
|
|
1912
|
+
// Map of cellKey (e.g. "A1") -> Set of cellKeys that depend ON this cell (dependents)
|
|
1913
|
+
dependents = /* @__PURE__ */ new Map();
|
|
1914
|
+
setCellFormula(cellKey, formulaStr) {
|
|
1915
|
+
const key = cellKey.toUpperCase();
|
|
1916
|
+
const oldDeps = this.dependencies.get(key) || /* @__PURE__ */ new Set();
|
|
1917
|
+
for (const dep of oldDeps) {
|
|
1918
|
+
this.dependents.get(dep)?.delete(key);
|
|
1919
|
+
}
|
|
1920
|
+
this.dependencies.delete(key);
|
|
1921
|
+
if (!formulaStr || !formulaStr.startsWith("=")) {
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
try {
|
|
1925
|
+
const ast = parseFormula(formulaStr);
|
|
1926
|
+
const deps = extractCellDependencies(ast);
|
|
1927
|
+
const newDeps = /* @__PURE__ */ new Set();
|
|
1928
|
+
for (const d of deps) {
|
|
1929
|
+
const depKey = `${d.col}${d.row}`.toUpperCase();
|
|
1930
|
+
newDeps.add(depKey);
|
|
1931
|
+
if (!this.dependents.has(depKey)) {
|
|
1932
|
+
this.dependents.set(depKey, /* @__PURE__ */ new Set());
|
|
1933
|
+
}
|
|
1934
|
+
this.dependents.get(depKey).add(key);
|
|
1935
|
+
}
|
|
1936
|
+
this.dependencies.set(key, newDeps);
|
|
1937
|
+
} catch {
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
getDependencies(cellKey) {
|
|
1941
|
+
return Array.from(this.dependencies.get(cellKey.toUpperCase()) || []);
|
|
1942
|
+
}
|
|
1943
|
+
getDependents(cellKey) {
|
|
1944
|
+
return Array.from(this.dependents.get(cellKey.toUpperCase()) || []);
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Detects if there is a cycle containing cellKey.
|
|
1948
|
+
*/
|
|
1949
|
+
hasCycle(cellKey) {
|
|
1950
|
+
const key = cellKey.toUpperCase();
|
|
1951
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1952
|
+
const recStack = /* @__PURE__ */ new Set();
|
|
1953
|
+
const dfs = (node) => {
|
|
1954
|
+
visited.add(node);
|
|
1955
|
+
recStack.add(node);
|
|
1956
|
+
const deps = this.dependencies.get(node) || /* @__PURE__ */ new Set();
|
|
1957
|
+
for (const neighbor of deps) {
|
|
1958
|
+
if (!visited.has(neighbor)) {
|
|
1959
|
+
if (dfs(neighbor)) return true;
|
|
1960
|
+
} else if (recStack.has(neighbor)) {
|
|
1961
|
+
return true;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
recStack.delete(node);
|
|
1965
|
+
return false;
|
|
1966
|
+
};
|
|
1967
|
+
return dfs(key);
|
|
1968
|
+
}
|
|
1969
|
+
/**
|
|
1970
|
+
* Returns cells affected by updating cellKey in topological order.
|
|
1971
|
+
*/
|
|
1972
|
+
getEvaluationOrder(startCellKey) {
|
|
1973
|
+
const start = startCellKey.toUpperCase();
|
|
1974
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1975
|
+
const recStack = /* @__PURE__ */ new Set();
|
|
1976
|
+
const order = [];
|
|
1977
|
+
let cycleDetected = false;
|
|
1978
|
+
const affected = /* @__PURE__ */ new Set();
|
|
1979
|
+
const collectAffected = (node) => {
|
|
1980
|
+
affected.add(node);
|
|
1981
|
+
const directDependents = this.dependents.get(node) || /* @__PURE__ */ new Set();
|
|
1982
|
+
for (const dep of directDependents) {
|
|
1983
|
+
if (!affected.has(dep)) {
|
|
1984
|
+
collectAffected(dep);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
1988
|
+
collectAffected(start);
|
|
1989
|
+
const visit = (node) => {
|
|
1990
|
+
if (recStack.has(node)) {
|
|
1991
|
+
cycleDetected = true;
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
if (visited.has(node)) return;
|
|
1995
|
+
recStack.add(node);
|
|
1996
|
+
visited.add(node);
|
|
1997
|
+
const deps = this.dependencies.get(node) || /* @__PURE__ */ new Set();
|
|
1998
|
+
for (const neighbor of deps) {
|
|
1999
|
+
if (affected.has(neighbor)) {
|
|
2000
|
+
visit(neighbor);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
recStack.delete(node);
|
|
2004
|
+
order.push(node);
|
|
2005
|
+
};
|
|
2006
|
+
for (const node of affected) {
|
|
2007
|
+
if (!visited.has(node)) {
|
|
2008
|
+
visit(node);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
return { order, hasCycle: cycleDetected };
|
|
2012
|
+
}
|
|
2013
|
+
};
|
|
2014
|
+
|
|
2015
|
+
// src/spreadsheet/datasource/index.ts
|
|
2016
|
+
var DataSourceBinder = class {
|
|
2017
|
+
dataSources = /* @__PURE__ */ new Map();
|
|
2018
|
+
register(dataSource) {
|
|
2019
|
+
this.dataSources.set(dataSource.id, dataSource);
|
|
2020
|
+
}
|
|
2021
|
+
unregister(dataSourceId) {
|
|
2022
|
+
this.dataSources.delete(dataSourceId);
|
|
2023
|
+
}
|
|
2024
|
+
getDataSource(id) {
|
|
2025
|
+
return this.dataSources.get(id);
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Generates or reconciles spreadsheet rows from a primary data source.
|
|
2029
|
+
*/
|
|
2030
|
+
syncRows(currentRows, dataSourceId) {
|
|
2031
|
+
const ds = this.dataSources.get(dataSourceId);
|
|
2032
|
+
if (!ds) return currentRows;
|
|
2033
|
+
const records = ds.getRecords();
|
|
2034
|
+
const existingRecordRowMap = /* @__PURE__ */ new Map();
|
|
2035
|
+
const freeformRows = [];
|
|
2036
|
+
for (const row of currentRows) {
|
|
2037
|
+
if (row.type === "record" && row.recordId) {
|
|
2038
|
+
existingRecordRowMap.set(row.recordId, row);
|
|
2039
|
+
} else {
|
|
2040
|
+
freeformRows.push(row);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
const nextRows = [];
|
|
2044
|
+
let rowIndex = 0;
|
|
2045
|
+
for (const record of records) {
|
|
2046
|
+
const existing = existingRecordRowMap.get(record.id);
|
|
2047
|
+
nextRows.push({
|
|
2048
|
+
id: existing?.id || `row_${record.id}`,
|
|
2049
|
+
index: rowIndex++,
|
|
2050
|
+
height: existing?.height || 32,
|
|
2051
|
+
recordId: record.id,
|
|
2052
|
+
type: "record"
|
|
2053
|
+
});
|
|
2054
|
+
existingRecordRowMap.delete(record.id);
|
|
2055
|
+
}
|
|
2056
|
+
for (const freeform of freeformRows) {
|
|
2057
|
+
nextRows.push({
|
|
2058
|
+
...freeform,
|
|
2059
|
+
index: rowIndex++
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
return nextRows;
|
|
2063
|
+
}
|
|
2064
|
+
/**
|
|
2065
|
+
* Resolves a record's bound field value.
|
|
2066
|
+
*/
|
|
2067
|
+
resolveBoundValue(dataSourceId, recordId, field) {
|
|
2068
|
+
const ds = this.dataSources.get(dataSourceId);
|
|
2069
|
+
if (!ds) return void 0;
|
|
2070
|
+
const records = ds.getRecords();
|
|
2071
|
+
const record = records.find((r) => r.id === recordId);
|
|
2072
|
+
return record ? record[field] : void 0;
|
|
2073
|
+
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Dispatches an update to the underlying data source.
|
|
2076
|
+
*/
|
|
2077
|
+
async commitBoundValue(dataSourceId, recordId, field, value) {
|
|
2078
|
+
const ds = this.dataSources.get(dataSourceId);
|
|
2079
|
+
if (!ds) return;
|
|
2080
|
+
await ds.updateRecord(recordId, field, value);
|
|
2081
|
+
}
|
|
2082
|
+
};
|
|
2083
|
+
|
|
2084
|
+
// src/spreadsheet/controller.ts
|
|
2085
|
+
var SpreadsheetController = class {
|
|
2086
|
+
document;
|
|
2087
|
+
activeCell = null;
|
|
2088
|
+
selectedRange = null;
|
|
2089
|
+
editingCell = null;
|
|
2090
|
+
draftValue = "";
|
|
2091
|
+
binder = new DataSourceBinder();
|
|
2092
|
+
depGraph = new DependencyGraph();
|
|
2093
|
+
evaluator;
|
|
2094
|
+
primaryDataSourceId;
|
|
2095
|
+
onDocumentChange;
|
|
2096
|
+
onSelectionChange;
|
|
2097
|
+
onCellCommit;
|
|
2098
|
+
// History stack for undo/redo
|
|
2099
|
+
undoStack = [];
|
|
2100
|
+
redoStack = [];
|
|
2101
|
+
isApplyingHistory = false;
|
|
2102
|
+
constructor(options = {}) {
|
|
2103
|
+
const defaultCols = [
|
|
2104
|
+
{ id: "col_a", key: "A", title: "A", width: 120, type: "freeform" },
|
|
2105
|
+
{ id: "col_b", key: "B", title: "B", width: 120, type: "freeform" },
|
|
2106
|
+
{ id: "col_c", key: "C", title: "C", width: 120, type: "freeform" },
|
|
2107
|
+
{ id: "col_d", key: "D", title: "D", width: 120, type: "freeform" }
|
|
2108
|
+
];
|
|
2109
|
+
const defaultRows = options.primaryDataSourceId ? [] : Array.from({ length: 10 }, (_, i) => ({
|
|
2110
|
+
id: `row_${i + 1}`,
|
|
2111
|
+
index: i,
|
|
2112
|
+
height: 32,
|
|
2113
|
+
type: "freeform"
|
|
2114
|
+
}));
|
|
2115
|
+
this.document = {
|
|
2116
|
+
id: options.document?.id || `sheet_${Date.now()}`,
|
|
2117
|
+
name: options.document?.name || "Sheet 1",
|
|
2118
|
+
columns: options.document?.columns || defaultCols,
|
|
2119
|
+
rows: options.document?.rows || defaultRows,
|
|
2120
|
+
cells: options.document?.cells || {},
|
|
2121
|
+
createdAt: options.document?.createdAt || Date.now(),
|
|
2122
|
+
updatedAt: options.document?.updatedAt || Date.now()
|
|
2123
|
+
};
|
|
2124
|
+
this.primaryDataSourceId = options.primaryDataSourceId;
|
|
2125
|
+
this.onDocumentChange = options.onDocumentChange;
|
|
2126
|
+
this.onSelectionChange = options.onSelectionChange;
|
|
2127
|
+
this.onCellCommit = options.onCellCommit;
|
|
2128
|
+
if (options.dataSources) {
|
|
2129
|
+
options.dataSources.forEach((ds) => this.binder.register(ds));
|
|
2130
|
+
}
|
|
2131
|
+
this.evaluator = new FormulaEvaluator({
|
|
2132
|
+
resolveCell: (colStr, rowNum) => this.resolveCellByCoord(colStr, rowNum),
|
|
2133
|
+
resolveRange: (sCol, sRow, eCol, eRow) => this.resolveRangeByCoords(sCol, sRow, eCol, eRow),
|
|
2134
|
+
resolveProperty: (prop) => this.resolveCurrentRowProperty(prop)
|
|
2135
|
+
});
|
|
2136
|
+
this.syncWithDataSource();
|
|
2137
|
+
this.recalculateAll();
|
|
2138
|
+
}
|
|
2139
|
+
registerDataSource(ds) {
|
|
2140
|
+
this.binder.register(ds);
|
|
2141
|
+
this.syncWithDataSource();
|
|
2142
|
+
this.recalculateAll();
|
|
2143
|
+
}
|
|
2144
|
+
setPrimaryDataSource(dataSourceId) {
|
|
2145
|
+
this.primaryDataSourceId = dataSourceId;
|
|
2146
|
+
this.syncWithDataSource();
|
|
2147
|
+
this.recalculateAll();
|
|
2148
|
+
}
|
|
2149
|
+
syncWithDataSource() {
|
|
2150
|
+
if (!this.primaryDataSourceId) return;
|
|
2151
|
+
this.document.rows = this.binder.syncRows(this.document.rows, this.primaryDataSourceId);
|
|
2152
|
+
this.recomputeRowIndices();
|
|
2153
|
+
}
|
|
2154
|
+
recomputeRowIndices() {
|
|
2155
|
+
this.document.rows.forEach((r, idx) => {
|
|
2156
|
+
r.index = idx;
|
|
2157
|
+
});
|
|
2158
|
+
this.document.columns.forEach((c, idx) => {
|
|
2159
|
+
c.key = indexToColName(idx);
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
getColumn(colIdOrKey) {
|
|
2163
|
+
return this.document.columns.find(
|
|
2164
|
+
(c) => c.id === colIdOrKey || c.key.toUpperCase() === colIdOrKey.toUpperCase()
|
|
2165
|
+
);
|
|
2166
|
+
}
|
|
2167
|
+
getRow(rowIdOrIndex) {
|
|
2168
|
+
if (typeof rowIdOrIndex === "number") {
|
|
2169
|
+
return this.document.rows[rowIdOrIndex];
|
|
2170
|
+
}
|
|
2171
|
+
return this.document.rows.find((r) => r.id === rowIdOrIndex);
|
|
2172
|
+
}
|
|
2173
|
+
getCell(rowId, colId) {
|
|
2174
|
+
const existing = this.document.cells[rowId]?.[colId];
|
|
2175
|
+
if (existing) return existing;
|
|
2176
|
+
const col = this.document.columns.find((c) => c.id === colId);
|
|
2177
|
+
const row = this.document.rows.find((r) => r.id === rowId);
|
|
2178
|
+
if (col && row && col.type === "bound" && col.binding && row.recordId) {
|
|
2179
|
+
const boundVal = this.binder.resolveBoundValue(
|
|
2180
|
+
col.binding.dataSource,
|
|
2181
|
+
row.recordId,
|
|
2182
|
+
col.binding.field
|
|
2183
|
+
);
|
|
2184
|
+
return {
|
|
2185
|
+
raw: boundVal !== void 0 ? String(boundVal) : "",
|
|
2186
|
+
value: boundVal ?? "",
|
|
2187
|
+
format: col.format
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
if (col && col.type === "formula" && col.formula) {
|
|
2191
|
+
return {
|
|
2192
|
+
raw: col.formula,
|
|
2193
|
+
value: "",
|
|
2194
|
+
format: col.format
|
|
2195
|
+
};
|
|
2196
|
+
}
|
|
2197
|
+
return { raw: "", value: "", format: col?.format };
|
|
2198
|
+
}
|
|
2199
|
+
setCellValue(rowId, colId, raw) {
|
|
2200
|
+
this.recordSnapshot();
|
|
2201
|
+
if (!this.document.cells[rowId]) {
|
|
2202
|
+
this.document.cells[rowId] = {};
|
|
2203
|
+
}
|
|
2204
|
+
const col = this.document.columns.find((c) => c.id === colId);
|
|
2205
|
+
const row = this.document.rows.find((r) => r.id === rowId);
|
|
2206
|
+
if (col?.type === "bound" && col.binding && row?.recordId) {
|
|
2207
|
+
this.binder.commitBoundValue(
|
|
2208
|
+
col.binding.dataSource,
|
|
2209
|
+
row.recordId,
|
|
2210
|
+
col.binding.field,
|
|
2211
|
+
raw
|
|
2212
|
+
);
|
|
2213
|
+
}
|
|
2214
|
+
const cellKey = this.getCellKey(rowId, colId);
|
|
2215
|
+
this.depGraph.setCellFormula(cellKey, raw.startsWith("=") ? raw : null);
|
|
2216
|
+
this.document.cells[rowId][colId] = {
|
|
2217
|
+
raw,
|
|
2218
|
+
value: raw,
|
|
2219
|
+
format: col?.format
|
|
2220
|
+
};
|
|
2221
|
+
this.document.updatedAt = Date.now();
|
|
2222
|
+
this.recalculateCell(rowId, colId);
|
|
2223
|
+
this.notifyChange();
|
|
2224
|
+
const evaluatedVal = this.document.cells[rowId][colId]?.value;
|
|
2225
|
+
this.onCellCommit?.(rowId, colId, raw, evaluatedVal);
|
|
2226
|
+
}
|
|
2227
|
+
getCellKey(rowId, colId) {
|
|
2228
|
+
const row = this.document.rows.find((r) => r.id === rowId);
|
|
2229
|
+
const col = this.document.columns.find((c) => c.id === colId);
|
|
2230
|
+
if (!row || !col) return `${colId}_${rowId}`;
|
|
2231
|
+
return `${col.key}${row.index + 1}`.toUpperCase();
|
|
2232
|
+
}
|
|
2233
|
+
recalculateCell(rowId, colId) {
|
|
2234
|
+
const cellKey = this.getCellKey(rowId, colId);
|
|
2235
|
+
const { order, hasCycle } = this.depGraph.getEvaluationOrder(cellKey);
|
|
2236
|
+
if (hasCycle) {
|
|
2237
|
+
if (this.document.cells[rowId]?.[colId]) {
|
|
2238
|
+
this.document.cells[rowId][colId].error = "#CYCLE!";
|
|
2239
|
+
this.document.cells[rowId][colId].value = "#CYCLE!";
|
|
2240
|
+
}
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
for (const key of order) {
|
|
2244
|
+
this.evalSingleCellByKey(key);
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
recalculateAll() {
|
|
2248
|
+
for (const row of this.document.rows) {
|
|
2249
|
+
for (const col of this.document.columns) {
|
|
2250
|
+
const cell = this.getCell(row.id, col.id);
|
|
2251
|
+
const cellKey = `${col.key}${row.index + 1}`.toUpperCase();
|
|
2252
|
+
if (cell.raw.startsWith("=")) {
|
|
2253
|
+
this.depGraph.setCellFormula(cellKey, cell.raw);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
for (const row of this.document.rows) {
|
|
2258
|
+
for (const col of this.document.columns) {
|
|
2259
|
+
const cellKey = `${col.key}${row.index + 1}`.toUpperCase();
|
|
2260
|
+
this.evalSingleCellByKey(cellKey);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
evalSingleCellByKey(cellKey) {
|
|
2265
|
+
const match = cellKey.match(/^([A-Za-z]+)(\d+)$/);
|
|
2266
|
+
if (!match) return;
|
|
2267
|
+
const colName = match[1].toUpperCase();
|
|
2268
|
+
const rowNum = parseInt(match[2], 10);
|
|
2269
|
+
const col = this.document.columns.find((c) => c.key.toUpperCase() === colName);
|
|
2270
|
+
const row = this.document.rows[rowNum - 1];
|
|
2271
|
+
if (!col || !row) return;
|
|
2272
|
+
const cell = this.getCell(row.id, col.id);
|
|
2273
|
+
if (!cell.raw.startsWith("=")) {
|
|
2274
|
+
if (cell.error === "#CYCLE!") {
|
|
2275
|
+
cell.error = void 0;
|
|
2276
|
+
}
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
try {
|
|
2280
|
+
const computed = this.evaluator.evaluate(cell.raw);
|
|
2281
|
+
if (!this.document.cells[row.id]) {
|
|
2282
|
+
this.document.cells[row.id] = {};
|
|
2283
|
+
}
|
|
2284
|
+
this.document.cells[row.id][col.id] = {
|
|
2285
|
+
...cell,
|
|
2286
|
+
value: computed,
|
|
2287
|
+
error: void 0
|
|
2288
|
+
};
|
|
2289
|
+
} catch (err) {
|
|
2290
|
+
if (!this.document.cells[row.id]) {
|
|
2291
|
+
this.document.cells[row.id] = {};
|
|
2292
|
+
}
|
|
2293
|
+
this.document.cells[row.id][col.id] = {
|
|
2294
|
+
...cell,
|
|
2295
|
+
value: "#ERROR!",
|
|
2296
|
+
error: err?.message || "Formula error"
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
resolveCellByCoord(colStr, rowNum) {
|
|
2301
|
+
const col = this.document.columns.find((c) => c.key.toUpperCase() === colStr.toUpperCase());
|
|
2302
|
+
const row = this.document.rows[rowNum - 1];
|
|
2303
|
+
if (!col || !row) return 0;
|
|
2304
|
+
const cell = this.getCell(row.id, col.id);
|
|
2305
|
+
return cell.value !== void 0 && cell.value !== "" ? cell.value : 0;
|
|
2306
|
+
}
|
|
2307
|
+
resolveRangeByCoords(sCol, sRow, eCol, eRow) {
|
|
2308
|
+
const startC = colNameToIndex(sCol);
|
|
2309
|
+
const endC = colNameToIndex(eCol);
|
|
2310
|
+
const minC = Math.min(startC, endC);
|
|
2311
|
+
const maxC = Math.max(startC, endC);
|
|
2312
|
+
const minR = Math.min(sRow, eRow);
|
|
2313
|
+
const maxR = Math.max(sRow, eRow);
|
|
2314
|
+
const results = [];
|
|
2315
|
+
for (let c = minC; c <= maxC; c++) {
|
|
2316
|
+
const colName = indexToColName(c);
|
|
2317
|
+
const col = this.document.columns.find((colDef) => colDef.key.toUpperCase() === colName);
|
|
2318
|
+
if (!col) continue;
|
|
2319
|
+
for (let r = minR; r <= maxR; r++) {
|
|
2320
|
+
const row = this.document.rows[r - 1];
|
|
2321
|
+
if (!row) continue;
|
|
2322
|
+
const cell = this.getCell(row.id, col.id);
|
|
2323
|
+
results.push(cell.value !== void 0 && cell.value !== "" ? cell.value : 0);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
return results;
|
|
2327
|
+
}
|
|
2328
|
+
resolveCurrentRowProperty(prop) {
|
|
2329
|
+
if (!this.activeCell) return 0;
|
|
2330
|
+
const row = this.document.rows[this.activeCell.row];
|
|
2331
|
+
if (!row || !row.recordId || !this.primaryDataSourceId) return 0;
|
|
2332
|
+
return this.binder.resolveBoundValue(this.primaryDataSourceId, row.recordId, prop) ?? 0;
|
|
2333
|
+
}
|
|
2334
|
+
// --- Selection and Navigation ---
|
|
2335
|
+
selectCell(row, col, extendSelection = false) {
|
|
2336
|
+
const clampedRow = Math.max(0, Math.min(row, this.document.rows.length - 1));
|
|
2337
|
+
const clampedCol = Math.max(0, Math.min(col, this.document.columns.length - 1));
|
|
2338
|
+
const rowObj = this.document.rows[clampedRow];
|
|
2339
|
+
const colObj = this.document.columns[clampedCol];
|
|
2340
|
+
this.activeCell = {
|
|
2341
|
+
row: clampedRow,
|
|
2342
|
+
col: clampedCol,
|
|
2343
|
+
rowId: rowObj?.id,
|
|
2344
|
+
colId: colObj?.id
|
|
2345
|
+
};
|
|
2346
|
+
if (extendSelection && this.selectedRange) {
|
|
2347
|
+
this.selectedRange = {
|
|
2348
|
+
startRow: this.selectedRange.startRow,
|
|
2349
|
+
startCol: this.selectedRange.startCol,
|
|
2350
|
+
endRow: clampedRow,
|
|
2351
|
+
endCol: clampedCol
|
|
2352
|
+
};
|
|
2353
|
+
} else {
|
|
2354
|
+
this.selectedRange = {
|
|
2355
|
+
startRow: clampedRow,
|
|
2356
|
+
startCol: clampedCol,
|
|
2357
|
+
endRow: clampedRow,
|
|
2358
|
+
endCol: clampedCol
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
this.onSelectionChange?.(this.activeCell, this.selectedRange);
|
|
2362
|
+
}
|
|
2363
|
+
selectRange(range) {
|
|
2364
|
+
this.selectedRange = range;
|
|
2365
|
+
this.onSelectionChange?.(this.activeCell, this.selectedRange);
|
|
2366
|
+
}
|
|
2367
|
+
startEditing(row, col, initialValue) {
|
|
2368
|
+
const targetRow = row ?? this.activeCell?.row ?? 0;
|
|
2369
|
+
const targetCol = col ?? this.activeCell?.col ?? 0;
|
|
2370
|
+
this.selectCell(targetRow, targetCol);
|
|
2371
|
+
const rowObj = this.document.rows[targetRow];
|
|
2372
|
+
const colObj = this.document.columns[targetCol];
|
|
2373
|
+
if (!rowObj || !colObj) return;
|
|
2374
|
+
this.editingCell = {
|
|
2375
|
+
row: targetRow,
|
|
2376
|
+
col: targetCol,
|
|
2377
|
+
rowId: rowObj.id,
|
|
2378
|
+
colId: colObj.id
|
|
2379
|
+
};
|
|
2380
|
+
const cell = this.getCell(rowObj.id, colObj.id);
|
|
2381
|
+
this.draftValue = initialValue !== void 0 ? initialValue : cell.raw;
|
|
2382
|
+
}
|
|
2383
|
+
commitEditing(valueToCommit) {
|
|
2384
|
+
if (!this.editingCell) return;
|
|
2385
|
+
const val = valueToCommit !== void 0 ? valueToCommit : this.draftValue;
|
|
2386
|
+
if (this.editingCell.rowId && this.editingCell.colId) {
|
|
2387
|
+
this.setCellValue(this.editingCell.rowId, this.editingCell.colId, val);
|
|
2388
|
+
}
|
|
2389
|
+
this.editingCell = null;
|
|
2390
|
+
this.draftValue = "";
|
|
2391
|
+
}
|
|
2392
|
+
cancelEditing() {
|
|
2393
|
+
this.editingCell = null;
|
|
2394
|
+
this.draftValue = "";
|
|
2395
|
+
}
|
|
2396
|
+
moveCursor(direction, extendSelection = false) {
|
|
2397
|
+
if (!this.activeCell) {
|
|
2398
|
+
this.selectCell(0, 0);
|
|
2399
|
+
return;
|
|
2400
|
+
}
|
|
2401
|
+
let nextRow = this.activeCell.row;
|
|
2402
|
+
let nextCol = this.activeCell.col;
|
|
2403
|
+
switch (direction) {
|
|
2404
|
+
case "up":
|
|
2405
|
+
nextRow = Math.max(0, nextRow - 1);
|
|
2406
|
+
break;
|
|
2407
|
+
case "down":
|
|
2408
|
+
nextRow = Math.min(this.document.rows.length - 1, nextRow + 1);
|
|
2409
|
+
break;
|
|
2410
|
+
case "left":
|
|
2411
|
+
nextCol = Math.max(0, nextCol - 1);
|
|
2412
|
+
break;
|
|
2413
|
+
case "right":
|
|
2414
|
+
nextCol = Math.min(this.document.columns.length - 1, nextCol + 1);
|
|
2415
|
+
break;
|
|
2416
|
+
}
|
|
2417
|
+
this.selectCell(nextRow, nextCol, extendSelection);
|
|
2418
|
+
}
|
|
2419
|
+
// --- Grid Structure Mutations ---
|
|
2420
|
+
insertColumn(index, config) {
|
|
2421
|
+
this.recordSnapshot();
|
|
2422
|
+
const newId = config?.id || `col_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
2423
|
+
const newCol = {
|
|
2424
|
+
id: newId,
|
|
2425
|
+
key: indexToColName(index),
|
|
2426
|
+
title: config?.title || indexToColName(index),
|
|
2427
|
+
width: config?.width || 130,
|
|
2428
|
+
type: config?.type || "freeform",
|
|
2429
|
+
binding: config?.binding,
|
|
2430
|
+
formula: config?.formula,
|
|
2431
|
+
format: config?.format || "text",
|
|
2432
|
+
readOnly: config?.readOnly
|
|
2433
|
+
};
|
|
2434
|
+
this.document.columns.splice(index, 0, newCol);
|
|
2435
|
+
this.recomputeRowIndices();
|
|
2436
|
+
this.recalculateAll();
|
|
2437
|
+
this.notifyChange();
|
|
2438
|
+
}
|
|
2439
|
+
deleteColumn(colId) {
|
|
2440
|
+
this.recordSnapshot();
|
|
2441
|
+
this.document.columns = this.document.columns.filter((c) => c.id !== colId);
|
|
2442
|
+
for (const rowId of Object.keys(this.document.cells)) {
|
|
2443
|
+
delete this.document.cells[rowId][colId];
|
|
2444
|
+
}
|
|
2445
|
+
this.recomputeRowIndices();
|
|
2446
|
+
this.recalculateAll();
|
|
2447
|
+
this.notifyChange();
|
|
2448
|
+
}
|
|
2449
|
+
insertRow(index, config) {
|
|
2450
|
+
this.recordSnapshot();
|
|
2451
|
+
const newId = config?.id || `row_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
2452
|
+
const newRow = {
|
|
2453
|
+
id: newId,
|
|
2454
|
+
index,
|
|
2455
|
+
height: config?.height || 32,
|
|
2456
|
+
recordId: config?.recordId,
|
|
2457
|
+
type: config?.type || "freeform"
|
|
2458
|
+
};
|
|
2459
|
+
this.document.rows.splice(index, 0, newRow);
|
|
2460
|
+
this.recomputeRowIndices();
|
|
2461
|
+
this.recalculateAll();
|
|
2462
|
+
this.notifyChange();
|
|
2463
|
+
}
|
|
2464
|
+
deleteRow(rowId) {
|
|
2465
|
+
this.recordSnapshot();
|
|
2466
|
+
this.document.rows = this.document.rows.filter((r) => r.id !== rowId);
|
|
2467
|
+
delete this.document.cells[rowId];
|
|
2468
|
+
this.recomputeRowIndices();
|
|
2469
|
+
this.recalculateAll();
|
|
2470
|
+
this.notifyChange();
|
|
2471
|
+
}
|
|
2472
|
+
setColumnWidth(colId, width) {
|
|
2473
|
+
const col = this.document.columns.find((c) => c.id === colId);
|
|
2474
|
+
if (col) {
|
|
2475
|
+
col.width = Math.max(50, width);
|
|
2476
|
+
this.notifyChange();
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
// --- Clipboard TSV/CSV ---
|
|
2480
|
+
exportToTsv() {
|
|
2481
|
+
return this.document.rows.map(
|
|
2482
|
+
(row) => this.document.columns.map((col) => {
|
|
2483
|
+
const cell = this.getCell(row.id, col.id);
|
|
2484
|
+
return String(cell.value ?? "").replace(/\t/g, " ");
|
|
2485
|
+
}).join(" ")
|
|
2486
|
+
).join("\n");
|
|
2487
|
+
}
|
|
2488
|
+
importFromTsv(tsvData, startRow = 0, startCol = 0) {
|
|
2489
|
+
this.recordSnapshot();
|
|
2490
|
+
const lines = tsvData.trim().split("\n");
|
|
2491
|
+
lines.forEach((line, rIdx) => {
|
|
2492
|
+
const cells = line.split(" ");
|
|
2493
|
+
const targetR = startRow + rIdx;
|
|
2494
|
+
while (targetR >= this.document.rows.length) {
|
|
2495
|
+
this.insertRow(this.document.rows.length);
|
|
2496
|
+
}
|
|
2497
|
+
const row = this.document.rows[targetR];
|
|
2498
|
+
cells.forEach((val, cIdx) => {
|
|
2499
|
+
const targetC = startCol + cIdx;
|
|
2500
|
+
while (targetC >= this.document.columns.length) {
|
|
2501
|
+
this.insertColumn(this.document.columns.length);
|
|
2502
|
+
}
|
|
2503
|
+
const col = this.document.columns[targetC];
|
|
2504
|
+
this.setCellValue(row.id, col.id, val);
|
|
2505
|
+
});
|
|
2506
|
+
});
|
|
2507
|
+
this.recalculateAll();
|
|
2508
|
+
this.notifyChange();
|
|
2509
|
+
}
|
|
2510
|
+
// --- Undo/Redo Snapshots ---
|
|
2511
|
+
recordSnapshot() {
|
|
2512
|
+
if (this.isApplyingHistory) return;
|
|
2513
|
+
this.undoStack.push(JSON.stringify(this.document));
|
|
2514
|
+
if (this.undoStack.length > 50) this.undoStack.shift();
|
|
2515
|
+
this.redoStack = [];
|
|
2516
|
+
}
|
|
2517
|
+
undo() {
|
|
2518
|
+
if (this.undoStack.length === 0) return;
|
|
2519
|
+
this.isApplyingHistory = true;
|
|
2520
|
+
this.redoStack.push(JSON.stringify(this.document));
|
|
2521
|
+
const previous = JSON.parse(this.undoStack.pop());
|
|
2522
|
+
this.document = previous;
|
|
2523
|
+
this.recalculateAll();
|
|
2524
|
+
this.notifyChange();
|
|
2525
|
+
this.isApplyingHistory = false;
|
|
2526
|
+
}
|
|
2527
|
+
redo() {
|
|
2528
|
+
if (this.redoStack.length === 0) return;
|
|
2529
|
+
this.isApplyingHistory = true;
|
|
2530
|
+
this.undoStack.push(JSON.stringify(this.document));
|
|
2531
|
+
const next = JSON.parse(this.redoStack.pop());
|
|
2532
|
+
this.document = next;
|
|
2533
|
+
this.recalculateAll();
|
|
2534
|
+
this.notifyChange();
|
|
2535
|
+
this.isApplyingHistory = false;
|
|
2536
|
+
}
|
|
2537
|
+
notifyChange() {
|
|
2538
|
+
this.onDocumentChange?.(this.document);
|
|
2539
|
+
}
|
|
2540
|
+
};
|
|
1117
2541
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1118
2542
|
0 && (module.exports = {
|
|
1119
2543
|
ColorHighlighter,
|
|
2544
|
+
DataSourceBinder,
|
|
2545
|
+
DependencyGraph,
|
|
1120
2546
|
EditorController,
|
|
1121
2547
|
FontSize,
|
|
2548
|
+
FormulaEvaluator,
|
|
2549
|
+
FormulaParser,
|
|
1122
2550
|
GradientText,
|
|
1123
2551
|
INSTRUCTION_REGEX,
|
|
1124
2552
|
LoadingNode,
|
|
2553
|
+
Mention,
|
|
1125
2554
|
SlashCommands,
|
|
1126
2555
|
SmilieReplacer,
|
|
2556
|
+
SpreadsheetController,
|
|
1127
2557
|
TEMPLATE_REGEX,
|
|
1128
2558
|
TemplateSuggestions,
|
|
1129
2559
|
VARIABLE_REGEX,
|
|
2560
|
+
colNameToIndex,
|
|
1130
2561
|
createEditor,
|
|
2562
|
+
createMentionExtension,
|
|
1131
2563
|
createRichTextPreset,
|
|
1132
2564
|
escapeHtml,
|
|
2565
|
+
extractCellDependencies,
|
|
1133
2566
|
extractInstructionNames,
|
|
2567
|
+
extractMentionsFromDoc,
|
|
1134
2568
|
extractTemplateNames,
|
|
1135
2569
|
extractVariableNames,
|
|
1136
2570
|
findHexColors,
|
|
1137
2571
|
getEditorText,
|
|
1138
2572
|
getLatestVersion,
|
|
1139
2573
|
htmlToPlainText,
|
|
2574
|
+
indexToColName,
|
|
1140
2575
|
instructionRegex,
|
|
1141
2576
|
mapRawOffsetToResolved,
|
|
1142
2577
|
mapResolvedOffsetToRaw,
|
|
2578
|
+
parseCellReference,
|
|
2579
|
+
parseFormula,
|
|
2580
|
+
parseRange,
|
|
1143
2581
|
plainTextToTipTapHtml,
|
|
1144
2582
|
resolveTemplates,
|
|
1145
2583
|
resolveTemplatesWithMapping,
|
|
1146
2584
|
resolveVariables,
|
|
1147
2585
|
templateRegex,
|
|
2586
|
+
tokenizeFormula,
|
|
1148
2587
|
tokenizePrompt,
|
|
1149
2588
|
variableRegex
|
|
1150
2589
|
});
|