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