archiving
Some checks failed
Build Roam Site / build (push) Has been cancelled

This commit is contained in:
2026-06-03 14:12:58 +01:00
parent a758ae1793
commit ac1d3839ba
22 changed files with 1180 additions and 285 deletions

View File

@@ -1,132 +1,113 @@
"""
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".
Generates sidebar-tree.json from the org-roam directory.
Output shape:
{
"groups": [
"mocs": [
{ "title": "Brain MOC", "url": "/20241210233721-brain_moc.html", "pinned": true },
...
],
"tree": [
{
"label": "java",
"files": [
{ "title": "Java MOC", "url": "/20250402185735-java_moc.html" },
...
]
"label": "Articles",
"children": [],
"files": [{ "title": "...", "url": "/Articles/....html" }]
},
...
{
"label": "uncategorised",
"files": [ ... ]
}
]
],
"exclude": [".packages"]
}
"""
import json
import re
from pathlib import Path
from collections import defaultdict
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, 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)
"""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)
category_match = category_pattern.search(content)
id_match = id_pattern.search(content)
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",
"category": category_match.group(1).strip() if category_match else None,
"id": id_match.group(1).strip() if id_match else None,
"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 generate_sidebar_tree(org_dir: Path, out_file: Path) -> None:
"""Generate sidebar-tree.json grouped by #+category:."""
print(" Generating sidebar-tree.json...")
def is_moc(meta: dict) -> bool:
return "moc" in meta.get("tags", set())
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))
]
def build_mocs(org_dir: Path) -> list[dict]:
"""Collect MOC notes tagged with :moc:."""
mocs = []
groups: dict[str, list[dict]] = defaultdict(list)
for file_path in org_files:
meta = get_org_metadata(file_path)
if not meta:
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
# 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",
}
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})
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}")
mocs.sort(key=lambda m: (not m["pinned"], m["title"].lower()))
return mocs
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."""
@@ -135,8 +116,6 @@ def build_tree(org_dir: Path) -> list:
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": []}
@@ -148,13 +127,14 @@ def build_tree(org_dir: Path) -> list:
insert_node(node["children"], parts[1:], entry)
tree = []
root_files = []
org_files = [
f for f in org_dir.rglob("*.org")
if not re.search(r"/\.#[^/]+\.org$", str(f))
]
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
for file_path in org_files:
meta = get_org_metadata(file_path)
if not meta:
continue
@@ -169,10 +149,16 @@ def build_tree(org_dir: Path) -> list:
"url": f"/{relative.with_suffix('')}.html",
}
folder_parts = relative.parts[:-1] # ignore filename
insert_node(tree, list(folder_parts), entry)
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})
# Sort everything
def sort_tree(nodes):
nodes.sort(key=lambda n: n["label"].lower())
for node in nodes:
@@ -180,14 +166,54 @@ def build_tree(org_dir: Path) -> list:
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"
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)