import type { Route } from "@std/http/unstable-route"; import { withAuth } from "../../routes/middleware.ts"; import { renderHTML } from "../../utils/render.tsx"; import { codeToHtml } from "@shikijs/shiki"; import { DocsPage } from "./templates/DocsPage.tsx"; import { DocsIndexPage } from "./templates/DocsIndexPage.tsx"; import { render } from "preact-render-to-string"; import { CodeBlock } from "./templates/fragments/CodeBlock.tsx"; import { marked } from "marked"; import type { Tokens } from "marked"; import { markedHighlight } from "marked-highlight"; // Categorized documentation structure const DOCS_CATEGORIES = [ { category: "Getting Started", docs: [ { slug: "intro", title: "Introduction", description: "Overview of Slices platform", }, { slug: "getting-started", title: "Quick Start", description: "Learn how to set up and use Slices", }, ], }, { category: "Core Concepts", docs: [ { slug: "concepts", title: "Core Concepts", description: "Understand slices, lexicons, and collections", }, ], }, { category: "Reference", docs: [ { slug: "api-reference", title: "API Reference", description: "Complete endpoint documentation", }, { slug: "graphql-api", title: "GraphQL API", description: "Query indexed data with GraphQL", }, { slug: "sdk-usage", title: "SDK Usage", description: "Advanced client patterns and examples", }, ], }, { category: "Extensions", docs: [ { slug: "plugins", title: "Plugins", description: "Extensions and tools for development", }, ], }, ]; // Flatten for backward compatibility const AVAILABLE_DOCS = DOCS_CATEGORIES.flatMap((category) => category.docs); const DOCS_PATH = Deno.env.get("DOCS_PATH") || "../docs"; async function readMarkdownFile(slug: string): Promise { try { const filePath = `${DOCS_PATH}/${slug}.md`; const content = await Deno.readTextFile(filePath); return content; } catch (error) { console.error(`Failed to read ${slug}.md:`, error); return null; } } // Decode HTML entities function decodeHtmlEntities(text: string): string { const entities: Record = { "&": "&", "<": "<", ">": ">", """: '"', "'": "'", " ": " ", }; return text.replace(/&[^;]+;/g, (entity) => entities[entity] || entity); } // Extract headers for table of contents function extractHeaders(html: string) { const headerRegex = /]*>(.*?)<\/h[1-6]>/g; const headers: Array<{ level: number; text: string; id: string }> = []; let match; while ((match = headerRegex.exec(html)) !== null) { const level = parseInt(match[1]); const rawText = match[2].replace(/<[^>]+>/g, ""); // Strip HTML tags const text = decodeHtmlEntities(rawText); // Decode HTML entities const id = text .toLowerCase() .replace(/[^\w\s-]/g, "") // Remove special characters .replace(/\s+/g, "-") // Replace spaces with hyphens .trim(); headers.push({ level, text, id }); } return headers; } // Markdown to HTML converter using marked with custom renderer and Shiki async function markdownToHtml(markdown: string): Promise { // Configure marked with highlight extension for code blocks marked.use( markedHighlight({ async: true, async highlight(code, lang, info) { // Parse filename from info string (e.g., "javascript filename.js") const parts = (info || "").split(/\s+/); const filename = parts.length > 1 ? parts.slice(1).join(" ") : undefined; // Handle Mermaid diagrams - client-side rendering if (lang === "mermaid") { return `
${code}
`; } const highlightedCode = await codeToHtml(code, { lang: lang || "text", themes: { light: "github-light", dark: "github-dark", }, }); // Generate the complete code block using composition if (filename) { return render( {filename} ); } else { return render(); } }, langPrefix: "language-", }) ); // Configure marked with custom renderer const renderer = new marked.Renderer(); // Custom inline code renderer renderer.codespan = function (token: Tokens.Codespan) { return `${token.text}`; }; // Custom header renderer with IDs and styling renderer.heading = function (token: Tokens.Heading) { const text = this.parser.parseInline(token.tokens); const level = token.depth; const id = text .toLowerCase() .replace(/<[^>]+>/g, "") // Strip HTML tags .replace(/[^\w\s-]/g, "") // Remove special characters .replace(/\s+/g, "-") // Replace spaces with hyphens .trim(); const styles = { 1: "text-2xl font-bold text-zinc-900 dark:text-white mt-10 mb-6", 2: "text-xl font-bold text-zinc-900 dark:text-white mt-10 mb-4", 3: "text-lg font-semibold text-zinc-900 dark:text-white mt-8 mb-4", 4: "text-base font-semibold text-zinc-900 dark:text-white mt-6 mb-3", 5: "text-sm font-semibold text-zinc-900 dark:text-white mt-4 mb-2", 6: "text-xs font-semibold text-zinc-900 dark:text-white mt-4 mb-2", }; return `${text}`; }; // Custom link renderer to handle .md links renderer.link = function (token: Tokens.Link) { let href = token.href; const title = token.title; const text = this.parser.parseInline(token.tokens); // Convert relative .md links to docs routes if (href.endsWith(".md") && !href.startsWith("http")) { const slug = href.replace(/^\.\//, "").replace(/\.md$/, ""); href = `/docs/${slug}`; } const titleAttr = title ? ` title="${title}"` : ""; return `${text}`; }; // Custom paragraph renderer renderer.paragraph = function (token: Tokens.Paragraph) { const text = this.parser.parseInline(token.tokens); return `

