Skip to content

[Question] Is it possible to get raw parameters from template? #561

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Cubody opened this issue Dec 5, 2023 · 13 comments
Open

[Question] Is it possible to get raw parameters from template? #561

Cubody opened this issue Dec 5, 2023 · 13 comments
Labels

Comments

@Cubody
Copy link

Cubody commented Dec 5, 2023

Is there any built-in way to get parameters from a template? Something like hb.Create().GetParameters("{{some_parameter}}") with return of string[] { "some_parameter" }.

As I've seen, all the parsing functions are internal. It would be nice to have some kind of option to get the parameters.
I can do a PR for such a thing if you let me know how to do it in the best way.

First, the template turns into tokens, and then expressions are built. As I understand it, the library understands exactly where the parameters are at the moment of the expression build.

Also, I don't see any reason to make it internal, it seems like a useful tool for template analyze.

@Cubody Cubody added the question label Dec 5, 2023
@Cubody Cubody changed the title [Question] Short question content [Question] Is it possible to get raw parameters from template? Dec 5, 2023
@oformaniuk
Copy link
Member

You can use helperMissing to get somewhat similar behavior. If this is not something you're looking for - this is not the first time such functionality is requested so I'd be happy to merge PR enabling this.

@attilah
Copy link

attilah commented Aug 2, 2024

I second it, it would be useful to get out the parameter names / expressions from a template, could enable some enhanced use cases for validation or when the functionailty is tied to a UI auto populate a form with required variable names that the template expects.

@attilah
Copy link

attilah commented Aug 7, 2024

@oformaniuk could you give some pointers on the approach one should take to make it into a PR?

@attilah
Copy link

attilah commented Feb 7, 2025

@oformaniuk As I needed the feature did a quick test on some templates we have, will test with some complex ones as well which has conditions and such, but so far I was able to dump out the variables.

@Cubody
Copy link
Author

Cubody commented Feb 23, 2025

Sorry guys, I forgotten about this thread. I've already made a decision, it lives with me locally, I can somehow get there and make MR.
Btw my solution is just nodes visitor.

This may not cover some cases, but it works for most of my examples.

using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using HandlebarsDotNet.Helpers;
using HandlebarsDotNet.Helpers.BlockHelpers;
using HandlebarsDotNet.PathStructure;
using HandlebarsDotNet.Runtime;

namespace HandlebarsDotNet.Compiler
{
    public static class ExpressionPathResolver
    {
        public static HashSet<string> ExtractPathsFromExpressions(
            IEnumerable<Expression> expressions,
            ICompiledHandlebarsConfiguration config)
        {
            var paths = new HashSet<string>();

            foreach (var expression in expressions)
                ExtractPaths(expression, config, paths);

            return paths;
        }

        private static void ExtractPaths(
            Expression expression,
            ICompiledHandlebarsConfiguration config,
            ISet<string> paths,
            bool isIteration = false)
        {
            switch (expression)
            {
                #region ExpressionParseLogic
                case ConditionalExpression condExpr:
                    ExtractPaths(condExpr.Test, config, paths, isIteration);
                    ExtractPaths(condExpr.IfTrue, config, paths, isIteration);
                    ExtractPaths(condExpr.IfFalse, config, paths, isIteration);
                    break;

                case BoolishExpression boolExpr:
                    ExtractPaths(boolExpr.Condition, config, paths, isIteration);
                    break;

                case IteratorExpression iterExpr:
                    ExtractPaths(iterExpr.Sequence, config, paths, isIteration);
                    ExtractPaths(iterExpr.IfEmpty, config, paths, isIteration: true);
                    ExtractPaths(iterExpr.Body, config, paths, isIteration: true);
                    break;

                case StatementExpression statementExpression:
                    ExtractPaths(statementExpression.Body, config, paths, isIteration);
                    break;

                case PathExpression pathExpr:
                    AddPath(pathExpr.Path, paths, isIteration);
                    break;

                case BlockExpression blockExpr:
                    foreach (var expr in blockExpr.Expressions)
                        ExtractPaths(expr, config, paths, isIteration);
                    break;

                case BlockHelperExpression blockHelperExpr:
                    if (blockHelperExpr.Arguments.Any())
                    {
                        foreach (var argument in blockHelperExpr.Arguments)
                            ExtractPaths(argument, config, paths, isIteration);
                    }
                    else
                    {
                        var pathInfo = PathInfo.Parse(blockHelperExpr.HelperName);
                        AddPath(pathInfo.TrimmedPath, paths, isIteration);
                    }
                    
                    ExtractPaths(blockHelperExpr.Body, config, paths, isIteration: true);
                    break;
                
                case SubExpressionExpression subExpressionExpr:
                    ExtractPaths(subExpressionExpr.Expression, config, paths, isIteration);
                    break;
                
                case HelperExpression helperExpr:
                    foreach (var arg in helperExpr.Arguments)
                        ExtractPaths(arg, config, paths, isIteration);
                    break;
                #endregion
            }
        }

