#!/usr/bin/env python3 """Generate a minimal, self-contained OKF bundle visualizer (stdlib only). Emits one HTML file: a Cytoscape.js force-directed graph of the bundle's concepts (nodes colored by `type`), with a side panel that renders the selected concept's markdown body via marked. Both libraries load from a CDN; the bundle is embedded as JSON, so no backend and no data leaves the page. """ from __future__ import annotations import argparse import html import json import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) from okf_common import (concept_id, extract_links, iter_concepts, parse_frontmatter) def _target_id(bundle, path, target, ids): t = target.split("#", 1)[0].strip() if not t or "://" in t or not t.endswith(".md"): return None if t.startswith("/"): cid = t.lstrip("/")[:-3] else: rel = (path.parent / t).resolve().relative_to(Path(bundle).resolve()) cid = rel.as_posix()[:-3] return cid if cid in ids else None def build_graph(bundle_dir): bundle = Path(bundle_dir) concepts = list(iter_concepts(bundle)) ids = {concept_id(bundle, p) for p in concepts} nodes, edges = [], [] for path in concepts: cid = concept_id(bundle, path) text = path.read_text(encoding="utf-8") try: meta, body = parse_frontmatter(text) except Exception: meta, body = None, text meta = meta or {} nodes.append({ "id": cid, "type": str(meta.get("type", "Concept")), "title": str(meta.get("title", Path(cid).name)), "description": str(meta.get("description", "")), "body": body, }) for target in extract_links(body): tid = _target_id(bundle, path, target, ids) if tid and tid != cid: edges.append({"source": cid, "target": tid}) return {"nodes": nodes, "edges": edges} _HTML = """ __NAME__ — OKF Viewer
__NAME__

Click a concept node to view it.

""" def render_html(graph, name="OKF Bundle"): data = json.dumps(graph, ensure_ascii=False).replace("