Highly ambitious ATProtocol AppView service and sdks
at fix-postgres 359 lines 11 kB view raw
1/// <reference lib="deno.ns" /> 2 3import { marked } from "marked"; 4import { markedHighlight } from "marked-highlight"; 5import { codeToHtml } from "shiki"; 6import type { Tokens } from "marked"; 7 8// Categorized documentation structure 9const DOCS_CATEGORIES = [ 10 { 11 category: "Getting Started", 12 docs: [ 13 { 14 slug: "intro", 15 title: "Introduction", 16 description: "Overview of Slices platform", 17 }, 18 { 19 slug: "getting-started", 20 title: "Quick Start", 21 description: "Learn how to set up and use Slices", 22 }, 23 ], 24 }, 25 { 26 category: "Core Concepts", 27 docs: [ 28 { 29 slug: "concepts", 30 title: "Core Concepts", 31 description: "Understand slices, lexicons, and collections", 32 }, 33 ], 34 }, 35 { 36 category: "Reference", 37 docs: [ 38 { 39 slug: "api-reference", 40 title: "API Reference", 41 description: "Complete endpoint documentation", 42 }, 43 { 44 slug: "graphql-api", 45 title: "GraphQL API", 46 description: "Query indexed data with GraphQL", 47 }, 48 { 49 slug: "sdk-usage", 50 title: "SDK Usage", 51 description: "Advanced client patterns and examples", 52 }, 53 ], 54 }, 55 { 56 category: "Extensions", 57 docs: [ 58 { 59 slug: "plugins", 60 title: "Plugins", 61 description: "Extensions and tools for development", 62 }, 63 ], 64 }, 65]; 66 67// Flatten for quick lookup 68const AVAILABLE_DOCS = DOCS_CATEGORIES.flatMap((category) => category.docs); 69 70const DOCS_PATH = Deno.env.get("DOCS_PATH") || "../docs"; 71 72async function readMarkdownFile(slug: string): Promise<string | null> { 73 try { 74 const filePath = `${DOCS_PATH}/${slug}.md`; 75 const content = await Deno.readTextFile(filePath); 76 return content; 77 } catch (error) { 78 console.error(`Failed to read ${slug}.md:`, error); 79 return null; 80 } 81} 82 83// Decode HTML entities 84function decodeHtmlEntities(text: string): string { 85 const entities: Record<string, string> = { 86 "&amp;": "&", 87 "&lt;": "<", 88 "&gt;": ">", 89 "&quot;": '"', 90 "&#39;": "'", 91 "&nbsp;": " ", 92 }; 93 94 return text.replace(/&[^;]+;/g, (entity) => entities[entity] || entity); 95} 96 97// Extract headers for table of contents 98function extractHeaders(html: string) { 99 const headerRegex = /<h([1-6])[^>]*>(.*?)<\/h[1-6]>/g; 100 const headers: Array<{ level: number; text: string; id: string }> = []; 101 102 let match; 103 while ((match = headerRegex.exec(html)) !== null) { 104 const level = parseInt(match[1]); 105 const rawText = match[2].replace(/<[^>]+>/g, ""); // Strip HTML tags 106 const text = decodeHtmlEntities(rawText); // Decode HTML entities 107 const id = text 108 .toLowerCase() 109 .replace(/[^\w\s-]/g, "") // Remove special characters 110 .replace(/\s+/g, "-") // Replace spaces with hyphens 111 .trim(); 112 113 headers.push({ level, text, id }); 114 } 115 116 return headers; 117} 118 119// Markdown to HTML converter using marked with Shiki syntax highlighting 120async function markdownToHtml(markdown: string): Promise<string> { 121 // Configure marked with highlight extension for code blocks 122 marked.use( 123 markedHighlight({ 124 async: true, 125 async highlight(code, lang, _info) { 126 const highlightedCode = await codeToHtml(code, { 127 lang: lang || "text", 128 theme: "ayu-dark", 129 }); 130 131 // Add padding and overflow to the pre element 132 const styledCode = highlightedCode.replace( 133 '<pre class="shiki', 134 '<pre class="shiki !p-4 overflow-x-auto', 135 ); 136 137 // Wrap in a container with border and rounded corners 138 return `<div class="my-4 border border-zinc-800 rounded overflow-hidden">${styledCode}</div>`; 139 }, 140 langPrefix: "language-", 141 }), 142 ); 143 144 // Configure marked with custom renderer 145 const renderer = new marked.Renderer(); 146 147 // Custom inline code renderer 148 renderer.codespan = function (token: Tokens.Codespan) { 149 return `<code class="bg-zinc-800/50 text-zinc-200 px-1.5 py-0.5 rounded text-xs font-mono">${token.text}</code>`; 150 }; 151 152 // Custom header renderer with IDs and styling 153 renderer.heading = function (token: Tokens.Heading) { 154 const text = this.parser.parseInline(token.tokens); 155 const level = token.depth; 156 const id = text 157 .toLowerCase() 158 .replace(/<[^>]+>/g, "") // Strip HTML tags 159 .replace(/[^\w\s-]/g, "") // Remove special characters 160 .replace(/\s+/g, "-") // Replace spaces with hyphens 161 .trim(); 162 163 const styles = { 164 1: "text-xl font-semibold text-white mt-8 mb-4", 165 2: "text-lg font-semibold text-white mt-8 mb-3", 166 3: "text-base font-medium text-zinc-100 mt-6 mb-3", 167 4: "text-sm font-medium text-zinc-200 mt-4 mb-2", 168 5: "text-xs font-medium text-zinc-200 mt-4 mb-2", 169 6: "text-xs font-medium text-zinc-300 mt-3 mb-2", 170 }; 171 172 return `<h${level} id="${id}" class="${ 173 styles[level as keyof typeof styles] 174 }">${text}</h${level}>`; 175 }; 176 177 // Custom link renderer to handle .md links 178 renderer.link = function (token: Tokens.Link) { 179 let href = token.href; 180 const title = token.title; 181 const text = this.parser.parseInline(token.tokens); 182 183 // Convert relative .md links to docs routes 184 if (href.endsWith(".md") && !href.startsWith("http")) { 185 const slug = href.replace(/^\.\//, "").replace(/\.md$/, ""); 186 href = `/docs/${slug}`; 187 } 188 189 const titleAttr = title ? ` title="${title}"` : ""; 190 return `<a href="${href}"${titleAttr} class="text-blue-400 hover:text-blue-300 underline">${text}</a>`; 191 }; 192 193 // Custom paragraph renderer 194 renderer.paragraph = function (token: Tokens.Paragraph) { 195 const text = this.parser.parseInline(token.tokens); 196 return `<p class="mb-3 leading-relaxed text-zinc-400">${text}</p>`; 197 }; 198 199 // Custom list renderer - just add styling classes 200 renderer.list = function (token: Tokens.List) { 201 const ordered = token.ordered; 202 const tag = ordered ? "ol" : "ul"; 203 const listStyle = ordered ? "list-decimal" : "list-disc"; 204 205 // Use default marked behavior for rendering items 206 let body = ""; 207 for (let i = 0; i < token.items.length; i++) { 208 body += this.listitem(token.items[i]); 209 } 210 211 return `<${tag} class="${listStyle} list-inside my-3 text-zinc-400">${body}</${tag}>`; 212 }; 213 214 renderer.listitem = function (token: Tokens.ListItem) { 215 let text = ""; 216 if (token.task) { 217 const checkbox = `<input ${ 218 token.checked ? 'checked="" ' : "" 219 }disabled="" type="checkbox"/>`; 220 if (token.loose) { 221 if (token.tokens.length > 0 && token.tokens[0].type === "paragraph") { 222 token.tokens[0].text = checkbox + " " + token.tokens[0].text; 223 if ( 224 token.tokens[0].tokens && token.tokens[0].tokens.length > 0 && 225 token.tokens[0].tokens[0].type === "text" 226 ) { 227 token.tokens[0].tokens[0].text = checkbox + " " + 228 token.tokens[0].tokens[0].text; 229 } 230 } else { 231 token.tokens.unshift( 232 { type: "text", raw: checkbox + " ", text: checkbox + " " } as Tokens.Text, 233 ); 234 } 235 } else { 236 text += checkbox + " "; 237 } 238 } 239 240 text += this.parser.parse(token.tokens, !!token.loose); 241 242 return `<li class="mb-1.5">${text}</li>`; 243 }; 244 245 // Custom strong/bold renderer 246 renderer.strong = function (token: Tokens.Strong) { 247 const text = this.parser.parseInline(token.tokens); 248 return `<strong class="font-medium text-zinc-100">${text}</strong>`; 249 }; 250 251 // Custom emphasis/italic renderer 252 renderer.em = function (token: Tokens.Em) { 253 const text = this.parser.parseInline(token.tokens); 254 return `<em class="italic">${text}</em>`; 255 }; 256 257 // Custom code block renderer - return content as-is since marked-highlight handles it 258 renderer.code = function (token: Tokens.Code) { 259 return token.text; // marked-highlight has already processed this 260 }; 261 262 // Custom table renderer 263 renderer.table = function (token: Tokens.Table) { 264 const header = token.header 265 .map((cell: Tokens.TableCell, i: number) => { 266 const text = this.parser.parseInline(cell.tokens); 267 const align = token.align[i] 268 ? ` style="text-align: ${token.align[i]}"` 269 : ""; 270 return `<th class="px-3 py-2 text-left text-xs font-medium text-zinc-300 border-b border-zinc-800"${align}>${text}</th>`; 271 }) 272 .join(""); 273 274 const rows = token.rows 275 .map((row: Tokens.TableCell[]) => { 276 const cells = row 277 .map((cell: Tokens.TableCell, i: number) => { 278 const text = this.parser.parseInline(cell.tokens); 279 const align = token.align[i] 280 ? ` style="text-align: ${token.align[i]}"` 281 : ""; 282 return `<td class="px-3 py-2 text-xs text-zinc-300 border-b border-zinc-800/50"${align}>${text}</td>`; 283 }) 284 .join(""); 285 return `<tr class="hover:bg-zinc-800/20">${cells}</tr>`; 286 }) 287 .join(""); 288 289 return `<div class="overflow-x-auto my-4 border border-zinc-800 rounded"> 290 <table class="min-w-full"> 291 <thead class="bg-zinc-800/30"> 292 <tr>${header}</tr> 293 </thead> 294 <tbody> 295 ${rows} 296 </tbody> 297 </table> 298 </div>`; 299 }; 300 301 // Set options and use the custom renderer 302 marked.setOptions({ 303 renderer: renderer, 304 gfm: true, 305 breaks: false, // This helps with multi-line list items 306 pedantic: false, 307 }); 308 309 // Parse markdown to HTML using marked (this handles multi-line list items properly) 310 const html = await marked.parse(markdown); 311 312 return html; 313} 314 315export function handleDocsIndex(_req: Request): Response { 316 return Response.json({ 317 categories: DOCS_CATEGORIES, 318 }); 319} 320 321export async function handleDocsDetail(req: Request): Promise<Response> { 322 const url = new URL(req.url); 323 const slug = url.pathname.split("/")[3]; // /api/docs/{slug} 324 325 if (!slug) { 326 return Response.json({ error: "Missing slug" }, { status: 400 }); 327 } 328 329 // Check if slug is valid 330 const docInfo = AVAILABLE_DOCS.find((doc) => doc.slug === slug); 331 if (!docInfo) { 332 return Response.json( 333 { error: "Documentation page not found" }, 334 { status: 404 }, 335 ); 336 } 337 338 // Read markdown content 339 const markdownContent = await readMarkdownFile(slug); 340 if (!markdownContent) { 341 return Response.json( 342 { error: "Documentation content not found" }, 343 { status: 404 }, 344 ); 345 } 346 347 // Convert to HTML with Shiki syntax highlighting 348 const htmlContent = await markdownToHtml(markdownContent); 349 350 // Extract headers for table of contents 351 const headers = extractHeaders(htmlContent); 352 353 return Response.json({ 354 title: docInfo.title, 355 content: htmlContent, 356 headers: headers, 357 slug: slug, 358 }); 359}