Mail Merge: Merging Templates with Mustache Syntax in C#
Learn how to merge templates using the mustache syntax in C# using TX Text Control .NET Server. Instead of using the built-in merge fields, this article shows how to use textual placeholders including merge blocks with the mustache syntax.

TX Text Control provides a powerful, MS Word compatible Mail
The following screenshot shows the UI to insert a merge field in the Document Editor:
Mustache Syntax
But what if you have existing templates in other formats, such as TXT or HTML, that don't support MS Word merge fields?
The mustache syntax is a simple template language that can be used to insert values into a template. The following example shows a simple template with merge fields using the mustache syntax:
A merge field is enclosed in double curly braces. It consists entirely of plain text. The name of the merge field is the key that is used to look up the value in the data source.
Merge Blocks
Mustache also supports merge blocks that can be used to repeat content based on a collection of items. The following example shows a simple template with a merge block:
A merge block is enclosed in double curly braces with a hash symbol and ends with a slash. The merge block name is the key used to look up the collection in the data source.
Using Mustache Syntax with MailMerge
In order to use templates created with the mustache syntax in TX Text Control, the template must be converted into a TX Text Control compatible format. The MustacheMatcher class uses regular expressions to find merge fields and merge blocks.
Where is the code?
The complete code of the MustacheMatcher class is available at the end of this article.
The following code finds all merge fields in a string:
private static List<FieldInfo> FindMergeFields(string input)
{
const string pattern = @"\{\{(?!#|/)(.*?)\}\}";
var matches = new List<FieldInfo>();
foreach (Match match in Regex.Matches(input, pattern))
{
matches.Add(new FieldInfo
{
StartIndex = match.Index,
EndIndex = match.Index + match.Length,
FieldName = Regex.Replace(match.Groups[1].Value, @"\s+", "")
});
}
return matches;
}
These results and then used to replace them with actual merge fields:
foreach (var field in FindMergeFields(text))
{
ReplaceWithMergeField(textControl, field);
}
private static void ReplaceWithMergeField(ServerTextControl textControl, FieldInfo field)
{
textControl.Select(field.StartIndex, field.EndIndex - field.StartIndex);
var selectionText = textControl.Selection.Text;
var mergeField = new MergeField
{
Name = field.FieldName,
Text = selectionText,
ApplicationField =
{
DoubledInputPosition = true,
HighlightMode = HighlightMode.Activated
}
};
textControl.Selection.Text = string.Empty;
textControl.ApplicationFields.Add(mergeField.ApplicationField);
}
The same is done for merge blocks, which can also be nested at different hierarchy levels:
private static List<BlockInfo> FindMergeBlocks(string input)
{
const string pattern = @"\{\{(#foreach\s+\w+|\s*\/foreach\s+\w+)\}\}";
var matches = new List<BlockInfo>();
var stack = new Stack<Match>();
foreach (Match match in Regex.Matches(input, pattern))
{
if (match.Value.StartsWith("{{#foreach"))
{
stack.Push(match);
}
else if (match.Value.StartsWith("{{/foreach") && stack.Count > 0)
{
var startMatch = stack.Pop();
var startVar = ExtractVariableName(startMatch.Value);
var endVar = ExtractVariableName(match.Value);
if (startVar == endVar)
{
matches.Add(new BlockInfo
{
StartIndex = startMatch.Index + 1,
EndIndex = match.Index + 1 + match.Length,
BlockName = startVar
});
}
}
}
matches.Sort((x, y) => x.StartIndex.CompareTo(y.StartIndex));
return matches;
}
The results are converted to Sub
foreach (var block in FindMergeBlocks(text))
{
AddSubTextPart(textControl, block);
}
private static void AddSubTextPart(ServerTextControl textControl, BlockInfo block)
{
var subTextPart = new SubTextPart("txmb_" + block.BlockName, 1, block.StartIndex, block.EndIndex - block.StartIndex);
textControl.SubTextParts.Add(subTextPart);
}
Using the MustacheMatcher
The following code snippet shows how to use the MustacheMatcher class to convert a template with mustache syntax into a TX Text Control compatible format:
using TXTextControl.ServerTextControl serverTextControl = new TXTextControl.ServerTextControl();
serverTextControl.Create();
serverTextControl.Load(document, TXTextControl.BinaryStreamType.InternalUnicodeFormat);
MustacheMatcher.Convert(serverTextControl);
The following screenshot shows the result of the conversion and the document after the merge fields have been populated:
Full Sources
The following code shows the complete MustacheMatcher class:
namespace TXTextControl.DocumentServer.Fields
{
using System.Collections.Generic;
using System.Text.RegularExpressions;
using TXTextControl;
public class TagInfo
{
public int StartIndex { get; set; }
public int EndIndex { get; set; }
}
public class FieldInfo : TagInfo
{
public string FieldName { get; set; }
}
public class BlockInfo : TagInfo
{
public string BlockName { get; set; }
}
public class MustacheMatcher
{
public static void Convert(ServerTextControl textControl)
{
var text = textControl.Text.Replace("\r\n", "\n");
// Process merge fields
foreach (var field in FindMergeFields(text))
{
ReplaceWithMergeField(textControl, field);
}
// Process merge blocks
foreach (var block in FindMergeBlocks(text))
{
AddSubTextPart(textControl, block);
}
// Process special elements
RemoveSpecialElements(textControl, text);
}
private static void ReplaceWithMergeField(ServerTextControl textControl, FieldInfo field)
{
textControl.Select(field.StartIndex, field.EndIndex - field.StartIndex);
var selectionText = textControl.Selection.Text;
var mergeField = new MergeField
{
Name = field.FieldName,
Text = selectionText,
ApplicationField =
{
DoubledInputPosition = true,
HighlightMode = HighlightMode.Activated
}
};
textControl.Selection.Text = string.Empty;
textControl.ApplicationFields.Add(mergeField.ApplicationField);
}
private static void AddSubTextPart(ServerTextControl textControl, BlockInfo block)
{
var subTextPart = new SubTextPart("txmb_" + block.BlockName, 1, block.StartIndex, block.EndIndex - block.StartIndex);
textControl.SubTextParts.Add(subTextPart);
}
private static void RemoveSpecialElements(ServerTextControl textControl, string text)
{
var matchElements = FindSpecialElements(text);
int indexOffset = 0;
foreach (var tag in matchElements)
{
textControl.Select(tag.StartIndex - indexOffset, tag.EndIndex - tag.StartIndex);
indexOffset += textControl.Selection.Length;
textControl.Selection.Text = string.Empty;
}
}
private static List<FieldInfo> FindMergeFields(string input)
{
const string pattern = @"\{\{(?!#|/)(.*?)\}\}";
var matches = new List<FieldInfo>();
foreach (Match match in Regex.Matches(input, pattern))
{
matches.Add(new FieldInfo
{
StartIndex = match.Index,
EndIndex = match.Index + match.Length,
FieldName = Regex.Replace(match.Groups[1].Value, @"\s+", "")
});
}
return matches;
}
private static List<TagInfo> FindSpecialElements(string input)
{
const string pattern = @"\{\{(#|\/)(.*?)\}\}";
var matches = new List<TagInfo>();
foreach (Match match in Regex.Matches(input, pattern))
{
matches.Add(new TagInfo
{
StartIndex = match.Index,
EndIndex = match.Index + match.Length
});
}
return matches;
}
private static List<BlockInfo> FindMergeBlocks(string input)
{
const string pattern = @"\{\{(#foreach\s+\w+|\s*\/foreach\s+\w+)\}\}";
var matches = new List<BlockInfo>();
var stack = new Stack<Match>();
foreach (Match match in Regex.Matches(input, pattern))
{
if (match.Value.StartsWith("{{#foreach"))
{
stack.Push(match);
}
else if (match.Value.StartsWith("{{/foreach") && stack.Count > 0)
{
var startMatch = stack.Pop();
var startVar = ExtractVariableName(startMatch.Value);
var endVar = ExtractVariableName(match.Value);
if (startVar == endVar)
{
matches.Add(new BlockInfo
{
StartIndex = startMatch.Index + 1,
EndIndex = match.Index + 1 + match.Length,
BlockName = startVar
});
}
}
}
matches.Sort((x, y) => x.StartIndex.CompareTo(y.StartIndex));
return matches;
}
private static string ExtractVariableName(string tag)
{
var startIndex = tag.StartsWith("{{#foreach") ? 10 : 11;
var length = tag.Length - startIndex - 2;
return tag.Substring(startIndex, length).Trim();
}
}
}
Conclusion
Using the mustache syntax with TX Text Control's MailMerge engine is a powerful way to merge data into templates that are not based on MS Word merge fields. The MustacheMatcher class can be used to convert templates with mustache syntax into a TX Text Control compatible format.
Download and Fork This Sample on GitHub
We proudly host our sample code on github.com/TextControl.
Please fork and contribute.
Requirements for this sample
- TX Text Control .NET Server
ASP.NET
Integrate document processing into your applications to create documents such as PDFs and MS Word documents, including client-side document editing, viewing, and electronic signatures.
- Angular
- Blazor
- React
- JavaScript
- ASP.NET MVC, ASP.NET Core, and WebForms
Related Post
Programmatically Convert MS Word DOCX Documents to PDF in .NET C#
This article shows how to convert MS Word DOCX documents to PDF in .NET C# using the ServerTextControl component. The example shows how to load a DOCX file from a file or from a variable and how…