fable 3.1.72 → 3.1.74
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/docs/README.md +30 -6
- package/docs/_brand.json +18 -0
- package/docs/_playground.json +10 -0
- package/docs/_sidebar.md +2 -0
- package/docs/_version.json +3 -3
- package/docs/architecture.md +201 -39
- package/docs/index.html +6 -7
- package/docs/pict-docuserve.min.js +91 -0
- package/docs/pict-docuserve.min.js.map +1 -0
- package/docs/playground.md +38 -0
- package/docs/retold-catalog.json +1 -1
- package/docs/retold-keyword-index.json +8721 -8105
- package/docs/services/README.md +26 -9
- package/docs/services/anticipate.md +104 -40
- package/docs/services/csv-parser.md +63 -35
- package/docs/services/data-format.md +154 -49
- package/docs/services/data-generation.md +77 -16
- package/docs/services/dates.md +103 -36
- package/docs/services/environment-data.md +13 -2
- package/docs/services/expression-parser.md +280 -68
- package/docs/services/file-persistence.md +142 -150
- package/docs/services/logging.md +93 -37
- package/docs/services/logic.md +70 -22
- package/docs/services/manifest.md +114 -26
- package/docs/services/math.md +168 -63
- package/docs/services/meta-template.md +312 -158
- package/docs/services/object-cache.md +94 -11
- package/docs/services/operation.md +68 -6
- package/docs/services/progress-time.md +74 -13
- package/docs/services/progress-tracker-set.md +101 -3
- package/docs/services/rest-client.md +136 -104
- package/docs/services/settings-manager.md +133 -40
- package/docs/services/template.md +71 -22
- package/docs/services/utility.md +121 -29
- package/docs/services/uuid.md +58 -10
- package/package.json +2 -2
- package/source/services/Fable-Service-MetaTemplate/MetaTemplate-StringParser.js +6 -0
- package/test/MetaTemplating_tests.js +77 -0
- package/.claude/settings.local.json +0 -8
- package/docs/css/docuserve.css +0 -327
|
@@ -1,26 +1,54 @@
|
|
|
1
1
|
# MetaTemplate Service
|
|
2
2
|
|
|
3
|
+
The MetaTemplate service is a **low-level pattern-replacement primitive**.
|
|
4
|
+
You register one or more `(start, end, parserFn)` patterns; `parseString()`
|
|
5
|
+
then walks the template string character by character, hands each
|
|
6
|
+
between-tag region to the matching parser function, and emits the
|
|
7
|
+
function's return value into the output.
|
|
8
|
+
|
|
9
|
+
It is intentionally minimal: there are no built-in tags. The `{~Name~}`
|
|
10
|
+
syntax shown below is just convention — every pattern is something you
|
|
11
|
+
wire up yourself with `addPattern()`. End markers are short by design
|
|
12
|
+
(typically 1–2 characters such as `~}`, `>>`, `}`); the engine's
|
|
13
|
+
character-by-character WordTree match is built around that.
|
|
14
|
+
|
|
3
15
|
## Access
|
|
4
16
|
|
|
5
17
|
```javascript
|
|
18
|
+
const libFable = require('fable');
|
|
19
|
+
const fable = new libFable({ Product: 'MetaTemplateDemo', ProductVersion: '1.0.0' });
|
|
20
|
+
|
|
6
21
|
// On-demand service - instantiate when needed
|
|
7
22
|
const metaTemplate = fable.instantiateServiceProvider('MetaTemplate');
|
|
23
|
+
console.log('parseString:', typeof metaTemplate.parseString);
|
|
24
|
+
console.log('addPattern:', typeof metaTemplate.addPattern);
|
|
8
25
|
```
|
|
9
26
|
|
|
10
27
|
## Basic Usage
|
|
11
28
|
|
|
12
|
-
###
|
|
29
|
+
### Register a Substitution Pattern
|
|
30
|
+
|
|
31
|
+
The minimum useful setup: one `{~name~}` pattern whose parser function
|
|
32
|
+
resolves `name` against the data object using dot/bracket notation.
|
|
13
33
|
|
|
14
34
|
```javascript
|
|
35
|
+
const libFable = require('fable');
|
|
36
|
+
const fable = new libFable({ Product: 'MetaTemplateDemo', ProductVersion: '1.0.0' });
|
|
15
37
|
const metaTemplate = fable.instantiateServiceProvider('MetaTemplate');
|
|
16
38
|
|
|
17
|
-
|
|
18
|
-
|
|
39
|
+
metaTemplate.addPattern('{~', '~}', (pInner, pData) => {
|
|
40
|
+
const value = fable.Utility.getValueByHash(pData, pInner.trim());
|
|
41
|
+
return value == null ? '' : String(value);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const result = metaTemplate.parseString('Hello, {~Name~}!', { Name: 'World' });
|
|
45
|
+
console.log(result); // 'Hello, World!'
|
|
19
46
|
```
|
|
20
47
|
|
|
21
|
-
### Template Syntax
|
|
48
|
+
### Template Syntax (this convention)
|
|
22
49
|
|
|
23
|
-
|
|
50
|
+
With the `{~ ~}` pattern registered above, these all work because
|
|
51
|
+
`fable.Utility.getValueByHash` understands dot/bracket paths:
|
|
24
52
|
|
|
25
53
|
| Pattern | Description | Example |
|
|
26
54
|
|---------|-------------|---------|
|
|
@@ -28,125 +56,185 @@ MetaTemplate uses a different syntax from the standard Template service:
|
|
|
28
56
|
| `{~Object.Property~}` | Nested property | `{~User.Name~}` |
|
|
29
57
|
| `{~Array[0]~}` | Array access | `{~Items[0]~}` |
|
|
30
58
|
|
|
31
|
-
## Advanced Features
|
|
32
|
-
|
|
33
|
-
### Conditional Rendering
|
|
34
|
-
|
|
35
59
|
```javascript
|
|
36
|
-
const
|
|
37
|
-
{
|
|
38
|
-
|
|
39
|
-
{~End:If:ShowGreeting~}
|
|
40
|
-
`;
|
|
60
|
+
const libFable = require('fable');
|
|
61
|
+
const fable = new libFable({ Product: 'MetaTemplateDemo', ProductVersion: '1.0.0' });
|
|
62
|
+
const metaTemplate = fable.instantiateServiceProvider('MetaTemplate');
|
|
41
63
|
|
|
42
|
-
metaTemplate.
|
|
43
|
-
|
|
64
|
+
metaTemplate.addPattern('{~', '~}', (pInner, pData) => {
|
|
65
|
+
const value = fable.Utility.getValueByHash(pData, pInner.trim());
|
|
66
|
+
return value == null ? '' : String(value);
|
|
67
|
+
});
|
|
44
68
|
|
|
45
|
-
|
|
46
|
-
|
|
69
|
+
const data = {
|
|
70
|
+
User: { Name: 'Alice' },
|
|
71
|
+
Items: ['first', 'second', 'third']
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
console.log(metaTemplate.parseString('Name: {~User.Name~}, Item 1: {~Items[0]~}, Item 3: {~Items[2]~}', data));
|
|
47
75
|
```
|
|
48
76
|
|
|
49
|
-
|
|
77
|
+
## Iteration (via parser-function logic)
|
|
78
|
+
|
|
79
|
+
The parser function receives the iterator address and inline body
|
|
80
|
+
separated by `|`, looks up the array, and joins the rendered bodies:
|
|
50
81
|
|
|
51
82
|
```javascript
|
|
52
|
-
const
|
|
53
|
-
{
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
83
|
+
const libFable = require('fable');
|
|
84
|
+
const fable = new libFable({ Product: 'MetaTemplateDemo', ProductVersion: '1.0.0' });
|
|
85
|
+
const metaTemplate = fable.instantiateServiceProvider('MetaTemplate');
|
|
86
|
+
|
|
87
|
+
// {~each ItemsAddress|template-using-{{Record.Field}}~}
|
|
88
|
+
// Use {{ }} inside the body to reference per-row fields; the parser
|
|
89
|
+
// substitutes them per iteration.
|
|
90
|
+
metaTemplate.addPattern('{~each ', '~}', (pInner, pData) => {
|
|
91
|
+
const sepIndex = pInner.indexOf('|');
|
|
92
|
+
const address = pInner.slice(0, sepIndex).trim();
|
|
93
|
+
const body = pInner.slice(sepIndex + 1);
|
|
94
|
+
const list = fable.Utility.getValueByHash(pData, address) || [];
|
|
95
|
+
|
|
96
|
+
return list.map((Record) =>
|
|
97
|
+
body.replace(/\{\{([^}]+)\}\}/g, (_, hash) => {
|
|
98
|
+
const v = fable.Utility.getValueByHash({ Record }, hash.trim());
|
|
99
|
+
return v == null ? '' : String(v);
|
|
100
|
+
})
|
|
101
|
+
).join('');
|
|
102
|
+
});
|
|
57
103
|
|
|
58
104
|
const data = {
|
|
59
105
|
Items: [
|
|
60
|
-
{ Name: 'Apple',
|
|
106
|
+
{ Name: 'Apple', Value: 1.50 },
|
|
61
107
|
{ Name: 'Banana', Value: 0.75 }
|
|
62
108
|
]
|
|
63
109
|
};
|
|
64
110
|
|
|
65
|
-
metaTemplate.
|
|
66
|
-
|
|
67
|
-
// - Apple: 1.50
|
|
68
|
-
// - Banana: 0.75
|
|
111
|
+
const out = metaTemplate.parseString('{~each Items|- {{Record.Name}}: {{Record.Value}}\n~}', data);
|
|
112
|
+
console.log(out);
|
|
69
113
|
```
|
|
70
114
|
|
|
71
|
-
|
|
115
|
+
## Nested Templates (two-pass rendering)
|
|
116
|
+
|
|
117
|
+
`parseString` is sync when no callback is provided. Render the inner
|
|
118
|
+
fragment first, then drop the result into the outer template's data:
|
|
72
119
|
|
|
73
120
|
```javascript
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
</div>
|
|
78
|
-
`;
|
|
121
|
+
const libFable = require('fable');
|
|
122
|
+
const fable = new libFable({ Product: 'MetaTemplateDemo', ProductVersion: '1.0.0' });
|
|
123
|
+
const metaTemplate = fable.instantiateServiceProvider('MetaTemplate');
|
|
79
124
|
|
|
80
|
-
|
|
125
|
+
metaTemplate.addPattern('{~', '~}', (pInner, pData) => {
|
|
126
|
+
const value = fable.Utility.getValueByHash(pData, pInner.trim());
|
|
127
|
+
return value == null ? '' : String(value);
|
|
128
|
+
});
|
|
81
129
|
|
|
82
|
-
|
|
83
|
-
const
|
|
130
|
+
const innerContent = '<p>Hello, {~Name~}!</p>';
|
|
131
|
+
const outerTemplate = '<div class="container">\n{~InnerContent~}\n</div>';
|
|
84
132
|
|
|
85
|
-
|
|
86
|
-
const outer = metaTemplate.
|
|
133
|
+
const inner = metaTemplate.parseString(innerContent, { Name: 'World' });
|
|
134
|
+
const outer = metaTemplate.parseString(outerTemplate, { InnerContent: inner });
|
|
135
|
+
console.log(outer);
|
|
87
136
|
```
|
|
88
137
|
|
|
89
|
-
## Template Inheritance
|
|
138
|
+
## Template Inheritance (compose strings, render once)
|
|
90
139
|
|
|
91
|
-
|
|
140
|
+
There is no native template-inheritance system — the same effect is
|
|
141
|
+
achieved by composing the final template string in JS, then calling
|
|
142
|
+
`parseString` once.
|
|
143
|
+
|
|
144
|
+
### Define a Base Template
|
|
92
145
|
|
|
93
146
|
```javascript
|
|
94
|
-
const baseTemplate =
|
|
95
|
-
<!DOCTYPE html>
|
|
96
|
-
<html>
|
|
97
|
-
<head><title>{~Title~}</title></head>
|
|
98
|
-
<body>
|
|
99
|
-
<header>{~Header~}</header>
|
|
100
|
-
<main>{~Content~}</main>
|
|
101
|
-
<footer>{~Footer~}</footer>
|
|
102
|
-
</body>
|
|
103
|
-
</html>
|
|
104
|
-
|
|
147
|
+
const baseTemplate = [
|
|
148
|
+
'<!DOCTYPE html>',
|
|
149
|
+
'<html>',
|
|
150
|
+
'<head><title>{~Title~}</title></head>',
|
|
151
|
+
'<body>',
|
|
152
|
+
' <header>{~Header~}</header>',
|
|
153
|
+
' <main>{~Content~}</main>',
|
|
154
|
+
' <footer>{~Footer~}</footer>',
|
|
155
|
+
'</body>',
|
|
156
|
+
'</html>'
|
|
157
|
+
].join('\n');
|
|
158
|
+
|
|
159
|
+
console.log('baseTemplate length:', baseTemplate.length, 'chars');
|
|
105
160
|
```
|
|
106
161
|
|
|
107
|
-
###
|
|
162
|
+
### Use the Base Template
|
|
108
163
|
|
|
109
164
|
```javascript
|
|
165
|
+
const libFable = require('fable');
|
|
166
|
+
const fable = new libFable({ Product: 'MetaTemplateDemo', ProductVersion: '1.0.0' });
|
|
167
|
+
const metaTemplate = fable.instantiateServiceProvider('MetaTemplate');
|
|
168
|
+
|
|
169
|
+
metaTemplate.addPattern('{~', '~}', (pInner, pData) => {
|
|
170
|
+
const value = fable.Utility.getValueByHash(pData, pInner.trim());
|
|
171
|
+
return value == null ? '' : String(value);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const baseTemplate = [
|
|
175
|
+
'<!DOCTYPE html>',
|
|
176
|
+
'<html>',
|
|
177
|
+
'<head><title>{~Title~}</title></head>',
|
|
178
|
+
'<body>',
|
|
179
|
+
' <header>{~Header~}</header>',
|
|
180
|
+
' <main>{~Content~}</main>',
|
|
181
|
+
' <footer>{~Footer~}</footer>',
|
|
182
|
+
'</body>',
|
|
183
|
+
'</html>'
|
|
184
|
+
].join('\n');
|
|
185
|
+
|
|
110
186
|
const pageData = {
|
|
111
|
-
Title:
|
|
112
|
-
Header:
|
|
187
|
+
Title: 'My Page',
|
|
188
|
+
Header: '<h1>Welcome</h1>',
|
|
113
189
|
Content: '<p>Main content here</p>',
|
|
114
|
-
Footer:
|
|
190
|
+
Footer: '<p>© 2024</p>'
|
|
115
191
|
};
|
|
116
192
|
|
|
117
|
-
const page = metaTemplate.
|
|
193
|
+
const page = metaTemplate.parseString(baseTemplate, pageData);
|
|
194
|
+
console.log(page);
|
|
118
195
|
```
|
|
119
196
|
|
|
120
197
|
## Use Cases
|
|
121
198
|
|
|
122
|
-
### Email Templates
|
|
199
|
+
### Email Templates (substitution + iteration patterns)
|
|
123
200
|
|
|
124
201
|
```javascript
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
{~Begin:If:HasOrder~}
|
|
129
|
-
Your order #{~Order.Number~} has been {~Order.Status~}.
|
|
130
|
-
|
|
131
|
-
Items:
|
|
132
|
-
{~Begin:Each:Order.Items~}
|
|
133
|
-
- {~Record.Name~} x {~Record.Quantity~}: ${~Record.Price~}
|
|
134
|
-
{~End:Each:Order.Items~}
|
|
135
|
-
|
|
136
|
-
Total: ${~Order.Total~}
|
|
137
|
-
{~End:If:HasOrder~}
|
|
202
|
+
const libFable = require('fable');
|
|
203
|
+
const fable = new libFable({ Product: 'MetaTemplateDemo', ProductVersion: '1.0.0' });
|
|
204
|
+
const metaTemplate = fable.instantiateServiceProvider('MetaTemplate');
|
|
138
205
|
|
|
139
|
-
{~
|
|
140
|
-
|
|
141
|
-
|
|
206
|
+
metaTemplate.addPattern('{~', '~}', (pInner, pData) => {
|
|
207
|
+
const value = fable.Utility.getValueByHash(pData, pInner.trim());
|
|
208
|
+
return value == null ? '' : String(value);
|
|
209
|
+
});
|
|
210
|
+
metaTemplate.addPattern('{~each ', '~}', (pInner, pData) => {
|
|
211
|
+
const sepIndex = pInner.indexOf('|');
|
|
212
|
+
const address = pInner.slice(0, sepIndex).trim();
|
|
213
|
+
const body = pInner.slice(sepIndex + 1);
|
|
214
|
+
const list = fable.Utility.getValueByHash(pData, address) || [];
|
|
215
|
+
return list.map((Record) =>
|
|
216
|
+
body.replace(/\{\{([^}]+)\}\}/g, (_, hash) => {
|
|
217
|
+
const v = fable.Utility.getValueByHash({ Record }, hash.trim());
|
|
218
|
+
return v == null ? '' : String(v);
|
|
219
|
+
})
|
|
220
|
+
).join('');
|
|
221
|
+
});
|
|
142
222
|
|
|
143
|
-
|
|
144
|
-
{~
|
|
145
|
-
|
|
223
|
+
const emailTemplate = [
|
|
224
|
+
'Dear {~Recipient.Name~},',
|
|
225
|
+
'',
|
|
226
|
+
'Your order #{~Order.Number~} has been {~Order.Status~}.',
|
|
227
|
+
'',
|
|
228
|
+
'Items:',
|
|
229
|
+
'{~each Order.Items|- {{Record.Name}} x {{Record.Quantity}}: ${{Record.Price}}\n~}',
|
|
230
|
+
'Total: ${~Order.Total~}',
|
|
231
|
+
'',
|
|
232
|
+
'Best regards,',
|
|
233
|
+
'{~Sender.Name~}'
|
|
234
|
+
].join('\n');
|
|
146
235
|
|
|
147
236
|
const emailData = {
|
|
148
237
|
Recipient: { Name: 'John' },
|
|
149
|
-
HasOrder: true,
|
|
150
238
|
Order: {
|
|
151
239
|
Number: '12345',
|
|
152
240
|
Status: 'shipped',
|
|
@@ -156,113 +244,179 @@ const emailData = {
|
|
|
156
244
|
],
|
|
157
245
|
Total: '109.97'
|
|
158
246
|
},
|
|
159
|
-
IsPromotion: false,
|
|
160
247
|
Sender: { Name: 'Support Team' }
|
|
161
248
|
};
|
|
162
249
|
|
|
163
|
-
|
|
250
|
+
console.log(metaTemplate.parseString(emailTemplate, emailData));
|
|
164
251
|
```
|
|
165
252
|
|
|
166
|
-
### Report Generation
|
|
253
|
+
### Report Generation (substitution + iteration)
|
|
167
254
|
|
|
168
255
|
```javascript
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
{
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
256
|
+
const libFable = require('fable');
|
|
257
|
+
const fable = new libFable({ Product: 'MetaTemplateDemo', ProductVersion: '1.0.0' });
|
|
258
|
+
const metaTemplate = fable.instantiateServiceProvider('MetaTemplate');
|
|
259
|
+
|
|
260
|
+
metaTemplate.addPattern('{~', '~}', (pInner, pData) => {
|
|
261
|
+
const value = fable.Utility.getValueByHash(pData, pInner.trim());
|
|
262
|
+
return value == null ? '' : String(value);
|
|
263
|
+
});
|
|
264
|
+
metaTemplate.addPattern('{~each ', '~}', (pInner, pData) => {
|
|
265
|
+
const sepIndex = pInner.indexOf('|');
|
|
266
|
+
const address = pInner.slice(0, sepIndex).trim();
|
|
267
|
+
const body = pInner.slice(sepIndex + 1);
|
|
268
|
+
const list = fable.Utility.getValueByHash(pData, address) || [];
|
|
269
|
+
return list.map((Record) =>
|
|
270
|
+
body.replace(/\{\{([^}]+)\}\}/g, (_, hash) => {
|
|
271
|
+
const v = fable.Utility.getValueByHash({ Record }, hash.trim());
|
|
272
|
+
return v == null ? '' : String(v);
|
|
273
|
+
})
|
|
274
|
+
).join('');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const reportTemplate = [
|
|
278
|
+
'# {~Report.Title~}',
|
|
279
|
+
'Generated: {~Report.Date~}',
|
|
280
|
+
'',
|
|
281
|
+
'## Summary',
|
|
282
|
+
'- Total Records: {~Summary.TotalRecords~}',
|
|
283
|
+
'- Processed: {~Summary.Processed~}',
|
|
284
|
+
'- Errors: {~Summary.Errors~}',
|
|
285
|
+
'',
|
|
286
|
+
'## Details',
|
|
287
|
+
'{~each Details|### {{Record.Category}}\n{{Record.Description}}\nCount: {{Record.Count}}\n\n~}'
|
|
288
|
+
].join('\n');
|
|
289
|
+
|
|
290
|
+
const reportData = {
|
|
291
|
+
Report: { Title: 'Sales Report', Date: '2024-01-15' },
|
|
292
|
+
Summary: { TotalRecords: 1000, Processed: 980, Errors: 20 },
|
|
293
|
+
Details: [
|
|
294
|
+
{ Category: 'Q1', Description: 'First-quarter results', Count: 240 },
|
|
295
|
+
{ Category: 'Q2', Description: 'Second-quarter results', Count: 260 }
|
|
296
|
+
]
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
console.log(metaTemplate.parseString(reportTemplate, reportData));
|
|
192
300
|
```
|
|
193
301
|
|
|
194
302
|
### Configuration File Generation
|
|
195
303
|
|
|
196
304
|
```javascript
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
{
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
305
|
+
const libFable = require('fable');
|
|
306
|
+
const fable = new libFable({ Product: 'MetaTemplateDemo', ProductVersion: '1.0.0' });
|
|
307
|
+
const metaTemplate = fable.instantiateServiceProvider('MetaTemplate');
|
|
308
|
+
|
|
309
|
+
metaTemplate.addPattern('{~', '~}', (pInner, pData) => {
|
|
310
|
+
const value = fable.Utility.getValueByHash(pData, pInner.trim());
|
|
311
|
+
return value == null ? '' : String(value);
|
|
312
|
+
});
|
|
313
|
+
metaTemplate.addPattern('{~each ', '~}', (pInner, pData) => {
|
|
314
|
+
const sepIndex = pInner.indexOf('|');
|
|
315
|
+
const address = pInner.slice(0, sepIndex).trim();
|
|
316
|
+
const body = pInner.slice(sepIndex + 1);
|
|
317
|
+
const list = fable.Utility.getValueByHash(pData, address) || [];
|
|
318
|
+
return list.map((Record) =>
|
|
319
|
+
body.replace(/\{\{([^}]+)\}\}/g, (_, hash) => {
|
|
320
|
+
const v = fable.Utility.getValueByHash({ Record }, hash.trim());
|
|
321
|
+
return v == null ? '' : String(v);
|
|
322
|
+
})
|
|
323
|
+
).join('');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const configTemplate = [
|
|
327
|
+
'# Application Configuration',
|
|
328
|
+
'# Generated for {~Environment~}',
|
|
329
|
+
'',
|
|
330
|
+
'server:',
|
|
331
|
+
' host: {~Server.Host~}',
|
|
332
|
+
' port: {~Server.Port~}',
|
|
333
|
+
'',
|
|
334
|
+
'database:',
|
|
335
|
+
' host: {~Database.Host~}',
|
|
336
|
+
' name: {~Database.Name~}',
|
|
337
|
+
'',
|
|
338
|
+
'{~each Features|{{Record.Name}}:\n enabled: {{Record.Enabled}}\n\n~}'
|
|
339
|
+
].join('\n');
|
|
340
|
+
|
|
341
|
+
const config = metaTemplate.parseString(configTemplate, {
|
|
219
342
|
Environment: 'production',
|
|
220
|
-
Server:
|
|
221
|
-
Database: { Host: 'db.example.com', Name: 'myapp'
|
|
343
|
+
Server: { Host: '0.0.0.0', Port: 8080 },
|
|
344
|
+
Database: { Host: 'db.example.com', Name: 'myapp' },
|
|
222
345
|
Features: [
|
|
223
346
|
{ Name: 'cache', Enabled: true },
|
|
224
347
|
{ Name: 'debug', Enabled: false }
|
|
225
348
|
]
|
|
226
349
|
});
|
|
350
|
+
|
|
351
|
+
console.log(config);
|
|
227
352
|
```
|
|
228
353
|
|
|
229
354
|
### Dynamic Forms
|
|
230
355
|
|
|
231
356
|
```javascript
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
{~
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
357
|
+
const libFable = require('fable');
|
|
358
|
+
const fable = new libFable({ Product: 'MetaTemplateDemo', ProductVersion: '1.0.0' });
|
|
359
|
+
const metaTemplate = fable.instantiateServiceProvider('MetaTemplate');
|
|
360
|
+
|
|
361
|
+
metaTemplate.addPattern('{~', '~}', (pInner, pData) => {
|
|
362
|
+
const value = fable.Utility.getValueByHash(pData, pInner.trim());
|
|
363
|
+
return value == null ? '' : String(value);
|
|
364
|
+
});
|
|
365
|
+
metaTemplate.addPattern('{~each ', '~}', (pInner, pData) => {
|
|
366
|
+
const sepIndex = pInner.indexOf('|');
|
|
367
|
+
const address = pInner.slice(0, sepIndex).trim();
|
|
368
|
+
const body = pInner.slice(sepIndex + 1);
|
|
369
|
+
const list = fable.Utility.getValueByHash(pData, address) || [];
|
|
370
|
+
return list.map((Record) =>
|
|
371
|
+
body.replace(/\{\{([^}]+)\}\}/g, (_, hash) => {
|
|
372
|
+
const v = fable.Utility.getValueByHash({ Record }, hash.trim());
|
|
373
|
+
return v == null ? '' : String(v);
|
|
374
|
+
})
|
|
375
|
+
).join('');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const formTemplate = [
|
|
379
|
+
'<form action="{~Form.Action~}" method="{~Form.Method~}">',
|
|
380
|
+
'{~each Form.Fields| <div class="field"><label for="{{Record.Id}}">{{Record.Label}}</label><input type="{{Record.Type}}" id="{{Record.Id}}" name="{{Record.Name}}"></div>\n~}',
|
|
381
|
+
' <button type="submit">{~Form.SubmitText~}</button>',
|
|
382
|
+
'</form>'
|
|
383
|
+
].join('\n');
|
|
384
|
+
|
|
385
|
+
console.log(metaTemplate.parseString(formTemplate, {
|
|
386
|
+
Form: {
|
|
387
|
+
Action: '/signup',
|
|
388
|
+
Method: 'POST',
|
|
389
|
+
SubmitText: 'Sign Up',
|
|
390
|
+
Fields: [
|
|
391
|
+
{ Id: 'email', Name: 'email', Label: 'Email', Type: 'email' },
|
|
392
|
+
{ Id: 'name', Name: 'name', Label: 'Name', Type: 'text' }
|
|
393
|
+
]
|
|
394
|
+
}
|
|
395
|
+
}));
|
|
244
396
|
```
|
|
245
397
|
|
|
246
|
-
##
|
|
398
|
+
## Async Mode
|
|
247
399
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
| Loops | JavaScript `for` | `{~Begin:Each~}` |
|
|
253
|
-
| JavaScript execution | Yes | Limited |
|
|
254
|
-
| Use case | Code-heavy templates | Data-driven templates |
|
|
400
|
+
Pass a callback to `parseString` to run asynchronously — parser
|
|
401
|
+
functions can complete on their own schedule (via the WordTree's
|
|
402
|
+
`addPatternBoth` async-parser slot). The sync mode used above is
|
|
403
|
+
selected automatically when no callback is supplied.
|
|
255
404
|
|
|
256
|
-
##
|
|
405
|
+
## Comparison with Template Service
|
|
257
406
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
407
|
+
| Feature | Template (`fable.Utility.template`) | MetaTemplate |
|
|
408
|
+
|---------|-------------------------------------|--------------|
|
|
409
|
+
| Syntax | `<%= %>` / `<% %>` (underscore) | Whatever you register via `addPattern` |
|
|
410
|
+
| Control flow | JavaScript `<% if %>` / `<% for %>` | None built in; write parser-function logic for what you need |
|
|
411
|
+
| JavaScript execution | Yes (compiled function body) | No — pure callbacks |
|
|
412
|
+
| Use case | Code-heavy templates | Custom tag syntaxes, replacement-only templates |
|
|
262
413
|
|
|
263
414
|
## Notes
|
|
264
415
|
|
|
265
|
-
- MetaTemplate
|
|
266
|
-
-
|
|
267
|
-
-
|
|
268
|
-
-
|
|
416
|
+
- MetaTemplate has no built-in tag vocabulary; everything is `addPattern`.
|
|
417
|
+
- `parseString` is synchronous unless you pass a callback.
|
|
418
|
+
- The `Record` variable convention used in the iteration examples above
|
|
419
|
+
is a JS-side helper inside the iteration parser function — there is
|
|
420
|
+
nothing in MetaTemplate that names it.
|
|
421
|
+
- Keep end markers short (1–2 characters); the WordTree match is built
|
|
422
|
+
around that.
|