@halleyassist/rule-templater 0.0.1 → 0.0.4
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/README.md +231 -231
- package/dist/rule-templater.browser.js +2453 -0
- package/index.d.ts +91 -91
- package/index.js +0 -0
- package/package.json +47 -47
- package/src/RuleTemplate.ebnf.js +34 -27
- package/src/RuleTemplate.production.ebnf.js +1 -0
- package/src/RuleTemplater.browser.js +0 -0
- package/src/RuleTemplater.js +348 -339
- package/src/RuleTemplater.production.js +349 -0
- package/src/TemplateFilters.js +62 -62
package/src/RuleTemplater.js
CHANGED
|
@@ -1,340 +1,349 @@
|
|
|
1
|
-
// Note: We import the internal RuleParser.ebnf to extend the grammar with template rules.
|
|
2
|
-
// This creates coupling to the internal structure of @halleyassist/rule-parser.
|
|
3
|
-
const RuleParserRules = require('@halleyassist/rule-parser/src/RuleParser.ebnf'),
|
|
4
|
-
TemplateGrammar = require('./RuleTemplate.ebnf'),
|
|
5
|
-
TemplateFilters = require('./TemplateFilters'),
|
|
6
|
-
{Parser} = require('ebnf');
|
|
7
|
-
|
|
8
|
-
let ParserCache = null;
|
|
9
|
-
|
|
10
|
-
const VariableTypes = [
|
|
11
|
-
'string',
|
|
12
|
-
'number',
|
|
13
|
-
'boolean',
|
|
14
|
-
'object',
|
|
15
|
-
'time period',
|
|
16
|
-
'time value',
|
|
17
|
-
'string array',
|
|
18
|
-
'number array',
|
|
19
|
-
'boolean array',
|
|
20
|
-
'object array'
|
|
21
|
-
]
|
|
22
|
-
|
|
23
|
-
const AllowedTypeMapping = {
|
|
24
|
-
'string': ['string_atom', 'string_concat'],
|
|
25
|
-
'number': ['number_atom', 'math_expr'],
|
|
26
|
-
'boolean': ['boolean_atom', 'boolean_expr'],
|
|
27
|
-
'time period': ['time_period_atom'],
|
|
28
|
-
'time value': ['time_value_atom', 'tod_atom'],
|
|
29
|
-
'string array': ['string_array'],
|
|
30
|
-
'number array': ['number_array'],
|
|
31
|
-
'boolean array': ['boolean_array'],
|
|
32
|
-
'object': ['object_atom'],
|
|
33
|
-
'object array': ['object_array']
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// Merge the base grammar with template-specific grammar rules
|
|
37
|
-
const extendedGrammar = [...RuleParserRules]
|
|
38
|
-
for(const rule of TemplateGrammar){
|
|
39
|
-
const idx = extendedGrammar.findIndex(r => r.name === rule.name);
|
|
40
|
-
if(idx !== -1){
|
|
41
|
-
extendedGrammar[idx] = rule;
|
|
42
|
-
} else {
|
|
43
|
-
extendedGrammar.push(rule);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Add template_value as an alternative to value_atom so templates can be parsed
|
|
48
|
-
const valueAtomIdx = extendedGrammar.findIndex(r => r.name === 'value_atom');
|
|
49
|
-
if (valueAtomIdx !== -1) {
|
|
50
|
-
extendedGrammar[valueAtomIdx].bnf.push(['template_value']);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Export the parser rules for potential external use
|
|
54
|
-
const ParserRules = extendedGrammar;
|
|
55
|
-
|
|
56
|
-
class RuleTemplate {
|
|
57
|
-
constructor(ruleTemplateText, ast) {
|
|
58
|
-
this.ruleTemplateText = ruleTemplateText;
|
|
59
|
-
this.ast = ast;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Parse a rule template string and return a RuleTemplate instance
|
|
64
|
-
* @param {string} ruleTemplate - The template string to parse
|
|
65
|
-
* @returns {RuleTemplate} Instance with AST and template text
|
|
66
|
-
*/
|
|
67
|
-
static parse(ruleTemplate){
|
|
68
|
-
if(!ParserCache){
|
|
69
|
-
ParserCache = new Parser(ParserRules, {debug: false})
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const ast = ParserCache.getAST(ruleTemplate.trim(), 'statement_main');
|
|
73
|
-
return new RuleTemplate(ruleTemplate, ast);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Extract variables from the template using the AST
|
|
78
|
-
* @returns {Array} Array of {name, filters: []} objects
|
|
79
|
-
*/
|
|
80
|
-
extractVariables(){
|
|
81
|
-
const variables = [];
|
|
82
|
-
const seen = new Set();
|
|
83
|
-
|
|
84
|
-
const traverse = (node) => {
|
|
85
|
-
if (!node) return;
|
|
86
|
-
|
|
87
|
-
// Check if this is a template_value node
|
|
88
|
-
if (node.type === 'template_value') {
|
|
89
|
-
// Extract the variable information
|
|
90
|
-
const varInfo = this._extractVariableFromNode(node);
|
|
91
|
-
if (varInfo && !seen.has(varInfo.name)) {
|
|
92
|
-
seen.add(varInfo.name);
|
|
93
|
-
variables.push(varInfo);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Traverse children
|
|
98
|
-
if (node.children) {
|
|
99
|
-
for (const child of node.children) {
|
|
100
|
-
traverse(child);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
traverse(this.ast);
|
|
106
|
-
return variables;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Extract variable name and filters from a template_value AST node
|
|
111
|
-
* @private
|
|
112
|
-
*/
|
|
113
|
-
_extractVariableFromNode(node) {
|
|
114
|
-
if (node.type !== 'template_value') return null;
|
|
115
|
-
|
|
116
|
-
// Find the template_expr child
|
|
117
|
-
const templateExpr = node.children?.find(c => c.type === 'template_expr');
|
|
118
|
-
if (!templateExpr) return null;
|
|
119
|
-
|
|
120
|
-
// Extract the path (variable name)
|
|
121
|
-
const templatePath = templateExpr.children?.find(c => c.type === 'template_path');
|
|
122
|
-
if (!templatePath || !templatePath.text) return null;
|
|
123
|
-
|
|
124
|
-
const name = templatePath.text.trim();
|
|
125
|
-
|
|
126
|
-
// Extract filters
|
|
127
|
-
const filters = [];
|
|
128
|
-
for (const child of templateExpr.children || []) {
|
|
129
|
-
if (child.type === 'template_filter_call') {
|
|
130
|
-
const filterName = this._extractFilterName(child);
|
|
131
|
-
if (filterName) {
|
|
132
|
-
filters.push(filterName);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return { name, filters };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Extract filter name from template_filter_call node
|
|
142
|
-
* @private
|
|
143
|
-
*/
|
|
144
|
-
_extractFilterName(node) {
|
|
145
|
-
const filterNameNode = node.children?.find(c => c.type === 'template_filter_name');
|
|
146
|
-
if (!filterNameNode || !filterNameNode.text) return null;
|
|
147
|
-
|
|
148
|
-
return filterNameNode.text.trim();
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Validate variable types against the AST
|
|
153
|
-
* @param {Object} variables - Object mapping variable names to {type} objects
|
|
154
|
-
* @returns {Object} Object with validation results: {valid: boolean, errors: []}
|
|
155
|
-
*/
|
|
156
|
-
validate(variables) {
|
|
157
|
-
if (!variables || typeof variables !== 'object') {
|
|
158
|
-
return {
|
|
159
|
-
valid: false,
|
|
160
|
-
errors: ['Variables must be provided as an object']
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const errors = [];
|
|
165
|
-
const extractedVars = this.extractVariables();
|
|
166
|
-
|
|
167
|
-
for (const varInfo of extractedVars) {
|
|
168
|
-
const varName = varInfo.name;
|
|
169
|
-
|
|
170
|
-
// Check if variable is provided
|
|
171
|
-
if (!variables.hasOwnProperty(varName)) {
|
|
172
|
-
errors.push(`Variable '${varName}' not provided in variables object`);
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const varData = variables[varName];
|
|
177
|
-
if (typeof varData !== 'object') {
|
|
178
|
-
errors.push(`Variable '${varName}' must be an object`);
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const { type } = varData;
|
|
183
|
-
|
|
184
|
-
// Validate type if provided
|
|
185
|
-
if (type && !VariableTypes.includes(type)) {
|
|
186
|
-
errors.push(`Invalid variable type '${type}' for variable '${varName}'`);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return {
|
|
191
|
-
valid: errors.length === 0,
|
|
192
|
-
errors
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Prepare the template by replacing variables with their values
|
|
198
|
-
* Rebuilds from AST by iterating through children
|
|
199
|
-
* @param {Object} variables - Object mapping variable names to {value, type} objects
|
|
200
|
-
* @returns {string} The prepared rule string
|
|
201
|
-
*/
|
|
202
|
-
prepare(variables){
|
|
203
|
-
if (!variables || typeof variables !== 'object') {
|
|
204
|
-
throw new Error('Variables must be provided as an object');
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Rebuild the rule string from AST
|
|
208
|
-
return this._rebuildFromAST(this.ast, variables);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Rebuild rule string from AST node, replacing template_value nodes with variable values
|
|
213
|
-
* @private
|
|
214
|
-
* @param {Object} node - AST node
|
|
215
|
-
* @param {Object} variables - Object mapping variable names to {value, type} objects
|
|
216
|
-
* @returns {string} Rebuilt string
|
|
217
|
-
*/
|
|
218
|
-
_rebuildFromAST(node, variables) {
|
|
219
|
-
if (!node) return '';
|
|
220
|
-
|
|
221
|
-
// If this is a template_value node, replace it with the computed value
|
|
222
|
-
if (node.type === 'template_value') {
|
|
223
|
-
return this._computeTemplateReplacement(node, variables);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// If node has no children, it's a leaf - return its text
|
|
227
|
-
if (!node.children || node.children.length === 0) {
|
|
228
|
-
return node.text || '';
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Node has children - rebuild by iterating through children and preserving gaps
|
|
232
|
-
let result = '';
|
|
233
|
-
const originalText = node.text || '';
|
|
234
|
-
let lastEnd = node.start || 0;
|
|
235
|
-
|
|
236
|
-
for (const child of node.children) {
|
|
237
|
-
// Add any text between the last child's end and this child's start (gaps/syntax)
|
|
238
|
-
if (child.start !== undefined && child.start > lastEnd) {
|
|
239
|
-
result += originalText.substring(lastEnd - (node.start || 0), child.start - (node.start || 0));
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Add the child's rebuilt text
|
|
243
|
-
result += this._rebuildFromAST(child, variables);
|
|
244
|
-
|
|
245
|
-
// Update lastEnd to this child's end position
|
|
246
|
-
if (child.end !== undefined) {
|
|
247
|
-
lastEnd = child.end;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Add any remaining text after the last child
|
|
252
|
-
if (node.end !== undefined && lastEnd < node.end) {
|
|
253
|
-
result += originalText.substring(lastEnd - (node.start || 0), node.end - (node.start || 0));
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return result;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Compute the replacement value for a template_value node
|
|
261
|
-
* @private
|
|
262
|
-
* @param {Object} node - template_value AST node
|
|
263
|
-
* @param {Object} variables - Object mapping variable names to {value, type} objects
|
|
264
|
-
* @returns {string} Replacement string
|
|
265
|
-
*/
|
|
266
|
-
_computeTemplateReplacement(node, variables) {
|
|
267
|
-
const templateInfo = this._extractVariableFromNode(node);
|
|
268
|
-
if (!templateInfo) {
|
|
269
|
-
throw new Error(`Failed to extract variable information from template node`);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const varName = templateInfo.name;
|
|
273
|
-
|
|
274
|
-
// Validate variable is provided
|
|
275
|
-
if (!variables.hasOwnProperty(varName)) {
|
|
276
|
-
throw new Error(`Variable '${varName}' not provided in variables object`);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const varData = variables[varName];
|
|
280
|
-
if (typeof varData !== 'object' || !varData.hasOwnProperty('value')) {
|
|
281
|
-
throw new Error(`Variable '${varName}' must be an object with 'value' property`);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
let { value, type } = varData;
|
|
285
|
-
|
|
286
|
-
//
|
|
287
|
-
if (
|
|
288
|
-
throw new Error(`
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
//
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
1
|
+
// Note: We import the internal RuleParser.ebnf to extend the grammar with template rules.
|
|
2
|
+
// This creates coupling to the internal structure of @halleyassist/rule-parser.
|
|
3
|
+
const RuleParserRules = require('@halleyassist/rule-parser/src/RuleParser.ebnf'),
|
|
4
|
+
TemplateGrammar = require('./RuleTemplate.ebnf'),
|
|
5
|
+
TemplateFilters = require('./TemplateFilters'),
|
|
6
|
+
{Parser} = require('ebnf');
|
|
7
|
+
|
|
8
|
+
let ParserCache = null;
|
|
9
|
+
|
|
10
|
+
const VariableTypes = [
|
|
11
|
+
'string',
|
|
12
|
+
'number',
|
|
13
|
+
'boolean',
|
|
14
|
+
'object',
|
|
15
|
+
'time period',
|
|
16
|
+
'time value',
|
|
17
|
+
'string array',
|
|
18
|
+
'number array',
|
|
19
|
+
'boolean array',
|
|
20
|
+
'object array'
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
const AllowedTypeMapping = {
|
|
24
|
+
'string': ['string_atom', 'string_concat'],
|
|
25
|
+
'number': ['number_atom', 'math_expr'],
|
|
26
|
+
'boolean': ['boolean_atom', 'boolean_expr'],
|
|
27
|
+
'time period': ['time_period_atom'],
|
|
28
|
+
'time value': ['time_value_atom', 'tod_atom'],
|
|
29
|
+
'string array': ['string_array'],
|
|
30
|
+
'number array': ['number_array'],
|
|
31
|
+
'boolean array': ['boolean_array'],
|
|
32
|
+
'object': ['object_atom'],
|
|
33
|
+
'object array': ['object_array']
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Merge the base grammar with template-specific grammar rules
|
|
37
|
+
const extendedGrammar = [...RuleParserRules]
|
|
38
|
+
for(const rule of TemplateGrammar){
|
|
39
|
+
const idx = extendedGrammar.findIndex(r => r.name === rule.name);
|
|
40
|
+
if(idx !== -1){
|
|
41
|
+
extendedGrammar[idx] = rule;
|
|
42
|
+
} else {
|
|
43
|
+
extendedGrammar.push(rule);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Add template_value as an alternative to value_atom so templates can be parsed
|
|
48
|
+
const valueAtomIdx = extendedGrammar.findIndex(r => r.name === 'value_atom');
|
|
49
|
+
if (valueAtomIdx !== -1) {
|
|
50
|
+
extendedGrammar[valueAtomIdx].bnf.push(['template_value']);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Export the parser rules for potential external use
|
|
54
|
+
const ParserRules = extendedGrammar;
|
|
55
|
+
|
|
56
|
+
class RuleTemplate {
|
|
57
|
+
constructor(ruleTemplateText, ast) {
|
|
58
|
+
this.ruleTemplateText = ruleTemplateText;
|
|
59
|
+
this.ast = ast;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse a rule template string and return a RuleTemplate instance
|
|
64
|
+
* @param {string} ruleTemplate - The template string to parse
|
|
65
|
+
* @returns {RuleTemplate} Instance with AST and template text
|
|
66
|
+
*/
|
|
67
|
+
static parse(ruleTemplate){
|
|
68
|
+
if(!ParserCache){
|
|
69
|
+
ParserCache = new Parser(ParserRules, {debug: false})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const ast = ParserCache.getAST(ruleTemplate.trim(), 'statement_main');
|
|
73
|
+
return new RuleTemplate(ruleTemplate, ast);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Extract variables from the template using the AST
|
|
78
|
+
* @returns {Array} Array of {name, filters: []} objects
|
|
79
|
+
*/
|
|
80
|
+
extractVariables(){
|
|
81
|
+
const variables = [];
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
|
|
84
|
+
const traverse = (node) => {
|
|
85
|
+
if (!node) return;
|
|
86
|
+
|
|
87
|
+
// Check if this is a template_value node
|
|
88
|
+
if (node.type === 'template_value') {
|
|
89
|
+
// Extract the variable information
|
|
90
|
+
const varInfo = this._extractVariableFromNode(node);
|
|
91
|
+
if (varInfo && !seen.has(varInfo.name)) {
|
|
92
|
+
seen.add(varInfo.name);
|
|
93
|
+
variables.push(varInfo);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Traverse children
|
|
98
|
+
if (node.children) {
|
|
99
|
+
for (const child of node.children) {
|
|
100
|
+
traverse(child);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
traverse(this.ast);
|
|
106
|
+
return variables;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Extract variable name and filters from a template_value AST node
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
_extractVariableFromNode(node) {
|
|
114
|
+
if (node.type !== 'template_value') return null;
|
|
115
|
+
|
|
116
|
+
// Find the template_expr child
|
|
117
|
+
const templateExpr = node.children?.find(c => c.type === 'template_expr');
|
|
118
|
+
if (!templateExpr) return null;
|
|
119
|
+
|
|
120
|
+
// Extract the path (variable name)
|
|
121
|
+
const templatePath = templateExpr.children?.find(c => c.type === 'template_path');
|
|
122
|
+
if (!templatePath || !templatePath.text) return null;
|
|
123
|
+
|
|
124
|
+
const name = templatePath.text.trim();
|
|
125
|
+
|
|
126
|
+
// Extract filters
|
|
127
|
+
const filters = [];
|
|
128
|
+
for (const child of templateExpr.children || []) {
|
|
129
|
+
if (child.type === 'template_filter_call') {
|
|
130
|
+
const filterName = this._extractFilterName(child);
|
|
131
|
+
if (filterName) {
|
|
132
|
+
filters.push(filterName);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { name, filters };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Extract filter name from template_filter_call node
|
|
142
|
+
* @private
|
|
143
|
+
*/
|
|
144
|
+
_extractFilterName(node) {
|
|
145
|
+
const filterNameNode = node.children?.find(c => c.type === 'template_filter_name');
|
|
146
|
+
if (!filterNameNode || !filterNameNode.text) return null;
|
|
147
|
+
|
|
148
|
+
return filterNameNode.text.trim();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Validate variable types against the AST
|
|
153
|
+
* @param {Object} variables - Object mapping variable names to {type} objects
|
|
154
|
+
* @returns {Object} Object with validation results: {valid: boolean, errors: []}
|
|
155
|
+
*/
|
|
156
|
+
validate(variables) {
|
|
157
|
+
if (!variables || typeof variables !== 'object') {
|
|
158
|
+
return {
|
|
159
|
+
valid: false,
|
|
160
|
+
errors: ['Variables must be provided as an object']
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const errors = [];
|
|
165
|
+
const extractedVars = this.extractVariables();
|
|
166
|
+
|
|
167
|
+
for (const varInfo of extractedVars) {
|
|
168
|
+
const varName = varInfo.name;
|
|
169
|
+
|
|
170
|
+
// Check if variable is provided
|
|
171
|
+
if (!variables.hasOwnProperty(varName)) {
|
|
172
|
+
errors.push(`Variable '${varName}' not provided in variables object`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const varData = variables[varName];
|
|
177
|
+
if (typeof varData !== 'object') {
|
|
178
|
+
errors.push(`Variable '${varName}' must be an object`);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const { type } = varData;
|
|
183
|
+
|
|
184
|
+
// Validate type if provided
|
|
185
|
+
if (type && !VariableTypes.includes(type)) {
|
|
186
|
+
errors.push(`Invalid variable type '${type}' for variable '${varName}'`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
valid: errors.length === 0,
|
|
192
|
+
errors
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Prepare the template by replacing variables with their values
|
|
198
|
+
* Rebuilds from AST by iterating through children
|
|
199
|
+
* @param {Object} variables - Object mapping variable names to {value, type} objects
|
|
200
|
+
* @returns {string} The prepared rule string
|
|
201
|
+
*/
|
|
202
|
+
prepare(variables){
|
|
203
|
+
if (!variables || typeof variables !== 'object') {
|
|
204
|
+
throw new Error('Variables must be provided as an object');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Rebuild the rule string from AST
|
|
208
|
+
return this._rebuildFromAST(this.ast, variables);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Rebuild rule string from AST node, replacing template_value nodes with variable values
|
|
213
|
+
* @private
|
|
214
|
+
* @param {Object} node - AST node
|
|
215
|
+
* @param {Object} variables - Object mapping variable names to {value, type} objects
|
|
216
|
+
* @returns {string} Rebuilt string
|
|
217
|
+
*/
|
|
218
|
+
_rebuildFromAST(node, variables) {
|
|
219
|
+
if (!node) return '';
|
|
220
|
+
|
|
221
|
+
// If this is a template_value node, replace it with the computed value
|
|
222
|
+
if (node.type === 'template_value') {
|
|
223
|
+
return this._computeTemplateReplacement(node, variables);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// If node has no children, it's a leaf - return its text
|
|
227
|
+
if (!node.children || node.children.length === 0) {
|
|
228
|
+
return node.text || '';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Node has children - rebuild by iterating through children and preserving gaps
|
|
232
|
+
let result = '';
|
|
233
|
+
const originalText = node.text || '';
|
|
234
|
+
let lastEnd = node.start || 0;
|
|
235
|
+
|
|
236
|
+
for (const child of node.children) {
|
|
237
|
+
// Add any text between the last child's end and this child's start (gaps/syntax)
|
|
238
|
+
if (child.start !== undefined && child.start > lastEnd) {
|
|
239
|
+
result += originalText.substring(lastEnd - (node.start || 0), child.start - (node.start || 0));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Add the child's rebuilt text
|
|
243
|
+
result += this._rebuildFromAST(child, variables);
|
|
244
|
+
|
|
245
|
+
// Update lastEnd to this child's end position
|
|
246
|
+
if (child.end !== undefined) {
|
|
247
|
+
lastEnd = child.end;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Add any remaining text after the last child
|
|
252
|
+
if (node.end !== undefined && lastEnd < node.end) {
|
|
253
|
+
result += originalText.substring(lastEnd - (node.start || 0), node.end - (node.start || 0));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Compute the replacement value for a template_value node
|
|
261
|
+
* @private
|
|
262
|
+
* @param {Object} node - template_value AST node
|
|
263
|
+
* @param {Object} variables - Object mapping variable names to {value, type} objects
|
|
264
|
+
* @returns {string} Replacement string
|
|
265
|
+
*/
|
|
266
|
+
_computeTemplateReplacement(node, variables) {
|
|
267
|
+
const templateInfo = this._extractVariableFromNode(node);
|
|
268
|
+
if (!templateInfo) {
|
|
269
|
+
throw new Error(`Failed to extract variable information from template node`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const varName = templateInfo.name;
|
|
273
|
+
|
|
274
|
+
// Validate variable is provided
|
|
275
|
+
if (!variables.hasOwnProperty(varName)) {
|
|
276
|
+
throw new Error(`Variable '${varName}' not provided in variables object`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const varData = variables[varName];
|
|
280
|
+
if (typeof varData !== 'object' || !varData.hasOwnProperty('value')) {
|
|
281
|
+
throw new Error(`Variable '${varName}' must be an object with 'value' property`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let { value, type } = varData;
|
|
285
|
+
|
|
286
|
+
// Require type property for all variables
|
|
287
|
+
if (!varData.hasOwnProperty('type')) {
|
|
288
|
+
throw new Error(`Variable '${varName}' must have a 'type' property`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Validate type
|
|
292
|
+
if (!VariableTypes.includes(type)) {
|
|
293
|
+
throw new Error(`Invalid variable type '${type}' for variable '${varName}'`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Apply filters if present
|
|
297
|
+
if (templateInfo.filters && templateInfo.filters.length > 0) {
|
|
298
|
+
for (const filterName of templateInfo.filters) {
|
|
299
|
+
if (!TemplateFilters[filterName]) {
|
|
300
|
+
throw new Error(`Unknown filter '${filterName}'`);
|
|
301
|
+
}
|
|
302
|
+
value = TemplateFilters[filterName](value);
|
|
303
|
+
}
|
|
304
|
+
// After applying filters, the result is already a string representation
|
|
305
|
+
return String(value);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Convert value to string representation based on type
|
|
309
|
+
if (type === 'string') {
|
|
310
|
+
// Escape backslashes first, then quotes in string values.
|
|
311
|
+
// Order is critical: escaping backslashes first prevents double-escaping.
|
|
312
|
+
// E.g., "test\" becomes "test\\" then "test\\\"" (correct)
|
|
313
|
+
// If reversed, "test\" would become "test\\"" then "test\\\\"" (incorrect)
|
|
314
|
+
return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
315
|
+
} else if (type === 'number') {
|
|
316
|
+
return String(value);
|
|
317
|
+
} else if (type === 'boolean') {
|
|
318
|
+
return value ? 'true' : 'false';
|
|
319
|
+
} else {
|
|
320
|
+
// Default behavior - just insert the value as-is
|
|
321
|
+
return String(value);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Helper method to validate if an AST node matches a variable type
|
|
327
|
+
* @param {Object} astNode - The AST node to validate
|
|
328
|
+
* @param {string} variableType - The expected variable type
|
|
329
|
+
* @returns {boolean} True if valid, false otherwise
|
|
330
|
+
*/
|
|
331
|
+
static validateVariableNode(astNode, variableType) {
|
|
332
|
+
if (!astNode || !astNode.type) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const allowedTypes = AllowedTypeMapping[variableType];
|
|
337
|
+
if (!allowedTypes) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return allowedTypes.includes(astNode.type);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Export the class and parser rules
|
|
346
|
+
module.exports = RuleTemplate;
|
|
347
|
+
module.exports.ParserRules = ParserRules;
|
|
348
|
+
module.exports.VariableTypes = VariableTypes;
|
|
340
349
|
module.exports.TemplateFilters = TemplateFilters;
|