Add gophermap generation scripts (Phase 2)
Create scripts for generating category gophermaps with ASCII art headers and a blog index gophermap. Also includes root gophermap template with "FROM THE WEB" section for blog integration.
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user