mikel 0.28.0 → 0.30.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/README.md CHANGED
@@ -433,10 +433,10 @@ const result = m(template, data, options);
433
433
  console.log(result); // Output: "Hello, World!"
434
434
  ```
435
435
 
436
- Custom helper functions receive a single object as argument, containing the following keys:
436
+ Custom helper functions receive a single `params` object as argument, containing the following fields:
437
437
 
438
438
  - `args`: an array containing the variables with the helper is called in the template.
439
- - `opt`: an object containing the keyword arguments provided to the helper.
439
+ - `options`: an object containing the keyword arguments provided to the helper.
440
440
  - `data`: the current data where the helper has been executed.
441
441
  - `variables`: an object containing the runtime variables available in the current context (e.g., `@root`, `@index`, etc.).
442
442
  - `fn`: a function that executes the template provided in the helper block and returns a string with the evaluated template in the provided context.
@@ -562,10 +562,10 @@ Mikel allows users to define custom functions that can be used within templates
562
562
 
563
563
  Functions should be provided in the `options.functions` field of the options object when rendering a template. Each function is defined by a name and a corresponding function that performs the desired operation.
564
564
 
565
- Functions will receive a single object as argument, containing the following keys:
565
+ Functions will receive a single `params` object as argument, containing the following keys:
566
566
 
567
567
  - `args`: an array containing the variables with the function is called in the template.
568
- - `opt`: an object containing the keyword arguments provided to the function.
568
+ - `options`: an object containing the keyword arguments provided to the function.
569
569
  - `data`: the current data object where the function has been executed.
570
570
  - `variables`: an object containing the runtime variables available in the current context (e.g., `@root`, `@index`, etc.).
571
571
 
@@ -592,7 +592,7 @@ console.log(result); // --> "My name is: John Doe"
592
592
 
593
593
  #### Expand function arguments using the spread operator
594
594
 
595
- > This feature was addedin `v0.20.0`.
595
+ > This feature was added in `v0.20.0`.
596
596
 
597
597
  You can use the spread operator `...` to expand the arguments of a function. This allows you to pass an array of values as individual arguments to the function, or to pass an object as keyword arguments.
598
598
 
@@ -632,7 +632,7 @@ const data = {
632
632
  const options = {
633
633
  functions: {
634
634
  fullName: params => {
635
- return `${params.opt.firstName} ${params.opt.lastName}`;
635
+ return `${params.options.firstName} ${params.options.lastName}`;
636
636
  }
637
637
  },
638
638
  };
@@ -641,6 +641,66 @@ const result = m("Users: {{=fullName ...user1}} and {{=fullName ...user2}}", dat
641
641
  console.log(result); // --> "Users: John Doe and Alice Smith"
642
642
  ```
643
643
 
644
+ Of course, Jose — here’s a version of the **Subexpressions** documentation written to perfectly match the tone, structure, and formatting conventions of the current README.
645
+ It follows the same patterns: short intro, version note, examples, concise explanations, no extra fluff.
646
+
647
+ ### Subexpressions
648
+
649
+ > Added in `v0.30.0`.
650
+
651
+ Subexpressions allow you to evaluate a function call inside another function call. They are written using parentheses, and can be used anywhere a normal function argument is allowed. Example:
652
+
653
+ ```hbs
654
+ {{=sum (sum 3 4) 3}}
655
+ ```
656
+
657
+ In this example, the inner expression is evaluated first:
658
+
659
+ - `(sum 3 4)` → `7`
660
+ - `sum 7 3` → `10`
661
+
662
+ Result:
663
+
664
+ ```
665
+ 10
666
+ ```
667
+
668
+ #### Nested subexpressions
669
+
670
+ Subexpressions can be nested to any depth:
671
+
672
+ ```hbs
673
+ {{=sum (sum 1 (sum 2 3)) 4}}
674
+ ```
675
+
676
+ #### Using strings inside subexpressions
677
+
678
+ Strings behave the same way inside subexpressions, including quoted strings with spaces:
679
+
680
+ ```hbs
681
+ {{=concat "Hello " (upper name)}}
682
+ ```
683
+
684
+ If `name = "world"`:
685
+
686
+ ```
687
+ Hello WORLD
688
+ ```
689
+
690
+ #### Variables inside subexpresspressions
691
+
692
+ You can reference variables or paths normally:
693
+
694
+ ```hbs
695
+ {{=sum (sum price tax) shipping}}
696
+ ```
697
+
698
+ #### Limitations
699
+
700
+ - Subexpressions are currently supported **only for functions** (`{{=...}}`).
701
+ - Subexpressions inside helper arguments are not yet supported.
702
+ - Parentheses must be balanced; malformed expressions will throw an error.
703
+
644
704
 
