/// import { marked } from "marked"; import { markedHighlight } from "marked-highlight"; import { codeToHtml } from "shiki"; import type { Tokens } from "marked"; // 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 quick lookup 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 Shiki syntax highlighting async function markdownToHtml(markdown: string): Promise { // Configure marked with highlight extension for code blocks marked.use( markedHighlight({ async: true, async highlight(code, lang, _info) { const highlightedCode = await codeToHtml(code, { lang: lang || "text", theme: "ayu-dark", }); // Add padding and overflow to the pre element const styledCode = highlightedCode.replace( '
${styledCode}`;
      },
      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-xl font-semibold text-white mt-8 mb-4",
      2: "text-lg font-semibold text-white mt-8 mb-3",
      3: "text-base font-medium text-zinc-100 mt-6 mb-3",
      4: "text-sm font-medium text-zinc-200 mt-4 mb-2",
      5: "text-xs font-medium text-zinc-200 mt-4 mb-2",
      6: "text-xs font-medium text-zinc-300 mt-3 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 - just add styling classes renderer.list = function (token: Tokens.List) { const ordered = token.ordered; const tag = ordered ? "ol" : "ul"; const listStyle = ordered ? "list-decimal" : "list-disc"; // Use default marked behavior for rendering items let body = ""; for (let i = 0; i < token.items.length; i++) { body += this.listitem(token.items[i]); } return `<${tag} class="${listStyle} list-inside my-3 text-zinc-400">${body}`; }; renderer.listitem = function (token: Tokens.ListItem) { let text = ""; if (token.task) { const checkbox = ``; if (token.loose) { if (token.tokens.length > 0 && token.tokens[0].type === "paragraph") { token.tokens[0].text = checkbox + " " + token.tokens[0].text; if ( token.tokens[0].tokens && token.tokens[0].tokens.length > 0 && token.tokens[0].tokens[0].type === "text" ) { token.tokens[0].tokens[0].text = checkbox + " " + token.tokens[0].tokens[0].text; } } else { token.tokens.unshift( { type: "text", raw: checkbox + " ", text: checkbox + " " } as Tokens.Text, ); } } else { text += checkbox + " "; } } text += this.parser.parse(token.tokens, !!token.loose); return `
  • ${text}
  • `; }; // 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; } export function handleDocsIndex(_req: Request): Response { return Response.json({ categories: DOCS_CATEGORIES, }); } export async function handleDocsDetail(req: Request): Promise { const url = new URL(req.url); const slug = url.pathname.split("/")[3]; // /api/docs/{slug} if (!slug) { return Response.json({ error: "Missing slug" }, { status: 400 }); } // Check if slug is valid const docInfo = AVAILABLE_DOCS.find((doc) => doc.slug === slug); if (!docInfo) { return Response.json( { error: "Documentation page not found" }, { status: 404 }, ); } // Read markdown content const markdownContent = await readMarkdownFile(slug); if (!markdownContent) { return Response.json( { error: "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 Response.json({ title: docInfo.title, content: htmlContent, headers: headers, slug: slug, }); }