@odoo/o-spreadsheet 14.0.0 → 14.0.1

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.
Files changed (198) hide show
  1. package/.eslintrc.json +20 -0
  2. package/.husky/post-checkout +9 -0
  3. package/.husky/pre-commit +39 -0
  4. package/.prettierignore +1 -0
  5. package/astToFormula.patch +0 -0
  6. package/demo/data.js +255 -0
  7. package/demo/index.html +9 -0
  8. package/demo/libs/Chart.bundle.js +20755 -0
  9. package/demo/main.js +101 -0
  10. package/doc/readme.md +46 -0
  11. package/package.json +3 -17
  12. package/rollup.config.js +23 -0
  13. package/src/base_plugin.ts +102 -0
  14. package/src/components/autofill.ts +146 -0
  15. package/src/components/bottom_bar.ts +199 -0
  16. package/src/components/color_picker.ts +150 -0
  17. package/src/components/composer/autocomplete_dropdown.ts +135 -0
  18. package/src/components/composer/composer.ts +487 -0
  19. package/src/components/composer/content_editable_helper.ts +146 -0
  20. package/src/components/figures/chart.ts +70 -0
  21. package/src/components/figures/container.ts +268 -0
  22. package/src/components/figures/text_figure.ts +36 -0
  23. package/src/components/grid.ts +635 -0
  24. package/src/components/helpers/dom_helpers.ts +7 -0
  25. package/src/components/helpers/drag_and_drop.ts +17 -0
  26. package/src/components/icons.ts +36 -0
  27. package/src/components/index.ts +1 -0
  28. package/src/components/menu.ts +288 -0
  29. package/src/components/overlay.ts +457 -0
  30. package/src/components/scrollbar.ts +22 -0
  31. package/src/components/selection_input.ts +168 -0
  32. package/src/components/side_panel/cell_is_rule_editor.ts +169 -0
  33. package/src/components/side_panel/chart_panel.ts +104 -0
  34. package/src/components/side_panel/color_scale_rule_editor.ts +145 -0
  35. package/src/components/side_panel/conditional_formatting.ts +396 -0
  36. package/src/components/side_panel/side_panel.ts +127 -0
  37. package/src/components/side_panel/translations_terms.ts +45 -0
  38. package/src/components/spreadsheet.ts +198 -0
  39. package/src/components/top_bar.ts +535 -0
  40. package/src/constants.ts +26 -0
  41. package/src/data.ts +149 -0
  42. package/src/fonts.ts +41 -0
  43. package/src/formulas/compiler.ts +258 -0
  44. package/src/formulas/composer_tokenizer.ts +70 -0
  45. package/src/formulas/index.ts +15 -0
  46. package/src/formulas/normalize.ts +52 -0
  47. package/src/formulas/parser.ts +280 -0
  48. package/src/formulas/range_tokenizer.ts +97 -0
  49. package/src/formulas/tokenizer.ts +242 -0
  50. package/src/functions/arguments.ts +114 -0
  51. package/src/functions/dates.ts +302 -0
  52. package/src/functions/helpers.ts +592 -0
  53. package/src/functions/index.ts +52 -0
  54. package/src/functions/module_database.ts +282 -0
  55. package/src/functions/module_date.ts +743 -0
  56. package/src/functions/module_info.ts +69 -0
  57. package/src/functions/module_logical.ts +208 -0
  58. package/src/functions/module_lookup.ts +185 -0
  59. package/src/functions/module_math.ts +1252 -0
  60. package/src/functions/module_operators.ts +291 -0
  61. package/src/functions/module_statistical.ts +882 -0
  62. package/src/functions/module_text.ts +370 -0
  63. package/src/helpers/color.ts +30 -0
  64. package/src/helpers/coordinates.ts +71 -0
  65. package/src/helpers/grid_manipulation.ts +68 -0
  66. package/src/helpers/index.ts +7 -0
  67. package/src/helpers/misc.ts +76 -0
  68. package/src/helpers/numbers.ts +117 -0
  69. package/src/helpers/uuid.ts +19 -0
  70. package/src/helpers/zones.ts +263 -0
  71. package/src/history.ts +186 -0
  72. package/src/index.ts +84 -0
  73. package/src/model.ts +261 -0
  74. package/src/plugins/autofill.ts +412 -0
  75. package/src/plugins/chart.ts +404 -0
  76. package/src/plugins/clipboard.ts +461 -0
  77. package/src/plugins/conditional_format.ts +445 -0
  78. package/src/plugins/core.ts +1502 -0
  79. package/src/plugins/edition.ts +157 -0
  80. package/src/plugins/evaluation.ts +428 -0
  81. package/src/plugins/figures.ts +123 -0
  82. package/src/plugins/formatting.ts +630 -0
  83. package/src/plugins/highlight.ts +187 -0
  84. package/src/plugins/index.ts +32 -0
  85. package/src/plugins/merge.ts +524 -0
  86. package/src/plugins/renderer.ts +571 -0
  87. package/src/plugins/selection.ts +562 -0
  88. package/src/plugins/selection_inputs.ts +326 -0
  89. package/src/registries/autofill_modifiers.ts +76 -0
  90. package/src/registries/autofill_rules.ts +98 -0
  91. package/src/registries/figure_registry.ts +30 -0
  92. package/src/registries/index.ts +7 -0
  93. package/src/registries/menu_items_registry.ts +110 -0
  94. package/src/registries/menus/cell_menu_registry.ts +79 -0
  95. package/src/registries/menus/col_menu_registry.ts +63 -0
  96. package/src/registries/menus/index.ts +6 -0
  97. package/src/registries/menus/menu_items_actions.ts +425 -0
  98. package/src/registries/menus/row_menu_registry.ts +63 -0
  99. package/src/registries/menus/sheet_menu_registry.ts +75 -0
  100. package/src/registries/menus/topbar_menu_registry.ts +204 -0
  101. package/src/registries/side_panel_registry.ts +26 -0
  102. package/src/registries/topbar_component_registry.ts +12 -0
  103. package/src/registry.ts +51 -0
  104. package/src/translation.ts +44 -0
  105. package/src/types/autofill.ts +48 -0
  106. package/src/types/chart.ts +26 -0
  107. package/src/types/commands.ts +716 -0
  108. package/src/types/conditional_formatting.ts +124 -0
  109. package/src/types/env.ts +13 -0
  110. package/src/types/functions.ts +35 -0
  111. package/src/types/getters.ts +103 -0
  112. package/src/types/index.ts +20 -0
  113. package/src/types/misc.ts +143 -0
  114. package/src/types/rendering.ts +41 -0
  115. package/src/types/workbook_data.ts +62 -0
  116. package/tests/__mocks__/uuid.ts +7 -0
  117. package/tests/canvas.mock.ts +34 -0
  118. package/tests/components/__mocks__/content_editable_helper.ts +79 -0
  119. package/tests/components/__mocks__/scrollbar.ts +18 -0
  120. package/tests/components/__snapshots__/autocomplete_dropdown_test.ts.snap +29 -0
  121. package/tests/components/__snapshots__/bottom_bar_test.ts.snap +75 -0
  122. package/tests/components/__snapshots__/context_menu_test.ts.snap +88 -0
  123. package/tests/components/__snapshots__/grid_test.ts.snap +60 -0
  124. package/tests/components/__snapshots__/spreadsheet_test.ts.snap +455 -0
  125. package/tests/components/__snapshots__/top_bar_test.ts.snap +705 -0
  126. package/tests/components/autocomplete_dropdown_test.ts +315 -0
  127. package/tests/components/autofill_test.ts +167 -0
  128. package/tests/components/bottom_bar_test.ts +262 -0
  129. package/tests/components/chart_side_panel_test.ts +123 -0
  130. package/tests/components/composer_test.ts +400 -0
  131. package/tests/components/context_menu_test.ts +825 -0
  132. package/tests/components/grid_manipulation_test.ts +163 -0
  133. package/tests/components/grid_test.ts +581 -0
  134. package/tests/components/overlay_test.ts +541 -0
  135. package/tests/components/selection_input_test.ts +259 -0
  136. package/tests/components/side_panel_test.ts +148 -0
  137. package/tests/components/spreadsheet_test.ts +194 -0
  138. package/tests/components/top_bar_test.ts +427 -0
  139. package/tests/data_test.ts +175 -0
  140. package/tests/dom_helper.ts +42 -0
  141. package/tests/formulas/__snapshots__/compiler_test.ts.snap +302 -0
  142. package/tests/formulas/compiler_test.ts +266 -0
  143. package/tests/formulas/composer_tokenizer_test.ts +137 -0
  144. package/tests/formulas/expressions_test.ts +57 -0
  145. package/tests/formulas/formulas_test.ts +80 -0
  146. package/tests/formulas/normalize_test.ts +82 -0
  147. package/tests/formulas/parser_test.ts +224 -0
  148. package/tests/formulas/range_tokenizer_test.ts +187 -0
  149. package/tests/formulas/tokenizer_test.ts +164 -0
  150. package/tests/functions/arguments_test.ts +141 -0
  151. package/tests/functions/dates_test.ts +1081 -0
  152. package/tests/functions/functions_test.ts +53 -0
  153. package/tests/functions/helper_test.ts +98 -0
  154. package/tests/functions/module_database_test.ts +311 -0
  155. package/tests/functions/module_date_test.ts +941 -0
  156. package/tests/functions/module_info_test.ts +139 -0
  157. package/tests/functions/module_logical_test.ts +369 -0
  158. package/tests/functions/module_lookup_test.ts +541 -0
  159. package/tests/functions/module_math_test.ts +2482 -0
  160. package/tests/functions/module_operators_test.ts +955 -0
  161. package/tests/functions/module_statistical_test.ts +2014 -0
  162. package/tests/functions/module_text_test.ts +667 -0
  163. package/tests/helpers/coordinates_test.ts +25 -0
  164. package/tests/helpers/misc_test.ts +95 -0
  165. package/tests/helpers/numbers_test.ts +196 -0
  166. package/tests/helpers/zones_test.ts +97 -0
  167. package/tests/helpers.ts +384 -0
  168. package/tests/menu_items_registry_test.ts +540 -0
  169. package/tests/model/plugins/__snapshots__/chart_test.ts.snap +90 -0
  170. package/tests/model/plugins/chart_test.ts +562 -0
  171. package/tests/model_test.ts +82 -0
  172. package/tests/plugins/__snapshots__/conditional_formatting_test.ts.snap +131 -0
  173. package/tests/plugins/__snapshots__/renderer_test.ts.snap +371 -0
  174. package/tests/plugins/autofill_test.ts +457 -0
  175. package/tests/plugins/borders_test.ts +295 -0
  176. package/tests/plugins/clipboard_test.ts +1180 -0
  177. package/tests/plugins/conditional_formatting_test.ts +1253 -0
  178. package/tests/plugins/core_test.ts +614 -0
  179. package/tests/plugins/edition_test.ts +100 -0
  180. package/tests/plugins/evaluation_async_test.ts +332 -0
  181. package/tests/plugins/evaluation_dates_test.ts +79 -0
  182. package/tests/plugins/evaluation_test.ts +901 -0
  183. package/tests/plugins/figures_test.ts +327 -0
  184. package/tests/plugins/formatting_test.ts +391 -0
  185. package/tests/plugins/grid_manipulation_test.ts +1366 -0
  186. package/tests/plugins/highlight_test.ts +413 -0
  187. package/tests/plugins/history_test.ts +142 -0
  188. package/tests/plugins/import_export_test.ts +276 -0
  189. package/tests/plugins/merges_test.ts +490 -0
  190. package/tests/plugins/navigation_test.ts +181 -0
  191. package/tests/plugins/plugins/evaluation_test.ts +111 -0
  192. package/tests/plugins/renderer_test.ts +598 -0
  193. package/tests/plugins/resizing_test.ts +152 -0
  194. package/tests/plugins/selection_input_test.ts +409 -0
  195. package/tests/plugins/selection_test.ts +442 -0
  196. package/tests/plugins/sheets_test.ts +643 -0
  197. package/tests/plugins/style_test.ts +90 -0
  198. package/tsconfig.json +22 -0