645
705
  ## API
646
706
 
package/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export type MikelHelper = (params: {
2
2
  args: any[];
3
- opt: Record<string, any>;
3
+ opt?: Record<string, any>;
4
+ options: Record<string, any>;
4
5
  tokens: string[];
5
6
  data: Record<string, any>;
6
7
  variables: Record<string, any>;
@@ -14,7 +15,8 @@ export type MikelPartial = {
14
15
 
15
16
  export type MikelFunction = (params: {
16
17
  args: any[];
17
- opt: Record<string, any>;
18
+ opt?: Record<string, any>;
19
+ options: Record<string,any>;
18
20
  data: Record<string, any>;
19
21
  variables: Record<string, any>;
20
22
  }) => string | void;
package/index.js CHANGED
@@ -26,29 +26,80 @@ const untokenize = (ts = [], s = "{{", e = "}}") => {
26
26
  return ts.length > 0 ? ts.reduce((p, t, i) => p + (i % 2 === 0 ? e : s) + t) : "";
27
27
  };
28
28
 
29
+ // @description tokenize args
30
+ const tokenizeArgs = (str = "", tokens = [], strings = []) => {
31
+ let current = "", depth = 0;
32
+ // 1. replace strings
33
+ str = str.replace(/"([^"\\]|\\.)*"/g, (match) => {
34
+ const id = `__STR${strings.length}__`;
35
+ strings.push(match);
36
+ return id;
37
+ });
38
+ // 2. tokenize arguments
39
+ for (let i = 0; i < str.length; i++) {
40
+ const c = str[i];
41
+ if (c === "(") {
42
+ depth++;
43
+ current = current + c;
44
+ } else if (c === ")") {
45
+ depth--;
46
+ current = current + c;
47
+ } else if (c === " " && depth === 0) {
48
+ if (current.trim()) {
49
+ tokens.push(current.trim());
50
+ }
51
+ current = "";
52
+ } else {
53
+ current = current + c;
54
+ }
55
+ }
56
+ // 3. add current token
57
+ if (current.trim()) {
58
+ tokens.push(current.trim());
59
+ }
60
+ // 4. replace strings back and return parsed tokens
61
+ return tokens.map(token => {
62
+ return token.replace(/__STR(\d+)__/g, (_, i) => strings[i]);
63
+ });
64
+ };
65
+
29
66
  // @description parse string arguments
