返回 ppt-master
marker_attributes.py
根目录 / skills / ppt-master / scripts / svg_to_pptx / native_objects / marker_attributes.py
1 """Canonical and compatible attribute access for chart/table replacements."""
2
3 from __future__ import annotations
4
5 from collections.abc import Callable
6 from xml.etree import ElementTree as ET
7
8
9 REPLACE_WITH_ATTR = "data-pptx-replace-with"
10 LEGACY_REPLACE_WITH_ATTR = "data-pptx-native"
11 REPLACEMENT_STATUS_ATTR = "data-pptx-replacement-status"
12 LEGACY_REPLACEMENT_STATUS_ATTR = "data-pptx-native-status"
13 IMPORT_SOURCE_ATTR = "data-pptx-import-source"
14 LEGACY_IMPORT_SOURCE_ATTR = "data-pptx-native-source"
15 FALLBACK_KIND_ATTR = "data-pptx-fallback-kind"
16 LEGACY_FALLBACK_KIND_ATTR = "data-pptx-visual-status"
17 LEGACY_ROUTE_STATUS_ATTR = "data-pptx-route-status"
18
19
20 class NativeMarkerAttributeError(ValueError):
21 """Raised when canonical and compatible marker attributes conflict."""
22
23
24 def _normalized_token(value: str) -> str:
25 return value.strip().lower()
26
27
28 def _resolved_alias(
29 elem: ET.Element,
30 canonical: str,
31 legacy: str,
32 *,
33 normalize: Callable[[str], str] = _normalized_token,
34 ) -> str | None:
35 canonical_raw = elem.get(canonical)
36 legacy_raw = elem.get(legacy)
37 canonical_value = normalize(canonical_raw) if canonical_raw is not None else None
38 legacy_value = normalize(legacy_raw) if legacy_raw is not None else None
39 if (
40 canonical_value is not None
41 and legacy_value is not None
42 and canonical_value != legacy_value
43 ):
44 raise NativeMarkerAttributeError(
45 f"{canonical}={canonical_raw!r} conflicts with compatible "
46 f"{legacy}={legacy_raw!r}"
47 )
48 return canonical_value if canonical_value is not None else legacy_value
49
50
51 def native_replacement_kind(elem: ET.Element) -> str:
52 """Return the requested chart/table replacement kind, or an empty string."""
53 return _resolved_alias(elem, REPLACE_WITH_ATTR, LEGACY_REPLACE_WITH_ATTR) or ""
54
55
56 def native_replacement_status(elem: ET.Element) -> str:
57 """Return the fallback-only replacement status, or an empty string."""
58 return (
59 _resolved_alias(
60 elem,
61 REPLACEMENT_STATUS_ATTR,
62 LEGACY_REPLACEMENT_STATUS_ATTR,
63 normalize=str.strip,
64 )
65 or ""
66 )
67
68
69 def native_import_source(elem: ET.Element) -> str:
70 """Return replacement payload provenance, or an empty string."""
71 return (
72 _resolved_alias(
73 elem,
74 IMPORT_SOURCE_ATTR,
75 LEGACY_IMPORT_SOURCE_ATTR,
76 normalize=str.strip,
77 )
78 or ""
79 )
80
81
82 def native_fallback_kind(elem: ET.Element) -> str | None:
83 """Return the visible fallback classification when declared."""
84 return _resolved_alias(
85 elem,
86 FALLBACK_KIND_ATTR,
87 LEGACY_FALLBACK_KIND_ATTR,
88 normalize=str.strip,
89 )
90
91
92 def native_metadata_payload_matches(
93 elem: ET.Element,
94 parent_kind: str,
95 ) -> bool:
96 """Return whether one metadata node carries this replacement payload.
97
98 Compatible metadata kind attributes remain readable, but any declared
99 kind must agree with both its alias and the parent replacement marker.
100 """
101 native_kind = elem.get(LEGACY_REPLACE_WITH_ATTR)
102 compatible_kind = elem.get("data-pptx-kind")
103 for attr, raw in (
104 (LEGACY_REPLACE_WITH_ATTR, native_kind),
105 ("data-pptx-kind", compatible_kind),
106 ):
107 if raw is not None and raw != raw.strip():
108 raise NativeMarkerAttributeError(
109 f"metadata {attr} must not contain surrounding whitespace"
110 )
111 if native_kind is not None and compatible_kind is not None:
112 if _normalized_token(native_kind) != _normalized_token(compatible_kind):
113 raise NativeMarkerAttributeError(
114 "metadata data-pptx-native conflicts with data-pptx-kind"
115 )
116 declared_raw = native_kind if native_kind is not None else compatible_kind
117 declared_kind = _normalized_token(declared_raw) if declared_raw is not None else None
118 if declared_kind is not None and declared_kind != parent_kind:
119 raise NativeMarkerAttributeError(
120 f"metadata kind {declared_kind!r} conflicts with parent "
121 f"replacement kind {parent_kind!r}"
122 )
123 metadata_type_raw = elem.get("type")
124 if metadata_type_raw is not None and metadata_type_raw != metadata_type_raw.strip():
125 raise NativeMarkerAttributeError(
126 "metadata type must not contain surrounding whitespace"
127 )
128 metadata_type = (metadata_type_raw or "").lower()
129 return metadata_type == "application/json" or declared_kind == parent_kind
130
131
132 def native_marker_legacy_warnings(elem: ET.Element) -> list[str]:
133 """Return migration advice for compatible legacy marker spellings."""
134 tag = elem.tag.rsplit("}", 1)[-1]
135 if tag == "metadata":
136 native_kind = elem.get(LEGACY_REPLACE_WITH_ATTR)
137 compatible_kind = elem.get("data-pptx-kind")
138 if (
139 native_kind is not None
140 and compatible_kind is not None
141 and _normalized_token(native_kind) != _normalized_token(compatible_kind)
142 ):
143 return []
144 warnings = []
145 for legacy in (LEGACY_REPLACE_WITH_ATTR, "data-pptx-kind"):
146 if elem.get(legacy) is not None:
147 warnings.append(
148 f"legacy metadata attribute {legacy} is compatible; use "
149 'type="application/json" because the parent replacement '
150 "marker determines the payload kind. No change is required "
151 "for export."
152 )
153 return warnings
154
155 try:
156 native_replacement_kind(elem)
157 native_replacement_status(elem)
158 native_import_source(elem)
159 native_fallback_kind(elem)
160 except NativeMarkerAttributeError:
161 return []
162
163 warnings = []
164 replacements = (
165 (LEGACY_REPLACE_WITH_ATTR, REPLACE_WITH_ATTR),
166 (LEGACY_REPLACEMENT_STATUS_ATTR, REPLACEMENT_STATUS_ATTR),
167 (LEGACY_IMPORT_SOURCE_ATTR, IMPORT_SOURCE_ATTR),
168 (LEGACY_FALLBACK_KIND_ATTR, FALLBACK_KIND_ATTR),
169 )
170 for legacy, canonical in replacements:
171 if elem.get(legacy) is not None:
172 warnings.append(
173 f"legacy attribute {legacy} is compatible; use {canonical} "
174 "for project-canonical SVG. No change is required for export."
175 )
176 if elem.get(LEGACY_ROUTE_STATUS_ATTR) is not None:
177 warnings.append(
178 f"legacy attribute {LEGACY_ROUTE_STATUS_ATTR} is compatible; omit it "
179 f"because {FALLBACK_KIND_ATTR}='placeholder' already implies the "
180 "reconstruction-only route. No change is required for export."
181 )
182 return warnings
183
183 lines PYTHON