        private static void AddPath(string path, ISet<string> paths, bool isIteration)
        {
            if (isIteration && !path.StartsWith("@root")) return;

            var processedPath = isIteration ? path.Replace("@root.", "") : path;
            paths.Add(processedPath);
        }
    }
}

source/Handlebars/Compiler/HandlebarsCompiler.cs

public static HashSet<string> GetVariables(ExtendedStringReader source, CompilationContext compilationContext)
        {
            var configuration = compilationContext.Configuration;
            var createdFeatures = configuration.Features;
            for (var index = 0; index < createdFeatures.Count; index++)
            {
                createdFeatures[index].OnCompiling(configuration);
            }
            
            var tokens = Tokenizer.Tokenize(source).ToArray();
            var expressions = ExpressionBuilder.ConvertTokensToExpressions(tokens, configuration);

            var variables = ExpressionPathResolver.ExtractPathsFromExpressions(expressions, configuration);

            return variables;
        }

source/Handlebars/HandlebarsEnvironment.cs

public HashSet<string> GetVariables(TextReader template)
        {
            using var container = AmbientContext.Use(_ambientContext);
            
            var configuration = CompiledConfiguration ?? new HandlebarsConfigurationAdapter(Configuration);
            
            var formatterProvider = new FormatterProvider(configuration.FormatterProviders);
            var objectDescriptorFactory = new ObjectDescriptorFactory(configuration.ObjectDescriptorProviders);
            
            var localContext = AmbientContext.Create(
                _ambientContext, 
                formatterProvider: formatterProvider,
                descriptorFactory: objectDescriptorFactory
            );
            
            using var localContainer = AmbientContext.Use(localContext);
            
            var compilationContext = new CompilationContext(configuration);
            using var reader = new ExtendedStringReader(template);
            
            var variables = HandlebarsCompiler.GetVariables(reader, compilationContext);

            return variables;
        }
        
        public HashSet<string> GetVariables(string template)
        {
            using var reader = new StringReader(template);
            var variables = GetVariables(reader);
            return variables;
        }

Also some tests

using Xunit;

namespace HandlebarsDotNet.Test;

public class ExpressionPathResolveTests
{
    public class HandlebarsTests
    {
        [Theory]
        [ClassData(typeof(HandlebarsEnvGenerator))]
        public void BasicTemplate_GetVariables_Return_One(IHandlebars handlebars)
        {
            var source = "{{name}}";
            var variables = handlebars.GetVariables(source);

            Assert.Equal("name", string.Join(", ", variables));
        }
        
        [Theory]
        [ClassData(typeof(HandlebarsEnvGenerator))]
        public void ConditionsTemplate_GetVariables_Return_Two(IHandlebars handlebars)
        {
            var source = "{{#if some_bool}}{{name}}{{/if}}";
            var variables = handlebars.GetVariables(source);

            Assert.Equal("some_bool, name", string.Join(", ", variables));
        }
        
        [Theory]
        [ClassData(typeof(HandlebarsEnvGenerator))]
        public void EachTemplate_GetVariables_Return_OnlyEachPath(IHandlebars handlebars)
        {
            var source = "{{#each array}}{{array_item}}{{/each}}";
            var variables = handlebars.GetVariables(source);

            Assert.Equal("array", string.Join(", ", variables));
        }
        
        [Theory]
        [ClassData(typeof(HandlebarsEnvGenerator))]
        public void EachTemplate_WithoutHelper_GetVariables_Return_OnlyIterationPath(IHandlebars handlebars)
        {
            var source = "{{#some_array}}{{array_item}}{{/some_array}}";
            var variables = handlebars.GetVariables(source);

            Assert.Equal("some_array", string.Join(", ", variables));
        }
        
