Files
org_roam/generate_sidebar_tree.py
2026-04-19 23:11:00 +01:00

196 lines
5.2 KiB
Python

"""
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()