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.
@@ -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 = ["to", "in"];
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 isIdentifier = (s) => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(s);
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
- // 1. Extract all alphanumeric words into an array
24
- const words = str.match(/[a-z0-9]+/gi) || [];
25
-
26
- // 2. Identify the current character and the one immediately before it
27
- const currentChar = str[charIndex] || null;
28
- const prevChar = charIndex > 0 ? str[charIndex - 1] : null;
29
-
30
- // 3. Find the word that contains the current charIndex
31
- let start = charIndex;
32
- // Move pointer back to the start of the current word
33
- while (start > 0 && /[a-z0-9]/i.test(str[start - 1])) start--;
34
-
35
- let end = charIndex;
36
- // Move pointer forward to the end of the current word
37
- while (end < str.length && /[a-z0-9]/i.test(str[end])) end++;
38
-
39
- const currentWord = str.substring(start, end);
40
-
41
- // 4. Find the word that appears before the currentWord in the sequence
42
- const currentWordIdx = words.indexOf(currentWord);
43
- const prevWord = currentWordIdx > 0 ? words[currentWordIdx - 1] : null;
44
-
45
- // 5. Find the word that appears after the currentWord
46
- const nextWord = (currentWordIdx !== -1 && currentWordIdx < words.length - 1)
47
- ? words[currentWordIdx + 1]
48
- : null;
49
-
50
- return {
51
- prevWord: prevWord,
52
- prevChar: prevChar,
53
- currentWord: currentWord,
54
- currentChar: currentChar,
55
- nextWord: nextWord
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 = (prev) =>
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 === "Operator" ||
62
- prev.type === "UnaryOperator" ||
63
- (prev.type === "Parenthesis" && prev.value !== ")") ||
64
- prev.type === "ArrayStart" ||
65
- prev.type === "Semicolon" ||
66
- prev.type === "Comma" ||
67
- prev.type === "Ternary";
68
-
69
- const flushCurrent = (nextChar, index) => {
70
- if (!current) return;
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: "Boolean", value: current.toLowerCase() === "true" });
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: "Keyword", value: current, pos: index });
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: "BigInt", value: BigInt(current.slice(0, -1)), pos: index });
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: "Number", value: parseInt(current, 16), pos: index });
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: "Number", value: parseInt(current, 2), pos: index });
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: "Number", value: parseFloat(current), pos: index });
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: "ImaginaryLiteral",
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] === "-" ? -1 : 1;
149
+ const sign = current[0] === '-' ? -1 : 1;
128
150
  tokens.push({
129
- type: "ImaginaryLiteral",
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) ? "NumberWithUnit" : "UnknownUnit",
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 === "to" || prevWord === "in") {
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: "Unit", value: current, pos: index });
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
- if (isIdentifier(current)) {
172
- if (nextChar === "(") {
193
+ if (isIdentifier(current)) {
194
+ if (nextChar === '(') {
173
195
  tokens.push({
174
- type: "Function",
196
+ type: 'Function',
175
197
  name: current,
176
- pos: index
198
+ pos: index,
177
199
  });
178
200
  } else {
179
201
  tokens.push({
180
- type: "Identifier",
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
- let char = expr[i];
196
- let next = expr[i + 1];
216
+ const char = expr[i];
217
+ const next = expr[i + 1];
197
218
 
198
219
  // comments
199
- if (char === "/" && next === "/") {
200
- while (i < expr.length && expr[i] !== "\n") 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 === "/" && next === "*") {
227
+ if (char === '/' && next === '*') {
205
228
  i += 2;
206
- while (i < expr.length && !(expr[i] === "*" && expr[i + 1] === "/")) 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: "String",
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: "Operator", value: twoChar, pos: i });
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: "Ternary", value: "?" });
274
+ if (char === '?') {
275
+ tokens.push({ type: 'Ternary', value: '?' });
251
276
  continue;
252
277
  }
253
278
 
254
- // only treat ':' as ternary IF previous token was '?'
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 === "Ternary") {
260
- tokens.push({ type: "Ternary", value: ":" });
284
+ if (prev && prev.type === 'Ternary') {
285
+ tokens.push({ type: 'Ternary', value: ':' });
261
286
  } else {
262
- tokens.push({ type: "Colon" });
287
+ tokens.push({ type: 'Colon' });
263
288
  }
264
289
  continue;
265
290
  }
266
291
 
267
- // dot
268
- if (char === "." && /\d/.test(current) && /\d/.test(next)) {
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: "Dot", pos: i });
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 === "-" || char === "!") && isUnaryContext(prev)) {
285
- tokens.push({ type: "UnaryOperator", value: char, pos: i });
317
+ if ((char === '-' || char === '!') && isUnaryContext(prev)) {
318
+ tokens.push({ type: 'UnaryOperator', value: char, pos: i });
286
319
  } else {
287
- tokens.push({ type: "Operator", value: char, pos: i });
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: "Parenthesis", value: char, pos: i });
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: "ArrayStart", pos: i });
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: "ArrayEnd", pos: i });
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: "BlockStart", pos: i });
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: "BlockEnd", pos: i });
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: "Comma", pos: i });
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: "Semicolon", pos: i });
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) throw new Error("Unclosed string literal");
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 === "Number" && next?.type === "Unit") {
397
+ if (t?.type === 'Number' && next?.type === 'Unit') {
363
398
  merged.push({
364
- type: "NumberWithUnit",
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 && b &&
386
- (
387
- (["Number", "Identifier"].includes(a.type) ||
388
- (a.type === "Parenthesis" && a.value === ")") ||
389
- a.type === "ArrayEnd") &&
390
- (["Identifier", "Function"].includes(b.type) ||
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: "Operator", value: "*", implicit: true });
427
+ final.push({ type: 'Operator', value: '*', implicit: true });
395
428
  }
396
429
  }
397
430