#!/usr/bin/env python3 """ Documentation Generator Generates documentation files from marketplace data using Jinja2 templates. """ import json import os import sys import re from pathlib import Path from datetime import datetime from typing import Dict, List, Any, Optional import argparse # Try to use real Jinja2 if available, otherwise use SimpleTemplate fallback try: from jinja2 import Template as Jinja2Template USE_JINJA2 = True except ImportError: USE_JINJA2 = False class SimpleTemplate: """Minimal Jinja2-like template engine""" def __init__(self, template_str: str): self.template = template_str def apply_filter(self, value: Any, filter_name: str) -> Any: """Apply a filter to a value""" if filter_name == 'title': return str(value).replace('-', ' ').replace('_', ' ').title() elif filter_name == 'length': return len(value) if hasattr(value, '__len__') else 0 elif filter_name.startswith('join'): # Extract separator from filter (e.g., "join(', ')") match = re.search(r"join\(['\"]([^'\"]*)['\"]\)", filter_name) if match and isinstance(value, list): separator = match.group(1) return separator.join(str(v) for v in value) return str(value) return value def resolve_value(self, expr: str, context: Dict[str, Any]) -> Any: """Resolve a variable expression with optional filters""" # Split expression and filters parts = expr.strip().split('|') var_expr = parts[0].strip() filters = [f.strip() for f in parts[1:]] # Resolve the base variable value = context for key in var_expr.split('.'): key = key.strip() if isinstance(value, dict): value = value.get(key, '') elif hasattr(value, key): value = getattr(value, key) else: value = '' break # Apply filters for filter_name in filters: value = self.apply_filter(value, filter_name) return value def render(self, context: Dict[str, Any]) -> str: """Render template with context""" result = self.template # Handle nested loops with .items(): {% for key, value in dict.items() %}...{% endfor %} items_pattern = r'{%\s*for\s+(\w+)\s*,\s*(\w+)\s+in\s+([\w.]+)\.items\(\)\s*%}(.*?){%\s*endfor\s*%}' def replace_items_loop(match): key_var = match.group(1) value_var = match.group(2) dict_name = match.group(3) loop_body = match.group(4) dict_obj = self.resolve_value(dict_name, context) if not isinstance(dict_obj, dict): return "" output = [] for key, value in dict_obj.items(): loop_context = context.copy() loop_context[key_var] = key loop_context[value_var] = value # Recursively render the loop body template = SimpleTemplate(loop_body) body_result = template.render(loop_context) output.append(body_result) return "".join(output) result = re.sub(items_pattern, replace_items_loop, result, flags=re.DOTALL) # Handle loops with .keys(): {% for key in dict.keys() %}...{% endfor %} keys_pattern = r'{%\s*for\s+(\w+)\s+in\s+([\w.]+)\.keys\(\)\s*%}(.*?){%\s*endfor\s*%}' def replace_keys_loop(match): var_name = match.group(1) dict_name = match.group(2) loop_body = match.group(3) dict_obj = self.resolve_value(dict_name, context) if not isinstance(dict_obj, dict): return "" output = [] for key in dict_obj.keys(): loop_context = context.copy() loop_context[var_name] = key # Recursively render the loop body template = SimpleTemplate(loop_body) body_result = template.render(loop_context) output.append(body_result) return "".join(output) result = re.sub(keys_pattern, replace_keys_loop, result, flags=re.DOTALL) # Handle regular loops: {% for item in items %}...{% endfor %} for_pattern = r'{%\s*for\s+(\w+)\s+in\s+([\w.]+)\s*%}(.*?){%\s*endfor\s*%}' def replace_for(match): var_name = match.group(1) list_name = match.group(2) loop_body = match.group(3) items = self.resolve_value(list_name, context) if not isinstance(items, (list, dict)): return "" # Handle both lists and dict values if isinstance(items, dict): items = list(items.values()) output = [] for item in items: loop_context = context.copy() loop_context[var_name] = item # If item is a dict, also add its keys directly to context for easier access if isinstance(item, dict): for key, value in item.items(): loop_context[f"{var_name}.{key}"] = value # Recursively render the loop body template = SimpleTemplate(loop_body) body_result = template.render(loop_context) output.append(body_result) return "".join(output) result = re.sub(for_pattern, replace_for, result, flags=re.DOTALL) # Handle conditionals with comparison: {% if var1 == var2 %}...{% endif %} if_compare_pattern = r'{%\s*if\s+([\w.]+)\s*==\s*([\w.]+)\s*%}(.*?){%\s*endif\s*%}' def replace_if_compare(match): left_expr = match.group(1) right_expr = match.group(2) body = match.group(3) left_val = self.resolve_value(left_expr, context) right_val = self.resolve_value(right_expr, context) if left_val == right_val: template = SimpleTemplate(body) return template.render(context) return "" result = re.sub(if_compare_pattern, replace_if_compare, result, flags=re.DOTALL) # Handle conditionals with else: {% if condition %}...{% else %}...{% endif %} if_else_pattern = r'{%\s*if\s+([\w.]+)\s*%}(.*?){%\s*else\s*%}(.*?){%\s*endif\s*%}' def replace_if_else(match): condition = match.group(1) true_body = match.group(2) false_body = match.group(3) cond_val = self.resolve_value(condition, context) if cond_val: template = SimpleTemplate(true_body) return template.render(context) else: template = SimpleTemplate(false_body) return template.render(context) result = re.sub(if_else_pattern, replace_if_else, result, flags=re.DOTALL) # Handle simple conditionals: {% if condition %}...{% endif %} if_pattern = r'{%\s*if\s+([\w.]+)\s*%}(.*?){%\s*endif\s*%}' def replace_if(match): condition = match.group(1) body = match.group(2) cond_val = self.resolve_value(condition, context) if cond_val: template = SimpleTemplate(body) return template.render(context) return "" result = re.sub(if_pattern, replace_if, result, flags=re.DOTALL) # Replace variables with filters: {{ variable|filter }} var_pattern = r'{{\s*([\w.|()\'",\s]+)\s*}}' def replace_var(match): expr = match.group(1) value = self.resolve_value(expr, context) return str(value) if value is not None else '' result = re.sub(var_pattern, replace_var, result) # Clean up any remaining template syntax result = re.sub(r'{%.*?%}', '', result) result = re.sub(r'{{.*?}}', '', result) return result class DocGenerator: """Generates documentation from marketplace data""" def __init__( self, marketplace_path: str = ".claude-plugin/marketplace.json", templates_dir: str = "plugins/claude-plugin/skills/documentation-update/assets", output_dir: str = "docs", ): self.marketplace_path = Path(marketplace_path) self.templates_dir = Path(templates_dir) self.output_dir = Path(output_dir) self.marketplace_data: Dict[str, Any] = {} def load_marketplace(self) -> None: """Load marketplace.json""" if not self.marketplace_path.exists(): raise FileNotFoundError(f"Marketplace not found: {self.marketplace_path}") with open(self.marketplace_path, 'r') as f: self.marketplace_data = json.load(f) def extract_frontmatter(self, file_path: Path) -> Dict[str, str]: """Extract YAML frontmatter from a markdown file""" if not file_path.exists(): return {} try: with open(file_path, 'r') as f: content = f.read() # Match frontmatter between --- delimiters match = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL) if not match: return {} frontmatter_text = match.group(1) frontmatter = {} # Simple YAML parsing (key: value) for line in frontmatter_text.split('\n'): if ':' in line: key, value = line.split(':', 1) frontmatter[key.strip()] = value.strip().strip('"\'') return frontmatter except Exception as e: print(f"Warning: Could not parse frontmatter in {file_path}: {e}") return {} def build_context(self) -> Dict[str, Any]: """Build template context from marketplace data""" context = { "marketplace": self.marketplace_data, "now": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "plugins_by_category": {}, "all_agents": [], "all_skills": [], "all_commands": [], "stats": { "total_plugins": 0, "total_agents": 0, "total_commands": 0, "total_skills": 0, }, } if "plugins" not in self.marketplace_data: return context plugins = self.marketplace_data["plugins"] context["stats"]["total_plugins"] = len(plugins) # Organize plugins by category for plugin in plugins: category = plugin.get("category", "general") if category not in context["plugins_by_category"]: context["plugins_by_category"][category] = [] context["plugins_by_category"][category].append(plugin) plugin_name = plugin.get("name", "") plugin_dir = Path(f"plugins/{plugin_name}") # Extract agent information if "agents" in plugin: for agent_path in plugin["agents"]: agent_file = agent_path.replace("./agents/", "") full_path = plugin_dir / agent_path.lstrip('./') frontmatter = self.extract_frontmatter(full_path) context["all_agents"].append({ "plugin": plugin_name, "name": frontmatter.get("name", agent_file.replace(".md", "")), "file": agent_file, "description": frontmatter.get("description", ""), "model": frontmatter.get("model", ""), }) context["stats"]["total_agents"] += len(plugin["agents"]) # Extract command information if "commands" in plugin: for cmd_path in plugin["commands"]: cmd_file = cmd_path.replace("./commands/", "") full_path = plugin_dir / cmd_path.lstrip('./') frontmatter = self.extract_frontmatter(full_path) context["all_commands"].append({ "plugin": plugin_name, "name": frontmatter.get("name", cmd_file.replace(".md", "")), "file": cmd_file, "description": frontmatter.get("description", ""), }) context["stats"]["total_commands"] += len(plugin["commands"]) # Extract skill information if "skills" in plugin: for skill_path in plugin["skills"]: skill_name = skill_path.replace("./skills/", "") full_path = plugin_dir / skill_path.lstrip('./') / "SKILL.md" frontmatter = self.extract_frontmatter(full_path) context["all_skills"].append({ "plugin": plugin_name, "name": frontmatter.get("name", skill_name), "path": skill_name, "description": frontmatter.get("description", ""), }) context["stats"]["total_skills"] += len(plugin["skills"]) return context def render_template(self, template_name: str, context: Dict[str, Any]) -> str: """Render a template with context""" template_path = self.templates_dir / f"{template_name}.md.j2" if not template_path.exists(): raise FileNotFoundError(f"Template not found: {template_path}") with open(template_path, 'r') as f: template_content = f.read() if USE_JINJA2: # Use real Jinja2 for full compatibility template = Jinja2Template(template_content) return template.render(**context) else: # Fallback to SimpleTemplate template = SimpleTemplate(template_content) return template.render(context) def generate_all(self, dry_run: bool = False, specific_file: Optional[str] = None) -> None: """Generate all documentation files""" self.load_marketplace() context = self.build_context() docs_to_generate = { "agents": "agents.md", "agent-skills": "agent-skills.md", "plugins": "plugins.md", "usage": "usage.md", } if specific_file: if specific_file not in docs_to_generate: raise ValueError(f"Unknown documentation file: {specific_file}") docs_to_generate = {specific_file: docs_to_generate[specific_file]} for template_name, output_file in docs_to_generate.items(): try: print(f"Generating {output_file}...") content = self.render_template(template_name, context) if dry_run: print(f"\n--- {output_file} ---") print(content[:500] + "..." if len(content) > 500 else content) print() else: output_path = self.output_dir / output_file output_path.parent.mkdir(parents=True, exist_ok=True) with open(output_path, 'w') as f: f.write(content) print(f"āœ“ Generated {output_path}") except Exception as e: print(f"āŒ Error generating {output_file}: {e}") def main(): """Main entry point""" parser = argparse.ArgumentParser(description="Generate documentation from marketplace") parser.add_argument( "--marketplace", default=".claude-plugin/marketplace.json", help="Path to marketplace.json", ) parser.add_argument( "--templates", default="plugins/claude-plugin/skills/documentation-update/assets", help="Path to templates directory", ) parser.add_argument( "--output", default="docs", help="Output directory for documentation", ) parser.add_argument( "--file", choices=["agents", "agent-skills", "plugins", "usage"], help="Generate specific file only", ) parser.add_argument( "--dry-run", action="store_true", help="Show output without writing files", ) args = parser.parse_args() try: generator = DocGenerator( marketplace_path=args.marketplace, templates_dir=args.templates, output_dir=args.output, ) generator.generate_all(dry_run=args.dry_run, specific_file=args.file) if not args.dry_run: print("\nāœ“ Documentation generation complete") except Exception as e: print(f"āŒ Error: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()