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

176 lines
5.2 KiB
Python

"""Shared projection and integrity helpers for mirror-template text slots."""
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass
from xml.etree import ElementTree as ET
MODEL_TEXT_SLOT_KEYS = (
"selector",
"role",
"current_text",
"text_segments",
"tspan_count",
)
def _local_name(tag: str) -> str:
return tag.rsplit("}", 1)[-1]
def _ancestor_chain(
element: ET.Element,
parent_by_child: dict[ET.Element, ET.Element],
) -> list[ET.Element]:
chain = [element]
while chain[-1] in parent_by_child:
chain.append(parent_by_child[chain[-1]])
return chain
def _nearest_attribute(chain: list[ET.Element], name: str) -> str | None:
for element in chain:
value = element.get(name)
if value is not None:
return value
return None
def _text_selector(
element: ET.Element,
parent_by_child: dict[ET.Element, ET.Element],
*,
compact: bool,
) -> str:
segments: list[str] = []
current = element
while True:
element_id = (current.get("id") or "").strip()
if element_id:
segments.append(f"#{element_id}")
break
parent = parent_by_child.get(current)
tag = _local_name(current.tag)
if parent is None:
segments.append(tag)
break
same_tag = [child for child in parent if _local_name(child.tag) == tag]
if compact and len(same_tag) == 1:
segments.append(tag)
else:
segments.append(f"{tag}:nth-of-type({same_tag.index(current) + 1})")
current = parent
separator = ">" if compact else " > "
return separator.join(reversed(segments))
def _text_topology_sha256(element: ET.Element) -> str:
"""Hash text/tspan topology and attributes while excluding visible values."""
digest = hashlib.sha256()
def visit(node: ET.Element) -> None:
digest.update(_local_name(node.tag).encode("utf-8"))
for name, value in sorted(node.attrib.items()):
digest.update(b"\0a")
digest.update(name.encode("utf-8"))
digest.update(b"\0")
digest.update(value.encode("utf-8"))
for child in node:
digest.update(b"\0c")
visit(child)
digest.update(b"\0e")
visit(element)
return digest.hexdigest()
@dataclass(frozen=True)
class TemplateTextSlot:
selector: str
legacy_selector: str
role: str
current_text: str
text_segments: tuple[str, ...]
tspan_count: int
topology_sha256: str
editable: bool
def model_payload(self) -> dict[str, object]:
return {
"selector": self.selector,
"role": self.role,
"current_text": self.current_text,
"text_segments": list(self.text_segments),
"tspan_count": self.tspan_count,
}
def analyze_template_text_slots(root: ET.Element) -> tuple[TemplateTextSlot, ...]:
"""Derive the model projection and tool-only integrity facts from one SVG."""
parent_by_child = {
child: parent
for parent in root.iter()
for child in parent
}
slots: list[TemplateTextSlot] = []
for text_element in root.iter():
if _local_name(text_element.tag) != "text":
continue
chain = _ancestor_chain(text_element, parent_by_child)
placeholder = _nearest_attribute(chain, "data-pptx-placeholder")
editable_value = _nearest_attribute(chain, "data-pptx-editable")
inherited_layer = _nearest_attribute(chain, "data-pptx-layer")
tspans = [
child
for child in text_element.iter()
if child is not text_element and _local_name(child.tag) == "tspan"
]
segments = [
text_element.text or "",
*[(tspan.text or "") for tspan in tspans],
]
if tspans and not segments[0].strip():
segments = segments[1:]
slots.append(TemplateTextSlot(
selector=_text_selector(
text_element,
parent_by_child,
compact=True,
),
legacy_selector=_text_selector(
text_element,
parent_by_child,
compact=False,
),
role=placeholder or "text",
current_text="".join(text_element.itertext()),
text_segments=tuple(segments),
tspan_count=len(tspans),
topology_sha256=_text_topology_sha256(text_element),
editable=editable_value != "false" and inherited_layer is None,
))
selectors = [slot.selector for slot in slots]
if len(selectors) != len(set(selectors)):
raise ValueError("template text selectors are not unique")
return tuple(slots)
def text_slot_integrity_sha256(slots: tuple[TemplateTextSlot, ...]) -> str:
"""Hash selectors plus immutable text/tspan topology and attributes."""
payload = [
{
"selector": slot.selector,
"topology_sha256": slot.topology_sha256,
}
for slot in slots
]
serialized = json.dumps(
payload,
ensure_ascii=False,
separators=(",", ":"),
).encode("utf-8")
return hashlib.sha256(serialized).hexdigest()