jehugaleahsa / mustache-sharp

An extension of the mustache text template engine for .NET.
The Unlicense
306 stars 78 forks source link

Feature Request - Rename Placeholder in template #2

Closed PaulGrimshaw closed 11 years ago

PaulGrimshaw commented 11 years ago

A feature request we have is to be able to rename a particular field. The business objects that get passed into this library are often renamed, so it would be fantastic if there was a way to rename them on the fly and have Mustache# rename all the occurrences in a template, without removing the "{{" or any of the format strings etc.

var newTemplate = generator.RenameField("Customer.Name","Customer.FirstName");

Another related option would be to check whether a particular template contains a key/field name/path:

if (generator.ContainsKey("Customer.Name")) { ... }

jehugaleahsa commented 11 years ago

So are you looking to see whether a template contains the field or the object? On Apr 23, 2013 4:40 AM, "PaulGrimshaw" notifications@github.com wrote:

A feature request we have is to be able to rename a particular field. The business objects that get passed into this library are often renamed, so it would be fantastic if there was a way to rename them on the fly and have Mustache# rename all the occurrences in a template, without removing the "{{" or any of the format strings etc.

generator.RenameField("Customer.Name","Customer.FirstName");

Another related option would be to check whether a particular template contains a key/field name/path:

if (generator.ContainsKey("Customer.Name")) { ... }

— Reply to this email directly or view it on GitHubhttps://github.com/jehugaleahsa/mustache-sharp/issues/2 .

PaulGrimshaw commented 11 years ago

Well ideally i guess it would work for both:

