| 1 | #!/usr/bin/env python3 |
| 2 | """ |
| 3 | PPT Master - Examples Index Generator |
| 4 | |
| 5 | Automatically scans the examples directory and generates a README.md index file. |
| 6 | |
| 7 | Usage: |
| 8 | python3 scripts/generate_examples_index.py |
| 9 | python3 scripts/generate_examples_index.py examples |
| 10 | """ |
| 11 | |
| 12 | import argparse |
| 13 | import os |
| 14 | import sys |
| 15 | from collections import defaultdict |
| 16 | from datetime import datetime |
| 17 | from pathlib import Path |
| 18 | |
| 19 | from console_encoding import configure_utf8_stdio |
| 20 | |
| 21 | try: |
| 22 | from project_utils import find_all_projects, get_project_info, CANVAS_FORMATS |
| 23 | except ImportError: |
| 24 | print("Error: Cannot import the project_utils module") |
| 25 | print("Please ensure project_utils.py is in the same directory") |
| 26 | sys.exit(1) |
| 27 | |
| 28 | |
| 29 | def generate_examples_index(examples_dir: str = 'examples') -> str: |
| 30 | """ |
| 31 | Generate a README.md index for the examples directory |
| 32 | |
| 33 | Args: |
| 34 | examples_dir: Path to the examples directory |
| 35 | |
| 36 | Returns: |
| 37 | Generated README.md content |
| 38 | """ |
| 39 | examples_path = Path(examples_dir) |
| 40 | skill_dir = Path(__file__).resolve().parent.parent |
| 41 | |
| 42 | if not examples_path.exists(): |
| 43 | print(f"[ERROR] Directory not found: {examples_dir}") |
| 44 | return "" |
| 45 | |
| 46 | def skill_link(target: Path) -> str: |
| 47 | """Return a link from the generated index to a packaged Skill resource.""" |
| 48 | return Path( |
| 49 | os.path.relpath(target, start=examples_path.resolve()) |
| 50 | ).as_posix() |
| 51 | |
| 52 | print(f"[SCAN] Scanning directory: {examples_dir}") |
| 53 | |
| 54 | # Find all projects |
| 55 | projects = find_all_projects(examples_dir) |
| 56 | |
| 57 | if not projects: |
| 58 | print("[WARN] No projects found") |
| 59 | return "" |
| 60 | |
| 61 | print(f"Found {len(projects)} project(s)") |
| 62 | |
| 63 | # Collect project information |
| 64 | projects_info = [] |
| 65 | for project_path in projects: |
| 66 | info = get_project_info(str(project_path)) |
| 67 | projects_info.append(info) |
| 68 | |
| 69 | # Sort by date (newest first) |
| 70 | projects_info.sort(key=lambda x: x['date'], reverse=True) |
| 71 | |
| 72 | # Group by format |
| 73 | by_format = defaultdict(list) |
| 74 | for info in projects_info: |
| 75 | by_format[info['format']].append(info) |
| 76 | |
| 77 | # Generate README content |
| 78 | content = [] |
| 79 | content.append("# PPT Master Example Projects Index\n") |
| 80 | content.append("> This file is auto-generated by the packaged `scripts/generate_examples_index.py`\n") |
| 81 | content.append(f"> Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") |
| 82 | |
| 83 | # Overview statistics |
| 84 | content.append("## [Stats] Overview\n") |
| 85 | content.append(f"- **Total projects**: {len(projects_info)}") |
| 86 | content.append(f"- **Canvas formats**: {len(by_format)} type(s)") |
| 87 | |
| 88 | total_svgs = sum(info['svg_count'] for info in projects_info) |
| 89 | content.append(f"- **SVG files**: {total_svgs}") |
| 90 | |
| 91 | # Statistics by format |
| 92 | content.append("\n### Format Distribution\n") |
| 93 | for fmt_key in sorted(by_format.keys(), key=lambda x: len(by_format[x]), reverse=True): |
| 94 | count = len(by_format[fmt_key]) |
| 95 | fmt_name = CANVAS_FORMATS.get(fmt_key, {}).get('name', fmt_key) |
| 96 | content.append(f"- **{fmt_name}**: {count} project(s)") |
| 97 | |
| 98 | # Recently updated |
| 99 | content.append("\n## [New] Recently Updated\n") |
| 100 | for info in projects_info[:5]: |
| 101 | content.append( |
| 102 | f"- **{info['name']}** ({info['format_name']}) - {info['date_formatted']}") |
| 103 | |
| 104 | # Project list by format |
| 105 | content.append("\n## [List] Project List\n") |
| 106 | |
| 107 | # Define format display order |
| 108 | format_order = ['ppt169', 'ppt43', 'wechat', |
| 109 | 'xiaohongshu', 'moments', 'story', 'banner', 'a4'] |
| 110 | |
| 111 | for fmt_key in format_order: |
| 112 | if fmt_key not in by_format: |
| 113 | continue |
| 114 | |
| 115 | fmt_info = CANVAS_FORMATS.get(fmt_key, {}) |
| 116 | fmt_name = fmt_info.get('name', fmt_key) |
| 117 | dimensions = fmt_info.get('dimensions', '') |
| 118 | |
| 119 | content.append(f"\n### {fmt_name} ({dimensions})\n") |
| 120 | |
| 121 | projects_list = by_format[fmt_key] |
| 122 | # Sort by date |
| 123 | projects_list.sort(key=lambda x: x['date'], reverse=True) |
| 124 | |
| 125 | for info in projects_list: |
| 126 | # Project name and link |
| 127 | project_link = f"./{info['dir_name']}" |
| 128 | |
| 129 | # Build project entry |
| 130 | line = f"- **[{info['name']}]({project_link})**" |
| 131 | |
| 132 | # Add date |
| 133 | line += f" - {info['date_formatted']}" |
| 134 | |
| 135 | # Add SVG count |
| 136 | line += f" - {info['svg_count']} page(s)" |
| 137 | |
| 138 | content.append(line) |
| 139 | |
| 140 | # Other uncategorized formats |
| 141 | other_formats = set(by_format.keys()) - set(format_order) |
| 142 | if other_formats: |
| 143 | content.append("\n### Other Formats\n") |
| 144 | for fmt_key in sorted(other_formats): |
| 145 | projects_list = by_format[fmt_key] |
| 146 | for info in projects_list: |
| 147 | project_link = f"./{info['dir_name']}" |
| 148 | line = f"- **[{info['name']}]({project_link})**" |
| 149 | line += f" ({info['format_name']}) - {info['date_formatted']}" |
| 150 | line += f" - {info['svg_count']} page(s)" |
| 151 | content.append(line) |
| 152 | |
| 153 | # Usage instructions |
| 154 | content.append("\n## [Docs] Usage Instructions\n") |
| 155 | content.append("### Preview Projects\n") |
| 156 | content.append("Each project contains the following files:\n") |
| 157 | content.append("- `README.md` - Project documentation") |
| 158 | content.append("- `Design Spec & Content Outline.md` - Full design specification") |
| 159 | content.append("- `svg_output/` - SVG output files\n") |
| 160 | |
| 161 | content.append("**Method 1: Using an HTTP server (recommended)**\n") |
| 162 | content.append("```bash") |
| 163 | content.append( |
| 164 | "python3 -m http.server --directory examples/<project_name>/svg_output 8000") |
| 165 | content.append("# Visit http://localhost:8000") |
| 166 | content.append("```\n") |
| 167 | |
| 168 | content.append("**Method 2: Open SVG directly**\n") |
| 169 | content.append("```bash") |
| 170 | content.append( |
| 171 | "open examples/<project_name>/svg_output/slide_01_cover.svg") |
| 172 | content.append("```\n") |
| 173 | |
| 174 | # Create new project |
| 175 | content.append("### Create a New Project\n") |
| 176 | content.append("Refer to existing project structures, or use the project management tool:\n") |
| 177 | content.append("```bash") |
| 178 | content.append( |
| 179 | "python3 scripts/project_manager.py init my_project --format ppt169") |
| 180 | content.append("```\n") |
| 181 | |
| 182 | # Contribution guidelines |
| 183 | content.append("## [Contribute] Contributing Example Projects\n") |
| 184 | content.append("We welcome you to share your projects in the examples directory!\n") |
| 185 | content.append("### Project Requirements\n") |
| 186 | content.append("1. Follow the standard project structure") |
| 187 | content.append("2. Include a complete README.md and design specification") |
| 188 | content.append("3. SVG files must comply with technical specifications") |
| 189 | content.append("4. Directory naming format: `{project_name}_{format}_{YYYYMMDD}`\n") |
| 190 | |
| 191 | content.append("### Submission Process\n") |
| 192 | content.append("1. Create a project under the `examples/` directory") |
| 193 | content.append( |
| 194 | "2. Validate the project: `python3 scripts/project_manager.py validate examples/<project>`") |
| 195 | content.append("3. Update the index: `python3 scripts/generate_examples_index.py`") |
| 196 | content.append("4. Submit a Pull Request\n") |
| 197 | |
| 198 | # Related resources |
| 199 | content.append("## [Resources] Related Resources\n") |
| 200 | content.append(f"- [Workflow]({skill_link(skill_dir / 'SKILL.md')})") |
| 201 | content.append( |
| 202 | f"- [Canvas Formats]({skill_link(skill_dir / 'references' / 'canvas-formats.md')})") |
| 203 | content.append( |
| 204 | f"- [Role Definitions]({skill_link(skill_dir / 'references')})") |
| 205 | content.append( |
| 206 | f"- [Visualization Templates]({skill_link(skill_dir / 'templates' / 'README.md')})\n") |
| 207 | |
| 208 | # Footer |
| 209 | content.append("---\n") |
| 210 | content.append( |
| 211 | f"*Auto-generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} by PPT Master*") |
| 212 | |
| 213 | return "\n".join(content) |
| 214 | |
| 215 | |
| 216 | def build_parser() -> argparse.ArgumentParser: |
| 217 | """Build the examples-index CLI parser.""" |
| 218 | parser = argparse.ArgumentParser( |
| 219 | description="Generate the PPT Master examples README index.", |
| 220 | ) |
| 221 | parser.add_argument( |
| 222 | "examples_dir", |
| 223 | nargs="?", |
| 224 | default="examples", |
| 225 | help="Examples directory (default: examples)", |
| 226 | ) |
| 227 | return parser |
| 228 | |
| 229 | |
| 230 | def main(argv: list[str] | None = None) -> int: |
| 231 | """Run the CLI entry point.""" |
| 232 | parser = build_parser() |
| 233 | raw_argv = list(sys.argv[1:] if argv is None else argv) |
| 234 | if raw_argv == ["help"]: |
| 235 | parser.print_help() |
| 236 | return 0 |
| 237 | args = parser.parse_args(raw_argv) |
| 238 | configure_utf8_stdio() |
| 239 | examples_dir = args.examples_dir |
| 240 | |
| 241 | print("=" * 80) |
| 242 | print("PPT Master - Examples Index Generator") |
| 243 | print("=" * 80 + "\n") |
| 244 | |
| 245 | # Generate index content |
| 246 | content = generate_examples_index(examples_dir) |
| 247 | |
| 248 | if not content: |
| 249 | print("\n[ERROR] Generation failed") |
| 250 | return 1 |
| 251 | |
| 252 | # Write to file |
| 253 | output_file = Path(examples_dir) / 'README.md' |
| 254 | |
| 255 | try: |
| 256 | with open(output_file, 'w', encoding='utf-8') as f: |
| 257 | f.write(content) |
| 258 | |
| 259 | print(f"\n[OK] Index file generated: {output_file}") |
| 260 | print(f" Contains {len(content.splitlines())} lines") |
| 261 | |
| 262 | # Display statistics |
| 263 | projects_count = content.count('- **[') |
| 264 | print(f" Indexed {projects_count} project(s)") |
| 265 | |
| 266 | except Exception as e: |
| 267 | print(f"\n[ERROR] Failed to write file: {e}") |
| 268 | return 1 |
| 269 | |
| 270 | return 0 |
| 271 | |
| 272 | |
| 273 | if __name__ == '__main__': |
| 274 | raise SystemExit(main()) |
| 275 |