mikel 0.32.0 → 0.33.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 (4) hide show
  1. package/README.md +76 -17
  2. package/index.d.ts +13 -14
  3. package/index.js +142 -132
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -237,19 +237,6 @@ Partial metadata can be accessed using the `@partial` variable inside the partia
237
237
  - `@partial.attributes`: the custom data provided to the partial (if any).
238
238
  - `@partial.context`: the current rendering context.
239
239
 
240
- ### Inline partials
241
-
242
- > Added in `v0.28.0`.
243
-
244
- Inline partials allows you to define partials directly in your template. Use `>*` followed by the partial name to start the partial definition, and end the partial definition with a slash `/` followed by the partial name. For example, `{{>*foo}}` begins a partial definition called `foo`, and `{{/foo}}` ends it.
245
-
246
- Example:
247
-
248
- ```javascript
249
- const result = m(`{{>*foo}}Hello {{name}}!{{/foo}}{{>foo name="Bob"}}`, {});
250
- // Output: 'Hello Bob!'
251
- ```
252
-
253
240
  ### Helpers
254
241
 
255
242
  > Added in `v0.4.0`.
@@ -407,6 +394,78 @@ The `raw` helper allows to render the content of the block without evaluating it
407
394
  console.log(m("{{#raw}}Hello {{name}}!{{/raw}}", {name: "Bob"})); // --> 'Hello {{name}}!'
