using System.Globalization; using System.Runtime.CompilerServices; using System.Text.Json; using Fluid.Values; #nullable enable namespace Fluid; public class TemplateContext { protected int _recursion; protected int _steps; private Scope _localScope; // Partials resolved by the render in progress. A partial used in a loop would otherwise ask the // file provider for the same path on every iteration, which for a physical provider is a file // system call each time. Scoped to the outermost render so that a context reused for another // render still picks up a file that changed in between. private Dictionary? _loadedTemplates; private int _renderDepth; /// /// Initializes a new instance of . /// public TemplateContext() : this(TemplateOptions.Default) { } /// /// Initializes a new instance of . /// /// The model. /// The template options. public TemplateContext(object model, TemplateOptions options) : this(options) { ArgumentNullException.ThrowIfNull(model); if (!ReferenceEquals(options, TemplateOptions.Default)) { options.MemberAccessStrategy.RegisterGeneratedAccessor(model.GetType()); } if (model is FluidValue fluidValue) { Model = fluidValue; } else { Model = FluidValue.Create(model, options); } } /// /// Initializes a new instance of with the specified . /// /// The template options. /// An optional instance used when comparing model names. public TemplateContext(TemplateOptions options, StringComparer? modelNamesComparer = null) { modelNamesComparer ??= options.ModelNamesComparer; Options = options; _localScope = new Scope(options.GlobalScope, null, modelNamesComparer, null); RootScope = _localScope; CultureInfo = options.CultureInfo; MoneyOptions = options.MoneyOptions; TimeZone = options.TimeZone; Captured = options.Captured; Assigned = options.Assigned; Undefined = options.Undefined; Now = options.Now; MaxSteps = options.MaxSteps; MaxOutputSize = options.MaxOutputSize; MaxCollectionSize = options.MaxCollectionSize; ModelNamesComparer = modelNamesComparer; JsonSerializerOptions = options.JsonSerializerOptions; } /// /// Initializes a new instance of with a model. /// /// The model. /// An optional instance used when comparing model names. public TemplateContext(object model, StringComparer? modelNamesComparer = null) : this(TemplateOptions.Default, modelNamesComparer) { ArgumentNullException.ThrowIfNull(model); if (model is FluidValue fluidValue) { Model = fluidValue; } else { Model = FluidValue.Create(model, TemplateOptions.Default); } } /// /// Gets the . /// public TemplateOptions Options { get; protected set; } /// /// Gets or sets the maximum number of steps a script can execute. Leave to 0 for unlimited. /// public int MaxSteps { get; set; } = TemplateOptions.Default.MaxSteps; /// /// Gets or sets the maximum number of characters a template or captured block can render. /// Leave to 0 for unlimited. /// public int MaxOutputSize { get; set; } = TemplateOptions.Default.MaxOutputSize; /// /// Gets or sets the maximum number of items a template operation can materialize. /// Leave to 0 for unlimited. /// public int MaxCollectionSize { get; set; } = TemplateOptions.Default.MaxCollectionSize; /// /// Gets used when comparing model names. /// public StringComparer ModelNamesComparer { get; private set; } /// /// Gets or sets the instance used to render locale values like dates and numbers. /// public CultureInfo CultureInfo { get; set; } = TemplateOptions.Default.CultureInfo; /// /// Gets or sets the options used by the money filters. Assigning a different instance allows /// a currency to be selected per rendering, for example based on the current request. /// public MoneyOptions MoneyOptions { get; set; } = TemplateOptions.Default.MoneyOptions; /// /// Gets or sets the value to returned by the "now" keyword. /// public Func Now { get; set; } = TemplateOptions.Default.Now; /// /// Gets or sets the local time zone used when parsing or creating dates without specific ones. /// public TimeZoneInfo TimeZone { get; set; } = TemplateOptions.Default.TimeZone; /// /// Gets or sets the used by the json filter. /// public JsonSerializerOptions JsonSerializerOptions { get; set; } = TemplateOptions.Default.JsonSerializerOptions; /// /// Gets or sets the token used to cancel asynchronous template operations. /// public CancellationToken CancellationToken { get; set; } /// /// Increments the number of statements the current template is processing. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void IncrementSteps() { CancellationToken.ThrowIfCancellationRequested(); if (MaxSteps > 0 && _steps++ > MaxSteps) { ExceptionHelper.ThrowMaximumRecursionException(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void EnsureOutputSize(long size) { CancellationToken.ThrowIfCancellationRequested(); if (size < 0 || (MaxOutputSize > 0 && size > MaxOutputSize)) { ExceptionHelper.ThrowMaximumOutputSizeException(MaxOutputSize); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void EnsureCollectionSize(long size) { CancellationToken.ThrowIfCancellationRequested(); if (size < 0 || (MaxCollectionSize > 0 && size > MaxCollectionSize)) { ExceptionHelper.ThrowMaximumCollectionSizeException(MaxCollectionSize); } } /// /// Marks the start of a template render. Must be paired with . /// internal void EnterRender() { _renderDepth++; } /// /// Marks the end of a template render, forgetting the resolved partials once the outermost one ends. /// internal void ExitRender() { if (--_renderDepth == 0) { _loadedTemplates?.Clear(); } } internal bool TryGetLoadedTemplate(TemplateLoader.LoadedTemplateKey key, out TemplateLoader.LoadedTemplate loadedTemplate) { if (_loadedTemplates is null) { loadedTemplate = default; return false; } return _loadedTemplates.TryGetValue(key, out loadedTemplate); } internal void SetLoadedTemplate(TemplateLoader.LoadedTemplateKey key, TemplateLoader.LoadedTemplate loadedTemplate) { // Outside of a template render there is nothing to tell when the entry should be dropped. if (_renderDepth == 0) { return; } _loadedTemplates ??= []; _loadedTemplates[key] = loadedTemplate; } /// /// Gets the current scope. /// public Scope LocalScope => _localScope; /// /// Gets or sets the root scope. /// internal Scope RootScope { get; set; } /// /// Used to define custom object on this instance to be used in filters and statements /// but which are not available from the template. /// public Dictionary AmbientValues => field ??= []; /// /// Gets or sets a model object that is used to resolve properties in a template. This object is used if local and /// global scopes are unsuccessful. /// public FluidValue Model { get; } = NilValue.Instance; /// /// Gets or sets the delegate to execute when a Capture tag has been evaluated. /// public TemplateOptions.CapturedDelegate Captured { get; set; } /// /// Gets or sets the delegate to execute when an Assign tag has been evaluated. /// public TemplateOptions.AssignedDelegate Assigned { get; set; } /// /// Gets or sets the delegate to execute when an undefined value is used. /// public TemplateOptions.UndefinedDelegate Undefined { get; set; } /// /// Enters a scope with the specified lookup and assignment behavior. /// public ScopeLease EnterScope(ScopeBehavior behavior = ScopeBehavior.Local) { return new ScopeLease(this, EnterScopeCore(behavior)); } private Scope EnterScopeCore(ScopeBehavior behavior) { if (behavior != ScopeBehavior.Local && behavior != ScopeBehavior.WriteThrough && behavior != ScopeBehavior.Isolated) { throw new ArgumentOutOfRangeException(nameof(behavior)); } if (Options.MaxRecursion > 0 && _recursion >= Options.MaxRecursion) { ExceptionHelper.ThrowMaximumRecursionException(); } _recursion++; var previous = _localScope; var parent = behavior == ScopeBehavior.Isolated ? RootScope : previous; var assignmentTarget = behavior == ScopeBehavior.WriteThrough ? previous.AssignmentScope : null; return _localScope = new Scope(parent, assignmentTarget, ModelNamesComparer, previous); } private void ReleaseScopeCore(Scope scope) { if (!ReferenceEquals(_localScope, scope)) { ExceptionHelper.ThrowInvalidOperationException("Scopes must be released in reverse order"); } _recursion--; _localScope = scope.Previous; } /// /// Restores the previous scope when disposed. /// public readonly struct ScopeLease : IDisposable { private readonly TemplateContext _context; private readonly Scope _scope; internal ScopeLease(TemplateContext context, Scope scope) { _context = context; _scope = scope; } /// /// Restores the scope that was active before this lease was created. /// public void Dispose() { _context?.ReleaseScopeCore(_scope); } } /// /// Gets a value from the context. /// /// The name of the value. public FluidValue GetValue(string name) { return _localScope.GetValue(name); } /// /// Sets a value on the context. /// /// The name of the value. /// The value to set. /// public TemplateContext SetValue(string name, FluidValue value) { _localScope.SetValue(name, value); return this; } } public static class TemplateContextExtensions { public static TemplateContext SetValue(this TemplateContext context, string name, int value) { return context.SetValue(name, NumberValue.Create(value)); } public static TemplateContext SetValue(this TemplateContext context, string name, string value) { return context.SetValue(name, StringValue.Create(value)); } public static TemplateContext SetValue(this TemplateContext context, string name, char value) { return context.SetValue(name, StringValue.Create(value)); } public static TemplateContext SetValue(this TemplateContext context, string name, bool value) { return context.SetValue(name, BooleanValue.Create(value)); } public static TemplateContext SetValue(this TemplateContext context, string name, object value) { if (value == null) { return context.SetValue(name, NilValue.Instance); } return context.SetValue(name, FluidValue.Create(value, context.Options)); } public static TemplateContext SetValue(this TemplateContext context, string name, Func factory) { return context.SetValue(name, FactoryValue.Create(factory)); } }