exprify 1.0.4 → 1.0.6
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/HISTORY.md +49 -0
- package/README.md +100 -180
- package/SECURITY.md +18 -0
- package/bin/cli.mjs +234 -0
- package/dist/exprify.cjs.cjs +3558 -1220
- package/dist/exprify.cjs.cjs.map +1 -1
- package/dist/exprify.esm.js +3558 -1220
- package/dist/exprify.esm.js.map +1 -1
- package/dist/exprify.js +3560 -1222
- package/dist/exprify.js.map +1 -1
- package/dist/exprify.min.js +2 -2
- package/dist/exprify.min.js.map +1 -1
- package/package.json +44 -17
- package/src/core/context.js +35 -27
- package/src/core/exprify.js +880 -0
- package/src/function/executor.js +29 -20
- package/src/function/internal.js +1150 -153
- package/src/function/registry.js +23 -16
- package/src/index.js +1 -1
- package/src/math/bignumber.js +31 -0
- package/src/math/fraction.js +112 -0
- package/src/math/operations.js +38 -24
- package/src/parser/astBuild.js +276 -214
- package/src/parser/evaluator.js +431 -171
- package/src/parser/tokenizer.js +179 -146
- package/src/utils/decimal.js +264 -0
- package/src/utils/globalUnits.js +43 -35
- package/src/utils/matrix.js +14 -14
- package/src/utils/store.js +69 -47
- package/src/variables/store.js +18 -15
- package/src/core/Exprify.js +0 -369
package/src/parser/tokenizer.js
CHANGED
|
@@ -1,136 +1,158 @@
|
|
|
1
|
+
/** @param {string | any[]} expr */
|
|
1
2
|
export function tokenize(expr, context = {}) {
|
|
2
3
|
const tokens = [];
|
|
3
|
-
let current =
|
|
4
|
-
let quote =
|
|
4
|
+
let current = '';
|
|
5
|
+
let quote = '';
|
|
5
6
|
|
|
6
|
-
const operators = [
|
|
7
|
+
const operators = ['+', '-', '*', '/', '%', '^', '=', '>', '<', '!', '&', '|'];
|
|
8
|
+
// Two-char operators checked before single-char to avoid ambiguity (e.g., == vs =)
|
|
7
9
|
const multiOps = [
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
'==',
|
|
11
|
+
'>=',
|
|
12
|
+
'<=',
|
|
13
|
+
'&&',
|
|
14
|
+
'||',
|
|
15
|
+
'+=',
|
|
16
|
+
'-=',
|
|
17
|
+
'*=',
|
|
18
|
+
'/=',
|
|
19
|
+
'%=',
|
|
20
|
+
'?.',
|
|
21
|
+
'??',
|
|
22
|
+
'|>',
|
|
23
|
+
'->',
|
|
11
24
|
];
|
|
12
25
|
|
|
13
|
-
const parentheses =
|
|
14
|
-
const comma =
|
|
15
|
-
const semicolon =
|
|
16
|
-
const keywords = [
|
|
17
|
-
// const functions = context.functions?.getAllFunctionsName?.() || [];
|
|
18
|
-
const units = context.units?.getAllUnitsFlat?.() || [];
|
|
26
|
+
const parentheses = '()';
|
|
27
|
+
const comma = ',';
|
|
28
|
+
const semicolon = ';';
|
|
29
|
+
const keywords = ['to', 'in'];
|
|
19
30
|
|
|
20
|
-
const
|
|
31
|
+
const units = context.units?.getAllUnitsFlat?.() || [];
|
|
32
|
+
const isIdentifier = (/** @type {string} */ s) => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(s);
|
|
21
33
|
|
|
34
|
+
/**
|
|
35
|
+
* @param {any} str
|
|
36
|
+
* @param {number} charIndex
|
|
37
|
+
*/
|
|
22
38
|
function getContext(str, charIndex) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
39
|
+
const words = str.match(/[a-z0-9]+/gi) || [];
|
|
40
|
+
|
|
41
|
+
// 2. Identify the current character and the one immediately before it
|
|
42
|
+
const currentChar = str[charIndex] || null;
|
|
43
|
+
const prevChar = charIndex > 0 ? str[charIndex - 1] : null;
|
|
44
|
+
|
|
45
|
+
// 3. Find the word that contains the current charIndex
|
|
46
|
+
let start = charIndex;
|
|
47
|
+
|
|
48
|
+
while (start > 0 && /[a-z0-9]/i.test(str[start - 1])) {
|
|
49
|
+
start--;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let end = charIndex;
|
|
53
|
+
|
|
54
|
+
while (end < str.length && /[a-z0-9]/i.test(str[end])) {
|
|
55
|
+
end++;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const currentWord = str.substring(start, end);
|
|
59
|
+
|
|
60
|
+
// 4. Find the word that appears before the currentWord in the sequence
|
|
61
|
+
const currentWordIdx = words.indexOf(currentWord);
|
|
62
|
+
const prevWord = currentWordIdx > 0 ? words[currentWordIdx - 1] : null;
|
|
63
|
+
|
|
64
|
+
// 5. Find the word that appears after the currentWord
|
|
65
|
+
const nextWord =
|
|
66
|
+
currentWordIdx !== -1 && currentWordIdx < words.length - 1 ? words[currentWordIdx + 1] : null;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
prevWord: prevWord,
|
|
70
|
+
prevChar: prevChar,
|
|
71
|
+
currentWord: currentWord,
|
|
72
|
+
currentChar: currentChar,
|
|
73
|
+
nextWord: nextWord,
|
|
74
|
+
};
|
|
57
75
|
}
|
|
58
76
|
|
|
59
|
-
const isUnaryContext = (
|
|
77
|
+
const isUnaryContext = (
|
|
78
|
+
/** @type {{ type: string; value: any; pos: number; } | { type: string; value: string; pos?: undefined; } | { type: string; value?: undefined; pos?: undefined; } | { type: string; pos: number; value?: undefined; }} */ prev
|
|
79
|
+
) =>
|
|
60
80
|
!prev ||
|
|
61
|
-
prev.type ===
|
|
62
|
-
prev.type ===
|
|
63
|
-
(prev.type ===
|
|
64
|
-
prev.type ===
|
|
65
|
-
prev.type ===
|
|
66
|
-
prev.type ===
|
|
67
|
-
prev.type ===
|
|
68
|
-
|
|
69
|
-
const flushCurrent = (nextChar, index) => {
|
|
70
|
-
if (!current)
|
|
81
|
+
prev.type === 'Operator' ||
|
|
82
|
+
prev.type === 'UnaryOperator' ||
|
|
83
|
+
(prev.type === 'Parenthesis' && prev.value !== ')') ||
|
|
84
|
+
prev.type === 'ArrayStart' ||
|
|
85
|
+
prev.type === 'Semicolon' ||
|
|
86
|
+
prev.type === 'Comma' ||
|
|
87
|
+
prev.type === 'Ternary';
|
|
88
|
+
|
|
89
|
+
const flushCurrent = (/** @type {string | null} */ nextChar, /** @type {number} */ index) => {
|
|
90
|
+
if (!current) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
71
93
|
|
|
72
94
|
// BOOLEAN
|
|
73
95
|
if (/^(true|false)$/i.test(current)) {
|
|
74
|
-
tokens.push({ type:
|
|
75
|
-
current =
|
|
96
|
+
tokens.push({ type: 'Boolean', value: current.toLowerCase() === 'true' });
|
|
97
|
+
current = '';
|
|
76
98
|
return;
|
|
77
99
|
}
|
|
78
100
|
|
|
79
101
|
// KEYWORD
|
|
80
102
|
if (keywords.includes(current)) {
|
|
81
|
-
tokens.push({ type:
|
|
82
|
-
current =
|
|
103
|
+
tokens.push({ type: 'Keyword', value: current, pos: index });
|
|
104
|
+
current = '';
|
|
83
105
|
return;
|
|
84
106
|
}
|
|
85
107
|
|
|
86
108
|
// BIGINT
|
|
87
109
|
if (/^\d+n$/.test(current)) {
|
|
88
|
-
tokens.push({ type:
|
|
89
|
-
current =
|
|
110
|
+
tokens.push({ type: 'BigInt', value: BigInt(current.slice(0, -1)), pos: index });
|
|
111
|
+
current = '';
|
|
90
112
|
return;
|
|
91
113
|
}
|
|
92
114
|
|
|
93
115
|
// HEX
|
|
94
116
|
if (/^0x[0-9a-fA-F]+$/.test(current)) {
|
|
95
|
-
tokens.push({ type:
|
|
96
|
-
current =
|
|
117
|
+
tokens.push({ type: 'Number', value: parseInt(current, 16), pos: index });
|
|
118
|
+
current = '';
|
|
97
119
|
return;
|
|
98
120
|
}
|
|
99
121
|
|
|
100
122
|
// BINARY
|
|
101
123
|
if (/^0b[01]+$/.test(current)) {
|
|
102
|
-
tokens.push({ type:
|
|
103
|
-
current =
|
|
124
|
+
tokens.push({ type: 'Number', value: parseInt(current, 2), pos: index });
|
|
125
|
+
current = '';
|
|
104
126
|
return;
|
|
105
127
|
}
|
|
106
128
|
|
|
107
129
|
// NUMBER (including scientific)
|
|
108
130
|
if (/^[+-]?(\d+(\.\d+)?|\.\d+)(e[+-]?\d+)?$/i.test(current)) {
|
|
109
|
-
tokens.push({ type:
|
|
110
|
-
current =
|
|
131
|
+
tokens.push({ type: 'Number', value: parseFloat(current), pos: index });
|
|
132
|
+
current = '';
|
|
111
133
|
return;
|
|
112
134
|
}
|
|
113
135
|
|
|
114
136
|
// IMAGINARY NUMBER
|
|
115
137
|
if (/^[+-]?(\d+(\.\d+)?|\.\d+)(e[+-]?\d+)?i$/i.test(current)) {
|
|
116
138
|
tokens.push({
|
|
117
|
-
type:
|
|
139
|
+
type: 'ImaginaryLiteral',
|
|
118
140
|
value: parseFloat(current.slice(0, -1)),
|
|
119
|
-
pos: index
|
|
141
|
+
pos: index,
|
|
120
142
|
});
|
|
121
|
-
current =
|
|
143
|
+
current = '';
|
|
122
144
|
return;
|
|
123
145
|
}
|
|
124
146
|
|
|
125
147
|
// IMAGINARY UNIT
|
|
126
148
|
if (/^[+-]?i$/i.test(current)) {
|
|
127
|
-
const sign = current[0] ===
|
|
149
|
+
const sign = current[0] === '-' ? -1 : 1;
|
|
128
150
|
tokens.push({
|
|
129
|
-
type:
|
|
151
|
+
type: 'ImaginaryLiteral',
|
|
130
152
|
value: sign,
|
|
131
|
-
pos: index
|
|
153
|
+
pos: index,
|
|
132
154
|
});
|
|
133
|
-
current =
|
|
155
|
+
current = '';
|
|
134
156
|
return;
|
|
135
157
|
}
|
|
136
158
|
|
|
@@ -141,26 +163,26 @@ export function tokenize(expr, context = {}) {
|
|
|
141
163
|
const unit = numUnit[3];
|
|
142
164
|
|
|
143
165
|
tokens.push({
|
|
144
|
-
type: units.includes(unit) ?
|
|
166
|
+
type: units.includes(unit) ? 'NumberWithUnit' : 'UnknownUnit',
|
|
145
167
|
value,
|
|
146
168
|
unit,
|
|
147
|
-
pos: index
|
|
169
|
+
pos: index,
|
|
148
170
|
});
|
|
149
171
|
|
|
150
|
-
current =
|
|
172
|
+
current = '';
|
|
151
173
|
return;
|
|
152
174
|
}
|
|
153
175
|
|
|
154
176
|
// UNIT
|
|
155
177
|
if (units.includes(current)) {
|
|
156
|
-
const {prevWord} = getContext(expr, index);
|
|
157
|
-
if (nextChar !==
|
|
158
|
-
if (prevWord){
|
|
159
|
-
if (!isNaN(parseFloat(prevWord)) || prevWord ===
|
|
178
|
+
const { prevWord } = getContext(expr, index);
|
|
179
|
+
if (nextChar !== '(') {
|
|
180
|
+
if (prevWord) {
|
|
181
|
+
if (!isNaN(parseFloat(prevWord)) || prevWord === 'to' || prevWord === 'in') {
|
|
160
182
|
// console.log("Context for unit detection:", {current, prevWord, nextChar});
|
|
161
183
|
|
|
162
|
-
tokens.push({ type:
|
|
163
|
-
current =
|
|
184
|
+
tokens.push({ type: 'Unit', value: current, pos: index });
|
|
185
|
+
current = '';
|
|
164
186
|
return;
|
|
165
187
|
}
|
|
166
188
|
}
|
|
@@ -168,42 +190,45 @@ export function tokenize(expr, context = {}) {
|
|
|
168
190
|
}
|
|
169
191
|
|
|
170
192
|
// IDENTIFIER
|
|
171
|
-
|
|
172
|
-
if (nextChar ===
|
|
193
|
+
if (isIdentifier(current)) {
|
|
194
|
+
if (nextChar === '(') {
|
|
173
195
|
tokens.push({
|
|
174
|
-
type:
|
|
196
|
+
type: 'Function',
|
|
175
197
|
name: current,
|
|
176
|
-
pos: index
|
|
198
|
+
pos: index,
|
|
177
199
|
});
|
|
178
200
|
} else {
|
|
179
201
|
tokens.push({
|
|
180
|
-
type:
|
|
202
|
+
type: 'Identifier',
|
|
181
203
|
name: current,
|
|
182
|
-
pos: index
|
|
204
|
+
pos: index,
|
|
183
205
|
});
|
|
184
206
|
}
|
|
185
207
|
|
|
186
|
-
current =
|
|
208
|
+
current = '';
|
|
187
209
|
return;
|
|
188
210
|
}
|
|
189
211
|
|
|
190
212
|
throw new Error(`Invalid token "${current}" at index ${index}`);
|
|
191
213
|
};
|
|
192
|
-
|
|
193
214
|
|
|
194
215
|
for (let i = 0; i < expr.length; i++) {
|
|
195
|
-
|
|
196
|
-
|
|
216
|
+
const char = expr[i];
|
|
217
|
+
const next = expr[i + 1];
|
|
197
218
|
|
|
198
219
|
// comments
|
|
199
|
-
if (char ===
|
|
200
|
-
while (i < expr.length && expr[i] !==
|
|
220
|
+
if (char === '/' && next === '/') {
|
|
221
|
+
while (i < expr.length && expr[i] !== '\n') {
|
|
222
|
+
i++;
|
|
223
|
+
}
|
|
201
224
|
continue;
|
|
202
225
|
}
|
|
203
226
|
|
|
204
|
-
if (char ===
|
|
227
|
+
if (char === '/' && next === '*') {
|
|
205
228
|
i += 2;
|
|
206
|
-
while (i < expr.length && !(expr[i] ===
|
|
229
|
+
while (i < expr.length && !(expr[i] === '*' && expr[i + 1] === '/')) {
|
|
230
|
+
i++;
|
|
231
|
+
}
|
|
207
232
|
i++;
|
|
208
233
|
continue;
|
|
209
234
|
}
|
|
@@ -216,12 +241,12 @@ export function tokenize(expr, context = {}) {
|
|
|
216
241
|
} else if (quote === char) {
|
|
217
242
|
current += char;
|
|
218
243
|
tokens.push({
|
|
219
|
-
type:
|
|
244
|
+
type: 'String',
|
|
220
245
|
value: current.slice(1, -1),
|
|
221
|
-
pos: i
|
|
246
|
+
pos: i,
|
|
222
247
|
});
|
|
223
|
-
current =
|
|
224
|
-
quote =
|
|
248
|
+
current = '';
|
|
249
|
+
quote = '';
|
|
225
250
|
} else {
|
|
226
251
|
current += char;
|
|
227
252
|
}
|
|
@@ -229,7 +254,7 @@ export function tokenize(expr, context = {}) {
|
|
|
229
254
|
}
|
|
230
255
|
|
|
231
256
|
if (quote) {
|
|
232
|
-
if (char ===
|
|
257
|
+
if (char === '\\') {
|
|
233
258
|
current += char + expr[++i];
|
|
234
259
|
} else {
|
|
235
260
|
current += char;
|
|
@@ -241,38 +266,46 @@ export function tokenize(expr, context = {}) {
|
|
|
241
266
|
const twoChar = char + next;
|
|
242
267
|
if (multiOps.includes(twoChar)) {
|
|
243
268
|
flushCurrent(char, i);
|
|
244
|
-
tokens.push({ type:
|
|
269
|
+
tokens.push({ type: 'Operator', value: twoChar, pos: i });
|
|
245
270
|
i++;
|
|
246
271
|
continue;
|
|
247
272
|
}
|
|
248
273
|
|
|
249
|
-
if (char ===
|
|
250
|
-
tokens.push({ type:
|
|
274
|
+
if (char === '?') {
|
|
275
|
+
tokens.push({ type: 'Ternary', value: '?' });
|
|
251
276
|
continue;
|
|
252
277
|
}
|
|
253
278
|
|
|
254
|
-
//
|
|
255
|
-
if (char ===
|
|
279
|
+
// Colon after '?' is ternary separator; otherwise standalone (range, object key)
|
|
280
|
+
if (char === ':') {
|
|
256
281
|
flushCurrent(char, i);
|
|
257
282
|
const prev = tokens[tokens.length - 1];
|
|
258
283
|
|
|
259
|
-
if (prev && prev.type ===
|
|
260
|
-
tokens.push({ type:
|
|
284
|
+
if (prev && prev.type === 'Ternary') {
|
|
285
|
+
tokens.push({ type: 'Ternary', value: ':' });
|
|
261
286
|
} else {
|
|
262
|
-
tokens.push({ type:
|
|
287
|
+
tokens.push({ type: 'Colon' });
|
|
263
288
|
}
|
|
264
289
|
continue;
|
|
265
290
|
}
|
|
266
291
|
|
|
267
|
-
//
|
|
268
|
-
if (char ===
|
|
292
|
+
// Three dots form the spread operator (...)
|
|
293
|
+
if (char === '.' && next === '.' && expr[i + 2] === '.') {
|
|
294
|
+
flushCurrent(char, i);
|
|
295
|
+
tokens.push({ type: 'Spread', pos: i });
|
|
296
|
+
i += 2;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Dot between digits is a decimal separator, not property access
|
|
301
|
+
if (char === '.' && /\d/.test(current) && /\d/.test(next)) {
|
|
269
302
|
current += char;
|
|
270
303
|
continue;
|
|
271
304
|
}
|
|
272
305
|
|
|
273
|
-
if (char ===
|
|
306
|
+
if (char === '.') {
|
|
274
307
|
flushCurrent(char, i);
|
|
275
|
-
tokens.push({ type:
|
|
308
|
+
tokens.push({ type: 'Dot', pos: i });
|
|
276
309
|
continue;
|
|
277
310
|
}
|
|
278
311
|
|
|
@@ -281,10 +314,10 @@ export function tokenize(expr, context = {}) {
|
|
|
281
314
|
flushCurrent(char, i);
|
|
282
315
|
|
|
283
316
|
const prev = tokens[tokens.length - 1];
|
|
284
|
-
if ((char ===
|
|
285
|
-
tokens.push({ type:
|
|
317
|
+
if ((char === '-' || char === '!') && isUnaryContext(prev)) {
|
|
318
|
+
tokens.push({ type: 'UnaryOperator', value: char, pos: i });
|
|
286
319
|
} else {
|
|
287
|
-
tokens.push({ type:
|
|
320
|
+
tokens.push({ type: 'Operator', value: char, pos: i });
|
|
288
321
|
}
|
|
289
322
|
continue;
|
|
290
323
|
}
|
|
@@ -292,53 +325,53 @@ export function tokenize(expr, context = {}) {
|
|
|
292
325
|
// parenthesis
|
|
293
326
|
if (parentheses.includes(char)) {
|
|
294
327
|
flushCurrent(char, i);
|
|
295
|
-
tokens.push({ type:
|
|
328
|
+
tokens.push({ type: 'Parenthesis', value: char, pos: i });
|
|
296
329
|
continue;
|
|
297
330
|
}
|
|
298
331
|
|
|
299
332
|
// array
|
|
300
|
-
if (char ===
|
|
333
|
+
if (char === '[') {
|
|
301
334
|
flushCurrent(char, i);
|
|
302
|
-
tokens.push({ type:
|
|
335
|
+
tokens.push({ type: 'ArrayStart', pos: i });
|
|
303
336
|
continue;
|
|
304
337
|
}
|
|
305
338
|
|
|
306
|
-
if (char ===
|
|
339
|
+
if (char === ']') {
|
|
307
340
|
flushCurrent(char, i);
|
|
308
|
-
tokens.push({ type:
|
|
341
|
+
tokens.push({ type: 'ArrayEnd', pos: i });
|
|
309
342
|
continue;
|
|
310
343
|
}
|
|
311
344
|
|
|
312
345
|
// OBJECT START
|
|
313
|
-
if (char ===
|
|
346
|
+
if (char === '{') {
|
|
314
347
|
flushCurrent(char, i);
|
|
315
|
-
tokens.push({ type:
|
|
348
|
+
tokens.push({ type: 'BlockStart', pos: i });
|
|
316
349
|
continue;
|
|
317
350
|
}
|
|
318
351
|
|
|
319
352
|
// OBJECT END
|
|
320
|
-
if (char ===
|
|
353
|
+
if (char === '}') {
|
|
321
354
|
flushCurrent(char, i);
|
|
322
|
-
tokens.push({ type:
|
|
355
|
+
tokens.push({ type: 'BlockEnd', pos: i });
|
|
323
356
|
continue;
|
|
324
357
|
}
|
|
325
358
|
|
|
326
359
|
// comma
|
|
327
360
|
if (char === comma) {
|
|
328
361
|
flushCurrent(char, i);
|
|
329
|
-
tokens.push({ type:
|
|
362
|
+
tokens.push({ type: 'Comma', pos: i });
|
|
330
363
|
continue;
|
|
331
364
|
}
|
|
332
365
|
|
|
333
366
|
// semicolon
|
|
334
367
|
if (char === semicolon) {
|
|
335
368
|
flushCurrent(char, i);
|
|
336
|
-
tokens.push({ type:
|
|
369
|
+
tokens.push({ type: 'Semicolon', pos: i });
|
|
337
370
|
continue;
|
|
338
371
|
}
|
|
339
372
|
|
|
340
373
|
// space
|
|
341
|
-
if (char ===
|
|
374
|
+
if (char === ' ') {
|
|
342
375
|
flushCurrent(next, i);
|
|
343
376
|
continue;
|
|
344
377
|
}
|
|
@@ -351,7 +384,9 @@ export function tokenize(expr, context = {}) {
|
|
|
351
384
|
}
|
|
352
385
|
}
|
|
353
386
|
|
|
354
|
-
if (quote)
|
|
387
|
+
if (quote) {
|
|
388
|
+
throw new Error('Unclosed string literal');
|
|
389
|
+
}
|
|
355
390
|
|
|
356
391
|
// merge number + unit
|
|
357
392
|
const merged = [];
|
|
@@ -359,12 +394,12 @@ export function tokenize(expr, context = {}) {
|
|
|
359
394
|
const t = tokens[i];
|
|
360
395
|
const next = tokens[i + 1];
|
|
361
396
|
|
|
362
|
-
if (t?.type ===
|
|
397
|
+
if (t?.type === 'Number' && next?.type === 'Unit') {
|
|
363
398
|
merged.push({
|
|
364
|
-
type:
|
|
399
|
+
type: 'NumberWithUnit',
|
|
365
400
|
value: t.value,
|
|
366
401
|
unit: next.value,
|
|
367
|
-
pos: t.pos
|
|
402
|
+
pos: t.pos,
|
|
368
403
|
});
|
|
369
404
|
i++;
|
|
370
405
|
continue;
|
|
@@ -373,7 +408,7 @@ export function tokenize(expr, context = {}) {
|
|
|
373
408
|
merged.push(t);
|
|
374
409
|
}
|
|
375
410
|
|
|
376
|
-
// implicit multiplication
|
|
411
|
+
// Insert implicit * between tokens where multiplication is implied (e.g., "2x" -> "2*x", ")(a)" -> ")*(a)")
|
|
377
412
|
const final = [];
|
|
378
413
|
for (let i = 0; i < merged.length; i++) {
|
|
379
414
|
const a = merged[i];
|
|
@@ -382,16 +417,14 @@ export function tokenize(expr, context = {}) {
|
|
|
382
417
|
final.push(a);
|
|
383
418
|
|
|
384
419
|
if (
|
|
385
|
-
a &&
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
(b.type === "Parenthesis" && b.value === "("))
|
|
392
|
-
)
|
|
420
|
+
a &&
|
|
421
|
+
b &&
|
|
422
|
+
(['Number', 'Identifier'].includes(a.type) ||
|
|
423
|
+
(a.type === 'Parenthesis' && a.value === ')') ||
|
|
424
|
+
a.type === 'ArrayEnd') &&
|
|
425
|
+
(['Identifier', 'Function'].includes(b.type) || (b.type === 'Parenthesis' && b.value === '('))
|
|
393
426
|
) {
|
|
394
|
-
final.push({ type:
|
|
427
|
+
final.push({ type: 'Operator', value: '*', implicit: true });
|
|
395
428
|
}
|
|
396
429
|
}
|
|
397
430
|
|