package/src/data.ts ADDED
@@ -0,0 +1,149 @@
1
+ import { uuidv4 } from "./helpers/index";
2
+ import { SheetData, Workbook, WorkbookData } from "./types/index";
3
+
4
+ /**
5
+ * This is the current state version number. It should be incremented each time
6
+ * a breaking change is made in the way the state is handled, and an upgrade
7
+ * function should be defined
8
+ */
9
+ export const CURRENT_VERSION = 5;
10
+
11
+ /**
12
+ * This function tries to load anything that could look like a valid
13
+ * workbookData object. It applies any migrations, if needed, and return a
14
+ * current, complete workbookData object.
15
+ *
16
+ * It also ensures that there is at least one sheet.
17
+ */
18
+ export function load(data?: any): WorkbookData {
19
+ if (!data) {
20
+ return createEmptyWorkbookData();
21
+ }
22
+ data = Object.assign({}, data);
23
+
24
+ // apply migrations, if needed
25
+ if ("version" in data) {
26
+ if (data.version < CURRENT_VERSION) {
27
+ data = migrate(data);
28
+ }
29
+ }
30
+
31
+ // sanity check: try to fix missing fields/corrupted state by providing
32
+ // sensible default values
33
+ data = Object.assign(createEmptyWorkbookData(), data, { version: CURRENT_VERSION });
34
+ data.sheets = data.sheets.map((s, i) => Object.assign(createEmptySheet(`Sheet${i + 1}`), s));
35
+ if (!data.sheets.map((s) => s.id).includes(data.activeSheet)) {
36
+ data.activeSheet = data.sheets[0].id;
37
+ }
38
+
39
+ if (data.sheets.length === 0) {
40
+ data.sheets.push(createEmptySheet());
41
+ }
42
+ return data;
43
+ }
44
+
45
+ // -----------------------------------------------------------------------------
46
+ // Migrations
47
+ // -----------------------------------------------------------------------------
48
+
49
+ interface Migration {
50
+ from: number;
51
+ to: number;
52
+ applyMigration(data: any): any;
53
+ description: string;
54
+ }
55
+
56
+ function migrate(data: any): WorkbookData {
57
+ const index = MIGRATIONS.findIndex((m) => m.from === data.version);
58
+ for (let i = index; i < MIGRATIONS.length; i++) {
59
+ data = MIGRATIONS[i].applyMigration(data);
60
+ }
61
+ return data;
62
+ }
63
+
64
+ const MIGRATIONS: Migration[] = [
65
+ {
66
+ description: "add the `activeSheet` field on data",
67
+ from: 1,
68
+ to: 2,
69
+ applyMigration(data: any): any {
70
+ if (data.sheets && data.sheets[0]) {
71
+ data.activeSheet = data.sheets[0].name;
72
+ }
73
+ return data;
74
+ },
75
+ },
76
+ {
77
+ description: "add an id field in each sheet",
78
+ from: 2,
79
+ to: 3,
80
+ applyMigration(data: any): any {
81
+ if (data.sheets && data.sheets.length) {
82
+ for (let sheet of data.sheets) {
83
+ sheet.id = sheet.id || sheet.name;
84
+ }
85
+ }
86
+ return data;
87
+ },
88
+ },
89
+ {
90
+ description: "activeSheet is now an id, not the name of a sheet",
91
+ from: 3,
92
+ to: 4,
93
+ applyMigration(data: any): any {
94
+ const activeSheet = data.sheets.find((s) => s.name === data.activeSheet);
95
+ data.activeSheet = activeSheet.id;
96
+ return data;
97
+ },
98
+ },
99
+ {
100
+ description: "add figures object in each sheets",
101
+ from: 4,
102
+ to: 5,
103
+ applyMigration(data: any): any {
104
+ for (let sheet of data.sheets) {
105
+ sheet.figures = sheet.figures || [];
106
+ }
107
+ return data;
108
+ },
109
+ },
110
+ ];
111
+
112
+ // -----------------------------------------------------------------------------
113
+ // Helpers
114
+ // -----------------------------------------------------------------------------
115
+ function createEmptySheet(name: string = "Sheet1"): SheetData {
116
+ return {
117
+ id: uuidv4(),
118
+ name,
119
+ colNumber: 26,
120
+ rowNumber: 100,
121
+ cells: {},
122
+ cols: {},
123
+ rows: {},
124
+ merges: [],
125
+ conditionalFormats: [],
126
+ figures: [],
127
+ };
128
+ }
129
+
130
+ export function createEmptyWorkbookData(): WorkbookData {
131
+ const data = {
132
+ version: CURRENT_VERSION,
133
+ sheets: [createEmptySheet("Sheet1")],
134
+ activeSheet: "",
135
+ entities: {},
136
+ styles: {},
137
+ borders: {},
138
+ };
139
+ data.activeSheet = data.sheets[0].id;
140
+ return data;
141
+ }
142
+
143
+ export function createEmptyWorkbook(): Workbook {
144
+ return {
145
+ visibleSheets: [],
146
+ sheets: {},
147
+ activeSheet: null as any,
148
+ };
149
+ }
package/src/fonts.ts ADDED
@@ -0,0 +1,41 @@
1
+ export interface Font {
2
+ key: string;
3
+ description: string;
4
+ }
5
+
6
+ export interface FontSize {
7
+ pt: number;
8
+ px: number;
9
+ }
10
+
11
+ export const fonts: Font[] = [
12
+ { key: "Arial", description: "Arial" },
13
+ { key: "Helvetica", description: "Helvetica" },
14
+ { key: "Courier New", description: "Courier New" },
15
+ { key: "Verdana", description: "Verdana" },
16
+ { key: "Lato", description: "Lato" },
17
+ ];
18
+
19
+ export const fontSizes: FontSize[] = [
20
+ { pt: 7.5, px: 10 },
21
+ { pt: 8, px: 11 },
22
+ { pt: 9, px: 12 },
23
+ { pt: 10, px: 13 },
24
+ { pt: 10.5, px: 14 },
25
+ { pt: 11, px: 15 },
26
+ { pt: 12, px: 16 },
27
+ { pt: 14, px: 18.7 },
28
+ { pt: 15, px: 20 },
29
+ { pt: 16, px: 21.3 },
30
+ { pt: 18, px: 24 },
31
+ { pt: 22, px: 29.3 },
32
+ { pt: 24, px: 32 },
33
+ { pt: 26, px: 34.7 },
34
+ { pt: 36, px: 48 },
35
+ ];
36
+
37
+ export const fontSizeMap: { [pt: number]: number } = {};
38
+
39
+ for (let font of fontSizes) {
40
+ fontSizeMap[font.pt] = font.px;
41
+ }
@@ -0,0 +1,258 @@
1
+ import { functionRegistry } from "../functions/index";
2
+ import { _lt } from "../translation";
3
+ import { Arg, CompiledFormula, NormalizedFormula } from "../types/index";
4
+ import { AST, ASTAsyncFuncall, ASTFuncall, parse } from "./parser";
5
+
6
+ const functions = functionRegistry.content;
7
+
8
+ const OPERATOR_MAP = {
9
+ "=": "EQ",
10
+ "+": "ADD",
11
+ "-": "MINUS",
12
+ "*": "MULTIPLY",
13
+ "/": "DIVIDE",
14
+ ">=": "GTE",
15
+ "<>": "NE",
16
+ ">": "GT",
17
+ "<=": "LTE",
18
+ "<": "LT",
19
+ "^": "POWER",
20
+ "&": "CONCATENATE",
21
+ };
22
+
23
+ const UNARY_OPERATOR_MAP = {
24
+ "-": "UMINUS",
25
+ "+": "UPLUS",
26
+ };
27
+
28
+ // this cache contains all compiled function code, grouped by "structure". For
29
+ // example, "=2*sum(A1:A4)" and "=2*sum(B1:B4)" are compiled into the same
30
+ // structural function.
31
+ //
32
+ // It is only exported for testing purposes
33
+ export const functionCache: { [key: string]: CompiledFormula } = {};
34
+
35
+ // -----------------------------------------------------------------------------
36
+ // COMPILER
37
+ // -----------------------------------------------------------------------------
38
+ const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
39
+
40
+ export function compile(str: NormalizedFormula): CompiledFormula {
41
+ // TODO: rename that 'str' variable. Doesn't make sense
42
+ let isAsync = false;
43
+
44
+ if (!functionCache[str.text]) {
45
+ const ast = parse(str.text);
46
+ let nextId = 1;
47
+ const code = [`// ${str.text}`];
48
+
49
+ if (ast.type === "BIN_OPERATION" && ast.value === ":") {
50
+ throw new Error(_lt("Invalid formula"));
51
+ }
52
+ if (ast.type === "UNKNOWN") {
53
+ throw new Error(_lt("Invalid formula"));
54
+ }
55
+
56
+ /**
57
+ * This function compile the function arguments. It is mostly straightforward,
58
+ * except that there is a non trivial transformation in one situation:
59
+ *
60
+ * If a function argument is asking for a range, and get a cell, we transform
61
+ * the cell value into a range. This allow the grid model to differentiate
62
+ * between a cell value and a non cell value.
63
+ */
64
+ function compileFunctionArgs(ast: ASTAsyncFuncall | ASTFuncall): string[] {
65
+ const functionDefinition = functions[ast.value.toUpperCase()];
66
+ let argDefinition: Arg;
67
+
68
+ const result: string[] = [];
69
+ const currentFunctionArguments = ast.args;
70
+
71
+ const isRepeating = functionDefinition.args.length
72
+ ? functionDefinition.args[functionDefinition.args.length - 1].repeating
73
+ : false;
74
+ let minArg = 0;
75
+ let maxArg = isRepeating ? Infinity : functionDefinition.args.length;
76
+ for (let arg of functionDefinition.args) {
77
+ if (!arg.optional) {
78
+ minArg++;
79
+ }
80
+ }
81
+ if (currentFunctionArguments.length < minArg || currentFunctionArguments.length > maxArg) {
82
+ throw new Error(
83
+ _lt(
84
+ "Invalid number of arguments for the %s function. Expected %s, but got %s instead.",
85
+ ast.value.toUpperCase(),
86
+ functionDefinition.args.length.toString(),
87
+ currentFunctionArguments.length.toString()
88
+ )
89
+ );
90
+ }
91
+ for (let i = 0; i < currentFunctionArguments.length; i++) {
92
+ const arg = currentFunctionArguments[i];
93
+ argDefinition = functionDefinition.args[i] || argDefinition!;
94
+ const isLazy = argDefinition && argDefinition.lazy;
95
+ const types = (argDefinition && argDefinition.type) || [];
96
+ const hasRange = types.some(
97
+ (t) =>
98
+ t === "RANGE" ||
99
+ t === "RANGE<BOOLEAN>" ||
100
+ t === "RANGE<DATE>" ||
101
+ t === "RANGE<NUMBER>" ||
102
+ t === "RANGE<STRING>"
103
+ );
104
+ const isRangeOnly = types.every(
105
+ (t) =>
106
+ t === "RANGE" ||
107
+ t === "RANGE<BOOLEAN>" ||
108
+ t === "RANGE<DATE>" ||
109
+ t === "RANGE<NUMBER>" ||
110
+ t === "RANGE<STRING>"
111
+ );
112
+ if (isRangeOnly && arg.type !== "REFERENCE") {
113
+ throw new Error(
114
+ _lt(
115
+ "Function %s expects the parameter %s to be reference to a cell or range, not a %s.",
116
+ ast.value.toUpperCase(),
117
+ (i + 1).toString(),
118
+ arg.type.toLowerCase().replace("normalized_", "")
119
+ )
120
+ );
121
+ }
122
+ if (
123
+ // a range of more than 1 cell
124
+ arg.type === "BIN_OPERATION" &&
125
+ arg.value === ":" &&
126
+ arg.left.value !== arg.right.value &&
127
+ !hasRange
128
+ ) {
129
+ throw new Error(
130
+ _lt(
131
+ "Function %s expects the parameter %s to be a single value or a single cell reference, not a range.",
132
+ ast.value.toUpperCase(),
133
+ (i + 1).toString()
134
+ )
135
+ );
136
+ }
137
+ let argValue = compileAST(arg, isLazy, hasRange, {
138
+ functionName: ast.value.toUpperCase(),
139
+ paramIndex: i + 1,
140
+ });
141
+ result.push(argValue);
142
+ }
143
+ return result;
144
+ }
145
+
146
+ /**
147
+ * This function compiles all the information extracted by the parser into an
148
+ * executable code for the evaluation of the cells content. It uses a cache to
149
+ * not reevaluate identical code structures.
150
+ *
151
+ * The function is sensitive to parameter “isLazy”. It may vary when compiling
152
+ * function arguments:
153
+ *
154
+ * - isLazy: In some cases the function arguments does not need to be
155
+ * evaluated before entering the functions. For example the IF function might
156
+ * take invalid arguments that do not need to be evaluate and thus should not
157
+ * create an error. For this we have lazy arguments.
158
+ *
159
+ */
160
+
161
+ function compileAST(
162
+ ast: AST,
163
+ isLazy = false,
164
+ hasRange = false,
165
+ referenceVerification: {
166
+ functionName?: string;
167
+ paramIndex?: number;
168
+ } = {}
169
+ ): string {
170
+ let id, left, right, args, fnName, statement;
171
+ if (ast.debug) {
172
+ code.push("debugger;");
173
+ }
174
+ switch (ast.type) {
175
+ case "BOOLEAN":
176
+ case "NUMBER": // probably dead case
177
+ case "STRING": // probably dead case
178
+ id = nextId++;
179
+ statement = ast.value;
180
+ break;
181
+ case "NORMALIZED_NUMBER":
182
+ case "NORMALIZED_STRING":
183
+ id = nextId++;
184
+ statement = `deps[${ast.value}]`;
185
+ break;
186
+ case "REFERENCE":
187
+ const referenceText = str.dependencies[ast.value];
188
+ if (!referenceText) {
189
+ id = nextId++;
190
+ statement = `null`;
191
+ break;
192
+ }
193
+ id = nextId++;
194
+ if (hasRange) {
195
+ statement = `range(${ast.value}, deps, sheetId)`;
196
+ } else {
197
+ statement = `ref(${ast.value}, deps, sheetId,
198
+ "${referenceVerification.functionName}", ${referenceVerification.paramIndex})`;
199
+ }
200
+ break;
201
+ case "FUNCALL":
202
+ id = nextId++;
203
+ args = compileFunctionArgs(ast);
204
+ fnName = ast.value.toUpperCase();
205
+ code.push(`ctx.__lastFnCalled = '${fnName}'`);
206
+ statement = `ctx['${fnName}'](${args})`;
207
+ break;
208
+ case "ASYNC_FUNCALL":
209
+ id = nextId++;
210
+ isAsync = true;
211
+ args = compileFunctionArgs(ast);
212
+ fnName = ast.value.toUpperCase();
213
+ code.push(`ctx.__lastFnCalled = '${fnName}'`);
214
+ statement = `await ctx['${fnName}'](${args})`;
215
+ break;
216
+ case "UNARY_OPERATION":
217
+ id = nextId++;
218
+ right = compileAST(ast.right);
219
+ fnName = UNARY_OPERATOR_MAP[ast.value];
220
+ code.push(`ctx.__lastFnCalled = '${fnName}'`);
221
+ statement = `ctx['${fnName}']( ${right})`;
222
+ break;
223
+ case "BIN_OPERATION":
224
+ id = nextId++;
225
+ left = compileAST(ast.left);
226
+ right = compileAST(ast.right);
227
+ fnName = OPERATOR_MAP[ast.value];
228
+ code.push(`ctx.__lastFnCalled = '${fnName}'`);
229
+ statement = `ctx['${fnName}'](${left}, ${right})`;
230
+ break;
231
+ case "UNKNOWN":
232
+ if (!isLazy) {
233
+ return "null";
234
+ }
235
+ id = nextId++;
236
+ statement = `null`;
237
+ break;
238
+ }
239
+ code.push(`let _${id} = ` + (isLazy ? `()=> ` : ``) + statement);
240
+ return `_${id}`;
241
+ }
242
+ code.push(`return ${compileAST(ast)};`);
243
+
244
+ const Constructor = isAsync ? AsyncFunction : Function;
245
+ let baseFunction = new Constructor(
246
+ "deps", // the dependencies in the current formula
247
+ "sheetId", // the sheet the formula is currently evaluating
248
+ "ref", // a function to access a certain dependency at a given index
249
+ "range", // same as above, but guarantee that the result is in the form of a range
250
+ "ctx",
251
+ code.join("\n")
252
+ );
253
+ functionCache[str.text] = baseFunction;
254
+ functionCache[str.text].async = isAsync;
255
+ }
256
+
257
+ return functionCache[str.text];
258
+ }
@@ -0,0 +1,70 @@
1
+ import { Token } from ".";
2
+ import { mergeSymbolsIntoRanges } from "./range_tokenizer";
3
+ import { tokenize } from "./tokenizer";
4
+
5
+ /**
6
+ * Enriched Token is used by the composer to add information on the tokens that
7
+ * are only needed during the edition of a formula.
8
+ *
9
+ * The information added are:
10
+ * - parenthesis matching
11
+ * - range detection (replaces the tokens that composes the range with 1 token)
12
+ * - length, start and end of each token
13
+ */
14
+
15
+ export interface EnrichedToken extends Token {
16
+ start: number;
17
+ end: number;
18
+ length: number;
19
+ parenIndex?: number;
20
+ }
21
+
22
+ /**
23
+ * Add the following information on tokens:
24
+ * - length
25
+ * - start
26
+ * - end
27
+ */
28
+ function enrichTokens(tokens: Token[]): EnrichedToken[] {
29
+ let current = 0;
30
+ return tokens.map((x) => {
31
+ const len = x.value.toString().length;
32
+ const token: EnrichedToken = Object.assign({}, x, {
33
+ start: current,
34
+ end: current + len,
35
+ length: len,
36
+ });
37
+ current = token.end;
38
+ return token;
39
+ });
40
+ }
41
+
42
+ /**
43
+ * add on each token the length, start and end
44
+ * also matches the opening to its closing parenthesis (using the same number)
45
+ */
46
+ function mapParenthesis(tokens: EnrichedToken[]): EnrichedToken[] {
47
+ let maxParen = 1;
48
+ const stack: number[] = [];
49
+ return tokens.map((token) => {
50
+ if (token.type === "LEFT_PAREN") {
51
+ stack.push(maxParen);
52
+ token.parenIndex = maxParen;
53
+ maxParen++;
54
+ } else if (token.type === "RIGHT_PAREN") {
55
+ token.parenIndex = stack.pop();
56
+ }
57
+ return token;
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Take the result of the tokenizer and transform it to be usable in the composer.
63
+ *
64
+ * @param formula
65
+ */
66
+ export function composerTokenize(formula: string): EnrichedToken[] {
67
+ const tokens = tokenize(formula);
68
+
69
+ return mapParenthesis(enrichTokens(mergeSymbolsIntoRanges(tokens)));
70
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The formulas module provides all functionality related to manipulating
3
+ * formulas:
4
+ *
5
+ * - tokenization (transforming a string into a list of tokens)
6
+ * - parsing (same, but into an AST (Abstract Syntax Tree))
7
+ * - compiler (getting an executable function representing a formula)
8
+ */
9
+
10
+ export { compile } from "./compiler";
11
+ export { composerTokenize, EnrichedToken } from "./composer_tokenizer";
12
+ export { normalize } from "./normalize";
13
+ export { cellReference, parse, rangeReference } from "./parser";
14
+ export { rangeTokenize } from "./range_tokenizer";
15
+ export { Token, tokenize } from "./tokenizer";
@@ -0,0 +1,52 @@
1
+ import { parseNumber } from "../helpers/numbers";
2
+ import { Dependencies, NormalizedFormula } from "../types";
3
+ import { cellReference } from "./parser";
4
+ import { rangeTokenize } from "./range_tokenizer";
5
+ import { FORMULA_REF_IDENTIFIER } from "./tokenizer";
6
+
7
+ /**
8
+ * parses a formula (as a string) into the same formula,
9
+ * but with the references to other cells as well as strings and numbers extracted
10
+ *
11
+ * =sum(a3:b1) + c3 --> =sum(|0|) + |1|
12
+ *
13
+ * = sum(a3, 5) --> =sum(|0|, |N1|)
14
+ *
15
+ * =CONCAT(CONCAT(1, "beta"), A1) --> =CONCAT(CONCAT(|N0|, |S1|), |2|)
16
+ *
17
+ * Strings and Numbers are marked with a prefix of their type (S and N) to be detected
18
+ * as their normalized form by the parser as they won't be processed like the references by the compiler.
19
+ *
20
+ * @param formula
21
+ */
22
+ export function normalize(formula: string): NormalizedFormula {
23
+ const tokens = rangeTokenize(formula);
24
+
25
+ let references: Dependencies = [];
26
+
27
+ let noRefFormula = "".concat(
28
+ ...tokens.map<string>((token) => {
29
+ if (token.type === "SYMBOL" && cellReference.test(token.value)) {
30
+ const value = token.value.trim();
31
+ if (!references.includes(value)) {
32
+ references.push(value);
33
+ }
34
+ return `${FORMULA_REF_IDENTIFIER}${references.indexOf(value)}${FORMULA_REF_IDENTIFIER}`;
35
+ } else if (["STRING", "NUMBER"].includes(token.type)) {
36
+ const value =
37
+ token.type === "STRING"
38
+ ? token.value.slice(1).slice(0, token.value.length - 2)
39
+ : parseNumber(token.value);
40
+ if (!references.includes(value)) {
41
+ references.push(value);
42
+ }
43
+ return `${FORMULA_REF_IDENTIFIER}${token.type[0]}${references.indexOf(
44
+ value
45
+ )}${FORMULA_REF_IDENTIFIER}`;
46
+ } else {
47
+ return token.value;
48
+ }
49
+ })
50
+ );
51
+ return { text: noRefFormula, dependencies: references };
52
+ }