feat: templates
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter
|
||||
@@ -10,8 +11,10 @@ router = APIRouter()
|
||||
SOURCES_DIR = Path(os.environ.get("SOURCES_DIR", "/data/sources"))
|
||||
PAGES_DIR = Path(os.environ.get("PAGES_DIR", "/data/pages"))
|
||||
|
||||
# Matches Micron links: [label`slug] or [label`slug.mu]
|
||||
_INTERNAL_LINK = re.compile(r'\[([^`\]]+)`([a-zA-Z0-9_-]+)(?:\.mu)?\]')
|
||||
# Match µFrame link nodes: link "display" "/page/slug.mu" or link "display" "slug"
|
||||
_UF_LINK = re.compile(r'^\s*link\s+', re.IGNORECASE)
|
||||
# Fallback: Micron links [label`slug] or [label`slug.mu]
|
||||
_MICRON_LINK = re.compile(r'\[([^`\]]+)`([a-zA-Z0-9_-]+)(?:\.mu)?\]')
|
||||
|
||||
|
||||
class GraphNode(BaseModel):
|
||||
@@ -38,19 +41,78 @@ def _all_page_names() -> set[str]:
|
||||
names.add(f.stem)
|
||||
if SOURCES_DIR.is_dir():
|
||||
for f in SOURCES_DIR.iterdir():
|
||||
if f.suffix == ".mu" and f.is_file():
|
||||
if f.suffix in (".uf", ".mu") and f.is_file():
|
||||
names.add(f.stem)
|
||||
return names
|
||||
|
||||
|
||||
def _extract_title(micron: str) -> str | None:
|
||||
for line in micron.splitlines():
|
||||
def _extract_title(source: str) -> str | None:
|
||||
"""Extract title from µFrame page or heading, or legacy Micron >Title."""
|
||||
for line in source.splitlines():
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
if stripped.lower().startswith("page "):
|
||||
try:
|
||||
parts = shlex.split(stripped)
|
||||
if len(parts) >= 2:
|
||||
return parts[1]
|
||||
except ValueError:
|
||||
pass
|
||||
break
|
||||
if stripped.lower().startswith("heading "):
|
||||
try:
|
||||
parts = shlex.split(stripped)
|
||||
if len(parts) >= 3:
|
||||
return parts[2]
|
||||
except ValueError:
|
||||
pass
|
||||
break
|
||||
if stripped.startswith(">") and not stripped.startswith(">>"):
|
||||
return stripped[1:].strip()
|
||||
break
|
||||
return None
|
||||
|
||||
|
||||
def _extract_links(source: str, all_names: set[str]) -> list[str]:
|
||||
"""Extract internal link targets from µFrame or Micron source."""
|
||||
targets: list[str] = []
|
||||
|
||||
for line in source.splitlines():
|
||||
stripped = line.strip()
|
||||
|
||||
# µFrame: link "display" "/page/slug.mu" or link "display" "slug"
|
||||
if _UF_LINK.match(stripped):
|
||||
try:
|
||||
parts = shlex.split(stripped)
|
||||
if len(parts) >= 3:
|
||||
dest = parts[2]
|
||||
# Normalize: /page/slug.mu → slug
|
||||
slug = dest.rsplit("/", 1)[-1].removesuffix(".mu")
|
||||
if slug in all_names:
|
||||
targets.append(slug)
|
||||
except ValueError:
|
||||
pass
|
||||
continue
|
||||
|
||||
# Fallback: Micron link syntax [label`slug]
|
||||
for m in _MICRON_LINK.finditer(stripped):
|
||||
slug = m.group(2)
|
||||
if slug in all_names:
|
||||
targets.append(slug)
|
||||
|
||||
return targets
|
||||
|
||||
|
||||
def _source_path(name: str) -> Path | None:
|
||||
"""Get source file path, preferring .uf over .mu."""
|
||||
uf = SOURCES_DIR / f"{name}.uf"
|
||||
if uf.is_file():
|
||||
return uf
|
||||
mu = SOURCES_DIR / f"{name}.mu"
|
||||
return mu if mu.is_file() else None
|
||||
|
||||
|
||||
@router.get("/graph", response_model=GraphData)
|
||||
async def get_graph():
|
||||
all_names = _all_page_names()
|
||||
@@ -58,18 +120,15 @@ async def get_graph():
|
||||
edges: list[GraphEdge] = []
|
||||
|
||||
for name in sorted(all_names):
|
||||
src_path = SOURCES_DIR / f"{name}.mu"
|
||||
src_path = _source_path(name)
|
||||
mu_path = PAGES_DIR / f"{name}.mu"
|
||||
|
||||
title = None
|
||||
if src_path.is_file():
|
||||
if src_path:
|
||||
content = src_path.read_text(encoding="utf-8")
|
||||
title = _extract_title(content)
|
||||
|
||||
for match in _INTERNAL_LINK.finditer(content):
|
||||
target = match.group(2)
|
||||
if target in all_names:
|
||||
edges.append(GraphEdge(source=name, target=target))
|
||||
for target in _extract_links(content, all_names):
|
||||
edges.append(GraphEdge(source=name, target=target))
|
||||
|
||||
nodes.append(GraphNode(
|
||||
id=name,
|
||||
|
||||
Reference in New Issue
Block a user