Skip to content

Commit a9f8c63

Browse files
authored
feat: allow to provide keyword arguments in helpers and functions (#23)
1 parent aaca5a2 commit a9f8c63

File tree

3 files changed

+63
-17
lines changed

3 files changed

+63
-17
lines changed

README.md

+17-9
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ console.log(m("{{#with autor}}{{name}} <{{email}}>{{/with}}", data)); // --> 'Bo
239239
### Custom Helpers
240240

241241
> Added in `v0.5.0`.
242+
> Breaking change introduced in `v0.12.0`.
242243
243244
Custom helpers should be provided as an object in the `options.helpers` field, where each key represents the name of the helper and the corresponding value is a function defining the helper's behavior.
244245

@@ -251,8 +252,8 @@ const data = {
251252
};
252253
const options = {
253254
helpers: {
254-
customHelper: value => {
255-
return `Hello, ${value}!`;
255+
customHelper: params => {
256+
return `Hello, ${params.args[0]}!`;
256257
},
257258
},
258259
};
@@ -261,8 +262,10 @@ const result = m(template, data, options);
261262
console.log(result); // Output: "Hello, World!"
262263
```
263264

264-
Custom helper functions receive multiple arguments, where the first N arguments are the variables with the helper is called in the template, and the last argument is an options object containing the following keys:
265+
Custom helper functions receive a single object as argument, containing the following keys:
265266

267+
- `args`: an array containing the variables with the helper is called in the template.
268+
- `opt`: an object containing the keyword arguments provided to the helper.
266269
- `context`: the current context (data) where the helper has been executed.
267270
- `fn`: a function that executes the template provided in the helper block and returns a string with the evaluated template in the provided context.
268271

@@ -278,8 +281,8 @@ const data = {
278281
};
279282
const options = {
280283
helpers: {
281-
customEach: (items, opt) => {
282-
return items.map((item, index) => opt.fn({ ...item, index: index})).join("");
284+
customEach: ({args, fn}) => {
285+
return args[0].map((item, index) => fn({ ...item, index: index})).join("");
283286
},
284287
},
285288
};
@@ -343,11 +346,18 @@ The `@last` variable allows to check if the current iteration using the `#each`
343346
### Functions
344347

345348
> Added in `v0.8.0`.
349+
> Breaking change introduced in `v0.12.0`.
346350
347351
Mikel allows users to define custom functions that can be used within templates to perform dynamic operations. Functions can be invoked in the template using the `=` character, followed by the function name and the variables to be provided to the function. Variables should be separated by spaces.
348352

349353
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.
350354

355+
Functions will receive a single object as argument, containing the following keys:
356+
357+
- `args`: an array containing the variables with the function is called in the template.
358+
- `opt`: an object containing the keyword arguments provided to the function.
359+
- `context`: the current context (data) where the function has been executed.
360+
351361
Example:
352362

353363
```javascript
@@ -359,8 +369,8 @@ const data = {
359369
};
360370
const options = {
361371
functions: {
362-
fullName: (firstName, lastName) => {
363-
return `${firstName} ${lastName}`;
372+
fullName: ({args}) => {
373+
return `${args[0]} ${args[1]}`;
364374
}
365375
},
366376
};
@@ -369,8 +379,6 @@ const result = m("My name is: {{=fullName user.firstName user.lastName}}", data,
369379
console.log(result); // --> "My name is: John Doe"
370380
```
371381

372-
In this example, the custom function `fullName` is defined to take two arguments, `firstName` and `lastName`, and return the full name. The template then uses this function to concatenate and render the full name.
373-
374382

375383
## API
376384

index.js

+17-7
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ const escape = s => s.toString().replace(/[&<>\"']/g, m => escapedChars[m]);
1111

1212
const get = (c, p) => (p === "." ? c : p.split(".").reduce((x, k) => x?.[k], c)) ?? "";
1313

14+
// @description parse string arguments
15+
const parseArgs = (argString = "", context = {}, vars = {}) => {
16+
const [t, ...args] = argString.trim().match(/(?:[^\s"]+|"[^"]*")+/g);
17+
const argv = args.filter(a => !a.includes("=")).map(a => parse(a, context, vars));
18+
const opt = Object.fromEntries(args.filter(a => a.includes("=")).map(a => {
19+
const [k, v] = a.split("=");
20+
return [k, parse(v, context, vars)];
21+
}));
22+
return [t, argv, opt];
23+
};
24+
25+
// @description parse a string value to a native type
1426
const parse = (v, context = {}, vars = {}) => {
1527
if ((v.startsWith(`"`) && v.endsWith(`"`)) || /^-?\d+\.?\d*$/.test(v) || v === "true" || v === "false" || v === "null") {
1628
return JSON.parse(v);
@@ -64,10 +76,11 @@ const create = (template = "", options = {}) => {
6476
output.push(get(context, tokens[i].slice(1).trim()));
6577
}
6678
else if (tokens[i].startsWith("#") && typeof helpers[tokens[i].slice(1).trim().split(" ")[0]] === "function") {
67-
const [t, ...args] = tokens[i].slice(1).trim().match(/(?:[^\s"]+|"[^"]*")+/g);
79+
const [t, args, opt] = parseArgs(tokens[i].slice(1), context, vars);
6880
const j = i + 1;
6981
output.push(helpers[t]({
70-
args: args.map(v => parse(v, context, vars)),
82+
args: args,
83+
opt: opt,
7184
context: context,
7285
fn: (blockContext = {}, blockVars = {}, blockOutput = []) => {
7386
i = compile(tokens, blockOutput, blockContext, {...vars, ...blockVars, root: vars.root}, j, t);
@@ -101,12 +114,9 @@ const create = (template = "", options = {}) => {
101114
}
102115
}
103116
else if (tokens[i].startsWith("=")) {
104-
const [t, ...args] = tokens[i].slice(1).trim().match(/(?:[^\s"]+|"[^"]*")+/g);
117+
const [t, args, opt] = parseArgs(tokens[i].slice(1), context, vars);
105118
if (typeof functions[t] === "function") {
106-
output.push(functions[t]({
107-
args: args.map(v => parse(v, context, vars)),
108-
context: context,
109-
}) || "");
119+
output.push(functions[t]({args, opt, context}) || "");
110120
}
111121
}
112122
else if (tokens[i].startsWith("/")) {

test.js

+29-1
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,15 @@ describe("templating", () => {
255255
assert.equal(m(`{{#customEqual value "no"}}Yes!!{{/customEqual}}`, {value: "yes"}, options), "");
256256
assert.equal(m(`{{#customEqual value false}}Yes!!{{/customEqual}}`, {value: "yes"}, options), "");
257257
});
258+
259+
it("should allow to provide keyword arguments", () => {
260+
const options = {
261+
helpers: {
262+
concat: params => params.args.join(params.opt.delimiter || " "),
263+
},
264+
};
265+
assert.equal(m(`{{#concat a b delimiter=","}}{{/concat}}`, {a: "hello", b: "world"}, options), "hello,world");
266+
});
258267
});
259268

260269
describe("{{@root}}", () => {
@@ -285,7 +294,7 @@ describe("templating", () => {
285294
const options = {
286295
functions: {
287296
toUpperCase: params => params.args[0].toUpperCase(),
288-
concat: params => params.args.join(" "),
297+
concat: params => params.args.join(params.opt.delimiter || " "),
289298
},
290299
};
291300

@@ -308,6 +317,25 @@ describe("templating", () => {
308317
it("should allow to execute funcions inside helpers blocks", () => {
309318
assert.equal(m(`{{#each names}}{{=toUpperCase .}}, {{/each}}`, {names: ["bob", "susan"]}, options), "BOB, SUSAN, ");
310319
});
320+
321+
it("should support keywods in function arguments", () => {
322+
assert.equal(m(`{{=concat a b delimiter=","}}`, {a: "Hello", b: "World"}, options), "Hello,World");
323+
});
324+
325+
it("should support context variables as keyword arguments", () => {
326+
const data = {
327+
name: "Bob",
328+
surname: "Doe",
329+
};
330+
const options = {
331+
functions: {
332+
sayWelcome: ({args, opt}) => {
333+
return `Welcome, ${[args[0], opt.surname || ""].filter(Boolean).join(" ")}`;
334+
},
335+
},
336+
};
337+
assert.equal(m("{{=sayWelcome name surname=surname}}", data, options), "Welcome, Bob Doe");
338+
});
311339
});
312340
});
313341

0 commit comments

Comments
 (0)