${text}

`; }; // Custom list renderer - wrap with classes const originalList = renderer.list.bind(renderer); renderer.list = function(token: Tokens.List) { const html = originalList(token); const type = token.ordered ? 'ol' : 'ul'; const classNames = token.ordered ? 'list-decimal list-inside my-4 text-zinc-700 dark:text-zinc-300' : 'list-disc list-inside my-4 text-zinc-700 dark:text-zinc-300'; return html.replace(new RegExp(`<${type}>`), `<${type} class="${classNames}">`); }; // Custom list item renderer const originalListitem = renderer.listitem.bind(renderer); renderer.listitem = function(token: Tokens.ListItem) { const html = originalListitem(token); return html.replace('
  • ', '
  • '); }; // Custom strong/bold renderer renderer.strong = function (token: Tokens.Strong) { const text = this.parser.parseInline(token.tokens); return `${text}`; }; // Custom emphasis/italic renderer renderer.em = function (token: Tokens.Em) { const text = this.parser.parseInline(token.tokens); return `${text}`; }; // Custom code block renderer - return content as-is since marked-highlight handles it renderer.code = function (token: Tokens.Code) { return token.text; // marked-highlight has already processed this }; // Custom table renderer renderer.table = function (token: Tokens.Table) { const header = token.header .map((cell: Tokens.TableCell, i: number) => { const text = this.parser.parseInline(cell.tokens); const align = token.align[i] ? ` style="text-align: ${token.align[i]}"` : ""; return `${text}`; }) .join(""); const rows = token.rows .map((row: Tokens.TableCell[]) => { const cells = row .map((cell: Tokens.TableCell, i: number) => { const text = this.parser.parseInline(cell.tokens); const align = token.align[i] ? ` style="text-align: ${token.align[i]}"` : ""; return `${text}`; }) .join(""); return `${cells}`; }) .join(""); return `
    ${header} ${rows}
    `; }; // Set options and use the custom renderer marked.setOptions({ renderer: renderer, gfm: true, breaks: false, // This helps with multi-line list items pedantic: false, }); // Parse markdown to HTML using marked (this handles multi-line list items properly) const html = await marked.parse(markdown); return html; } async function handleDocsIndex(request: Request): Promise { const { currentUser } = await withAuth(request); return renderHTML( ); } async function handleDocsPage(request: Request): Promise { const { currentUser } = await withAuth(request); const url = new URL(request.url); const slug = url.pathname.split("/")[2]; // /docs/{slug} if (!slug) { // Redirect to docs index return new Response(null, { status: 302, headers: { Location: "/docs" }, }); } // Check if slug is valid const docInfo = AVAILABLE_DOCS.find((doc) => doc.slug === slug); if (!docInfo) { return new Response("Documentation page not found", { status: 404 }); } // Read markdown content const markdownContent = await readMarkdownFile(slug); if (!markdownContent) { return new Response("Documentation content not found", { status: 404 }); } // Convert to HTML with Shiki syntax highlighting const htmlContent = await markdownToHtml(markdownContent); // Extract headers for table of contents const headers = extractHeaders(htmlContent); return renderHTML( ); } // ============================================================================ // ROUTE EXPORTS // ============================================================================ export const docsRoutes: Route[] = [ // Docs index page { method: "GET", pattern: new URLPattern({ pathname: "/docs" }), handler: handleDocsIndex, }, // Individual docs pages { method: "GET", pattern: new URLPattern({ pathname: "/docs/:slug" }), handler: handleDocsPage, }, ];