        [Theory]
        [ClassData(typeof(HandlebarsEnvGenerator))]
        public void EachTemplate_WithRoot_GetVariables_Return_EachPathAndRoot(IHandlebars handlebars)
        {
            var source = "{{#each array}}{{@root.global_var}}{{/each}}";
            var variables = handlebars.GetVariables(source);

            Assert.Equal("array, global_var", string.Join(", ", variables));
        }
        
        [Theory]
        [ClassData(typeof(HandlebarsEnvGenerator))]
        public void EachTemplate_WithoutHelper_WithRoot_GetVariables_Return_EachPathAndRoot(IHandlebars handlebars)
        {
            var source = "{{#some_array}}{{@root.global_var}}{{/some_array}}";
            var variables = handlebars.GetVariables(source);

            Assert.Equal("some_array, global_var", string.Join(", ", variables));
        }
        
        [Theory]
        [ClassData(typeof(HandlebarsEnvGenerator))]
        public void EachTemplate_WithoutHelper_WithConditionInBody_Return_OnlyIterationPath(IHandlebars handlebars)
        {
            var source = "{{#some_array}}{{#if some_bool}}{{array_item}}{{/if}}{{/some_array}}";
            var variables = handlebars.GetVariables(source);

            Assert.Equal("some_array", string.Join(", ", variables));
        }
        
        [Theory]
        [ClassData(typeof(HandlebarsEnvGenerator))]
        public void WithTemplate_Return_OnlyWithPath(IHandlebars handlebars)
        {
            var source = "{{#with person}}\n{{firstname}} {{lastname}}\n{{/with}}";
            var variables = handlebars.GetVariables(source);

            Assert.Equal("person", string.Join(", ", variables));
        }
        
        [Theory]
        [ClassData(typeof(HandlebarsEnvGenerator))]
        public void ConditionsTemplate_WithHelper_GetVariables_Return_Two(IHandlebars handlebars)
        {
            handlebars.RegisterHelper("bold", 
                (writer, options, _, _) =>
                {
                    writer.WriteSafeString("<bold>");
                    writer.WriteSafeString(options.Inverse());
                    writer.WriteSafeString("</bold>");
                });
            
            var source = "{{#if (bold some_bool)}}{{name}}{{/if}}";
            var variables = handlebars.GetVariables(source);

            Assert.Equal("some_bool, name", string.Join(", ", variables));
        }
    }
}

@attilah
Copy link

attilah commented Feb 23, 2025

@Cubody this looks promising, as the other solution that was suggested partially works, because if I have a condition block within the template and the condition is false, then the block is not processed so if there is a variable inside then it will not be called by the callback. I'll give your code a shot later, for now as we only need it for better UX we solved it with client side parsing, which works well.

@rexm
Copy link
Member

rexm commented Feb 23, 2025

Nice. How does Handlebars expose this information?

@Cubody
Copy link
Author

Cubody commented Feb 23, 2025

Handlebars parses template to tokens (nodes) and all structure is like expression tree, you can deep into this tree and take what you want

It is possible to approach this from the other side and get the parameters right away when parsing, but I used a ready-made structure

@Cubody
Copy link
Author

Cubody commented Feb 23, 2025

I've looked at other libraries in other languages, and I haven't found a way to get the parameters in any of them. It seemed strange to me. However, I needed it because my templates have many parameters that are generated based on various complex processes, and the templates are not tied to them in any way. In other words, the one who creates the template can specify any parameters. But there is no guarantee that such parameters exist. I needed to verify that the parameters entered in the template exist in the system.

@attilah
Copy link

attilah commented Feb 23, 2025

@Cubody our use case is similar, depending on the user's settings, what level of variable validation they want: relaxed or strict. If strict then we validate against variable definitions they've added, but when you have 40+ variables, populating them by hand is subpar UX, so we pre-parse the template then pre-populate the variable editor for them.

@rexm
Copy link
Member

rexm commented Feb 23, 2025

@Cubody I know how this library works, I wrote it. I’m asking in terms of ergonomics, does the original handlebars JS library expose this information? If so, how?

@Cubody
Copy link
Author

Cubody commented Feb 23, 2025

Sorry, I didn't look up the authors, thanks for the library by the way)
I was looking at the js library, there was no such thing there. At least there were no public handles for it.

@attilah
Copy link

attilah commented Feb 23, 2025

Similarly to the printer.js implementation you can write a visitor and do as you like, IIRC this was the way how we implemented it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants