""" generate_sidebar_tree.py Generates sidebar-tree.json from your org-roam directory. Groups notes by #+category: property. Notes without a category go into "uncategorised". Output shape: { "groups": [ { "label": "java", "files": [ { "title": "Java MOC", "url": "/20250402185735-java_moc.html" }, ... ] }, ... { "label": "uncategorised", "files": [ ... ] } ] } """ import json import re from pathlib import Path from collections import defaultdict def get_org_metadata(file_path: Path) -> dict: """Extract title, category (and optionally id) from an Org file.""" title_pattern = re.compile(r"^#\+title:\s*(.+)", re.IGNORECASE | re.MULTILINE) category_pattern = re.compile(r"^#\+category:\s*(.+)", re.IGNORECASE | re.MULTILINE) id_pattern = re.compile(r"^:ID:\s*(.+)", re.MULTILINE) try: content = file_path.read_text(encoding="utf-8", errors="ignore") except OSError: return {} title_match = title_pattern.search(content) category_match = category_pattern.search(content) id_match = id_pattern.search(content) return { "title": title_match.group(1).strip() if title_match else "Untitled", "category": category_match.group(1).strip() if category_match else None, "id": id_match.group(1).strip() if id_match else None, } def generate_sidebar_tree(org_dir: Path, out_file: Path) -> None: """Generate sidebar-tree.json grouped by #+category:.""" print(" Generating sidebar-tree.json...") org_dir = org_dir.expanduser().resolve() out_file = out_file.expanduser() # Collect all .org files, skip lock/temp files org_files = [ f for f in org_dir.rglob("*.org") if not re.search(r"/\.#[^/]+\.org$", str(f)) ] groups: dict[str, list[dict]] = defaultdict(list) for file_path in org_files: meta = get_org_metadata(file_path) if not meta: continue # Build the URL the same way as search-index.json try: relative = file_path.relative_to(org_dir).with_suffix("") except ValueError: continue entry = { "title": meta["title"], "url": f"/{relative}.html", } category = (meta["category"] or "").strip().lower() or "uncategorised" groups[category].append(entry) # Sort files within each group alphabetically by title for cat in groups: groups[cat].sort(key=lambda e: e["title"].lower()) # Sort groups alphabetically, but always put "uncategorised" last sorted_labels = sorted( groups.keys(), key=lambda k: (k == "uncategorised", k.lower()) ) output = { "tree": build_tree(org_dir) } out_file.parent.mkdir(parents=True, exist_ok=True) with open(out_file, "w", encoding="utf-8") as f: json.dump(output, f, ensure_ascii=False, indent=2) files_count, folder_count = count_nodes(output["tree"]) print(f" sidebar-tree.json generated ({folder_count} folders, {files_count} files)") print(f" → {out_file}") def count_nodes(nodes): total_files = 0 total_folders = 0 if not nodes: return 0, 0 # ✅ IMPORTANT BASE CASE for node in nodes: total_folders += 1 total_files += len(node.get("files", [])) child_files, child_folders = count_nodes(node.get("children", [])) total_files += child_files total_folders += child_folders return total_files, total_folders def build_tree(org_dir: Path) -> list: """Build nested folder tree based on filesystem structure.""" def insert_node(nodes, parts, entry): if not parts: return label = parts[0] # Find or create folder node = next((n for n in nodes if n["label"] == label), None) if not node: node = {"label": label, "children": [], "files": []} nodes.append(node) if len(parts) == 1: node["files"].append(entry) else: insert_node(node["children"], parts[1:], entry) tree = [] org_files = [ f for f in org_dir.rglob("*.org") if not re.search(r"/\.#[^/]+\.org$", str(f)) ] for file_path in org_files: meta = get_org_metadata(file_path) if not meta: continue try: relative = file_path.relative_to(org_dir) except ValueError: continue entry = { "title": meta["title"], "url": f"/{relative.with_suffix('')}.html", } folder_parts = relative.parts[:-1] # ignore filename insert_node(tree, list(folder_parts), entry) # Sort everything def sort_tree(nodes): nodes.sort(key=lambda n: n["label"].lower()) for node in nodes: node["files"].sort(key=lambda f: f["title"].lower()) sort_tree(node["children"]) sort_tree(tree) return tree def main(): base_dir = Path.home() / "master-folder" / "org_files" / "org_roam" sidebar_tree = base_dir / "output" / "assets" / "sidebar-tree.json" generate_sidebar_tree(base_dir, sidebar_tree) if __name__ == "__main__": main()