
Document Docx
- 365 installs
- 73 repo stars
- Updated July 13, 2026
- vasilyu1983/ai-agents-public
document-docx is an agent skill that creates, edits, and parses Microsoft Word .docx files using python-docx, docxtpl, mammoth.js, and the Node docx library for developers generating reports, contracts, and template-driv
About
document-docx is an agent skill from vasilyu1983/ai-agents-public that enables programmatic creation, editing, and analysis of Microsoft Word .docx files for reports, contracts, proposals, and template-driven outputs. The skill routes tasks to python-docx and docxtpl in Python for structural edits and mail-merge templates, the Node.js docx library for TypeScript server-side generation, and mammoth.js for DOCX-to-HTML conversion and text extraction. Developers reach for document-docx when generating styled Word reports, filling docxtpl templates non-developers can edit in Word, extracting tables and metadata, or inspecting tracked changes via OOXML tooling. document-docx notes that legacy .doc files require LibreOffice conversion first, python-docx cannot create true tracked changes reliably, and tables of contents refresh only after opening in Word. Install with npx skills add vasilyu1983/ai-agents-public --skill document-docx when automating document pipelines that must output .docx artifacts instead of PDF-only exports.
- document-docx
- Documentation
- AI-coding skill
Document Docx by the numbers
- 365 all-time installs (skills.sh)
- +5 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #423 of 1,879 Documentation skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/vasilyu1983/ai-agents-public --skill document-docxAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 365 |
|---|---|
| repo stars | ★ 73 |
| Last updated | July 13, 2026 |
| Repository | vasilyu1983/ai-agents-public ↗ |
How do you generate Word docx files programmatically?
Helps with documentation tasks.
Who is it for?
Developers automating Word report, contract, or mail-merge workflows in Python or Node.js who need library-specific guidance.
Skip if: Teams needing native .doc legacy editing or reliable tracked-change authoring purely through python-docx without Word or OOXML tooling.
When should I use this skill?
User asks to create, edit, parse, or convert .docx files, Word reports, contracts, docxtpl templates, or mammoth HTML extraction.
What you get
Styled .docx files, template-filled documents, extracted text or tables, and optional DOCX-to-HTML conversions.
- Generated .docx files
- Template-filled documents
- Extracted text or HTML output
By the numbers
- Covers 7 document task types across Python and Node.js libraries
- Supports python-docx, docxtpl, mammoth.js, and Node docx workflows
Files
Document DOCX Skill - Quick Reference
This skill enables creation, editing, and analysis of .docx files for reports, contracts, proposals, documentation, and template-driven outputs.
Modern best practices (2026):
- Prefer templates + styles over manual formatting.
- Treat
.docxas the editable source; treat PDF as a release artifact. - If distributing externally, include basic accessibility hygiene (headings, table headers, alt text).
Quick Reference
| Task | Tool/Library | Language | When to Use |
|---|---|---|---|
| Create DOCX | python-docx | Python | Reports, contracts, proposals |
| Create DOCX | docx | Node.js | Server-side document generation |
| Convert to HTML | mammoth.js | Node.js | Web display, content extraction |
| Parse DOCX | python-docx | Python | Extract text, tables, metadata |
| Template fill | docxtpl | Python | Mail merge, template-based generation |
| Review workflow | Word compare, comments/highlights | Any | Human review without OOXML surgery |
| Tracked changes | OOXML inspection, docx4j/OpenXML SDK/Aspose | Any | True redlines or parsing tracked changes |
Tool Selection
- Prefer
docxtplwhen non-developers must edit layout/design in Word. - Prefer
python-docxfor structural edits (paragraphs/tables/headers/footers) when formatting complexity is moderate. - Prefer
docx(Node.js) for server-side generation in TypeScript-heavy stacks. - Prefer
mammothfor text-first extraction or DOCX-to-HTML (best effort; may drop some layout fidelity).
Known Limits (Plan Around These)
.doc(legacy) is not supported by these libraries; convert to.docxfirst (e.g., LibreOffice).python-docxcannot reliably create true tracked changes; use Word compare or specialized OOXML tooling.- Tables of Contents and many fields are placeholders until opened/updated in Word.
Core Operations
Create Document (Python - python-docx)
from docx import Document
from docx.shared import Inches, Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH
doc = Document()
# Title
title = doc.add_heading('Document Title', 0)
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
# Paragraph with formatting
para = doc.add_paragraph()
run = para.add_run('Bold and ')
run.bold = True
run = para.add_run('italic text.')
run.italic = True
# Table
table = doc.add_table(rows=3, cols=3)
table.style = 'Table Grid'
for i, row in enumerate(table.rows):
for j, cell in enumerate(row.cells):
cell.text = f'Row {i+1}, Col {j+1}'
# Image
doc.add_picture('image.png', width=Inches(4))
# Save
doc.save('output.docx')Create Document (Node.js - docx)
import { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell } from 'docx';
import * as fs from 'fs';
const doc = new Document({
sections: [{
properties: {},
children: [
new Paragraph({
children: [
new TextRun({ text: 'Bold text', bold: true }),
new TextRun({ text: ' and normal text.' }),
],
}),
new Table({
rows: [
new TableRow({
children: [
new TableCell({ children: [new Paragraph('Cell 1')] }),
new TableCell({ children: [new Paragraph('Cell 2')] }),
],
}),
],
}),
],
}],
});
Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync('output.docx', buffer);
});Template-Based Generation (Python - docxtpl)
from docxtpl import DocxTemplate
doc = DocxTemplate('template.docx')
context = {
'company_name': 'Acme Corp',
'date': '2025-01-15',
'items': [
{'name': 'Widget A', 'price': 100},
{'name': 'Widget B', 'price': 200},
]
}
doc.render(context)
doc.save('filled_template.docx')Extract Content (Python - python-docx)
from docx import Document
doc = Document('input.docx')
# Extract all text
full_text = []
for para in doc.paragraphs:
full_text.append(para.text)
# Extract tables
for table in doc.tables:
for row in table.rows:
row_data = [cell.text for cell in row.cells]
print(row_data)Styling Reference
| Element | Python Method | Node.js Class |
|---|---|---|
| Heading 1 | add_heading(text, 1) | HeadingLevel.HEADING_1 |
| Bold | run.bold = True | TextRun({ bold: true }) |
| Italic | run.italic = True | TextRun({ italics: true }) |
| Font size | run.font.size = Pt(12) | TextRun({ size: 24 }) (half-points) |
| Alignment | WD_ALIGN_PARAGRAPH.CENTER | AlignmentType.CENTER |
| Page break | doc.add_page_break() | new PageBreak() |
Do / Avoid (Dec 2025)
Do
- Use consistent heading levels and a table of contents for long docs.
- Capture decisions and action items with owners and due dates.
- Store docs in a versioned, searchable system.
Avoid
- Manual formatting instead of styles (breaks consistency).
- Docs with no owner or review cadence (stale quickly).
- Copy/pasting without updating definitions and links.
Output Quality Checklist
- Structure: consistent heading hierarchy, styles, and (when needed) an auto-generated table of contents.
- Decisions: decisions/actions captured with owner + due date (not buried in prose).
- Versioning: doc ID + version + change summary; review cadence defined.
- Accessibility hygiene: headings/reading order are correct; table headers are marked; alt text for non-decorative images.
- Reuse: use
assets/doc-template-pack.mdfor decision logs and recurring doc types.
Optional: AI / Automation
Use only when explicitly requested and policy-compliant.
- Summarize meeting notes into decisions/actions; humans verify accuracy.
- Draft first-pass docs from outlines; do not invent facts or quotes.
Navigation
Resources
- references/docx-patterns.md - Advanced formatting, styles, headers/footers
- references/template-workflows.md - Mail merge, batch generation
- references/tracked-changes.md - Tracked changes: what is feasible, and what is not
- references/accessibility-compliance.md - WCAG 2.2 AA, reading order, alt text, EU EAA
- references/cross-platform-compatibility.md - Rendering across Word, Google Docs, LibreOffice
- references/document-automation-pipelines.md - CI/CD batch generation, quality gates
- data/sources.json - Library documentation links
Scripts
scripts/docx_inspect_ooxml.py- Dependency-free OOXML inspection (including tracked changes signals)scripts/docx_extract.py- Extract text/tables to JSON (requirespython-docx)scripts/docx_render_template.py- Render adocxtpltemplate (requiresdocxtpl)scripts/docx_to_html.mjs- Convert.docxto HTML (requiresmammoth)
Templates
- assets/report-template.md - Standard report structure
- assets/contract-template.md - Legal document structure
- assets/doc-template-pack.md - Decision log, meeting notes, changelog templates
Related Skills
- ../document-pdf/SKILL.md - PDF generation and conversion
- ../docs-codebase/SKILL.md - Technical writing patterns
Fact-Checking
- Use web search/web fetch to verify current external facts, versions, pricing, deadlines, regulations, or platform behavior before final answers.
- Prefer primary sources; report source links and dates for volatile information.
- If web access is unavailable, state the limitation and mark guidance as unverified.
Contract Template
Copy-paste structure for legal documents, agreements, and formal contracts.
---
Contract Structure
CONTRACT DOCUMENT
├── Header Block
│ ├── Agreement title
│ ├── Contract number (optional)
│ └── Effective date
├── Parties Section
│ ├── Party A (full legal name, address)
│ └── Party B (full legal name, address)
├── Recitals (WHEREAS clauses)
│ ├── Background context
│ └── Purpose of agreement
├── Definitions
│ └── Key terms defined
├── Terms and Conditions
│ ├── 1. Scope of Work/Services
│ ├── 2. Term and Termination
│ ├── 3. Compensation/Payment
│ ├── 4. Confidentiality
│ ├── 5. Intellectual Property
│ ├── 6. Representations & Warranties
│ ├── 7. Limitation of Liability
│ ├── 8. Indemnification
│ └── 9. General Provisions
│ ├── Governing Law
│ ├── Dispute Resolution
│ ├── Notices
│ ├── Entire Agreement
│ └── Amendments
├── Signature Block
│ ├── Party A signature, name, title, date
│ └── Party B signature, name, title, date
└── Exhibits/Schedules
├── Exhibit A: Scope of Work
├── Exhibit B: Pricing
└── Exhibit C: SLA (if applicable)---
Python Implementation
Contract Generator
from docx import Document
from docx.shared import Inches, Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT
from datetime import datetime
def create_contract(contract_data: dict, output_path: str):
"""Generate formal contract document."""
doc = Document()
# Set narrow margins for legal documents
for section in doc.sections:
section.top_margin = Inches(1)
section.bottom_margin = Inches(1)
section.left_margin = Inches(1.25)
section.right_margin = Inches(1.25)
# ----- HEADER -----
title = doc.add_heading(contract_data['title'], 0)
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
if contract_data.get('contract_number'):
num_para = doc.add_paragraph()
num_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
num_para.add_run(f"Contract No. {contract_data['contract_number']}")
date_para = doc.add_paragraph()
date_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
date_para.add_run(f"Effective Date: {contract_data['effective_date']}")
doc.add_paragraph() # Spacing
# ----- PARTIES -----
doc.add_paragraph(
f"This Agreement is entered into by and between:"
)
parties_para = doc.add_paragraph()
parties_para.add_run(f"{contract_data['party_a']['name']}").bold = True
parties_para.add_run(f", a {contract_data['party_a']['type']} ")
parties_para.add_run(f"located at {contract_data['party_a']['address']} ")
parties_para.add_run('("Party A"); and')
parties_para2 = doc.add_paragraph()
parties_para2.add_run(f"{contract_data['party_b']['name']}").bold = True
parties_para2.add_run(f", a {contract_data['party_b']['type']} ")
parties_para2.add_run(f"located at {contract_data['party_b']['address']} ")
parties_para2.add_run('("Party B").')
doc.add_paragraph()
# ----- RECITALS -----
doc.add_heading('RECITALS', 1)
for i, recital in enumerate(contract_data.get('recitals', []), 1):
para = doc.add_paragraph()
para.add_run(f"WHEREAS, ").bold = True
para.add_run(recital)
now_para = doc.add_paragraph()
now_para.add_run("NOW, THEREFORE, ").bold = True
now_para.add_run(
"in consideration of the mutual covenants and agreements herein, "
"the parties agree as follows:"
)
doc.add_paragraph()
# ----- DEFINITIONS -----
if contract_data.get('definitions'):
doc.add_heading('1. DEFINITIONS', 1)
for term, definition in contract_data['definitions'].items():
para = doc.add_paragraph()
para.add_run(f'"{term}"').bold = True
para.add_run(f" means {definition}")
# ----- NUMBERED SECTIONS -----
section_num = 2 if contract_data.get('definitions') else 1
for section in contract_data.get('sections', []):
doc.add_heading(f"{section_num}. {section['title'].upper()}", 1)
if isinstance(section['content'], str):
doc.add_paragraph(section['content'])
elif isinstance(section['content'], list):
for i, item in enumerate(section['content'], 1):
para = doc.add_paragraph()
para.add_run(f"{section_num}.{i} ").bold = True
para.add_run(item)
section_num += 1
# ----- SIGNATURE BLOCK -----
doc.add_page_break()
doc.add_heading('SIGNATURES', 1)
doc.add_paragraph(
"IN WITNESS WHEREOF, the parties have executed this Agreement "
f"as of the date first written above."
)
doc.add_paragraph()
doc.add_paragraph()
# Create signature table
sig_table = doc.add_table(rows=4, cols=2)
sig_table.alignment = WD_TABLE_ALIGNMENT.CENTER
# Party A signature
sig_table.cell(0, 0).text = contract_data['party_a']['name'].upper()
sig_table.cell(1, 0).text = "_" * 40
sig_table.cell(2, 0).text = "Signature"
sig_table.cell(3, 0).text = "Name: ________________ Title: ________________ Date: ________"
# Party B signature
sig_table.cell(0, 1).text = contract_data['party_b']['name'].upper()
sig_table.cell(1, 1).text = "_" * 40
sig_table.cell(2, 1).text = "Signature"
sig_table.cell(3, 1).text = "Name: ________________ Title: ________________ Date: ________"
# ----- EXHIBITS -----
if contract_data.get('exhibits'):
doc.add_page_break()
for exhibit in contract_data['exhibits']:
doc.add_heading(f"EXHIBIT {exhibit['letter']}: {exhibit['title']}", 1)
doc.add_paragraph(exhibit.get('content', '[To be attached]'))
doc.save(output_path)
return output_path
# ----- USAGE EXAMPLE -----
contract_data = {
'title': 'SERVICE AGREEMENT',
'contract_number': 'SA-2025-001',
'effective_date': 'January 15, 2025',
'party_a': {
'name': 'Acme Corporation',
'type': 'Delaware corporation',
'address': '123 Main Street, Wilmington, DE 19801'
},
'party_b': {
'name': 'TechServices LLC',
'type': 'California limited liability company',
'address': '456 Innovation Drive, San Francisco, CA 94105'
},
'recitals': [
'Party A desires to engage Party B to provide certain professional services;',
'Party B has the expertise and resources to provide such services;',
'The parties wish to set forth the terms under which Party B will provide services to Party A.'
],
'definitions': {
'Services': 'the professional services described in Exhibit A.',
'Deliverables': 'the work product to be delivered under this Agreement.',
'Confidential Information': 'any non-public information disclosed by either party.',
'Term': 'the period beginning on the Effective Date and continuing for twelve (12) months.'
},
'sections': [
{
'title': 'Scope of Services',
'content': [
'Party B shall provide the Services described in Exhibit A.',
'Party B shall perform all Services in a professional and workmanlike manner.',
'Party B shall comply with all applicable laws and regulations.'
]
},
{
'title': 'Compensation',
'content': [
'Party A shall pay Party B the fees set forth in Exhibit B.',
'Payment shall be due within thirty (30) days of invoice.',
'Late payments shall bear interest at 1.5% per month.'
]
},
{
'title': 'Term and Termination',
'content': [
'This Agreement shall remain in effect for the Term unless earlier terminated.',
'Either party may terminate with thirty (30) days written notice.',
'Upon termination, Party B shall deliver all Deliverables completed to date.'
]
},
{
'title': 'Confidentiality',
'content': 'Each party agrees to maintain the confidentiality of the other party\'s Confidential Information and not to disclose such information to third parties without prior written consent.'
},
{
'title': 'Limitation of Liability',
'content': 'IN NO EVENT SHALL EITHER PARTY BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THIS AGREEMENT.'
},
{
'title': 'Governing Law',
'content': 'This Agreement shall be governed by and construed in accordance with the laws of the State of Delaware, without regard to conflicts of law principles.'
}
],
'exhibits': [
{'letter': 'A', 'title': 'SCOPE OF WORK', 'content': '[Detailed scope to be attached]'},
{'letter': 'B', 'title': 'PRICING SCHEDULE', 'content': '[Fee schedule to be attached]'}
]
}
create_contract(contract_data, 'service_agreement.docx')---
docxtpl Template Version
Template File (contract_template.docx)
{{ contract_title }}
Contract No. {{ contract_number }}
Effective Date: {{ effective_date }}
This Agreement is entered into by and between:
{{ party_a_name }}, a {{ party_a_type }} located at {{ party_a_address }} ("Party A"); and
{{ party_b_name }}, a {{ party_b_type }} located at {{ party_b_address }} ("Party B").
RECITALS
{% for recital in recitals %}
WHEREAS, {{ recital }}
{% endfor %}
NOW, THEREFORE, in consideration of the mutual covenants herein, the parties agree:
{% for section in sections %}
{{ loop.index }}. {{ section.title }}
{{ section.content }}
{% endfor %}
----------------------------------------
SIGNATURES
IN WITNESS WHEREOF, the parties have executed this Agreement as of the Effective Date.
{{ party_a_name }} {{ party_b_name }}
_________________________ _________________________
Signature Signature
Name: ___________________ Name: ___________________
Title: ___________________ Title: ___________________
Date: ___________________ Date: ___________________Python Fill Script
from docxtpl import DocxTemplate
doc = DocxTemplate("contract_template.docx")
context = {
'contract_title': 'MASTER SERVICE AGREEMENT',
'contract_number': 'MSA-2025-042',
'effective_date': 'February 1, 2025',
'party_a_name': 'Global Industries Inc.',
'party_a_type': 'Nevada corporation',
'party_a_address': '789 Corporate Blvd, Las Vegas, NV 89101',
'party_b_name': 'Premier Consulting Group',
'party_b_type': 'Texas limited partnership',
'party_b_address': '321 Business Park, Austin, TX 78701',
'recitals': [
'Party A requires consulting services for digital transformation;',
'Party B specializes in enterprise digital solutions;',
'Both parties wish to formalize their business relationship.'
],
'sections': [
{'title': 'SERVICES', 'content': 'Party B shall provide consulting services as detailed in the attached Statement of Work.'},
{'title': 'TERM', 'content': 'This Agreement shall have an initial term of one (1) year.'},
{'title': 'FEES', 'content': 'Party A shall pay fees as specified in the applicable Statement of Work.'},
{'title': 'CONFIDENTIALITY', 'content': 'Both parties shall maintain confidentiality of proprietary information.'},
{'title': 'GOVERNING LAW', 'content': 'This Agreement is governed by the laws of the State of Texas.'},
]
}
doc.render(context)
doc.save('master_service_agreement.docx')---
Common Contract Types
| Type | Key Sections | Special Considerations |
|---|---|---|
| NDA | Definition of Confidential Info, Term, Return of Materials | One-way vs mutual |
| Service Agreement | Scope, Deliverables, Payment, Term | SOW attachments |
| Employment | Position, Compensation, Benefits, Termination | At-will language |
| License | Grant, Restrictions, Fees, Term | IP ownership |
| SaaS | Service Levels, Data Security, Uptime | SLA attachment |
---
Legal Formatting Best Practices
Typography
| Element | Formatting |
|---|---|
| Section headers | ALL CAPS, Bold |
| Subsections | Title Case, Bold |
| Definitions | "Term" in quotes, bold on first use |
| Cross-references | "Section X" capitalized |
Numbering
1. FIRST LEVEL SECTION
1.1 Second level subsection
(a) Third level item
(b) Third level item
(i) Fourth level itemBoilerplate Language
Standard clauses to include:
- Entire Agreement: This Agreement constitutes the entire agreement...
- Severability: If any provision is held invalid...
- Waiver: Failure to enforce any provision...
- Assignment: Neither party may assign without consent...
- Notices: All notices shall be in writing...
- Counterparts: May be executed in counterparts...
---
Related Resources
- SKILL.md - Quick reference
- report-template.md - Report structure
- docx-patterns.md - Advanced formatting
Doc Template Pack (Core, Non-AI)
Purpose: copy-paste templates for common internal docs (decision logs, meeting notes, changelog entries).
Inputs
- Context and stakeholders
- Links to source artifacts (tickets, PRDs, PRs, dashboards)
Outputs
- Consistent docs that are searchable, reviewable, and decision-oriented
Core
1) Decision Log Entry
Title: {{DECISION_TITLE}} Date: {{DATE}} Owner: {{OWNER}} Status: Proposed / Accepted / Rejected / Superseded
Decision:
- We will: {{DECISION}}
Context:
- Why now: {{WHY_NOW}}
- Constraints: {{CONSTRAINTS}}
- Options considered: {{OPTIONS}}
Decision rules:
- We choose this because: {{RATIONALE}}
- We will revisit if: {{REVISIT_TRIGGERS}}
Risks:
- {{RISK_1}}
- {{RISK_2}}
Links:
- Ticket/PRD: {{LINKS}}
2) Meeting Notes
Meeting: {{MEETING_NAME}} Date/time: {{DATE_TIME}} Owner: {{OWNER}} Attendees: {{ATTENDEES}}
Goal:
- {{GOAL}}
Agenda:
- {{AGENDA}}
Notes (one idea per line):
- {{NOTES}}
Decisions:
- {{DECISIONS}}
Action items:
| Action | Owner | Due | Status |
|---|---|---|---|
| {{ACTION}} | {{OWNER}} | {{DATE}} | Not started |
Open questions:
- {{QUESTIONS}}
3) Changelog Entry
Release: {{VERSION_OR_DATE}} Owner: {{OWNER}}
Added:
- {{ITEM}}
Changed:
- {{ITEM}}
Fixed:
- {{ITEM}}
Deprecated:
- {{ITEM}}
Security:
- {{ITEM}}
Links:
- PRs/issues: {{LINKS}}
Decision Rules
- If a decision is made, it must be recorded with owner + rationale + revisit triggers.
- If a meeting creates action items, capture owner + due date in the notes.
Risks
- Docs without owners go stale
- Meetings without decisions create churn
- Changelogs without links are not auditable
Optional: AI / Automation
Use only if allowed by policy and data handling rules.
- Summarize long meeting transcripts into decisions/actions; humans verify accuracy.
- Draft changelog entries from PR titles; humans edit and ensure correctness.
Report Template
Copy-paste structure for professional reports with python-docx or docxtpl.
---
Report Structure
REPORT DOCUMENT
├── Title Page
│ ├── Report title (Heading 0, centered)
│ ├── Subtitle/date
│ ├── Author/organization
│ └── Logo (optional)
├── Table of Contents
├── Executive Summary (1 page max)
├── Body Sections
│ ├── Introduction/Background
│ ├── Methodology (if applicable)
│ ├── Findings/Results
│ │ ├── Section 1 with tables/charts
│ │ ├── Section 2 with data
│ │ └── Section 3 with analysis
│ ├── Discussion
│ └── Recommendations
├── Conclusion
├── Appendices
│ ├── Appendix A: Data Tables
│ ├── Appendix B: Methodology Details
│ └── Appendix C: References
└── Footer (page numbers, confidentiality notice)---
Python Implementation
Full Report Generator
from docx import Document
from docx.shared import Inches, Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.section import WD_ORIENT
from datetime import datetime
def create_report(title: str, author: str, sections: list, output_path: str):
"""Generate professional report document."""
doc = Document()
# ----- TITLE PAGE -----
# Add spacing before title
for _ in range(5):
doc.add_paragraph()
# Report title
title_para = doc.add_heading(title, 0)
title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
# Subtitle/date
date_para = doc.add_paragraph()
date_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
date_run = date_para.add_run(datetime.now().strftime("%B %Y"))
date_run.font.size = Pt(14)
# Author
author_para = doc.add_paragraph()
author_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
author_para.add_run(f"Prepared by: {author}")
# Page break after title
doc.add_page_break()
# ----- TABLE OF CONTENTS -----
doc.add_heading('Table of Contents', 1)
toc_para = doc.add_paragraph()
toc_para.add_run('[Update field in Word to generate TOC]').italic = True
doc.add_page_break()
# ----- EXECUTIVE SUMMARY -----
doc.add_heading('Executive Summary', 1)
doc.add_paragraph(
'This section provides a high-level overview of the report findings, '
'key insights, and recommendations. Keep to one page maximum.'
)
doc.add_page_break()
# ----- BODY SECTIONS -----
for section in sections:
doc.add_heading(section['title'], section.get('level', 1))
if 'content' in section:
doc.add_paragraph(section['content'])
if 'table' in section:
add_table(doc, section['table'])
if 'bullets' in section:
for bullet in section['bullets']:
doc.add_paragraph(bullet, style='List Bullet')
# ----- APPENDICES -----
doc.add_page_break()
doc.add_heading('Appendices', 1)
doc.add_heading('Appendix A: Supporting Data', 2)
doc.add_paragraph('[Insert supporting data tables here]')
# ----- FOOTER -----
section = doc.sections[0]
footer = section.footer
footer_para = footer.paragraphs[0]
footer_para.text = f"Confidential | {datetime.now().year}"
footer_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
doc.save(output_path)
return output_path
def add_table(doc, table_data: dict):
"""Add formatted table to document."""
headers = table_data['headers']
rows = table_data['rows']
table = doc.add_table(rows=len(rows) + 1, cols=len(headers))
table.style = 'Table Grid'
# Header row
for i, header in enumerate(headers):
cell = table.rows[0].cells[i]
cell.text = header
cell.paragraphs[0].runs[0].bold = True
# Data rows
for row_idx, row_data in enumerate(rows):
for col_idx, cell_data in enumerate(row_data):
table.rows[row_idx + 1].cells[col_idx].text = str(cell_data)
doc.add_paragraph() # Spacing after table
# ----- USAGE EXAMPLE -----
sections = [
{
'title': 'Introduction',
'level': 1,
'content': 'This report analyzes the quarterly performance metrics...'
},
{
'title': 'Methodology',
'level': 1,
'content': 'Data was collected from multiple sources including...',
'bullets': [
'Internal CRM system (Q1-Q4 2024)',
'Customer surveys (n=500)',
'Market research reports'
]
},
{
'title': 'Key Findings',
'level': 1,
'content': 'Analysis revealed the following trends:'
},
{
'title': 'Revenue Analysis',
'level': 2,
'table': {
'headers': ['Quarter', 'Revenue', 'Growth'],
'rows': [
['Q1', '$1.2M', '+5%'],
['Q2', '$1.4M', '+17%'],
['Q3', '$1.3M', '-7%'],
['Q4', '$1.8M', '+38%'],
]
}
},
{
'title': 'Recommendations',
'level': 1,
'bullets': [
'Increase marketing spend in Q2 to capitalize on seasonal trends',
'Implement customer retention program',
'Expand into adjacent market segments'
]
},
{
'title': 'Conclusion',
'level': 1,
'content': 'Based on the analysis, we recommend proceeding with...'
}
]
create_report(
title="Quarterly Performance Report",
author="Analytics Team",
sections=sections,
output_path="quarterly_report.docx"
)---
docxtpl Template Version
Template File (report_template.docx)
Create in Word with this structure:
{{ report_title }}
{{ subtitle }}
Prepared by: {{ author }}
Date: {{ date }}
----------------------------------------
TABLE OF CONTENTS
[Update in Word]
----------------------------------------
EXECUTIVE SUMMARY
{{ executive_summary }}
----------------------------------------
{% for section in sections %}
{{ section.title }}
{{ section.content }}
{%tr for row in section.table_rows %}
| {{ row.col1 }} | {{ row.col2 }} | {{ row.col3 }} |
{%tr endfor %}
{% endfor %}
----------------------------------------
APPENDICES
{{ appendix_content }}Python Fill Script
from docxtpl import DocxTemplate
from datetime import datetime
doc = DocxTemplate("report_template.docx")
context = {
'report_title': 'Annual Performance Review',
'subtitle': 'Fiscal Year 2024',
'author': 'Strategic Planning Team',
'date': datetime.now().strftime('%B %d, %Y'),
'executive_summary': '''
This annual review summarizes key achievements, challenges, and
strategic recommendations for the upcoming fiscal year. Overall
performance exceeded targets by 12%, driven by strong Q4 results.
''',
'sections': [
{
'title': 'Financial Performance',
'content': 'Revenue grew 18% year-over-year...',
'table_rows': [
{'col1': 'Metric', 'col2': '2023', 'col3': '2024'},
{'col1': 'Revenue', 'col2': '$4.2M', 'col3': '$4.9M'},
{'col1': 'Profit', 'col2': '$0.8M', 'col3': '$1.1M'},
]
},
{
'title': 'Strategic Initiatives',
'content': 'Three major initiatives were completed...',
'table_rows': []
}
],
'appendix_content': 'Detailed data available upon request.'
}
doc.render(context)
doc.save('annual_review_2024.docx')---
Style Guide
Typography
| Element | Font | Size | Style |
|---|---|---|---|
| Title | Calibri Light | 28pt | Bold, Centered |
| Heading 1 | Calibri | 16pt | Bold |
| Heading 2 | Calibri | 14pt | Bold |
| Body | Calibri | 11pt | Normal |
| Caption | Calibri | 10pt | Italic |
Spacing
| Element | Before | After |
|---|---|---|
| Heading 1 | 24pt | 12pt |
| Heading 2 | 18pt | 6pt |
| Paragraph | 0pt | 10pt |
| Table | 12pt | 12pt |
Page Layout
| Property | Value |
|---|---|
| Margins | 1" all sides |
| Header | 0.5" from edge |
| Footer | 0.5" from edge |
| Line spacing | 1.15 |
---
Related Resources
- SKILL.md - Quick reference
- contract-template.md - Legal document structure
- docx-patterns.md - Advanced formatting
{
"metadata": {
"skill": "document-docx",
"updated": "2026-01-26",
"total_sources": 8,
"description": "DOCX automation libraries plus accessibility and document quality guidance.",
"version": "2.1"
},
"categories": {
"python_libraries": [
{
"name": "python-docx Documentation",
"url": "https://python-docx.readthedocs.io/",
"type": "documentation",
"relevance": "Primary reference for creating and editing .docx files in Python.",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["python", "docx"]
},
{
"name": "docxtpl Documentation",
"url": "https://docxtpl.readthedocs.io/",
"type": "documentation",
"relevance": "Template-based DOCX generation (mail merge-style) in Python.",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["python", "docx", "templates"]
}
],
"nodejs_libraries": [
{
"name": "docx (docx.js)",
"url": "https://docx.js.org/",
"type": "documentation",
"relevance": "Generate .docx documents in Node.js with structured APIs.",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["nodejs", "docx"]
},
{
"name": "mammoth.js",
"url": "https://github.com/mwilliamson/mammoth.js",
"type": "library",
"relevance": "Convert .docx to HTML and extract content reliably for downstream processing.",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["nodejs", "docx", "conversion"]
}
],
"quality_and_accessibility": [
{
"name": "Make your Word documents accessible (Microsoft Support)",
"url": "https://support.microsoft.com/en-us/office/make-your-word-documents-accessible-to-people-with-disabilities-d9bf3683-87ac-47ea-b91a-78dcacb3c66d",
"type": "guide",
"relevance": "Baseline accessibility practices for Word documents (styles, headings, tables, alt text).",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["accessibility", "word"]
},
{
"name": "Office Open XML (ECMA-376)",
"url": "https://www.ecma-international.org/publications-and-standards/standards/ecma-376/",
"type": "specification",
"relevance": "Underlying standard behind .docx; helpful for interoperability edge cases.",
"update_frequency": "static",
"access": "free",
"add_as_web_search": false,
"tags": ["spec", "ooxml", "docx"]
},
{
"name": "Open XML SDK (Microsoft)",
"url": "https://learn.microsoft.com/en-us/office/open-xml/open-xml-sdk",
"type": "documentation",
"relevance": "Low-level .docx (OOXML) manipulation patterns, useful when high-level libs cannot express required changes.",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["dotnet", "ooxml", "docx"]
},
{
"name": "docx4j (docx4java)",
"url": "https://www.docx4java.org/trac/docx4j",
"type": "documentation",
"relevance": "Java library for advanced .docx (OOXML) operations, including revision- and interoperability-heavy workflows.",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["java", "ooxml", "docx"]
}
]
}
}
DOCX Accessibility Compliance
Patterns for producing accessible DOCX files that work with screen readers and meet WCAG 2.2 AA / EN 301 549 requirements.
Contents
- Heading Hierarchy
- Alt Text for Images
- Accessible Tables
- Color Contrast and Font Sizing
- Document Language
- WCAG 2.2 AA Checklist
- Word Accessibility Checker
- EU EAA / EN 301 549
- Do / Avoid
---
Heading Hierarchy
Screen readers build a navigation tree from heading levels. Skipping levels (H1 to H3 with no H2) breaks that tree.
from docx import Document
doc = Document()
doc.add_heading('Annual Report', level=1) # One H1 per document
doc.add_heading('Financial Summary', level=2)
doc.add_paragraph('Revenue grew 12% year-over-year.')
doc.add_heading('Regional Breakdown', level=3) # Sequential: 1 → 2 → 3
doc.add_heading('Operational Highlights', level=2)Rules: one Heading 1 per document (document title). Never skip levels. Do not use bold normal text as a fake heading.
---
Alt Text for Images
python-docx has no high-level alt text API. Set it via OOXML:
from docx.shared import Inches
from docx.oxml.ns import qn
inline_shape = doc.add_paragraph().add_run().add_picture('chart.png', width=Inches(4))
docPr = inline_shape._inline.find(qn('wp:docPr'))
docPr.set('descr', 'Bar chart showing Q1-Q4 revenue growth') # Alt text
docPr.set('title', 'Revenue Chart')
# Decorative image: empty description signals "skip this"
# docPr.set('descr', '')---
Accessible Tables
Mark the first row as a header so screen readers announce column names and the row repeats on page breaks:
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
table = doc.add_table(rows=3, cols=3)
table.style = 'Table Grid'
trPr = table.rows[0]._tr.get_or_add_trPr()
trPr.append(OxmlElement('w:tblHeader'))
for i, text in enumerate(['Name', 'Role', 'Department']):
table.rows[0].cells[i].text = textAvoid merged cells, nested tables, and tables used for page layout.
---
Color Contrast and Font Sizing
| Requirement | WCAG 2.2 AA Threshold |
|---|---|
| Normal text contrast | 4.5:1 minimum |
| Large text (>= 18pt or 14pt bold) | 3:1 minimum |
| Non-text elements (charts, icons) | 3:1 against background |
from docx.shared import Pt, RGBColor
run = doc.add_paragraph().add_run('Accessible body text')
run.font.size = Pt(11)
run.font.color.rgb = RGBColor(0x1A, 0x1A, 0x1A) # Near-black
run.font.name = 'Arial'Never rely on color alone to convey meaning. Add text labels ("Warning:") alongside color cues.
---
Document Language
Set language so screen readers pick the correct speech synthesizer:
from docx.oxml.ns import qn
styles_el = doc.styles.element
rPr = styles_el.find(qn('w:docDefaults')).find(qn('w:rPrDefault')).find(qn('w:rPr'))
lang = rPr.find(qn('w:lang'))
if lang is not None:
lang.set(qn('w:val'), 'en-US')---
WCAG 2.2 AA Checklist
[ ] Heading hierarchy is sequential (no skipped levels)
[ ] Every informative image has alt text
[ ] Decorative images marked decorative (empty descr)
[ ] Tables have header rows (w:tblHeader)
[ ] No nested or layout tables
[ ] Color contrast meets 4.5:1 for body text
[ ] Font size 11pt+ for body text
[ ] Color is not sole means of conveying information
[ ] Document language is set
[ ] Hyperlinks have descriptive text (not "click here")
[ ] Lists use List Bullet / List Number styles (not manual dashes)---
Word Accessibility Checker: Catches vs Misses
| Catches | Misses |
|---|---|
| Missing alt text | Poor-quality alt text ("image1.png") |
| Missing table header rows | Complex merged-cell reading order |
| Blank table cells | Color contrast failures (no ratio check) |
| Missing document title | Logical reading order in multi-column layouts |
Run: Review tab > Check Accessibility. Treat as minimum bar, not a full audit.
---
EU EAA / EN 301 549 Relevance
The European Accessibility Act (effective June 2025) requires EN 301 549 compliance. Section 10 applies WCAG success criteria to non-web documents: structure, alt text, contrast, language, reading order. Documents published on websites or sent as part of a service fall in scope. Enforce accessibility at the template level, not through post-hoc remediation.
---
Do / Avoid
| Do | Avoid |
|---|---|
| Use built-in heading styles | Bold normal text as fake headings |
| Write descriptive alt text (what + why) | Filename as alt text |
| Mark header rows on every table | Layout tables for positioning |
| Set document language at the template level | Leaving language unset |
| Test with Accessibility Checker + manual review | Relying solely on automated checks |
| Use 11pt+ body text, high-contrast colors | Light gray text, decorative fonts below 10pt |
---
Related Resources
- docx-patterns.md - Advanced formatting and styles
- template-workflows.md - Template-based generation
- SKILL.md - Parent DOCX skill
- WCAG 2.2 / EN 301 549
Cross-Platform DOCX Compatibility
Rendering differences across Microsoft Word, Google Docs, and LibreOffice. Safe features, risky features, font handling, conversion strategies, and testing workflow.
Contents
- Rendering Differences
- Safe Features
- Risky Features
- Font Handling
- Testing Strategy
- Conversion Best Practices
- Do / Avoid
- Related Resources
---
Rendering Differences
| Area | Word | Google Docs | LibreOffice |
|---|---|---|---|
| Headings, bold/italic | Exact | Correct (minor spacing drift) | Correct (font substitution common) |
| Simple tables | Exact | Cell padding may shift | Border rendering varies |
| Merged / nested tables | Full support | Merged cells break; nesting lost | Nested tables misaligned |
| Inline images | Exact | Exact | Exact |
| Floating images | Full support | Converted to inline or lost | Position may shift |
| SmartArt | Full support | Flattened to image or missing | Not rendered |
| Macros (VBA) | Executes | Stripped | Not supported |
| Multi-level numbering | Full support | May flatten to simple list | Restart rules can break |
| Content controls / form fields | Full support | Not interactive | Partial rendering |
---
Safe Features
These render consistently across all three platforms:
- Headings (levels 1-6, built-in styles)
- Bold, italic, underline, strikethrough
- Font name and size (if font available)
- Paragraph alignment (left, center, right, justify)
- Simple tables (no merges, no nesting)
- Inline images (PNG, JPEG)
- Single-level bulleted and numbered lists
- Page breaks
- Basic headers/footers (text + page numbers)
- Hyperlinks---
Risky Features
| Feature | Risk | Failure Mode |
|---|---|---|
| Merged table cells | Medium | Content shifts in Google Docs |
| Nested tables | High | Layout breaks in LibreOffice |
| SmartArt | High | Missing or flattened |
| Floating images | Medium | Repositioned or inlined |
| Embedded objects (Excel, PDF) | High | Not rendered outside Word |
| Macros / VBA | High | Stripped or blocked |
| Multi-level numbering | Medium | Indentation and restart rules break |
| Text effects (glow, 3D) | Medium | Stripped in Docs and LibreOffice |
---
Font Handling
When a font is missing, the platform substitutes a fallback, changing line breaks and page count.
Strategies:
1. Use universally available fonts: Arial, Times New Roman, Courier New
2. Embed fonts: Word > Options > Save > "Embed fonts" (adds 500KB-2MB)
Google Docs ignores embedded fonts. LibreOffice reads them.
3. Convert to PDF for guaranteed fidelity when recipients only need to readfrom docx.shared import Pt
run = doc.add_paragraph().add_run('Cross-platform safe text')
run.font.name = 'Arial' # Widely available
run.font.size = Pt(11)---
Testing Strategy
Per-template testing workflow:
1. Open in Microsoft Word (desktop) — baseline reference
2. Open in Google Docs — check tables, images, numbering, fonts
3. Open in LibreOffice Writer — check layout, fonts, headers
4. Compare page count — if different, font substitution is changing line breaks
Re-test after any structural template change.CI smoke test — verify LibreOffice can open and convert:
import subprocess, os
def validate_docx_opens(path: str) -> bool:
result = subprocess.run(
['libreoffice', '--headless', '--convert-to', 'pdf', path,
'--outdir', '/tmp/docx-check'], capture_output=True, timeout=30)
pdf = f"/tmp/docx-check/{os.path.basename(path).replace('.docx', '.pdf')}"
return result.returncode == 0 and os.path.exists(pdf)---
Conversion Best Practices
| Method | Fidelity | Platform | Notes |
|---|---|---|---|
| Word COM/VBA | Highest | Windows only | Best for Word-specific features |
| LibreOffice headless | Good | Cross-platform | Minor layout drift on complex docs |
| docx2pdf (Python) | High | Windows/macOS | Wraps Word or LibreOffice |
| mammoth (DOCX → HTML) | Text only | Cross-platform | Loses layout; good for content extraction |
from docx2pdf import convert
convert("report.docx", "report.pdf") # Single file
convert("reports/", "pdfs/") # Batch directory---
Do / Avoid
| Do | Avoid |
|---|---|
| Use built-in heading and list styles | Custom XML formatting only Word understands |
| Stick to Arial, Times New Roman | Calibri without fallback for non-Windows users |
| Use simple tables (no merges, no nesting) | Nested or complex merged-cell tables |
| Test in Word, Google Docs, and LibreOffice | Assuming Word rendering is universal |
| Convert to PDF when editing is not needed | Distributing DOCX with SmartArt or macros |
| Use inline images (PNG/JPEG) | Floating images for cross-platform documents |
---
Related Resources
- docx-patterns.md - Advanced formatting and styles
- accessibility-compliance.md - DOCX accessibility patterns
- template-workflows.md - Template-based generation
- SKILL.md - Parent DOCX skill
- mammoth.js
- docx2pdf
Document Automation Pipelines
CI/CD and batch automation for DOCX generation. Architecture, pipeline integration, template versioning, quality gates, error handling, and a GitHub Actions example.
Contents
- Pipeline Architecture
- Template Versioning
- Data Sources and Batch Generation
- Quality Gates
- Error Handling
- GitHub Actions Workflow
- Do / Avoid
---
Pipeline Architecture
Template (.docx, git-tracked)
↓
Data Source (JSON / CSV / API / DB)
↓
Renderer (docxtpl / python-docx)
↓
Quality Gates (size, parse, unresolved vars)
↓
Output (.docx, optional .pdf)Templates are design artifacts in git. Data is injected at render time. Output is validated before delivery.
---
Template Versioning
DOCX files are binary -- diffs are not human-readable. Store in templates/, name with version (invoice-v3.docx), use Git LFS for files > 500KB, never edit outside the repo.
git lfs track "templates/*.docx"Validate template variables before render:
from docxtpl import DocxTemplate
def validate_template(path: str, expected: list[str]) -> list[str]:
declared = DocxTemplate(path).get_undeclared_template_variables()
missing = [v for v in expected if v not in declared]
return [f"Missing: {missing}"] if missing else []---
Data Sources and Batch Generation
| Source | Library | Use Case |
|---|---|---|
| JSON | json.load() | Config-driven, small datasets |
| CSV | csv.DictReader() | Mail merge, tabular records |
| REST API | requests | Live CRM/ERP data |
| Database | sqlalchemy | Bulk reports from production |
from docxtpl import DocxTemplate
from concurrent.futures import ProcessPoolExecutor
import os
def render_one(args):
template_path, record, output_path = args
doc = DocxTemplate(template_path)
doc.render(record)
doc.save(output_path)
return output_path
def batch_parallel(template_path, records, output_dir, workers=4):
os.makedirs(output_dir, exist_ok=True)
tasks = [(template_path, r, f"{output_dir}/{r['id']}.docx") for r in records]
with ProcessPoolExecutor(max_workers=workers) as pool:
return list(pool.map(render_one, tasks))Each worker re-opens the template -- docxtpl is not thread-safe.
---
Quality Gates
import os, re
from docx import Document
def quality_gate(path: str, max_mb: float = 10.0) -> list[str]:
fails = []
if not os.path.exists(path) or os.path.getsize(path) == 0:
return ['File missing or empty']
if os.path.getsize(path) / 1_048_576 > max_mb:
fails.append(f'Exceeds {max_mb}MB limit')
try:
doc = Document(path)
except Exception as e:
return [f'Corrupted: {e}']
unresolved = re.findall(r'\{\{.*?\}\}', '\n'.join(p.text for p in doc.paragraphs))
if unresolved:
fails.append(f'Unresolved variables: {unresolved}')
return failsChecklist: file exists and size > 0, within size budget, parses without error, no unresolved {{ variables }}, PDF conversion succeeds if required.
---
Error Handling
| Error | Cause | Fix |
|---|---|---|
FileNotFoundError | Wrong template path | Validate path before batch loop |
Jinja2 UndefinedError | Missing context variable | Pre-check with get_undeclared_template_variables() |
| Corrupted output | Broken template XML or bad input chars | Validate template; sanitize input data |
| Font not found in PDF step | CI image missing the font | Install fonts in Dockerfile or use Arial |
| CI timeout | Large batch or slow PDF conversion | Parallelize; split batches; increase timeout |
---
GitHub Actions Workflow
name: Generate Documents
on:
workflow_dispatch:
inputs:
data_file: { description: 'JSON data file', required: true, default: 'data/clients.json' }
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { lfs: true }
- uses: actions/setup-python@v5
with: { python-version: '3.12' }
- run: pip install python-docx docxtpl
- run: sudo apt-get install -y libreoffice-writer
- run: python scripts/generate_batch.py --template templates/invoice.docx --data ${{ github.event.inputs.data_file }} --output output/
- run: python scripts/quality_gate.py --dir output/
- uses: actions/upload-artifact@v4
with: { name: generated-documents, path: output/, retention-days: 30 }---
Do / Avoid
| Do | Avoid |
|---|---|
| Version templates in git (LFS for large files) | Editing templates outside the repo |
| Validate template variables before batch runs | Discovering missing vars mid-batch |
| Run quality gates on every generated file | Assuming render success = correct output |
| Use ProcessPoolExecutor for large batches | Sharing DocxTemplate across threads |
| Install fonts in CI Docker image | Relying on fonts that differ local vs CI |
| Sanitize input data (strip control chars) | Passing raw API output into templates |
---
Related Resources
- template-workflows.md - docxtpl patterns and mail merge
- cross-platform-compatibility.md - Rendering and conversion
- accessibility-compliance.md - Accessible output
- SKILL.md - Parent DOCX skill
Advanced DOCX Patterns
Deep-dive into formatting, styles, headers/footers, and document structure.
Contents
- Document Styles
- Headers and Footers
- Advanced Tables
- Page Layout
- Images and Shapes
- Hyperlinks
- Table of Contents
- Node.js Equivalents
- Related Resources
Document Styles
Built-in Styles (Python)
from docx import Document
from docx.shared import Pt, RGBColor
from docx.enum.style import WD_STYLE_TYPE
doc = Document()
# Use built-in styles
doc.add_heading('Title', 0) # Title style
doc.add_heading('Heading 1', 1)
doc.add_heading('Heading 2', 2)
doc.add_paragraph('Normal paragraph text.')
doc.add_paragraph('Quote style', style='Quote')
doc.add_paragraph('List item', style='List Bullet')Custom Styles
from docx.shared import Pt, Inches
from docx.enum.text import WD_LINE_SPACING
# Create custom paragraph style
styles = doc.styles
custom_style = styles.add_style('CustomBody', WD_STYLE_TYPE.PARAGRAPH)
custom_style.font.name = 'Arial'
custom_style.font.size = Pt(11)
custom_style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
custom_style.paragraph_format.space_after = Pt(12)
# Apply custom style
doc.add_paragraph('Styled paragraph', style='CustomBody')Character Styles
# Create character style for emphasis
char_style = styles.add_style('Emphasis', WD_STYLE_TYPE.CHARACTER)
char_style.font.italic = True
char_style.font.color.rgb = RGBColor(0x42, 0x24, 0xE9)
# Apply in paragraph
para = doc.add_paragraph()
para.add_run('Normal text with ')
para.add_run('emphasized text', style='Emphasis')
para.add_run(' inline.')---
Headers and Footers
Basic Header/Footer
from docx.shared import Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH
# Access default section
section = doc.sections[0]
# Header
header = section.header
header_para = header.paragraphs[0]
header_para.text = "Company Name"
header_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
# Footer with page numbers
footer = section.footer
footer_para = footer.paragraphs[0]
footer_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
# Add page number field
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
run = footer_para.add_run()
fldChar1 = OxmlElement('w:fldChar')
fldChar1.set(qn('w:fldCharType'), 'begin')
run._r.append(fldChar1)
instrText = OxmlElement('w:instrText')
instrText.text = "PAGE"
run._r.append(instrText)
fldChar2 = OxmlElement('w:fldChar')
fldChar2.set(qn('w:fldCharType'), 'end')
run._r.append(fldChar2)Different First Page Header
section = doc.sections[0]
section.different_first_page_header_footer = True
# First page header (e.g., logo, full header)
first_header = section.first_page_header
first_header.paragraphs[0].text = "FULL COMPANY HEADER - First Page Only"
# Subsequent pages header (simplified)
header = section.header
header.paragraphs[0].text = "Company Name"Header with Logo
header = section.header
header_para = header.paragraphs[0]
# Add logo
run = header_para.add_run()
run.add_picture('logo.png', width=Inches(1.5))
# Add company name next to logo
header_para.add_run('\t\tCompany Name')---
Advanced Tables
Table with Merged Cells
from docx.shared import Inches, Pt
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.oxml.ns import nsdecls
from docx.oxml import parse_xml
table = doc.add_table(rows=4, cols=4)
table.style = 'Table Grid'
# Merge cells for header
cell_a1 = table.cell(0, 0)
cell_b1 = table.cell(0, 1)
cell_a1.merge(cell_b1)
cell_a1.text = "Merged Header"
# Set column widths
for row in table.rows:
row.cells[0].width = Inches(2)
row.cells[1].width = Inches(1.5)Styled Table with Alternating Rows
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
def shade_cell(cell, color):
"""Apply shading to a cell."""
shading = OxmlElement('w:shd')
shading.set(qn('w:fill'), color)
cell._tc.get_or_add_tcPr().append(shading)
# Create table
table = doc.add_table(rows=5, cols=3)
table.style = 'Table Grid'
# Header row styling
for cell in table.rows[0].cells:
shade_cell(cell, "4472C4") # Blue header
cell.paragraphs[0].runs[0].font.color.rgb = RGBColor(255, 255, 255)
cell.paragraphs[0].runs[0].bold = True
# Alternating row colors
for i, row in enumerate(table.rows[1:], 1):
color = "D9E2F3" if i % 2 == 0 else "FFFFFF"
for cell in row.cells:
shade_cell(cell, color)---
Page Layout
Margins and Orientation
from docx.shared import Inches
from docx.enum.section import WD_ORIENT
section = doc.sections[0]
# Set margins
section.top_margin = Inches(1)
section.bottom_margin = Inches(1)
section.left_margin = Inches(1.25)
section.right_margin = Inches(1.25)
# Landscape orientation
section.orientation = WD_ORIENT.LANDSCAPE
# Swap width and height for landscape
new_width = section.page_height
new_height = section.page_width
section.page_width = new_width
section.page_height = new_heightSection Breaks
from docx.enum.section import WD_SECTION
# Add content to first section
doc.add_paragraph("First section content")
# Add section break (new page)
doc.add_section(WD_SECTION.NEW_PAGE)
# Second section with different orientation
section2 = doc.sections[1]
section2.orientation = WD_ORIENT.LANDSCAPE
doc.add_paragraph("Second section - landscape")Columns
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
section = doc.sections[0]
sectPr = section._sectPr
# Create 2-column layout
cols = OxmlElement('w:cols')
cols.set(qn('w:num'), '2')
cols.set(qn('w:space'), '720') # Space between columns (in twips)
sectPr.append(cols)---
Images and Shapes
Image Positioning
from docx.shared import Inches, Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH
# Inline image (centered)
para = doc.add_paragraph()
para.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = para.add_run()
run.add_picture('chart.png', width=Inches(5))
# Image with caption
doc.add_paragraph('Figure 1: Sales Chart', style='Caption')Floating Image (Advanced)
from docx.oxml.ns import nsmap, qn
from docx.oxml import OxmlElement
def add_float_picture(paragraph, image_path, width, pos_x, pos_y):
"""Add floating image at specified position."""
run = paragraph.add_run()
inline = run.add_picture(image_path, width=width).inline
# Convert to anchor (floating)
anchor = OxmlElement('wp:anchor')
# ... (complex XML manipulation for positioning)
# See python-docx GitHub issues for full implementation---
Hyperlinks
Add Hyperlink
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
def add_hyperlink(paragraph, url, text, color="0000FF", underline=True):
"""Add clickable hyperlink to paragraph."""
part = paragraph.part
r_id = part.relate_to(
url,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
is_external=True,
)
hyperlink = OxmlElement('w:hyperlink')
hyperlink.set(qn('r:id'), r_id)
new_run = OxmlElement('w:r')
rPr = OxmlElement('w:rPr')
if color:
c = OxmlElement('w:color')
c.set(qn('w:val'), color)
rPr.append(c)
if underline:
u = OxmlElement('w:u')
u.set(qn('w:val'), 'single')
rPr.append(u)
new_run.append(rPr)
t = OxmlElement('w:t')
t.text = text
new_run.append(t)
hyperlink.append(new_run)
paragraph._p.append(hyperlink)
return hyperlink
# Usage
para = doc.add_paragraph("Visit ")
add_hyperlink(para, "https://example.com", "our website")
para.add_run(" for more info.")---
Table of Contents
Generate TOC Placeholder
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
def add_toc(doc):
"""Add Table of Contents field."""
paragraph = doc.add_paragraph()
run = paragraph.add_run()
fldChar1 = OxmlElement('w:fldChar')
fldChar1.set(qn('w:fldCharType'), 'begin')
instrText = OxmlElement('w:instrText')
instrText.set(qn('xml:space'), 'preserve')
instrText.text = 'TOC \\o "1-3" \\h \\z \\u'
fldChar2 = OxmlElement('w:fldChar')
fldChar2.set(qn('w:fldCharType'), 'separate')
fldChar3 = OxmlElement('w:fldChar')
fldChar3.set(qn('w:fldCharType'), 'end')
run._r.append(fldChar1)
run._r.append(instrText)
run._r.append(fldChar2)
run._r.append(fldChar3)
# Note: TOC updates when document is opened in Word
add_toc(doc)
doc.add_paragraph("Right-click TOC in Word and select 'Update Field'", style='Caption')---
Node.js Equivalents
Styles (docx library)
import { Document, Paragraph, TextRun, HeadingLevel, AlignmentType } from 'docx';
const doc = new Document({
styles: {
paragraphStyles: [
{
id: "CustomBody",
name: "Custom Body",
basedOn: "Normal",
next: "Normal",
run: {
font: "Arial",
size: 22, // Half-points
},
paragraph: {
spacing: { after: 240 }, // Twips
},
},
],
},
sections: [{
children: [
new Paragraph({
text: "Styled paragraph",
style: "CustomBody",
}),
],
}],
});Headers/Footers (docx library)
import { Document, Header, Footer, Paragraph, PageNumber, AlignmentType } from 'docx';
const doc = new Document({
sections: [{
headers: {
default: new Header({
children: [
new Paragraph({
text: "Company Name",
alignment: AlignmentType.CENTER,
}),
],
}),
},
footers: {
default: new Footer({
children: [
new Paragraph({
alignment: AlignmentType.CENTER,
children: [
new TextRun("Page "),
new PageNumber(),
new TextRun(" of "),
new PageNumber({ numberOfPages: true }),
],
}),
],
}),
},
children: [
new Paragraph({ text: "Document content" }),
],
}],
});---
Related Resources
- SKILL.md - Quick reference
- template-workflows.md - Batch generation patterns
- python-docx Documentation
- docx.js Documentation
Template Workflows
Mail merge, batch generation, and document automation patterns.
Contents
- Template-Based Generation
- docxtpl Basics
- Mail Merge Patterns
- Batch Generation
- Advanced Template Features
- Workflow Automation
- Error Handling
- Related Resources
Template-Based Generation
Why Use Templates?
| Approach | Pros | Cons |
|---|---|---|
| Pure python-docx | Full control, dynamic structure | More code, harder to maintain |
| docxtpl templates | Visual design in Word, simple code | Less dynamic, template constraints |
| Hybrid | Best of both | More complexity |
Rule of thumb: Use templates when non-developers need to maintain document design.
---
docxtpl Basics
Template Syntax
Templates use Jinja2 syntax inside Word documents:
| Syntax | Purpose | Example |
|---|---|---|
{{ var }} | Variable | {{ customer_name }} |
{% for item in items %} | Loop | Iterate over list |
{% if condition %} | Conditional | Show/hide sections |
{%p ... %} | Paragraph-level | Whole paragraph conditional |
{%tr ... %} | Table row | Loop over table rows |
{%tc ... %} | Table cell | Loop over columns |
Basic Template Fill
from docxtpl import DocxTemplate
doc = DocxTemplate("invoice_template.docx")
context = {
'invoice_number': 'INV-2025-001',
'customer_name': 'Acme Corporation',
'date': '2025-01-15',
'items': [
{'description': 'Consulting Services', 'hours': 40, 'rate': 150, 'total': 6000},
{'description': 'Development', 'hours': 80, 'rate': 125, 'total': 10000},
],
'subtotal': 16000,
'tax': 1600,
'total': 17600,
}
doc.render(context)
doc.save('invoice_INV-2025-001.docx')Template File Structure
In Word, create invoice_template.docx:
INVOICE
Invoice #: {{ invoice_number }}
Date: {{ date }}
Customer: {{ customer_name }}
┌─────────────────────────────────────────────────┐
│ Description │ Hours │ Rate │ Total │
├─────────────────────────────────────────────────┤
│{%tr for item in items %} │
│ {{ item.description }} │ {{ item.hours }} │ ${{ item.rate }} │ ${{ item.total }} │
│{%tr endfor %} │
├─────────────────────────────────────────────────┤
│ │ │ Subtotal: ${{ subtotal }} │
│ │ │ Tax: ${{ tax }} │
│ │ │ TOTAL: ${{ total }} │
└─────────────────────────────────────────────────┘---
Mail Merge Patterns
Single-File Mail Merge
from docxtpl import DocxTemplate
import json
# Load recipients
with open('recipients.json') as f:
recipients = json.load(f)
# Generate personalized documents
for recipient in recipients:
doc = DocxTemplate("letter_template.docx")
doc.render(recipient)
doc.save(f"letters/letter_{recipient['id']}.docx")recipients.json Structure
[
{
"id": "001",
"name": "John Smith",
"company": "Acme Corp",
"address": "123 Main St",
"city": "New York",
"state": "NY",
"zip": "10001"
},
{
"id": "002",
"name": "Jane Doe",
"company": "Tech Inc",
"address": "456 Oak Ave",
"city": "San Francisco",
"state": "CA",
"zip": "94102"
}
]Mail Merge with CSV
import csv
from docxtpl import DocxTemplate
def mail_merge_csv(template_path, csv_path, output_dir):
"""Generate documents from CSV data."""
with open(csv_path, newline='', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
doc = DocxTemplate(template_path)
doc.render(row)
# Generate unique filename
filename = f"{output_dir}/{row.get('id', row.get('name', 'doc'))}.docx"
doc.save(filename)
print(f"Generated: {filename}")
# Usage
mail_merge_csv(
template_path="contract_template.docx",
csv_path="clients.csv",
output_dir="contracts"
)---
Batch Generation
Parallel Processing
from docxtpl import DocxTemplate
from concurrent.futures import ProcessPoolExecutor
import os
def generate_document(args):
"""Generate single document (for parallel execution)."""
template_path, context, output_path = args
doc = DocxTemplate(template_path)
doc.render(context)
doc.save(output_path)
return output_path
def batch_generate(template_path, contexts, output_dir, max_workers=4):
"""Generate multiple documents in parallel."""
os.makedirs(output_dir, exist_ok=True)
# Prepare arguments for each document
tasks = [
(template_path, ctx, f"{output_dir}/{ctx['filename']}.docx")
for ctx in contexts
]
with ProcessPoolExecutor(max_workers=max_workers) as executor:
results = list(executor.map(generate_document, tasks))
return results
# Usage
contexts = [
{'filename': 'report_q1', 'quarter': 'Q1', 'revenue': 1000000},
{'filename': 'report_q2', 'quarter': 'Q2', 'revenue': 1200000},
{'filename': 'report_q3', 'quarter': 'Q3', 'revenue': 1100000},
{'filename': 'report_q4', 'quarter': 'Q4', 'revenue': 1500000},
]
generated = batch_generate("quarterly_template.docx", contexts, "reports")
print(f"Generated {len(generated)} documents")Progress Tracking
from tqdm import tqdm
from docxtpl import DocxTemplate
def batch_generate_with_progress(template_path, contexts, output_dir):
"""Generate documents with progress bar."""
os.makedirs(output_dir, exist_ok=True)
for ctx in tqdm(contexts, desc="Generating documents"):
doc = DocxTemplate(template_path)
doc.render(ctx)
doc.save(f"{output_dir}/{ctx['filename']}.docx")---
Advanced Template Features
Conditional Sections
Template (contract_template.docx):
{{ company_name }} Agreement
{%p if include_nda %}
CONFIDENTIALITY CLAUSE
This agreement includes non-disclosure provisions...
{%p endif %}
{%p if payment_terms == 'net30' %}
Payment is due within 30 days of invoice date.
{%p elif payment_terms == 'net60' %}
Payment is due within 60 days of invoice date.
{%p else %}
Payment is due upon receipt.
{%p endif %}Python:
context = {
'company_name': 'Acme Corp',
'include_nda': True,
'payment_terms': 'net30',
}Nested Loops
Template:
{% for department in departments %}
Department: {{ department.name }}
Employees:
{%tr for emp in department.employees %}
| {{ emp.name }} | {{ emp.role }} | {{ emp.email }} |
{%tr endfor %}
{% endfor %}Python:
context = {
'departments': [
{
'name': 'Engineering',
'employees': [
{'name': 'Alice', 'role': 'Lead', 'email': 'alice@co.com'},
{'name': 'Bob', 'role': 'Senior', 'email': 'bob@co.com'},
]
},
{
'name': 'Sales',
'employees': [
{'name': 'Carol', 'role': 'Manager', 'email': 'carol@co.com'},
]
},
]
}Images in Templates
from docxtpl import DocxTemplate, InlineImage
from docx.shared import Inches
doc = DocxTemplate("report_template.docx")
# Add image to context
context = {
'company_logo': InlineImage(doc, 'logo.png', width=Inches(2)),
'chart': InlineImage(doc, 'sales_chart.png', width=Inches(5)),
'signature': InlineImage(doc, 'signature.png', height=Inches(0.5)),
}
doc.render(context)
doc.save('report.docx')Template placeholder: {{ company_logo }}
Rich Text (Subdocuments)
from docxtpl import DocxTemplate, RichText
doc = DocxTemplate("template.docx")
# Create rich text with formatting
rt = RichText()
rt.add('Important: ', bold=True, color='FF0000')
rt.add('Please review before signing.')
context = {
'notice': rt,
}
doc.render(context)---
Workflow Automation
Complete Pipeline
import os
import json
from datetime import datetime
from docxtpl import DocxTemplate
from pathlib import Path
class DocumentPipeline:
"""End-to-end document generation pipeline."""
def __init__(self, template_dir: str, output_dir: str):
self.template_dir = Path(template_dir)
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
def load_data(self, data_source: str) -> list:
"""Load data from JSON file."""
with open(data_source) as f:
return json.load(f)
def validate_context(self, context: dict, required_fields: list) -> bool:
"""Validate required fields are present."""
missing = [f for f in required_fields if f not in context]
if missing:
raise ValueError(f"Missing required fields: {missing}")
return True
def generate(self, template_name: str, context: dict, output_name: str = None):
"""Generate single document."""
template_path = self.template_dir / template_name
doc = DocxTemplate(str(template_path))
doc.render(context)
if output_name is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_name = f"{template_name.replace('.docx', '')}_{timestamp}.docx"
output_path = self.output_dir / output_name
doc.save(str(output_path))
return output_path
def batch_generate(self, template_name: str, data_source: str,
filename_field: str = 'id'):
"""Generate multiple documents from data source."""
data = self.load_data(data_source)
generated = []
for item in data:
output_name = f"{item.get(filename_field, 'doc')}.docx"
path = self.generate(template_name, item, output_name)
generated.append(path)
return generated
# Usage
pipeline = DocumentPipeline(
template_dir="templates",
output_dir="generated"
)
# Single document
pipeline.generate(
template_name="invoice.docx",
context={'customer': 'Acme', 'amount': 5000},
output_name="invoice_acme.docx"
)
# Batch generation
pipeline.batch_generate(
template_name="contract.docx",
data_source="clients.json",
filename_field="client_id"
)Integration with APIs
import requests
from docxtpl import DocxTemplate
def generate_from_api(template_path: str, api_url: str, output_path: str):
"""Fetch data from API and generate document."""
# Fetch data
response = requests.get(api_url)
response.raise_for_status()
data = response.json()
# Generate document
doc = DocxTemplate(template_path)
doc.render(data)
doc.save(output_path)
return output_path
# Example: Generate invoice from order API
generate_from_api(
template_path="invoice_template.docx",
api_url="https://api.example.com/orders/12345",
output_path="invoices/order_12345.docx"
)---
Error Handling
Robust Generation
from docxtpl import DocxTemplate
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def safe_generate(template_path: str, context: dict, output_path: str) -> bool:
"""Generate document with error handling."""
try:
doc = DocxTemplate(template_path)
doc.render(context)
doc.save(output_path)
logger.info(f"Generated: {output_path}")
return True
except FileNotFoundError:
logger.error(f"Template not found: {template_path}")
return False
except KeyError as e:
logger.error(f"Missing template variable: {e}")
return False
except Exception as e:
logger.error(f"Generation failed: {e}")
return False
def batch_generate_safe(template_path: str, contexts: list, output_dir: str):
"""Batch generate with failure tracking."""
results = {'success': [], 'failed': []}
for ctx in contexts:
output_path = f"{output_dir}/{ctx.get('id', 'unknown')}.docx"
if safe_generate(template_path, ctx, output_path):
results['success'].append(output_path)
else:
results['failed'].append(ctx)
logger.info(f"Generated: {len(results['success'])}, Failed: {len(results['failed'])}")
return results---
Related Resources
- SKILL.md - Quick reference
- docx-patterns.md - Advanced formatting
- docxtpl Documentation
Tracked Changes in DOCX (OOXML)
Decision Guide
- If you need a human-friendly redline: generate the revised
.docxand use Microsoft Word "Compare" to create tracked changes. - If you need review notes: add comments or highlight runs instead of trying to generate tracked changes.
- If you need to parse tracked changes in an existing
.docx: inspect OOXML (word/document.xml) for tracked-revision tags. - If you must generate true tracked changes programmatically: prefer dedicated OOXML tooling (docx4j, Open XML SDK) or a commercial library (Aspose.Words).
What Is Feasible with Common Libraries
python-docx: strong for structure and styling; does not provide a first-class API for tracked changes and may flatten them when editing.docx(Node.js): good for generation; not designed for tracked changes workflows.mammoth: best-effort text-first extraction/HTML conversion; not a revision-preserving tool.
OOXML Markers to Look For
- Tracked revisions in
word/document.xml: <w:ins ...>inserted content<w:del ...>deleted content<w:moveFrom ...>/<w:moveTo ...>moved content- Comments:
- Definitions in
word/comments.xml - References in
word/document.xmlvia comment range and reference tags
Quick Inspection (No Dependencies)
- Run:
python scripts/docx_inspect_ooxml.py input.docx --json - Use the counts to decide whether to:
- Avoid editing with high-level libraries, or
- Move to an OOXML-level approach for this document.
Safety Notes
- Treat
.docxas a zip archive; always work on a copy when editing OOXML directly. - Avoid "string replace" edits in
document.xmlunless you also validate the resulting XML well-formedness.
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
def _require_python_docx():
try:
from docx import Document # type: ignore
return Document
except ImportError as exc:
raise RuntimeError(
"Missing dependency: python-docx. Install with: pip install python-docx"
) from exc
def extract_docx(docx_path: Path) -> dict[str, Any]:
Document = _require_python_docx()
doc = Document(str(docx_path))
paragraphs = [p.text for p in doc.paragraphs]
tables: list[list[list[str]]] = []
for table in doc.tables:
tables.append([[cell.text for cell in row.cells] for row in table.rows])
props = doc.core_properties
core_properties = {
"title": props.title,
"subject": props.subject,
"author": props.author,
"category": props.category,
"comments": props.comments,
"created": props.created.isoformat() if props.created else None,
"modified": props.modified.isoformat() if props.modified else None,
}
return {
"path": str(docx_path),
"core_properties": core_properties,
"paragraphs": paragraphs,
"tables": tables,
}
def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(description="Extract text and tables from a .docx into JSON.")
parser.add_argument("docx", type=Path, help="Path to a .docx file")
parser.add_argument("--out", type=Path, help="Output JSON path (defaults to stdout)")
args = parser.parse_args(argv)
if not args.docx.exists():
print(f"File not found: {args.docx}", file=sys.stderr)
return 2
try:
payload = extract_docx(args.docx)
except Exception as exc:
print(str(exc), file=sys.stderr)
return 2
output = json.dumps(payload, indent=2, ensure_ascii=False)
if args.out:
args.out.write_text(output, encoding="utf-8")
else:
print(output)
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import sys
import zipfile
from dataclasses import dataclass
from pathlib import Path
from typing import Any
@dataclass(frozen=True)
class InspectionResult:
path: str
parts_present: list[str]
counts: dict[str, int]
def _read_zip_member(zip_file: zipfile.ZipFile, name: str) -> bytes | None:
try:
with zip_file.open(name) as file:
return file.read()
except KeyError:
return None
def inspect_docx(docx_path: Path) -> InspectionResult:
with zipfile.ZipFile(docx_path) as zip_file:
parts_present = sorted(zip_file.namelist())
document_xml = _read_zip_member(zip_file, "word/document.xml") or b""
comments_xml = _read_zip_member(zip_file, "word/comments.xml") or b""
track_changes_xml = _read_zip_member(zip_file, "word/trackRevisions.xml") or b""
def count(tag: bytes) -> int:
return document_xml.count(tag) + comments_xml.count(tag) + track_changes_xml.count(tag)
counts = {
"w:ins": count(b"<w:ins"),
"w:del": count(b"<w:del"),
"w:moveFrom": count(b"<w:moveFrom"),
"w:moveTo": count(b"<w:moveTo"),
"comments:present": 1 if comments_xml else 0,
"comments:references": document_xml.count(b"commentRangeStart")
+ document_xml.count(b"commentRangeEnd")
+ document_xml.count(b"commentReference"),
}
return InspectionResult(path=str(docx_path), parts_present=parts_present, counts=counts)
def _to_json(result: InspectionResult) -> dict[str, Any]:
return {
"path": result.path,
"parts_present": result.parts_present,
"counts": result.counts,
}
def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(
description="Inspect a .docx (OOXML zip) for common signals like tracked changes and comments."
)
parser.add_argument("docx", type=Path, help="Path to a .docx file")
parser.add_argument("--json", action="store_true", help="Emit JSON to stdout")
parser.add_argument("--list-parts", action="store_true", help="List all zip members (OOXML parts)")
args = parser.parse_args(argv)
if not args.docx.exists():
print(f"File not found: {args.docx}", file=sys.stderr)
return 2
if args.docx.suffix.lower() != ".docx":
print("Expected a .docx file. For .doc, convert to .docx first.", file=sys.stderr)
return 2
try:
result = inspect_docx(args.docx)
except zipfile.BadZipFile:
print("Not a valid .docx (zip) file.", file=sys.stderr)
return 2
if args.json:
print(json.dumps(_to_json(result), indent=2, ensure_ascii=False))
return 0
print(f"File: {result.path}")
for key in sorted(result.counts.keys()):
print(f"{key}: {result.counts[key]}")
if args.list_parts:
print("\nOOXML parts:")
for name in result.parts_present:
print(f"- {name}")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
def _require_docxtpl():
try:
from docxtpl import DocxTemplate # type: ignore
return DocxTemplate
except ImportError as exc:
raise RuntimeError("Missing dependency: docxtpl. Install with: pip install docxtpl") from exc
def render_template(template_path: Path, context: dict[str, Any], output_path: Path) -> None:
DocxTemplate = _require_docxtpl()
doc = DocxTemplate(str(template_path))
doc.render(context)
doc.save(str(output_path))
def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(description="Render a docxtpl template (.docx) from a JSON context.")
parser.add_argument("template", type=Path, help="Path to a .docx template file")
parser.add_argument("context", type=Path, help="Path to a JSON file containing template variables")
parser.add_argument("output", type=Path, help="Output .docx path")
args = parser.parse_args(argv)
if not args.template.exists():
print(f"Template not found: {args.template}", file=sys.stderr)
return 2
if not args.context.exists():
print(f"Context not found: {args.context}", file=sys.stderr)
return 2
try:
context = json.loads(args.context.read_text(encoding="utf-8"))
if not isinstance(context, dict):
raise ValueError("Context JSON must be an object/dict at the top level.")
render_template(args.template, context, args.output)
except Exception as exc:
print(str(exc), file=sys.stderr)
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
#!/usr/bin/env node
import fs from "node:fs";
import process from "node:process";
async function main(argv) {
if (argv.length < 2) {
console.error("Usage: node scripts/docx_to_html.mjs <input.docx> <output.html>");
process.exit(2);
}
const [inputPath, outputPath] = argv;
let mammoth;
try {
mammoth = await import("mammoth");
} catch (err) {
console.error("Missing dependency: mammoth. Install with: npm i mammoth");
process.exit(2);
}
const docxBuffer = fs.readFileSync(inputPath);
const result = await mammoth.convertToHtml({ buffer: docxBuffer });
const html = `<!doctype html><html><head><meta charset="utf-8"></head><body>${result.value}</body></html>`;
fs.writeFileSync(outputPath, html, { encoding: "utf-8" });
if (result.messages?.length) {
for (const message of result.messages) console.error(String(message));
}
}
main(process.argv.slice(2)).catch((err) => {
console.error(String(err));
process.exit(2);
});
Related skills
FAQ
Which libraries does document-docx use?
document-docx routes creation to python-docx or Node docx, template filling to docxtpl, HTML extraction to mammoth.js, and tracked-change inspection to OOXML tooling such as docx4j or OpenXML SDK.
Does document-docx support legacy .doc files?
document-docx targets .docx only. Legacy .doc files must be converted to .docx first, for example with LibreOffice, before python-docx or mammoth.js can process them.