🎯 Core Implementation: - StubGenerator class with intelligent heading hierarchy generation - CLI command 'generate-stub' with comprehensive options (--output, --style, --title) - Multiple placeholder styles: default, custom, detailed - Full file I/O support and error handling 📊 Features Delivered: - Template generation from JSON schemas with proper heading structure - Intelligent section naming based on document hierarchy - Round-trip validation: generated stubs validate against source schemas - Integration with existing schema generation and validation workflow 🧪 Quality Assurance: - 23 comprehensive tests covering all functionality - Complete TDD8 methodology: RED-GREEN-REFACTOR cycle - CLI integration tests and error handling validation - 417/417 total tests passing - no regressions 🔄 Bidirectional Workflow Complete: Schema Generation (✅ Issue #5) → Schema Validation (✅ Issue #7) → Stub Generation (✅ Issue #6) This completes the critical template-driven document creation workflow essential for arc42 architecture documentation system goals. Usage Examples: markitect generate-stub blog_schema.json --output template.md markitect generate-stub schema.json --style detailed --title "My Document" 🎖️ Strategic Achievement: Template generation foundation complete and production-ready 🧪 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
253 lines
9.9 KiB
Python
253 lines
9.9 KiB
Python
"""
|
|
Stub Generator for Issue #6: Generate a Markdown Stub from a Schema.
|
|
|
|
This module provides functionality to create markdown template files from JSON schemas
|
|
with appropriate placeholder content and structural elements.
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Dict, Any, Optional, List, Callable
|
|
|
|
|
|
# Constants for better maintainability
|
|
DEFAULT_TITLE = "Document Title"
|
|
HEADING_PREFIX_LEVEL_1 = "#"
|
|
LEVEL_KEY_PREFIX = "level_"
|
|
|
|
|
|
class StubGenerator:
|
|
"""
|
|
Generates markdown stub/template files from JSON schemas.
|
|
|
|
Creates markdown documents with proper heading hierarchy and placeholder
|
|
content based on the structural definitions in JSON schemas.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize the stub generator."""
|
|
self.placeholder_styles: Dict[str, Callable[[str], str]] = {
|
|
'default': self._generate_default_placeholder,
|
|
'custom': self._generate_custom_placeholder,
|
|
'detailed': self._generate_detailed_placeholder
|
|
}
|
|
|
|
def generate_stub_from_schema(self, schema: Dict[str, Any],
|
|
placeholder_style: str = 'default',
|
|
title: Optional[str] = None) -> str:
|
|
"""
|
|
Generate a markdown stub from a JSON schema dictionary.
|
|
|
|
Args:
|
|
schema: JSON schema as dictionary
|
|
placeholder_style: Style of placeholder content ('default', 'custom', 'detailed')
|
|
title: Custom title for the document (overrides schema title)
|
|
|
|
Returns:
|
|
Generated markdown content as string
|
|
"""
|
|
# Extract title
|
|
doc_title = title or schema.get('title', DEFAULT_TITLE)
|
|
|
|
# Start building the markdown content
|
|
lines = []
|
|
|
|
# Extract heading structure from schema
|
|
headings_schema = schema.get('properties', {}).get('headings', {})
|
|
heading_properties = headings_schema.get('properties', {})
|
|
|
|
if not heading_properties:
|
|
# Create a minimal document if no heading structure is defined
|
|
lines.append(f"# {doc_title}")
|
|
lines.append("")
|
|
lines.append(self._get_placeholder_content(placeholder_style, "main"))
|
|
lines.append("")
|
|
else:
|
|
# Generate content based on heading structure
|
|
lines.extend(self._generate_content_from_headings(
|
|
heading_properties, doc_title, placeholder_style
|
|
))
|
|
|
|
return '\n'.join(lines)
|
|
|
|
def generate_stub_from_file(self, schema_file: Path) -> str:
|
|
"""
|
|
Generate a markdown stub from a JSON schema file.
|
|
|
|
Args:
|
|
schema_file: Path to JSON schema file
|
|
|
|
Returns:
|
|
Generated markdown content as string
|
|
|
|
Raises:
|
|
FileNotFoundError: If schema file doesn't exist
|
|
json.JSONDecodeError: If schema file contains invalid JSON
|
|
"""
|
|
if not schema_file.exists():
|
|
raise FileNotFoundError(f"Schema file not found: {schema_file}")
|
|
|
|
with open(schema_file, 'r', encoding='utf-8') as f:
|
|
schema = json.load(f)
|
|
|
|
return self.generate_stub_from_schema(schema)
|
|
|
|
def generate_stub_to_file(self, schema: Dict[str, Any],
|
|
output_file: Path,
|
|
placeholder_style: str = 'default',
|
|
title: Optional[str] = None) -> None:
|
|
"""
|
|
Generate a markdown stub and save it to a file.
|
|
|
|
Args:
|
|
schema: JSON schema as dictionary
|
|
output_file: Path where to save the generated markdown
|
|
placeholder_style: Style of placeholder content
|
|
title: Custom title for the document
|
|
"""
|
|
content = self.generate_stub_from_schema(schema, placeholder_style, title)
|
|
|
|
# Ensure parent directory exists
|
|
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with open(output_file, 'w', encoding='utf-8') as f:
|
|
f.write(content)
|
|
|
|
def _generate_content_from_headings(self, heading_properties: Dict[str, Any],
|
|
doc_title: str, placeholder_style: str) -> List[str]:
|
|
"""Generate markdown content from heading structure."""
|
|
lines = []
|
|
|
|
# Sort heading levels to ensure proper hierarchy
|
|
levels = sorted([key for key in heading_properties.keys() if key.startswith(LEVEL_KEY_PREFIX)])
|
|
|
|
# Calculate heading counts for each level
|
|
heading_counts = self._calculate_heading_counts(levels, heading_properties)
|
|
|
|
# Generate the content with proper hierarchy
|
|
if 1 in heading_counts:
|
|
# Start with H1
|
|
lines.append(f"# {doc_title}")
|
|
lines.append("")
|
|
lines.append(self._get_placeholder_content(placeholder_style, "introduction"))
|
|
lines.append("")
|
|
|
|
# Generate H2+ headings
|
|
for level in sorted(heading_counts.keys()):
|
|
if level == 1:
|
|
continue # Already handled
|
|
|
|
count = heading_counts[level]
|
|
for i in range(count):
|
|
heading_prefix = '#' * level
|
|
section_name = self._generate_section_name(level, i + 1)
|
|
|
|
lines.append(f"{heading_prefix} {section_name}")
|
|
lines.append("")
|
|
lines.append(self._get_placeholder_content(placeholder_style, f"section_level_{level}"))
|
|
lines.append("")
|
|
else:
|
|
# No H1, start with whatever level is available
|
|
for level in sorted(heading_counts.keys()):
|
|
count = heading_counts[level]
|
|
for i in range(count):
|
|
heading_prefix = '#' * level
|
|
if level == min(heading_counts.keys()) and i == 0:
|
|
section_name = doc_title
|
|
else:
|
|
section_name = self._generate_section_name(level, i + 1)
|
|
|
|
lines.append(f"{heading_prefix} {section_name}")
|
|
lines.append("")
|
|
lines.append(self._get_placeholder_content(placeholder_style, f"section_level_{level}"))
|
|
lines.append("")
|
|
|
|
return lines
|
|
|
|
def _calculate_heading_counts(self, levels: List[str], heading_properties: Dict[str, Any]) -> Dict[int, int]:
|
|
"""Calculate the required count for each heading level."""
|
|
heading_counts = {}
|
|
for level_key in levels:
|
|
level_num = int(level_key.split('_')[1])
|
|
level_props = heading_properties[level_key]
|
|
|
|
# Get the required count from minItems/maxItems
|
|
min_items = level_props.get('minItems', 1)
|
|
max_items = level_props.get('maxItems', min_items)
|
|
count = min_items # Use minimum required count
|
|
|
|
heading_counts[level_num] = count
|
|
return heading_counts
|
|
|
|
def _generate_section_name(self, level: int, index: int) -> str:
|
|
"""Generate appropriate section names based on level and index."""
|
|
section_names = {
|
|
2: ['Introduction', 'Main Content', 'Conclusion', 'Summary', 'Overview'],
|
|
3: ['Background', 'Analysis', 'Implementation', 'Results', 'Discussion'],
|
|
4: ['Details', 'Examples', 'Notes', 'Additional Info'],
|
|
5: ['Subsection A', 'Subsection B', 'Subsection C'],
|
|
6: ['Item', 'Point', 'Note']
|
|
}
|
|
|
|
if level in section_names and index <= len(section_names[level]):
|
|
return section_names[level][index - 1]
|
|
else:
|
|
return f"Section {index}"
|
|
|
|
def _get_placeholder_content(self, style: str, section_type: str) -> str:
|
|
"""Get placeholder content based on style and section type."""
|
|
generator = self.placeholder_styles.get(style, self.placeholder_styles['default'])
|
|
return generator(section_type)
|
|
|
|
def _generate_default_placeholder(self, section_type: str) -> str:
|
|
"""Generate default placeholder content."""
|
|
return f"TODO: Add content for {section_type} section."
|
|
|
|
def _generate_custom_placeholder(self, section_type: str) -> str:
|
|
"""Generate custom style placeholder content."""
|
|
placeholders = {
|
|
"introduction": "Write an engaging introduction that outlines the main topic. Add your content here.",
|
|
"main": "Add your main content here.",
|
|
"section_level_2": "Describe the key points for this section.",
|
|
"section_level_3": "Provide detailed information and examples.",
|
|
"section_level_4": "Include specific details and supporting information.",
|
|
}
|
|
return placeholders.get(section_type, f"Content for {section_type} goes here.")
|
|
|
|
def _generate_detailed_placeholder(self, section_type: str) -> str:
|
|
"""Generate detailed placeholder content with guidance."""
|
|
detailed_placeholders = {
|
|
"introduction": """<!-- Introduction Section -->
|
|
Write an engaging introduction that:
|
|
- Introduces the main topic
|
|
- Provides context and background
|
|
- Outlines what the reader will learn
|
|
|
|
TODO: Replace this placeholder with your introduction content.""",
|
|
"main": """<!-- Main Content Section -->
|
|
This is the primary content area. Consider including:
|
|
- Key information and concepts
|
|
- Supporting details and examples
|
|
- Clear explanations and analysis
|
|
|
|
TODO: Add your main content here.""",
|
|
"section_level_2": """<!-- Section Content -->
|
|
This section should cover:
|
|
- Main points related to the section topic
|
|
- Supporting information and details
|
|
- Examples or case studies if relevant
|
|
|
|
TODO: Add content for this section.""",
|
|
"section_level_3": """<!-- Subsection Content -->
|
|
Provide detailed information including:
|
|
- Specific details and explanations
|
|
- Examples and illustrations
|
|
- References to related concepts
|
|
|
|
TODO: Add detailed content for this subsection.""",
|
|
}
|
|
|
|
return detailed_placeholders.get(
|
|
section_type,
|
|
f"<!-- {section_type.title()} Section -->\nTODO: Add content for {section_type}."
|
|
) |