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:

  1. Data normalization — JSON is flattened into dot- and bracket-notation paths
  2. Document transformation — placeholders are replaced across all document parts
  3. 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 _usedKeys HashSet 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 Merge method
  • Uses format-appropriate XML traversal
  • Reports used keys and unresolved placeholders
  • Maintains the identical placeholder syntax {{key}}

Summary

  • All document parts are processedReplacePlaceholdersInDocx explicitly 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 PlaceholderPattern ensures consistent, performant substitution
  • Complete audit trailUsedKeys and UnresolvedPlaceholders provide 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →