Files
markitect-main/markitect/stub_generator.py
tegwick d8c2d198e3 feat: Complete Issue #6 - Generate Markdown Stub from Schema
🎯 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>
2025-09-30 03:31:48 +02:00

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}."
)