← Back to SKILL.md1#!/usr/bin/env python32# MIT License. Part of the Hermes docx skill.3"""Fill {{placeholder}} tokens in a .docx from a JSON mapping.4 5Tokens are replaced everywhere: body paragraphs, tables (including nested6tables), headers and footers. Run formatting is preserved; tokens split7across runs are handled.8 9Usage:10 docx_template.py template.docx values.json output.docx11 docx_template.py template.docx values.json output.docx --strict12 13values.json: {"name": "Ada", "date": "2026-01-01"} fills {{name}}, {{date}}.14With --strict, exits 1 if any {{token}} remains unfilled after processing.15"""16from __future__ import annotations17 18import argparse19import json20import re21import sys22 23from docx import Document24 25from docx_common import iter_all_paragraphs, replace_in_paragraph26 27TOKEN_RE = re.compile(r"\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}")28 29 30def main() -> int:31 ap = argparse.ArgumentParser(32 description="Fill {{token}} placeholders in a .docx from JSON.")33 ap.add_argument("template", help="input .docx with {{tokens}}")34 ap.add_argument("values", help="JSON file of token -> value")35 ap.add_argument("output", help="output .docx path")36 ap.add_argument("--strict", action="store_true",37 help="fail if any token remains unfilled")38 args = ap.parse_args()39 40 with open(args.values, encoding="utf-8") as f:41 values = json.load(f)42 43 doc = Document(args.template)44 filled = {}45 for para in iter_all_paragraphs(doc):46 # Normalize whitespace variants like {{ name }} first.47 for m in set(TOKEN_RE.findall(para.text)):48 if m in values:49 # Replace any spacing variant with canonical token, then fill.50 for variant in set(51 t.group(0) for t in TOKEN_RE.finditer(para.text)52 if t.group(1) == m):53 n = replace_in_paragraph(para, variant, str(values[m]))54 filled[m] = filled.get(m, 0) + n55 56 remaining = sorted({m for para in iter_all_paragraphs(doc)57 for m in TOKEN_RE.findall(para.text)})58 doc.save(args.output)59 result = {"ok": True, "output": args.output, "filled": filled,60 "unfilled_tokens": remaining}61 if args.strict and remaining:62 result["ok"] = False63 print(json.dumps(result, ensure_ascii=False))64 return 165 print(json.dumps(result, ensure_ascii=False))66 return 067 68 69if __name__ == "__main__":70 sys.exit(main())71