Add script directory to sys.path so imports work when scripts are called from remote_publish.sh. Also remove unused variable and no-op regex.
237 lines
6.7 KiB
Python
237 lines
6.7 KiB
Python
#!/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()
|