spb/ultra-sharp-agent-skills Public
Ultra-Sharp Agent Skills — a research-first skill-authoring system + 72 production-ready skills for AI agents.
Python 100%
1<!--2Author: Simon-Pierre Boucher3Contact: contact@spboucher.ai4-->56# DOCX Recipes — python-docx (+ pandoc, raw XML)78## Contents9- Create with formatting (styles, fonts, page setup, images, headers/footers)10- Tables11- Read / extract (text, tables, structure)12- Modify (find-and-replace across runs, insert/delete paragraphs)13- Raw-XML escape hatch (tracked changes, comments)14- Convert (docx ↔ markdown/pdf)15- Gotchas1617## Create with formatting1819```python20from docx import Document21from docx.shared import Pt, Inches, RGBColor22from docx.enum.text import WD_ALIGN_PARAGRAPH2324doc = Document()2526# Page setup — US Letter (python-docx defaults to the template's size)27section = doc.sections[0]28section.page_width, section.page_height = Inches(8.5), Inches(11)29section.left_margin = section.right_margin = Inches(1)3031# Built-in styles: use them so a table of contents works32doc.add_heading("Title of Report", level=0) # style "Title"33doc.add_heading("Introduction", level=1) # style "Heading 1"3435p = doc.add_paragraph("Body text with ")36run = p.add_run("bold emphasis")37run.bold = True38run.font.size = Pt(11)39run.font.color.rgb = RGBColor(0x44, 0x72, 0xC4)40p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY4142# Bullets and numbers come from styles, never literal characters43doc.add_paragraph("First point", style="List Bullet")44doc.add_paragraph("Step one", style="List Number")4546# Image, sized by width (height scales proportionally)47doc.add_picture("chart.png", width=Inches(5))4849# Header/footer50doc.sections[0].header.paragraphs[0].text = "Confidential"51doc.sections[0].footer.paragraphs[0].text = "Page footer"5253doc.save("report.docx")54```5556## Tables5758```python59table = doc.add_table(rows=1, cols=3)60table.style = "Table Grid" # built-in style name61hdr = table.rows[0].cells62for i, h in enumerate(["Region", "Q1", "Q2"]):63 hdr[i].text = h64 hdr[i].paragraphs[0].runs[0].bold = True65for region, q1, q2 in [("East", "100", "150")]:66 row = table.add_row().cells67 row[0].text, row[1].text, row[2].text = region, q1, q26869# Merge cells70a = table.cell(0, 0); b = table.cell(0, 1)71merged = a.merge(b)72```7374## Read / extract7576```python77from docx import Document78doc = Document("report.docx")7980# All body text in order (paragraphs only — table text is separate)81text = "\n".join(p.text for p in doc.paragraphs)8283# Tables → list of rows84tables = [[[cell.text for cell in row.cells] for row in t.rows] for t in doc.tables]8586# Structure: headings with levels87outline = [(p.style.name, p.text) for p in doc.paragraphs if p.style.name.startswith("Heading")]88```8990Full-fidelity read: `pandoc -t markdown report.docx` (keeps headings, lists, tables, links).9192## Modify9394**Find-and-replace — the run-splitting problem.** Word splits a paragraph's text into runs at arbitrary points, so a target string often spans runs. Safe pattern: operate at paragraph level, rebuild runs only when the paragraph actually matches.9596```python97def replace_in_paragraph(p, old, new):98 if old not in p.text:99 return100 # Concatenate, replace, put everything in the first run, empty the rest.101 # Trade-off: intra-paragraph formatting collapses to the first run's format.102 full = p.text.replace(old, new)103 for run in p.runs:104 run.text = ""105 if p.runs:106 p.runs[0].text = full107 else:108 p.add_run(full)109110doc = Document("report.docx")111for p in doc.paragraphs:112 replace_in_paragraph(p, "FY2025", "FY2026")113for t in doc.tables:114 for row in t.rows:115 for cell in row.cells:116 for p in cell.paragraphs:117 replace_in_paragraph(p, "FY2025", "FY2026")118doc.save("report.docx")119```120121**Insert/delete paragraphs:**122```python123# Insert before an existing paragraph124target = doc.paragraphs[3]125new_p = target.insert_paragraph_before("Inserted text", style="Normal")126127# Delete: python-docx has no API — remove the XML element128p = doc.paragraphs[5]129p._element.getparent().remove(p._element)130```131132## Raw-XML escape hatch133134For tracked changes (`w:ins`/`w:del`), comments, or anything python-docx lacks:135136```bash137mkdir unpacked && cd unpacked && unzip -o ../report.docx138# edit word/document.xml (and word/comments.xml for comments)139zip -r ../report-edited.docx . -x '.*' # zip from inside so paths stay relative140```141142Accept all tracked changes = keep `w:ins` content (strip the wrapper tag), delete `w:del` elements entirely. Validate the result opens: `python3 -c "from docx import Document; Document('report-edited.docx')"`.143144## Convert145146```bash147pandoc report.md -o report.docx # markdown → docx148pandoc -t markdown report.docx -o report.md # docx → markdown149soffice --headless --convert-to pdf report.docx # docx → pdf150soffice --headless --convert-to docx legacy.doc # .doc → .docx151```152153## Gotchas154155- **python-docx cannot read or write tracked changes, comments, or fields** (page numbers, TOC field codes) — use the XML escape hatch.156- **A TOC inserted programmatically shows empty until Word/LibreOffice refreshes fields**; headings must use built-in `Heading N` styles for it to populate.157- **Runs split unpredictably** — never assume one run per paragraph; see the find-and-replace pattern above.158- **`doc.paragraphs` skips text inside tables, headers, footers, and text boxes** — iterate those containers separately.159- **New documents inherit the bundled default template** (Calibri, A4 in some builds); set page size and margins explicitly when layout matters.160- **Style names are English built-ins** ("Heading 1", "Table Grid") regardless of Word's UI language; a missing custom style raises `KeyError` on use.161- **`.dotx` templates:** open normally, but save as `.docx` unless the user wants a template back.162