docx-placeholder-replacement · git:20260424.d50cf5a · 2026-04-24 · sha256 4496ef0169492e19
docx-placeholder-replacement git:20260424.d50cf5aA
Immutable. This exact content never changes and is served at /api/v1/blob/4496ef0169492e19.
---
name: docx-placeholder-replacement
description: Robustly replace {{PLACEHOLDER}} tokens in Word documents, handling split runs across paragraphs, tables, headers, and footers.
---
# Robust Placeholder Replacement in Word Documents
## The Split-Run Problem
Word's XML engine often breaks a single word like `{{CANDIDATE_NAME}}` across multiple runs:
```
run[0]: '{{CANDI'
run[1]: 'DATE_NAME}}'
```
A naive `run.text.replace()` will miss these. Always reconstruct from `para.text`.
## Safe Replacement Algorithm
```python
import re
def replace_in_paragraph(para, data):
"""Replace all {{KEY}} placeholders in paragraph, handling split runs."""
full_text = para.text
pattern = re.compile(r'\{\{([A-Z0-9_]+)\}\}')
if not pattern.search(full_text):
return # Nothing to do
# Build replacement
def replacer(m):
key = m.group(1)
return str(data[key]) if key in data else m.group(0)
new_text = pattern.sub(replacer, full_text)
if new_text != full_text and para.runs:
# Put all text in first run, clear the rest
para.runs[0].text = new_text
for run in para.runs[1:]:
run.text = ''
```
## Apply to Entire Document
```python
def replace_all_placeholders(doc, data):
"""Replace placeholders everywhere: body, tables, headers, footers."""
# Body paragraphs
for para in doc.paragraphs:
replace_in_paragraph(para, data)
# Tables (with nested table recursion)
def process_table(table):
for row in table.rows:
for cell in row.cells:
for para in cell.paragraphs:
replace_in_paragraph(para, data)
for nested in cell.tables:
process_table(nested)
for table in doc.tables:
process_table(table)
# Headers and footers
for section in doc.sections:
for para in section.header.paragraphs:
replace_in_paragraph(para, data)
for para in section.footer.paragraphs:
replace_in_paragraph(para, data)
```
## Notes
- Only the first run's formatting is preserved after merging. If parts of the placeholder text have different formatting (e.g., bold `{{NAME}}`), that formatting is lost — which is usually acceptable for template filling.
- Unknown placeholders (keys not in `data`) are left as-is by default. Adjust the `replacer` function to raise errors or substitute empty strings if needed.