/** * DocsSurface — builtin documentation viewer (v0.6.2) * * Reads globals: * __SECTION__ — active doc slug (e.g. "GETTING-STARTED") * __BASE__ — base path * * Fetches markdown from GET /api/v1/docs/:name, renders with * a simple markdown-to-HTML converter. Sidebar lists all docs. * * v0.6.2: dark mode fix, topbar navigation, error handling. */ const { html } = window; const { useState, useEffect, useCallback, useMemo } = hooks; const { render } = preact; import { Topbar } from '../../shell/topbar.js'; function DocsSurface() { const [docs, setDocs] = useState([]); const [active, setActive] = useState(window.__SECTION__ || 'GETTING-STARTED'); const [content, setContent] = useState(''); const [title, setTitle] = useState(''); const [loading, setLoading] = useState(true); const [listError, setListError] = useState(false); // Load doc list useEffect(() => { if (!sw?.api?.get) return; sw.api.get('/api/v1/docs').then(r => { setDocs(Array.isArray(r) ? r : r.data || []); setListError(false); }).catch(() => { setListError(true); }); }, []); // Load active doc const loadDoc = useCallback(async (slug) => { setLoading(true); setActive(slug); try { const resp = await sw.api.get(`/api/v1/docs/${slug}`); const d = resp.data || resp; setTitle(d.title || slug); setContent(d.content || 'Document not found.'); } catch (e) { setContent('Failed to load document.'); } setLoading(false); // Update URL without reload const base = window.__BASE__ || ''; history.replaceState(null, '', `${base}/docs/${slug}`); }, []); useEffect(() => { loadDoc(active); }, []); function navigate(slug) { if (slug === active) return; loadDoc(slug); // Scroll to top const el = document.querySelector('.docs-content'); if (el) el.scrollTop = 0; } // Extract document outline from markdown content const outline = useMemo(() => { if (!content) return []; const headings = []; let inCode = false; for (const line of content.split('\n')) { if (line.startsWith('```')) { inCode = !inCode; continue; } if (inCode) continue; const m = line.match(/^(#{1,4})\s+(.+)/); if (m) { const text = m[2].replace(/[*_`\[\]]/g, ''); const id = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); headings.push({ level: m[1].length, text, id }); } } return headings; }, [content]); function scrollToHeading(id) { const el = document.getElementById(id); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); } function retryList() { setListError(false); sw.api.get('/api/v1/docs').then(r => { setDocs(Array.isArray(r) ? r : r.data || []); }).catch(() => { setListError(true); }); } return html` <${Topbar} title="Documentation" />
${loading ? html`
Loading\u2026
` : html`
`}
${!loading && outline.length > 2 ? html` ` : null}
`; } // ── Simple markdown renderer ───────────────── // Handles: headings, code blocks, inline code, bold, italic, links, lists, paragraphs, tables, hr. // Not a full CommonMark parser — good enough for documentation. function renderMarkdown(md) { if (!md) return ''; let html = ''; const lines = md.split('\n'); let i = 0; let inCode = false; let codeLang = ''; let codeLines = []; let inList = false; let listType = ''; let inTable = false; let tableRows = []; function esc(s) { return s.replace(/&/g, '&').replace(//g, '>'); } function inline(s) { s = esc(s); // Links [text](url) s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); // Bold **text** or __text__ s = s.replace(/\*\*(.+?)\*\*/g, '$1'); s = s.replace(/__(.+?)__/g, '$1'); // Italic *text* or _text_ s = s.replace(/(?$1'); // Inline code `code` s = s.replace(/`([^`]+)`/g, '$1'); return s; } function flushTable() { if (!inTable || tableRows.length === 0) return; inTable = false; html += ''; const headers = tableRows[0]; for (const h of headers) html += ``; html += ''; for (let r = 2; r < tableRows.length; r++) { html += ''; for (const cell of tableRows[r]) html += ``; html += ''; } html += '
${inline(h.trim())}
${inline(cell.trim())}
'; tableRows = []; } function flushList() { if (!inList) return; inList = false; html += listType === 'ol' ? '' : ''; } while (i < lines.length) { const line = lines[i]; // Fenced code blocks if (line.startsWith('```')) { if (inCode) { html += `
${esc(codeLines.join('\n'))}
`; inCode = false; codeLines = []; codeLang = ''; } else { flushList(); flushTable(); inCode = true; codeLang = line.slice(3).trim(); } i++; continue; } if (inCode) { codeLines.push(line); i++; continue; } // Table rows if (line.includes('|') && line.trim().startsWith('|')) { flushList(); const cells = line.split('|').slice(1, -1); if (!inTable) inTable = true; // Skip separator row (---|---) if (cells.every(c => /^[\s:-]+$/.test(c))) { tableRows.push(cells); // keep for row counting } else { tableRows.push(cells); } i++; continue; } else { flushTable(); } // Headings const headingMatch = line.match(/^(#{1,6})\s+(.+)/); if (headingMatch) { flushList(); const level = headingMatch[1].length; const text = headingMatch[2]; const id = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); html += `${inline(text)}`; i++; continue; } // Horizontal rule if (/^(-{3,}|\*{3,}|_{3,})$/.test(line.trim())) { flushList(); html += '
'; i++; continue; } // Unordered list if (/^\s*[-*+]\s+/.test(line)) { if (!inList || listType !== 'ul') { flushList(); inList = true; listType = 'ul'; html += '