deckbuilder 1.0.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,541 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Robust Convention-Based Naming System for PowerPoint Template Placeholders
4
+
5
+ Provides standardized, semantic placeholder naming using multi-tier detection
6
+ with graceful fallbacks for template variations and missing elements.
7
+ """
8
+
9
+ import re
10
+ from dataclasses import dataclass
11
+ from typing import Dict, List, Optional
12
+
13
+
14
+ @dataclass
15
+ class PlaceholderContext:
16
+ """Context information for placeholder naming decisions"""
17
+
18
+ layout_name: str
19
+ placeholder_idx: str
20
+ placeholder_type: Optional[str] = None
21
+ total_placeholders: int = 0
22
+ existing_names: List[str] = None
23
+ powerpoint_type: Optional[int] = None
24
+
25
+
26
+ @dataclass
27
+ class SemanticInfo:
28
+ """Semantic information for a placeholder"""
29
+
30
+ content_type: str
31
+ position: str
32
+ index: int = 1
33
+ confidence: float = 1.0 # How confident we are in this detection
34
+
35
+
36
+ class NamingConvention:
37
+ """
38
+ Robust convention-based placeholder naming system with multi-tier detection.
39
+
40
+ Handles template variations gracefully and provides semantic names even when
41
+ footer elements or other placeholders are missing.
42
+
43
+ Format: {ContentType}_{Position}_{Index}
44
+ Example: title_top_1, content_col1_1, date_footer_1
45
+ """
46
+
47
+ def __init__(self):
48
+ self.layout_mappings = self._build_layout_mappings()
49
+ self.powerpoint_type_map = self._build_powerpoint_type_map()
50
+ self.universal_patterns = self._build_universal_patterns()
51
+
52
+ def generate_placeholder_name(self, context: PlaceholderContext) -> str:
53
+ """
54
+ Generate standardized placeholder name using multi-tier detection.
55
+
56
+ Args:
57
+ context: PlaceholderContext with layout and placeholder information
58
+
59
+ Returns:
60
+ Standardized placeholder name with confidence score
61
+ """
62
+ semantic_info = self._detect_semantic_info(context)
63
+
64
+ # Generate name from semantic info
65
+ if semantic_info.position and semantic_info.position != "main":
66
+ return f"{semantic_info.content_type}_{semantic_info.position}_{semantic_info.index}"
67
+ else:
68
+ return f"{semantic_info.content_type}_{semantic_info.index}"
69
+
70
+ def _detect_semantic_info(self, context: PlaceholderContext) -> SemanticInfo:
71
+ """Multi-tier detection with confidence scoring"""
72
+
73
+ # Tier 1: Exact layout + index mapping (highest confidence)
74
+ if semantic_info := self._tier1_exact_mapping(context):
75
+ semantic_info.confidence = 1.0
76
+ return semantic_info
77
+
78
+ # Tier 2: PowerPoint built-in types (high confidence)
79
+ if semantic_info := self._tier2_powerpoint_types(context):
80
+ semantic_info.confidence = 0.9
81
+ return semantic_info
82
+
83
+ # Tier 3: Universal patterns (medium confidence)
84
+ if semantic_info := self._tier3_universal_patterns(context):
85
+ semantic_info.confidence = 0.7
86
+ return semantic_info
87
+
88
+ # Tier 4: Layout-based inference (lower confidence)
89
+ if semantic_info := self._tier4_layout_inference(context):
90
+ semantic_info.confidence = 0.5
91
+ return semantic_info
92
+
93
+ # Tier 5: Generic fallback (lowest confidence)
94
+ return self._tier5_generic_fallback(context)
95
+
96
+ def _tier1_exact_mapping(self, context: PlaceholderContext) -> Optional[SemanticInfo]:
97
+ """Tier 1: Exact layout + index mapping"""
98
+ layout_key = self._normalize_layout_name(context.layout_name)
99
+
100
+ if layout_key not in self.layout_mappings:
101
+ return None
102
+
103
+ mapping = self.layout_mappings[layout_key]
104
+ idx = context.placeholder_idx
105
+
106
+ # Check required placeholders first
107
+ if idx in mapping.get("required", {}):
108
+ info = mapping["required"][idx]
109
+ return SemanticInfo(content_type=info["type"], position=info["position"], index=1)
110
+
111
+ # Check optional placeholders (like footer elements)
112
+ if idx in mapping.get("optional", {}):
113
+ info = mapping["optional"][idx]
114
+ return SemanticInfo(content_type=info["type"], position=info["position"], index=1)
115
+
116
+ return None
117
+
118
+ def _tier2_powerpoint_types(self, context: PlaceholderContext) -> Optional[SemanticInfo]:
119
+ """Tier 2: PowerPoint built-in placeholder types"""
120
+ if context.powerpoint_type is None:
121
+ return None
122
+
123
+ type_mapping = self.powerpoint_type_map.get(context.powerpoint_type)
124
+ if not type_mapping:
125
+ return None
126
+
127
+ # Determine position based on layout and index
128
+ position = self._infer_position_from_layout(context)
129
+
130
+ return SemanticInfo(content_type=type_mapping["type"], position=position, index=1)
131
+
132
+ def _tier3_universal_patterns(self, context: PlaceholderContext) -> Optional[SemanticInfo]:
133
+ """Tier 3: Universal patterns that work across layouts"""
134
+ idx = context.placeholder_idx
135
+
136
+ # Universal pattern: Index 0 is almost always the main title
137
+ if idx == "0":
138
+ return SemanticInfo(content_type="title", position="top", index=1)
139
+
140
+ # Universal pattern: Footer elements (may not exist in all templates)
141
+ if idx in ["10", "11", "12"]:
142
+ footer_types = {"10": "date", "11": "footer", "12": "slide_number"}
143
+ return SemanticInfo(content_type=footer_types[idx], position="footer", index=1)
144
+
145
+ # Universal pattern: Index 1 is often main content
146
+ if idx == "1":
147
+ return SemanticInfo(content_type="content", position="main", index=1)
148
+
149
+ return None
150
+
151
+ def _tier4_layout_inference(self, context: PlaceholderContext) -> Optional[SemanticInfo]:
152
+ """Tier 4: Infer from layout structure and index patterns"""
153
+ layout_name = context.layout_name.lower()
154
+ idx = int(context.placeholder_idx) if context.placeholder_idx.isdigit() else 0
155
+
156
+ # Column layouts: Infer column position from index
157
+ if "column" in layout_name:
158
+ return self._infer_column_semantic(context, idx)
159
+
160
+ # Comparison layouts: Infer left/right from index
161
+ if "comparison" in layout_name:
162
+ return self._infer_comparison_semantic(context, idx)
163
+
164
+ # Picture layouts: Infer picture vs caption
165
+ if "picture" in layout_name or "caption" in layout_name:
166
+ return self._infer_picture_semantic(context, idx)
167
+
168
+ # Agenda/list layouts: Infer item position
169
+ if "agenda" in layout_name or "6" in layout_name:
170
+ return self._infer_agenda_semantic(context, idx)
171
+
172
+ return None
173
+
174
+ def _tier5_generic_fallback(self, context: PlaceholderContext) -> SemanticInfo:
175
+ """Tier 5: Generic fallback naming"""
176
+ return SemanticInfo(
177
+ content_type="content",
178
+ position="main",
179
+ index=int(context.placeholder_idx) if context.placeholder_idx.isdigit() else 1,
180
+ confidence=0.1,
181
+ )
182
+
183
+ def _infer_column_semantic(
184
+ self, context: PlaceholderContext, idx: int
185
+ ) -> Optional[SemanticInfo]:
186
+ """Infer semantic info for column layouts"""
187
+ # Four columns: title=13,15,17,19 content=14,16,18,20
188
+ # Three columns: title=13,15,17 content=14,16,18
189
+
190
+ if idx in [13, 14]: # Column 1
191
+ content_type = "title" if idx == 13 else "content"
192
+ return SemanticInfo(content_type=content_type, position="col1", index=1)
193
+ elif idx in [15, 16]: # Column 2
194
+ content_type = "title" if idx == 15 else "content"
195
+ return SemanticInfo(content_type=content_type, position="col2", index=1)
196
+ elif idx in [17, 18]: # Column 3
197
+ content_type = "title" if idx == 17 else "content"
198
+ return SemanticInfo(content_type=content_type, position="col3", index=1)
199
+ elif idx in [19, 20]: # Column 4 (four columns only)
200
+ content_type = "title" if idx == 19 else "content"
201
+ return SemanticInfo(content_type=content_type, position="col4", index=1)
202
+
203
+ return None
204
+
205
+ def _infer_comparison_semantic(
206
+ self, context: PlaceholderContext, idx: int
207
+ ) -> Optional[SemanticInfo]:
208
+ """Infer semantic info for comparison layouts"""
209
+ if idx in [1, 2]: # Left side
210
+ content_type = "title" if idx == 1 else "content"
211
+ return SemanticInfo(content_type=content_type, position="left", index=1)
212
+ elif idx in [3, 4]: # Right side
213
+ content_type = "title" if idx == 3 else "content"
214
+ return SemanticInfo(content_type=content_type, position="right", index=1)
215
+
216
+ return None
217
+
218
+ def _infer_picture_semantic(
219
+ self, context: PlaceholderContext, idx: int
220
+ ) -> Optional[SemanticInfo]:
221
+ """Infer semantic info for picture layouts"""
222
+ if idx == 1:
223
+ return SemanticInfo(content_type="image", position="main", index=1)
224
+ elif idx == 2:
225
+ return SemanticInfo(content_type="text", position="caption", index=1)
226
+
227
+ return None
228
+
229
+ def _infer_agenda_semantic(
230
+ self, context: PlaceholderContext, idx: int
231
+ ) -> Optional[SemanticInfo]:
232
+ """Infer semantic info for agenda layouts"""
233
+ # Agenda items mapping based on actual template structure
234
+ agenda_mapping = {
235
+ 28: ("number", "item1"),
236
+ 18: ("content", "item1"),
237
+ 29: ("number", "item2"),
238
+ 20: ("content", "item2"),
239
+ 30: ("number", "item3"),
240
+ 22: ("content", "item3"),
241
+ 31: ("number", "item4"),
242
+ 19: ("content", "item4"),
243
+ 32: ("number", "item5"),
244
+ 21: ("content", "item5"),
245
+ 33: ("number", "item6"),
246
+ 34: ("content", "item6"),
247
+ }
248
+
249
+ if idx in agenda_mapping:
250
+ content_type, position = agenda_mapping[idx]
251
+ return SemanticInfo(content_type=content_type, position=position, index=1)
252
+
253
+ return None
254
+
255
+ def _infer_position_from_layout(self, context: PlaceholderContext) -> str:
256
+ """Infer position based on layout type and index"""
257
+ idx = context.placeholder_idx
258
+ layout_name = context.layout_name.lower()
259
+
260
+ if idx == "0":
261
+ return "top"
262
+ elif idx in ["10", "11", "12"]:
263
+ return "footer"
264
+ elif "column" in layout_name:
265
+ return "col1" # Default to first column
266
+ elif "comparison" in layout_name:
267
+ return "left" # Default to left side
268
+ else:
269
+ return "main"
270
+
271
+ def _normalize_layout_name(self, layout_name: str) -> str:
272
+ """Normalize layout name for mapping lookup"""
273
+ return layout_name.lower().replace(" ", "_")
274
+
275
+ def _build_layout_mappings(self) -> Dict[str, Dict]:
276
+ """Build comprehensive layout mappings with required/optional elements"""
277
+ return {
278
+ "title_slide": {
279
+ "required": {
280
+ "0": {"type": "title", "position": "top"},
281
+ "1": {"type": "subtitle", "position": "main"},
282
+ },
283
+ "optional": {
284
+ "10": {"type": "date", "position": "footer"},
285
+ "11": {"type": "footer", "position": "footer"},
286
+ "12": {"type": "slide_number", "position": "footer"},
287
+ },
288
+ },
289
+ "title_and_content": {
290
+ "required": {
291
+ "0": {"type": "title", "position": "top"},
292
+ "1": {"type": "content", "position": "main"},
293
+ },
294
+ "optional": {
295
+ "10": {"type": "date", "position": "footer"},
296
+ "11": {"type": "footer", "position": "footer"},
297
+ "12": {"type": "slide_number", "position": "footer"},
298
+ },
299
+ },
300
+ "section_header": {
301
+ "required": {
302
+ "0": {"type": "title", "position": "top"},
303
+ "1": {"type": "text", "position": "main"},
304
+ },
305
+ "optional": {
306
+ "10": {"type": "date", "position": "footer"},
307
+ "11": {"type": "footer", "position": "footer"},
308
+ "12": {"type": "slide_number", "position": "footer"},
309
+ },
310
+ },
311
+ "two_content": {
312
+ "required": {
313
+ "0": {"type": "title", "position": "top"},
314
+ "1": {"type": "content", "position": "left"},
315
+ "2": {"type": "content", "position": "right"},
316
+ },
317
+ "optional": {
318
+ "10": {"type": "date", "position": "footer"},
319
+ "11": {"type": "footer", "position": "footer"},
320
+ "12": {"type": "slide_number", "position": "footer"},
321
+ },
322
+ },
323
+ "comparison": {
324
+ "required": {
325
+ "0": {"type": "title", "position": "top"},
326
+ "1": {"type": "title", "position": "left"},
327
+ "2": {"type": "content", "position": "left"},
328
+ "3": {"type": "title", "position": "right"},
329
+ "4": {"type": "content", "position": "right"},
330
+ },
331
+ "optional": {
332
+ "10": {"type": "date", "position": "footer"},
333
+ "11": {"type": "footer", "position": "footer"},
334
+ "12": {"type": "slide_number", "position": "footer"},
335
+ },
336
+ },
337
+ "three_columns_with_titles": {
338
+ "required": {
339
+ "0": {"type": "title", "position": "top"},
340
+ "13": {"type": "title", "position": "col1"},
341
+ "14": {"type": "content", "position": "col1"},
342
+ "15": {"type": "title", "position": "col2"},
343
+ "16": {"type": "content", "position": "col2"},
344
+ "17": {"type": "title", "position": "col3"},
345
+ "18": {"type": "content", "position": "col3"},
346
+ },
347
+ "optional": {
348
+ "10": {"type": "date", "position": "footer"},
349
+ "11": {"type": "footer", "position": "footer"},
350
+ "12": {"type": "slide_number", "position": "footer"},
351
+ },
352
+ },
353
+ "four_columns_with_titles": {
354
+ "required": {
355
+ "0": {"type": "title", "position": "top"},
356
+ "13": {"type": "title", "position": "col1"},
357
+ "14": {"type": "content", "position": "col1"},
358
+ "15": {"type": "title", "position": "col2"},
359
+ "16": {"type": "content", "position": "col2"},
360
+ "17": {"type": "title", "position": "col3"},
361
+ "18": {"type": "content", "position": "col3"},
362
+ "19": {"type": "title", "position": "col4"},
363
+ "20": {"type": "content", "position": "col4"},
364
+ },
365
+ "optional": {
366
+ "10": {"type": "date", "position": "footer"},
367
+ "11": {"type": "footer", "position": "footer"},
368
+ "12": {"type": "slide_number", "position": "footer"},
369
+ },
370
+ },
371
+ "picture_with_caption": {
372
+ "required": {
373
+ "0": {"type": "title", "position": "top"},
374
+ "1": {"type": "image", "position": "main"},
375
+ "2": {"type": "text", "position": "caption"},
376
+ },
377
+ "optional": {
378
+ "10": {"type": "date", "position": "footer"},
379
+ "11": {"type": "footer", "position": "footer"},
380
+ "12": {"type": "slide_number", "position": "footer"},
381
+ },
382
+ },
383
+ # Additional layouts can be added here
384
+ }
385
+
386
+ def _build_powerpoint_type_map(self) -> Dict[int, Dict[str, str]]:
387
+ """Build mapping of PowerPoint placeholder types to semantic types"""
388
+ # Note: These constants would come from python-pptx PP_PLACEHOLDER enum
389
+ return {
390
+ 1: {"type": "title"}, # PP_PLACEHOLDER.TITLE
391
+ 2: {"type": "content"}, # PP_PLACEHOLDER.BODY
392
+ 3: {"type": "text"}, # PP_PLACEHOLDER.TEXT
393
+ 4: {"type": "date"}, # PP_PLACEHOLDER.DATE
394
+ 5: {"type": "slide_number"}, # PP_PLACEHOLDER.SLIDE_NUMBER
395
+ 6: {"type": "footer"}, # PP_PLACEHOLDER.FOOTER
396
+ 7: {"type": "subtitle"}, # PP_PLACEHOLDER.SUBTITLE
397
+ 18: {"type": "image"}, # PP_PLACEHOLDER.PICTURE
398
+ }
399
+
400
+ def _build_universal_patterns(self) -> Dict[str, Dict]:
401
+ """Build universal patterns that work across all layouts"""
402
+ return {
403
+ "title_indices": ["0"],
404
+ "footer_indices": ["10", "11", "12"],
405
+ "main_content_indices": ["1"],
406
+ }
407
+
408
+ def convert_template_to_conventions(self, template_analysis: Dict) -> Dict:
409
+ """
410
+ Convert existing template analysis to use convention-based naming.
411
+
412
+ Args:
413
+ template_analysis: Template analysis from CLI tools
414
+
415
+ Returns:
416
+ Updated analysis with convention-based placeholder names
417
+ """
418
+ converted_analysis = template_analysis.copy()
419
+ layouts = converted_analysis.get("layouts", {})
420
+
421
+ for layout_name, layout_info in layouts.items():
422
+ placeholders = layout_info.get("placeholders", {})
423
+ converted_placeholders = {}
424
+
425
+ for idx, current_name in placeholders.items():
426
+ context = PlaceholderContext(
427
+ layout_name=layout_name,
428
+ placeholder_idx=idx,
429
+ total_placeholders=len(placeholders),
430
+ existing_names=list(placeholders.values()),
431
+ )
432
+
433
+ convention_name = self.generate_placeholder_name(context)
434
+ converted_placeholders[idx] = convention_name
435
+
436
+ layout_info["placeholders"] = converted_placeholders
437
+
438
+ return converted_analysis
439
+
440
+ def validate_naming_consistency(self, template_analysis: Dict) -> Dict:
441
+ """
442
+ Validate naming consistency and detect improvement opportunities.
443
+
444
+ Returns:
445
+ Validation results with consistency scores and recommendations
446
+ """
447
+ layouts = template_analysis.get("layouts", {})
448
+ validation_results = {
449
+ "consistency_score": 0.0,
450
+ "convention_compliance": 0.0,
451
+ "issues": [],
452
+ "recommendations": [],
453
+ "pattern_analysis": {},
454
+ }
455
+
456
+ # Analyze current naming patterns
457
+ all_names = []
458
+ convention_names = []
459
+
460
+ for layout_name, layout_info in layouts.items():
461
+ placeholders = layout_info.get("placeholders", {})
462
+ all_names.extend(placeholders.values())
463
+
464
+ # Generate convention names for comparison
465
+ for idx in placeholders.keys():
466
+ context = PlaceholderContext(
467
+ layout_name=layout_name,
468
+ placeholder_idx=idx,
469
+ total_placeholders=len(placeholders),
470
+ )
471
+ convention_name = self.generate_placeholder_name(context)
472
+ convention_names.append(convention_name)
473
+
474
+ # Calculate compliance scores
475
+ if all_names:
476
+ convention_compliant = sum(
477
+ 1 for name in all_names if self._follows_convention_format(name)
478
+ )
479
+ validation_results["convention_compliance"] = convention_compliant / len(all_names)
480
+
481
+ # Generate recommendations
482
+ if validation_results["convention_compliance"] < 0.8:
483
+ validation_results["recommendations"].append(
484
+ "Consider using 'enhance --use-conventions' to apply standardized naming"
485
+ )
486
+
487
+ # Detect naming patterns
488
+ patterns = {}
489
+ for name in all_names:
490
+ pattern = self._extract_naming_pattern(name)
491
+ patterns[pattern] = patterns.get(pattern, 0) + 1
492
+
493
+ validation_results["pattern_analysis"] = patterns
494
+
495
+ return validation_results
496
+
497
+ def _follows_convention_format(self, placeholder_name: str) -> bool:
498
+ """Check if placeholder name follows convention format"""
499
+ # Convention format: {ContentType}_{Position}_{Index} or {ContentType}_{Index}
500
+ pattern = r"^(title|subtitle|content|text|image|number|date|footer|slide_number)_([a-z0-9]+_)?\d+$"
501
+ return bool(re.match(pattern, placeholder_name.lower()))
502
+
503
+ def _extract_naming_pattern(self, placeholder_name: str) -> str:
504
+ """Extract naming pattern from placeholder name"""
505
+ # Simplify name to pattern (e.g., "Title 1" -> "Title N")
506
+ return re.sub(r"\d+", "N", placeholder_name)
507
+
508
+
509
+ def test_naming_convention():
510
+ """Test the robust naming convention system"""
511
+ convention = NamingConvention()
512
+
513
+ test_cases = [
514
+ # Test Four Columns layout
515
+ PlaceholderContext("Four Columns With Titles", "13"),
516
+ PlaceholderContext("Four Columns With Titles", "14"),
517
+ PlaceholderContext("Four Columns With Titles", "0"),
518
+ PlaceholderContext("Four Columns With Titles", "10"), # Optional footer
519
+ # Test unknown layout
520
+ PlaceholderContext("Custom Layout", "1"),
521
+ PlaceholderContext("Custom Layout", "5"),
522
+ # Test minimal template (no footer)
523
+ PlaceholderContext("Simple Title", "0"),
524
+ PlaceholderContext("Simple Title", "1"),
525
+ ]
526
+
527
+ print("Testing Robust Naming Convention:")
528
+ print("=" * 50)
529
+
530
+ for context in test_cases:
531
+ name = convention.generate_placeholder_name(context)
532
+ semantic_info = convention._detect_semantic_info(context)
533
+ print(f"Layout: {context.layout_name}")
534
+ print(f"Index: {context.placeholder_idx}")
535
+ print(f"Generated: {name}")
536
+ print(f"Confidence: {semantic_info.confidence:.1f}")
537
+ print("-" * 30)
538
+
539
+
540
+ if __name__ == "__main__":
541
+ test_naming_convention()
@@ -0,0 +1,101 @@
1
+ """
2
+ PowerPoint Placeholder Type Constants and Mappings
3
+
4
+ This module defines semantic groupings of PowerPoint placeholder types for
5
+ generic content placement without hardcoding layout names. This allows the
6
+ deckbuilder to work with any PowerPoint template by detecting placeholder
7
+ types rather than relying on specific layout configurations.
8
+
9
+ Based on python-pptx PP_PLACEHOLDER_TYPE enumeration.
10
+ """
11
+
12
+ from pptx.enum.shapes import PP_PLACEHOLDER_TYPE
13
+
14
+ # Title-related placeholders - for slide titles and headings
15
+ TITLE_PLACEHOLDERS = {
16
+ PP_PLACEHOLDER_TYPE.TITLE, # TITLE (1) - Standard slide title
17
+ PP_PLACEHOLDER_TYPE.CENTER_TITLE, # CENTER_TITLE (3) - Centered title (title slides)
18
+ PP_PLACEHOLDER_TYPE.VERTICAL_TITLE, # VERTICAL_TITLE (5) - Vertical orientation title
19
+ }
20
+
21
+ # Subtitle placeholders - for slide subtitles (typically on title slides)
22
+ SUBTITLE_PLACEHOLDERS = {PP_PLACEHOLDER_TYPE.SUBTITLE} # SUBTITLE (4) - Subtitle text
23
+
24
+ # Main content placeholders - for primary slide content, bullets, paragraphs
25
+ CONTENT_PLACEHOLDERS = {
26
+ PP_PLACEHOLDER_TYPE.BODY, # BODY (2) - Main content area
27
+ PP_PLACEHOLDER_TYPE.VERTICAL_BODY, # VERTICAL_BODY (6) - Vertical text content
28
+ }
29
+
30
+ # Media and object placeholders - for rich content like images, charts, tables
31
+ MEDIA_PLACEHOLDERS = {
32
+ PP_PLACEHOLDER_TYPE.PICTURE, # PICTURE (18) - Image placeholders
33
+ PP_PLACEHOLDER_TYPE.CHART, # CHART (8) - Chart/graph placeholders
34
+ PP_PLACEHOLDER_TYPE.TABLE, # TABLE (12) - Data table placeholders
35
+ PP_PLACEHOLDER_TYPE.MEDIA_CLIP, # MEDIA_CLIP (10) - Video/audio content
36
+ PP_PLACEHOLDER_TYPE.OBJECT, # OBJECT (7) - Generic embedded objects
37
+ PP_PLACEHOLDER_TYPE.VERTICAL_OBJECT, # VERTICAL_OBJECT (17) - Vertical objects
38
+ }
39
+
40
+ # Layout and metadata placeholders - for slide decorations and information
41
+ LAYOUT_PLACEHOLDERS = {
42
+ PP_PLACEHOLDER_TYPE.HEADER, # HEADER (14) - Page header text
43
+ PP_PLACEHOLDER_TYPE.FOOTER, # FOOTER (15) - Page footer text
44
+ PP_PLACEHOLDER_TYPE.DATE, # DATE (16) - Date/timestamp
45
+ PP_PLACEHOLDER_TYPE.SLIDE_NUMBER, # SLIDE_NUMBER (13) - Slide numbering
46
+ }
47
+
48
+ # Specialized placeholders - for specific content types
49
+ SPECIAL_PLACEHOLDERS = {
50
+ PP_PLACEHOLDER_TYPE.ORG_CHART, # ORG_CHART (11) - Organization charts
51
+ PP_PLACEHOLDER_TYPE.BITMAP, # BITMAP (9) - Bitmap images
52
+ PP_PLACEHOLDER_TYPE.SLIDE_IMAGE, # SLIDE_IMAGE (101) - Slide thumbnail images
53
+ PP_PLACEHOLDER_TYPE.MIXED, # MIXED (-2) - Mixed content types
54
+ }
55
+
56
+ # All placeholder types grouped by semantic function
57
+ ALL_PLACEHOLDER_GROUPS = {
58
+ "title": TITLE_PLACEHOLDERS,
59
+ "subtitle": SUBTITLE_PLACEHOLDERS,
60
+ "content": CONTENT_PLACEHOLDERS,
61
+ "media": MEDIA_PLACEHOLDERS,
62
+ "layout": LAYOUT_PLACEHOLDERS,
63
+ "special": SPECIAL_PLACEHOLDERS,
64
+ }
65
+
66
+
67
+ def get_placeholder_category(placeholder_type):
68
+ """
69
+ Determine the semantic category of a placeholder type.
70
+
71
+ Args:
72
+ placeholder_type: PP_PLACEHOLDER_TYPE enum value
73
+
74
+ Returns:
75
+ str: Category name ('title', 'subtitle', 'content', 'media', 'layout', 'special')
76
+ None: If placeholder type is not recognized
77
+ """
78
+ for category, types in ALL_PLACEHOLDER_GROUPS.items():
79
+ if placeholder_type in types:
80
+ return category
81
+ return None
82
+
83
+
84
+ def is_title_placeholder(placeholder_type):
85
+ """Check if placeholder type is for titles."""
86
+ return placeholder_type in TITLE_PLACEHOLDERS
87
+
88
+
89
+ def is_subtitle_placeholder(placeholder_type):
90
+ """Check if placeholder type is for subtitles."""
91
+ return placeholder_type in SUBTITLE_PLACEHOLDERS
92
+
93
+
94
+ def is_content_placeholder(placeholder_type):
95
+ """Check if placeholder type is for main content."""
96
+ return placeholder_type in CONTENT_PLACEHOLDERS
97
+
98
+
99
+ def is_media_placeholder(placeholder_type):
100
+ """Check if placeholder type is for media/objects."""
101
+ return placeholder_type in MEDIA_PLACEHOLDERS