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,862 @@
1
+ """
2
+ Structured Frontmatter System for Clean YAML Layout Authoring
3
+
4
+ This module provides clean, human-readable YAML structures that abstract away
5
+ PowerPoint placeholder names while maintaining full functionality. It includes:
6
+
7
+ 1. Registry of structured patterns for different layout types
8
+ 2. Bidirectional conversion between structured YAML and placeholder mappings
9
+ 3. Validation system for structured frontmatter
10
+ 4. Fallback handling when structured parsing fails
11
+
12
+ Based on the Template Discovery System specification (Option C).
13
+ """
14
+
15
+ from typing import Any, Dict, List, Optional, Union
16
+
17
+
18
+ class StructuredFrontmatterRegistry:
19
+ """Registry of structured frontmatter patterns for different layout types"""
20
+
21
+ def __init__(self, template_mapping: Optional[Dict] = None):
22
+ """Initialize with template mapping from JSON file"""
23
+ self.template_mapping = template_mapping or {}
24
+
25
+ def get_structure_patterns(self):
26
+ """Get structure patterns that define how to parse structured frontmatter"""
27
+ return {
28
+ "Four Columns With Titles": {
29
+ "structure_type": "columns",
30
+ "description": "Four-column comparison layout with individual titles and content",
31
+ "yaml_pattern": {
32
+ "layout": "Four Columns With Titles",
33
+ "title": str,
34
+ "columns": [{"title": str, "content": str}],
35
+ },
36
+ "validation": {
37
+ "min_columns": 1,
38
+ "max_columns": 4,
39
+ "required_fields": ["title", "columns"],
40
+ },
41
+ "example": """---
42
+ layout: Four Columns
43
+ title: Feature Comparison
44
+ columns:
45
+ - title: Performance
46
+ content: Fast processing with optimized algorithms
47
+ - title: Security
48
+ content: Enterprise-grade encryption and compliance
49
+ - title: Usability
50
+ content: Intuitive interface with minimal learning curve
51
+ - title: Cost
52
+ content: Competitive pricing with flexible plans
53
+ ---""",
54
+ },
55
+ "Three Columns With Titles": {
56
+ "structure_type": "columns",
57
+ "description": "Three-column layout with individual titles and content",
58
+ "yaml_pattern": {
59
+ "layout": "Three Columns With Titles",
60
+ "title": str,
61
+ "columns": [{"title": str, "content": str}],
62
+ },
63
+ "validation": {
64
+ "min_columns": 1,
65
+ "max_columns": 3,
66
+ "required_fields": ["title", "columns"],
67
+ },
68
+ "example": """---
69
+ layout: Three Columns With Titles
70
+ title: Key Features
71
+ columns:
72
+ - title: Performance
73
+ content: Fast processing with optimized algorithms
74
+ - title: Security
75
+ content: Enterprise-grade encryption and compliance
76
+ - title: Usability
77
+ content: Intuitive interface with minimal learning curve
78
+ ---""",
79
+ },
80
+ "Three Columns": {
81
+ "structure_type": "columns",
82
+ "description": "Three-column layout with content only (no titles)",
83
+ "yaml_pattern": {
84
+ "layout": "Three Columns",
85
+ "title": str,
86
+ "columns": [{"content": str}],
87
+ },
88
+ "validation": {
89
+ "min_columns": 1,
90
+ "max_columns": 3,
91
+ "required_fields": ["title", "columns"],
92
+ },
93
+ "example": """---
94
+ layout: Three Columns
95
+ title: Benefits Overview
96
+ columns:
97
+ - content: Fast processing with optimized algorithms and sub-millisecond response times
98
+ - content: Enterprise-grade encryption with SOC2 and GDPR compliance
99
+ - content: Intuitive interface with minimal learning curve and comprehensive docs
100
+ ---""",
101
+ },
102
+ "Four Columns": {
103
+ "structure_type": "columns",
104
+ "description": "Four-column layout with content only (no titles)",
105
+ "yaml_pattern": {
106
+ "layout": "Four Columns",
107
+ "title": str,
108
+ "columns": [{"content": str}],
109
+ },
110
+ "validation": {
111
+ "min_columns": 1,
112
+ "max_columns": 4,
113
+ "required_fields": ["title", "columns"],
114
+ },
115
+ "example": """---
116
+ layout: Four Columns
117
+ title: Complete Feature Set
118
+ columns:
119
+ - content: Fast processing with optimized algorithms and sub-millisecond response times
120
+ - content: Enterprise-grade encryption with SOC2 and GDPR compliance
121
+ - content: Intuitive interface with minimal learning curve and comprehensive docs
122
+ - content: Transparent pricing with flexible plans and proven ROI
123
+ ---""",
124
+ },
125
+ "Comparison": {
126
+ "structure_type": "comparison",
127
+ "description": "Side-by-side comparison layout for contrasting two options",
128
+ "yaml_pattern": {
129
+ "layout": "Comparison",
130
+ "title": str,
131
+ "comparison": {
132
+ "left": {"title": str, "content": str},
133
+ "right": {"title": str, "content": str},
134
+ },
135
+ },
136
+ "mapping_rules": {"title": "semantic:title"},
137
+ "validation": {
138
+ "required_fields": ["title", "comparison"],
139
+ "required_comparison_fields": ["left", "right"],
140
+ },
141
+ "example": """---
142
+ layout: Comparison
143
+ title: Solution Analysis
144
+ comparison:
145
+ left:
146
+ title: Traditional Approach
147
+ content: Proven reliability with established workflows
148
+ right:
149
+ title: Modern Solution
150
+ content: Advanced features with improved efficiency
151
+ ---""",
152
+ },
153
+ "Two Content": {
154
+ "structure_type": "sections",
155
+ "description": "Side-by-side layout with two content areas",
156
+ "yaml_pattern": {
157
+ "layout": "Two Content",
158
+ "title": str,
159
+ "sections": [{"title": str, "content": [str]}],
160
+ },
161
+ "mapping_rules": {"title": "semantic:title"},
162
+ "validation": {
163
+ "required_fields": ["title", "sections"],
164
+ "min_sections": 2,
165
+ "max_sections": 2,
166
+ },
167
+ "example": """---
168
+ layout: Two Content
169
+ title: Before and After
170
+ sections:
171
+ - title: Current State
172
+ content:
173
+ - Manual processes
174
+ - Time-consuming workflows
175
+ - title: Future State
176
+ content:
177
+ - Automated systems
178
+ - Streamlined operations
179
+ ---""",
180
+ },
181
+ "Picture with Caption": {
182
+ "structure_type": "media",
183
+ "description": "Media slide with image placeholder and caption text",
184
+ "yaml_pattern": {
185
+ "layout": "Picture with Caption",
186
+ "title": str,
187
+ "media": {
188
+ "image_path": str, # NEW - Primary image source (optional)
189
+ "alt_text": str, # NEW - Accessibility support (optional)
190
+ "caption": str,
191
+ "description": str,
192
+ },
193
+ },
194
+ "mapping_rules": {"title": "semantic:title"},
195
+ "validation": {"required_fields": ["title", "media"]},
196
+ "example": """---
197
+ layout: Picture with Caption
198
+ title: System Architecture
199
+ media:
200
+ image_path: "assets/architecture_diagram.png" # Primary image source
201
+ alt_text: "System architecture overview" # Accessibility support
202
+ caption: High-level system architecture diagram
203
+ description: |
204
+ Main components include:
205
+ • Frontend: React-based interface
206
+ • API: RESTful services
207
+ • Database: PostgreSQL with Redis
208
+ ---""",
209
+ },
210
+ "Agenda, 6 Textboxes": {
211
+ "structure_type": "agenda",
212
+ "description": "Six-item agenda layout with numbered items",
213
+ "yaml_pattern": {
214
+ "layout": "Agenda, 6 Textboxes",
215
+ "title": str,
216
+ "agenda": [{"number": str, "item": str}],
217
+ },
218
+ "validation": {"required_fields": ["title", "agenda"], "max_items": 6},
219
+ "example": """---
220
+ layout: Agenda, 6 Textboxes
221
+ title: Meeting Agenda
222
+ agenda:
223
+ - number: "01"
224
+ item: Opening remarks
225
+ - number: "02"
226
+ item: Market analysis
227
+ ---""",
228
+ },
229
+ "Title and 6-item Lists": {
230
+ "structure_type": "lists",
231
+ "description": "Six-item list layout with numbers, titles, and content",
232
+ "yaml_pattern": {
233
+ "layout": "Title and 6-item Lists",
234
+ "title": str,
235
+ "lists": [{"number": str, "title": str, "content": str}],
236
+ },
237
+ "validation": {"required_fields": ["title", "lists"], "max_items": 6},
238
+ "example": """---
239
+ layout: Title and 6-item Lists
240
+ title: Feature Overview
241
+ lists:
242
+ - number: "01"
243
+ title: Authentication
244
+ content: Secure login system
245
+ ---""",
246
+ },
247
+ "SWOT Analysis": {
248
+ "structure_type": "analysis",
249
+ "description": "SWOT analysis layout with four quadrants",
250
+ "yaml_pattern": {
251
+ "layout": "SWOT Analysis",
252
+ "title": str,
253
+ "swot": {
254
+ "strengths": str,
255
+ "weaknesses": str,
256
+ "opportunities": str,
257
+ "threats": str,
258
+ },
259
+ },
260
+ "validation": {"required_fields": ["title", "swot"]},
261
+ "example": """---
262
+ layout: SWOT Analysis
263
+ title: Strategic Analysis
264
+ swot:
265
+ strengths: Strong market position
266
+ weaknesses: Limited resources
267
+ opportunities: New markets
268
+ threats: Competition
269
+ ---""",
270
+ },
271
+ }
272
+
273
+ def get_structure_definition(self, layout_name: str) -> Dict[str, Any]:
274
+ """Get structure definition for a layout with dynamically built mapping rules"""
275
+ patterns = self.get_structure_patterns()
276
+ pattern = patterns.get(layout_name, {})
277
+
278
+ if not pattern:
279
+ return {}
280
+
281
+ # Build mapping rules dynamically from template mapping
282
+ mapping_rules = self._build_mapping_rules(layout_name)
283
+
284
+ return {**pattern, "mapping_rules": mapping_rules}
285
+
286
+ def _build_mapping_rules(self, layout_name: str) -> Dict[str, str]:
287
+ """Build mapping rules dynamically from template JSON"""
288
+
289
+ if not self.template_mapping:
290
+ return {}
291
+
292
+ layouts = self.template_mapping.get("layouts", {})
293
+ layout_info = layouts.get(layout_name, {})
294
+ placeholders = layout_info.get("placeholders", {})
295
+
296
+ mapping_rules = {"title": "semantic:title"} # Always use semantic for title
297
+
298
+ if layout_name == "Four Columns With Titles":
299
+ # Find column placeholders using convention-based patterns
300
+ col_title_placeholders = []
301
+ col_content_placeholders = []
302
+
303
+ for _idx, placeholder_name in placeholders.items():
304
+ name_lower = placeholder_name.lower()
305
+ if "title_col" in name_lower:
306
+ col_title_placeholders.append((_idx, placeholder_name))
307
+ elif "content_col" in name_lower:
308
+ col_content_placeholders.append((_idx, placeholder_name))
309
+
310
+ # Sort by placeholder index to get correct order
311
+ col_title_placeholders.sort(key=lambda x: int(x[0]))
312
+ col_content_placeholders.sort(key=lambda x: int(x[0]))
313
+
314
+ # Build mapping rules for each column
315
+ for i, (_idx, placeholder_name) in enumerate(col_title_placeholders[:4]):
316
+ mapping_rules[f"columns[{i}].title"] = placeholder_name
317
+
318
+ for i, (_idx, placeholder_name) in enumerate(col_content_placeholders[:4]):
319
+ mapping_rules[f"columns[{i}].content"] = placeholder_name
320
+
321
+ elif layout_name == "Comparison":
322
+ # Find comparison placeholders using convention-based patterns
323
+ left_placeholders = {}
324
+ right_placeholders = {}
325
+
326
+ for _idx, placeholder_name in placeholders.items():
327
+ name_lower = placeholder_name.lower()
328
+ if "_left_" in name_lower:
329
+ if "title" in name_lower:
330
+ left_placeholders["title"] = placeholder_name
331
+ elif "content" in name_lower:
332
+ left_placeholders["content"] = placeholder_name
333
+ elif "_right_" in name_lower:
334
+ if "title" in name_lower:
335
+ right_placeholders["title"] = placeholder_name
336
+ elif "content" in name_lower:
337
+ right_placeholders["content"] = placeholder_name
338
+
339
+ # Map to comparison structure
340
+ if "title" in left_placeholders:
341
+ mapping_rules["comparison.left.title"] = left_placeholders["title"]
342
+ if "content" in left_placeholders:
343
+ mapping_rules["comparison.left.content"] = left_placeholders["content"]
344
+ if "title" in right_placeholders:
345
+ mapping_rules["comparison.right.title"] = right_placeholders["title"]
346
+ if "content" in right_placeholders:
347
+ mapping_rules["comparison.right.content"] = right_placeholders["content"]
348
+
349
+ elif layout_name == "Two Content":
350
+ # Find content placeholders using convention-based patterns
351
+ content_placeholders = []
352
+
353
+ for _idx, placeholder_name in placeholders.items():
354
+ name_lower = placeholder_name.lower()
355
+ if "content_" in name_lower and ("_left_" in name_lower or "_right_" in name_lower):
356
+ if "_left_" in name_lower:
357
+ content_placeholders.append((0, placeholder_name))
358
+ elif "_right_" in name_lower:
359
+ content_placeholders.append((1, placeholder_name))
360
+
361
+ content_placeholders.sort()
362
+
363
+ # Map to sections
364
+ for order, placeholder_name in content_placeholders:
365
+ mapping_rules[f"sections[{order}].content"] = placeholder_name
366
+
367
+ elif layout_name == "Three Columns With Titles":
368
+ # Find column placeholders using convention-based patterns
369
+ col_title_placeholders = []
370
+ col_content_placeholders = []
371
+
372
+ for _idx, placeholder_name in placeholders.items():
373
+ name_lower = placeholder_name.lower()
374
+ if "title_col" in name_lower:
375
+ col_title_placeholders.append((_idx, placeholder_name))
376
+ elif "content_col" in name_lower:
377
+ col_content_placeholders.append((_idx, placeholder_name))
378
+
379
+ # Sort by placeholder index to get correct order
380
+ col_title_placeholders.sort(key=lambda x: int(x[0]))
381
+ col_content_placeholders.sort(key=lambda x: int(x[0]))
382
+
383
+ # Build mapping rules for each column (max 3)
384
+ for i, (_idx, placeholder_name) in enumerate(col_title_placeholders[:3]):
385
+ mapping_rules[f"columns[{i}].title"] = placeholder_name
386
+
387
+ for i, (_idx, placeholder_name) in enumerate(col_content_placeholders[:3]):
388
+ mapping_rules[f"columns[{i}].content"] = placeholder_name
389
+
390
+ elif layout_name == "Three Columns":
391
+ # Find column content placeholders using convention-based patterns
392
+ col_content_placeholders = []
393
+
394
+ for _idx, placeholder_name in placeholders.items():
395
+ name_lower = placeholder_name.lower()
396
+ if "content_col" in name_lower:
397
+ col_content_placeholders.append((_idx, placeholder_name))
398
+
399
+ # Sort by placeholder index to get correct order
400
+ col_content_placeholders.sort(key=lambda x: int(x[0]))
401
+
402
+ # Build mapping rules for each column content (max 3)
403
+ for i, (_idx, placeholder_name) in enumerate(col_content_placeholders[:3]):
404
+ mapping_rules[f"columns[{i}].content"] = placeholder_name
405
+
406
+ elif layout_name == "Four Columns":
407
+ # Find column content placeholders using convention-based patterns
408
+ col_content_placeholders = []
409
+
410
+ for _idx, placeholder_name in placeholders.items():
411
+ name_lower = placeholder_name.lower()
412
+ if "content_col" in name_lower:
413
+ col_content_placeholders.append((_idx, placeholder_name))
414
+
415
+ # Sort by placeholder index to get correct order
416
+ col_content_placeholders.sort(key=lambda x: int(x[0]))
417
+
418
+ # Build mapping rules for each column content (max 4)
419
+ for i, (_idx, placeholder_name) in enumerate(col_content_placeholders[:4]):
420
+ mapping_rules[f"columns[{i}].content"] = placeholder_name
421
+
422
+ elif layout_name == "Picture with Caption":
423
+ # Find caption and image placeholders using convention-based patterns
424
+ for _idx, placeholder_name in placeholders.items():
425
+ name_lower = placeholder_name.lower()
426
+ if "text_caption" in name_lower:
427
+ mapping_rules["media.caption"] = placeholder_name
428
+ elif "image" in name_lower:
429
+ mapping_rules["media.image_path"] = placeholder_name
430
+
431
+ # For Picture with Caption, description goes to the text area below image
432
+ # Don't map to semantic:content to avoid conflicts with image placeholder
433
+
434
+ elif layout_name == "Agenda, 6 Textboxes":
435
+ # Find agenda item placeholders using convention-based patterns
436
+ number_placeholders = []
437
+ content_placeholders = []
438
+
439
+ for _idx, placeholder_name in placeholders.items():
440
+ name_lower = placeholder_name.lower()
441
+ if "number_item" in name_lower:
442
+ number_placeholders.append((_idx, placeholder_name))
443
+ elif "content_item" in name_lower:
444
+ content_placeholders.append((_idx, placeholder_name))
445
+
446
+ number_placeholders.sort(key=lambda x: int(x[0]))
447
+ content_placeholders.sort(key=lambda x: int(x[0]))
448
+
449
+ # Map agenda items
450
+ for i, (_idx, placeholder_name) in enumerate(number_placeholders[:6]):
451
+ mapping_rules[f"agenda[{i}].number"] = placeholder_name
452
+ for i, (_idx, placeholder_name) in enumerate(content_placeholders[:6]):
453
+ mapping_rules[f"agenda[{i}].item"] = placeholder_name
454
+
455
+ elif layout_name == "Title and 6-item Lists":
456
+ # Find list item placeholders using convention-based patterns
457
+ number_placeholders = []
458
+ content_placeholders = []
459
+ title_placeholders = []
460
+
461
+ for _idx, placeholder_name in placeholders.items():
462
+ name_lower = placeholder_name.lower()
463
+ if "number_item" in name_lower:
464
+ number_placeholders.append((_idx, placeholder_name))
465
+ elif "content_item" in name_lower:
466
+ content_placeholders.append((_idx, placeholder_name))
467
+ elif "content_" in name_lower and "_1" in name_lower:
468
+ # These are the title placeholders for lists
469
+ title_placeholders.append((_idx, placeholder_name))
470
+
471
+ number_placeholders.sort(key=lambda x: int(x[0]))
472
+ content_placeholders.sort(key=lambda x: int(x[0]))
473
+ title_placeholders.sort(key=lambda x: int(x[0]))
474
+
475
+ # Map list items
476
+ for i, (_idx, placeholder_name) in enumerate(number_placeholders[:6]):
477
+ mapping_rules[f"lists[{i}].number"] = placeholder_name
478
+ for i, (_idx, placeholder_name) in enumerate(content_placeholders[:6]):
479
+ mapping_rules[f"lists[{i}].content"] = placeholder_name
480
+ for i, (_idx, placeholder_name) in enumerate(title_placeholders[:6]):
481
+ mapping_rules[f"lists[{i}].title"] = placeholder_name
482
+
483
+ elif layout_name == "SWOT Analysis":
484
+ # Find SWOT content placeholders using convention-based patterns
485
+ swot_placeholders = []
486
+
487
+ for _idx, placeholder_name in placeholders.items():
488
+ name_lower = placeholder_name.lower()
489
+ if "content_" in name_lower and "_1" not in name_lower:
490
+ # These are content placeholders like content_16, content_17, etc.
491
+ swot_placeholders.append((_idx, placeholder_name))
492
+
493
+ swot_placeholders.sort(key=lambda x: int(x[0]))
494
+
495
+ # Map SWOT quadrants (assuming they are in order:
496
+ # strengths, weaknesses, opportunities, threats)
497
+ swot_keys = ["strengths", "weaknesses", "opportunities", "threats"]
498
+ for i, key in enumerate(swot_keys):
499
+ if i < len(swot_placeholders):
500
+ mapping_rules[f"swot.{key}"] = swot_placeholders[i][1]
501
+
502
+ return mapping_rules
503
+
504
+ def supports_structured_frontmatter(self, layout_name: str) -> bool:
505
+ """Check if layout supports structured frontmatter"""
506
+ patterns = self.get_structure_patterns()
507
+ return layout_name in patterns
508
+
509
+ def get_supported_layouts(self) -> List[str]:
510
+ """Get list of layouts that support structured frontmatter"""
511
+ patterns = self.get_structure_patterns()
512
+ return list(patterns.keys())
513
+
514
+ def get_example(self, layout_name: str) -> Optional[str]:
515
+ """Get example structured frontmatter for a layout"""
516
+ definition = self.get_structure_definition(layout_name)
517
+ return definition.get("example")
518
+
519
+
520
+ class StructuredFrontmatterConverter:
521
+ """Convert structured frontmatter to placeholder mappings (one-way only)"""
522
+
523
+ def __init__(self, layout_mapping: Optional[Dict] = None):
524
+ self.layout_mapping = layout_mapping or {}
525
+ self.registry = StructuredFrontmatterRegistry(layout_mapping)
526
+
527
+ def convert_structured_to_placeholders(self, structured_data: Dict[str, Any]) -> Dict[str, Any]:
528
+ """Convert structured frontmatter to placeholder field names"""
529
+
530
+ layout_name = structured_data.get("layout")
531
+ if not layout_name:
532
+ return structured_data
533
+
534
+ structure_def = self.registry.get_structure_definition(layout_name)
535
+ if not structure_def:
536
+ # No structured definition available, return original data for backward compatibility
537
+ return structured_data
538
+
539
+ # Create result with type field for supported layouts
540
+ result = {"type": layout_name}
541
+
542
+ # Copy title if present
543
+ if "title" in structured_data:
544
+ result["title"] = structured_data["title"]
545
+
546
+ mapping_rules = structure_def.get("mapping_rules", {})
547
+
548
+ # Process each mapping rule
549
+ for structured_path, placeholder_target in mapping_rules.items():
550
+ value = self._extract_value_by_path(structured_data, structured_path)
551
+ if value is not None:
552
+ if placeholder_target.startswith("semantic:"):
553
+ # Use semantic field name directly
554
+ semantic_field = placeholder_target.split(":", 1)[1]
555
+ result[semantic_field] = value
556
+ else:
557
+ # Use exact placeholder name
558
+ result[placeholder_target] = value
559
+
560
+ return result
561
+
562
+ def _extract_value_by_path(self, data: Dict[str, Any], path: str) -> Any:
563
+ """Extract value from nested dict using dot notation path with array support"""
564
+
565
+ # Handle array indexing like "columns[0].title"
566
+ if "[" in path and "]" in path:
567
+ return self._extract_array_value(data, path)
568
+
569
+ # Handle simple dot notation like "comparison.left.title"
570
+ keys = path.split(".")
571
+ current = data
572
+
573
+ for key in keys:
574
+ if isinstance(current, dict) and key in current:
575
+ current = current[key]
576
+ else:
577
+ return None
578
+
579
+ return current
580
+
581
+ def _extract_array_value(self, data: Dict[str, Any], path: str) -> Any:
582
+ """Extract value from array using path like 'columns[0].title'"""
583
+
584
+ # Parse "columns[0].title" into parts
585
+ parts = self._parse_path_with_arrays(path)
586
+
587
+ # Navigate through the data structure
588
+ current = data
589
+ for part in parts:
590
+ if isinstance(part, int):
591
+ if isinstance(current, list) and len(current) > part:
592
+ current = current[part]
593
+ else:
594
+ return None
595
+ elif isinstance(current, dict) and part in current:
596
+ current = current[part]
597
+ else:
598
+ return None
599
+
600
+ return current
601
+
602
+ def _set_value_by_path(self, data: Dict[str, Any], path: str, value: Any) -> None:
603
+ """Set value in nested dict using dot notation path with array support"""
604
+
605
+ if "[" in path and "]" in path:
606
+ self._set_array_value(data, path, value)
607
+ return
608
+
609
+ keys = path.split(".")
610
+ current = data
611
+
612
+ # Navigate to the parent of the target key
613
+ for key in keys[:-1]:
614
+ if key not in current:
615
+ current[key] = {}
616
+ current = current[key]
617
+
618
+ # Set the final value
619
+ current[keys[-1]] = value
620
+
621
+ def _set_array_value(self, data: Dict[str, Any], path: str, value: Any) -> None:
622
+ """Set value in array structure, creating arrays as needed"""
623
+
624
+ parts = self._parse_path_with_arrays(path)
625
+ current = data
626
+
627
+ # Navigate through all but the last part, creating structure as needed
628
+ for i, part in enumerate(parts[:-1]):
629
+ if isinstance(part, int):
630
+ # Current should be a list, ensure it exists and has enough elements
631
+ if not isinstance(current, list):
632
+ current = []
633
+ while len(current) <= part:
634
+ current.append({})
635
+ current = current[part]
636
+ else:
637
+ # Current should be a dict
638
+ if part not in current:
639
+ # Look ahead to see if next part is an array index
640
+ next_part = parts[i + 1] if i + 1 < len(parts) else None
641
+ if isinstance(next_part, int):
642
+ current[part] = []
643
+ else:
644
+ current[part] = {}
645
+ current = current[part]
646
+
647
+ # Set the final value
648
+ final_part = parts[-1]
649
+ if isinstance(final_part, int):
650
+ if not isinstance(current, list):
651
+ current = []
652
+ while len(current) <= final_part:
653
+ current.append(None)
654
+ current[final_part] = value
655
+ else:
656
+ current[final_part] = value
657
+
658
+ def _parse_path_with_arrays(self, path: str) -> List[Union[str, int]]:
659
+ """Parse path like 'columns[0].title' into ['columns', 0, 'title']"""
660
+
661
+ parts = []
662
+ current_part = ""
663
+ i = 0
664
+
665
+ while i < len(path):
666
+ if path[i] == "[":
667
+ # Add the current part if it exists
668
+ if current_part:
669
+ parts.append(current_part)
670
+ current_part = ""
671
+
672
+ # Find the closing bracket and extract the index
673
+ j = i + 1
674
+ while j < len(path) and path[j] != "]":
675
+ j += 1
676
+
677
+ if j < len(path):
678
+ index_str = path[i + 1 : j]
679
+ try:
680
+ index = int(index_str)
681
+ parts.append(index)
682
+ except ValueError:
683
+ # If it's not a number, treat as string key
684
+ parts.append(index_str)
685
+
686
+ i = j + 1
687
+ # Skip the dot after the bracket if it exists
688
+ if i < len(path) and path[i] == ".":
689
+ i += 1
690
+ else:
691
+ # Malformed path, just add the bracket as text
692
+ current_part += path[i]
693
+ i += 1
694
+ elif path[i] == ".":
695
+ if current_part:
696
+ parts.append(current_part)
697
+ current_part = ""
698
+ i += 1
699
+ else:
700
+ current_part += path[i]
701
+ i += 1
702
+
703
+ # Add any remaining part
704
+ if current_part:
705
+ parts.append(current_part)
706
+
707
+ return parts
708
+
709
+
710
+ class StructuredFrontmatterValidator:
711
+ """Validate structured frontmatter against layout requirements"""
712
+
713
+ def __init__(self):
714
+ self.registry = StructuredFrontmatterRegistry()
715
+
716
+ def validate_structured_frontmatter(
717
+ self, data: Dict[str, Any], layout_name: str
718
+ ) -> Dict[str, Any]:
719
+ """Validate structured frontmatter against layout requirements"""
720
+
721
+ structure_def = self.registry.get_structure_definition(layout_name)
722
+ if not structure_def:
723
+ return {
724
+ "valid": True,
725
+ "warnings": ["No validation rules available for this layout"],
726
+ "errors": [],
727
+ }
728
+
729
+ validation_rules = structure_def.get("validation", {})
730
+ result = {"valid": True, "warnings": [], "errors": []}
731
+
732
+ # Check required fields
733
+ required_fields = validation_rules.get("required_fields", [])
734
+ for field in required_fields:
735
+ if field not in data:
736
+ result["valid"] = False
737
+ result["errors"].append(f"Missing required field: '{field}'")
738
+
739
+ # Layout-specific validation
740
+ if layout_name in ["Four Columns", "Four Columns With Titles"] and "columns" in data:
741
+ self._validate_four_columns(data, validation_rules, result)
742
+ elif layout_name == "Comparison" and "comparison" in data:
743
+ self._validate_comparison(data, validation_rules, result)
744
+ elif layout_name == "Two Content" and "sections" in data:
745
+ self._validate_two_content(data, validation_rules, result)
746
+
747
+ return result
748
+
749
+ def _validate_four_columns(
750
+ self, data: Dict[str, Any], rules: Dict[str, Any], result: Dict[str, Any]
751
+ ) -> None:
752
+ """Validate Four Columns specific structure"""
753
+ columns = data.get("columns", [])
754
+
755
+ min_cols = rules.get("min_columns", 1)
756
+ max_cols = rules.get("max_columns", 4)
757
+
758
+ if len(columns) < min_cols:
759
+ result["valid"] = False
760
+ result["errors"].append(f"Expected at least {min_cols} columns, got {len(columns)}")
761
+ elif len(columns) > max_cols:
762
+ result["warnings"].append(
763
+ f"Expected at most {max_cols} columns, got {len(columns)} "
764
+ f"(extra columns will be ignored)"
765
+ )
766
+
767
+ # Validate each column structure
768
+ for i, column in enumerate(columns):
769
+ if not isinstance(column, dict):
770
+ result["errors"].append(
771
+ f"Column {i + 1} must be an object with 'title' and 'content'"
772
+ )
773
+ continue
774
+
775
+ if "title" not in column:
776
+ result["warnings"].append(f"Column {i + 1} missing 'title' field")
777
+ if "content" not in column:
778
+ result["warnings"].append(f"Column {i + 1} missing 'content' field")
779
+
780
+ def _validate_comparison(
781
+ self, data: Dict[str, Any], rules: Dict[str, Any], result: Dict[str, Any]
782
+ ) -> None:
783
+ """Validate Comparison specific structure"""
784
+ comparison = data.get("comparison", {})
785
+
786
+ required_sides = rules.get("required_comparison_fields", ["left", "right"])
787
+ for side in required_sides:
788
+ if side not in comparison:
789
+ result["valid"] = False
790
+ result["errors"].append(f"Missing required comparison side: '{side}'")
791
+ else:
792
+ side_data = comparison[side]
793
+ if not isinstance(side_data, dict):
794
+ result["errors"].append(f"Comparison '{side}' must be an object")
795
+ else:
796
+ if "title" not in side_data:
797
+ result["warnings"].append(f"Comparison '{side}' missing 'title' field")
798
+ if "content" not in side_data:
799
+ result["warnings"].append(f"Comparison '{side}' missing 'content' field")
800
+
801
+ def _validate_two_content(
802
+ self, data: Dict[str, Any], rules: Dict[str, Any], result: Dict[str, Any]
803
+ ) -> None:
804
+ """Validate Two Content specific structure"""
805
+ sections = data.get("sections", [])
806
+
807
+ min_sections = rules.get("min_sections", 2)
808
+ max_sections = rules.get("max_sections", 2)
809
+
810
+ if len(sections) < min_sections:
811
+ result["valid"] = False
812
+ result["errors"].append(
813
+ f"Expected at least {min_sections} sections, got {len(sections)}"
814
+ )
815
+ elif len(sections) > max_sections:
816
+ result["warnings"].append(
817
+ f"Expected at most {max_sections} sections, got {len(sections)} "
818
+ f"(extra sections will be ignored)"
819
+ )
820
+
821
+
822
+ def get_structured_frontmatter_help(
823
+ layout_name: str = None, template_mapping: Dict = None
824
+ ) -> Dict[str, Any]:
825
+ """Get help information for structured frontmatter"""
826
+
827
+ registry = StructuredFrontmatterRegistry(template_mapping)
828
+
829
+ if layout_name:
830
+ # Specific layout help
831
+ definition = registry.get_structure_definition(layout_name)
832
+ if not definition:
833
+ return {
834
+ "error": f"Layout '{layout_name}' does not support structured frontmatter",
835
+ "supported_layouts": registry.get_supported_layouts(),
836
+ }
837
+
838
+ return {
839
+ "layout": layout_name,
840
+ "description": definition["description"],
841
+ "structure_type": definition["structure_type"],
842
+ "example": definition["example"],
843
+ "validation_rules": definition.get("validation", {}),
844
+ "mapping_rules": definition["mapping_rules"],
845
+ }
846
+ else:
847
+ # General help
848
+ patterns = registry.get_structure_patterns()
849
+ return {
850
+ "supported_layouts": registry.get_supported_layouts(),
851
+ "layout_info": {
852
+ name: {
853
+ "description": definition["description"],
854
+ "structure_type": definition["structure_type"],
855
+ }
856
+ for name, definition in patterns.items()
857
+ },
858
+ "usage": (
859
+ "Use 'layout: <LayoutName>' in frontmatter, then follow the structured "
860
+ "format for that layout"
861
+ ),
862
+ }