Files
2025-11-29 18:28:02 +08:00

477 lines
17 KiB
Python
Executable File

#!/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()