How OfficeCLI Merge Command Handles Placeholders Across Paragraphs, Tables, and Headers
The OfficeCLI merge command replaces {{placeholder}} tokens in paragraphs, tables, headers, footers, footnotes, endnotes, and comments by walking every <w:t> text node in the Word document's XML and applying a single-pass regex substitution.
The merge command in the iOfficeAI/OfficeCLI repository provides deterministic template merging for DOCX, XLSX, and PPTX files. Understanding how it processes placeholders across different document structures helps developers build reliable document automation workflows.
Where the Merge Logic Lives
All placeholder handling is implemented in src/officecli/Core/TemplateMerger.cs. The Merge method (lines 57-88) serves as the entry point, dispatching to format-specific routines based on file extension.
The merge workflow follows three distinct phases:
- Data normalization — JSON is flattened into dot- and bracket-notation paths
- Document transformation — placeholders are replaced across all document parts
- Validation — unresolved placeholders are reported to the user
How Placeholders Are Defined
The placeholder pattern is defined as a compiled regex at line 28:
// From TemplateMerger.cs, line 28
private static readonly Regex PlaceholderPattern = new Regex(
@"\{\{\s*(\w[\w.\-\[\] ]*?)\s*\}\}",
RegexOptions.Compiled);
This pattern matches:
- Double curly braces
{{...}} - Optional whitespace inside braces
- Keys starting with a word character
- Keys containing letters, digits, dots, hyphens, brackets, and spaces
- Nested paths like
{{user.name}}or{{items[0].title}}
Processing DOCX Files: The Complete Document Walk
For Word documents, ReplacePlaceholdersInDocx (lines 21-30) enumerates every relevant part:
// From TemplateMerger.cs, lines 21-30
private void ReplacePlaceholdersInDocx(WordprocessingDocument doc, Dictionary<string, string> data)
{
var parts = new List<OpenXmlPart>
{
doc.MainDocumentPart,
// Headers and footers are explicitly collected
..doc.MainDocumentPart.HeaderParts,
..doc.MainDocumentPart.FooterParts,
doc.MainDocumentPart.FootnotesPart,
doc.MainDocumentPart.EndnotesPart,
doc.MainDocumentPart.WordprocessingCommentsPart
};
foreach (var part in parts.Where(p => p != null))
{
ReplaceInPart(part, data);
}
}
Handling Paragraphs, Tables, and All Text Containers
The actual replacement occurs in ReplaceInPart, which walks the XML document:
// Pattern used in SinglePassReplace (lines 62-82)
private void ReplaceInPart(OpenXmlPart part, Dictionary<string, string> data)
{
var root = part.RootElement;
if (root == null) return;
// Every text element is examined, regardless of container
foreach (var textElement in root.Descendants<DocumentFormat.OpenXml.Wordprocessing.Text>())
{
textElement.Text = SinglePassReplace(textElement.Text, data);
}
root.Save();
}
Tables require no special handling because table cells (<w:tc>) contain paragraphs (<w:p>) which contain runs (<w:r>) which contain text elements (<w:t>). The descendant traversal automatically reaches nested structures.
The Single-Pass Replacement Engine
The SinglePassReplace method (lines 62-82) performs substitution while tracking usage:
private string SinglePassReplace(string input, Dictionary<string, string> data)
{
return PlaceholderPattern.Replace(input, match =>
{
var key = match.Groups[1].Value.Trim();
if (data.TryGetValue(key, out var value))
{
_usedKeys.Add(key);
_replacementCount++;
return value;
}
// Unmatched placeholders remain unchanged
return match.Value;
});
}
This approach ensures:
- Deterministic results — one pass, no recursive replacement
- Usage tracking — the
_usedKeysHashSet records which data keys were consumed - Safe fallback — missing keys leave
{{placeholder}}intact for later inspection
Detecting Unresolved Placeholders
After replacement, ScanUnresolvedDocx (lines 85-107) performs a second pass to identify remaining placeholders:
// Usage example from the CLI
var result = TemplateMerger.Merge(
templatePath: "template.docx",
outputPath: "output.docx",
data: data,
force: true);
// result.UnresolvedPlaceholders contains any {{key}} still present
foreach (var unresolved in result.UnresolvedPlaceholders)
{
Console.WriteLine($"Warning: '{unresolved}' was not replaced");
}
The scan concatenates paragraph text (including headers and footers) and applies the same regex pattern to extract unmatched tokens.
Practical Implementation Example
using OfficeCLI.Core;
// 1. Parse merge data from JSON
var json = @"{
""company"": ""Acme Corp"",
""contact"": {
""name"": ""Jane Smith"",
""email"": ""jane@acme.com""
},
""orders"": [{""id"": ""ORD-001"", ""total"": 1500.00}]
}";
var data = TemplateMerger.ParseMergeData(json);
// Flattened keys: "company", "contact.name", "contact.email", "orders[0].id", "orders[0].total"
// 2. Execute merge
var mergeResult = TemplateMerger.Merge(
templatePath: "invoice-template.docx",
outputPath: "invoice-2024-001.docx",
data: data,
force: true); // Overwrite existing output
// 3. Verify completeness
Console.WriteLine($"Keys used: {mergeResult.UsedKeys.Count}");
Console.WriteLine($"Replacements made: {mergeResult.ReplacementCount}");
if (mergeResult.UnresolvedPlaceholders.Any())
{
Console.WriteLine("Unresolved placeholders:");
mergeResult.UnresolvedPlaceholders.ToList().ForEach(p => Console.WriteLine($" - {p}"));
}
Cross-Format Consistency
The same architectural pattern applies to XLSX (MergeXlsx) and PPTX (MergePptx) files. Each implementation:
- Dispatches from the central
Mergemethod - Uses format-appropriate XML traversal
- Reports used keys and unresolved placeholders
- Maintains the identical placeholder syntax
{{key}}
Summary
- All document parts are processed —
ReplacePlaceholdersInDocxexplicitly includes headers, footers, footnotes, endnotes, and comments alongside the main document body - Tables work transparently — cell text is reached through descendant traversal of
<w:t>elements without dedicated table logic - Single-pass regex replacement — the compiled
PlaceholderPatternensures consistent, performant substitution - Complete audit trail —
UsedKeysandUnresolvedPlaceholdersprovide visibility into merge results - Implementation location — all logic resides in
src/officecli/Core/TemplateMerger.cs
Frequently Asked Questions
What placeholder syntax does OfficeCLI merge support?
OfficeCLI uses double curly braces with optional whitespace: {{key}}, {{ key }}, {{user.profile.name}}, or {{items[0].value}}. The regex pattern at line 28 of TemplateMerger.cs permits word characters, dots, hyphens, brackets, and spaces within keys.
Will placeholders in table cells be replaced?
Yes. Table cells contain <w:t> text elements just like regular paragraphs. The Descendants<Text>() traversal in ReplaceInPart reaches all nested structures automatically, so no special table handling is required.
How does the merge command report missing data keys?
After replacement, ScanUnresolvedDocx scans all document parts and returns any remaining {{...}} patterns in the UnresolvedPlaceholders list. The CLI displays these warnings so users can identify incomplete data preparation.
Can I use the same JSON data structure for DOCX, XLSX, and PPTX templates?
Yes. The ParseMergeData method (lines 39-88) normalizes JSON into a flat dictionary before format-specific merging begins. The same data object works across all three output formats, with each Merge* method handling its own XML structure.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →