返回 ppt-master
marker_status.py
1 """Validate chart/table 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", "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(f"{REPLACE_WITH_ATTR} must use lowercase chart or table")
130 if visual_raw is not None and visual_raw != visual_raw.strip():
131 errors.append(f"{FALLBACK_KIND_ATTR} must not contain surrounding whitespace")
132 if legacy_visual_raw is not None and legacy_visual_raw != legacy_visual_raw.strip():
133 errors.append(
134 f"{LEGACY_FALLBACK_KIND_ATTR} must not contain surrounding whitespace"
135 )
136 if route_raw is not None and route_raw != route:
137 errors.append(f"{LEGACY_ROUTE_STATUS_ATTR} must not contain surrounding whitespace")
138 if visual is not None and visual not in VISUAL_STATUSES:
139 errors.append(f"unsupported {FALLBACK_KIND_ATTR} value: {visual!r}")
140 if route is not None and route not in ROUTE_STATUSES:
141 errors.append(f"unsupported {LEGACY_ROUTE_STATUS_ATTR} value: {route!r}")
142 if any(raw is not None for raw in native_raw_values.values()):
143 if native not in REPLACEMENT_KINDS:
144 errors.append(f"unsupported {REPLACE_WITH_ATTR} value: {native!r}")
145 if any(raw is not None for raw in fallback_status_raw_values.values()):
146 if not fallback:
147 errors.append(f"{REPLACEMENT_STATUS_ATTR} must not be empty")
148 elif fallback not in REPLACEMENT_STATUS_CODES:
149 errors.append(
150 f"unsupported {REPLACEMENT_STATUS_ATTR} value: {fallback!r}"
151 )
152 if any(raw is not None for raw in import_source_raw_values.values()):
153 source = native_import_source(elem)
154 if source != "pptx":
155 errors.append(f"unsupported {IMPORT_SOURCE_ATTR} value: {source!r}")
156 if (
157 visual_raw is None
158 and legacy_visual_raw is not None
159 and visual == "placeholder"
160 and route != "reconstruction-only"
161 ):
162 errors.append(
163 f"{LEGACY_FALLBACK_KIND_ATTR}='placeholder' requires "
164 f"{LEGACY_ROUTE_STATUS_ATTR}='reconstruction-only'"
165 )
166 if route == "reconstruction-only" and visual != "placeholder":
167 errors.append(
168 f"{LEGACY_ROUTE_STATUS_ATTR}='reconstruction-only' requires "
169 f"{FALLBACK_KIND_ATTR}='placeholder'"
170 )
171 if native and fallback:
172 errors.append(
173 f"data-pptx-replace-with and {REPLACEMENT_STATUS_ATTR} are mutually exclusive"
174 )
175 return errors
176
177
178 def native_marker_release_block_reason(elem: ET.Element) -> str | None:
179 """Return invalid status metadata that must block an export.
180
181 A valid ``reconstruction-only`` declaration is diagnostic rather than a
182 release block: default export keeps its visible placeholder, while an
183 an active replacement marker may still reconstruct a native Chart/Table.
184 """
185 errors = native_marker_status_errors(elem)
186 if errors:
187 return f"invalid-status: {errors[0]}"
188 return None
189
189 lines PYTHON