Some time ago I had the need to generate order confirmation emails using templates populated with order data such as ordered items, pricing and customer information.
One way is of course to serialize the order to XML and transform it via XSL but I had an urge to try something new.
So I decided to use IronPython and parse templates with {py py} blocks.
You can download the example PyBlocks solution here
Example PyBlocks template
Hello {py Console.Write(customer.Givenname+ ' ' + customer.Surname) py}!
The package will be sent to the following adress
{py
Console.WriteLine(customer.Street + ' ' + customer.StreetNumber)
Console.WriteLine(customer.ZipCode + ' ' + customer.City)
py}
Regards
//Some online store
Rendering the template
var engine = Python.CreateEngine(AppDomain.CurrentDomain);
var scope = new ScriptScope(engine, new Scope()).Engine.GetClrModule();
scope.SetVariable("customer", new Customer
{
City = "Stockholm",
Givenname = "Steve",
Street = "AppleRow",
StreetNumber = "23",
Surname = "Mobs",
ZipCode = "14565"
});
Console.WriteLine(PyBlocksRenderer.Render(engine, scope, template));
Output
Hello Steve Mobs! The package will be sent to the following adress AppleRow 23 14565 Stockholm Regards //Some online store
PyBlocksRenderer.cs
public class PyBlocksRenderer
{
/// <summary>
/// Renders the template using the specified engine and scope.
/// </summary>
/// <param name="pythonEngine">The python engine.</param>
/// <param name="scriptScope">The script scope.</param>
/// <param name="template">The template.</param>
/// <returns></returns>
public static string Render(ScriptEngine pythonEngine, ScriptScope scriptScope, string template)
{
//Replace tabs with 4 spaces using a string extension method
template = template.ReplaceTabsWithSpaces();
//Fetch a MatchCollection using a string extension method
var pythonBlocksMatchCollection = template.GetPythonBlockMatches();
//contains the matching {py..py} block as a key and the result as value
var pyBlockMatchResults = new List<KeyValuePair<string, string>>();
foreach (Match pythonBlockMatch in pythonBlocksMatchCollection)
{
//Fetch matching code block code and normalize indentation using a string extension method
var pythonCode = pythonBlockMatch.Groups["code"].Value.NormalizeIndentations();
//Add import to enable writing to the console from within the pythonCode
pythonCode = string.Format("from System import Console{0}{1}", Environment.NewLine, pythonCode);
//Execute script
var pyResult = ExecuteScript(pythonEngine, scriptScope, pythonCode);
//Fix result indentations
var line = template.GetLineAt(pythonBlockMatch.Index);
if (string.IsNullOrEmpty(line.Trim()))
{
var indentation = line.GetIndentation();
var pyLines = pyResult.GetLines().ToList();
var lines = from l in pyLines select pyLines.IndexOf(l) > 0 ? l.Prepend(" ", indentation) : l;
pyResult = string.Join(Environment.NewLine, lines.ToArray());
}
//Add the result of this execution to the list
pyBlockMatchResults.Add(new KeyValuePair<string, string>(pythonBlockMatch.Value, pyResult));
}
//find / replace template py blocks with results
foreach (var replace in pyBlockMatchResults)
template = template.Replace(replace.Key, replace.Value);
return template;
}
/// <summary>
/// Executes the script.
/// </summary>
/// <param name="engine">The engine.</param>
/// <param name="scope">The scope.</param>
/// <param name="code">The code.</param>
/// <returns>code output</returns>
public static string ExecuteScript(ScriptEngine engine, ScriptScope scope, string code)
{
//save reference to standard Console out
var stdOut = Console.Out;
//1: redirect the console to write to a stringwriter
//2: execute the script
//3: return the contents of the stringbuilder
//4: reset Console out to the saved reference above
//Exception: return the exception message
try
{
var sb = new StringBuilder();
Console.SetOut(new StringWriter(sb));
engine.Execute<string>(code, scope);
return sb.ToString();
}
catch (Exception ex)
{
return string.Format("#! {0}", ex.Message);
}
finally
{
Console.SetOut(stdOut);
}
}
}
StringExtensions.cs
public static class StringExtensions
{
/// <summary>
/// Replaces the tabs with spaces.
/// </summary>
/// <param name="str">The STR.</param>
/// <returns></returns>
public static string ReplaceTabsWithSpaces(this string str)
{
return str.Replace("\t", " ");
}
/// <summary>
/// Gets the python blocks.
/// </summary>
/// <param name="template">The template.</param>
/// <returns></returns>
public static MatchCollection GetPythonBlockMatches(this string template)
{
return Regex.Matches(template, "{py(?<code>.*?)py}", RegexOptions.IgnoreCase | RegexOptions.Singleline);
}
/// <summary>
/// Normalizes the indentations.
/// </summary>
/// <param name="str">The STR.</param>
/// <returns></returns>
public static string NormalizeIndentations(this string str)
{
str = str.ReplaceTabsWithSpaces();
var lines = from line in str.GetLines() where !string.IsNullOrEmpty(line.Trim()) select line;
var minimumIndent = lines.GetMinimumIndentation();
return string.Join(Environment.NewLine, (from line in lines select line.Substring(minimumIndent)).ToArray());
}
/// <summary>
/// Gets the lines.
/// </summary>
/// <param name="str">The STR.</param>
/// <returns></returns>
public static string[] GetLines(this string str)
{
return Regex.Split(str, "\\r?\\n");
}
/// <summary>
/// Gets the minimum indentation.
/// </summary>
/// <param name="lines">The lines.</param>
/// <returns></returns>
private static int GetMinimumIndentation(this IEnumerable<string> lines)
{
var minimumIndent = int.MinValue;
foreach (var line in lines)
{
var indent = line.GetIndentation();
minimumIndent = minimumIndent > indent || minimumIndent == int.MinValue ? indent : minimumIndent;
}
return minimumIndent > int.MinValue ? minimumIndent : 0;
}
/// <summary>
/// Gets the indentation.
/// </summary>
/// <param name="str">The STR.</param>
/// <returns></returns>
public static int GetIndentation(this string str)
{
return Regex.Match(str, "^\\s+").Value.Length;
}
/// <summary>
/// Gets the line at.
/// </summary>
/// <param name="str">The STR.</param>
/// <param name="index">The index.</param>
/// <returns></returns>
public static string GetLineAt(this string str, int index)
{
return str.Substring(0, index).GetLines().LastOrDefault() ?? string.Empty;
}
/// <summary>
/// Prepends the specified STR with value n times.
/// </summary>
/// <param name="str">The STR.</param>
/// <param name="value">The value.</param>
/// <param name="times">The times.</param>
/// <returns></returns>
public static string Prepend(this string str, string value, int times)
{
for (var i = 0; i < times; i++)
str = value + str;
return str;
}
}