@fw-components/formula-editor 2.0.3-formula-editor.2 → 2.0.7-cline-formulaeditor.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/formula-editor/src/cursor.js +142 -0
- package/dist/formula-editor/src/formula-builder.js +139 -0
- package/dist/formula-editor/src/formula-creator.js +83 -0
- package/dist/formula-editor/src/formula-editor.js +367 -0
- package/dist/formula-editor/src/helpers/types.js +16 -0
- package/dist/formula-editor/src/helpers.js +72 -0
- package/dist/formula-editor/src/parser.js +461 -0
- package/dist/formula-editor/src/recommendor.js +18 -0
- package/dist/formula-editor/src/styles/formula-editor-styles.js +149 -0
- package/dist/formula-editor/src/sub-components/operator-input.js +24 -0
- package/dist/formula-editor/src/suggestion-menu.js +198 -0
- package/dist/styles/src/button-styles.js +419 -0
- package/package.json +5 -5
- package/src/cursor.js +3 -3
- package/src/cursor.js.map +1 -1
- package/src/formula-builder.js.map +1 -1
- package/src/formula-editor.js.map +1 -1
- package/src/parser.js.map +1 -1
- package/src/recommendor.js.map +1 -1
- package/src/suggestion-menu.js.map +1 -1
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import Big from "big.js";
|
|
2
|
+
import { Expectation, Queue, Stack } from "./helpers.js";
|
|
3
|
+
import { Recommender } from "./recommendor.js";
|
|
4
|
+
export class Parser {
|
|
5
|
+
static { this.MAX_FORMULA_LENGTH = 1000; }
|
|
6
|
+
static { this.CACHE_SIZE = 100; }
|
|
7
|
+
constructor(variables, minSuggestionLen) {
|
|
8
|
+
this._parseCache = new Map();
|
|
9
|
+
this._rpnCache = new Map();
|
|
10
|
+
this.mathematicalOperators = new Set(["^", "+", "-", "*", "/", "sin", "cos", "tan", "sqrt", "log"]);
|
|
11
|
+
this.constants = new Map([
|
|
12
|
+
["pi", Math.PI],
|
|
13
|
+
["e", Math.E]
|
|
14
|
+
]);
|
|
15
|
+
this.operatorPrecedence = {
|
|
16
|
+
"^": 3,
|
|
17
|
+
"/": 2,
|
|
18
|
+
"*": 2,
|
|
19
|
+
"+": 1,
|
|
20
|
+
"-": 1,
|
|
21
|
+
};
|
|
22
|
+
this.variables = variables;
|
|
23
|
+
this._recommender = new Recommender(this.variables, minSuggestionLen);
|
|
24
|
+
}
|
|
25
|
+
clearCacheIfNeeded() {
|
|
26
|
+
if (this._parseCache.size > Parser.CACHE_SIZE) {
|
|
27
|
+
const entriesToDelete = Array.from(this._parseCache.keys())
|
|
28
|
+
.slice(0, Math.floor(Parser.CACHE_SIZE / 2));
|
|
29
|
+
entriesToDelete.forEach(key => this._parseCache.delete(key));
|
|
30
|
+
}
|
|
31
|
+
if (this._rpnCache.size > Parser.CACHE_SIZE) {
|
|
32
|
+
const entriesToDelete = Array.from(this._rpnCache.keys())
|
|
33
|
+
.slice(0, Math.floor(Parser.CACHE_SIZE / 2));
|
|
34
|
+
entriesToDelete.forEach(key => this._rpnCache.delete(key));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
parseInput(formula, prevCurPos = null, recommendation = null) {
|
|
38
|
+
// Check formula length
|
|
39
|
+
if (formula.length > Parser.MAX_FORMULA_LENGTH) {
|
|
40
|
+
return {
|
|
41
|
+
recommendations: null,
|
|
42
|
+
formattedContent: null,
|
|
43
|
+
formattedString: null,
|
|
44
|
+
newCursorPosition: prevCurPos ?? -1,
|
|
45
|
+
errorString: `Formula length exceeds maximum limit of ${Parser.MAX_FORMULA_LENGTH} characters`
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// Check cache for exact matches without recommendation
|
|
49
|
+
const cacheKey = `${formula}-${prevCurPos}`;
|
|
50
|
+
if (!recommendation && this._parseCache.has(cacheKey)) {
|
|
51
|
+
return { ...this._parseCache.get(cacheKey) };
|
|
52
|
+
}
|
|
53
|
+
let tokens = formula.match(/'[^']*'|\d+|[A-Za-z_][A-Za-z0-9_]*|[-+(),*^/:?\s]|sin|cos|tan|sqrt|log/g);
|
|
54
|
+
// Stores the positions of opening parentheses. This allows us to
|
|
55
|
+
// show "Unclosed parenthesis error" for positions which are far behind
|
|
56
|
+
// our current token
|
|
57
|
+
let parentheses = new Stack();
|
|
58
|
+
// The HTML formatted string which we eventually show on the view.
|
|
59
|
+
let formattedString = ``;
|
|
60
|
+
// The expectation that we have for the current token.
|
|
61
|
+
let expectation = Expectation.VARIABLE;
|
|
62
|
+
// Position of the current token in the formula string.
|
|
63
|
+
let currentPosition = 0;
|
|
64
|
+
// Previous 'token' (not a space or a new line) that we just encountered.
|
|
65
|
+
let previousToken = "";
|
|
66
|
+
let currentTokens = "";
|
|
67
|
+
// The object that we return as the output of the parsing result.
|
|
68
|
+
let parseOutput = {
|
|
69
|
+
recommendations: null,
|
|
70
|
+
formattedContent: null,
|
|
71
|
+
formattedString: null,
|
|
72
|
+
newCursorPosition: prevCurPos ?? -1,
|
|
73
|
+
errorString: null,
|
|
74
|
+
};
|
|
75
|
+
if (!formula.trim()) {
|
|
76
|
+
if (recommendation) {
|
|
77
|
+
formattedString = `<span class="wysiwygInternals">${recommendation}</span>`;
|
|
78
|
+
currentPosition += recommendation.length;
|
|
79
|
+
const parser = new DOMParser();
|
|
80
|
+
const doc = parser.parseFromString(formattedString, "text/html");
|
|
81
|
+
parseOutput.formattedContent = doc.querySelector("body");
|
|
82
|
+
parseOutput.formattedString = formattedString;
|
|
83
|
+
parseOutput.newCursorPosition = recommendation.length;
|
|
84
|
+
return parseOutput;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
tokens?.forEach((token) => {
|
|
88
|
+
// It is a number if it's in the defined variables, constants, or
|
|
89
|
+
// it's a valid number literal.
|
|
90
|
+
let isNumber = this.variables.has(token) || this.constants.has(token) || !Number.isNaN(Number(token)), isOperator = this.mathematicalOperators.has(token), isSpace = token.trim() == "", isBracket = token == "(" || token == ")";
|
|
91
|
+
// We don't really want anything for the spaces, other than simply
|
|
92
|
+
// adding them back to the view.
|
|
93
|
+
if (isSpace) {
|
|
94
|
+
formattedString = `${formattedString}${token}`;
|
|
95
|
+
currentPosition += token.length;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// If the cursor position is 'inside` the current token:
|
|
99
|
+
//
|
|
100
|
+
// 1. If we've got a recommendation to add, simply replace the
|
|
101
|
+
// word with the recommendation.
|
|
102
|
+
// 2. Ask the recommendor to fetch recommendations for this specific
|
|
103
|
+
// token/word.
|
|
104
|
+
if (currentPosition <= prevCurPos &&
|
|
105
|
+
currentPosition + token.length >= prevCurPos) {
|
|
106
|
+
// If a recommendation was provided, replace the correspoding
|
|
107
|
+
// word with it and move the cursor forward, accordingly.
|
|
108
|
+
if (recommendation) {
|
|
109
|
+
// Since we are sure that the recommendation will always correspond
|
|
110
|
+
// to a variable.
|
|
111
|
+
isNumber = true;
|
|
112
|
+
if (this.mathematicalOperators.has(token)) {
|
|
113
|
+
recommendation = token + recommendation;
|
|
114
|
+
}
|
|
115
|
+
// If the new cursor length somehow becomes larger than the
|
|
116
|
+
// length of the formula string, setting the caret to that
|
|
117
|
+
// length will move the caret to the start. Although this overflow
|
|
118
|
+
// won't happen, but still, this check prevents that.
|
|
119
|
+
parseOutput.newCursorPosition = Math.min(parseOutput.newCursorPosition +
|
|
120
|
+
recommendation.length -
|
|
121
|
+
token.length, formula.length + recommendation.length - token.length);
|
|
122
|
+
token = recommendation;
|
|
123
|
+
recommendation = null;
|
|
124
|
+
}
|
|
125
|
+
// Fetch recommendations nonetheless.
|
|
126
|
+
parseOutput.recommendations =
|
|
127
|
+
this._recommender.getRecommendation(token);
|
|
128
|
+
}
|
|
129
|
+
let tokenClassName = "";
|
|
130
|
+
// Don't check for errors if an error has already been encountered.
|
|
131
|
+
if (expectation != Expectation.UNDEFINED) {
|
|
132
|
+
// Unnecessary closing parenthesis
|
|
133
|
+
if (parentheses.isEmpty() && token == ")") {
|
|
134
|
+
parseOutput.errorString = `Unexpected ')' at position ${currentPosition}`;
|
|
135
|
+
tokenClassName += " error";
|
|
136
|
+
expectation = Expectation.UNDEFINED;
|
|
137
|
+
}
|
|
138
|
+
// Operator or ) after an operator. Eg: `23 / *` or `23 / )`
|
|
139
|
+
// Unary `+` and `-` are not an error as they might represent
|
|
140
|
+
// a positive or negative number respectively. But they will still
|
|
141
|
+
// be an error if the formula ends with them.
|
|
142
|
+
else if (expectation == Expectation.VARIABLE &&
|
|
143
|
+
!isNumber &&
|
|
144
|
+
token != "(" &&
|
|
145
|
+
!((token == "-" || token == "+") &&
|
|
146
|
+
(!currentTokens.trim() || this.mathematicalOperators.has(previousToken)))) {
|
|
147
|
+
parseOutput.errorString = `Expected variable/number at position ${currentPosition}`;
|
|
148
|
+
tokenClassName += " error";
|
|
149
|
+
expectation = Expectation.UNDEFINED;
|
|
150
|
+
}
|
|
151
|
+
// Number/Variable after the same. Eg: `a a` or `420 420`.
|
|
152
|
+
// Having a ) is fine. Eg: `23)` might be representing `(23 + 23)
|
|
153
|
+
else if (expectation == Expectation.OPERATOR &&
|
|
154
|
+
!isOperator &&
|
|
155
|
+
token != ")") {
|
|
156
|
+
parseOutput.errorString = `Expected mathematical operator at position ${currentPosition}`;
|
|
157
|
+
tokenClassName += " error";
|
|
158
|
+
expectation = Expectation.UNDEFINED;
|
|
159
|
+
}
|
|
160
|
+
// Unknown symbol/variable/word
|
|
161
|
+
else if (!(isNumber || isOperator || isBracket)) {
|
|
162
|
+
parseOutput.errorString = `Unknown word at position ${currentPosition}`;
|
|
163
|
+
tokenClassName += " error";
|
|
164
|
+
expectation = Expectation.UNDEFINED;
|
|
165
|
+
}
|
|
166
|
+
// The case of division by zero. Since we can't know if an expression evaluates
|
|
167
|
+
// to zero or not, that case can only be handled during calculation.
|
|
168
|
+
else if (isNumber &&
|
|
169
|
+
previousToken == "/" &&
|
|
170
|
+
(this.variables.get(token) == 0 || Number(token) == 0)) {
|
|
171
|
+
parseOutput.errorString = `Division by zero at position ${currentPosition}`;
|
|
172
|
+
tokenClassName += " error";
|
|
173
|
+
expectation = Expectation.UNDEFINED;
|
|
174
|
+
}
|
|
175
|
+
// Empty brackets. Default might be takn as 0, but that will only make sense
|
|
176
|
+
// in addition and subtraction and not in other operators, so making this
|
|
177
|
+
// case an error makes more sense.
|
|
178
|
+
else if (previousToken == "(" && token == ")") {
|
|
179
|
+
parseOutput.errorString = `Empty brackets at position ${currentPosition}`;
|
|
180
|
+
tokenClassName += " error";
|
|
181
|
+
expectation = Expectation.UNDEFINED;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Setting the expectation for the next token, if we have not encountered an
|
|
185
|
+
// error already.
|
|
186
|
+
if (expectation != Expectation.UNDEFINED) {
|
|
187
|
+
if (token == "(" || isOperator) {
|
|
188
|
+
expectation = Expectation.VARIABLE;
|
|
189
|
+
}
|
|
190
|
+
else if (token == ")" || isNumber) {
|
|
191
|
+
expectation = Expectation.OPERATOR;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (token == "(") {
|
|
195
|
+
parentheses.push(currentPosition);
|
|
196
|
+
tokenClassName += " bracket";
|
|
197
|
+
}
|
|
198
|
+
else if (token == ")") {
|
|
199
|
+
parentheses.pop();
|
|
200
|
+
tokenClassName += " bracket";
|
|
201
|
+
}
|
|
202
|
+
else if (isOperator) {
|
|
203
|
+
tokenClassName += " operator";
|
|
204
|
+
}
|
|
205
|
+
else if (expectation == Expectation.UNDEFINED) {
|
|
206
|
+
tokenClassName += " error";
|
|
207
|
+
}
|
|
208
|
+
// Since not using ShadowDOM, having these specific class names will prevent
|
|
209
|
+
// name collision.
|
|
210
|
+
formattedString = `${formattedString}<span class="wysiwygInternals ${tokenClassName}">${token}</span>`;
|
|
211
|
+
currentPosition += token.length;
|
|
212
|
+
previousToken = token;
|
|
213
|
+
currentTokens += token;
|
|
214
|
+
});
|
|
215
|
+
if (recommendation) {
|
|
216
|
+
parseOutput.newCursorPosition = Math.min(parseOutput.newCursorPosition +
|
|
217
|
+
recommendation.length, formula.length + recommendation.length);
|
|
218
|
+
formattedString = `${formattedString}<span class="wysiwygInternals">${recommendation}</span>`;
|
|
219
|
+
}
|
|
220
|
+
// If the formula ends with a mathematical operator, or has unclosed `(`
|
|
221
|
+
if (this.mathematicalOperators.has(previousToken)) {
|
|
222
|
+
// parseOutput.errorString = "Unexpected ending of formula.";
|
|
223
|
+
parseOutput.recommendations = Array.from(this.variables.keys());
|
|
224
|
+
}
|
|
225
|
+
else if (!parentheses.isEmpty()) {
|
|
226
|
+
parseOutput.errorString = `Unclosed '(' at position: ${parentheses.top()}`;
|
|
227
|
+
}
|
|
228
|
+
const parser = new DOMParser();
|
|
229
|
+
const doc = parser.parseFromString(formattedString, "text/html");
|
|
230
|
+
parseOutput.formattedContent = doc.querySelector("body");
|
|
231
|
+
parseOutput.formattedString = formattedString;
|
|
232
|
+
// Cache the result if no recommendation was used
|
|
233
|
+
if (!recommendation) {
|
|
234
|
+
this._parseCache.set(cacheKey, { ...parseOutput });
|
|
235
|
+
this.clearCacheIfNeeded();
|
|
236
|
+
}
|
|
237
|
+
return parseOutput;
|
|
238
|
+
}
|
|
239
|
+
buildRPN(formula) {
|
|
240
|
+
// Check cache
|
|
241
|
+
if (this._rpnCache.has(formula)) {
|
|
242
|
+
const cached = this._rpnCache.get(formula);
|
|
243
|
+
return cached ? cached.clone() : null;
|
|
244
|
+
}
|
|
245
|
+
if (this.parseInput(formula).errorString) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
const tokens = formula
|
|
249
|
+
.match(/'[^']*'|\d+|[A-Za-z_][A-Za-z0-9_]*|[-+(),*^/:?\s]/g)
|
|
250
|
+
?.filter((el) => !/\s+/.test(el) && el !== "");
|
|
251
|
+
// Handling the special case of unary `-` and `+`.
|
|
252
|
+
let previousToken = "";
|
|
253
|
+
let carriedToken = null;
|
|
254
|
+
const parsedTokens = [];
|
|
255
|
+
let currentTokens = "";
|
|
256
|
+
for (const token of tokens) {
|
|
257
|
+
if ((token == "+" || token == "-") &&
|
|
258
|
+
(!currentTokens.trim() || this.mathematicalOperators.has(previousToken))) {
|
|
259
|
+
carriedToken = token;
|
|
260
|
+
}
|
|
261
|
+
else if (carriedToken) {
|
|
262
|
+
parsedTokens.push(carriedToken + token);
|
|
263
|
+
carriedToken = null;
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
parsedTokens.push(token);
|
|
267
|
+
}
|
|
268
|
+
previousToken = token;
|
|
269
|
+
currentTokens += token;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Shunting Yard Algorithm (EW Dijkstra)
|
|
273
|
+
*/
|
|
274
|
+
const operatorStack = new Stack();
|
|
275
|
+
const outputQueue = new Queue();
|
|
276
|
+
for (const token of parsedTokens) {
|
|
277
|
+
if (token == "(") {
|
|
278
|
+
operatorStack.push("(");
|
|
279
|
+
}
|
|
280
|
+
else if (token == ")") {
|
|
281
|
+
while (operatorStack.top() != "(") {
|
|
282
|
+
outputQueue.enqueue(operatorStack.pop());
|
|
283
|
+
}
|
|
284
|
+
operatorStack.pop();
|
|
285
|
+
}
|
|
286
|
+
else if (this.mathematicalOperators.has(token)) {
|
|
287
|
+
while (this.mathematicalOperators.has(operatorStack.top()) &&
|
|
288
|
+
this.operatorPrecedence[token] <=
|
|
289
|
+
this.operatorPrecedence[operatorStack.top()]) {
|
|
290
|
+
outputQueue.enqueue(operatorStack.pop());
|
|
291
|
+
}
|
|
292
|
+
operatorStack.push(token);
|
|
293
|
+
}
|
|
294
|
+
else if (!Number.isNaN(token) && token != "") {
|
|
295
|
+
outputQueue.enqueue(token);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
while (operatorStack.top()) {
|
|
299
|
+
outputQueue.enqueue(operatorStack.pop());
|
|
300
|
+
}
|
|
301
|
+
// Cache the result
|
|
302
|
+
this._rpnCache.set(formula, outputQueue ? outputQueue.clone() : null);
|
|
303
|
+
this.clearCacheIfNeeded();
|
|
304
|
+
return outputQueue;
|
|
305
|
+
}
|
|
306
|
+
addParentheses(formula) {
|
|
307
|
+
const rpn = this.buildRPN(formula);
|
|
308
|
+
if (!rpn) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
const lexedRPN = [];
|
|
312
|
+
while (!rpn.isEmpty()) {
|
|
313
|
+
lexedRPN.push(rpn.dequeue());
|
|
314
|
+
}
|
|
315
|
+
// Stores the operators that we encounter in the RPN
|
|
316
|
+
let operatorStack = new Stack();
|
|
317
|
+
// Stores the `results`, which are essentially individual groups
|
|
318
|
+
// of tokens showing a meaningful value.
|
|
319
|
+
let resultStack = new Stack();
|
|
320
|
+
lexedRPN.forEach((symbol) => {
|
|
321
|
+
let parsedLeftExpression, parsedRightExpression;
|
|
322
|
+
// If we encounter a number or a variable in the RPN, it is itself
|
|
323
|
+
// a calculated entity (say a result in itself), needs no modification
|
|
324
|
+
// and can be directly put into the result stack.
|
|
325
|
+
if (this.variables.has(symbol) ||
|
|
326
|
+
(!isNaN(parseFloat(symbol)) && isFinite(parseFloat(symbol)))) {
|
|
327
|
+
resultStack.push(symbol);
|
|
328
|
+
operatorStack.push(null);
|
|
329
|
+
}
|
|
330
|
+
// If it is not a number/variable then it is an operator. We will
|
|
331
|
+
// take out previous operators from the `operatorStack`, compare
|
|
332
|
+
// them with the current one, adds brackets accordingly to the `results`
|
|
333
|
+
// around it, and then finally add it to the `operatorStack` for
|
|
334
|
+
// future reference.
|
|
335
|
+
else if (Object.keys(this.operatorPrecedence).includes(symbol)) {
|
|
336
|
+
let [rightExpression, leftExpression, operatorA, operatorB] = [
|
|
337
|
+
resultStack.pop(),
|
|
338
|
+
resultStack.pop(),
|
|
339
|
+
operatorStack.pop(),
|
|
340
|
+
operatorStack.pop(),
|
|
341
|
+
];
|
|
342
|
+
// The conditions that govern when to show a parenthesis.
|
|
343
|
+
if (this.operatorPrecedence[operatorB] <=
|
|
344
|
+
this.operatorPrecedence[symbol] ||
|
|
345
|
+
(this.operatorPrecedence[operatorB] ===
|
|
346
|
+
this.operatorPrecedence[symbol] &&
|
|
347
|
+
["/", "-"].includes(symbol))) {
|
|
348
|
+
parsedLeftExpression = `(${leftExpression})`;
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
parsedLeftExpression = leftExpression;
|
|
352
|
+
}
|
|
353
|
+
if (this.operatorPrecedence[operatorA] <=
|
|
354
|
+
this.operatorPrecedence[symbol] ||
|
|
355
|
+
(this.operatorPrecedence[operatorA] ===
|
|
356
|
+
this.operatorPrecedence[symbol] &&
|
|
357
|
+
["/", "-"].includes(symbol))) {
|
|
358
|
+
parsedRightExpression = `(${rightExpression})`;
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
parsedRightExpression = rightExpression;
|
|
362
|
+
}
|
|
363
|
+
// The bracket included expression is now itself a `result`
|
|
364
|
+
resultStack.push(`${parsedLeftExpression} ${symbol} ${parsedRightExpression}`);
|
|
365
|
+
operatorStack.push(symbol);
|
|
366
|
+
}
|
|
367
|
+
else
|
|
368
|
+
throw `${symbol} is not a recognized symbol`;
|
|
369
|
+
});
|
|
370
|
+
if (!resultStack.isEmpty()) {
|
|
371
|
+
return resultStack.pop();
|
|
372
|
+
}
|
|
373
|
+
else
|
|
374
|
+
throw `${lexedRPN} is not a correct RPN`;
|
|
375
|
+
}
|
|
376
|
+
evaluateFunction(name, value) {
|
|
377
|
+
switch (name) {
|
|
378
|
+
case 'sin': return Math.sin(value);
|
|
379
|
+
case 'cos': return Math.cos(value);
|
|
380
|
+
case 'tan': return Math.tan(value);
|
|
381
|
+
case 'sqrt':
|
|
382
|
+
if (value < 0)
|
|
383
|
+
throw new Error('Cannot calculate square root of negative number');
|
|
384
|
+
return Math.sqrt(value);
|
|
385
|
+
case 'log':
|
|
386
|
+
if (value <= 0)
|
|
387
|
+
throw new Error('Cannot calculate logarithm of non-positive number');
|
|
388
|
+
return Math.log(value);
|
|
389
|
+
default: throw new Error(`Unknown function: ${name}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
calculate(formula) {
|
|
393
|
+
let rpn = this.buildRPN(formula);
|
|
394
|
+
let calculationResult = {
|
|
395
|
+
result: undefined,
|
|
396
|
+
errorString: null,
|
|
397
|
+
};
|
|
398
|
+
if (!rpn) {
|
|
399
|
+
return calculationResult;
|
|
400
|
+
}
|
|
401
|
+
let calcStack = new Stack();
|
|
402
|
+
while (!rpn.isEmpty()) {
|
|
403
|
+
const frontItem = rpn.dequeue();
|
|
404
|
+
if (!this.mathematicalOperators.has(frontItem)) {
|
|
405
|
+
calcStack.push(Big(Number.parseFloat(this.variables.get(frontItem)?.toString() ?? frontItem)));
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
let operator = frontItem;
|
|
409
|
+
let numB = calcStack.pop();
|
|
410
|
+
let numA = calcStack.pop();
|
|
411
|
+
try {
|
|
412
|
+
// Handle constants
|
|
413
|
+
if (this.constants.has(frontItem)) {
|
|
414
|
+
calcStack.push(Big(this.constants.get(frontItem)));
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
// Handle functions
|
|
418
|
+
if (['sin', 'cos', 'tan', 'sqrt', 'log'].includes(frontItem)) {
|
|
419
|
+
const value = calcStack.pop();
|
|
420
|
+
try {
|
|
421
|
+
calcStack.push(Big(this.evaluateFunction(frontItem, parseFloat(value.toString()))));
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
calculationResult.errorString = error.message;
|
|
425
|
+
return calculationResult;
|
|
426
|
+
}
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
switch (operator) {
|
|
430
|
+
case "+":
|
|
431
|
+
calcStack.push(Big(numA).add(Big(numB)));
|
|
432
|
+
break;
|
|
433
|
+
case "-":
|
|
434
|
+
calcStack.push(Big(numA).sub(Big(numB)));
|
|
435
|
+
break;
|
|
436
|
+
case "*":
|
|
437
|
+
calcStack.push(Big(numA).mul(Big(numB)));
|
|
438
|
+
break;
|
|
439
|
+
case "/":
|
|
440
|
+
if (parseFloat(Big(numB).toString()) == 0) {
|
|
441
|
+
calculationResult.errorString = "Division by zero encountered";
|
|
442
|
+
return calculationResult;
|
|
443
|
+
}
|
|
444
|
+
calcStack.push(Big(numA).div(Big(numB)));
|
|
445
|
+
break;
|
|
446
|
+
// Big.js doesn't support exponentiating a Big to a Big, which
|
|
447
|
+
// is obvious due to performance overheads. Use this case with care.
|
|
448
|
+
case "^":
|
|
449
|
+
calcStack.push(Big(numA).pow(parseFloat(Big(numB).toString())));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
calculationResult.errorString = error;
|
|
454
|
+
return calculationResult;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
calculationResult.result = parseFloat(calcStack.top().toString());
|
|
459
|
+
return calculationResult;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { matchSorter } from 'match-sorter';
|
|
2
|
+
export class Recommender {
|
|
3
|
+
constructor(variables, minSuggestionLen) {
|
|
4
|
+
this._minimumSuggestionLength = minSuggestionLen > 0 ? minSuggestionLen : 1;
|
|
5
|
+
this.variableList = Array.from(variables.keys());
|
|
6
|
+
}
|
|
7
|
+
getRecommendation(word) {
|
|
8
|
+
if (word.length < this._minimumSuggestionLength) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const recommendations = matchSorter(this.variableList, word);
|
|
12
|
+
if (recommendations.length === 0 ||
|
|
13
|
+
(recommendations.length === 1 && recommendations[0] === word)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return recommendations;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { css } from "lit";
|
|
2
|
+
export const FormulaEditorStyles = css `
|
|
3
|
+
|
|
4
|
+
.formula-editor-label {
|
|
5
|
+
display: block;
|
|
6
|
+
font-size: var(--fe-label-font-size, 0.8rem);
|
|
7
|
+
color: var(--fe-label-color, #515151);
|
|
8
|
+
margin-bottom: var(--fe-label-margin-bottom, 1px);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
:host {
|
|
12
|
+
display: block;
|
|
13
|
+
position: relative;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
#wysiwyg-editor {
|
|
17
|
+
display: inline-block;
|
|
18
|
+
padding: var(--fe-padding, 8px);
|
|
19
|
+
caret-color: var(--fe-caret-color, #fff);
|
|
20
|
+
color: var(--fe-text-color, #f7f1ff);
|
|
21
|
+
line-height: 1.1;
|
|
22
|
+
width: var(--fe-width, 100%);
|
|
23
|
+
height: var(--fe-height, 60%);
|
|
24
|
+
border-radius: var(--fe-border-radius, 4px);
|
|
25
|
+
overflow: auto;
|
|
26
|
+
border: var(--fe-border, 2px solid black);
|
|
27
|
+
border-bottom: var(--fe-border-bottom, 0px solid black);
|
|
28
|
+
outline: none;
|
|
29
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
30
|
+
white-space: pre-wrap;
|
|
31
|
+
background-color: var(--fe-background-color, #222222);
|
|
32
|
+
margin: 0px;
|
|
33
|
+
box-sizing: border-box;
|
|
34
|
+
/* position: relative; */
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#wysiwyg-editor:empty:before {
|
|
38
|
+
content: attr(placeholder);
|
|
39
|
+
color: var(--fe-placeholder-color,grey);
|
|
40
|
+
pointer-events: none;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#wysiwyg-editor:focus {
|
|
44
|
+
border-color: var(--fe-focus-border-color, #7c5dfa);
|
|
45
|
+
box-shadow: 0 0 0 2px var(--fe-focus-shadow-color, rgba(124, 93, 250, 0.2));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Responsive styles */
|
|
49
|
+
@media (max-width: 768px) {
|
|
50
|
+
#wysiwyg-editor {
|
|
51
|
+
font-size: var(--fe-mobile-font-size, 14px);
|
|
52
|
+
padding: var(--fe-mobile-padding, 6px);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.wysiwygInternals.error {
|
|
57
|
+
text-decoration: underline;
|
|
58
|
+
-webkit-text-decoration-color: var(--fe-err-underline-color, #fc514f);
|
|
59
|
+
text-decoration-color: var(--fe-err-underline-color, #fc514f);
|
|
60
|
+
-webkit-text-decoration-style: wavy;
|
|
61
|
+
text-decoration-style: wavy;
|
|
62
|
+
/* text-decoration-thickness: 1px; */
|
|
63
|
+
text-decoration-color: var(--fe-err-underline-color, red);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.wysiwygInternals.bracket {
|
|
67
|
+
color: var(--fe-bracket-color, #fc514f);
|
|
68
|
+
transition: color 0.2s ease;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.wysiwygInternals.operator {
|
|
72
|
+
font-weight: bold;
|
|
73
|
+
color: var(--fe-operator-color, #fc618d);
|
|
74
|
+
transition: color 0.2s ease;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.wysiwygInternals.variable {
|
|
78
|
+
color: var(--fe-variable-color, #fc618d);
|
|
79
|
+
transition: color 0.2s ease;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.wysiwygInternals.function {
|
|
83
|
+
color: var(--fe-function-color, #82aaff);
|
|
84
|
+
font-style: italic;
|
|
85
|
+
transition: color 0.2s ease;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.wysiwygInternals.constant {
|
|
89
|
+
color: var(--fe-constant-color, #c3e88d);
|
|
90
|
+
font-weight: 500;
|
|
91
|
+
transition: color 0.2s ease;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* High contrast mode support */
|
|
95
|
+
@media (forced-colors: active) {
|
|
96
|
+
.wysiwygInternals.error {
|
|
97
|
+
forced-color-adjust: none;
|
|
98
|
+
text-decoration-color: CanvasText;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#wysiwyg-editor:focus {
|
|
102
|
+
outline: 2px solid Highlight;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.error-message {
|
|
107
|
+
color: var(--fe-error-color, #fc514f);
|
|
108
|
+
background-color: var(--fe-error-background, rgba(252, 81, 79, 0.1));
|
|
109
|
+
padding: var(--fe-error-padding, 8px);
|
|
110
|
+
border-radius: var(--fe-error-border-radius, 4px);
|
|
111
|
+
margin-top: var(--fe-error-margin-top, 4px);
|
|
112
|
+
animation: fadeIn 0.2s ease;
|
|
113
|
+
font-size: var(--fe-error-font-size, 0.8rem);
|
|
114
|
+
margin-top: var(--fe-error-margin-top, 4px);
|
|
115
|
+
min-height: var(--fe-error-min-height, 1.2em);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@keyframes fadeIn {
|
|
119
|
+
from {
|
|
120
|
+
opacity: 0;
|
|
121
|
+
transform: translateY(-4px);
|
|
122
|
+
}
|
|
123
|
+
to {
|
|
124
|
+
opacity: 1;
|
|
125
|
+
transform: translateY(0);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* Loading state */
|
|
130
|
+
.loading::after {
|
|
131
|
+
content: '';
|
|
132
|
+
position: absolute;
|
|
133
|
+
right: 8px;
|
|
134
|
+
top: 50%;
|
|
135
|
+
transform: translateY(-50%);
|
|
136
|
+
width: 12px;
|
|
137
|
+
height: 12px;
|
|
138
|
+
border: 2px solid var(--fe-loading-color, #fc618d);
|
|
139
|
+
border-radius: 50%;
|
|
140
|
+
border-top-color: transparent;
|
|
141
|
+
animation: spin 0.8s linear infinite;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@keyframes spin {
|
|
145
|
+
to {
|
|
146
|
+
transform: translateY(-50%) rotate(360deg);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
`;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { LitElement, html } from "lit";
|
|
8
|
+
import { customElement, property } from "lit/decorators.js";
|
|
9
|
+
import { Operator } from "../helpers/types";
|
|
10
|
+
let OperatorInput = class OperatorInput extends LitElement {
|
|
11
|
+
constructor() {
|
|
12
|
+
super(...arguments);
|
|
13
|
+
this.operator = Operator.NONE;
|
|
14
|
+
}
|
|
15
|
+
render() {
|
|
16
|
+
return html `<span> ${this.operator} </span>`;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
__decorate([
|
|
20
|
+
property()
|
|
21
|
+
], OperatorInput.prototype, "operator", void 0);
|
|
22
|
+
OperatorInput = __decorate([
|
|
23
|
+
customElement("operator-input")
|
|
24
|
+
], OperatorInput);
|