- Add ia_dev submodule (projects/smart_ide on forge 4nk) - Document APIs, orchestrator, gateway, local-office, rollout - Add systemd/scripts layout; relocate setup scripts - Remove obsolete nginx/enso-only docs from this repo scope
91 lines
3.1 KiB
Python
91 lines
3.1 KiB
Python
"""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)
|