using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using ServiceStack.Text; namespace ServiceStack.Script { /// /// Handlebars.js like each block /// Usages: {{#each customers}} {{Name}} {{/each}} /// {{#each customers}} {{it.Name}} {{/each}} /// {{#each num in numbers}} {{num}} {{/each}} /// {{#each num in [1,2,3]}} {{num}} {{/each}} /// {{#each numbers}} {{it}} {{else}} no numbers {{/each}} /// {{#each numbers}} {{it}} {{else if letters != null}} has letters {{else}} no numbers {{/each}} /// {{#each n in numbers where n > 5}} {{it}} {{else}} no numbers > 5 {{/each}} /// {{#each n in numbers where n > 5 orderby n skip 1 take 2}} {{it}} {{else}} no numbers > 5 {{/each}} /// public class EachScriptBlock : ScriptBlock { public override string Name => "each"; public override async Task WriteAsync(ScriptScopeContext scope, PageBlockFragment block, CancellationToken token) { if (block.Argument.IsNullOrEmpty()) throw new NotSupportedException("'each' block requires the collection to iterate"); var cache = (EachArg)scope.Context.Cache.GetOrAdd(block.ArgumentString, _ => ParseArgument(scope, block)); var collection = cache.Source.Evaluate(scope, out var syncResult, out var asyncResult) ? (IEnumerable)syncResult : (IEnumerable)(await asyncResult.ConfigAwait()); var index = 0; if (collection != null) { if (cache.Where != null || cache.OrderBy != null || cache.OrderByDescending != null || cache.Skip != null || cache.Take != null) { var filteredResults = new List>(); foreach (var element in collection) { // Add all properties into scope if called without explicit in argument var scopeArgs = !cache.HasExplicitBinding && CanExportScopeArgs(element) ? element.ToObjectDictionary() : new Dictionary(); scopeArgs[cache.Binding] = element; scopeArgs[nameof(index)] = AssertWithinMaxQuota(index++); var itemScope = scope.ScopeWithParams(scopeArgs); if (cache.Where != null) { var result = await cache.Where.EvaluateToBoolAsync(itemScope).ConfigAwait(); if (!result) continue; } filteredResults.Add(scopeArgs); } IEnumerable> selectedResults = filteredResults; var comparer = (IComparer)Comparer.Default; if (cache.OrderBy != null) { var i = 0; selectedResults = selectedResults.OrderBy(scopeArgs => { scopeArgs[nameof(index)] = i++; return cache.OrderBy.Evaluate(scope.ScopeWithParams(scopeArgs)); }, comparer); } else if (cache.OrderByDescending != null) { var i = 0; selectedResults = selectedResults.OrderByDescending(scopeArgs => { scopeArgs[nameof(index)] = i++; return cache.OrderByDescending.Evaluate(scope.ScopeWithParams(scopeArgs)); }, comparer); } if (cache.Skip != null) { var skip = cache.Skip.Evaluate(scope).ConvertTo(); selectedResults = selectedResults.Skip(skip); } if (cache.Take != null) { var take = cache.Take.Evaluate(scope).ConvertTo(); selectedResults = selectedResults.Take(take); } index = 0; foreach (var scopeArgs in selectedResults) { var itemScope = scope.ScopeWithParams(scopeArgs); itemScope.ScopedParams[nameof(index)] = index++; await WriteBodyAsync(itemScope, block, token).ConfigAwait(); } } else { foreach (var element in collection) { // Add all properties into scope if called without explicit in argument var scopeArgs = !cache.HasExplicitBinding && CanExportScopeArgs(element) ? element.ToObjectDictionary() : new Dictionary(); scopeArgs[cache.Binding] = element; scopeArgs[nameof(index)] = AssertWithinMaxQuota(index++); var itemScope = scope.ScopeWithParams(scopeArgs); await WriteBodyAsync(itemScope, block, token).ConfigAwait(); } } } if (index == 0) { await WriteElseAsync(scope, block.ElseBlocks, token).ConfigAwait(); } } EachArg ParseArgument(ScriptScopeContext scope, PageBlockFragment fragment) { var literal = fragment.Argument.Span.ParseJsExpression(out var token); if (token == null) throw new NotSupportedException("'each' block requires the collection to iterate"); var binding = "it"; literal = literal.AdvancePastWhitespace(); JsToken source, where, orderBy, orderByDescending, skip, take; where = orderBy = orderByDescending = skip = take = null; var hasExplicitBinding = literal.StartsWith("in "); if (hasExplicitBinding) { if (!(token is JsIdentifier identifier)) throw new NotSupportedException($"'each' block expected identifier but was {token.DebugToken()}"); binding = identifier.Name; literal = literal.Advance(3); literal = literal.ParseJsExpression(out source); if (source == null) throw new NotSupportedException("'each' block requires the collection to iterate"); } else { source = token; } if (literal.StartsWith("where ")) { literal = literal.Advance("where ".Length); literal = literal.ParseJsExpression(out where); } literal = literal.AdvancePastWhitespace(); if (literal.StartsWith("orderby ")) { literal = literal.Advance("orderby ".Length); literal = literal.ParseJsExpression(out orderBy); literal = literal.AdvancePastWhitespace(); if (literal.StartsWith("descending")) { literal = literal.Advance("descending".Length); orderByDescending = orderBy; orderBy = null; } } literal = literal.AdvancePastWhitespace(); if (literal.StartsWith("skip ")) { literal = literal.Advance("skip ".Length); literal = literal.ParseJsExpression(out skip); } literal = literal.AdvancePastWhitespace(); if (literal.StartsWith("take ")) { literal = literal.Advance("take ".Length); literal = literal.ParseJsExpression(out take); } return new EachArg(binding, hasExplicitBinding, source, where, orderBy, orderByDescending, skip, take); } class EachArg { public readonly string Binding; public readonly bool HasExplicitBinding; public readonly JsToken Source; public readonly JsToken Where; public readonly JsToken OrderBy; public readonly JsToken OrderByDescending; public readonly JsToken Skip; public readonly JsToken Take; public EachArg(string binding, bool hasExplicitBinding, JsToken source, JsToken where, JsToken orderBy, JsToken orderByDescending, JsToken skip, JsToken take) { Binding = binding; HasExplicitBinding = hasExplicitBinding; Source = source; Where = where; OrderBy = orderBy; OrderByDescending = orderByDescending; Skip = skip; Take = take; } } } }