mikel-eval 0.21.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.
Files changed (3) hide show
  1. package/README.md +212 -0
  2. package/index.js +328 -0
  3. package/package.json +31 -0
package/README.md ADDED
@@ -0,0 +1,212 @@
1
+ # mikel-eval
2
+
3
+ ![npm version](https://badgen.net/npm/v/mikel-eval?labelColor=1d2734&color=21bf81)
4
+ ![license](https://badgen.net/github/license/jmjuanes/mikel?labelColor=1d2734&color=21bf81)
5
+
6
+ The `mikel-eval` plugin extends the [mikel](https://github.com/jmjuanes/mikel) templating engine with expression evaluation capabilities. It allows you to evaluate mathematical, string, boolean, and array expressions directly within your templates.
7
+
8
+ ## Installation
9
+
10
+ Install the plugin using **npm** or **yarn**:
11
+
12
+ ```bash
13
+ # Using npm
14
+ npm install mikel-eval
15
+
16
+ # Using yarn
17
+ yarn add mikel-eval
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ Import and register the plugin with Mikel:
23
+
24
+ ```javascript
25
+ import mikel from "mikel";
26
+ import mikelEval from "mikel-eval";
27
+
28
+ // 1. create a new mikel instance for the given template
29
+ const m = mikel.create(`{{x}} * 2 = {{=eval "x * 2"}}`);
30
+
31
+ // 2. register the plugin
32
+ m.use(mikelEval());
33
+
34
+ // 3. render the template with the provided data context
35
+ console.log(m({x: 5})); // --> "5 * 2 = 10"
36
+ ```
37
+
38
+ You can also use this plugin with the default instance of Mikel:
39
+
40
+ ```javascript
41
+ import mikel from "mikel";
42
+ import mikelEval from "mikel-eval";
43
+
44
+ // 1. get the options generated by mikelEval
45
+ const options = mikelEval();
46
+
47
+ // 2. render the template with the default instance of mikel
48
+ console.log(mikel(`{{=eval "1 + 2"}}`, {}, options)); // --> "3"
49
+ ```
50
+
51
+ ## Features
52
+
53
+ Supported expressions:
54
+
55
+ - **Math:** `+`, `-`, `*`, `/`, `%`, `^`, and parentheses.
56
+ - **Strings:** Concatenation with `+` and string functions.
57
+ - **Booleans:** logical operators (`&&`, `||`, `!`) and comparisons (`==`, `!=`, `<`, `>`, `<=`, `>=`).
58
+ - **Arrays:** Array functions like `len`, `indexOf`, `join`, `in`.
59
+ - **Functions:** Built-in functions such as `abs`, `sqrt`, `max`, `min`, `replace`, `toUpperCase`, etc.
60
+
61
+ ## Built-in Functions
62
+
63
+ The following functions are available by default in expressions evaluated by mikel-eval:
64
+
65
+ ### Common Functions
66
+
67
+ #### `len(x)`
68
+
69
+ Returns the length of a string, array, or the number of keys in an object. Examples: `len([1,2,3])` → `3`, `len("abc")` → `3`.
70
+
71
+ #### `in(x, item)`
72
+
73
+ Checks if `item` is present in array, string, or object values. Examples: `in([1,2,3], 2)` → `true`, `in("hello", "ll")` → `true`.
74
+
75
+ #### `type(x)`
76
+
77
+ Returns the JavaScript type of the provided argument as a string. Examples: `type(123)` → `"number"`, `type("abc")` → `"string"`.
78
+
79
+ #### `if(condition, trueValue, falseValue)`
80
+
81
+ Returns `trueValue` if `condition` is `true`, otherwise returns `falseValue`. Example: `if(1 < 2, "yes", "no")` → `"yes"`.
82
+
83
+ ### Array Functions
84
+
85
+ #### `indexOf(array, item)`
86
+
87
+ Returns the index of `item` in `array`, or `-1` if not found. Example: `indexOf([1,2,3], 2)` → `1`.
88
+
89
+ #### `join(array, separator)`
90
+
91
+ Joins array elements into a string, separated by `separator` (default: `","`). Example: `join([1,2,3], "-")` → `"1-2-3"`.
92
+
93
+ ### Object Functions
94
+
95
+ #### `valueOf(obj, key)`
96
+
97
+ Returns the value of `obj[key]`. Example: `valueOf(myObject, "b")`.
98
+
99
+ ### String Functions
100
+
101
+ #### `startsWith(str, prefix)`
102
+
103
+ Returns `true` if `str` starts with `prefix`. Example: `startsWith("hello", "he")` → `true`.
104
+
105
+ #### `endsWith(str, suffix)`
106
+
107
+ Returns `true` if `str` ends with `suffix`. Example: `endsWith("hello", "lo")` → `true`.
108
+
109
+ #### `replace(str, search, replacement)`
110
+
111
+ Replaces all occurrences of `search` in `str` with `replacement`. Example: `replace("foo bar", "bar", "baz")` → `"foo baz"`.
112
+
113
+ #### `toUpperCase(str)`
114
+
115
+ Converts `str` to uppercase. Example: `toUpperCase("abc")` → `"ABC"`.
116
+
117
+ #### `toLowerCase(str)`
118
+
119
+ Converts `str` to lowercase. Example: `toLowerCase("ABC")` → `"abc"`.
120
+
121
+ #### `trim(str)`
122
+
123
+ Removes whitespace from both ends of `str`. Example: `trim(" hello ")` → `"hello"`.
124
+
125
+ ### Mathematical Functions
126
+
127
+ #### `min(...args)`
128
+
129
+ Returns the smallest of the provided numbers. Example: `min(1, 2, 3)` → `1`.
130
+
131
+ #### `max(...args)`
132
+
133
+ Returns the largest of the provided numbers. Example: `max(1, 2, 3)` → `3`.
134
+
135
+ #### `abs(x)`
136
+
137
+ Returns the absolute value of `x`. Example: `abs(-5)` → `5`.
138
+
139
+ #### `round(x)`
140
+
141
+ Rounds `x` to the nearest integer. Example: `round(2.5)` → `3`.
142
+
143
+ #### `ceil(x)`
144
+
145
+ Rounds `x` up to the next largest integer. Example: `ceil(2.1)` → `3`.
146
+
147
+ #### `floor(x)`
148
+
149
+ Rounds `x` down to the next smallest integer. Example: `floor(2.9)` → `2`.
150
+
151
+ #### `sqrt(x)`
152
+
153
+ Returns the square root of `x`. Example: `sqrt(16)` → `4`.
154
+
155
+ #### `pow(x, y)`
156
+
157
+ Returns `x` raised to the power of `y`. Example: `pow(2, 3)` → `8`.
158
+
159
+ #### `random()`
160
+
161
+ Returns a random number between 0 (inclusive) and 1 (exclusive). Example: `random()` → `0.123456...`.
162
+
163
+ ## API
164
+
165
+ ### `=eval` function
166
+
167
+ Evaluates the given expression and prints the result.
168
+
169
+ ```javascript
170
+ m(`{{=eval "1 + 2"}}`, {}); // Output: "3"
171
+ ```
172
+
173
+ You can use variables from your data context:
174
+
175
+ ```javascript
176
+ m(`{{=eval "'Hello ' + name"}}`, { name: "World" }); // Output: "Hello World"
177
+ m(`{{=eval "x * y"}}`, { x: 2, y: 3 }); // Output: "6"
178
+ ```
179
+
180
+ ### `#when` helper
181
+
182
+ Renders the block only if the evaluated expression is `true`.
183
+
184
+ ```javascript
185
+ m(`{{#when "x > 1"}}x is greater than 1{{/when}}`, { x: 2 }); // Output: "x is greater than 1"
186
+ m(`{{#when "'foo' == 'bar'"}}This will not render{{/when}}`, {}); // Output: ""
187
+ ```
188
+
189
+ You use variables from your data context:
190
+
191
+ ```javascript
192
+ m(`{{#when "x > 1"}}x is greater than 1{{/when}}`, { x: 2 }); // Output: "x is greater than 1"
193
+ m(`{{#when "x < 1"}}x is less than 1{{/when}}`, { x: 2 }); // Output: ""
194
+ ```
195
+
196
+ ## Custom Functions
197
+
198
+ You can provide your own functions via the plugin options:
199
+
200
+ ```javascript
201
+ const options = mikelEval({
202
+ functions: {
203
+ double: x => x * 2,
204
+ },
205
+ }));
206
+
207
+ console.log(mikel(`{{=eval "double(5)"}}`, {}, options)); // Output: "10"
208
+ ```
209
+
210
+ ## License
211
+
212
+ Licensed under the [MIT License](../../LICENSE).
package/index.js ADDED
@@ -0,0 +1,328 @@
1
+ // @description comparision variables
2
+ const comparationCharacters = ["=", "!", "<", ">", "&", "|"];
3
+ const comparationFunctions = {
4
+ "===": (a, b) => a === b,
5
+ "!==": (a, b) => a !== b,
6
+ "==": (a, b) => a === b,
7
+ "!=": (a, b) => a !== b,
8
+ "<=": (a, b) => a <= b,
9
+ ">=": (a, b) => a >= b,
10
+ "<": (a, b) => a < b,
11
+ ">": (a, b) => a > b,
12
+ "&&": (a, b) => a && b,
13
+ "||": (a, b) => a || b,
14
+ };
15
+
16
+ // @description: get a nested object value
17
+ const getIn = (c, p) => {
18
+ return (p === "." ? c : p.split(".").reduce((x, k) => x?.[k], c));
19
+ };
20
+
21
+ // parse a next character
22
+ const nextChar = ctx => {
23
+ ctx.pos = ctx.pos + 1;
24
+ ctx.current = (ctx.pos < ctx.str.length) ? ctx.str.charAt(ctx.pos) : -1;
25
+ };
26
+
27
+ // check if the current character is the provided character
28
+ const checkChar = (ctx, ch) =>{
29
+ while (ctx.current === " ") {
30
+ nextChar(ctx);
31
+ }
32
+ // Check if the current character is the provided character
33
+ if (ctx.current === ch) {
34
+ nextChar(ctx);
35
+ return true;
36
+ }
37
+ return false;
38
+ };
39
+
40
+ // parse a expression: addition or substraaction
41
+ const parseExpression = ctx => {
42
+ let x = parseTerm(ctx);
43
+ for (;;) {
44
+ if (checkChar(ctx, "+")) {
45
+ x = x + parseTerm(ctx); // addition
46
+ }
47
+ else if (checkChar(ctx, "-")) {
48
+ x = x - parseTerm(ctx); // subtraction
49
+ }
50
+ else {
51
+ return x;
52
+ }
53
+ }
54
+ };
55
+
56
+ // parse a term: multiplication or division
57
+ const parseTerm = ctx => {
58
+ let x = parseComparison(ctx);
59
+ for (;;) {
60
+ if (checkChar(ctx, "*")) {
61
+ x = x * parseComparison(ctx);
62
+ }
63
+ else if (checkChar(ctx, "/")) {
64
+ x = x / parseComparison(ctx);
65
+ }
66
+ else if (checkChar(ctx, "%")) {
67
+ x = x % parseComparison(ctx);
68
+ }
69
+ else {
70
+ return x;
71
+ }
72
+ }
73
+ };
74
+
75
+ // parse a comparison
76
+ const parseComparison = ctx => {
77
+ let x = parseFactor(ctx);
78
+ for(;;) {
79
+ // save the current starting position
80
+ const startPos = ctx.pos;
81
+ if (comparationCharacters.indexOf(ctx.current) !== -1) {
82
+ while (comparationCharacters.indexOf(ctx.current) !== -1) {
83
+ nextChar(ctx);
84
+ }
85
+ // get the comparison operator
86
+ const comparator = ctx.str.substring(startPos, ctx.pos)
87
+ if (typeof comparationFunctions[comparator] !== "function") {
88
+ throw new Error(`Unknown operator '${comparator}'`);
89
+ }
90
+ x = comparationFunctions[comparator](x, parseFactor(ctx));
91
+ }
92
+ else {
93
+ return x;
94
+ }
95
+ }
96
+ };
97
+
98
+ // parse a list of items
99
+ const parseList = ctx => {
100
+ const items = [];
101
+ do {
102
+ items.push(parseExpression(ctx));
103
+ }
104
+ while(checkChar(ctx, ","));
105
+ return items;
106
+ };
107
+
108
+ // parse a factor
109
+ const parseFactor = ctx => {
110
+ if (checkChar(ctx, "!")) {
111
+ return !parseFactor(ctx);
112
+ }
113
+ if (checkChar(ctx, "+")) {
114
+ return parseFactor(ctx);
115
+ }
116
+ if (checkChar(ctx, "-")) {
117
+ return -parseFactor(ctx);
118
+ }
119
+ let x;
120
+ const startPos = ctx.pos;
121
+ // subexpression
122
+ if (checkChar(ctx, "(")) {
123
+ x = parseExpression(ctx);
124
+ checkChar(ctx, ")");
125
+ }
126
+ else if (checkChar(ctx, "[")) {
127
+ x = parseList(ctx);
128
+ checkChar(ctx, "]");
129
+ }
130
+ // string factor
131
+ else if (checkChar(ctx, "'")) {
132
+ x = "";
133
+ while(ctx.current !== "'") {
134
+ x = x + ctx.current;
135
+ nextChar(ctx);
136
+ }
137
+ checkChar(ctx, "'");
138
+ }
139
+ // digit
140
+ else if ((ctx.current >= "0" && ctx.current <= "9") || ctx.current == ".") {
141
+ while ((ctx.current >= "0" && ctx.current <= "9") || ctx.current == ".") {
142
+ nextChar(ctx);
143
+ }
144
+ x = parseFloat(ctx.str.substring(startPos, ctx.pos));
145
+ }
146
+ // values functions
147
+ else if (ctx.current !== -1 && ctx.current.match(/^[A-Za-z]$/) !== null) {
148
+ while (ctx.current !== -1 && ctx.current.match(/^[a-zA-Z\[\]'"0-9\.\_]$/) !== null) {
149
+ nextChar(ctx);
150
+ }
151
+ let name = ctx.str.substring(startPos, ctx.pos);
152
+ if (name === "null" || name === "true" || name === "false") {
153
+ x = JSON.parse(name); // parse null, true or false
154
+ }
155
+ // check if there is a function to apply
156
+ else if (typeof ctx.functions[name] === "function") {
157
+ if (!checkChar(ctx, "(")) {
158
+ throw new Error(`Unexpected character '${ctx.current}' after function name '${name}'`);
159
+ }
160
+ const args = parseList(ctx);
161
+ if (!checkChar(ctx, ")")) {
162
+ throw new Error(`Unexpected character '${ctx.current}' after function arguments`);
163
+ }
164
+ // execute the function and save the value
165
+ x = ctx.functions[name].apply(null, args);
166
+ }
167
+ else {
168
+ x = getIn(ctx.values, name);
169
+ }
170
+ }
171
+ else {
172
+ throw new Error(`Unexpected character '${ctx.current}' at position ${ctx.pos}`);
173
+ }
174
+ // exponential operator ?
175
+ if (checkChar(ctx, "^")) {
176
+ x = Math.pow(x, parseFactor(ctx));
177
+ }
178
+ return x;
179
+ };
180
+
181
+ // @description default functions
182
+ const defaultFunctions = {
183
+ // common functions
184
+ len: x => {
185
+ if (Array.isArray(x) || typeof x === "string") {
186
+ return x.length;
187
+ } else if (typeof x === "object" && x !== null) {
188
+ return Object.keys(x).length;
189
+ }
190
+ throw new Error(`len() cannot be applied to type ${typeof x}`);
191
+ },
192
+ in: (x, item) => {
193
+ if (Array.isArray(x) || typeof x === "string") {
194
+ return x.includes(item);
195
+ }
196
+ else if (typeof x === "object" && x !== null) {
197
+ return Object.values(x).includes(item);
198
+ }
199
+ throw new Error(`in() cannot be applied to type ${typeof x}`);
200
+ },
201
+ type: x => typeof x,
202
+ if: (condition, trueValue, falseValue) => {
203
+ if (typeof condition !== "boolean") {
204
+ throw new Error(`if() expects a boolean condition, got ${typeof condition}`);
205
+ }
206
+ return condition ? trueValue : falseValue;
207
+ },
208
+ // array functions
209
+ indexOf: (array, item) => {
210
+ if (!Array.isArray(array)) {
211
+ throw new Error(`indexOf() expects an array, got ${typeof array}`);
212
+ }
213
+ return array.indexOf(item);
214
+ },
215
+ join: (array, separator = ",") => {
216
+ if (!Array.isArray(array)) {
217
+ throw new Error(`join() expects an array, got ${typeof array}`);
218
+ }
219
+ return array.join(separator);
220
+ },
221
+ // object functions
222
+ valueOf: (obj, key) => {
223
+ if (typeof obj !== "object" || obj === null) {
224
+ throw new Error(`valueOf() expects an object, got ${typeof obj}`);
225
+ }
226
+ return obj[key];
227
+ },
228
+ // string functions
229
+ startsWith: (str, prefix) => {
230
+ if (typeof str !== "string") {
231
+ throw new Error(`startsWith() expects a string, got ${typeof str}`);
232
+ }
233
+ return str.startsWith(prefix);
234
+ },
235
+ endsWith: (str, suffix) => {
236
+ if (typeof str !== "string") {
237
+ throw new Error(`endsWith() expects a string, got ${typeof str}`);
238
+ }
239
+ return str.endsWith(suffix);
240
+ },
241
+ replace: (str, search, replacement) => {
242
+ if (typeof str !== "string") {
243
+ throw new Error(`replace() expects a string, got ${typeof str}`);
244
+ }
245
+ return str.replace(new RegExp(search, "g"), replacement || "");
246
+ },
247
+ toUpperCase: str => {
248
+ if (typeof str !== "string") {
249
+ throw new Error(`toUpperCase() expects a string, got ${typeof str}`);
250
+ }
251
+ return str.toUpperCase();
252
+ },
253
+ toLowerCase: str => {
254
+ if (typeof str !== "string") {
255
+ throw new Error(`toLowerCase() expects a string, got ${typeof str}`);
256
+ }
257
+ return str.toLowerCase();
258
+ },
259
+ trim: str => {
260
+ if (typeof str !== "string") {
261
+ throw new Error(`trim() expects a string, got ${typeof str}`);
262
+ }
263
+ return str.trim();
264
+ },
265
+ // mathematical functions
266
+ min: (...args) => Math.min(...args),
267
+ max: (...args) => Math.max(...args),
268
+ abs: x => Math.abs(x),
269
+ round: x => Math.round(x),
270
+ ceil: x => Math.ceil(x),
271
+ floor: x => Math.floor(x),
272
+ sqrt: x => Math.sqrt(x),
273
+ pow: (x, y) => Math.pow(x, y),
274
+ random: () => Math.random(),
275
+ };
276
+
277
+ // @description: evaluate a string expression
278
+ // @param str {string} the expression to evaluate
279
+ // @param options {object} the options to use
280
+ // @param options.values {object} context where the expression will be evaluated
281
+ // @param options.functions {object} functions to use in the expression
282
+ const evaluate = (str = "", options = {}) => {
283
+ const context = {
284
+ pos: 0,
285
+ current: str.charAt(0) || "",
286
+ str: str,
287
+ values: options.values || {},
288
+ functions: {
289
+ ...defaultFunctions,
290
+ ...(options.functions || {}),
291
+ },
292
+ };
293
+ const result = parseExpression(context);
294
+ if (context.pos < context.str.length) {
295
+ throw new Error(`Unexpected '${context.current}' at position ${context.pos}`);
296
+ }
297
+ return result;
298
+ };
299
+
300
+ // @description evaluate plugin
301
+ const evaluatePlugin = (options = {}) => {
302
+ return {
303
+ functions: {
304
+ eval: params => {
305
+ return evaluate(params.args[0], {
306
+ values: params.data,
307
+ functions: options.functions,
308
+ });
309
+ },
310
+ },
311
+ helpers: {
312
+ when: params => {
313
+ const condition = evaluate(params.args[0], {
314
+ values: params.data,
315
+ functions: options.functions,
316
+ });
317
+ return !!condition ? params.fn(params.data) : "";
318
+ },
319
+ },
320
+ };
321
+ };
322
+
323
+ // assign additional options for this plugin
324
+ evaluatePlugin.evaluate = evaluate;
325
+ evaluatePlugin.defaultFunctions = defaultFunctions;
326
+
327
+ // export the evaluate plugin
328
+ export default evaluatePlugin;
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "mikel-eval",
3
+ "description": "A mikel plugin for evaluating JavaScript expressions.",
4
+ "version": "0.21.0",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": {
8
+ "name": "Josemi Juanes",
9
+ "email": "hello@josemi.xyz"
10
+ },
11
+ "repository": "https://github.com/jmjuanes/mikel",
12
+ "bugs": "https://github.com/jmjuanes/mikel/issues",
13
+ "exports": {
14
+ ".": "./index.js",
15
+ "./index.js": "./index.js",
16
+ "./package.json": "./package.json"
17
+ },
18
+ "scripts": {
19
+ "test": "node ./test.js"
20
+ },
21
+ "files": [
22
+ "README.md",
23
+ "index.js"
24
+ ],
25
+ "keywords": [
26
+ "mikel",
27
+ "plugin",
28
+ "eval",
29
+ "evaluate"
30
+ ]
31
+ }