""" generate_sidebar_tree.py Generates sidebar-tree.json from the org-roam directory. Output shape: { "mocs": [ { "title": "Brain MOC", "url": "/20241210233721-brain_moc.html", "pinned": true }, ... ], "tree": [ { "label": "Articles", "children": [], "files": [{ "title": "...", "url": "/Articles/....html" }] }, ... ], "exclude": [".packages"] } """ import json import re from pathlib import Path EXCLUDE_DIRS = {".packages", ".git", ".cursor"} EXCLUDE_FILES = {"index.org", "all-files.org"} MOC_PINNED_TITLES = {"brain moc"} PINNED_URL_SUFFIX = "brain_moc.html" def should_skip(file_path: Path, org_dir: Path) -> bool: """Return True if this org file should be excluded from the sidebar.""" try: relative = file_path.relative_to(org_dir) except ValueError: return True if file_path.name in EXCLUDE_FILES: return True for part in relative.parts[:-1]: if part in EXCLUDE_DIRS or part.startswith("."): return True return False def get_org_metadata(file_path: Path) -> dict: """Extract title, filetags, and id from an Org file.""" title_pattern = re.compile(r"^#\+title:\s*(.+)", re.IGNORECASE | re.MULTILINE) tags_pattern = re.compile(r"^#\+filetags:\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) tags_match = tags_pattern.search(content) id_match = id_pattern.search(content) tags_raw = tags_match.group(1).strip() if tags_match else "" tags = {t.strip(":").lower() for t in re.findall(r":[^:\s]+:", tags_raw)} return { "title": title_match.group(1).strip() if title_match else "Untitled", "tags": tags, "id": id_match.group(1).strip() if id_match else None, } def is_moc(meta: dict) -> bool: return "moc" in meta.get("tags", set()) def build_mocs(org_dir: Path) -> list[dict]: """Collect MOC notes tagged with :moc:.""" mocs = [] for file_path in org_dir.rglob("*.org"): if re.search(r"/\.#[^/]+\.org$", str(file_path)): continue if should_skip(file_path, org_dir): continue meta = get_org_metadata(file_path) if not meta or not is_moc(meta): continue try: relative = file_path.relative_to(org_dir).with_suffix("") except ValueError: continue url = f"/{relative}.html" title = meta["title"] pinned = ( title.strip().lower() in MOC_PINNED_TITLES or url.endswith(PINNED_URL_SUFFIX) ) mocs.append({"title": title, "url": url, "pinned": pinned}) mocs.sort(key=lambda m: (not m["pinned"], m["title"].lower())) return mocs 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] 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 = [] root_files = [] for file_path in org_dir.rglob("*.org"): if re.search(r"/\.#[^/]+\.org$", str(file_path)): continue if should_skip(file_path, org_dir): continue 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] if not folder_parts: root_files.append(entry) else: insert_node(tree, list(folder_parts), entry) if root_files: root_files.sort(key=lambda f: f["title"].lower()) tree.insert(0, {"label": "Garden", "children": [], "files": root_files}) 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 count_nodes(nodes): total_files = 0 total_folders = 0 if not nodes: return 0, 0 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 generate_sidebar_tree(org_dir: Path, out_file: Path) -> None: """Generate sidebar-tree.json with MOC hubs and folder tree.""" print(" Generating sidebar-tree.json...") org_dir = org_dir.expanduser().resolve() out_file = out_file.expanduser() output = { "mocs": build_mocs(org_dir), "tree": build_tree(org_dir), "exclude": sorted(EXCLUDE_DIRS), } 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 " f"({len(output['mocs'])} mocs, {folder_count} folders, {files_count} files)" ) print(f" → {out_file}") 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()