408
395
  ```
409
396
 
397
+ #### slot
398
+
399
+ > Added in `v0.33.0`.
400
+
401
+ The `slot` helper allows you to capture a block of template content and store it under a named key. Captured slots become available through the special `@slot` state variable.
402
+
403
+ ```javascript
404
+ const template = `
405
+ {{#slot "name"}}Bob{{/slot}}
406
+
407
+ Hello {{@slot.name}}!
408
+ `;
409
+
410
+ console.log(m(template, {})); // --> 'Hello Bob!'
411
+ ```
412
+
413
+ Slots are evaluated at render time, so they can contain variables, helpers, or any other template expressions. If the same slot name is defined more than once, **the last definition wins**.
414
+
415
+ #### macro
416
+
417
+ > Added in `v0.33.0`.
418
+
419
+ The `macro` helper allows you to register reusable chunks of content. Use `#macro` followed by the name of the reusable chunk.
420
+
421
+ Example:
422
+
423
+ ```javascript
424
+ const template = `
425
+ {{#macro "foo"}}
426
+ Hello {{name}}!
427
+ {{/macro}}
428
+
429
+ {{#call name="Bob"}}{{/call}}
430
+ `;
431
+
432
+ console.log(m(template, {})); // --> 'Hello Bob!'
433
+ ```
434
+
435
+ Macros can be executed using the `#call` helper.
436
+
437
+ #### call
438
+
439
+ > Added in `v0.33.0`.
440
+
441
+ The `call` helper allows you to execute registered macros. Key-value arguments will be passed as a data to the content inside the macro:
442
+
443
+ ```javascript
444
+ const template = `
445
+ {{#macro "sayHello"}}
446
+ Hello {{this.name}}!!
447
+ {{/macro}}
448
+
449
+ {{#call "sayHello" name="Bob"}}{{/call}}
450
+ `;
451
+
452
+ console.log(m(template, {})); // --> 'Hello Bob!!'
453
+ ```
454
+
455
+ Content inside the `#call` tag will be passed to the macro content in a `@content` state variable, similar as block partials.
456
+
457
+ ```javascript
458
+ const template = `
459
+ {{#macro "foo"}}
460
+ Hello {{@content}}!!
461
+ {{/macro}}
462
+
463
+ {{#call "foo"}}Bob{{/call}}
464
+ `;
465
+
466
+ console.log(m(template, {})); // --> 'Hello Bob!!'
467
+ ```
468
+
410
469
  ### Custom Helpers
411
470
 
412
471
  > Added in `v0.5.0`.
@@ -438,7 +497,7 @@ Custom helper functions receive a single `params` object as argument, containing
438
497
  - `args`: an array containing the variables with the helper is called in the template.
439
498
  - `options`: an object containing the keyword arguments provided to the helper.
440
499
  - `data`: the current data where the helper has been executed.
441
- - `variables`: an object containing the runtime variables available in the current context (e.g., `@root`, `@index`, etc.).
500
+ - `state`: an object containing the state variables available in the current context (e.g., `@root`, `@index`, etc.).
442
501
  - `fn`: a function that executes the template provided in the helper block and returns a string with the evaluated template in the provided context.
443
502
 
444
503
  The helper function must return a string, which will be injected into the result string. Example:
@@ -501,11 +560,11 @@ Inside any helper block, you can access metadata about the current invocation th
501
560
  - `@helper.options`: an object containing named (key-value) arguments.
502
561
  - `@helper.context`: the current rendering context.
503
562
 
504
- ### Runtime Variables
563
+ ### State Variables
505
564
 
506
565
  > Added in `v0.4.0`.
507
566
 
508
- Runtime Variables in Mikel provide convenient access to special values within your templates. These variables, denoted by the `@` symbol, allow users to interact with specific data contexts or values at runtime. Runtime variables are usually generated by helpers like `#each`.
567
+ State Variables in Mikel provide convenient access to special values within your templates. These variables, denoted by the `@` symbol, allow users to interact with specific data contexts or values at runtime. State variables are usually generated by helpers like `#each`.
509
568
 
510
569
  #### @root
511
570
 
@@ -567,7 +626,7 @@ Functions will receive a single `params` object as argument, containing the foll
567
626
  - `args`: an array containing the variables with the function is called in the template.
568
627
  - `options`: an object containing the keyword arguments provided to the function.
569
628
  - `data`: the current data object where the function has been executed.
570
- - `variables`: an object containing the runtime variables available in the current context (e.g., `@root`, `@index`, etc.).
629
+ - `state`: an object containing the state variables available in the current context (e.g., `@root`, `@index`, etc.).
571
630
 
572
631
  Example:
573
632
 
package/index.d.ts CHANGED
@@ -1,11 +1,15 @@
1
+ export type MikelHelperCallback = (
2
+ data?: Record<string, any>,
3
+ state?: Record<string, any>,
4
+ ) => string;
5
+
1
6
  export type MikelHelper = (params: {
2
7
  args: any[];
3
- opt?: Record<string, any>;
4
8
  options: Record<string, any>;
5
9
  tokens: string[];
6
10
  data: Record<string, any>;
7
- variables: Record<string, any>;
8
- fn: (blockData?: Record<string, any>, blockVars?: Record<string, any>, blockOutput?: string[]) => string;
11
+ state: Record<string, any>;
12
+ fn: MikelHelperCallback;
9
13
  }) => string;
10
14
 
11
15
  export type MikelPartial = {
@@ -15,29 +19,24 @@ export type MikelPartial = {
15
19
 
16
20
  export type MikelFunction = (params: {
17
21
  args: any[];
18
- opt?: Record<string, any>;
19
22
  options: Record<string,any>;
20
23
  data: Record<string, any>;
21
- variables: Record<string, any>;
24
+ state: Record<string, any>;
22
25
  }) => string | void;
23
26
 
24
- export type MikelContext = {
25
- helpers: Record<string, MikelHelper>;
26
- partials: Record<string, string | MikelPartial>;
27
- functions: Record<string, MikelFunction>;
28
- variables: Record<string, any>;
29
- };
30
-
31
27
  export type MikelOptions = {
32
28
  helpers?: Record<string, MikelHelper>;
33
29
  partials?: Record<string, string | MikelPartial>;
34
30
  functions?: Record<string, MikelFunction>;
35
- variables?: Record<string, any>;
31
+ };
32
+
33
+ export type MikelUseOptions = MikelOptions & {
34
+ initialState?: Record<string, any>;
36
35
  };
37
36
 
38
37
  export type Mikel = {
39
38
  (template: string, data?: any): string;
40
- use(options: Partial<MikelOptions> | ((ctx: MikelContext) => void)): Mikel;
39
+ use(options: Partial<MikelUseOptions>): void;
41
40
  addHelper(name: string, fn: MikelHelper): void;
42
41
  removeHelper(name: string): void;
43
42
  addFunction(name: string, fn: MikelFunction): void;
package/index.js CHANGED
@@ -64,31 +64,31 @@ const tokenizeArgs = (str = "", tokens = [], strings = []) => {
64
64
  };
65
65
 
66
66
  // @description parse string arguments
67
- const parseArgs = (str = "", data = {}, vars = {}, fns = {}, argv = [], opt = {}) => {
67
+ const parseArgs = (str = "", data = {}, state = {}, fns = {}, argv = [], opt = {}) => {
68
68
  const [t, ...args] = tokenizeArgs(str.trim());
69
69
  args.forEach(argStr => {
70
70
  if (argStr.includes("=") && !argStr.startsWith(`"`)) {
71
71
  const [k, v] = argStr.split("=");
72
- opt[k] = parse(v, data, vars, fns);
72
+ opt[k] = parse(v, data, state, fns);
73
73
  }
74
74
  else if (argStr.startsWith("...")) {
75
- const value = parse(argStr.replace(/^\.{3}/, ""), data, vars, fns);
75
+ const value = parse(argStr.replace(/^\.{3}/, ""), data, state, fns);
76
76
  if (!!value && typeof value === "object") {
77
77
  Array.isArray(value) ? argv.push(...value) : Object.assign(opt, value);
78
78
  }
79
79
  }
80
80
  else {
81
- argv.push(parse(argStr, data, vars, fns));
81
+ argv.push(parse(argStr, data, state, fns));
82
82
  }
83
83
  });
84
84
  return [t, argv, opt];
85
85
  };
86
86
 
87
87
  // @description evaluate an expression
88
- const evaluateExpression = (str = "", data = {}, vars = {}, fns = {}) => {
89
- const [ fnName, args, opt ] = parseArgs(str, data, vars, fns);
88
+ const evaluateExpression = (str = "", data = {}, state = {}, fns = {}) => {
89
+ const [fnName, args, options] = parseArgs(str, data, state, fns);
90
90
  if (typeof fns[fnName] === "function") {
91
- return fns[fnName]({args, opt, options: opt, data, variables: vars});
91
+ return fns[fnName]({ args, options, data, state });
92
92
  }
93
93
  // if no function has been found with this name
94
94
  // throw new Error(`Unknown function '${fnName}'`);
@@ -96,14 +96,14 @@ const evaluateExpression = (str = "", data = {}, vars = {}, fns = {}) => {
96
96
  };
97
97
 
98
98
  // @description parse a string value to a native type
99
- const parse = (v, data = {}, vars = {}, fns = {}) => {
99
+ const parse = (v, data = {}, state = {}, fns = {}) => {
100
100
  if (v.startsWith("(") && v.endsWith(")")) {
101
- return evaluateExpression(v.slice(1, -1).trim(), data, vars, fns);
101
+ return evaluateExpression(v.slice(1, -1).trim(), data, state, fns);
102
102
  }
103
103
  if ((v.startsWith(`"`) && v.endsWith(`"`)) || /^-?\d+\.?\d*$/.test(v) || v === "true" || v === "false" || v === "null") {
104
104
  return JSON.parse(v);
105
105
  }
106
- return (v || "").startsWith("@") ? get(vars, v.slice(1)) : get(data, v || ".");
106
+ return (v || "").startsWith("@") ? get(state, v.slice(1)) : get(data, v || ".");
107
107
  };
108
108
 
109
109
  // @description find the index of the closing token
@@ -122,6 +122,104 @@ const findClosingToken = (tokens, i, token) => {
122
122
  throw new Error(`Unmatched section end: {{${token}}}`);
123
123
  };
124
124
 
125
+ // @description internal method to compile the template
126
+ const compile = (ctx, tokens, output, data, state, index = 0, section = "") => {
127
+ let i = index;
128
+ while (i < tokens.length) {
129
+ if (i % 2 === 0) {
130
+ output.push(tokens[i]);
131
+ }
132
+ else if (tokens[i].startsWith("#") && typeof ctx.helpers[tokens[i].slice(1).trim().split(" ")[0]] === "function") {
133
+ const [t, args, opt] = parseArgs(tokens[i].slice(1), data, state);
134
+ const j = i + 1;
135
+ i = findClosingToken(tokens, j, t);
136
+ output.push(ctx.helpers[t]({
137
+ args: args,
138
+ options: opt,
139
+ tokens: tokens.slice(j, i),
140
+ data: data,
141
+ state: state,
142
+ fn: (blockData = {}, customBlockState = {}, blockOutput = []) => {
143
+ const blockState = {
144
+ ...state,
145
+ ...customBlockState,
146
+ helper: {
147
+ name: t,
148
+ options: opt || {},
149
+ args: args || [],
150
+ context: blockData,
151
+ },
152
+ parent: data,
153
+ root: state.root,
154
+ };
155
+ compile(ctx, tokens, blockOutput, blockData, blockState, j, t);
156
+ return blockOutput.join("");
157
+ },
158
+ }));
159
+ }
160
+ else if (tokens[i].startsWith("#") || tokens[i].startsWith("^")) {
161
+ const t = tokens[i].slice(1).trim();
162
+ const value = get(data, t);
163
+ const negate = tokens[i].startsWith("^");
164
+ if (!negate && value && Array.isArray(value)) {
165
+ const j = i + 1;
166
+ (value.length > 0 ? value : [""]).forEach(item => {
167
+ i = compile(ctx, tokens, value.length > 0 ? output : [], item, state, j, t);
168
+ });
169
+ }
170
+ else {
171
+ const includeOutput = (!negate && !!value) || (negate && !!!value);
172
+ i = compile(ctx, tokens, includeOutput ? output : [], data, state, i + 1, t);
173
+ }
174
+ }
175
+ else if (tokens[i].startsWith(">")) {
176
+ const [t, args, opt] = parseArgs(tokens[i].replace(/^>{1,2}/, ""), data, state);
177
+ const blockContent = []; // to store partial block content
178
+ if (tokens[i].startsWith(">>")) {
179
+ i = compile(ctx, tokens, blockContent, data, state, i + 1, t);
180
+ }
181
+ if (typeof ctx.partials[t] === "string" || typeof ctx.partials[t]?.body === "string") {
182
+ const partialBody = ctx.partials[t]?.body || ctx.partials[t];
183
+ const partialData = args.length > 0 ? args[0] : (Object.keys(opt).length > 0 ? opt : data);
184
+ const partialState = {
185
+ ...state,
186
+ content: blockContent.join(""),
187
+ partial: {
188
+ name: t,
189
+ attributes: ctx.partials[t]?.attributes || ctx.partials[t]?.data || {},
190
+ args: args || [],
191
+ options: opt || {},
192
+ context: partialData,
193
+ },
194
+ };
195
+ compile(ctx, tokenize(partialBody), output, partialData, partialState, 0, "");
196
+ }
197
+ }
198
+ else if (tokens[i].startsWith("=")) {
199
+ output.push(evaluateExpression(tokens[i].slice(1), data, state, ctx.functions) ?? "");
200
+ }
201
+ else if (tokens[i].startsWith("/")) {
202
+ if (tokens[i].slice(1).trim() !== section) {
203
+ throw new Error(`Unmatched section end: {{${tokens[i]}}}`);
204
+ }
205
+ break;
206
+ }
207
+ else {
208
+ const t = tokens[i].split("||").map(v => {
209
+ // check if the returned value should not be escaped
210
+ if (v.trim().startsWith("!")) {
211
+ return parse(v.trim().slice(1).trim(), data, state);
212
+ }
213
+ // escape the returned value
214
+ return escape(parse(v.trim(), data, state));
215
+ });
216
+ output.push(t.find(v => !!v) ?? "");
217
+ }
218
+ i = i + 1;
219
+ }
220
+ return i;
221
+ };
222
+
125
223
  // @description default helpers
126
224
  const defaultHelpers = {
127
225
  "each": p => {
@@ -146,140 +244,52 @@ const defaultHelpers = {
146
244
  "with": p => p.fn(p.args[0]),
147
245
  "escape": p => escape(p.fn(p.data)),
148
246
  "raw": p => untokenize(p.tokens),
247
+ "slot": params => {
248
+ if (typeof params.state.slot === "undefined") {
249
+ params.state.slot = {};
250
+ }
251
+ params.state.slot[params.args[0].trim()] = params.fn(params.data);
252
+ return "";
253
+ },
254
+ "macro": params => {
255
+ if (typeof params.state.macro === "undefined") {
256
+ params.state.macro = {};
257
+ }
258
+ params.state.macro[params.args[0].trim()] = untokenize(params.tokens).trim();
259
+ return "";
260
+ },
261
+ "call": params => {
262
+ const result = [];
263
+ const name = (params.args[0] || "").trim();
264
+ if (!!name && typeof params.state?.macro?.[name] === "string") {
265
+ compile(params.context, tokenize(params.state.macro[name]), result, params.options, {
266
+ ...params.state,
267
+ content: params.fn(params.data),
268
+ });
269
+ }
270
+ return result.join("");
271
+ },
149
272
  };
150
273
 
151
274
  // @description create a new instance of mikel
152
275
  const create = (options = {}) => {
153
- const ctx = {
276
+ const ctx = Object.freeze({
154
277
  helpers: Object.assign({}, defaultHelpers, options?.helpers || {}),
155
278
  partials: Object.assign({}, options?.partials || {}),
156
- functions: options?.functions || {},
157
- variables: {},
158
- };
159
- // internal method to compile the template
160
- const compile = (tokens, output, data, vars, index = 0, section = "") => {
161
- let i = index;
162
- while (i < tokens.length) {
163
- if (i % 2 === 0) {
164
- output.push(tokens[i]);
165
- }
166
- else if (tokens[i].startsWith("#") && typeof ctx.helpers[tokens[i].slice(1).trim().split(" ")[0]] === "function") {
167
- const [t, args, opt] = parseArgs(tokens[i].slice(1), data, vars);
168
- const j = i + 1;
169
- i = findClosingToken(tokens, j, t);
170
- output.push(ctx.helpers[t]({
171
- args: args,
172
- opt: opt, // deprecated
173
- options: opt,
174
- tokens: tokens.slice(j, i),
175
- data: data,
176
- variables: vars,
177
- fn: (blockData = {}, blockVars = {}, blockOutput = []) => {
178
- const helperVars = {
179
- ...vars,
180
- ...blockVars,
181
- helper: {
182
- name: t,
183
- options: opt || {},
184
- args: args || [],
185
- context: blockData,
186
- },
187
- parent: data,
188
- root: vars.root,
189
- };
190
- compile(tokens, blockOutput, blockData, helperVars, j, t);
191
- return blockOutput.join("");
192
- },
193
- }));
194
- }
195
- else if (tokens[i].startsWith("#") || tokens[i].startsWith("^")) {
196
- const t = tokens[i].slice(1).trim();
197
- const value = get(data, t);
198
- const negate = tokens[i].startsWith("^");
199
- if (!negate && value && Array.isArray(value)) {
200
- const j = i + 1;
201
- (value.length > 0 ? value : [""]).forEach(item => {
202
- i = compile(tokens, value.length > 0 ? output : [], item, vars, j, t);
203
- });
204
- }
205
- else {
206
- const includeOutput = (!negate && !!value) || (negate && !!!value);
207
- i = compile(tokens, includeOutput ? output : [], data, vars, i + 1, t);
208
- }
209
- }
210
- else if (tokens[i].startsWith(">*")) {
211
- const t = tokens[i].slice(2).trim(), partialTokens = tokens.slice(i + 1);
212
- const lastIndex = partialTokens.findIndex((token, j) => {
213
- return j % 2 !== 0 && token.trim().startsWith("/") && token.trim().endsWith(t);
214
- });
215
- if (typeof ctx.partials[t] === "undefined") {
216
- ctx.partials[t] = untokenize(partialTokens.slice(0, lastIndex));
217
- }
218
- i = i + lastIndex + 1;
219
- }
220
- else if (tokens[i].startsWith(">")) {
221
- const [t, args, opt] = parseArgs(tokens[i].replace(/^>{1,2}/, ""), data, vars);
222
- const blockContent = []; // to store partial block content
223
- if (tokens[i].startsWith(">>")) {
224
- i = compile(tokens, blockContent, data, vars, i + 1, t);
225
- }
226
- if (typeof ctx.partials[t] === "string" || typeof ctx.partials[t]?.body === "string") {
227
- const newData = args.length > 0 ? args[0] : (Object.keys(opt).length > 0 ? opt : data);
228
- const newVars = {
229
- ...vars,
230
- content: blockContent.join(""),
231
- partial: {
232
- name: t,
233
- attributes: ctx.partials[t]?.attributes || ctx.partials[t]?.data || {},
234
- args: args || [],
235
- options: opt || {},
236
- context: newData,
237
- },
238
- };
239
- compile(tokenize(ctx.partials[t]?.body || ctx.partials[t]), output, newData, newVars, 0, "");
240
- }
241
- }
242
- else if (tokens[i].startsWith("=")) {
243
- output.push(evaluateExpression(tokens[i].slice(1), data, vars, ctx.functions) ?? "");
244
- }
245
- else if (tokens[i].startsWith("/")) {
246
- if (tokens[i].slice(1).trim() !== section) {
247
- throw new Error(`Unmatched section end: {{${tokens[i]}}}`);
248
- }
249
- break;
250
- }
251
- else {
252
- const t = tokens[i].split("||").map(v => {
253
- // check if the returned value should not be escaped
254
- if (v.trim().startsWith("!")) {
255
- return parse(v.trim().slice(1).trim(), data, vars);
256
- }
257
- // escape the returned value
258
- return escape(parse(v.trim(), data, vars));
259
- });
260
- output.push(t.find(v => !!v) ?? "");
261
- }
262
- i = i + 1;
263
- }
264
- return i;
265
- };
279
+ functions: Object.assign({}, options?.functions || {}),
280
+ initialState: {}, // Object.assign({}, options?.initialState || {}),
281
+ });
266
282
  // entry method to compile the template with the provided data object
267
283
  const compileTemplate = (template, data = {}, output = []) => {
268
- compile(tokenize(template), output, data, {root: data, ...ctx.variables}, 0, "");
284
+ compile(ctx, tokenize(template), output, data, { ...ctx.initialState, root: data }, 0, "");
269
285
  return output.join("");
270
286
  };
271
287
  // assign api methods and return method to compile the template
272
288
  return Object.assign(compileTemplate, {
273
- use: newOptions => {
274
- if (typeof newOptions === "function") {
275
- newOptions(ctx);
276
- }
277
- else if (!!newOptions && typeof newOptions === "object") {
278
- ["helpers", "functions", "partials", "variables"].forEach(field => {
279
- Object.assign(ctx[field], newOptions?.[field] || {});
280
- });
281
- }
282
- return compileTemplate;
289
+ use: (newOptions = {}) => {
290
+ ["helpers", "functions", "partials", "initialState"].forEach(field => {
291
+ Object.assign(ctx[field], newOptions?.[field] || {});
292
+ });
283
293
  },
284
294
  addHelper: (name, fn) => ctx.helpers[name] = fn,
285
295
  removeHelper: name => delete ctx.helpers[name],
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.32.0",
4
+ "version": "0.33.0",
5
5
  "type": "module",
6
6
  "author": {
7
7
  "name": "Josemi Juanes",