import shutil from pathlib import Path import markdown from jinja2 import Environment, FileSystemLoader CONTENT_DIR = Path("src") BUILD_DIR = Path("_site") TEMPLATES = Path("templates") ASSETS_DIR = Path("assets") EXCLUDE_EXTENSIONS = {".gif", ".png", ".jpg", ".jpeg", ".webp", ".map"} env = Environment(loader=FileSystemLoader(TEMPLATES)) def should_exclude_file(path: Path) -> bool: if path.suffix.lower() in EXCLUDE_EXTENSIONS: return True name = path.name if name in {".git", "__pycache__", ".pytest_cache", "node_modules"}: return True if ".egg-info" in name: return True return False def get_output_path(src_path: Path, base_dir: Path, build_dir: Path) -> Path: rel_path = src_path.relative_to(base_dir) if src_path.suffix == ".md": return build_dir / rel_path.with_suffix(".html") return build_dir / rel_path def get_href_for_file(path: Path, base_path: Path = Path(".")) -> str: if path.suffix == ".md": return str((base_path / path.name).with_suffix(".html").as_posix()) return str((base_path / path.name).as_posix()) def convert_markdown_file(md_path: Path, output_path: Path) -> None: text = md_path.read_text(encoding="utf-8") html = markdown.markdown(text, extensions=["fenced_code", "tables", "pymdownx.tasklist"]) template = env.get_template("base.html") fake_path = "~/" + str(md_path.relative_to(CONTENT_DIR)).replace("\\", "/") output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_text( template.render(content=html, title=md_path.stem, fake_path=fake_path), encoding="utf-8" ) def copy_file_with_dirs(src: Path, dest: Path) -> None: dest.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(src, dest) def build_file_tree_item(path: Path, rel_base: Path = Path(".")) -> dict: if should_exclude_file(path): return None if path.is_dir(): children = build_directory_tree(path, rel_base / path.name) return ( { "name": path.name + "/", "path": path, "is_dir": True, "children": children, "href": None, # No links for directories } if children else None ) else: if should_exclude_file(path): return None return { "name": path.name, "path": path, "is_dir": False, "href": get_href_for_file(path, rel_base), } def build_directory_tree(directory: Path, rel_base: Path = Path(".")) -> list: items = [] entries = sorted(directory.iterdir(), key=lambda e: e.name.lower()) for entry in entries: item = build_file_tree_item(entry, rel_base) if item: items.append(item) return items def process_tree_files(tree: list, base_dir: Path, rel_path: Path = Path(".")) -> None: for item in tree: if item["is_dir"]: process_tree_files(item["children"], base_dir, rel_path / item["name"].rstrip("/")) else: src_path = item["path"] output_path = get_output_path(src_path, base_dir, BUILD_DIR) if src_path.suffix == ".md": convert_markdown_file(src_path, output_path) else: copy_file_with_dirs(src_path, output_path) def build_main_tree() -> list: tree = [] # Always put README first if it exists readme = CONTENT_DIR / "README.md" if readme.exists(): tree.append({"name": "README.md", "path": readme, "is_dir": False, "href": "README.html"}) # Collect all items (content + assets) for sorting all_items = [] # Add assets directory - scan from BUILD_DIR since that's where index.html is build_assets = BUILD_DIR / "assets" if build_assets.exists(): assets_tree = build_directory_tree(build_assets, Path("assets")) if assets_tree: all_items.append( { "name": "assets/", "path": build_assets, "is_dir": True, "children": assets_tree, "href": None, } ) # Add content directory items for item_path in CONTENT_DIR.iterdir(): if item_path.name == "README.md": # Already handled continue tree_item = build_file_tree_item(item_path) if tree_item: all_items.append(tree_item) # Sort all items alphabetically (directories first, then files) all_items.sort(key=lambda x: (not x["is_dir"], x["name"].lower())) tree.extend(all_items) return tree def render_index_page(tree: list, output_path: Path = BUILD_DIR / "index.html") -> None: template = env.get_template("filetree.html") output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_text(template.render(tree=tree, relpath=Path(".")), encoding="utf-8") def render_gallery_page(dir_path: Path, rel_path: Path, output_path: Path) -> None: template = env.get_template("gallery.html") images = [] if dir_path.exists(): for img in sorted(dir_path.iterdir()): if img.suffix.lower() in {".webp", ".png", ".jpg", ".jpeg", ".gif"}: images.append({"name": img.name, "path": img.name}) fake_path = "~/" + str(rel_path).replace("\\", "/") output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_text( template.render( images=images, fake_path=fake_path, ), encoding="utf-8", ) def clean_and_setup_build_dir() -> None: if BUILD_DIR.exists(): shutil.rmtree(BUILD_DIR) BUILD_DIR.mkdir() def build_site() -> None: clean_and_setup_build_dir() if ASSETS_DIR.exists(): shutil.copytree(ASSETS_DIR, BUILD_DIR / "assets", dirs_exist_ok=True) galleries = [ ("media/every-basil-hawkins/bw", "index.html"), ("media/every-basil-hawkins/color", "index.html"), ] for rel_dir, output_filename in galleries: source_path = ASSETS_DIR / rel_dir if source_path.exists(): gallery_output = BUILD_DIR / "assets" / rel_dir / output_filename render_gallery_page(source_path, Path(rel_dir), gallery_output) tree = build_main_tree() content_items = [item for item in tree if item["name"] != "assets/"] if content_items: process_tree_files(content_items, CONTENT_DIR) render_index_page(tree) if __name__ == "__main__": build_site()