Files
Templated-PPT-Master/runtime/ppt-master/scripts/template_fill_pptx/text_fill.py
T

118 lines
4.2 KiB
Python

"""apply: replace text inside cloned slide shapes while keeping frames editable.
``_set_container_text`` is the shared text-writing primitive and is also reused
by ``table_fill`` for table-cell edits.
"""
from __future__ import annotations
from typing import Any
from xml.etree import ElementTree as ET
from .ooxml import NS, _qn, _shape_identity, _text_containers
from .selectors import _replacement_text
def _shape_key_maps(slide_root: ET.Element, source_slide: int) -> dict[str, ET.Element]:
maps: dict[str, ET.Element] = {}
for order, container in enumerate(_text_containers(slide_root), start=1):
shape_id, shape_name = _shape_identity(container, order)
maps[f"slot_id:s{source_slide:02d}_sh{shape_id}"] = container
maps[f"shape_id:{shape_id}"] = container
if shape_name:
maps[f"shape_name:{shape_name}"] = container
return maps
def _ensure_text_nodes(container: ET.Element) -> list[ET.Element]:
text_nodes = container.findall(".//a:t", NS)
if text_nodes:
return text_nodes
tx_body = container.find(".//p:txBody", NS)
if tx_body is None:
tx_body = container.find(".//a:txBody", NS)
if tx_body is None:
return []
paragraph = tx_body.find("a:p", NS)
if paragraph is None:
paragraph = ET.SubElement(tx_body, _qn(NS["a"], "p"))
run = paragraph.find("a:r", NS)
if run is None:
run = ET.SubElement(paragraph, _qn(NS["a"], "r"))
text_node = run.find("a:t", NS)
if text_node is None:
text_node = ET.SubElement(run, _qn(NS["a"], "t"))
return [text_node]
def _ensure_paragraph_text_node(paragraph: ET.Element) -> list[ET.Element]:
text_nodes = paragraph.findall(".//a:t", NS)
if text_nodes:
return text_nodes
run = paragraph.find("a:r", NS)
if run is None:
run = ET.SubElement(paragraph, _qn(NS["a"], "r"))
text_node = run.find("a:t", NS)
if text_node is None:
text_node = ET.SubElement(run, _qn(NS["a"], "t"))
return [text_node]
def _set_paragraph_text(paragraph: ET.Element, text: str) -> None:
text_nodes = _ensure_paragraph_text_node(paragraph)
text_nodes[0].text = text
for node in text_nodes[1:]:
node.text = ""
def _set_container_text(container: ET.Element, text: str) -> None:
lines = text.splitlines() or [""]
paragraphs = container.findall(".//a:p", NS)
if len(lines) > 1 and paragraphs:
for index, paragraph in enumerate(paragraphs):
if index < len(lines):
_set_paragraph_text(paragraph, lines[index])
else:
_set_paragraph_text(paragraph, "")
if len(lines) > len(paragraphs):
_set_paragraph_text(paragraphs[-1], "\n".join(lines[len(paragraphs) - 1 :]))
return
text_nodes = _ensure_text_nodes(container)
if not text_nodes:
raise RuntimeError("Matched shape does not contain a text body")
if len(lines) <= len(text_nodes):
for index, node in enumerate(text_nodes):
node.text = lines[index] if index < len(lines) else ""
return
text_nodes[0].text = text
for node in text_nodes[1:]:
node.text = ""
def _apply_replacements_to_slide(
slide_root: ET.Element,
*,
source_slide: int,
replacements: list[dict[str, Any]],
) -> None:
maps = _shape_key_maps(slide_root, source_slide)
errors: list[str] = []
for replacement in replacements:
selectors = []
if replacement.get("slot_id"):
selectors.append(f"slot_id:{replacement['slot_id']}")
if replacement.get("shape_id"):
selectors.append(f"shape_id:{replacement['shape_id']}")
if replacement.get("shape_name"):
selectors.append(f"shape_name:{replacement['shape_name']}")
container = next((maps[key] for key in selectors if key in maps), None)
if container is None:
if replacement.get("optional"):
continue
errors.append(", ".join(selectors) or "<missing selector>")
continue
_set_container_text(container, _replacement_text(replacement))
if errors:
raise RuntimeError(f"Missing replacement target(s) on slide {source_slide}: {'; '.join(errors)}")