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.
- deckbuilder/__init__.py +22 -0
- deckbuilder/cli.py +544 -0
- deckbuilder/cli_tools.py +739 -0
- deckbuilder/engine.py +1546 -0
- deckbuilder/image_handler.py +291 -0
- deckbuilder/layout_intelligence.json +288 -0
- deckbuilder/layout_intelligence.py +398 -0
- deckbuilder/naming_conventions.py +541 -0
- deckbuilder/placeholder_types.py +101 -0
- deckbuilder/placekitten_integration.py +280 -0
- deckbuilder/structured_frontmatter.py +862 -0
- deckbuilder/table_styles.py +37 -0
- deckbuilder-1.0.0b1.dist-info/METADATA +378 -0
- deckbuilder-1.0.0b1.dist-info/RECORD +37 -0
- deckbuilder-1.0.0b1.dist-info/WHEEL +5 -0
- deckbuilder-1.0.0b1.dist-info/entry_points.txt +3 -0
- deckbuilder-1.0.0b1.dist-info/licenses/LICENSE +201 -0
- deckbuilder-1.0.0b1.dist-info/top_level.txt +4 -0
- mcp_server/__init__.py +9 -0
- mcp_server/content_analysis.py +436 -0
- mcp_server/content_optimization.py +822 -0
- mcp_server/layout_recommendations.py +595 -0
- mcp_server/main.py +550 -0
- mcp_server/tools.py +492 -0
- placekitten/README.md +561 -0
- placekitten/__init__.py +44 -0
- placekitten/core.py +184 -0
- placekitten/filters.py +183 -0
- placekitten/images/ACuteKitten-1.png +0 -0
- placekitten/images/ACuteKitten-2.png +0 -0
- placekitten/images/ACuteKitten-3.png +0 -0
- placekitten/images/TwoKitttens Playing-1.png +0 -0
- placekitten/images/TwoKitttens Playing-2.png +0 -0
- placekitten/images/TwoKitttensSleeping-1.png +0 -0
- placekitten/processor.py +262 -0
- placekitten/smart_crop.py +314 -0
- shared/__init__.py +9 -0
@@ -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
|
+
}
|