30
- const parseArgs = (str = "", data = {}, vars = {}, argv = [], opt = {}) => {
31
- const [t, ...args] = str.trim().match(/(?:[^\s"]+|"[^"]*")+/g);
67
+ const parseArgs = (str = "", data = {}, vars = {}, fns = {}, argv = [], opt = {}) => {
68
+ const [t, ...args] = tokenizeArgs(str.trim());
32
69
  args.forEach(argStr => {
33
70
  if (argStr.includes("=") && !argStr.startsWith(`"`)) {
34
71
  const [k, v] = argStr.split("=");
35
- opt[k] = parse(v, data, vars);
72
+ opt[k] = parse(v, data, vars, fns);
36
73
  }
37
74
  else if (argStr.startsWith("...")) {
38
- const value = parse(argStr.replace(/^\.{3}/, ""), data, vars);
75
+ const value = parse(argStr.replace(/^\.{3}/, ""), data, vars, fns);
39
76
  if (!!value && typeof value === "object") {
40
77
  Array.isArray(value) ? argv.push(...value) : Object.assign(opt, value);
41
78
  }
42
79
  }
43
80
  else {
44
- argv.push(parse(argStr, data, vars));
81
+ argv.push(parse(argStr, data, vars, fns));
45
82
  }
46
83
  });
47
84
  return [t, argv, opt];
48
85
  };
49
86
 
87
+ // @description evaluate an expression
88
+ const evaluateExpression = (str = "", data = {}, vars = {}, fns = {}) => {
89
+ const [ fnName, args, opt ] = parseArgs(str, data, vars, fns);
90
+ if (typeof fns[fnName] === "function") {
91
+ return fns[fnName]({args, opt, options: opt, data, variables: vars});
92
+ }
93
+ // if no function has been found with this name
94
+ // throw new Error(`Unknown function '${fnName}'`);
95
+ return "";
96
+ };
97
+
50
98
  // @description parse a string value to a native type
51
- const parse = (v, data = {}, vars = {}) => {
99
+ const parse = (v, data = {}, vars = {}, fns = {}) => {
100
+ if (v.startsWith("(") && v.endsWith(")")) {
101
+ return evaluateExpression(v.slice(1, -1).trim(), data, vars, fns);
102
+ }
52
103
  if ((v.startsWith(`"`) && v.endsWith(`"`)) || /^-?\d+\.?\d*$/.test(v) || v === "true" || v === "false" || v === "null") {
53
104
  return JSON.parse(v);
54
105
  }
@@ -75,9 +126,17 @@ const findClosingToken = (tokens, i, token) => {
75
126
  const defaultHelpers = {
76
127
  "each": p => {
77
128
  const items = typeof p.args[0] === "object" ? Object.entries(p.args[0] || {}) : [];
78
- const limit = Math.min(items.length - (p.opt.skip || 0), p.opt.limit || items.length);
79
- return items.slice(p.opt.skip || 0, (p.opt.skip || 0) + limit)
80
- .map((item, index) => p.fn(item[1], {index: index, key: item[0], value: item[1], first: index === 0, last: index === items.length - 1}))
129
+ const limit = Math.min(items.length - (p.options?.skip || 0), p.options?.limit || items.length);
130
+ return items.slice(p.options?.skip || 0, (p.options?.skip || 0) + limit)
131
+ .map((item, index) => {
132
+ return p.fn(item[1], {
133
+ index: index,
134
+ key: item[0],
135
+ value: item[1],
136
+ first: index === 0,
137
+ last: index === items.length - 1,
138
+ });
139
+ })
81
140
  .join("");
82
141
  },
83
142
  "if": p => !!p.args[0] ? p.fn(p.data) : "",
@@ -181,10 +240,7 @@ const create = (options = {}) => {
181
240
  }
182
241
  }
183
242
  else if (tokens[i].startsWith("=")) {
184
- const [t, args, opt] = parseArgs(tokens[i].slice(1), data, vars);
185
- if (typeof ctx.functions[t] === "function") {
186
- output.push(ctx.functions[t]({args, opt, data, variables: vars}) || "");
187
- }
243
+ output.push(evaluateExpression(tokens[i].slice(1), data, vars, ctx.functions) ?? "");
188
244
  }
189
245
  else if (tokens[i].startsWith("/")) {
190
246
  if (tokens[i].slice(1).trim() !== section) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mikel",
3
3
  "description": "Micro templating library with zero dependencies",
4
- "version": "0.28.0",
4
+ "version": "0.30.0",
5
5
  "type": "module",
6
6
  "author": {
7
7
  "name": "Josemi Juanes",