"""Apply document commands to a docx using python-docx. No fallback: raises on error.""" import io import logging from typing import Any from docx import Document logger = logging.getLogger(__name__) def apply_replace_text(doc: Document, search: str, replace: str) -> None: """Replace first occurrence of search with replace in all paragraphs.""" if not search: raise ValueError("replaceText: search must be non-empty") for paragraph in doc.paragraphs: if search in paragraph.text: for run in paragraph.runs: if search in run.text: run.text = run.text.replace(search, replace, 1) return paragraph.text = paragraph.text.replace(search, replace, 1) return for table in doc.tables: for row in table.rows: for cell in row.cells: for paragraph in cell.paragraphs: if search in paragraph.text: for run in paragraph.runs: if search in run.text: run.text = run.text.replace(search, replace, 1) return paragraph.text = paragraph.text.replace(search, replace, 1) return logger.warning("replaceText: search string not found: %s", repr(search[:50])) def apply_insert_paragraph( doc: Document, text: str, position: str = "end", ) -> None: """Insert a paragraph. position: 'end' (default) or 'start'.""" new_para = doc.add_paragraph(text) if position == "start" and len(doc.paragraphs) > 1: # Move the new paragraph to the start (python-docx adds at end) body = doc.element.body new_el = new_para._element body.remove(new_el) body.insert(0, new_el) elif position != "end" and position != "start": raise ValueError("insertParagraph: position must be 'start' or 'end'") def load_docx(content: bytes) -> Document: """Load docx from bytes.""" return Document(io.BytesIO(content)) def save_docx(doc: Document) -> bytes: """Save docx to bytes.""" buf = io.BytesIO() doc.save(buf) buf.seek(0) return buf.read() def apply_commands_docx(content: bytes, commands: list[dict[str, Any]]) -> bytes: """ Apply a list of commands to docx content. Returns new content. Commands: { "type": "replaceText", "search": "...", "replace": "..." } { "type": "insertParagraph", "text": "...", "position": "end"|"start" } """ doc = load_docx(content) for cmd in commands: ctype = cmd.get("type") if ctype == "replaceText": apply_replace_text( doc, search=str(cmd.get("search", "")), replace=str(cmd.get("replace", "")), ) elif ctype == "insertParagraph": apply_insert_paragraph( doc, text=str(cmd.get("text", "")), position=str(cmd.get("position", "end")), ) else: raise ValueError(f"Unknown command type: {ctype}") return save_docx(doc)