Add documentation routes and update README
- Update README with comprehensive documentation covering ActivityPub, OpenTimestamps anchoring, L1 integration, and all API endpoints - Add /docs routes to serve markdown documentation as styled HTML - Include common library documentation in web interface 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
184
server.py
184
server.py
@@ -3682,6 +3682,190 @@ async def download_client():
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Documentation Routes
|
||||
# ============================================================================
|
||||
|
||||
# Documentation paths
|
||||
L2_DOCS_DIR = Path(__file__).parent
|
||||
COMMON_DOCS_DIR = Path(__file__).parent.parent / "common"
|
||||
|
||||
L2_DOCS_MAP = {
|
||||
"l2": L2_DOCS_DIR / "README.md",
|
||||
"common": COMMON_DOCS_DIR / "README.md",
|
||||
}
|
||||
|
||||
|
||||
def render_markdown(content: str) -> str:
|
||||
"""Convert markdown to HTML with basic styling."""
|
||||
import re
|
||||
|
||||
# Escape HTML first
|
||||
content = content.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
# Code blocks (``` ... ```)
|
||||
def code_block_replace(match):
|
||||
lang = match.group(1) or ""
|
||||
code = match.group(2)
|
||||
return f'<pre class="bg-gray-800 p-4 rounded-lg overflow-x-auto text-sm"><code class="language-{lang}">{code}</code></pre>'
|
||||
content = re.sub(r'```(\w*)\n(.*?)```', code_block_replace, content, flags=re.DOTALL)
|
||||
|
||||
# Inline code
|
||||
content = re.sub(r'`([^`]+)`', r'<code class="bg-gray-700 px-1 rounded text-sm">\1</code>', content)
|
||||
|
||||
# Headers
|
||||
content = re.sub(r'^### (.+)$', r'<h3 class="text-lg font-semibold text-white mt-6 mb-2">\1</h3>', content, flags=re.MULTILINE)
|
||||
content = re.sub(r'^## (.+)$', r'<h2 class="text-xl font-bold text-white mt-8 mb-3 border-b border-gray-700 pb-2">\1</h2>', content, flags=re.MULTILINE)
|
||||
content = re.sub(r'^# (.+)$', r'<h1 class="text-2xl font-bold text-white mb-4">\1</h1>', content, flags=re.MULTILINE)
|
||||
|
||||
# Bold and italic
|
||||
content = re.sub(r'\*\*([^*]+)\*\*', r'<strong class="font-semibold">\1</strong>', content)
|
||||
content = re.sub(r'\*([^*]+)\*', r'<em>\1</em>', content)
|
||||
|
||||
# Links
|
||||
content = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'<a href="\2" class="text-blue-400 hover:underline">\1</a>', content)
|
||||
|
||||
# Tables
|
||||
def table_replace(match):
|
||||
lines = match.group(0).strip().split('\n')
|
||||
if len(lines) < 2:
|
||||
return match.group(0)
|
||||
|
||||
header = lines[0]
|
||||
rows = lines[2:] if len(lines) > 2 else []
|
||||
|
||||
header_cells = [cell.strip() for cell in header.split('|')[1:-1]]
|
||||
header_html = ''.join(f'<th class="px-4 py-2 text-left border-b border-gray-600">{cell}</th>' for cell in header_cells)
|
||||
|
||||
rows_html = ''
|
||||
for row in rows:
|
||||
cells = [cell.strip() for cell in row.split('|')[1:-1]]
|
||||
cells_html = ''.join(f'<td class="px-4 py-2 border-b border-gray-700">{cell}</td>' for cell in cells)
|
||||
rows_html += f'<tr class="hover:bg-gray-700">{cells_html}</tr>'
|
||||
|
||||
return f'<table class="w-full text-sm mb-4"><thead><tr class="bg-gray-700">{header_html}</tr></thead><tbody>{rows_html}</tbody></table>'
|
||||
|
||||
content = re.sub(r'(\|[^\n]+\|\n)+', table_replace, content)
|
||||
|
||||
# Bullet points
|
||||
content = re.sub(r'^- (.+)$', r'<li class="ml-4 list-disc">\1</li>', content, flags=re.MULTILINE)
|
||||
content = re.sub(r'(<li[^>]*>.*</li>\n?)+', r'<ul class="mb-4">\g<0></ul>', content)
|
||||
|
||||
# Paragraphs (lines not starting with < or whitespace)
|
||||
lines = content.split('\n')
|
||||
result = []
|
||||
in_paragraph = False
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if not stripped:
|
||||
if in_paragraph:
|
||||
result.append('</p>')
|
||||
in_paragraph = False
|
||||
result.append('')
|
||||
elif stripped.startswith('<'):
|
||||
if in_paragraph:
|
||||
result.append('</p>')
|
||||
in_paragraph = False
|
||||
result.append(line)
|
||||
else:
|
||||
if not in_paragraph:
|
||||
result.append('<p class="mb-4 text-gray-300">')
|
||||
in_paragraph = True
|
||||
result.append(line)
|
||||
if in_paragraph:
|
||||
result.append('</p>')
|
||||
content = '\n'.join(result)
|
||||
|
||||
return content
|
||||
|
||||
|
||||
@app.get("/docs", response_class=HTMLResponse)
|
||||
async def docs_index(request: Request):
|
||||
"""Documentation index page."""
|
||||
user = await get_optional_user(request)
|
||||
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html class="dark">
|
||||
<head>
|
||||
<title>Documentation - Art DAG L2</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>tailwind.config = {{ darkMode: 'class' }}</script>
|
||||
</head>
|
||||
<body class="bg-gray-900 text-gray-100 min-h-screen">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 px-6 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<a href="/" class="text-xl font-bold text-white">Art DAG L2</a>
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="/assets" class="text-gray-300 hover:text-white">Assets</a>
|
||||
<a href="/activities" class="text-gray-300 hover:text-white">Activities</a>
|
||||
<a href="/anchors/ui" class="text-gray-300 hover:text-white">Anchors</a>
|
||||
<a href="/docs" class="text-white font-semibold">Docs</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="max-w-4xl mx-auto p-8">
|
||||
<h1 class="text-3xl font-bold mb-8">Documentation</h1>
|
||||
<div class="grid gap-4">
|
||||
<a href="/docs/l2" class="block p-6 bg-gray-800 rounded-lg hover:bg-gray-700 transition">
|
||||
<h2 class="text-xl font-semibold text-white mb-2">L2 Server (ActivityPub)</h2>
|
||||
<p class="text-gray-400">Ownership registry, ActivityPub federation, and OpenTimestamps anchoring.</p>
|
||||
</a>
|
||||
<a href="/docs/common" class="block p-6 bg-gray-800 rounded-lg hover:bg-gray-700 transition">
|
||||
<h2 class="text-xl font-semibold text-white mb-2">Common Library</h2>
|
||||
<p class="text-gray-400">Shared components: Jinja2 templates, middleware, content negotiation, and utilities.</p>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>"""
|
||||
return HTMLResponse(html)
|
||||
|
||||
|
||||
@app.get("/docs/{doc_name}", response_class=HTMLResponse)
|
||||
async def docs_page(doc_name: str, request: Request):
|
||||
"""Render a markdown documentation file as HTML."""
|
||||
if doc_name not in L2_DOCS_MAP:
|
||||
raise HTTPException(404, f"Documentation '{doc_name}' not found")
|
||||
|
||||
doc_path = L2_DOCS_MAP[doc_name]
|
||||
if not doc_path.exists():
|
||||
raise HTTPException(404, f"Documentation file not found: {doc_path}")
|
||||
|
||||
content = doc_path.read_text()
|
||||
html_content = render_markdown(content)
|
||||
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html class="dark">
|
||||
<head>
|
||||
<title>{doc_name.upper()} - Art DAG Documentation</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>tailwind.config = {{ darkMode: 'class' }}</script>
|
||||
</head>
|
||||
<body class="bg-gray-900 text-gray-100 min-h-screen">
|
||||
<nav class="bg-gray-800 border-b border-gray-700 px-6 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<a href="/" class="text-xl font-bold text-white">Art DAG L2</a>
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="/assets" class="text-gray-300 hover:text-white">Assets</a>
|
||||
<a href="/activities" class="text-gray-300 hover:text-white">Activities</a>
|
||||
<a href="/anchors/ui" class="text-gray-300 hover:text-white">Anchors</a>
|
||||
<a href="/docs" class="text-white font-semibold">Docs</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="max-w-4xl mx-auto p-8">
|
||||
<div class="mb-4">
|
||||
<a href="/docs" class="text-blue-400 hover:underline">← Back to Documentation</a>
|
||||
</div>
|
||||
<article class="prose prose-invert max-w-none">
|
||||
{html_content}
|
||||
</article>
|
||||
</main>
|
||||
</body>
|
||||
</html>"""
|
||||
return HTMLResponse(html)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run("server:app", host="0.0.0.0", port=8200, workers=4)
|
||||
|
||||
Reference in New Issue
Block a user