Release standalone PPT template workflow 0.5.0
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
"""apply: page-to-page transitions for cloned slides.
|
||||
|
||||
Template Fill preserves each source transition unless the CLI or a per-slide
|
||||
plan entry requests a replacement. Effects and OOXML mutation come from the
|
||||
shared ``pptx_transitions`` core so every PPTX path uses the same writer.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from pptx_transitions import (
|
||||
AdvanceUpdate,
|
||||
EnterUpdate,
|
||||
apply_slide_motion,
|
||||
normalize_transition_effect_request,
|
||||
validate_seconds,
|
||||
)
|
||||
|
||||
KEEP_TRANSITION = "keep"
|
||||
# Preserve source transitions unless the CLI or a per-slide plan entry selects
|
||||
# a replacement. The duration is consumed only when a visual effect is written.
|
||||
DEFAULT_TRANSITION = KEEP_TRANSITION
|
||||
DEFAULT_TRANSITION_DURATION = 0.5
|
||||
TRANSITION_OBJECT_FIELDS = frozenset(
|
||||
{
|
||||
"effect",
|
||||
"effect_options",
|
||||
"duration",
|
||||
"advance_after",
|
||||
}
|
||||
)
|
||||
|
||||
_UNSET = object()
|
||||
|
||||
|
||||
def transition_unknown_fields(raw: dict[str, Any]) -> list[str]:
|
||||
"""Return unsupported fields from a per-slide transition object."""
|
||||
return sorted(set(raw) - TRANSITION_OBJECT_FIELDS)
|
||||
|
||||
|
||||
def _set_slide_transition(
|
||||
slide_root: ET.Element,
|
||||
*,
|
||||
effect: str | None,
|
||||
duration: float,
|
||||
effect_options: dict[str, object] | None = None,
|
||||
advance_after: float | None = None,
|
||||
) -> bool:
|
||||
"""Apply a legacy template-fill transition through the shared core.
|
||||
|
||||
``None`` and ``keep`` preserve the source transition. ``none`` removes the
|
||||
visual transition while retaining an explicitly requested auto-advance.
|
||||
Legacy ``advance_after`` allowed both click and timed advance, so it maps to
|
||||
``both`` rather than the stricter ``after`` mode. The return value reports
|
||||
whether the resulting slide contains an automatic advance.
|
||||
"""
|
||||
if effect is None or effect == KEEP_TRANSITION:
|
||||
enter = EnterUpdate(policy="preserve")
|
||||
advance = AdvanceUpdate(
|
||||
mode="preserve" if advance_after is None else "both",
|
||||
after=advance_after,
|
||||
)
|
||||
elif effect == "none":
|
||||
enter = EnterUpdate(policy="none", effect=None, duration=duration)
|
||||
advance = AdvanceUpdate(
|
||||
mode="click" if advance_after is None else "both",
|
||||
after=advance_after,
|
||||
)
|
||||
else:
|
||||
enter = EnterUpdate(
|
||||
policy="replace",
|
||||
effect=effect,
|
||||
duration=duration,
|
||||
effect_options=effect_options,
|
||||
)
|
||||
advance = AdvanceUpdate(
|
||||
mode="click" if advance_after is None else "both",
|
||||
after=advance_after,
|
||||
)
|
||||
|
||||
try:
|
||||
return apply_slide_motion(
|
||||
slide_root,
|
||||
enter=enter,
|
||||
advance=advance,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise RuntimeError(str(exc)) from exc
|
||||
|
||||
|
||||
def _resolve_slide_transition(
|
||||
item: dict[str, Any],
|
||||
*,
|
||||
default_effect: str | None,
|
||||
default_duration: float,
|
||||
) -> tuple[str | None, dict[str, object], float, float | None]:
|
||||
"""Pick a slide's transition from its plan entry, falling back to CLI defaults."""
|
||||
raw = item.get("transition", _UNSET)
|
||||
if raw is _UNSET:
|
||||
if default_effect in (None, "none", KEEP_TRANSITION):
|
||||
return default_effect, {}, default_duration, None
|
||||
effect, effect_options = normalize_transition_effect_request(
|
||||
default_effect,
|
||||
allow_none=False,
|
||||
)
|
||||
return effect, effect_options, default_duration, None
|
||||
if isinstance(raw, dict):
|
||||
unknown = transition_unknown_fields(raw)
|
||||
if unknown:
|
||||
raise RuntimeError(
|
||||
"Transition has unknown field(s): " + ", ".join(unknown)
|
||||
)
|
||||
effect = raw.get("effect", default_effect)
|
||||
raw_options = raw.get("effect_options")
|
||||
if raw_options is not None and "effect" not in raw:
|
||||
raise RuntimeError(
|
||||
"Transition effect_options requires an explicit effect"
|
||||
)
|
||||
duration = raw.get("duration", default_duration)
|
||||
advance_after = raw.get("advance_after")
|
||||
else:
|
||||
effect = None if raw is None else str(raw)
|
||||
raw_options = None
|
||||
duration = default_duration
|
||||
advance_after = None
|
||||
effect_options: dict[str, object] = {}
|
||||
if effect is not None and effect not in ("none", KEEP_TRANSITION):
|
||||
try:
|
||||
effect, effect_options = normalize_transition_effect_request(
|
||||
effect,
|
||||
raw_options,
|
||||
allow_none=False,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise RuntimeError(str(exc)) from exc
|
||||
elif raw_options not in (None, {}):
|
||||
raise RuntimeError(
|
||||
"Transition effect_options requires an explicit native effect"
|
||||
)
|
||||
try:
|
||||
resolved_duration = validate_seconds(
|
||||
duration,
|
||||
"transition duration",
|
||||
allow_zero=False,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise RuntimeError(str(exc)) from exc
|
||||
return effect, effect_options, resolved_duration, advance_after
|
||||
Reference in New Issue
Block a user