# How OfficeCLI Merge Command Handles Placeholders Across Paragraphs, Tables, and Headers

> Discover how OfficeCLI's merge command effectively handles {{placeholder}} tokens across paragraphs, tables, and headers. Learn about its single-pass regex substitution for seamless document updates.

- Repository: [OfficeAI/OfficeCLI](https://github.com/iofficeai/OfficeCLI)
- Tags: how-to-guide
- Published: 2026-08-05

---

**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](https://github.com/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`](https://github.com/iOfficeAI/OfficeCLI/blob/main/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:

```csharp
// 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:

```csharp
// 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:

```csharp
// 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:

```csharp
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:

```csharp
// 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

```csharp
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 processed** — `ReplacePlaceholdersInDocx` 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 trail** — `UsedKeys` and `UnresolvedPlaceholders` provide visibility into merge results
- **Implementation location** — all logic resides in [`src/officecli/Core/TemplateMerger.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/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`](https://github.com/iOfficeAI/OfficeCLI/blob/main/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.