@bedrockio/templates 0.1.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/CHANGELOG.md +3 -0
- package/README.md +167 -0
- package/dist/cjs/TemplateRenderer.js +62 -0
- package/dist/cjs/helpers.js +189 -0
- package/dist/cjs/index.js +8 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/utils.js +62 -0
- package/dist/esm/TemplateRenderer.js +56 -0
- package/dist/esm/helpers.js +182 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/utils.js +54 -0
- package/package.json +62 -0
- package/types/Interpolator.d.ts +9 -0
- package/types/Interpolator.d.ts.map +1 -0
- package/types/TemplateRenderer.d.ts +10 -0
- package/types/TemplateRenderer.d.ts.map +1 -0
- package/types/helpers.d.ts +30 -0
- package/types/helpers.d.ts.map +1 -0
- package/types/index.d.ts +2 -0
- package/types/index.d.ts.map +1 -0
- package/types/something.d.ts +2 -0
- package/types/something.d.ts.map +1 -0
- package/types/utils.d.ts +12 -0
- package/types/utils.d.ts.map +1 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# @bedrockio/templates
|
|
2
|
+
|
|
3
|
+
This package provides a wrapper for Handlebars with additional features for
|
|
4
|
+
custom templating. It standardizes template rendering with support for front
|
|
5
|
+
matter, sections, and custom helpers.
|
|
6
|
+
|
|
7
|
+
- [Install](#install)
|
|
8
|
+
- [Usage](#usage)
|
|
9
|
+
- [Templates](#templates)
|
|
10
|
+
- [Sections](#sections)
|
|
11
|
+
- [Helpers](#helpers)
|
|
12
|
+
- [Default Helpers](#default-helpers)
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
yarn install @bedrockio/templates
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
import { TemplateRenderer } from '@bedrockio/templates';
|
|
24
|
+
|
|
25
|
+
const renderer = new TemplateRenderer({
|
|
26
|
+
// Templates directory
|
|
27
|
+
dir: 'templates',
|
|
28
|
+
|
|
29
|
+
// Custom helpers (optional)
|
|
30
|
+
helpers: {
|
|
31
|
+
uppercase: (str) => str.toUpperCase(),
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// Default params (optional)
|
|
35
|
+
params: {
|
|
36
|
+
foo: 'bar',
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Render a template
|
|
41
|
+
const result = renderer.run({
|
|
42
|
+
// The template string or path
|
|
43
|
+
template: 'Hello {{name}}!',
|
|
44
|
+
|
|
45
|
+
// Parameters to interpolate
|
|
46
|
+
params: {
|
|
47
|
+
name: 'World',
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
console.log(result.body); // "Hello World!"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Templates
|
|
55
|
+
|
|
56
|
+
Templates use Handlebars syntax and support front matter for metadata:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
---
|
|
60
|
+
title: My Title
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
Hello, {{user.name}}!
|
|
64
|
+
|
|
65
|
+
{{#if user.isPremium}}
|
|
66
|
+
Welcome, premium user!
|
|
67
|
+
{{/if}}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Sections
|
|
71
|
+
|
|
72
|
+
Templates can be divided into named sections using three equals signs: `===` as
|
|
73
|
+
delimiters:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
=== SYSTEM ===
|
|
77
|
+
|
|
78
|
+
You are a helpful assistant.
|
|
79
|
+
|
|
80
|
+
=== USER ===
|
|
81
|
+
|
|
82
|
+
I am the user!
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Sections are accessed in the result:
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
const { sections } = renderer.run({
|
|
89
|
+
template: 'my-template,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
console.log(sections);
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
[
|
|
96
|
+
{
|
|
97
|
+
name:'SYSTEM',
|
|
98
|
+
content: 'You are a helpful assistant.'
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name:'USER',
|
|
102
|
+
content: 'I am the user!'
|
|
103
|
+
},
|
|
104
|
+
]
|
|
105
|
+
*/
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Helpers
|
|
110
|
+
|
|
111
|
+
The renderer includes default helpers and supports custom helpers. Custom
|
|
112
|
+
helpers can be passed during instantiation or per render call.
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
const renderer = new TemplateRenderer({
|
|
116
|
+
helpers: {
|
|
117
|
+
foo() {
|
|
118
|
+
return 'foo';
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Default Helpers
|
|
125
|
+
|
|
126
|
+
| Helper | Params | Example |
|
|
127
|
+
| -------------------- | ---------------------- | ----------------------------------------------------------- |
|
|
128
|
+
| **`date`** | | `2025-01-01` |
|
|
129
|
+
| **`dateLong`** | | `January 1, 2025` |
|
|
130
|
+
| **`dateMedium`** | | `Jan 1, 2025` |
|
|
131
|
+
| **`dateShort`** | | `1/1/2025` |
|
|
132
|
+
| **`time`** | | `7:00am` |
|
|
133
|
+
| **`time`** | `meridiem="caps"` | `2:00PM` |
|
|
134
|
+
| **`time`** | `meridiem="space"` | `2:00 pm` |
|
|
135
|
+
| **`time`** | `meridiem="period"` | `2:00 p.m.` |
|
|
136
|
+
| **`time`** | `meridiem="short"` | `2:00p` |
|
|
137
|
+
| **`timeLong`** | | `7:00:00am` |
|
|
138
|
+
| **`timeMedium`** | | `7:00am` |
|
|
139
|
+
| **`timeShort`** | | `7am` |
|
|
140
|
+
| **`timeZone`** | | `7:00am EST` |
|
|
141
|
+
| **`timeZone`** | `style="long"` | `7:00am Eastern Standard Time` |
|
|
142
|
+
| **`timeZone`** | `style="shortGeneric"` | `7:00am ET` |
|
|
143
|
+
| **`timeZone`** | `style="longGeneric"` | `7:00am Eastern Time` |
|
|
144
|
+
| **`dateTime`** | | `January 1, 2025 at 7:00am` |
|
|
145
|
+
| **`dateTimeLong`** | | `January 1, 2025 at 7:00am` |
|
|
146
|
+
| **`dateTimeMedium`** | | `Jan 1, 2025, 7:00am` |
|
|
147
|
+
| **`dateTimeShort`** | | `1/1/2025, 7:00am` |
|
|
148
|
+
| **`dateTimeZone`** | | `January 1, 2025 at 7:00am EST` |
|
|
149
|
+
| **`dateTimeZone`** | `style="long"` | `January 1, 2025 at 7:00am Eastern Standard Time` |
|
|
150
|
+
| **`dateTimeZone`** | `style="shortGeneric"` | `January 1, 2025 at 7:00am ET` |
|
|
151
|
+
| **`dateTimeZone`** | `style="longGeneric"` | `January 1, 2025 at 7:00am Eastern Time` |
|
|
152
|
+
| **`relTime`** | `date` | `6 months ago` |
|
|
153
|
+
| **`relTime`** | `date min=cutoff` | `January 1, 2025` |
|
|
154
|
+
| **`number`** | | `1. Frank` |
|
|
155
|
+
| **`link`** | `url text` | `[Hello](http://example.com)` |
|
|
156
|
+
| **`button`** | `url text` | `<a href="..." class="button" target="_blank">Click me</a>` |
|
|
157
|
+
| **`list`** | `arr` | `- one`<br>`- two`<br>`- three` |
|
|
158
|
+
|
|
159
|
+
**`relTime`** - Formats a date as relative time (e.g., "6 months ago"). When a
|
|
160
|
+
`min` or `max` cutoff date is provided, dates beyond that threshold will be
|
|
161
|
+
formatted as absolute dates instead of relative time.
|
|
162
|
+
|
|
163
|
+
**`number`** - Provides a 1-based index when used inside an `{{#each}}` loop.
|
|
164
|
+
Useful for creating numbered lists.
|
|
165
|
+
|
|
166
|
+
**`list`** - Converts an array into a markdown-formatted bullet list with each
|
|
167
|
+
item prefixed by `- `.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const handlebars_1 = __importDefault(require("handlebars"));
|
|
7
|
+
const lodash_es_1 = require("lodash-es");
|
|
8
|
+
const helpers_1 = require("./helpers");
|
|
9
|
+
const utils_1 = require("./utils");
|
|
10
|
+
class TemplateRenderer {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
/** @type {Object} */
|
|
13
|
+
this.options = {
|
|
14
|
+
...options,
|
|
15
|
+
helpers: {
|
|
16
|
+
...helpers_1.DEFAULT_HELPERS,
|
|
17
|
+
...options.helpers,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
run(options) {
|
|
22
|
+
const { template, params, helpers, ...rest } = this.resolveOptions(options);
|
|
23
|
+
if (!template) {
|
|
24
|
+
return {
|
|
25
|
+
body: '',
|
|
26
|
+
sections: [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const compiled = this.loadTemplate(template, rest);
|
|
30
|
+
return compiled(params, {
|
|
31
|
+
helpers: (0, helpers_1.resolveHelpers)(helpers, rest),
|
|
32
|
+
allowProtoPropertiesByDefault: true,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// Private
|
|
36
|
+
/** @returns {Object} */
|
|
37
|
+
resolveOptions(options = {}) {
|
|
38
|
+
return {
|
|
39
|
+
...this.options,
|
|
40
|
+
...options,
|
|
41
|
+
params: {
|
|
42
|
+
...this.options.params,
|
|
43
|
+
...options.params,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
loadTemplate = (0, lodash_es_1.memoize)((input, options) => {
|
|
48
|
+
const source = (0, utils_1.resolveTemplateSource)(input, options);
|
|
49
|
+
const template = handlebars_1.default.compile(source.trim());
|
|
50
|
+
return (params, options) => {
|
|
51
|
+
const output = template(params, options);
|
|
52
|
+
const { body, meta } = (0, utils_1.runFrontMatter)(output);
|
|
53
|
+
const sections = (0, utils_1.getSections)(body);
|
|
54
|
+
return {
|
|
55
|
+
body,
|
|
56
|
+
meta,
|
|
57
|
+
sections,
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
exports.default = TemplateRenderer;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEFAULT_HELPERS = void 0;
|
|
7
|
+
exports.resolveHelpers = resolveHelpers;
|
|
8
|
+
const chrono_1 = require("@bedrockio/chrono");
|
|
9
|
+
const handlebars_1 = __importDefault(require("handlebars"));
|
|
10
|
+
exports.DEFAULT_HELPERS = {
|
|
11
|
+
// date
|
|
12
|
+
date(arg) {
|
|
13
|
+
return new chrono_1.DateTime(arg).toDate();
|
|
14
|
+
},
|
|
15
|
+
dateLong(arg) {
|
|
16
|
+
return new chrono_1.DateTime(arg).toDateLong();
|
|
17
|
+
},
|
|
18
|
+
dateMedium(arg) {
|
|
19
|
+
return new chrono_1.DateTime(arg).toDateMedium();
|
|
20
|
+
},
|
|
21
|
+
dateShort(arg) {
|
|
22
|
+
return new chrono_1.DateTime(arg).toDateShort();
|
|
23
|
+
},
|
|
24
|
+
// time
|
|
25
|
+
time(arg, meridiem) {
|
|
26
|
+
return new chrono_1.DateTime(arg).toTimeMedium({
|
|
27
|
+
meridiem,
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
/**
|
|
31
|
+
* @param {Intl.DateTimeFormatOptions['timeZoneName']} [style='short']
|
|
32
|
+
*/
|
|
33
|
+
timeZone(arg, meridiem, style = 'short') {
|
|
34
|
+
return new chrono_1.DateTime(arg).toTimeWithZone({
|
|
35
|
+
meridiem,
|
|
36
|
+
timeZoneName: style,
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
timeLong(arg, meridiem) {
|
|
40
|
+
return new chrono_1.DateTime(arg).toTimeLong({
|
|
41
|
+
meridiem,
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
timeMedium(arg, meridiem) {
|
|
45
|
+
return new chrono_1.DateTime(arg).toTimeMedium({
|
|
46
|
+
meridiem,
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
timeShort(arg, meridiem) {
|
|
50
|
+
return new chrono_1.DateTime(arg).toTimeShort({
|
|
51
|
+
meridiem,
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
// datetime
|
|
55
|
+
dateTime(arg, meridiem) {
|
|
56
|
+
return new chrono_1.DateTime(arg).formatLong({
|
|
57
|
+
meridiem,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
/**
|
|
61
|
+
* @param {Intl.DateTimeFormatOptions['timeZoneName']} [style='short']
|
|
62
|
+
*/
|
|
63
|
+
dateTimeZone(arg, meridiem, style = 'short') {
|
|
64
|
+
return new chrono_1.DateTime(arg).formatWithZone({
|
|
65
|
+
meridiem,
|
|
66
|
+
timeZoneName: style,
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
dateTimeLong(arg, meridiem) {
|
|
70
|
+
return new chrono_1.DateTime(arg).formatLong({
|
|
71
|
+
meridiem,
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
dateTimeMedium(arg, meridiem) {
|
|
75
|
+
return new chrono_1.DateTime(arg).formatMedium({
|
|
76
|
+
meridiem,
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
dateTimeShort(arg, meridiem) {
|
|
80
|
+
return new chrono_1.DateTime(arg).formatShort({
|
|
81
|
+
meridiem,
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
// relative time
|
|
85
|
+
relTime(arg, min, max) {
|
|
86
|
+
return new chrono_1.DateTime(arg).relative({
|
|
87
|
+
min,
|
|
88
|
+
max,
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
number(options) {
|
|
92
|
+
const { index } = options.data;
|
|
93
|
+
if (index == null) {
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
return index + 1;
|
|
97
|
+
},
|
|
98
|
+
link(url, text) {
|
|
99
|
+
return new handlebars_1.default.SafeString(`[${text}](${url})`);
|
|
100
|
+
},
|
|
101
|
+
button(url, text) {
|
|
102
|
+
return generateHtml('a', {
|
|
103
|
+
text,
|
|
104
|
+
href: url,
|
|
105
|
+
class: 'button',
|
|
106
|
+
target: '_blank',
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
list(arr) {
|
|
110
|
+
return arr
|
|
111
|
+
.map((el) => {
|
|
112
|
+
return `- ${el}`;
|
|
113
|
+
})
|
|
114
|
+
.join('\n');
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
function resolveHelpers(helpers, options) {
|
|
118
|
+
const result = {};
|
|
119
|
+
for (let [key, value] of Object.entries(helpers)) {
|
|
120
|
+
result[key] = resolveHelper(value, options);
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
function resolveHelper(arg, options) {
|
|
125
|
+
const { names, handler } = resolveArgumentNames(arg);
|
|
126
|
+
return (...args) => {
|
|
127
|
+
return handler(...resolveHelperArgs(args, names, options));
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// Arguments
|
|
131
|
+
const ARGUMENT_NAMES_REG = /\w+\((.+)\) {/;
|
|
132
|
+
function resolveArgumentNames(arg) {
|
|
133
|
+
if (typeof arg === 'function') {
|
|
134
|
+
const handler = arg;
|
|
135
|
+
const match = handler.toString().match(ARGUMENT_NAMES_REG);
|
|
136
|
+
let names = [];
|
|
137
|
+
if (match) {
|
|
138
|
+
names = match[1]
|
|
139
|
+
.split(', ')
|
|
140
|
+
.map((arg) => {
|
|
141
|
+
return arg.split(' ')[0];
|
|
142
|
+
})
|
|
143
|
+
.filter((arg) => {
|
|
144
|
+
return arg !== 'options';
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return { handler, names };
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
return {
|
|
151
|
+
names: arg.params,
|
|
152
|
+
handler: arg.handler,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function resolveHelperArgs(args, names, options) {
|
|
157
|
+
const ordered = args.slice(0, -1);
|
|
158
|
+
const [meta] = args.slice(-1);
|
|
159
|
+
let params = { ...meta.hash };
|
|
160
|
+
ordered.forEach((value, i) => {
|
|
161
|
+
const name = names[i];
|
|
162
|
+
params[name] = value;
|
|
163
|
+
});
|
|
164
|
+
const resolved = names.map((name) => {
|
|
165
|
+
return params[name];
|
|
166
|
+
});
|
|
167
|
+
options = {
|
|
168
|
+
options,
|
|
169
|
+
data: meta.data,
|
|
170
|
+
};
|
|
171
|
+
return [...resolved, options];
|
|
172
|
+
}
|
|
173
|
+
function generateHtml(tag, props) {
|
|
174
|
+
const { text, ...rest } = props;
|
|
175
|
+
const attr = Object.entries(rest)
|
|
176
|
+
.map((entry) => {
|
|
177
|
+
const [key, value] = entry;
|
|
178
|
+
if (value) {
|
|
179
|
+
return [key, `"${value}"`].join('=');
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
.filter((a) => a)
|
|
183
|
+
.join(' ');
|
|
184
|
+
let html = `<${tag} ${attr}>`;
|
|
185
|
+
if (text) {
|
|
186
|
+
html += `${text}</${tag}>`;
|
|
187
|
+
}
|
|
188
|
+
return new handlebars_1.default.SafeString(html);
|
|
189
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TemplateRenderer = void 0;
|
|
7
|
+
var TemplateRenderer_1 = require("./TemplateRenderer");
|
|
8
|
+
Object.defineProperty(exports, "TemplateRenderer", { enumerable: true, get: function () { return __importDefault(TemplateRenderer_1).default; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "type": "commonjs" }
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveTemplateSource = resolveTemplateSource;
|
|
7
|
+
exports.runFrontMatter = runFrontMatter;
|
|
8
|
+
exports.getSections = getSections;
|
|
9
|
+
const fs_1 = require("fs");
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const front_matter_1 = __importDefault(require("front-matter"));
|
|
12
|
+
function resolveTemplateSource(arg, options) {
|
|
13
|
+
const { dir } = options;
|
|
14
|
+
if (dir) {
|
|
15
|
+
const filepath = path_1.default.resolve(dir, arg);
|
|
16
|
+
return readSource(filepath) || arg;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
return arg;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function runFrontMatter(str) {
|
|
23
|
+
let { body, attributes: meta } = (0, front_matter_1.default)(str);
|
|
24
|
+
return {
|
|
25
|
+
meta,
|
|
26
|
+
body: body.trim(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function tryReadFile(filepath, ext) {
|
|
30
|
+
try {
|
|
31
|
+
return (0, fs_1.readFileSync)(filepath + ext, 'utf-8');
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error.code !== 'ENOENT') {
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function readSource(filepath) {
|
|
40
|
+
return tryReadFile(filepath, '.md') || tryReadFile(filepath, '.txt');
|
|
41
|
+
}
|
|
42
|
+
// Sections
|
|
43
|
+
const SECTIONS_REG = /^=== (\w+) ===\n\n/gm;
|
|
44
|
+
function getSections(str) {
|
|
45
|
+
str = str.trim();
|
|
46
|
+
const arr = str.split(SECTIONS_REG).slice(1);
|
|
47
|
+
if (!arr.length) {
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
content: str,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
const sections = [];
|
|
55
|
+
for (let i = 0; i < arr.length; i += 2) {
|
|
56
|
+
sections.push({
|
|
57
|
+
title: arr[i],
|
|
58
|
+
content: arr[i + 1].trim(),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return sections;
|
|
62
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import Handlebars from 'handlebars';
|
|
2
|
+
import { memoize } from 'lodash-es';
|
|
3
|
+
import { DEFAULT_HELPERS, resolveHelpers } from './helpers.js';
|
|
4
|
+
import { getSections, resolveTemplateSource, runFrontMatter } from './utils.js';
|
|
5
|
+
export default class TemplateRenderer {
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
/** @type {Object} */
|
|
8
|
+
this.options = {
|
|
9
|
+
...options,
|
|
10
|
+
helpers: {
|
|
11
|
+
...DEFAULT_HELPERS,
|
|
12
|
+
...options.helpers,
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
run(options) {
|
|
17
|
+
const { template, params, helpers, ...rest } = this.resolveOptions(options);
|
|
18
|
+
if (!template) {
|
|
19
|
+
return {
|
|
20
|
+
body: '',
|
|
21
|
+
sections: [],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const compiled = this.loadTemplate(template, rest);
|
|
25
|
+
return compiled(params, {
|
|
26
|
+
helpers: resolveHelpers(helpers, rest),
|
|
27
|
+
allowProtoPropertiesByDefault: true,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
// Private
|
|
31
|
+
/** @returns {Object} */
|
|
32
|
+
resolveOptions(options = {}) {
|
|
33
|
+
return {
|
|
34
|
+
...this.options,
|
|
35
|
+
...options,
|
|
36
|
+
params: {
|
|
37
|
+
...this.options.params,
|
|
38
|
+
...options.params,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
loadTemplate = memoize((input, options) => {
|
|
43
|
+
const source = resolveTemplateSource(input, options);
|
|
44
|
+
const template = Handlebars.compile(source.trim());
|
|
45
|
+
return (params, options) => {
|
|
46
|
+
const output = template(params, options);
|
|
47
|
+
const { body, meta } = runFrontMatter(output);
|
|
48
|
+
const sections = getSections(body);
|
|
49
|
+
return {
|
|
50
|
+
body,
|
|
51
|
+
meta,
|
|
52
|
+
sections,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { DateTime } from '@bedrockio/chrono';
|
|
2
|
+
import Handlebars from 'handlebars';
|
|
3
|
+
export const DEFAULT_HELPERS = {
|
|
4
|
+
// date
|
|
5
|
+
date(arg) {
|
|
6
|
+
return new DateTime(arg).toDate();
|
|
7
|
+
},
|
|
8
|
+
dateLong(arg) {
|
|
9
|
+
return new DateTime(arg).toDateLong();
|
|
10
|
+
},
|
|
11
|
+
dateMedium(arg) {
|
|
12
|
+
return new DateTime(arg).toDateMedium();
|
|
13
|
+
},
|
|
14
|
+
dateShort(arg) {
|
|
15
|
+
return new DateTime(arg).toDateShort();
|
|
16
|
+
},
|
|
17
|
+
// time
|
|
18
|
+
time(arg, meridiem) {
|
|
19
|
+
return new DateTime(arg).toTimeMedium({
|
|
20
|
+
meridiem,
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
/**
|
|
24
|
+
* @param {Intl.DateTimeFormatOptions['timeZoneName']} [style='short']
|
|
25
|
+
*/
|
|
26
|
+
timeZone(arg, meridiem, style = 'short') {
|
|
27
|
+
return new DateTime(arg).toTimeWithZone({
|
|
28
|
+
meridiem,
|
|
29
|
+
timeZoneName: style,
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
timeLong(arg, meridiem) {
|
|
33
|
+
return new DateTime(arg).toTimeLong({
|
|
34
|
+
meridiem,
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
timeMedium(arg, meridiem) {
|
|
38
|
+
return new DateTime(arg).toTimeMedium({
|
|
39
|
+
meridiem,
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
timeShort(arg, meridiem) {
|
|
43
|
+
return new DateTime(arg).toTimeShort({
|
|
44
|
+
meridiem,
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
// datetime
|
|
48
|
+
dateTime(arg, meridiem) {
|
|
49
|
+
return new DateTime(arg).formatLong({
|
|
50
|
+
meridiem,
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
/**
|
|
54
|
+
* @param {Intl.DateTimeFormatOptions['timeZoneName']} [style='short']
|
|
55
|
+
*/
|
|
56
|
+
dateTimeZone(arg, meridiem, style = 'short') {
|
|
57
|
+
return new DateTime(arg).formatWithZone({
|
|
58
|
+
meridiem,
|
|
59
|
+
timeZoneName: style,
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
dateTimeLong(arg, meridiem) {
|
|
63
|
+
return new DateTime(arg).formatLong({
|
|
64
|
+
meridiem,
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
dateTimeMedium(arg, meridiem) {
|
|
68
|
+
return new DateTime(arg).formatMedium({
|
|
69
|
+
meridiem,
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
dateTimeShort(arg, meridiem) {
|
|
73
|
+
return new DateTime(arg).formatShort({
|
|
74
|
+
meridiem,
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
// relative time
|
|
78
|
+
relTime(arg, min, max) {
|
|
79
|
+
return new DateTime(arg).relative({
|
|
80
|
+
min,
|
|
81
|
+
max,
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
number(options) {
|
|
85
|
+
const { index } = options.data;
|
|
86
|
+
if (index == null) {
|
|
87
|
+
return '';
|
|
88
|
+
}
|
|
89
|
+
return index + 1;
|
|
90
|
+
},
|
|
91
|
+
link(url, text) {
|
|
92
|
+
return new Handlebars.SafeString(`[${text}](${url})`);
|
|
93
|
+
},
|
|
94
|
+
button(url, text) {
|
|
95
|
+
return generateHtml('a', {
|
|
96
|
+
text,
|
|
97
|
+
href: url,
|
|
98
|
+
class: 'button',
|
|
99
|
+
target: '_blank',
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
list(arr) {
|
|
103
|
+
return arr
|
|
104
|
+
.map((el) => {
|
|
105
|
+
return `- ${el}`;
|
|
106
|
+
})
|
|
107
|
+
.join('\n');
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
export function resolveHelpers(helpers, options) {
|
|
111
|
+
const result = {};
|
|
112
|
+
for (let [key, value] of Object.entries(helpers)) {
|
|
113
|
+
result[key] = resolveHelper(value, options);
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
function resolveHelper(arg, options) {
|
|
118
|
+
const { names, handler } = resolveArgumentNames(arg);
|
|
119
|
+
return (...args) => {
|
|
120
|
+
return handler(...resolveHelperArgs(args, names, options));
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// Arguments
|
|
124
|
+
const ARGUMENT_NAMES_REG = /\w+\((.+)\) {/;
|
|
125
|
+
function resolveArgumentNames(arg) {
|
|
126
|
+
if (typeof arg === 'function') {
|
|
127
|
+
const handler = arg;
|
|
128
|
+
const match = handler.toString().match(ARGUMENT_NAMES_REG);
|
|
129
|
+
let names = [];
|
|
130
|
+
if (match) {
|
|
131
|
+
names = match[1]
|
|
132
|
+
.split(', ')
|
|
133
|
+
.map((arg) => {
|
|
134
|
+
return arg.split(' ')[0];
|
|
135
|
+
})
|
|
136
|
+
.filter((arg) => {
|
|
137
|
+
return arg !== 'options';
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return { handler, names };
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
return {
|
|
144
|
+
names: arg.params,
|
|
145
|
+
handler: arg.handler,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function resolveHelperArgs(args, names, options) {
|
|
150
|
+
const ordered = args.slice(0, -1);
|
|
151
|
+
const [meta] = args.slice(-1);
|
|
152
|
+
let params = { ...meta.hash };
|
|
153
|
+
ordered.forEach((value, i) => {
|
|
154
|
+
const name = names[i];
|
|
155
|
+
params[name] = value;
|
|
156
|
+
});
|
|
157
|
+
const resolved = names.map((name) => {
|
|
158
|
+
return params[name];
|
|
159
|
+
});
|
|
160
|
+
options = {
|
|
161
|
+
options,
|
|
162
|
+
data: meta.data,
|
|
163
|
+
};
|
|
164
|
+
return [...resolved, options];
|
|
165
|
+
}
|
|
166
|
+
function generateHtml(tag, props) {
|
|
167
|
+
const { text, ...rest } = props;
|
|
168
|
+
const attr = Object.entries(rest)
|
|
169
|
+
.map((entry) => {
|
|
170
|
+
const [key, value] = entry;
|
|
171
|
+
if (value) {
|
|
172
|
+
return [key, `"${value}"`].join('=');
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
.filter((a) => a)
|
|
176
|
+
.join(' ');
|
|
177
|
+
let html = `<${tag} ${attr}>`;
|
|
178
|
+
if (text) {
|
|
179
|
+
html += `${text}</${tag}>`;
|
|
180
|
+
}
|
|
181
|
+
return new Handlebars.SafeString(html);
|
|
182
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as TemplateRenderer } from './TemplateRenderer.js';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import frontmatter from 'front-matter';
|
|
4
|
+
export function resolveTemplateSource(arg, options) {
|
|
5
|
+
const { dir } = options;
|
|
6
|
+
if (dir) {
|
|
7
|
+
const filepath = path.resolve(dir, arg);
|
|
8
|
+
return readSource(filepath) || arg;
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
return arg;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function runFrontMatter(str) {
|
|
15
|
+
let { body, attributes: meta } = frontmatter(str);
|
|
16
|
+
return {
|
|
17
|
+
meta,
|
|
18
|
+
body: body.trim(),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function tryReadFile(filepath, ext) {
|
|
22
|
+
try {
|
|
23
|
+
return readFileSync(filepath + ext, 'utf-8');
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code !== 'ENOENT') {
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function readSource(filepath) {
|
|
32
|
+
return tryReadFile(filepath, '.md') || tryReadFile(filepath, '.txt');
|
|
33
|
+
}
|
|
34
|
+
// Sections
|
|
35
|
+
const SECTIONS_REG = /^=== (\w+) ===\n\n/gm;
|
|
36
|
+
export function getSections(str) {
|
|
37
|
+
str = str.trim();
|
|
38
|
+
const arr = str.split(SECTIONS_REG).slice(1);
|
|
39
|
+
if (!arr.length) {
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
content: str,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
const sections = [];
|
|
47
|
+
for (let i = 0; i < arr.length; i += 2) {
|
|
48
|
+
sections.push({
|
|
49
|
+
title: arr[i],
|
|
50
|
+
content: arr[i + 1].trim(),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return sections;
|
|
54
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bedrockio/templates",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Bedrock utility for custom templates.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "vitest run",
|
|
8
|
+
"lint": "eslint",
|
|
9
|
+
"build": "scripts/build",
|
|
10
|
+
"eject": "scripts/eject",
|
|
11
|
+
"build:cjs": "tsc -p tsconfig.cjs.json",
|
|
12
|
+
"build:esm": "tsc -p tsconfig.esm.json && tsc-alias -f -p tsconfig.esm.json",
|
|
13
|
+
"build:types": "tsc -p tsconfig.types.json",
|
|
14
|
+
"prepublish": "yarn build"
|
|
15
|
+
},
|
|
16
|
+
"types": "types/index.d.ts",
|
|
17
|
+
"main": "./dist/cjs/index.js",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./types/index.d.ts",
|
|
21
|
+
"import": "./dist/esm/index.js",
|
|
22
|
+
"require": "./dist/cjs/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"contributors": [
|
|
26
|
+
{
|
|
27
|
+
"name": "Andrew Plummer",
|
|
28
|
+
"email": "andrew@rekall.ai"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/bedrockio/templates"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@bedrockio/chrono": "^0.7.0",
|
|
38
|
+
"front-matter": "^4.0.2",
|
|
39
|
+
"handlebars": "^4.7.8",
|
|
40
|
+
"lodash-es": "^4.17.21"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@bedrockio/eslint-plugin": "^1.3.0",
|
|
44
|
+
"@bedrockio/prettier-config": "^1.1.1",
|
|
45
|
+
"@sinonjs/fake-timers": "^15.0.0",
|
|
46
|
+
"eslint": "^9.37.0",
|
|
47
|
+
"tsc-alias": "^1.8.16",
|
|
48
|
+
"typescript": "^5.9.3",
|
|
49
|
+
"vitest": "^3.2.4"
|
|
50
|
+
},
|
|
51
|
+
"prettier": "@bedrockio/prettier-config",
|
|
52
|
+
"files": [
|
|
53
|
+
"dist/**",
|
|
54
|
+
"types/**",
|
|
55
|
+
"README.md",
|
|
56
|
+
"CHANGELOG.md"
|
|
57
|
+
],
|
|
58
|
+
"volta": {
|
|
59
|
+
"node": "22.20.0",
|
|
60
|
+
"yarn": "1.22.22"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Interpolator.d.ts","sourceRoot":"","sources":["../src/Interpolator.js"],"names":[],"mappings":"AA8HA;IACE,0BAGC;IAFC,aAAsB;IAIxB,sBAAkB;IAElB,kBAKG;IAuBH,uBAOC;IAED,kCAMC;CACF"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export default class TemplateRenderer {
|
|
2
|
+
constructor(options?: {});
|
|
3
|
+
/** @type {Object} */
|
|
4
|
+
options: any;
|
|
5
|
+
run(options: any): any;
|
|
6
|
+
/** @returns {Object} */
|
|
7
|
+
resolveOptions(options?: {}): any;
|
|
8
|
+
loadTemplate: any;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=TemplateRenderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TemplateRenderer.d.ts","sourceRoot":"","sources":["../src/TemplateRenderer.js"],"names":[],"mappings":"AAMA;IACE,0BASC;IARC,qBAAqB;IACrB,aAMC;IAGH,uBAgBC;IAID,wBAAwB;IACxB,kCASC;IAED,kBAgBG;CACJ"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function resolveHelpers(helpers: any, options: any): {};
|
|
2
|
+
export namespace DEFAULT_HELPERS {
|
|
3
|
+
function date(arg: any): string;
|
|
4
|
+
function dateLong(arg: any): any;
|
|
5
|
+
function dateMedium(arg: any): any;
|
|
6
|
+
function dateShort(arg: any): any;
|
|
7
|
+
function time(arg: any, meridiem: any): any;
|
|
8
|
+
/**
|
|
9
|
+
* @param {Intl.DateTimeFormatOptions['timeZoneName']} [style='short']
|
|
10
|
+
*/
|
|
11
|
+
function timeZone(arg: any, meridiem: any, style?: Intl.DateTimeFormatOptions["timeZoneName"]): any;
|
|
12
|
+
function timeLong(arg: any, meridiem: any): any;
|
|
13
|
+
function timeMedium(arg: any, meridiem: any): any;
|
|
14
|
+
function timeShort(arg: any, meridiem: any): any;
|
|
15
|
+
function dateTime(arg: any, meridiem: any): any;
|
|
16
|
+
/**
|
|
17
|
+
* @param {Intl.DateTimeFormatOptions['timeZoneName']} [style='short']
|
|
18
|
+
*/
|
|
19
|
+
function dateTimeZone(arg: any, meridiem: any, style?: Intl.DateTimeFormatOptions["timeZoneName"]): any;
|
|
20
|
+
function dateTimeLong(arg: any, meridiem: any): any;
|
|
21
|
+
function dateTimeMedium(arg: any, meridiem: any): any;
|
|
22
|
+
function dateTimeShort(arg: any, meridiem: any): any;
|
|
23
|
+
function relTime(arg: any, min: any, max: any): any;
|
|
24
|
+
function number(options: any): any;
|
|
25
|
+
function link(url: any, text: any): Handlebars.SafeString;
|
|
26
|
+
function button(url: any, text: any): Handlebars.SafeString;
|
|
27
|
+
function list(arr: any): any;
|
|
28
|
+
}
|
|
29
|
+
import Handlebars from 'handlebars';
|
|
30
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.js"],"names":[],"mappings":"AAyHA,+DAMC;;IA1HC,gCAEC;IACD,iCAEC;IACD,mCAEC;IACD,kCAEC;IAGD,4CAIC;IAED;;OAEG;IACH,mDAFW,IAAI,CAAC,qBAAqB,CAAC,cAAc,CAAC,OAOpD;IACD,gDAIC;IACD,kDAIC;IACD,iDAIC;IAGD,gDAIC;IAED;;OAEG;IACH,uDAFW,IAAI,CAAC,qBAAqB,CAAC,cAAc,CAAC,OAOpD;IACD,oDAIC;IACD,sDAIC;IACD,qDAIC;IAID,oDAKC;IAED,mCAMC;IAED,0DAEC;IAED,4DAOC;IAED,6BAMC;;uBArHoB,YAAY"}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"something.d.ts","sourceRoot":"","sources":["../src/something.js"],"names":[],"mappings":"AAgCA,gDAEgB,WAAM,iBAerB"}
|
package/types/utils.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function resolveTemplateSource(arg: any, options: any): any;
|
|
2
|
+
export function runFrontMatter(str: any): {
|
|
3
|
+
meta: any;
|
|
4
|
+
body: string;
|
|
5
|
+
};
|
|
6
|
+
export function getSections(str: any): {
|
|
7
|
+
content: any;
|
|
8
|
+
}[] | {
|
|
9
|
+
title: any;
|
|
10
|
+
content: any;
|
|
11
|
+
}[];
|
|
12
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.js"],"names":[],"mappings":"AAKA,mEASC;AAED;;;EAOC;AAoBD;;;;;IAsBC"}
|