feat: init
This commit is contained in:
81
backend/graph.py
Normal file
81
backend/graph.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
SOURCES_DIR = Path(os.environ.get("SOURCES_DIR", "/data/sources"))
|
||||
PAGES_DIR = Path(os.environ.get("PAGES_DIR", "/data/pages"))
|
||||
|
||||
# Matches markdown links: [text](slug) where slug has no protocol or path separators
|
||||
_INTERNAL_LINK = re.compile(r"\[([^\]]+)\]\(([a-zA-Z0-9_-]+)\)")
|
||||
|
||||
|
||||
class GraphNode(BaseModel):
|
||||
id: str
|
||||
published: bool
|
||||
title: str | None = None
|
||||
|
||||
|
||||
class GraphEdge(BaseModel):
|
||||
source: str
|
||||
target: str
|
||||
|
||||
|
||||
class GraphData(BaseModel):
|
||||
nodes: list[GraphNode]
|
||||
edges: list[GraphEdge]
|
||||
|
||||
|
||||
def _all_page_names() -> set[str]:
|
||||
names: set[str] = set()
|
||||
if PAGES_DIR.is_dir():
|
||||
for f in PAGES_DIR.iterdir():
|
||||
if f.suffix == ".mu" and f.is_file():
|
||||
names.add(f.stem)
|
||||
if SOURCES_DIR.is_dir():
|
||||
for f in SOURCES_DIR.iterdir():
|
||||
if f.suffix == ".md" and f.is_file():
|
||||
names.add(f.stem)
|
||||
return names
|
||||
|
||||
|
||||
def _extract_title(markdown: str) -> str | None:
|
||||
for line in markdown.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("# "):
|
||||
return stripped[2:].strip()
|
||||
return None
|
||||
|
||||
|
||||
@router.get("/graph", response_model=GraphData)
|
||||
async def get_graph():
|
||||
all_names = _all_page_names()
|
||||
nodes: list[GraphNode] = []
|
||||
edges: list[GraphEdge] = []
|
||||
|
||||
for name in sorted(all_names):
|
||||
md_path = SOURCES_DIR / f"{name}.md"
|
||||
mu_path = PAGES_DIR / f"{name}.mu"
|
||||
|
||||
title = None
|
||||
if md_path.is_file():
|
||||
content = md_path.read_text(encoding="utf-8")
|
||||
title = _extract_title(content)
|
||||
|
||||
# Parse internal links
|
||||
for match in _INTERNAL_LINK.finditer(content):
|
||||
target = match.group(2)
|
||||
if target in all_names:
|
||||
edges.append(GraphEdge(source=name, target=target))
|
||||
|
||||
nodes.append(GraphNode(
|
||||
id=name,
|
||||
published=mu_path.is_file(),
|
||||
title=title,
|
||||
))
|
||||
|
||||
return GraphData(nodes=nodes, edges=edges)
|
||||
Reference in New Issue
Block a user