mikel 0.10.0 → 0.11.1

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 +65 -22
  2. package/index.js +86 -118
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -340,27 +340,6 @@ The `@last` variable allows to check if the current iteration using the `#each`
340
340
  {{#each items}}{{@index}}:{{.}} {{#unless @last}},{{/unless}}{{/each}}
341
341
  ```
342
342
 
343
- ### Custom runtime variables
344
-
345
- > Added in `v0.5.0`.
346
-
347
- Mikel allows users to define custom data variables, providing enhanced flexibility and customization options for templates. These custom data variables can be accessed within the template using the `@` character.
348
-
349
- Custom data variables should be provided in the `options.variables` field of the options object when rendering a template. Each custom data variable should be defined as a key-value pair, where the key represents the variable name and the value represents the data associated with that variable.
350
-
351
- Example:
352
-
353
- ```javascript
354
- const result = m("Hello, {{@customVariable}}!", {}, {
355
- variables: {
356
- customVariable: "World",
357
- },
358
- });
359
- console.log(result); // --> 'Hello, World!'
360
- ```
361
-
362
- In this example, the custom data variable `customVariable` is defined with the value `"World"`, and it can be accessed in the template using `@customVariable`.
363
-
364
343
  ### Functions
365
344
 
366
345
  > Added in `v0.8.0`.
@@ -403,7 +382,6 @@ Render the given template string with the provided data object and options.
403
382
  - `data` (object): the data object containing the values to render.
404
383
  - `options` (object): an object containing the following optional values:
405
384
  - `partials` (object): an object containing the available partials.
406
- - `variables` (object): an object containing custom data variables.
407
385
  - `helpers` (object): an object containing custom helpers.
408
386
  - `functions` (object): and object containing custom functions.
409
387
 
@@ -420,6 +398,71 @@ const result = mikel("Hello, {{name}}!", data);
420
398
  console.log(result); // Output: "Hello, World!"
421
399
  ```
422
400
 
401
+ ### `mikel.create(template [, options])`
402
+
403
+ Allows to create an isolated instance of mikel, useful when you want to compile the same template using different data. The `template` argument is the template string, and the optional `options` argument is the same options object that you can pass to `mikel` method.
404
+
405
+ It returns a function that you can call with the data to compile the template.
406
+
407
+ ```javascript
408
+ import mikel from "mikel";
409
+
410
+ const template = mikel.create("Hello, {{name}}!");
411
+
412
+ console.log(template({name: "Bob"})); // --> "Hello, Bob!"
413
+ console.log(template({name: "Susan"})); // --> "Hello, Susan!"
414
+ ```
415
+
416
+ It also exposes the following additional methods:
417
+
418
+ #### `template.addHelper(helperName, helperFn)`
419
+
420
+ Allows to register a new helper instead of using the `options` object.
421
+
422
+ ```javascript
423
+ template.addHelper("foo", () => { ... });
424
+ ```
425
+
426
+ #### `template.removeHelper(helperName)`
427
+
428
+ Removes a previously added helper.
429
+
430
+ ```javascript
431
+ template.removeHelper("foo");
432
+ ```
433
+
434
+ #### `template.addPartial(partialName, partialCode)`
435
+
436
+ Registers a new partial instead of using the `options` object.
437
+
438
+ ```javascript
439
+ template.addPartial("bar", " ... ");
440
+ ```
441
+
442
+ #### `template.removePartial(partialName)`
443
+
444
+ Removes a previously added partial.
445
+
446
+ ```javascript
447
+ template.removePartial("bar");
448
+ ```
449
+
450
+ #### `template.addFunction(fnName, fn)`
451
+
452
+ Registers a new function instead of using the `options` object.
453
+
454
+ ```javascript
455
+ template.addFunction("foo", () => "...");
456
+ ```
457
+
458
+ #### `template.removeFunction(fnName)`
459
+
460
+ Removes a previously added function.
461
+
462
+ ```javascript
463
+ template.removeFunction("foo");
464
+ ```
465
+
423
466
  ### `mikel.escape(str)`
424
467
 
425
468
  This function converts special HTML characters `&`, `<`, `>`, `"`, and `'` to their corresponding HTML entities.
package/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  const tags = /\{\{|\}\}/;
2
- const endl = "\n";
3
2
  const escapedChars = {
4
3
  "&": "&amp;",
5
4
  "<": "&lt;",
@@ -13,62 +12,12 @@ const escape = s => s.toString().replace(/[&<>\"']/g, m => escapedChars[m]);
13
12
  const get = (c, p) => (p === "." ? c : p.split(".").reduce((x, k) => x?.[k], c)) ?? "";
14
13
 
15
14
  const parse = (v, context = {}, vars = {}) => {
16
- if ((v.startsWith(`"`) && v.endsWith(`"`)) || /^-?\d*\.?\d*$/.test(v) || v === "true" || v === "false" || v === "null") {
15
+ if ((v.startsWith(`"`) && v.endsWith(`"`)) || /^-?\d+\.?\d*$/.test(v) || v === "true" || v === "false" || v === "null") {
17
16
  return JSON.parse(v);
18
17
  }
19
18
  return (v || "").startsWith("@") ? get(vars, v.slice(1)) : get(context, v || ".");
20
19
  };
21
20
 
22
- // @description tiny yaml parser
23
- const yamlParser = (str = "") => {
24
- const lines = str.split(endl), result = {}, levels = [{ctx: result, indent: 0}];
25
- let level = 0, i = 0;
26
- while (i < lines.length) {
27
- const line = lines[i] || "";
28
- if (!!line.trim() && !line.trim().startsWith("#")) {
29
- const indent = (line.match(/^( *)/m)?.[0] || "").length;
30
- while(level > 0 && indent < levels[level].indent) {
31
- levels.pop();
32
- level = level - 1;
33
- }
34
- const isArrayItem = line.trim().startsWith("-");
35
- // TODO: improve the regex to capture the line structure
36
- let [_, key, value] = (line.trim().match(/^([^:"]*):(.*)$/m) || ["", line]).map(v => v.trim());
37
- // Check if is a new item of the array
38
- if (isArrayItem) {
39
- key = key.replace(/^(- *)/m, "");
40
- if (typeof value === "undefined") {
41
- value = key;
42
- }
43
- else {
44
- const newIndent = (line.slice(0, line.indexOf(key))).length;
45
- levels[level].ctx.push({});
46
- levels.push({ctx: levels[level].ctx[levels[level].ctx.length - 1], indent: newIndent});
47
- level = level + 1;
48
- }
49
- }
50
- // Check for empty value --> entering into a nested object or array
51
- if (!value) {
52
- const nextLine = lines[i + 1] || "";
53
- const nextIndent = (nextLine.match(/^( *)/m)?.[0] || "").length;
54
- levels[level].ctx[key] = nextLine.trim().startsWith("-") ? [] : {};
55
- if (nextIndent > levels[level].indent) {
56
- levels.push({ctx: levels[level].ctx[key], indent: nextIndent});
57
- level = level + 1;
58
- }
59
- }
60
- else if(value && Array.isArray(levels[level].ctx)) {
61
- levels[level].ctx.push(JSON.parse(value));
62
- }
63
- else if (value) {
64
- levels[level].ctx[key] = JSON.parse(value);
65
- }
66
- }
67
- i = i + 1;
68
- }
69
- return result;
70
- };
71
-
72
21
  // @description tiny front-matter parser
73
22
  const frontmatter = (str = "", parser = null) => {
74
23
  let body = (str || "").trim(), data = {};
@@ -76,11 +25,12 @@ const frontmatter = (str = "", parser = null) => {
76
25
  if (matches?.length === 2 && matches[0].index === 0) {
77
26
  const front = body.substring(0 + matches[0][1].length, matches[1].index).trim();
78
27
  body = body.substring(matches[1].index + matches[1][1].length).trim();
79
- data = typeof parser === "function" ? parser(front) : yamlParser(front);
28
+ data = typeof parser === "function" ? parser(front) : front;
80
29
  }
81
30
  return {body, data};
82
31
  };
83
32
 
33
+ // @description default helpers
84
34
  const defaultHelpers = {
85
35
  "each": (value, opt) => {
86
36
  return (typeof value === "object" ? Object.entries(value || {}) : [])
@@ -94,88 +44,106 @@ const defaultHelpers = {
94
44
  "with": (value, opt) => opt.fn(value),
95
45
  };
96
46
 
97
- const compile = (tokens, output, context, partials, helpers, vars, fn = {}, index = 0, section = "") => {
98
- let i = index;
99
- while (i < tokens.length) {
100
- if (i % 2 === 0) {
101
- output.push(tokens[i]);
102
- }
103
- else if (tokens[i].startsWith("@")) {
104
- output.push(get(vars, tokens[i].slice(1).trim() ?? "_") ?? "");
105
- }
106
- else if (tokens[i].startsWith("!")) {
107
- output.push(get(context, tokens[i].slice(1).trim()));
108
- }
109
- else if (tokens[i].startsWith("#") && typeof helpers[tokens[i].slice(1).trim().split(" ")[0]] === "function") {
110
- const [t, ...args] = tokens[i].slice(1).trim().match(/(?:[^\s"]+|"[^"]*")+/g);
111
- const j = i + 1;
112
- output.push(helpers[t](...args.map(v => parse(v, context, vars)), {
113
- context: context,
114
- fn: (blockContext = {}, blockVars = {}, blockOutput = []) => {
115
- i = compile(tokens, blockOutput, blockContext, partials, helpers, {...vars, ...blockVars, root: vars.root}, fn, j, t);
116
- return blockOutput.join("");
117
- },
118
- }));
119
- // Make sure that this block is executed at least once
120
- if (i + 1 === j) {
121
- i = compile(tokens, [], {}, {}, helpers, {}, {}, j, t);
47
+ // @description create a new instance of mikel
48
+ const create = (template = "", options = {}) => {
49
+ // initialize internal context
50
+ const helpers = Object.assign({}, defaultHelpers, options?.helpers || {});
51
+ const partials = options?.partials || {};
52
+ const functions = options?.functions || {};
53
+ // internal method to compile the template
54
+ const compile = (tokens, output, context, vars, index = 0, section = "") => {
55
+ let i = index;
56
+ while (i < tokens.length) {
57
+ if (i % 2 === 0) {
58
+ output.push(tokens[i]);
122
59
  }
123
- }
124
- else if (tokens[i].startsWith("#") || tokens[i].startsWith("^")) {
125
- const t = tokens[i].slice(1).trim();
126
- const value = get(context, t);
127
- const negate = tokens[i].startsWith("^");
128
- if (!negate && value && Array.isArray(value)) {
60
+ else if (tokens[i].startsWith("@")) {
61
+ output.push(get(vars, tokens[i].slice(1).trim() ?? "_") ?? "");
62
+ }
63
+ else if (tokens[i].startsWith("!")) {
64
+ output.push(get(context, tokens[i].slice(1).trim()));
65
+ }
66
+ 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);
129
68
  const j = i + 1;
130
- (value.length > 0 ? value : [""]).forEach(item => {
131
- i = compile(tokens, value.length > 0 ? output : [], item, partials, helpers, vars, fn, j, t);
132
- });
69
+ output.push(helpers[t](...args.map(v => parse(v, context, vars)), {
70
+ context: context,
71
+ fn: (blockContext = {}, blockVars = {}, blockOutput = []) => {
72
+ i = compile(tokens, blockOutput, blockContext, {...vars, ...blockVars, root: vars.root}, j, t);
73
+ return blockOutput.join("");
74
+ },
75
+ }));
76
+ // Make sure that this block is executed at least once
77
+ if (i + 1 === j) {
78
+ i = compile(tokens, [], {}, {}, j, t);
79
+ }
133
80
  }
134
- else {
135
- const includeOutput = (!negate && !!value) || (negate && !!!value);
136
- i = compile(tokens, includeOutput ? output : [], context, partials, helpers, vars, fn, i + 1, t);
81
+ else if (tokens[i].startsWith("#") || tokens[i].startsWith("^")) {
82
+ const t = tokens[i].slice(1).trim();
83
+ const value = get(context, t);
84
+ const negate = tokens[i].startsWith("^");
85
+ if (!negate && value && Array.isArray(value)) {
86
+ const j = i + 1;
87
+ (value.length > 0 ? value : [""]).forEach(item => {
88
+ i = compile(tokens, value.length > 0 ? output : [], item, vars, j, t);
89
+ });
90
+ }
91
+ else {
92
+ const includeOutput = (!negate && !!value) || (negate && !!!value);
93
+ i = compile(tokens, includeOutput ? output : [], context, vars, i + 1, t);
94
+ }
137
95
  }
138
- }
139
- else if (tokens[i].startsWith(">")) {
140
- const [t, v] = tokens[i].slice(1).trim().split(" ");
141
- if (typeof partials[t] === "string") {
142
- compile(partials[t].split(tags), output, v ? get(context, v) : context, partials, helpers, vars, fn, 0, "");
96
+ else if (tokens[i].startsWith(">")) {
97
+ const [t, v] = tokens[i].slice(1).trim().split(" ");
98
+ if (typeof partials[t] === "string") {
99
+ compile(partials[t].split(tags), output, v ? get(context, v) : context, vars, 0, "");
100
+ }
143
101
  }
144
- }
145
- else if (tokens[i].startsWith("=")) {
146
- const [t, ...args] = tokens[i].slice(1).trim().match(/(?:[^\s"]+|"[^"]*")+/g);
147
- if (typeof fn[t] === "function") {
148
- output.push(fn[t](...args.map(v => parse(v, context, vars))) || "");
102
+ else if (tokens[i].startsWith("=")) {
103
+ const [t, ...args] = tokens[i].slice(1).trim().match(/(?:[^\s"]+|"[^"]*")+/g);
104
+ if (typeof functions[t] === "function") {
105
+ output.push(functions[t](...args.map(v => parse(v, context, vars))) || "");
106
+ }
149
107
  }
150
- }
151
- else if (tokens[i].startsWith("/")) {
152
- if (tokens[i].slice(1).trim() !== section) {
153
- throw new Error(`Unmatched section end: {{${tokens[i]}}}`);
108
+ else if (tokens[i].startsWith("/")) {
109
+ if (tokens[i].slice(1).trim() !== section) {
110
+ throw new Error(`Unmatched section end: {{${tokens[i]}}}`);
111
+ }
112
+ break;
154
113
  }
155
- break;
156
- }
157
- else {
158
- output.push(escape(get(context, tokens[i].trim())));
114
+ else {
115
+ output.push(escape(get(context, tokens[i].trim())));
116
+ }
117
+ i = i + 1;
159
118
  }
160
- i = i + 1;
161
- }
162
- return i;
119
+ return i;
120
+ };
121
+ // entry method to compile the template with the provided data object
122
+ const compileTemplate = (data = {}, output = []) => {
123
+ compile(template.split(tags), output, data, {root: data}, 0, "");
124
+ return output.join("");
125
+ };
126
+ // assign api methods and return method to compile the template
127
+ return Object.assign(compileTemplate, {
128
+ addHelper: (name, fn) => helpers[name] = fn,
129
+ removeHelper: name => delete helpers[name],
130
+ addFunction: (name, fn) => functions[name] = fn,
131
+ removeFunction: name => delete functions[name],
132
+ addPartial: (name, partial) => partials[name] = partial,
133
+ removePartial: name => delete partials[name],
134
+ });
163
135
  };
164
136
 
165
137
  // @description main compiler function
166
- const mikel = (str, context = {}, opt = {}, output = []) => {
167
- const partials = Object.assign({}, opt.partials || {});
168
- const helpers = Object.assign({}, defaultHelpers, opt.helpers || {});
169
- const variables = Object.assign({}, opt.variables || {}, {root: context});
170
- compile(str.split(tags), output, context, partials, helpers, variables, opt.functions || {}, 0, "");
171
- return output.join("");
138
+ const mikel = (template = "", data = {}, options = {}) => {
139
+ return create(template, options)(data);
172
140
  };
173
141
 
174
142
  // @description assign utilities
143
+ mikel.create = create;
175
144
  mikel.escape = escape;
176
145
  mikel.get = get;
177
146
  mikel.parse = parse;
178
- mikel.yaml = yamlParser;
179
147
  mikel.frontmatter = frontmatter;
180
148
 
181
149
  export default mikel;
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.10.0",
4
+ "version": "0.11.1",
5
5
  "type": "module",
6
6
  "author": {
7
7
  "name": "Josemi Juanes",