-
Notifications
You must be signed in to change notification settings - Fork 24
/
TemplateParser.cs
451 lines (384 loc) · 20.2 KB
/
TemplateParser.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
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
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
using System;
using System.Collections.Generic;
using System.Linq;
using Sprache;
#if NET462
#else
using System.Diagnostics.CodeAnalysis;
#endif
namespace Octostache.Templates
{
public static class TemplateParser
{
static readonly Parser<Identifier> Identifier = Parse
.Char(c => char.IsLetter(c) || char.IsDigit(c) || char.IsWhiteSpace(c) || c == '_' || c == '-' || c == ':' || c == '/' || c == '~' || c == '(' || c == ')', "identifier")
.Except(Parse.WhiteSpace.FollowedBy("|"))
.Except(Parse.WhiteSpace.FollowedBy("}"))
.ExceptWhiteSpaceBeforeKeyword()
.AtLeastOnce()
.Text()
.Select(s => new Identifier(s.Trim()))
.WithPosition();
static readonly Parser<Identifier> IdentifierWithoutWhitespace = Parse
.Char(c => char.IsLetter(c) || char.IsDigit(c) || c == '_' || c == '-' || c == ':' || c == '/' || c == '~' || c == '(' || c == ')', "identifier")
.Except(Parse.WhiteSpace.FollowedBy("|"))
.Except(Parse.WhiteSpace.FollowedBy("}"))
.ExceptWhiteSpaceBeforeKeyword()
.AtLeastOnce()
.Text()
.Select(s => new Identifier(s.Trim()))
.WithPosition();
static readonly Parser<string> LDelim = Parse.String("#{").Except(Parse.String("#{/")).Text();
static readonly Parser<string> RDelim = Parse.String("}").Text();
static readonly Parser<SubstitutionToken> Substitution =
(from leftDelim in LDelim
from expression in Expression.Token()
from rightDelim in RDelim
select new SubstitutionToken(expression))
.WithPosition();
static readonly Parser<Indexer> SymbolIndexer =
(from index in Substitution.Token()
where index.Expression is SymbolExpression
select new Indexer((SymbolExpression) index.Expression))
.WithPosition();
// Parsing the string of the Index, recursively parse any nested index in the string.
// Eg: "Package[containers[0].container].Registry"
static readonly Parser<Indexer> StringIndexer =
from open in Parse.Char('[')
from parts in (from indexer in StringIndexer
select indexer.ToString())
.Or(Parse.CharExcept(new[] { ']', '[' }).AtLeastOnce().Text())
.Many()
.Token()
from close in Parse.Char(']')
select new Indexer(string.Join("", parts));
static readonly Parser<Indexer> Indexer =
(from index in
(from open in Parse.Char('[')
from index in SymbolIndexer.Token()
from close in Parse.Char(']')
select index) // NonEmpty Symbol Index
.Or(from index in StringIndexer
select index)
.WithPosition() // NonEmpty String Index
.Or(from open in Parse.Char('[')
from close in Parse.Char(']')
select new Indexer(string.Empty)) //Empty Index
select index)
.WithPosition()
.Named("indexer");
static readonly Parser<SymbolExpressionStep> TrailingStep =
Parse.Char('.').Then(_ => Identifier).Select(i => (SymbolExpressionStep) i)
.XOr(Indexer);
static readonly Parser<SymbolExpression> Symbol =
(from first in Identifier
from rest in TrailingStep.Many()
select new SymbolExpression(new[] { first }.Concat(rest)))
.WithPosition();
// Some trickery applied here to prevent a left-recursive definition
static readonly Parser<FunctionCallExpression> FilterChain =
from symbol in Symbol.Token().Optional().Select(s => s.IsDefined ? s.Get() : new SymbolExpression(new SymbolExpressionStep[] { }))
from chain in Parse.Char('|').Then(_ =>
from fn in IdentifierWithoutWhitespace.Named("filter").WithPosition().Token()
from option in
Conditional.Select(t => (TemplateToken) t)
.Or(Calculation)
.Or(Repetition)
.Or(Substitution)
.Or(IdentifierWithoutWhitespace.Token().Select(t => t.Text)
.Or(QuotedText)
.Or(EscapedQuotedText)
.Select(t => new TextToken(t)))
.Named("option").Many().Optional()
select new { Function = fn.Text, options = option }
).AtLeastOnce()
select (FunctionCallExpression) chain.Aggregate((ContentExpression) symbol,
(leftToken, fn) => new FunctionCallExpression(true, fn.Function, leftToken, fn.options.Get().ToArray()));
static readonly Parser<ContentExpression> Expression =
FilterChain.Select(c => (ContentExpression) c)
.Or(Symbol);
static readonly Parser<ConditionalToken> Conditional =
(from leftDelim in LDelim
from sp1 in Parse.WhiteSpace.Many()
from kw in Keyword("if").Or(Keyword("unless"))
from sp in Parse.WhiteSpace.AtLeastOnce()
from expression in ExpressionsMatch.Token().Or(LeftStringMatch.Token()).Or(RightStringMatch.Token()).Or(TruthyMatch.Token())
from sp2 in Parse.WhiteSpace.Many()
from rightDelim in RDelim
from truthy in Parse.Ref(() => IfTemplate)
from elseMatch in
(from el in Parse.String("#{else}")
from template in Parse.Ref(() => Template)
select template).Optional()
from end in Parse.String("#{/" + kw + "}")
let falsey = elseMatch.IsDefined ? elseMatch.Get() : Enumerable.Empty<TemplateToken>()
select kw == "if" ? new ConditionalToken(expression, truthy, falsey) : new ConditionalToken(expression, falsey, truthy))
.WithPosition();
static readonly Parser<CalculationToken> Calculation =
(from leftDelim in LDelim
from lsp in Parse.WhiteSpace.Many()
from kw in Keyword("calc")
from sp in Parse.WhiteSpace.AtLeastOnce()
from expression in Parse.Ref(() => CalculationExpression)
from rsp in Parse.WhiteSpace.Many()
from rightDelim in RDelim
select new CalculationToken(expression))
.WithPosition();
static readonly Parser<ICalculationComponent> CalculationConstant =
from number in Parse.Decimal.Select(double.Parse)
select new CalculationConstant(number);
// As "/" and "-" operators are also valid characters for identifiers - there are times people
// may want to wrap their variable names inside a calc block to avoid operator conflict
static readonly Parser<ICalculationComponent> WrappedCalculationVariable =
from leftDelim in Parse.String("{")
from lsp in Parse.WhiteSpace.Many()
from symbol in Symbol.Token()
from rsp in Parse.WhiteSpace.Many()
from rightDelim in RDelim
select new CalculationVariable(symbol);
static readonly Parser<ICalculationComponent> CalculationVariable =
from symbol in Symbol.Token()
select new CalculationVariable(symbol);
static readonly Parser<ICalculationComponent> CalculationValue =
CalculationConstant.XOr(WrappedCalculationVariable).XOr(CalculationVariable);
static readonly Parser<CalculationOperator> Add = CalculationOperator("+", Templates.CalculationOperator.Add);
static readonly Parser<CalculationOperator> Subtract = CalculationOperator("-", Templates.CalculationOperator.Subtract);
static readonly Parser<CalculationOperator> Multiply = CalculationOperator("*", Templates.CalculationOperator.Multiply);
static readonly Parser<CalculationOperator> Divide = CalculationOperator("/", Templates.CalculationOperator.Divide);
static readonly Parser<ICalculationComponent> CalculationFactor =
(from lparen in Parse.Char('(')
from expr in Parse.Ref(() => CalculationExpression)
from rparen in Parse.Char(')')
select expr).Named("expression")
.XOr(CalculationValue);
static readonly Parser<ICalculationComponent> CalculationTerm =
Parse.ChainOperator(Multiply.Or(Divide), CalculationFactor, (op, left, right) => new CalculationOperation(left, op, right));
static readonly Parser<ICalculationComponent> CalculationExpression =
Parse.ChainOperator(Add.Or(Subtract), CalculationTerm, (op, left, right) => new CalculationOperation(left, op, right));
static readonly Parser<ConditionalExpressionToken> TruthyMatch =
(from expression in Expression.Token()
select new ConditionalExpressionToken(expression))
.WithPosition();
static readonly Parser<ConditionalExpressionToken> ExpressionsMatch =
(from expression in Expression.Token()
from eq in Keyword("==").Token().Or(Keyword("!=").Token())
from compareTo in Expression.Token()
let isEq = eq == "=="
select new ConditionalSymbolExpressionToken(expression, isEq, compareTo))
.WithPosition();
static readonly Parser<string> QuotedText =
(from open in Parse.Char('"')
from content in Parse.CharExcept(new[] { '"', '#' }).Many().Text()
from close in Parse.Char('"')
select content).Token();
static readonly Parser<string> EscapedQuotedText =
(from open in Parse.String("\\\"")
from content in Parse.AnyChar.Until(Parse.String("\\\"")).Text()
select content).Token();
static readonly Parser<ConditionalExpressionToken> LeftStringMatch =
(from compareTo in QuotedText.Token().Or(EscapedQuotedText.Token())
from eq in Keyword("==").Token().Or(Keyword("!=").Token())
from expression in Expression.Token()
let isEq = eq == "=="
select new ConditionalStringExpressionToken(expression, isEq, compareTo))
.WithPosition();
static readonly Parser<ConditionalExpressionToken> RightStringMatch =
(from expression in Expression.Token()
from eq in Keyword("==").Token().Or(Keyword("!=").Token())
from compareTo in QuotedText.Token().Or(EscapedQuotedText.Token())
let isEq = eq == "=="
select new ConditionalStringExpressionToken(expression, isEq, compareTo))
.WithPosition();
static readonly Parser<RepetitionToken> Repetition =
(from leftDelim in LDelim
from sp1 in Parse.WhiteSpace.Many()
from keyEach in Keyword("each")
from sp2 in Parse.WhiteSpace.AtLeastOnce()
from enumerator in Identifier.Token()
from keyIn in Keyword("in").Token()
from expression in Symbol.Token()
from rightDelim in RDelim
from body in Parse.Ref(() => Template)
from end in Parse.String("#{/each}")
select new RepetitionToken(expression, enumerator, body))
.WithPosition();
static readonly Parser<TextToken> Text =
Parse.CharExcept('#').Select(c => c.ToString())
.Or(Parse.Char('#').End().Return("#"))
.Or(Parse.String("##").FollowedBy("#{").Return("#"))
.Or(Parse.String("##{").Select(c => "#{"))
.Or(Parse.Char('#').Then(_ => Parse.CharExcept('{').Select(c => "#" + c)))
.AtLeastOnce()
.Select(s => new TextToken(s.ToArray()))
.WithPosition();
static readonly Parser<TemplateToken> Token =
Conditional.Select(t => (TemplateToken) t)
.Or(Calculation)
.Or(Repetition)
.Or(Substitution)
.Or(Text);
static readonly Parser<TemplateToken[]> Template =
Token.Many().Select(tokens => tokens.ToArray());
static readonly Parser<TemplateToken[]> IfTemplate =
Token.Except(Parse.String("#{else}")).Many().Select(tokens => tokens.ToArray());
static readonly Parser<TemplateToken[]> ContinueOnErrorsTemplate =
Token.ContinueMany().Select(tokens => tokens.ToArray());
static readonly ItemCache<TemplateWithError> TemplateCache = new ItemCache<TemplateWithError>("OctostacheTemplate", 100, TimeSpan.FromMinutes(10));
static readonly ItemCache<TemplateWithError> TemplateContinueCache = new ItemCache<TemplateWithError>("OctostacheTemplate", 100, TimeSpan.FromMinutes(10));
static readonly ItemCache<SymbolExpression> PathCache = new ItemCache<SymbolExpression>("OctostachePath", 100, TimeSpan.FromMinutes(10));
static Parser<CalculationOperator> CalculationOperator(string op, CalculationOperator @operator) => Parse.String(op).Token().Return(@operator);
static Parser<T> FollowedBy<T>(this Parser<T> parser, string lookahead)
{
if (parser == null)
throw new ArgumentNullException(nameof(parser));
return i =>
{
var result = parser(i);
if (!result.WasSuccessful)
return result;
// ReSharper disable once ArrangeRedundantParentheses
if (result.Remainder.Position >= (i.Source.Length - lookahead.Length))
return Result.Failure<T>(result.Remainder, "end of input reached while expecting lookahead", new[] { lookahead });
var next = i.Source.Substring(result.Remainder.Position, lookahead.Length);
return next != lookahead
? Result.Failure<T>(result.Remainder, $"unexpected {next}", new[] { lookahead })
: result;
};
}
static Parser<char> ExceptWhiteSpaceBeforeKeyword(this Parser<char> parser)
{
if (parser == null)
throw new ArgumentNullException(nameof(parser));
return i =>
{
var result = parser(i);
if (!result.WasSuccessful || !char.IsWhiteSpace(result.Value))
return result;
foreach (var keyword in new[] { "in", "==", "!=" })
{
var length = keyword.Length;
if (i.Source.Length <= result.Remainder.Position + length)
continue;
if (!char.IsWhiteSpace(i.Source[result.Remainder.Position + length]))
continue;
var match = i.Source.Substring(result.Remainder.Position, length);
if (match == keyword)
{
return Result.Failure<char>(result.Remainder, $"unexpected keyword used {keyword}", new[] { keyword });
}
}
return result;
};
}
static Parser<string> Keyword(string text)
{
return Parse.IgnoreCase(text).Text().Select(t => t.ToLowerInvariant());
}
static Parser<T> WithPosition<T>(this Parser<T> parser) where T : IInputToken
{
return i =>
{
var r = parser(i);
if (r.WasSuccessful)
// ReSharper disable once PossibleStructMemberModificationOfNonVariableStruct
r.Value.InputPosition = new Position(i.Position, i.Line, i.Column);
return r;
};
}
// ReSharper disable once UnusedMember.Local
// Used by the Test framework, to clear the caches before each test - as it is a static collection.
static void ClearCache()
{
TemplateCache.Clear();
TemplateContinueCache.Clear();
PathCache.Clear();
}
/// <summary>
/// Gets the names of variable replacement arguments that are resolvable by inspection of the template.
/// This excludes variables referenced inside and iterator (foreach) as the items cannot be determined without the
/// actual variable collection. The collection itself is returned.
/// </summary>
/// <param name="template"></param>
/// <param name="haltOnError"></param>
/// <returns></returns>
public static HashSet<string> ParseTemplateAndGetArgumentNames(string template, bool haltOnError = true)
{
var parser = haltOnError ? Template : ContinueOnErrorsTemplate;
var templateTokens = parser.End().Parse(template);
return new HashSet<string>(templateTokens.SelectMany(t => t.GetArguments()));
}
public static Template ParseTemplate(string template)
{
if (TryParseTemplate(template, out var result, out var error))
{
return result;
}
throw new ArgumentException($"Invalid template: {error}", nameof(template));
}
public static bool TryParseTemplate(string template, [NotNullWhen(true)] out Template? result, [NotNullWhen(false)] out string? error, bool haltOnError = true)
{
var parser = haltOnError ? Template : ContinueOnErrorsTemplate;
var cache = haltOnError ? TemplateCache : TemplateContinueCache;
var item = cache.GetOrAdd(template,
() =>
{
var tokens = parser.End().TryParse(template);
return new TemplateWithError
{
Result = tokens.WasSuccessful ? new Template(tokens.Value) : null,
Error = tokens.WasSuccessful ? null : tokens.ToString(),
};
});
error = item?.Error;
result = item?.Result;
return result != null && error == null;
}
internal static bool TryParseIdentifierPath(string path, [NotNullWhen(true)] out SymbolExpression? expression)
{
expression = PathCache.GetOrAdd(path,
() =>
{
var result = Symbol.TryParse(path);
return result.WasSuccessful ? result.Value : null;
});
return expression != null;
}
// A copy of Sprache's built in Many but when it hits an error the unparsed text is returned
// as a text token and we continue
// ReSharper disable once MemberCanBePrivate.Global
public static Parser<IEnumerable<TemplateToken>> ContinueMany(this Parser<TemplateToken> parser)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
return i =>
{
var remainder = i;
var result = new List<TemplateToken>();
var r = parser(i);
while (true)
{
if (remainder.Equals(r.Remainder))
break;
if (r.WasSuccessful)
{
result.Add(r.Value);
}
else
{
var consumed = Consumed(remainder, r.Remainder);
result.Add(new TextToken(consumed));
}
remainder = r.Remainder;
r = parser(remainder);
}
return Result.Success<IEnumerable<TemplateToken>>(result, remainder);
};
}
// ReSharper disable once MemberCanBePrivate.Global
public static string Consumed(IInput before, IInput after) => before.Source.Substring(before.Position, after.Position - before.Position);
class TemplateWithError
{
public Template? Result { get; set; }
public string? Error { get; set; }
}
}
}