diff --git a/scripts/gopher/generate_gophermaps.py b/scripts/gopher/generate_gophermaps.py new file mode 100644 index 0000000..d27e578 --- /dev/null +++ b/scripts/gopher/generate_gophermaps.py @@ -0,0 +1,232 @@ +#!/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 yaml +from pathlib import Path + +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() diff --git a/scripts/gopher/update_root_gophermap.py b/scripts/gopher/update_root_gophermap.py new file mode 100644 index 0000000..254fb84 --- /dev/null +++ b/scripts/gopher/update_root_gophermap.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +""" +Update the root gophermap to include blog section. + +This script generates a complete root gophermap with the new "FROM THE WEB" +section while preserving the existing structure (phlogs, conference notes, etc). + +Usage: + python scripts/gopher/update_root_gophermap.py + python scripts/gopher/update_root_gophermap.py --output gopher_build/ +""" + +import argparse +from pathlib import Path + +# Paths +SCRIPT_DIR = Path(__file__).parent +PROJECT_ROOT = SCRIPT_DIR.parent.parent +DEFAULT_OUTPUT = PROJECT_ROOT / "gopher_build" + +# 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 echo_directive(command: str) -> str: + """Generate a dynamic =echo directive.""" + return f"=echo \"{command}\"" + + +# Root gophermap template with blog section integrated +ROOT_GOPHERMAP_TEMPLATE = """\ +i \tfake\t(NULL)\t0 +i ''~`` \tfake\t(NULL)\t0 +i ( o o ) \tfake\t(NULL)\t0 +i+-------------------.oooO--(_)--Oooo.---------------+\tfake\t(NULL)\t0 +i| |\tfake\t(NULL)\t0 +i| THE DOUBLE LUNCH DISPATCH |\tfake\t(NULL)\t0 +i| mnw @ sdf |\tfake\t(NULL)\t0 +i| |\tfake\t(NULL)\t0 +i+---------------------------------------------------+\tfake\t(NULL)\t0 +i \tfake\t(NULL)\t0 +imnw(at)sdf.org | SDF VOIP Ext. 1908 \tfake\t(NULL)\t0 +i \tfake\t(NULL)\t0 +i===================================================\tfake\t(NULL)\t0 +i DAILY PHLOGS \tfake\t(NULL)\t0 +i===================================================\tfake\t(NULL)\t0 +i \tfake\t(NULL)\t0 +1Daily Journal\tjournal/ +=echo "0Latest Entry\tjournal/`ls -t journal/ | grep -v gophermap | head -1`" +1Historical Phlogs (2018-2019)\tolder/ +i \tfake\t(NULL)\t0 +i===================================================\tfake\t(NULL)\t0 +i FROM THE WEB \tfake\t(NULL)\t0 +i===================================================\tfake\t(NULL)\t0 +i \tfake\t(NULL)\t0 +iPosts from mnw.sdf.org \tfake\t(NULL)\t0 +i \tfake\t(NULL)\t0 +1Blog Categories\tblog/ +=echo "0Latest Blog Post\t`find ~/gopher/blog -name '*.txt' -type f -printf '%T@ %p\\n' 2>/dev/null | sort -rn | head -1 | awk '{print $2}' | sed 's|.*/gopher/||'`" +i \tfake\t(NULL)\t0 +i===================================================\tfake\t(NULL)\t0 +i CONFERENCE NOTES \tfake\t(NULL)\t0 +i===================================================\tfake\t(NULL)\t0 +i \tfake\t(NULL)\t0 +iPhlogs from conferences (rough notes) \tfake\t(NULL)\t0 +i \tfake\t(NULL)\t0 +1HOPE 2024\thope_2024/ +1FOSSY 2024\tfossy_2024/ +1LISA 2019\tLISA19/ +1SeaGL 2019\tseagl_2019/ +1Texas Linux Fest 2019\ttxlf_2019/ +1HOPE 2018\thope_2018/ +1SeaGL 2018\tseagl_2018/ +i \tfake\t(NULL)\t0 +i===================================================\tfake\t(NULL)\t0 +i OTHER THINGS \tfake\t(NULL)\t0 +i===================================================\tfake\t(NULL)\t0 +i \tfake\t(NULL)\t0 +1Comedy\tcomedy/ +1Short Stories\tstories/ +i \tfake\t(NULL)\t0 +i .oooO \tfake\t(NULL)\t0 +i ( ) Oooo. \tfake\t(NULL)\t0 +i+-------------------\\ (----( )-----------------+ \tfake\t(NULL)\t0 +i \\_) ) / \tfake\t(NULL)\t0 +i (_/ \tfake\t(NULL)\t0 +i \tfake\t(NULL)\t0 +iMy SDF User Info \tfake\t(NULL)\t0 +=echo "`/usr/pkg/bin/uinfo mnw | tr -d '\\-=><'`" +i \tfake\t(NULL)\t0 +i __^__ __^__\tfake\t(NULL)\t0 +i( ___ )--------------------------------------------( ___ )\tfake\t(NULL)\t0 +i | / | This gopher space proudly hosted by SDF | \\ |\tfake\t(NULL)\t0 +i | / | mnw on pixelfed.social and tilde.zone | \\ |\tfake\t(NULL)\t0 +i |___| Web: mnw.sdf.org |___|\tfake\t(NULL)\t0 +i(_____)--------------------------------------------(_____)\tfake\t(NULL)\t0 +i \tfake\t(NULL)\t0 +iThanks for reading! Enjoy an SDF oneliner: \tfake\t(NULL)\t0 +=echo "`/usr/pkg/bin/oneliner | tr -d '\\-=><'`" +""" + + +def generate_root_gophermap(output_dir: Path = None) -> Path: + """Generate the updated root gophermap.""" + output_dir = output_dir or DEFAULT_OUTPUT + output_dir.mkdir(parents=True, exist_ok=True) + + gophermap_path = output_dir / "gophermap" + gophermap_path.write_text(ROOT_GOPHERMAP_TEMPLATE) + + return gophermap_path + + +def main(): + parser = argparse.ArgumentParser( + description="Generate updated root gophermap with blog section" + ) + parser.add_argument("--output", "-o", type=Path, help="Output directory") + args = parser.parse_args() + + output = args.output or DEFAULT_OUTPUT + path = generate_root_gophermap(output) + print(f"Generated: {path}") + print("\nTo deploy, run:") + print(f" scp {path} mnw@sdf.org:~/gopher/gophermap") + + +if __name__ == "__main__": + main()