返回 ppt-master
marker_status.py
1 """Validate native replacement fallback and release-route attributes."""
2
3 from __future__ import annotations
4
5 from xml.etree import ElementTree as ET
6
7 from .marker_attributes import (
8 FALLBACK_KIND_ATTR,
9 IMPORT_SOURCE_ATTR,
10 LEGACY_FALLBACK_KIND_ATTR,
11 LEGACY_IMPORT_SOURCE_ATTR,
12 LEGACY_REPLACEMENT_STATUS_ATTR,
13 LEGACY_REPLACE_WITH_ATTR,
14 LEGACY_ROUTE_STATUS_ATTR,
15 REPLACEMENT_STATUS_ATTR,
16 REPLACE_WITH_ATTR,
17 NativeMarkerAttributeError,
18 native_fallback_kind,
19 native_import_source,
20 native_replacement_kind,
21 native_replacement_status,
22 )
23
24
25 VISUAL_STATUSES = frozenset({"source-preview", "normalized", "placeholder"})
26 ROUTE_STATUSES = frozenset({"reconstruction-only"})
27 REPLACEMENT_KINDS = frozenset({"chart", "formula", "table"})
28 # Closed importer outputs from chart_to_svg, chartex_to_svg, and tbl_to_svg.
29 # This includes codes forwarded through their dynamic ``status`` parameters.
30 REPLACEMENT_STATUS_CODES = frozenset({
31 "unsupported-3d-chart",
32 "unsupported-chart-analysis-features",
33 "unsupported-chart-axis-number-format",
34 "unsupported-chart-axis-options",
35 "unsupported-chart-axis-titles",
36 "unsupported-chart-bar-options",
37 "unsupported-chart-bubble-options",
38 "unsupported-chart-cache",
39 "unsupported-chart-category-format",
40 "unsupported-chart-data-labels",
41 "unsupported-chart-data-table",
42 "unsupported-chart-doughnut-options",
43 "unsupported-chart-legend-position",
44 "unsupported-chart-line-style",
45 "unsupported-chart-of-pie-options",
46 "unsupported-chart-parse",
47 "unsupported-chart-part",
48 "unsupported-chart-pie-options",
49 "unsupported-chart-plot",
50 "unsupported-chart-point-labels",
51 "unsupported-chart-radar-style",
52 "unsupported-chart-reference",
53 "unsupported-chart-relationship",
54 "unsupported-chart-scatter-style",
55 "unsupported-chart-schema",
56 "unsupported-chart-series-data-labels",
57 "unsupported-chart-series-order",
58 "unsupported-chart-series-style",
59 "unsupported-chart-type",
60 "unsupported-chart-uri",
61 "unsupported-chartex-cache",
62 "unsupported-chartex-data-id",
63 "unsupported-chartex-dimension",
64 "unsupported-chartex-parse",
65 "unsupported-chartex-part",
66 "unsupported-chartex-schema",
67 "unsupported-chartex-series",
68 "unsupported-chartex-structure",
69 "unsupported-chartex-type",
70 "unsupported-combo-category-format",
71 "unsupported-combo-category-layout",
72 "unsupported-combo-chart",
73 "unsupported-combo-series-order",
74 "unsupported-date-axis",
75 "unsupported-date-system",
76 "unsupported-formatted-category-cache",
77 "unsupported-merge-topology",
78 "unsupported-native-transform",
79 "unsupported-stock-chart",
80 "unsupported-table-direct-formatting",
81 "unsupported-table-geometry",
82 "unsupported-table-size",
83 "unsupported-table-style",
84 })
85
86
87 def native_marker_status_errors(elem: ET.Element) -> list[str]:
88 """Return invalid or contradictory replacement status declarations."""
89 errors: list[str] = []
90 native_raw_values = {
91 REPLACE_WITH_ATTR: elem.get(REPLACE_WITH_ATTR),
92 LEGACY_REPLACE_WITH_ATTR: elem.get(LEGACY_REPLACE_WITH_ATTR),
93 }
94 fallback_status_raw_values = {
95 REPLACEMENT_STATUS_ATTR: elem.get(REPLACEMENT_STATUS_ATTR),
96 LEGACY_REPLACEMENT_STATUS_ATTR: elem.get(LEGACY_REPLACEMENT_STATUS_ATTR),
97 }
98 import_source_raw_values = {
99 IMPORT_SOURCE_ATTR: elem.get(IMPORT_SOURCE_ATTR),
100 LEGACY_IMPORT_SOURCE_ATTR: elem.get(LEGACY_IMPORT_SOURCE_ATTR),
101 }
102 visual_raw = elem.get(FALLBACK_KIND_ATTR)
103 legacy_visual_raw = elem.get(LEGACY_FALLBACK_KIND_ATTR)
104 route_raw = elem.get(LEGACY_ROUTE_STATUS_ATTR)
105 try:
106 visual = native_fallback_kind(elem)
107 native = native_replacement_kind(elem)
108 fallback = native_replacement_status(elem)
109 native_import_source(elem)
110 except NativeMarkerAttributeError as exc:
111 errors.append(str(exc))
112 return errors
113
114 route = route_raw.strip() if route_raw is not None else None
115
116 for attr, raw in (
117 *native_raw_values.items(),
118 *fallback_status_raw_values.items(),
119 *import_source_raw_values.items(),
120 ):
121 if raw is not None and raw != raw.strip():
122 errors.append(f"{attr} must not contain surrounding whitespace")
123 canonical_kind_raw = native_raw_values[REPLACE_WITH_ATTR]
124 if (
125 canonical_kind_raw is not None
126 and canonical_kind_raw == canonical_kind_raw.strip()
127 and canonical_kind_raw != canonical_kind_raw.lower()
128 ):
129 errors.append(
130 f"{REPLACE_WITH_ATTR} must use lowercase chart, formula, or table"
131 )
132 if visual_raw is not None and visual_raw != visual_raw.strip():
133 errors.append(f"{FALLBACK_KIND_ATTR} must not contain surrounding whitespace")
134 if legacy_visual_raw is not None and legacy_visual_raw != legacy_visual_raw.strip():
135 errors.append(
136 f"{LEGACY_FALLBACK_KIND_ATTR} must not contain surrounding whitespace"
137 )
138 if route_raw is not None and route_raw != route:
139 errors.append(f"{LEGACY_ROUTE_STATUS_ATTR} must not contain surrounding whitespace")
140 if visual is not None and visual not in VISUAL_STATUSES:
141 errors.append(f"unsupported {FALLBACK_KIND_ATTR} value: {visual!r}")
142 if route is not None and route not in ROUTE_STATUSES:
143 errors.append(f"unsupported {LEGACY_ROUTE_STATUS_ATTR} value: {route!r}")
144 if any(raw is not None for raw in native_raw_values.values()):
145 if native not in REPLACEMENT_KINDS:
146 errors.append(f"unsupported {REPLACE_WITH_ATTR} value: {native!r}")
147 if any(raw is not None for raw in fallback_status_raw_values.values()):
148 if not fallback:
149 errors.append(f"{REPLACEMENT_STATUS_ATTR} must not be empty")
150 elif fallback not in REPLACEMENT_STATUS_CODES:
151 errors.append(
152 f"unsupported {REPLACEMENT_STATUS_ATTR} value: {fallback!r}"
153 )
154 if any(raw is not None for raw in import_source_raw_values.values()):
155 source = native_import_source(elem)
156 if source != "pptx":
157 errors.append(f"unsupported {IMPORT_SOURCE_ATTR} value: {source!r}")
158 if (
159 visual_raw is None
160 and legacy_visual_raw is not None
161 and visual == "placeholder"
162 and route != "reconstruction-only"
163 ):
164 errors.append(
165 f"{LEGACY_FALLBACK_KIND_ATTR}='placeholder' requires "
166 f"{LEGACY_ROUTE_STATUS_ATTR}='reconstruction-only'"
167 )
168 if route == "reconstruction-only" and visual != "placeholder":
169 errors.append(
170 f"{LEGACY_ROUTE_STATUS_ATTR}='reconstruction-only' requires "
171 f"{FALLBACK_KIND_ATTR}='placeholder'"
172 )
173 if native and fallback:
174 errors.append(
175 f"data-pptx-replace-with and {REPLACEMENT_STATUS_ATTR} are mutually exclusive"
176 )
177 return errors
178
179
180 def native_marker_release_block_reason(elem: ET.Element) -> str | None:
181 """Return invalid status metadata that must block an export.
182
183 A valid ``reconstruction-only`` declaration is diagnostic rather than a
184 release block: default export keeps its visible placeholder, while an
185 an active replacement marker may still reconstruct a native Chart/Table.
186 """
187 errors = native_marker_status_errors(elem)
188 if errors:
189 return f"invalid-status: {errors[0]}"
190 return None
191
191 lines PYTHON