@cocreate/calculate 1.16.5 → 1.17.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/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [1.17.0](https://github.com/CoCreate-app/CoCreate-calculate/compare/v1.16.5...v1.17.0) (2026-03-01)
2
+
3
+
4
+ ### Features
5
+
6
+ * Enhance element initialization and calculation logic ([ce7a6a0](https://github.com/CoCreate-app/CoCreate-calculate/commit/ce7a6a0cf42a63ee092fba38626179cbf573d5cb))
7
+
1
8
  ## [1.16.5](https://github.com/CoCreate-app/CoCreate-calculate/compare/v1.16.4...v1.16.5) (2026-02-09)
2
9
 
3
10
 
package/demo/index.html CHANGED
@@ -1,25 +1,99 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
- <head>
4
- <title>Calculate | CoCreateJS</title>
5
- <!-- CoCreate Favicon -->
6
- <link
7
- rel="icon"
8
- type="image/png"
9
- sizes="32x32"
10
- href="../assets/favicon.ico" />
11
- <link rel="manifest" href="/manifest.webmanifest" />
12
- </head>
13
- <body>
14
- <input value="12" calculate="$value * 100" />
15
- <input class="class1" key="total" id="id1" value="12" />
16
- <input class="class1" key="total" id="id2" value="13" />
17
-
18
- <input id="te" calculate="($document #id1) + ($document #id2)" />
19
- <input calculate="($document [key='total']) + 1" />
20
- <h1 calculate="($document [key='total']) + ($document .class1)">sum</h1>
21
-
22
- <!--<script src="../dist/CoCreate-calculate.js"></script>-->
23
- <script src="https://CoCreate.app/dist/CoCreate.js"></script>
24
- </body>
25
- </html>
3
+ <head>
4
+ <title>Calculate | CoCreateJS</title>
5
+ <!-- CoCreate Favicon -->
6
+ <link
7
+ rel="icon"
8
+ type="image/png"
9
+ sizes="32x32"
10
+ href="../assets/favicon.ico" />
11
+
12
+ <script>
13
+ // Prevent ServiceWorker InvalidStateError in sandboxed preview environments
14
+ // by safely hiding the serviceWorker API from feature detection before external scripts load.
15
+ try {
16
+ Object.defineProperty(navigator, 'serviceWorker', {
17
+ value: undefined,
18
+ configurable: true
19
+ });
20
+ } catch (e) {}
21
+ </script>
22
+
23
+ <style>
24
+ body { font-family: sans-serif; padding: 20px; max-width: 800px; margin: auto; }
25
+ .section { background: #f9f9f9; padding: 15px; margin-bottom: 20px; border-radius: 8px; border: 1px solid #ddd; }
26
+ input { margin-bottom: 10px; padding: 5px; }
27
+ label { display: inline-block; width: 150px; font-weight: bold; }
28
+ </style>
29
+ </head>
30
+ <body>
31
+ <h1>Safe Parser Calculate Examples</h1>
32
+
33
+ <div class="section">
34
+ <h3>Original DOM Selectors</h3>
35
+ <label>Self Value:</label>
36
+ <input value="12" calculate="$value * 100" /><br>
37
+
38
+ <label>Input 1 (id1):</label>
39
+ <input class="class1" key="total" id="id1" value="12" /><br>
40
+
41
+ <label>Input 2 (id2):</label>
42
+ <input class="class1" key="total" id="id2" value="13" /><br>
43
+
44
+ <label>Sum id1 + id2:</label>
45
+ <input id="te" calculate="($document #id1) + ($document #id2)" /><br>
46
+
47
+ <label>Key Total + 1:</label>
48
+ <input calculate="($document [key='total']) + 1" />
49
+
50
+ <h3 calculate="($document [key='total']) + ($document .class1)">sum</h3>
51
+ </div>
52
+
53
+ <div class="section">
54
+ <h3>Math Functions & Constants</h3>
55
+ <label>Base Number:</label>
56
+ <input id="baseNum" value="25" /><br>
57
+
58
+ <!-- Using Math.sqrt() -->
59
+ <label>Square Root:</label>
60
+ <input calculate="sqrt(($document #baseNum))" /><br>
61
+
62
+ <!-- Using Math.pow() -->
63
+ <label>Power (base^2):</label>
64
+ <input calculate="pow(($document #baseNum), 2)" /><br>
65
+
66
+ <!-- Using Math.max() with selectors -->
67
+ <label>Max Value:</label>
68
+ <input calculate="max(($document #id1), ($document #id2), 20)" /><br>
69
+
70
+ <!-- Using Constants -->
71
+ <label>Num * PI:</label>
72
+ <input calculate="round(($document #baseNum) * PI)" /><br>
73
+ </div>
74
+
75
+ <div class="section">
76
+ <h3>Ternary Conditionals (? :)</h3>
77
+ <label>User Age:</label>
78
+ <input id="age" value="20" /><br>
79
+
80
+ <!-- Basic Ternary String Output -->
81
+ <label>Status:</label>
82
+ <input calculate="($document #age) >= 18 ? 'Adult' : 'Minor'" readonly style="background: #eee;" /><br>
83
+
84
+ <!-- Nested Ternary for Tiers -->
85
+ <label>Score:</label>
86
+ <input id="score" value="85" /><br>
87
+
88
+ <label>Grade:</label>
89
+ <input calculate="($document #score) >= 90 ? 'A' : ($document #score) >= 80 ? 'B' : 'C'" readonly style="background: #eee;" /><br>
90
+
91
+ <!-- Math inside Ternary condition -->
92
+ <label>Combined Goal:</label>
93
+ <input calculate="(($document #id1) + ($document #id2)) > 20 ? 'Goal Reached!' : 'Keep Going...'" readonly style="background: #eee; width: 200px;" />
94
+ </div>
95
+
96
+ <!--<script src="../dist/CoCreate-calculate.js"></script>-->
97
+ <script src="https://geneomes.com/dist/CoCreate.js"></script>
98
+ </body>
99
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/calculate",
3
- "version": "1.16.5",
3
+ "version": "1.17.0",
4
4
  "description": "A handy vanilla JavaScript calculator, concatenate multiple elements containing integers & execute calculates. Can be used for creating invoices,making payments & any kind of complex calculate. Easily configured using HTML5 attributes and/or JavaScript API.",
5
5
  "keywords": [
6
6
  "calculate",
package/src/index.js CHANGED
@@ -1,60 +1,60 @@
1
1
  import observer from "@cocreate/observer"; // Module for observing DOM mutations.
2
- import { queryElements } from "@cocreate/utils"; // Utility for querying DOM elements.
2
+ import { queryElements, safeParse } from "@cocreate/utils"; // Utility for querying DOM elements and safe parsing.
3
3
  import "@cocreate/element-prototype"; // Include custom element prototype extensions.
4
4
 
5
5
  // Initializes the calculation elements within the document.
6
6
  function init() {
7
- // Select all elements in the document with a "calculate" attribute.
8
- let calculateElements = document.querySelectorAll("[calculate]");
9
- // Initialize each of the selected elements.
10
- initElements(calculateElements);
7
+ // Select all elements in the document with a "calculate" attribute.
8
+ let calculateElements = document.querySelectorAll("[calculate]");
9
+ // Initialize each of the selected elements.
10
+ initElements(calculateElements);
11
11
  }
12
12
 
13
13
  // Initialize multiple elements by invoking initElement for each.
14
14
  function initElements(elements) {
15
- // Iterate through the collection of elements and initialize each one.
16
- for (let el of elements) initElement(el);
15
+ // Iterate through the collection of elements and initialize each one.
16
+ for (let el of elements) initElement(el);
17
17
  }
18
18
 
19
19
  // Asynchronously initializes an individual element with setup for calculations.
20
20
  async function initElement(element) {
21
- // Fetch the calculate string from the element's attribute.
22
- let calculate = element.getAttribute("calculate");
23
- // Return early if the calculate string contains placeholders or template syntax.
24
- if (calculate.includes("{{") || calculate.includes("{[")) return;
25
-
26
- // Extract selectors from the calculate attribute value.
27
- let selectors = getSelectors(element.attributes["calculate"].value);
28
-
29
- // Iterate through each selector and set up elements impacted by them.
30
- for (let i = 0; i < selectors.length; i++) {
31
- // Find input elements based on the selector criteria.
32
- let inputs = queryElements({
33
- element,
34
- selector: selectors[i],
35
- type: "selector"
36
- });
37
-
38
- // Set up events for each found input element.
39
- for (let input of inputs) {
40
- initEvent(element, input);
41
- }
42
-
43
- // Initialize an observer to monitor newly added nodes that match the selector.
44
- observer.init({
45
- name: "calculateSelectorInit",
46
- types: ["addedNodes"],
47
- selector: selectors[i],
48
- // Callback function to run when nodes matching the selector are added.
49
- callback(mutation) {
50
- // Initialize events for the new element and update calculation.
51
- initEvent(element, mutation.target);
52
- setCalcationValue(element);
53
- }
54
- });
55
- }
56
- // Set initial calculation value when an element is being initialized.
57
- setCalcationValue(element);
21
+ // Fetch the calculate string from the element's attribute.
22
+ let calculate = element.getAttribute("calculate");
23
+ // Return early if the calculate string contains placeholders or template syntax.
24
+ if (calculate.includes("{{") || calculate.includes("{[")) return;
25
+
26
+ // Extract selectors from the calculate attribute value.
27
+ let selectors = getSelectors(element.attributes["calculate"].value);
28
+
29
+ // Iterate through each selector and set up elements impacted by them.
30
+ for (let i = 0; i < selectors.length; i++) {
31
+ // Find input elements based on the selector criteria.
32
+ let inputs = queryElements({
33
+ element,
34
+ selector: selectors[i],
35
+ type: "selector"
36
+ });
37
+
38
+ // Set up events for each found input element.
39
+ for (let input of inputs) {
40
+ initEvent(element, input);
41
+ }
42
+
43
+ // Initialize an observer to monitor newly added nodes that match the selector.
44
+ observer.init({
45
+ name: "calculateSelectorInit",
46
+ types: ["addedNodes"],
47
+ selector: selectors[i],
48
+ // Callback function to run when nodes matching the selector are added.
49
+ callback(mutation) {
50
+ // Initialize events for the new element and update calculation.
51
+ initEvent(element, mutation.target);
52
+ setCalcationValue(element);
53
+ }
54
+ });
55
+ }
56
+ // Set initial calculation value when an element is being initialized.
57
+ setCalcationValue(element);
58
58
  }
59
59
 
60
60
  /**
@@ -66,32 +66,32 @@ async function initElement(element) {
66
66
  * @returns {string[]} An array of unique matching selector strings found.
67
67
  */
68
68
  function getSelectors(string) {
69
- if (!string) {
70
- return []; // Return an empty array if input is null, undefined, or an empty string
71
- }
72
-
73
- // Regex provided by user: Finds parentheses, allows optional space,
74
- // captures from '$' + keyword + word boundary + rest until ')'
75
- const selectorRegex =
76
- /\(\s*(\$(?:selector|closest|parent|next|previous|document|frame|top)\b[^)]*)\)/g;
77
-
78
- const uniqueMatches = new Set();
79
- let match;
80
-
81
- // Use regex.exec() in a loop to find all matches
82
- while ((match = selectorRegex.exec(string)) !== null) {
83
- // match[1] contains the captured group (e.g., "$selector .button")
84
- // Add the trimmed match to the Set. Duplicates are automatically ignored.
85
- uniqueMatches.add(match[1].trim());
86
-
87
- // Handle potential edge case with zero-length matches to prevent infinite loops
88
- // Although less likely with this specific regex, it's good practice
89
- if (match.index === selectorRegex.lastIndex) {
90
- selectorRegex.lastIndex++;
91
- }
92
- }
93
-
94
- return Array.from(uniqueMatches);
69
+ if (!string) {
70
+ return []; // Return an empty array if input is null, undefined, or an empty string
71
+ }
72
+
73
+ // Regex provided by user: Finds parentheses, allows optional space,
74
+ // captures from '$' + keyword + word boundary + rest until ')'
75
+ const selectorRegex =
76
+ /\(\s*(\$(?:selector|closest|parent|next|previous|document|frame|top)\b[^)]*)\)/g;
77
+
78
+ const uniqueMatches = new Set();
79
+ let match;
80
+
81
+ // Use regex.exec() in a loop to find all matches
82
+ while ((match = selectorRegex.exec(string)) !== null) {
83
+ // match[1] contains the captured group (e.g., "$selector .button")
84
+ // Add the trimmed match to the Set. Duplicates are automatically ignored.
85
+ uniqueMatches.add(match[1].trim());
86
+
87
+ // Handle potential edge case with zero-length matches to prevent infinite loops
88
+ // Although less likely with this specific regex, it's good practice
89
+ if (match.index === selectorRegex.lastIndex) {
90
+ selectorRegex.lastIndex++;
91
+ }
92
+ }
93
+
94
+ return Array.from(uniqueMatches);
95
95
  }
96
96
 
97
97
  // Map: Key = InputElement, Value = Array of Elements to update
@@ -106,871 +106,111 @@ const initializedInputs = new Map();
106
106
  * @param {HTMLInputElement} input The input element that triggers the update.
107
107
  */
108
108
  function initEvent(element, input) {
109
- const calculteElements = initializedInputs.get(input);
110
- if (calculteElements) {
111
- calculteElements.add(element);
112
- return;
113
- }
114
-
115
- initializedInputs.set(input, new Set([element]));
116
-
117
- input.addEventListener("input", function () {
118
- const elementsToUpdate = initializedInputs.get(input);
119
-
120
- if (elementsToUpdate) {
121
- for (const element of elementsToUpdate) {
122
- setCalcationValue(element);
123
- }
124
- }
125
- });
109
+ const calculteElements = initializedInputs.get(input);
110
+ if (calculteElements) {
111
+ calculteElements.add(element);
112
+ return;
113
+ }
114
+
115
+ initializedInputs.set(input, new Set([element]));
116
+
117
+ input.addEventListener("input", function () {
118
+ const elementsToUpdate = initializedInputs.get(input);
119
+
120
+ if (elementsToUpdate) {
121
+ for (const element of elementsToUpdate) {
122
+ setCalcationValue(element);
123
+ }
124
+ }
125
+ });
126
126
  }
127
127
 
128
128
  // Asynchronously set the calculated value for the given element.
129
129
  async function setCalcationValue(element) {
130
- // Get the expression or formula from the element's "calculate" attribute.
131
- let calString = await getValues(element);
132
- // Evaluate the formula and set the calculated value back to the element.
133
- element.setValue(calculate(calString));
130
+ // Get the expression or formula from the element's "calculate" attribute.
131
+ let calString = await getValues(element);
132
+ // Evaluate the formula and set the calculated value back to the element.
133
+ element.setValue(calculate(calString));
134
134
  }
135
135
 
136
136
  // Asynchronously retrieve values necessary for computing the calculation attribute of an element.
137
137
  async function getValues(element) {
138
- // Get the expression that needs to be evaluated from the "calculate" attribute.
139
- let calculate = element.getAttribute("calculate");
140
-
141
- // Parse the expression to extract any selectors which values need to contribute to calculation.
142
- let selectors = getSelectors(element.attributes["calculate"].value);
143
-
144
- // For each selector, retrieve and calculate the respective value.
145
- for (let i = 0; i < selectors.length; i++) {
146
- let value = 0; // Default value in case no input is found for the selector.
147
-
148
- // Query DOM elements based on selector.
149
- let inputs = queryElements({
150
- element,
151
- selector: selectors[i],
152
- type: "selector"
153
- });
154
-
155
- // Iterate through inputs/elements matched by the selector.
156
- for (let input of inputs) {
157
- // Initialize event listeners on inputs so that changes can update the calculation.
158
- initEvent(element, input);
159
- let val = null;
160
-
161
- // Attempt to get the value from the input element, if it can provide it.
162
- if (input.getValue) {
163
- val = Number(await input.getValue());
164
- }
165
-
166
- // Only accumulate valid numeric values.
167
- if (!Number.isNaN(val)) {
168
- value += val;
169
- } else {
170
- console.warn(
171
- `Invalid value for selector "${selectors[i]}". Defaulting to 0.`
172
- );
173
- }
174
- }
175
-
176
- // Replace the placeholder in the calculation expression with the accumulated value.
177
- calculate = calculate.replaceAll(`(${selectors[i]})`, value);
178
- }
179
- return calculate; // Return the resolved calculation expression.
138
+ // Get the expression that needs to be evaluated from the "calculate" attribute.
139
+ let calculateAttr = element.getAttribute("calculate");
140
+
141
+ // Parse the expression to extract any selectors which values need to contribute to calculation.
142
+ let selectors = getSelectors(element.attributes["calculate"].value);
143
+
144
+ // For each selector, retrieve and calculate the respective value.
145
+ for (let i = 0; i < selectors.length; i++) {
146
+ let value = 0; // Default value in case no input is found for the selector.
147
+
148
+ // Query DOM elements based on selector.
149
+ let inputs = queryElements({
150
+ element,
151
+ selector: selectors[i],
152
+ type: "selector"
153
+ });
154
+
155
+ // Iterate through inputs/elements matched by the selector.
156
+ for (let input of inputs) {
157
+ // Initialize event listeners on inputs so that changes can update the calculation.
158
+ initEvent(element, input);
159
+ let val = null;
160
+
161
+ // Attempt to get the value from the input element, if it can provide it.
162
+ if (input.getValue) {
163
+ val = Number(await input.getValue());
164
+ }
165
+
166
+ // Only accumulate valid numeric values.
167
+ if (!Number.isNaN(val)) {
168
+ value += val;
169
+ } else {
170
+ console.warn(
171
+ `Invalid value for selector "${selectors[i]}". Defaulting to 0.`
172
+ );
173
+ }
174
+ }
175
+
176
+ // Replace the placeholder in the calculation expression with the accumulated value.
177
+ calculateAttr = calculateAttr.replaceAll(`(${selectors[i]})`, value);
178
+ }
179
+ return calculateAttr; // Return the resolved calculation expression.
180
180
  }
181
181
 
182
- // Defines mathematical constants available in expressions.
183
- const constants = { PI: Math.PI, E: Math.E };
184
-
185
- // Defines allowed mathematical functions and maps them to their respective JavaScript Math counterparts.
186
- const functions = {
187
- abs: Math.abs, // Absolute value
188
- ceil: Math.ceil, // Ceiling function
189
- floor: Math.floor, // Floor function
190
- round: Math.round, // Round to nearest integer
191
- max: Math.max, // Maximum value (assumes arity 2 in RPN)
192
- min: Math.min, // Minimum value (assumes arity 2 in RPN)
193
- pow: Math.pow, // Exponentiation
194
- sqrt: Math.sqrt, // Square root
195
- log: Math.log, // Natural logarithm
196
- sin: Math.sin, // Sine function
197
- cos: Math.cos, // Cosine function
198
- tan: Math.tan // Tangent function
199
- };
200
-
201
182
  /**
202
- * Tokenizer for Core Mathematical Expressions.
203
- * Converts a mathematical expression string (without ternary operators) into an array of tokens.
204
- * Each token is an object with 'type' and 'value'.
205
- * Supported types: 'literal', 'identifier', 'operator', 'function', 'open_paren', 'close_paren', 'comma', 'unknown'.
183
+ * Main entry point for evaluating an expression string securely.
184
+ * Passes the string off to the 0-dependency Recursive Descent Parser.
206
185
  *
207
- * @param {string} expression - The mathematical expression string to tokenize.
208
- * @returns {Array<object>} An array of token objects.
186
+ * @param {string | any} expression - The expression string to evaluate.
187
+ * @param {object} context - Optional JSON context for dot notation variables.
188
+ * @returns {any} The evaluated result, or null if an error occurs.
209
189
  */
210
- function tokenizeCore(expression) {
211
- const tokens = [];
212
- // Regular expression to capture recognized patterns:
213
- // 1: Numbers (integer or decimal)
214
- // 2: Identifiers (variable names, function names, constants like PI) - starting with letter or _, followed by letters, numbers, or _
215
- // 3: Multi-character comparison operators (>=, <=, ==, !=)
216
- // 4: Single-character operators, parentheses, comma, or whitespace that might be part of an operator later (like '<' in '<=')
217
- // 5: Whitespace sequences
218
- const regex =
219
- /(\d+(?:\.\d+)?)|([a-zA-Z_][a-zA-Z0-9_]*)|(>=|<=|==|!=)|([\+\-\*\/%^ \(\),<>])|(\s+)/g;
220
- let match;
221
- let lastToken = null; // Keep track of the previous token to help identify unary minus
222
- let expectedIndex = 0; // Track the expected start index of the next token
223
-
224
- // Iterate through all matches found by the regex in the expression string
225
- while ((match = regex.exec(expression)) !== null) {
226
- /* ... */ // Assume original complex logic might be here, focusing on the provided snippet
227
-
228
- // Check for unrecognized character sequences between valid tokens
229
- if (match.index !== expectedIndex) {
230
- const gap = expression.substring(expectedIndex, match.index);
231
- // Ignore gaps that are only whitespace
232
- if (gap.trim() !== "") {
233
- // Issue a warning for unrecognized characters, but attempt to continue tokenizing
234
- console.warn(
235
- `Invalid character sequence found near index ${expectedIndex}: '${gap}'`
236
- );
237
- // Note: Consider adding an 'error' or 'unknown_sequence' token type if needed for stricter parsing downstream
238
- }
239
- }
240
-
241
- let tokenStr = match[0]; // The matched string
242
- let token; // The token object to be created
243
-
244
- // Group 5: Whitespace
245
- if (match[5]) {
246
- // Ignore whitespace; simply advance the expected index
247
- expectedIndex = regex.lastIndex;
248
- continue; // Move to the next match
249
- }
250
-
251
- // Group 1: Literal (Number)
252
- if (match[1]) {
253
- token = { type: "literal", value: parseFloat(tokenStr) };
254
- }
255
- // Group 2: Identifier (Constant or Function Name)
256
- else if (match[2]) {
257
- if (tokenStr in constants) {
258
- // If it's a known constant, treat it as a literal value
259
- token = { type: "literal", value: constants[tokenStr] };
260
- } else if (tokenStr in functions) {
261
- // If it's a known function name
262
- token = { type: "function", value: tokenStr };
263
- } else {
264
- // If it's not a known constant or function
265
- console.warn(`Unknown identifier: ${tokenStr}`);
266
- // Create an 'unknown' token type. This allows the process to continue,
267
- // but downstream functions (like Shunting-Yard or evaluator) should handle or ignore it.
268
- token = { type: "unknown", value: tokenStr };
269
- }
270
- }
271
- // Group 3: Comparison Operators (>=, <=, ==, !=)
272
- else if (match[3]) {
273
- token = {
274
- type: "operator",
275
- value: tokenStr,
276
- precedence: 1, // Lower precedence than arithmetic operators
277
- associativity: "left"
278
- };
279
- }
280
- // Group 4: Other Operators/Punctuation (+, -, *, /, %, ^, (, ), ,, <, >)
281
- else if (match[4]) {
282
- tokenStr = tokenStr.trim(); // Remove surrounding whitespace if captured by the regex group
283
- // This check should ideally not be needed if regex correctly excludes pure whitespace via group 5, but acts as a safeguard.
284
- if (!tokenStr) {
285
- expectedIndex = regex.lastIndex;
286
- continue;
287
- }
288
-
289
- // Distinguish between unary minus and binary subtraction
290
- if (tokenStr === "-") {
291
- const isUnary =
292
- lastToken === null || // Beginning of expression
293
- ["operator", "open_paren", "comma"].includes(
294
- lastToken?.type
295
- ); // Following an operator, open parenthesis, or comma
296
-
297
- token = isUnary
298
- ? {
299
- // Unary minus
300
- type: "operator",
301
- value: "unary-", // Special value to distinguish from binary minus
302
- precedence: 4, // Higher precedence than multiplication/division
303
- associativity: "right"
304
- }
305
- : {
306
- // Binary minus (subtraction)
307
- type: "operator",
308
- value: "-",
309
- precedence: 2, // Same precedence as addition
310
- associativity: "left"
311
- };
312
- } else if (tokenStr === "+") {
313
- // Note: Unary plus is often ignored or handled implicitly, but could be tokenized similarly if needed.
314
- token = {
315
- type: "operator",
316
- value: "+",
317
- precedence: 2,
318
- associativity: "left"
319
- };
320
- } else if (tokenStr === "*" || tokenStr === "/") {
321
- token = {
322
- type: "operator",
323
- value: tokenStr,
324
- precedence: 3,
325
- associativity: "left"
326
- };
327
- } else if (tokenStr === "%") {
328
- // Modulo operator
329
- token = {
330
- type: "operator",
331
- value: tokenStr,
332
- precedence: 3,
333
- associativity: "left"
334
- };
335
- } else if (tokenStr === "^") {
336
- // Exponentiation operator
337
- token = {
338
- type: "operator",
339
- value: "^",
340
- precedence: 5,
341
- associativity: "right"
342
- }; // Highest precedence, right-associative
343
- } else if (tokenStr === ">" || tokenStr === "<") {
344
- // Simple comparison operators
345
- token = {
346
- type: "operator",
347
- value: tokenStr,
348
- precedence: 1,
349
- associativity: "left"
350
- }; // Same low precedence as other comparisons
351
- } else if (tokenStr === "(") {
352
- token = { type: "open_paren", value: "(" };
353
- } else if (tokenStr === ")") {
354
- token = { type: "close_paren", value: ")" };
355
- } else if (tokenStr === ",") {
356
- // Comma, typically used as function argument separator
357
- token = { type: "comma", value: "," };
358
- } else {
359
- // If the character is captured by group 4 but isn't handled above
360
- console.warn(
361
- `Unhandled punctuation/operator token: '${tokenStr}'`
362
- );
363
- // Mark as unknown
364
- token = { type: "unknown", value: tokenStr };
365
- }
366
- } else {
367
- // This block should theoretically not be reached if the regex covers all cases correctly.
368
- // It acts as a fallback error indicator.
369
- console.warn(
370
- `Tokenizer internal regex error: No group matched near '${expression.substring(
371
- expectedIndex
372
- )}'`
373
- );
374
- // Optionally create an 'error' token or skip
375
- }
376
-
377
- // If a valid (or unknown) token was created, add it to the list
378
- if (token) {
379
- tokens.push(token);
380
- lastToken = token; // Update lastToken for the next iteration's unary minus check
381
- }
382
-
383
- // Advance the expected starting position for the next token search
384
- expectedIndex = regex.lastIndex;
385
- }
386
-
387
- // After the loop, check if the entire string was consumed by the tokenizer
388
- if (expectedIndex < expression.length) {
389
- const trailing = expression.substring(expectedIndex).trim();
390
- // If there are non-whitespace characters remaining, they were not tokenized
391
- if (trailing) {
392
- console.warn(`Invalid trailing characters ignored: '${trailing}'`);
393
- }
394
- }
395
-
396
- return tokens;
397
- }
398
-
399
- /**
400
- * Converts an infix token stream (from tokenizeCore) to a Reverse Polish Notation (RPN) queue.
401
- * Implements the Shunting-yard algorithm. Does not handle ternary operators directly.
402
- * Handles operator precedence and associativity, functions, and parentheses.
403
- *
404
- * @param {Array<object>} tokens - The array of token objects from tokenizeCore.
405
- * @returns {Array<object>} An array of token objects arranged in RPN order.
406
- */
407
- function shuntingYardCore(tokens) {
408
- const outputQueue = []; // Stores the RPN output
409
- const operatorStack = []; // Temporary stack for operators, functions, and parentheses
410
-
411
- // Helper function to view the top element of a stack without removing it
412
- const peek = (stack) => (stack.length > 0 ? stack[stack.length - 1] : null);
413
-
414
- // Process each token from the input array
415
- for (const token of tokens) {
416
- // If the token is invalid or marked as unknown by the tokenizer, skip it.
417
- if (!token || token.type === "unknown") {
418
- console.warn(
419
- `Shunting-Yard skipping unknown token: ${token?.value}`
420
- );
421
- continue;
422
- }
423
-
424
- /* ... */ // Assume original SY logic structure might be here
425
-
426
- // Handle token based on its type
427
- switch (token.type) {
428
- case "literal":
429
- // Literals (numbers) are immediately added to the output queue.
430
- outputQueue.push(token);
431
- break;
432
-
433
- case "function":
434
- // Functions are pushed onto the operator stack.
435
- operatorStack.push(token);
436
- break;
437
-
438
- case "comma":
439
- // Commas indicate separation of arguments in a function call.
440
- // Pop operators from the stack to the output until an opening parenthesis is found.
441
- while (peek(operatorStack)?.type !== "open_paren") {
442
- const topOp = peek(operatorStack);
443
- // If the stack becomes empty before finding '(', it implies mismatched parentheses or comma.
444
- if (topOp === null) {
445
- console.warn(
446
- "Mismatched comma or parentheses detected during comma handling."
447
- );
448
- // Break to prevent infinite loop in error case. Consider throwing an error for stricter handling.
449
- break;
450
- }
451
- outputQueue.push(operatorStack.pop());
452
- }
453
- // The '(' remains on the stack to mark the start of the arguments.
454
- break;
455
-
456
- case "operator":
457
- // Handle operators based on precedence and associativity.
458
- const currentOp = token;
459
- let topOp = peek(operatorStack);
460
- // While there's an operator on the stack with higher or equal precedence (considering associativity)...
461
- while (
462
- topOp?.type === "operator" &&
463
- ((currentOp.associativity === "left" &&
464
- currentOp.precedence <= topOp.precedence) ||
465
- (currentOp.associativity === "right" &&
466
- currentOp.precedence < topOp.precedence))
467
- ) {
468
- // Pop the operator from the stack to the output queue.
469
- outputQueue.push(operatorStack.pop());
470
- topOp = peek(operatorStack); // Check the new top operator
471
- }
472
- // Push the current operator onto the stack.
473
- operatorStack.push(currentOp);
474
- break;
475
-
476
- case "open_paren":
477
- // Opening parentheses are always pushed onto the operator stack.
478
- operatorStack.push(token);
479
- break;
480
-
481
- case "close_paren":
482
- // Closing parenthesis: process operators until the matching opening parenthesis.
483
- let foundOpenParen = false;
484
- while (peek(operatorStack)?.type !== "open_paren") {
485
- const opToPop = operatorStack.pop();
486
- // If the stack runs out before finding '(', parentheses are mismatched.
487
- if (!opToPop) {
488
- console.warn(
489
- "Mismatched parentheses: Closing parenthesis found without matching open parenthesis."
490
- );
491
- // Break to prevent potential infinite loop if stack is empty.
492
- break;
493
- }
494
- outputQueue.push(opToPop);
495
- }
496
-
497
- // If an opening parenthesis was found, pop it from the stack (it's not added to output).
498
- if (peek(operatorStack)?.type === "open_paren") {
499
- operatorStack.pop();
500
- foundOpenParen = true;
501
- } // Mismatch case already warned inside the loop
502
-
503
- // If the token preceding the parenthesis pair was a function name, pop it to the output.
504
- // This places the function after its arguments in RPN.
505
- if (peek(operatorStack)?.type === "function") {
506
- outputQueue.push(operatorStack.pop());
507
- }
508
- break;
509
-
510
- default:
511
- // Should not happen if tokenizer provides known types, but acts as a safeguard.
512
- console.warn(
513
- `Unknown token type encountered in Shunting-Yard: ${token.type}`
514
- );
515
- }
516
- }
517
-
518
- // After processing all tokens, pop any remaining operators/functions from the stack to the output queue.
519
- while (peek(operatorStack) !== null) {
520
- const op = operatorStack.pop();
521
- // If an opening parenthesis is found here, it means parentheses were mismatched.
522
- if (op.type === "open_paren") {
523
- console.warn(
524
- "Mismatched parentheses: Open parenthesis remaining on stack at the end."
525
- );
526
- // Continue processing other operators, but the RPN is likely invalid.
527
- } else {
528
- outputQueue.push(op);
529
- }
530
- }
531
-
532
- return outputQueue; // Return the final RPN token queue.
533
- }
534
-
535
- /**
536
- * Evaluates a Reverse Polish Notation (RPN) token queue generated by shuntingYardCore.
537
- * Performs the actual calculations based on operators and function calls.
538
- * Handles basic error conditions like stack underflow, division by zero, and unknown tokens.
539
- * Returns the numerical result or null if evaluation fails.
540
- *
541
- * @param {Array<object>} rpnQueue - The array of token objects in RPN order.
542
- * @returns {number | null} The calculated numerical result, or null if an error occurs.
543
- */
544
- function evaluateRPNCore(rpnQueue) {
545
- if (!rpnQueue || rpnQueue.length === 0) {
546
- return null;
547
- }
548
-
549
- const evaluationStack = []; // Stack used to hold operands during RPN evaluation.
550
-
551
- for (const token of rpnQueue) {
552
- if (token.type === "literal") {
553
- evaluationStack.push(token.value);
554
- } else if (token.type === "operator") {
555
- if (token.value === "unary-") {
556
- if (evaluationStack.length < 1) {
557
- console.warn(
558
- `Stack underflow error during unary '-' operation.`
559
- );
560
- return null;
561
- }
562
- // Pop the operand, negate it, and push the result back.
563
- evaluationStack.push(-evaluationStack.pop());
564
- }
565
- // Handle binary operators
566
- else {
567
- // Requires two operands on the stack.
568
- if (evaluationStack.length < 2) {
569
- console.warn(
570
- `Stack underflow error during binary '${token.value}' operation.`
571
- );
572
- return null;
573
- }
574
- // Pop the top two operands. Note: the second operand (b) is popped first.
575
- const b = evaluationStack.pop();
576
- const a = evaluationStack.pop();
577
- let result;
578
-
579
- // Perform the operation based on the operator value.
580
- switch (token.value) {
581
- case "+":
582
- result = a + b;
583
- break;
584
- case "-":
585
- result = a - b;
586
- break;
587
- case "*":
588
- result = a * b;
589
- break;
590
- case "/":
591
- // Check for division by zero.
592
- if (b === 0) {
593
- console.warn("Division by zero encountered.");
594
- return null;
595
- }
596
- result = a / b;
597
- break;
598
- case "%":
599
- // Check for modulo by zero (JavaScript's % operator returns NaN in this case).
600
- if (b === 0) {
601
- console.warn("Modulo by zero encountered.");
602
- return null; // Return null for consistency with division by zero.
603
- }
604
- result = a % b;
605
- break;
606
- case "^":
607
- result = Math.pow(a, b);
608
- break;
609
- // Comparison operators return 1 for true, 0 for false, consistent with C-like behavior.
610
- case ">":
611
- result = a > b ? 1 : 0;
612
- break;
613
- case "<":
614
- result = a < b ? 1 : 0;
615
- break;
616
- case ">=":
617
- result = a >= b ? 1 : 0;
618
- break;
619
- case "<=":
620
- result = a <= b ? 1 : 0;
621
- break;
622
- case "==":
623
- result = a === b ? 1 : 0;
624
- break; // Use strict equality
625
- case "!=":
626
- result = a !== b ? 1 : 0;
627
- break; // Use strict inequality
628
- default:
629
- console.warn(
630
- `Unknown operator encountered during evaluation: ${token.value}`
631
- );
632
- return null;
633
- }
634
- evaluationStack.push(result);
635
- }
636
- }
637
- // If the token is a function call...
638
- else if (token.type === "function") {
639
- // Look up the function implementation (assuming 'functions' is a globally accessible object/map).
640
- const func = functions[token.value];
641
- if (!func) {
642
- // If the function name is not found in the available functions.
643
- console.warn(
644
- `Unknown function encountered during evaluation: ${token.value}`
645
- );
646
- return null;
647
- }
648
-
649
- // Determine the expected number of arguments (arity) for the function.
650
- // Note: Relying solely on func.length can be unreliable for functions with default parameters or rest parameters.
651
- // This example uses a mix of func.length and hardcoded arity for common Math functions.
652
- // A more robust implementation might store arity explicitly alongside the function definition.
653
- let arity = func.length; // Default assumption based on function definition
654
- // Explicitly define arity for functions where .length might be ambiguous or for built-ins.
655
- // (Example adjustments - tailor these to the actual functions defined)
656
- if (["max", "min", "pow"].includes(token.value)) arity = 2;
657
- if (
658
- [
659
- "sqrt",
660
- "abs",
661
- "ceil",
662
- "floor",
663
- "round",
664
- "log",
665
- "sin",
666
- "cos",
667
- "tan"
668
- ].includes(token.value)
669
- )
670
- arity = 1;
671
- // Add more overrides as needed for your specific function set.
672
-
673
- // Check if there are enough operands on the stack for the function's arity.
674
- if (evaluationStack.length < arity) {
675
- console.warn(
676
- `Stack underflow for function '${token.value}'. Need ${arity} args, found ${evaluationStack.length}.`
677
- );
678
- return null;
679
- }
680
-
681
- // Pop the required number of arguments from the stack.
682
- const args = [];
683
- for (let i = 0; i < arity; i++) {
684
- args.push(evaluationStack.pop());
685
- }
686
-
687
- try {
688
- // Call the function with the arguments. Since they were popped in reverse,
689
- const functionResult = func(...args.reverse());
690
- evaluationStack.push(functionResult);
691
- } catch (funcError) {
692
- // Catch errors that might occur during the function's execution (e.g., Math.log(-1) -> NaN, invalid inputs).
693
- console.warn(
694
- `Error executing function '${token.value}': ${funcError.message}`
695
- );
696
- return null;
697
- }
698
- } else {
699
- // If a token type other than literal, operator, or function appears in the RPN queue.
700
- // This might indicate an error in the RPN generation (Shunting-Yard).
701
- console.warn(
702
- `Unknown RPN token type encountered: ${token?.type} (Value: ${token?.value})`
703
- );
704
- return null;
705
- }
706
- }
707
-
708
- // After processing all tokens, the evaluation stack should contain exactly one value: the final result.
709
- if (evaluationStack.length !== 1) {
710
- // If the stack size is not 1, it usually indicates an invalid expression or a bug in the RPN conversion/evaluation.
711
- console.warn(
712
- `Evaluation finished with invalid stack size: ${
713
- evaluationStack.length
714
- }. Contents: ${JSON.stringify(evaluationStack)}`
715
- );
716
- return null;
717
- }
718
-
719
- const finalResult = evaluationStack[0];
720
-
721
- // Validate the final result to ensure it's a usable number.
722
- // Allow 0 and 1 specifically, as they are valid results from boolean comparisons.
723
- if (finalResult === 0 || finalResult === 1) {
724
- return finalResult;
725
- }
726
- // Check if the result is a finite number (not NaN, Infinity, or -Infinity).
727
- if (typeof finalResult !== "number" || !Number.isFinite(finalResult)) {
728
- console.warn(
729
- `Final evaluation result is not a valid finite number: ${finalResult}`
730
- );
731
- return null;
732
- }
733
-
734
- return finalResult;
735
- }
736
-
737
- /**
738
- * Parses a string expression to find the components of a *top-level* ternary expression.
739
- * Looks for the first '?' and its corresponding ':' at the same parenthesis nesting level.
740
- * Returns an object with { condition, trueExpr, falseExpr } if found, otherwise null.
741
- * Respects parentheses to avoid splitting nested ternaries incorrectly.
742
- *
743
- * @param {string} expression - The expression string to parse.
744
- * @returns {{condition: string, trueExpr: string, falseExpr: string} | null} Object with parts or null.
745
- */
746
- function parseTernary(expression) {
747
- let parenLevel = 0; // Tracks nesting level of parentheses
748
- let qIndex = -1; // Index of the top-level '?'
749
-
750
- // First pass: Find the first '?' at parenthesis level 0.
751
- for (let i = 0; i < expression.length; i++) {
752
- const char = expression[i];
753
- if (char === "(") {
754
- parenLevel++;
755
- } else if (char === ")") {
756
- parenLevel--;
757
- } else if (char === "?" && parenLevel === 0) {
758
- // Found the '?' at the top level
759
- qIndex = i;
760
- break; // Stop searching once the first top-level '?' is found
761
- }
762
-
763
- // Error check: If parenLevel goes below 0, parentheses are mismatched.
764
- if (parenLevel < 0) {
765
- console.warn(
766
- `Mismatched parentheses detected (too many ')') in ternary structure near index ${i}.`
767
- );
768
- return null; // Indicate parsing failure due to invalid structure
769
- }
770
- }
771
-
772
- // If no top-level '?' was found, it's not a simple ternary structure at this level.
773
- if (qIndex === -1) {
774
- return null;
775
- }
776
-
777
- // Second pass: Find the corresponding ':' at level 0, starting *after* the '?'.
778
- parenLevel = 0; // Reset parenthesis level counter for the colon search
779
- let cIndex = -1; // Index of the top-level ':'
780
- for (let i = qIndex + 1; i < expression.length; i++) {
781
- const char = expression[i];
782
- if (char === "(") {
783
- parenLevel++;
784
- } else if (char === ")") {
785
- parenLevel--;
786
- } else if (char === ":" && parenLevel === 0) {
787
- // Found the matching ':' at the top level
788
- cIndex = i;
789
- break; // Stop searching
790
- }
791
- // Error check during colon search
792
- if (parenLevel < 0) {
793
- console.warn(
794
- `Mismatched parentheses detected (too many ')') after '?' in ternary structure near index ${i}.`
795
- );
796
- return null; // Indicate parsing failure
797
- }
798
- }
799
-
800
- // If no matching top-level ':' was found after the '?', the structure is invalid.
801
- if (cIndex === -1) {
802
- console.warn(
803
- `Invalid ternary structure: No matching top-level ':' found for '?' at index ${qIndex}.`
804
- );
805
- return null;
806
- }
807
-
808
- // Extract the three parts of the ternary expression.
809
- const condition = expression.substring(0, qIndex).trim();
810
- const trueExpr = expression.substring(qIndex + 1, cIndex).trim();
811
- const falseExpr = expression.substring(cIndex + 1).trim();
812
-
813
- // Validate that none of the parts are empty after trimming.
814
- if (!condition || !trueExpr || !falseExpr) {
815
- console.warn(
816
- `Invalid ternary structure: empty part detected in "${expression}". Condition: "${condition}", True: "${trueExpr}", False: "${falseExpr}".`
817
- );
818
- return null;
819
- }
820
-
821
- return { condition, trueExpr, falseExpr };
822
- }
823
-
824
- /**
825
- * Main entry point for evaluating a mathematical expression string.
826
- * Handles nested ternary operators (`? :`) recursively with short-circuiting.
827
- * For non-ternary expressions or sub-expressions, it uses the core engine:
828
- * Tokenizer -> Shunting-Yard -> RPN Evaluator.
829
- * Provides graceful handling of common errors, returning null on failure.
830
- *
831
- * @param {string | any} expression - The expression string to evaluate. Non-string inputs are converted.
832
- * @returns {number | null} The final calculated result, or null if evaluation fails or the expression is invalid.
833
- */
834
- function calculate(expression) {
835
- // Store the original input, converting to string if necessary, for logging context.
836
- const originalExpr =
837
- typeof expression === "string" ? expression : String(expression);
838
-
839
- try {
840
- // Ensure we are working with a trimmed string.
841
- let currentExpr =
842
- typeof expression === "string" ? expression.trim() : "";
843
-
844
- // Handle empty or whitespace-only expressions immediately.
845
- if (!currentExpr) {
846
- // Warning is optional here, depends if empty input is expected or an error.
847
- // console.warn("Expression is empty or evaluates to empty string.");
848
- return null; // Return null for empty expression.
849
- }
850
-
851
- /* --- Optional Step: Remove Fully Wrapping Parentheses ---
852
- * This simplifies parsing by removing redundant outer parentheses, e.g., "((1 + 2))" becomes "1 + 2".
853
- * It iteratively unwraps as long as the outermost characters are '(' and ')'
854
- * and they correctly balance across the entire contained expression.
855
- */
856
- let unwrapped = false; // Flag to track if any unwrapping occurred (mainly for debugging)
857
- while (currentExpr.startsWith("(") && currentExpr.endsWith(")")) {
858
- let balance = 0;
859
- let canUnwrap = true; // Assume it can be unwrapped unless proven otherwise
860
-
861
- // Handle edge case like "()" which cannot be unwrapped to an empty string meaningfully here.
862
- if (currentExpr.length <= 2) {
863
- canUnwrap = false;
864
- break;
865
- }
866
-
867
- // Check if the parentheses truly wrap the *entire* internal expression.
868
- for (let i = 0; i < currentExpr.length; i++) {
869
- if (currentExpr[i] === "(") balance++;
870
- else if (currentExpr[i] === ")") balance--;
871
-
872
- // If balance returns to 0 *before* the very last character,
873
- // it means the parentheses don't wrap the whole thing, e.g., "(1) + (2)".
874
- if (balance === 0 && i < currentExpr.length - 1) {
875
- canUnwrap = false;
876
- break;
877
- }
878
- // If balance goes negative at any point, parentheses are mismatched.
879
- if (balance < 0) {
880
- canUnwrap = false; // Should ideally be caught later, but good safeguard.
881
- break;
882
- }
883
- }
884
-
885
- // The final balance must also be 0 for the wrapping to be valid.
886
- if (balance !== 0) {
887
- canUnwrap = false;
888
- }
889
-
890
- // If the checks pass, perform the unwrap.
891
- if (canUnwrap) {
892
- currentExpr = currentExpr
893
- .substring(1, currentExpr.length - 1)
894
- .trim();
895
- unwrapped = true;
896
- } else {
897
- // If cannot unwrap this layer, stop the unwrapping process.
898
- break;
899
- }
900
- }
901
- /* --- End Parenthesis Unwrapping --- */
902
-
903
- // 1. Attempt to parse the current (potentially unwrapped) expression as a top-level ternary.
904
- const ternaryParts = parseTernary(currentExpr);
905
-
906
- // 2. If it successfully parsed as a ternary structure...
907
- if (ternaryParts) {
908
- // 2a. Recursively evaluate the condition part first.
909
- const condResult = calculate(ternaryParts.condition);
910
-
911
- // Handle cases where the condition itself fails to evaluate.
912
- if (condResult === null) {
913
- // Log a warning indicating the condition evaluation failed.
914
- console.warn(
915
- `Failed to evaluate ternary condition: "${ternaryParts.condition}" in context: "${originalExpr}". Defaulting to false branch.`
916
- );
917
- // Proceed as if the condition is false for robustness, evaluating the 'falseExpr'.
918
- // Alternatively, could return null here to propagate the failure.
919
- return calculate(ternaryParts.falseExpr);
920
- }
921
-
922
- // 2b. Short-circuiting: Evaluate *only* the required branch based on the condition result.
923
- // The core evaluator returns 1 for true comparisons, 0 for false.
924
- // Any non-zero number is treated as "truthy" here.
925
- if (condResult) {
926
- // Checks for truthiness (non-zero result)
927
- return calculate(ternaryParts.trueExpr); // Evaluate the true branch recursively.
928
- } else {
929
- return calculate(ternaryParts.falseExpr); // Evaluate the false branch recursively.
930
- }
931
- }
932
- // 3. If it's not a top-level ternary (or parseTernary returned null due to errors)...
933
- else {
934
- // Evaluate the expression using the standard core math engine pipeline.
935
- const tokens = tokenizeCore(currentExpr);
936
- const rpnQueue = shuntingYardCore(tokens);
937
- // evaluateRPNCore handles internal errors (like division by zero, unknown tokens) and returns null on failure.
938
- const result = evaluateRPNCore(rpnQueue);
939
- return result; // Return the result (which could be a number or null).
940
- }
941
- } catch (error) {
942
- // Catch any unexpected runtime errors during the calculation process.
943
- const contextExpr =
944
- originalExpr.length > 50
945
- ? originalExpr.substring(0, 47) + "..." // Truncate long expressions for logging
946
- : originalExpr;
947
- console.warn(
948
- `Unexpected calculation error: ${error.message} (Expression context: "${contextExpr}")`,
949
- error
950
- );
951
- return null;
952
- }
190
+ function calculate(expression, context = {}) {
191
+ // Offload parsing to the safe utility function
192
+ return safeParse(expression, context);
953
193
  }
954
194
 
955
195
  observer.init([
956
- {
957
- name: "CoCreateCalculateChangeValue",
958
- types: ["attributes"],
959
- attributeFilter: ["calculate"],
960
- callback(mutation) {
961
- setCalcationValue(mutation.target);
962
- }
963
- },
964
- {
965
- name: "CoCreateCalculateInit",
966
- types: ["addedNodes"],
967
- selector: "[calculate]",
968
- callback(mutation) {
969
- initElement(mutation.target);
970
- }
971
- }
196
+ {
197
+ name: "CoCreateCalculateChangeValue",
198
+ types: ["attributes"],
199
+ attributeFilter: ["calculate"],
200
+ callback(mutation) {
201
+ setCalcationValue(mutation.target);
202
+ }
203
+ },
204
+ {
205
+ name: "CoCreateCalculateInit",
206
+ types: ["addedNodes"],
207
+ selector: "[calculate]",
208
+ callback(mutation) {
209
+ initElement(mutation.target);
210
+ }
211
+ }
972
212
  ]);
973
213
 
974
- init(); //
214
+ init();
975
215
 
976
- export default { initElements, initElement, calculate };
216
+ export default { initElements, initElement, calculate };