forked from
slices.network/slices
Highly ambitious ATProtocol AppView service and sdks
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 "&": "&",
87 "<": "<",
88 ">": ">",
89 """: '"',
90 "'": "'",
91 " ": " ",
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}