| 1 | #!/usr/bin/env python3 |
| 2 | """ |
| 3 | PPT Master - PPTX Hyperlink Resolver |
| 4 | |
| 5 | Resolve native DrawingML click actions into the canonical SVG ``href`` form |
| 6 | used by the authoring and round-trip pipelines. |
| 7 | |
| 8 | Usage: |
| 9 | from .hyperlinks import resolve_click_hyperlink |
| 10 | |
| 11 | Examples: |
| 12 | result = resolve_click_hyperlink(rels, "rId3", "", slide_index_by_part=roster) |
| 13 | |
| 14 | Dependencies: |
| 15 | PPT Master hyperlink contract. |
| 16 | """ |
| 17 | |
| 18 | from __future__ import annotations |
| 19 | |
| 20 | from dataclasses import dataclass |
| 21 | |
| 22 | from hyperlink_contract import ( |
| 23 | HYPERLINK_REL_TYPE, |
| 24 | HyperlinkContractError, |
| 25 | SLIDE_JUMP_ACTION, |
| 26 | SLIDE_REL_TYPE, |
| 27 | parse_hyperlink_target, |
| 28 | ) |
| 29 | |
| 30 | |
| 31 | @dataclass(frozen=True) |
| 32 | class HyperlinkResolution: |
| 33 | """Resolved SVG target or one actionable source-package diagnostic.""" |
| 34 | |
| 35 | href: str | None = None |
| 36 | error: str | None = None |
| 37 | |
| 38 | |
| 39 | def resolve_click_hyperlink( |
| 40 | relationships: dict[str, dict[str, str]], |
| 41 | relationship_id: str, |
| 42 | action: str, |
| 43 | *, |
| 44 | slide_index_by_part: dict[str, int], |
| 45 | ) -> HyperlinkResolution: |
| 46 | """Resolve a click hyperlink relationship from one OOXML source part.""" |
| 47 | if action == "ppaction://media": |
| 48 | return HyperlinkResolution() |
| 49 | if not relationship_id: |
| 50 | return HyperlinkResolution(error="click hyperlink has no relationship id") |
| 51 | relationship = relationships.get(relationship_id) |
| 52 | if relationship is None: |
| 53 | return HyperlinkResolution( |
| 54 | error=f"click hyperlink relationship {relationship_id!r} is missing" |
| 55 | ) |
| 56 | rel_type = relationship.get("type", "") |
| 57 | target = relationship.get("target", "") |
| 58 | external = relationship.get("external") == "1" |
| 59 | |
| 60 | if action == SLIDE_JUMP_ACTION: |
| 61 | if external or rel_type != SLIDE_REL_TYPE: |
| 62 | return HyperlinkResolution( |
| 63 | error="hlinksldjump does not reference an internal slide" |
| 64 | ) |
| 65 | slide_index = slide_index_by_part.get(target) |
| 66 | if slide_index is None: |
| 67 | return HyperlinkResolution( |
| 68 | error=f"slide jump target {target!r} is not in the presentation roster" |
| 69 | ) |
| 70 | return HyperlinkResolution(href=f"#slide-{slide_index}") |
| 71 | |
| 72 | if action.startswith("ppaction://") and action not in { |
| 73 | "ppaction://hlinkurl", |
| 74 | }: |
| 75 | return HyperlinkResolution( |
| 76 | error=f"unsupported PowerPoint click action {action!r}" |
| 77 | ) |
| 78 | if not external or rel_type != HYPERLINK_REL_TYPE: |
| 79 | return HyperlinkResolution( |
| 80 | error="click hyperlink does not reference an external hyperlink" |
| 81 | ) |
| 82 | try: |
| 83 | parsed = parse_hyperlink_target(target) |
| 84 | except HyperlinkContractError as exc: |
| 85 | return HyperlinkResolution(error=str(exc)) |
| 86 | if parsed.kind != "external": |
| 87 | return HyperlinkResolution( |
| 88 | error="external hyperlink relationship resolved to a slide target" |
| 89 | ) |
| 90 | return HyperlinkResolution(href=parsed.raw) |
| 91 | |
| 92 | |
| 93 | __all__ = ["HyperlinkResolution", "resolve_click_hyperlink"] |
| 94 |