mikel-frontmatter 0.31.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 +381 -0
  2. package/index.d.ts +46 -0
  3. package/index.js +165 -0
  4. package/package.json +31 -0
package/README.md ADDED
@@ -0,0 +1,381 @@
1
+ # mikel-frontmatter
2
+
3
+ ![npm version](https://badgen.net/npm/v/mikel-frontmatter?labelColor=1d2734&color=21bf81)
4
+ ![license](https://badgen.net/github/license/jmjuanes/mikel?labelColor=1d2734&color=21bf81)
5
+
6
+ A [mikel](https://github.com/jmjuanes/mikel) plugin to define new data inside templates using a frontmatter-like syntax. Supports both **YAML** and **JSON** formats.
7
+
8
+ ## Installation
9
+
10
+ You can install Mikel via npm or yarn:
11
+
12
+ ```bash
13
+ ## Install using npm
14
+ $ npm install mikel mikel-frontmatter
15
+
16
+ ## Install using yarn
17
+ $ yarn add mikel mikel-frontmatter
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ This plugin provides a `{{#frontmatter}}` helper that allows you to define metadata at the beginning of your templates, similar to frontmatter in Markdown files. The parsed data is stored as a variable accessible via `@frontmatter` (or a custom variable name).
23
+
24
+ The plugin automatically detects the format:
25
+ - **YAML format**: When the content doesn't start with `{`
26
+ - **JSON format**: When the content starts with `{` and ends with `}`
27
+
28
+ ### YAML Example
29
+
30
+ ```javascript
31
+ import mikel from "mikel";
32
+ import mikelFrontmatter from "mikel-frontmatter";
33
+
34
+ const template = `
35
+ {{#frontmatter}}
36
+ title: My Page Title
37
+ author: John Doe
38
+ date: 2026-02-03
39
+ tags:
40
+ - javascript
41
+ - templating
42
+ settings:
43
+ published: true
44
+ featured: false
45
+ {{/frontmatter}}
46
+
47
+ <h1>{{@frontmatter.title}}</h1>
48
+ <p>By {{@frontmatter.author}} on {{@frontmatter.date}}</p>
49
+
50
+ {{#if @frontmatter.settings.published}}
51
+ <span class="badge">Published</span>
52
+ {{/if}}
53
+
54
+ <ul>
55
+ {{#each @frontmatter.tags}}
56
+ <li>{{this}}</li>
57
+ {{/each}}
58
+ </ul>
59
+ `;
60
+
61
+ const render = mikel.create();
62
+ render.use(mikelFrontmatter());
63
+
64
+ const result = render(template, {});
65
+ console.log(result);
66
+ ```
67
+
68
+ ### JSON Example
69
+
70
+ ```javascript
71
+ import mikel from "mikel";
72
+ import mikelFrontmatter from "mikel-frontmatter";
73
+
74
+ const template = `
75
+ {{#frontmatter}}
76
+ {
77
+ "title": "My Page Title",
78
+ "author": "John Doe",
79
+ "date": "2026-02-03",
80
+ "tags": ["javascript", "templating"],
81
+ "settings": {
82
+ "published": true,
83
+ "featured": false
84
+ }
85
+ }
86
+ {{/frontmatter}}
87
+
88
+ <h1>{{@frontmatter.title}}</h1>
89
+ <p>By {{@frontmatter.author}} on {{@frontmatter.date}}</p>
90
+
91
+ {{#if @frontmatter.settings.published}}
92
+ <span class="badge">Published</span>
93
+ {{/if}}
94
+
95
+ <ul>
96
+ {{#each @frontmatter.tags}}
97
+ <li>{{this}}</li>
98
+ {{/each}}
99
+ </ul>
100
+ `;
101
+
102
+ const render = mikel.create();
103
+ render.use(mikelFrontmatter());
104
+
105
+ const result = render(template, {});
106
+ console.log(result);
107
+ ```
108
+
109
+ ## Features
110
+
111
+ ### Format Auto-Detection
112
+
113
+ The plugin automatically detects whether your frontmatter is in YAML or JSON format:
114
+
115
+ ```javascript
116
+ // YAML format (default)
117
+ {{#frontmatter}}
118
+ title: My Title
119
+ author: John Doe
120
+ {{/frontmatter}}
121
+
122
+ // JSON format (automatically detected)
123
+ {{#frontmatter}}
124
+ {
125
+ "title": "My Title",
126
+ "author": "John Doe"
127
+ }
128
+ {{/frontmatter}}
129
+ ```
130
+
131
+ ### Basic Key-Value Pairs
132
+
133
+ ```javascript
134
+ const template = `
135
+ {{#frontmatter}}
136
+ title: Hello World
137
+ author: Jane Smith
138
+ version: 1.0.0
139
+ {{/frontmatter}}
140
+
141
+ Title: {{@frontmatter.title}}
142
+ `;
143
+ ```
144
+
145
+ ### Data Types
146
+
147
+ The plugin supports common data types in both YAML and JSON:
148
+
149
+ **YAML:**
150
+ - **Strings**: `name: John Doe` or `name: "John Doe"` or `name: 'John Doe'`
151
+ - **Numbers**: `age: 25` or `price: 19.99`
152
+ - **Booleans**: `published: true` or `active: false` (also `yes/no`, `on/off`)
153
+ - **Null**: `value: null` or `value: ~` or `value:`
154
+
155
+ **JSON:**
156
+ - **Strings**: `"name": "John Doe"`
157
+ - **Numbers**: `"age": 25` or `"price": 19.99`
158
+ - **Booleans**: `"published": true` or `"active": false`
159
+ - **Null**: `"value": null`
160
+
161
+ ### Nested Objects
162
+
163
+ Both YAML and JSON support nested objects:
164
+
165
+ **YAML:**
166
+ ```javascript
167
+ const template = `
168
+ {{#frontmatter}}
169
+ author:
170
+ name: John Doe
171
+ email: john@example.com
172
+ social:
173
+ twitter: @johndoe
174
+ github: johndoe
175
+ {{/frontmatter}}
176
+
177
+ Author: {{@frontmatter.author.name}} ({{@frontmatter.author.email}})
178
+ Twitter: {{@frontmatter.author.social.twitter}}
179
+ `;
180
+ ```
181
+
182
+ **JSON:**
183
+ ```javascript
184
+ const template = `
185
+ {{#frontmatter}}
186
+ {
187
+ "author": {
188
+ "name": "John Doe",
189
+ "email": "john@example.com",
190
+ "social": {
191
+ "twitter": "@johndoe",
192
+ "github": "johndoe"
193
+ }
194
+ }
195
+ }
196
+ {{/frontmatter}}
197
+
198
+ Author: {{@frontmatter.author.name}} ({{@frontmatter.author.email}})
199
+ Twitter: {{@frontmatter.author.social.twitter}}
200
+ `;
201
+ ```
202
+
203
+ ### Arrays
204
+
205
+ Both formats support arrays:
206
+
207
+ **YAML:**
208
+ ```javascript
209
+ const template = `
210
+ {{#frontmatter}}
211
+ tags:
212
+ - javascript
213
+ - nodejs
214
+ - template
215
+ colors:
216
+ - red
217
+ - green
218
+ - blue
219
+ {{/frontmatter}}
220
+
221
+ Tags: {{#each @frontmatter.tags}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}
222
+ `;
223
+ ```
224
+
225
+ **JSON:**
226
+ ```javascript
227
+ const template = `
228
+ {{#frontmatter}}
229
+ {
230
+ "tags": ["javascript", "nodejs", "template"],
231
+ "colors": ["red", "green", "blue"]
232
+ }
233
+ {{/frontmatter}}
234
+
235
+ Tags: {{#each @frontmatter.tags}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}
236
+ `;
237
+ ```
238
+
239
+ ### Arrays of Objects
240
+
241
+ **YAML:**
242
+ ```javascript
243
+ const template = `
244
+ {{#frontmatter}}
245
+ contributors:
246
+ - name: Alice
247
+ role: Developer
248
+ - name: Bob
249
+ role: Designer
250
+ - name: Carol
251
+ role: Manager
252
+ {{/frontmatter}}
253
+
254
+ {{#each @frontmatter.contributors}}
255
+ <div>{{name}} - {{role}}</div>
256
+ {{/each}}
257
+ `;
258
+ ```
259
+
260
+ **JSON:**
261
+ ```javascript
262
+ const template = `
263
+ {{#frontmatter}}
264
+ {
265
+ "contributors": [
266
+ { "name": "Alice", "role": "Developer" },
267
+ { "name": "Bob", "role": "Designer" },
268
+ { "name": "Carol", "role": "Manager" }
269
+ ]
270
+ }
271
+ {{/frontmatter}}
272
+
273
+ {{#each @frontmatter.contributors}}
274
+ <div>{{name}} - {{role}}</div>
275
+ {{/each}}
276
+ `;
277
+ ```
278
+
279
+ ### Custom Variable Name
280
+
281
+ Use the `as` option to store the frontmatter data in a custom variable:
282
+
283
+ ```javascript
284
+ const template = `
285
+ {{#frontmatter as="meta"}}
286
+ title: My Page
287
+ description: A great page
288
+ {{/frontmatter}}
289
+
290
+ <title>{{@meta.title}}</title>
291
+ <meta name="description" content="{{@meta.description}}">
292
+ `;
293
+ ```
294
+
295
+ ### Multiple Frontmatter Blocks
296
+
297
+ You can have multiple `{{#frontmatter}}` blocks in a template. Later blocks will overwrite earlier ones:
298
+
299
+ ```javascript
300
+ const template = `
301
+ {{#frontmatter}}
302
+ title: Original Title
303
+ author: John
304
+ {{/frontmatter}}
305
+
306
+ {{#frontmatter}}
307
+ title: Updated Title
308
+ status: draft
309
+ {{/frontmatter}}
310
+
311
+ Title: {{@frontmatter.title}}
312
+ Author: {{@frontmatter.author}}
313
+ Status: {{@frontmatter.status}}
314
+ `;
315
+
316
+ // Output:
317
+ // Title: Updated Title
318
+ // Author: (empty, overwritten)
319
+ // Status: draft
320
+ ```
321
+
322
+ ## API
323
+
324
+ ### mikelFrontmatter(options?)
325
+
326
+ Creates a new instance of the frontmatter plugin.
327
+
328
+ **Parameters:**
329
+ - `options` (optional): Configuration options for the plugin.
330
+ - `parser` (Function): Custom parser function to override the default YAML/JSON parser.
331
+
332
+ **Returns:** A plugin object with a `frontmatter` helper.
333
+
334
+ ### Helper: `{{#frontmatter}}...{{/frontmatter}}`
335
+
336
+ Parses the content inside the block as YAML or JSON (auto-detected) and stores it as a variable.
337
+
338
+ **Options:**
339
+ - `as`: Custom variable name (default: `"frontmatter"`). Example: `{{#frontmatter as="meta"}}`
340
+
341
+ **Output:** This helper doesn't produce any output in the rendered template.
342
+
343
+ ### Custom Parser
344
+
345
+ You can provide a custom parser function if you need special parsing logic:
346
+
347
+ ```javascript
348
+ const customParser = (content) => {
349
+ // Your custom parsing logic
350
+ return parsedData;
351
+ };
352
+
353
+ const render = mikel.create();
354
+ render.use(mikelFrontmatter({ parser: customParser }));
355
+ ```
356
+
357
+ ## YAML Syntax Support
358
+
359
+ This plugin includes a basic YAML parser that supports:
360
+
361
+ - ✅ Key-value pairs
362
+ - ✅ Nested objects (with indentation)
363
+ - ✅ Arrays (using `- item` syntax)
364
+ - ✅ Arrays of objects
365
+ - ✅ Strings, numbers, booleans, null
366
+ - ✅ Quoted strings (`"..."` or `'...'`)
367
+ - ✅ Comments (lines starting with `#`)
368
+ - ✅ Boolean variants (`yes/no`, `on/off`, `true/false`)
369
+ - ❌ Multi-line strings (with `|` or `>`)
370
+ - ❌ Anchors and aliases (`&anchor`, `*alias`)
371
+ - ❌ Complex YAML features
372
+
373
+ For most common use cases, this subset is sufficient.
374
+
375
+ ## JSON Support
376
+
377
+ The plugin fully supports standard JSON syntax through the native `JSON.parse()` method. If the frontmatter content starts with `{` and ends with `}`, it will be automatically parsed as JSON.
378
+
379
+ ## License
380
+
381
+ Licensed under the [MIT License](../../LICENSE).
package/index.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Options for the mikel-frontmatter plugin
3
+ */
4
+ export interface MikelFrontmatterOptions {
5
+ /**
6
+ * Custom parser function to override the default YAML/JSON parser
7
+ * @param content - The raw frontmatter content string
8
+ * @returns Parsed data object
9
+ */
10
+ parser?: (content: string) => Record<string, any>;
11
+ }
12
+
13
+ /**
14
+ * YAML parser function
15
+ * @param yaml - YAML string to parse
16
+ * @returns Parsed object
17
+ */
18
+ export type YamlParser = (yaml: string) => Record<string, any>;
19
+
20
+ /**
21
+ * Mikel frontmatter plugin
22
+ * @param options - Plugin configuration options
23
+ * @returns Plugin object with helpers
24
+ */
25
+ declare function mikelFrontmatter(options?: MikelFrontmatterOptions): {
26
+ helpers: {
27
+ frontmatter: (params: {
28
+ args: any[];
29
+ opt?: Record<string, any>;
30
+ options: Record<string, any>;
31
+ tokens: string[];
32
+ data: Record<string, any>;
33
+ variables: Record<string, any>;
34
+ fn: (blockData?: Record<string, any>, blockVars?: Record<string, any>, blockOutput?: string[]) => string;
35
+ }) => string;
36
+ };
37
+ };
38
+
39
+ /**
40
+ * YAML parser exposed for direct use
41
+ */
42
+ declare namespace mikelFrontmatter {
43
+ export const yamlParser: YamlParser;
44
+ }
45
+
46
+ export default mikelFrontmatter;
package/index.js ADDED
@@ -0,0 +1,165 @@
1
+ // @description parse a single value from YAML
2
+ const parseValue = (str = "") => {
3
+ // str = str.trim();
4
+ // 1. quoted strings
5
+ if ((str.startsWith(`"`) && str.endsWith(`"`)) || (str.startsWith(`'`) && str.endsWith(`'`))) {
6
+ return str.slice(1, -1);
7
+ }
8
+ // 2. inline array
9
+ if (str.startsWith("[") && str.endsWith("]")) {
10
+ return str.slice(1, -1).split(",").map(item => parseValue(item.trim()));
11
+ }
12
+ // 3. inline object
13
+ if (str.startsWith("{") && str.endsWith("}")) {
14
+ return str.slice(1, -1).split(",").reduce((result, pair) => {
15
+ const [key, value] = pair.split(":");
16
+ if (key && value) {
17
+ result[key.trim()] = parseValue(value.trim());
18
+ }
19
+ return result;
20
+ }, {});
21
+ }
22
+ // 4. booleans
23
+ if (str === "true" || str === "yes" || str === "on") {
24
+ return true;
25
+ }
26
+ if (str === "false" || str === "no" || str === "off") {
27
+ return false;
28
+ }
29
+ // 5. null values
30
+ if (str === "null" || str === "~") {
31
+ return null;
32
+ }
33
+ // 6. numbers
34
+ if (/^-?\d+(\.\d+)?$/.test(str)) {
35
+ return parseFloat(str);
36
+ }
37
+ // 5. return as-is
38
+ return str;
39
+ };
40
+
41
+ // @description get the current indentation level
42
+ const getIndent = (line = "") => {
43
+ return line.length - line.trimStart().length;
44
+ };
45
+
46
+ // @description parse a simple YAML string into an object
47
+ const parseYaml = (yaml = "") => {
48
+ const lines = yaml.split("\n");
49
+ let i = 0;
50
+ const parse = (minIndent = 0, result = {}) => {
51
+ while (i < lines.length) {
52
+ const line = lines[i];
53
+ const trimmed = line.trim();
54
+ const indent = getIndent(line);
55
+
56
+ // skip empty and comments
57
+ if (!trimmed || trimmed[0] === "#") {
58
+ i++;
59
+ continue;
60
+ }
61
+ // Return if dedented
62
+ if (indent < minIndent) {
63
+ return result;
64
+ }
65
+ // Array item
66
+ if (trimmed.startsWith("- ")) {
67
+ // First array item - initialize array
68
+ if (!Array.isArray(result)) {
69
+ result = [];
70
+ }
71
+ const content = trimmed.slice(2).trim();
72
+ i++;
73
+ if (!content) {
74
+ // Nested content on next lines
75
+ result.push(parse(indent + 2, {}));
76
+ }
77
+ // inline content with key:value (object)
78
+ else if (content.includes(":")) {
79
+ const obj = {};
80
+ const colonIdx = content.indexOf(":");
81
+ if (colonIdx > 0) {
82
+ obj[content.slice(0, colonIdx).trim()] = parseValue(content.slice(colonIdx + 1).trim());
83
+ }
84
+ // Check for nested content on following lines
85
+ if (i < lines.length && getIndent(lines[i]) > indent) {
86
+ Object.assign(obj, parse(indent + 2, {}));
87
+ }
88
+ result.push(obj);
89
+ }
90
+ else {
91
+ // Simple value
92
+ result.push(parseValue(content));
93
+ }
94
+ }
95
+ // Key-value pair
96
+ else if (trimmed.includes(":")) {
97
+ const colonIdx = trimmed.indexOf(":");
98
+ const key = trimmed.slice(0, colonIdx).trim();
99
+ const value = trimmed.slice(colonIdx + 1).trim();
100
+ i++;
101
+ if (!value) {
102
+ // Check if next line is an array or object
103
+ if (i < lines.length && getIndent(lines[i]) > indent) {
104
+ const nextLine = lines[i].trim();
105
+ if (nextLine.startsWith("- ")) {
106
+ result[key] = parse(indent + 2, []);
107
+ } else {
108
+ result[key] = parse(indent + 2, {});
109
+ }
110
+ } else {
111
+ result[key] = null;
112
+ }
113
+ } else {
114
+ result[key] = parseValue(value);
115
+ }
116
+ }
117
+ else {
118
+ i++;
119
+ }
120
+ }
121
+ return result;
122
+ };
123
+ return parse(0, {});
124
+ };
125
+
126
+ // @description internal method to parse the content of the frontmatter block
127
+ const parseFrontmatterBlock = (content = "", parser = null) => {
128
+ // 1. use custom parser if provided
129
+ if (typeof parser === "function") {
130
+ return parser(content);
131
+ }
132
+ // 2. guess format (YAML or JSON)
133
+ const trimmed = content.trim();
134
+ if ((trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]"))) {
135
+ return JSON.parse(content);
136
+ }
137
+ return parseYaml(content);
138
+ };
139
+
140
+ // @description plugin to register a #frontmatter helper
141
+ // @param {Object} options - plugin options
142
+ // @param {Function} options.parser - custom YAML parser function
143
+ const mikelFrontmatter = (options = {}) => {
144
+ return {
145
+ helpers: {
146
+ frontmatter: params => {
147
+ const variableName = params.options.as || "frontmatter";
148
+ // const format = params.options.format || "yaml";
149
+ const content = parseFrontmatterBlock(params.fn(params.data) || "", options.parser);
150
+ // register the variable (overwrite if it already exists)
151
+ Object.assign(params.variables, {
152
+ [variableName]: content,
153
+ });
154
+ // don't render anything
155
+ return "";
156
+ },
157
+ },
158
+ };
159
+ };
160
+
161
+ // assign additional metadata to the plugin function
162
+ mikelFrontmatter.yamlParser = parseYaml;
163
+
164
+ // export the plugin as default
165
+ export default mikelFrontmatter;
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "mikel-frontmatter",
3
+ "description": "A mikel plugin to define new data inside templates using a frontmatter-like syntax.",
4
+ "version": "0.31.0",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Josemi Juanes",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/jmjuanes/mikel.git",
11
+ "directory": "packages/mikel-frontmatter"
12
+ },
13
+ "exports": {
14
+ ".": "./index.js",
15
+ "./index.js": "./index.js",
16
+ "./package.json": "./package.json"
17
+ },
18
+ "types": "./index.d.ts",
19
+ "files": [
20
+ "README.md",
21
+ "index.js",
22
+ "index.d.ts"
23
+ ],
24
+ "keywords": [
25
+ "mikel",
26
+ "plugin",
27
+ "frontmatter",
28
+ "metadata",
29
+ "yaml"
30
+ ]
31
+ }