#!/usr/bin/env python3 """ Generate gophermap files for blog categories and index. Usage: python scripts/gopher/generate_gophermaps.py python scripts/gopher/generate_gophermaps.py --output gopher_build/blog/ """ import argparse import re import sys import yaml from pathlib import Path # Add script directory to path for imports sys.path.insert(0, str(Path(__file__).parent)) from ascii_art import ( HEADER_BLOG_INDEX, DIR_TO_HEADER, DIR_TO_DESCRIPTION, ) # Paths SCRIPT_DIR = Path(__file__).parent PROJECT_ROOT = SCRIPT_DIR.parent.parent CONTENT_DIR = PROJECT_ROOT / "content" / "posts" DEFAULT_OUTPUT = PROJECT_ROOT / "gopher_build" / "blog" # Gophermap formatting TAB = "\t" FAKE_ENTRY = f"{TAB}fake{TAB}(NULL){TAB}0" def info_line(text: str = "") -> str: """Generate an info (i) line for gophermap.""" return f"i{text}{FAKE_ENTRY}" def file_link(label: str, path: str) -> str: """Generate a text file (0) link.""" return f"0{label}{TAB}{path}" def dir_link(label: str, path: str) -> str: """Generate a directory (1) link.""" return f"1{label}{TAB}{path}" def html_link(label: str, url: str) -> str: """Generate an HTML (h) link.""" return f"h{label}{TAB}URL:{url}" def parse_frontmatter(filepath: Path) -> dict: """Parse YAML frontmatter from a markdown file.""" content = filepath.read_text() if not content.startswith("---"): return {} end_match = re.search(r"\n---\n", content[3:]) if not end_match: return {} yaml_end = end_match.start() + 3 yaml_content = content[3:yaml_end] try: return yaml.safe_load(yaml_content) or {} except yaml.YAMLError: return {} def get_posts_by_category(content_dir: Path) -> dict: """Get all phlog-enabled posts grouped by category.""" from ascii_art import SERIES_TO_DIR categories = {} for post_path in content_dir.glob("*.md"): meta = parse_frontmatter(post_path) # Skip if not phlog-enabled or is draft if not meta.get("phlog", False) or meta.get("draft", False): continue series = meta.get("series", "Fun Center") category = SERIES_TO_DIR.get(series, "fun-center") if category not in categories: categories[category] = [] # Extract date date_obj = meta.get("date") if hasattr(date_obj, "strftime"): date_str = date_obj.strftime("%Y-%m-%d") else: date_str = str(date_obj)[:10] if date_obj else "1970-01-01" categories[category].append({ "slug": post_path.stem, "title": meta.get("title", post_path.stem), "date": date_str, "summary": meta.get("summary", ""), }) # Sort each category by date (newest first) for category in categories: categories[category].sort(key=lambda x: x["date"], reverse=True) return categories def generate_category_gophermap(category: str, posts: list, output_dir: Path) -> Path: """Generate a gophermap for a category.""" header = DIR_TO_HEADER.get(category, "") category_dir = output_dir / category category_dir.mkdir(parents=True, exist_ok=True) lines = [] # Add header art for line in header.strip().split("\n"): lines.append(info_line(line)) lines.append(info_line()) # Divider lines.append(info_line("-" * 52)) lines.append(info_line()) # Posts for post in posts: date = post["date"] title = post["title"] slug = post["slug"] summary = post.get("summary", "") lines.append(file_link(f"[{date}] {title}", f"{slug}.txt")) if summary: lines.append(info_line(f" {summary[:48]}")) lines.append(info_line()) # Footer divider and navigation lines.append(info_line("-" * 52)) lines.append(dir_link("Back to Blog Index", "../")) lines.append(dir_link("Back to Main Menu", "/users/mnw/")) gophermap_path = category_dir / "gophermap" gophermap_path.write_text("\n".join(lines)) return gophermap_path def generate_blog_index(categories: dict, output_dir: Path) -> Path: """Generate the main blog index gophermap.""" lines = [] # Add header art for line in HEADER_BLOG_INDEX.strip().split("\n"): lines.append(info_line(line)) lines.append(info_line(" The Double Lunch Dispatch")) lines.append(info_line()) # Divider lines.append(info_line("-" * 48)) lines.append(info_line()) # Category listings category_info = { "franks-couch": ("Frank's Couch - Movie Reviews", "posts about films watched from the couch"), "fun-center": ("Fun Center - Tech Posts", "posts about technology and open source"), "beercalls": ("Beercalls - Thursday Night Adventures", "Yearly logs of Austin beer adventures"), } for category, (label, description) in category_info.items(): count = len(categories.get(category, [])) if count > 0: lines.append(dir_link(label, f"{category}/")) lines.append(info_line(f" {count} {description}")) lines.append(info_line()) # Footer lines.append(info_line("-" * 48)) lines.append(html_link("View on the web", "https://mnw.sdf.org/posts/")) lines.append(dir_link("Back to Main Menu", "/users/mnw/")) output_dir.mkdir(parents=True, exist_ok=True) gophermap_path = output_dir / "gophermap" gophermap_path.write_text("\n".join(lines)) return gophermap_path def generate_all(output_dir: Path = None) -> dict: """Generate all gophermaps.""" output_dir = output_dir or DEFAULT_OUTPUT # Get posts by category categories = get_posts_by_category(CONTENT_DIR) results = { "categories": {}, "index": None, } # Generate category gophermaps for category, posts in categories.items(): path = generate_category_gophermap(category, posts, output_dir) results["categories"][category] = { "path": path, "count": len(posts), } print(f"Generated: {path.relative_to(output_dir)} ({len(posts)} posts)") # Generate blog index results["index"] = generate_blog_index(categories, output_dir) print(f"Generated: {results['index'].relative_to(output_dir)}") return results def main(): parser = argparse.ArgumentParser(description="Generate gophermaps for blog") parser.add_argument("--output", "-o", type=Path, help="Output directory") args = parser.parse_args() output = args.output or DEFAULT_OUTPUT results = generate_all(output) total_posts = sum(c["count"] for c in results["categories"].values()) print(f"\nGenerated gophermaps for {total_posts} posts in {len(results['categories'])} categories") if __name__ == "__main__": main()