For me, the field is more important: if (generator.TemplateContains("Customer.Name") { //This would return true only if the Customer.Name field was in the template }

However the object would also be useful: if (generator.TemplateContains("Customer") { //This would return true if ANY customer fields are in the template }

jehugaleahsa commented 11 years ago

This could be tricky. Say someone is looking to replace Customer.Address.ZipCode. I might see ZipCode and need to determine whether it is under Address and Customer. Remember, the Generator isn't specific to a particular object graph. In other words, the same template could work for a Person with a FirstName and a Dog with a FirstName. So, I really couldn't tell in some contexts whether I was replacing the correct tag. You can technically do other goofy stuff like this.Customer.Address.ZipCode which would throw other wrenches in.

Typically, when you compile to a Generator you plan on reusing it for multiple records of the same type. I am curious about your scenario where the names of your properties are changing on-the-fly. I am also curious whether you could modify the original template (just a string) and recompile it into a new generator. I think the string manipulation approach has just as many context-related issues, though.

I thought about passing the object along with the KeyNotFound event. The problem is that the code will search up the object graph when a field is not found in the current scope. By the time I raise the KeyNotFound, I am at the top of the object graph. In the case of compound keys (Customer.Address.ZipCode) I would be at the Address level when I discovered that ZipCde didn't exist. In this case, you might be able to deal with the error by setting the Substitution property.

I hope I have illustrated why this is a complex request. It would be helpful if you could kind of explain your situation a little. Like I said, I am curious why the names of your properties are changing on-the-fly. I want to fully explore the options before I start making changes.

PaulGrimshaw commented 11 years ago

Where we have used this (with great results) is in a dynamic form application where customers can create "HTML Email Templates" that get sent when the form is submitted. The customers can change the form fields, as well as the email template. Behind the scenes we create an Expando object to feed the mustache# generator and render out the emails.

When a customer renames a form element, we would like to update the templates where it is used. We can do this by creating a dummy object with the same shape as the normal objects, but the "Values" of the properties are in fact the new name. However, the problem is that the "{{" braces are obviously removed, and the formatting too. It would be great if we could somehow tell the engine to ignore the formatting and leave the braces in place.

The reason we want to check whether a field exists is for performance. When an updated template is saved by the customer, we would like to store a list of the "Fields" that are in use, which would enable our engine to build smaller expando objects (some forms have hundreds of fields).

Any suggestions would be greatly appreciated, even if you could point us at the relevant source code where this might be achieved (although we'd prefer to work with your version to keep on top of updates etc).

jehugaleahsa commented 11 years ago

I had an idea that partially addresses your concerns. I can raise an event every time a key is found in the template. You can use this to keep track of which fields are being used (sticking them in a HashSet or something). This event will also allow you to view the alignment and format strings. Note that this will always return the fully-qualified key.

Additionally, you will be able to update the key, alignment and format strings. So, if you knew you wanted to use "name" instead of "fname", you could test for it and reassign. If you don't want any formatting, you can just set it to blank.

So, you would grab your template from the database. You'd create a FormatCompiler. You'd add a handler and call Compile. By the time you got the Generator back, your handler will have run for each key found.

PaulGrimshaw commented 11 years ago

This definitely sounds like it would work, although I'd need to see a couple of simple code examples to fully understand.

I like the idea of being able to update the format strings, and this addition would certainly make the tool very powerful.

By the way, what do the alignment strings do? I don't see any example in your Readme.

jehugaleahsa commented 11 years ago

Okay. I just pushed out a new version of the code with the changes I described. Let me show you a simple code snippet:

FormatCompiler compiler = new FormatCompiler();
HashSet<string> keys = new HashSet<string>();
compiler.PlaceholderFound += (o, e) =>
{
    keys.Add(e.Key);
};
Generator generator = compiler.Compile(@"Hello {{FirstName}} {{LastName}}");

This code will collect all keys that appear in the template (FirstName and LastName). You could use this list to build your ExpandoObject with just the keys that are used. Keep in mind that the Key property is the key as it appears in the template. So, if it is a compound key (Customer.Address.ZipCode), the full key will be returned rather than just the last piece (ZipCode).

I think this is a good starting point. Let's see where we can go from here.

jehugaleahsa commented 11 years ago

BTW, I am thinking the name MissingKeyEventArgs was a mistake. I think KeyNotFoundEventArgs would have been more appropriate. I am debating changing this name in a later release. I might also create a KeyFound event that fires when a key is found during generation to compliment KeyNotFound.

jehugaleahsa commented 11 years ago

Check out this link Format Specifiers to learn more about alignment. It probably won't do you any good if you're building emails.

PaulGrimshaw commented 11 years ago

This looks good. I will plug this into our project and let you know how it goes.

PaulGrimshaw commented 11 years ago

Tried this in the project, however we were hoping to be able to know the context of a key, e.g:

{{#each Customer}}

would return an existing path of "Customer.FullName", rather than just "FullName"... I understand this would be hard, but assumed that as the Mustache# engine understands the context of the {{"FullName"}} for rendering that it would be possible...

PaulGrimshaw commented 11 years ago

I guess this is what you explained in your comment at the top would be tricky. Thanks for the heads up about the rename, will look out for it.

jehugaleahsa commented 11 years ago

I renamed the event args and created a new one for consistency.

jehugaleahsa commented 11 years ago

I have added a new property to the PlaceholderFoundEventArgs class called Context. It is an array of Context objects. Each Context has a Tag and a Argument property. The Tag is the tag that caused a new context to be created. The Argument is the argument (key) passed to the tag in order to create the new scope. Right now, I am only creating Context instances for each and with tags. There is also a top-level tag whose tag name is blank and the argument will always be "this".

Here's where things get tricky on your end:

You will have to be extra smart to make sure you are handling these cases. Only you know what the structure of your objects are, so there's only so much I can do.

This was tricky to do, so hopefully it's enough that you can proceed.

jehugaleahsa commented 11 years ago

Ah, bugs....

Right as I was going to bed last night I realized that the code I pushed was actually wrong. I forgot two things: when exiting a tag, I needed to pop the context; and more than one tag argument can form a new context/scope.

In the first case, I was adding new context any time I found a new tag that created a new scope (each and with). However, once I finished parsing the content of these tags, I wasn't popping the context off the stack. Here's an example:

{{#with Address}}{{ZipCode}}{{/with}}
{{FirstName}}

In the case of ZipCode, the context would have indicated that it was within a with tag. However, I was not popping the stack when I hit the closing tag, so FirstName would have said it was inside the with tag, too. This has been fixed.

The second issue is really only something that would have mattered if someone created their own tags. Both of the with and each tag only use a single parameter (context and collection, respectively) to create their scopes. However, someone creating their own tag could use multiple parameters. For instance, someone could create a zip tag that worked like this:

 {{#zip Quantities Prices}}{{Quantity}} x {{Price}} = {{Total}}{{/zip}}

The assumption is that the zip tag would create a new object with three properties: Quantity, Price and Total. Why anyone would want to do that is beyond me, but I support it nonetheless.

The only impact this has on the code is that now Context has a Parameters property, which is an array of ContextParameter. ContextParameter has two properties, one for the parameter name and another for the key passed as the argument.

PaulGrimshaw commented 11 years ago

No worries.

Will download the latest version this evening and give it a go.