PyBlocks

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;
        }
    }
  1. Very clever.
    I’m always in doubt if I should bring in a templating engine for e-mails or it it’s just too much muck for too little buck.

Leave a Comment


NOTE - You can use these HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>