@tiramisu-docs/kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -0
- package/components.json +14 -0
- package/dist/bin/mcp.d.ts +2 -0
- package/dist/bin/mcp.js +4 -0
- package/dist/config.d.ts +99 -0
- package/dist/config.js +36 -0
- package/dist/highlight.d.ts +10 -0
- package/dist/highlight.js +93 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/lib/components/index.d.ts +16 -0
- package/dist/lib/components/index.js +18 -0
- package/dist/lib/components/tiramisu/lang-icons.d.ts +4 -0
- package/dist/lib/components/tiramisu/lang-icons.js +77 -0
- package/dist/lib/components/ui/alert/index.d.ts +5 -0
- package/dist/lib/components/ui/alert/index.js +6 -0
- package/dist/lib/components/ui/badge/index.d.ts +2 -0
- package/dist/lib/components/ui/badge/index.js +1 -0
- package/dist/lib/components/ui/button/index.d.ts +4 -0
- package/dist/lib/components/ui/button/index.js +2 -0
- package/dist/lib/components/ui/card/index.d.ts +8 -0
- package/dist/lib/components/ui/card/index.js +10 -0
- package/dist/lib/components/ui/collapsible/index.d.ts +1 -0
- package/dist/lib/components/ui/collapsible/index.js +1 -0
- package/dist/lib/components/ui/dropdown-menu/index.d.ts +18 -0
- package/dist/lib/components/ui/dropdown-menu/index.js +18 -0
- package/dist/lib/components/ui/scroll-area/index.d.ts +1 -0
- package/dist/lib/components/ui/scroll-area/index.js +1 -0
- package/dist/lib/components/ui/separator/index.d.ts +1 -0
- package/dist/lib/components/ui/separator/index.js +1 -0
- package/dist/lib/components/ui/sheet/index.d.ts +3 -0
- package/dist/lib/components/ui/sheet/index.js +3 -0
- package/dist/lib/components/ui/tabs/index.d.ts +5 -0
- package/dist/lib/components/ui/tabs/index.js +7 -0
- package/dist/lib/open-links.d.ts +22 -0
- package/dist/lib/open-links.js +33 -0
- package/dist/lib/routes/docs/[...slug]/+page.d.ts +25 -0
- package/dist/lib/routes/docs/[...slug]/+page.js +109 -0
- package/dist/lib/utils.d.ts +5 -0
- package/dist/lib/utils.js +5 -0
- package/dist/mcp.d.ts +24 -0
- package/dist/mcp.js +155 -0
- package/dist/scan.d.ts +15 -0
- package/dist/scan.js +72 -0
- package/dist/seo.d.ts +63 -0
- package/dist/seo.js +160 -0
- package/dist/tiramisu-grammar.d.ts +2 -0
- package/dist/tiramisu-grammar.js +77 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.js +1 -0
- package/dist/vite.d.ts +33 -0
- package/dist/vite.js +406 -0
- package/package.json +74 -0
- package/src/config.ts +133 -0
- package/src/highlight.ts +110 -0
- package/src/index.ts +6 -0
- package/src/lib/components/DocPage.svelte +430 -0
- package/src/lib/components/DocsLayout.svelte +145 -0
- package/src/lib/components/Footer.svelte +26 -0
- package/src/lib/components/Navbar.svelte +117 -0
- package/src/lib/components/PageFooter.svelte +63 -0
- package/src/lib/components/PrevNextNav.svelte +83 -0
- package/src/lib/components/SearchDialog.svelte +130 -0
- package/src/lib/components/Sidebar.svelte +237 -0
- package/src/lib/components/TableOfContents.svelte +50 -0
- package/src/lib/components/TopBar.svelte +407 -0
- package/src/lib/components/index.ts +19 -0
- package/src/lib/components/tiramisu/Accordion.svelte +16 -0
- package/src/lib/components/tiramisu/Badge.svelte +16 -0
- package/src/lib/components/tiramisu/Callout.svelte +26 -0
- package/src/lib/components/tiramisu/CodeBlock.svelte +56 -0
- package/src/lib/components/tiramisu/CodeTabs.svelte +123 -0
- package/src/lib/components/tiramisu/Demo.svelte +15 -0
- package/src/lib/components/tiramisu/FileTree.svelte +67 -0
- package/src/lib/components/tiramisu/MathBlock.svelte +26 -0
- package/src/lib/components/tiramisu/Mermaid.svelte +30 -0
- package/src/lib/components/tiramisu/NavCard.svelte +49 -0
- package/src/lib/components/tiramisu/Steps.svelte +60 -0
- package/src/lib/components/tiramisu/Tabs.svelte +87 -0
- package/src/lib/components/tiramisu/ZoomImage.svelte +114 -0
- package/src/lib/components/tiramisu/lang-icons.ts +81 -0
- package/src/lib/open-links.ts +50 -0
- package/src/lib/routes/docs/[...slug]/+page.svelte +26 -0
- package/src/lib/routes/docs/[...slug]/+page.ts +117 -0
- package/src/lib/styles/theme.css +222 -0
- package/src/lib/utils.ts +10 -0
- package/src/mcp.ts +180 -0
- package/src/scan.ts +92 -0
- package/src/seo.ts +193 -0
- package/src/tiramisu-grammar.ts +80 -0
- package/src/types.ts +71 -0
- package/src/virtual.d.ts +11 -0
- package/src/vite.ts +478 -0
- package/tests/config.test.ts +60 -0
- package/tests/mcp.test.ts +116 -0
- package/tests/scan.test.ts +48 -0
- package/tests/seo.test.ts +174 -0
- package/tests/vite.test.ts +283 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface OpenLinkOptions {
|
|
2
|
+
baseUrl: string
|
|
3
|
+
slug: string
|
|
4
|
+
locale?: string
|
|
5
|
+
github?: { repo: string; branch?: string; dir?: string }
|
|
6
|
+
mcp?: boolean | string
|
|
7
|
+
title?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface OpenLink {
|
|
11
|
+
label: string
|
|
12
|
+
href?: string
|
|
13
|
+
copy?: string
|
|
14
|
+
icon: "chatgpt" | "claude" | "cursor" | "github" | "mcp" | "vscode"
|
|
15
|
+
type: "open" | "edit" | "mcp"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getPageUrl(opts: OpenLinkOptions): string {
|
|
19
|
+
const localePart = opts.locale ? `${opts.locale}/` : ""
|
|
20
|
+
const slug = opts.slug === "index" ? "" : opts.slug.replace(/\/index$/, "")
|
|
21
|
+
return `${opts.baseUrl}/docs/${localePart}${slug}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getGitHubEditUrl(opts: OpenLinkOptions): string | null {
|
|
25
|
+
if (!opts.github) return null
|
|
26
|
+
const branch = opts.github.branch ?? "main"
|
|
27
|
+
const dir = opts.github.dir ?? "src/docs"
|
|
28
|
+
const localePart = opts.locale ? `${opts.locale}/` : ""
|
|
29
|
+
return `https://github.com/${opts.github.repo}/edit/${branch}/${dir}/${localePart}${opts.slug}.tiramisu`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getOpenLinks(opts: OpenLinkOptions): OpenLink[] {
|
|
33
|
+
const pageUrl = getPageUrl(opts)
|
|
34
|
+
const prompt = encodeURIComponent(`Read ${pageUrl} and answer questions about the content.`)
|
|
35
|
+
const cursorPrompt = encodeURIComponent(`Read ${pageUrl}, I want to ask questions about it.`)
|
|
36
|
+
const links: OpenLink[] = [
|
|
37
|
+
{ label: "Open in ChatGPT", href: `https://chat.openai.com/?q=${prompt}`, icon: "chatgpt", type: "open" },
|
|
38
|
+
{ label: "Open in Claude", href: `https://claude.ai/new?q=${prompt}`, icon: "claude", type: "open" },
|
|
39
|
+
{ label: "Open in Cursor", href: `https://cursor.com/link/prompt?text=${cursorPrompt}`, icon: "cursor", type: "open" },
|
|
40
|
+
]
|
|
41
|
+
if (opts.mcp) {
|
|
42
|
+
const mcpUrl = typeof opts.mcp === "string" ? opts.mcp : `${opts.baseUrl}/mcp`
|
|
43
|
+
links.push({ label: "Connect with MCP", copy: mcpUrl, icon: "mcp", type: "mcp" })
|
|
44
|
+
const mcpMeta = JSON.stringify({ name: opts.title ?? "Documentation", url: mcpUrl })
|
|
45
|
+
links.push({ label: "Connect to VSCode", href: `vscode:mcp/install?${encodeURIComponent(mcpMeta)}`, icon: "vscode", type: "mcp" })
|
|
46
|
+
}
|
|
47
|
+
const ghUrl = getGitHubEditUrl(opts)
|
|
48
|
+
if (ghUrl) links.push({ label: "Edit on GitHub", href: ghUrl, icon: "github", type: "edit" })
|
|
49
|
+
return links
|
|
50
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { DocsLayout, DocPage } from "$lib/components";
|
|
3
|
+
|
|
4
|
+
let { data }: { data: Record<string, any> } = $props();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<DocsLayout
|
|
8
|
+
config={data.config}
|
|
9
|
+
sidebar={data.activeSidebar}
|
|
10
|
+
headings={data.headings}
|
|
11
|
+
sections={data.sections}
|
|
12
|
+
locale={data.locale}
|
|
13
|
+
locales={data.locales}
|
|
14
|
+
showFallbackBanner={data.showFallbackBanner}
|
|
15
|
+
>
|
|
16
|
+
<DocPage
|
|
17
|
+
meta={data.meta}
|
|
18
|
+
lastEdited={data.lastEdited}
|
|
19
|
+
slug={data.slug}
|
|
20
|
+
baseUrl={data.config?.url}
|
|
21
|
+
sidebar={data.activeSidebar}
|
|
22
|
+
siteName={data.config?.title}
|
|
23
|
+
>
|
|
24
|
+
<svelte:component this={data.component} />
|
|
25
|
+
</DocPage>
|
|
26
|
+
</DocsLayout>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { error, redirect } from "@sveltejs/kit";
|
|
2
|
+
import type { VirtualModule, LocaleData, ResolvedSection, SidebarGroup } from "../../../../types.js";
|
|
3
|
+
|
|
4
|
+
export async function load({ params }: { params: { slug?: string } }) {
|
|
5
|
+
const rawSlug = params.slug || "index";
|
|
6
|
+
const mod: VirtualModule = await import("virtual:tiramisu-docs");
|
|
7
|
+
|
|
8
|
+
// If i18n is enabled, parse locale from slug
|
|
9
|
+
if (mod.locales && mod.defaultLocale) {
|
|
10
|
+
const segments = rawSlug.split("/");
|
|
11
|
+
const possibleLocale = segments[0];
|
|
12
|
+
const localeData = mod.locales[possibleLocale];
|
|
13
|
+
|
|
14
|
+
if (localeData) {
|
|
15
|
+
const locale = possibleLocale;
|
|
16
|
+
const slug = segments.slice(1).join("/") || "index";
|
|
17
|
+
return loadDoc(localeData, slug, locale, mod);
|
|
18
|
+
} else {
|
|
19
|
+
// No locale prefix — redirect to default locale
|
|
20
|
+
throw redirect(302, `/docs/${mod.defaultLocale}/${rawSlug}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// No i18n — legacy behavior
|
|
25
|
+
return loadLegacy(mod, rawSlug);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadLegacy(mod: VirtualModule, slug: string) {
|
|
29
|
+
let importFn = mod.docImports[slug] ?? mod.docImports[slug + "/index"];
|
|
30
|
+
|
|
31
|
+
// Root /docs with no page — redirect to first section
|
|
32
|
+
if (!importFn && slug === "index" && mod.sections) {
|
|
33
|
+
const firstSection = mod.sections.find((s) => s.path);
|
|
34
|
+
if (firstSection) throw redirect(302, `/docs/${firstSection.path}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!importFn) throw error(404, "Page not found");
|
|
38
|
+
if (!mod.docImports[slug]) slug = slug + "/index";
|
|
39
|
+
|
|
40
|
+
return importFn().then((c) => {
|
|
41
|
+
const doc = mod.docs.find((d) => d.slug === slug);
|
|
42
|
+
let activeSidebar: SidebarGroup[] = mod.sidebar;
|
|
43
|
+
if (mod.sections) {
|
|
44
|
+
activeSidebar = findActiveSectionSidebar(mod.sections, slug) ?? mod.sidebar;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
component: c.default ?? c,
|
|
48
|
+
meta: doc?.meta ?? {},
|
|
49
|
+
headings: doc?.headings ?? [],
|
|
50
|
+
lastEdited: doc?.lastEdited,
|
|
51
|
+
slug,
|
|
52
|
+
sections: mod.sections ?? undefined,
|
|
53
|
+
activeSidebar,
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function loadDoc(localeData: LocaleData, slug: string, locale: string, mod: VirtualModule) {
|
|
59
|
+
type ImportFn = () => Promise<{ default: any }>;
|
|
60
|
+
let importFn: ImportFn | undefined = localeData.docImports[slug] ?? localeData.docImports[slug + "/index"];
|
|
61
|
+
if (!localeData.docImports[slug] && localeData.docImports[slug + "/index"]) slug = slug + "/index";
|
|
62
|
+
let showFallbackBanner = false;
|
|
63
|
+
|
|
64
|
+
if (!importFn) {
|
|
65
|
+
const defaultData = mod.locales?.[mod.defaultLocale!];
|
|
66
|
+
importFn = defaultData?.docImports[slug] ?? defaultData?.docImports[slug + "/index"];
|
|
67
|
+
if (!defaultData?.docImports[slug] && defaultData?.docImports[slug + "/index"]) slug = slug + "/index";
|
|
68
|
+
if (!importFn) {
|
|
69
|
+
// Root /docs/<locale> with no index page — redirect to first section
|
|
70
|
+
if (slug === "index" && localeData.sections) {
|
|
71
|
+
const firstSection = localeData.sections.find((s) => s.path);
|
|
72
|
+
if (firstSection) throw redirect(302, `/docs/${locale}/${firstSection.path}`);
|
|
73
|
+
}
|
|
74
|
+
throw error(404, "Page not found");
|
|
75
|
+
}
|
|
76
|
+
showFallbackBanner = true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const component = await importFn();
|
|
80
|
+
const doc = localeData.docs.find((d) => d.slug === slug)
|
|
81
|
+
?? mod.locales?.[mod.defaultLocale!]?.docs.find((d) => d.slug === slug);
|
|
82
|
+
|
|
83
|
+
const sections = localeData.sections;
|
|
84
|
+
let activeSidebar: SidebarGroup[] = localeData.sidebar;
|
|
85
|
+
if (sections) {
|
|
86
|
+
activeSidebar = findActiveSectionSidebar(sections, slug) ?? localeData.sidebar;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
component: component.default ?? component,
|
|
91
|
+
meta: doc?.meta ?? {},
|
|
92
|
+
headings: doc?.headings ?? [],
|
|
93
|
+
lastEdited: doc?.lastEdited,
|
|
94
|
+
slug,
|
|
95
|
+
locale,
|
|
96
|
+
locales: Object.keys(mod.locales!),
|
|
97
|
+
sections,
|
|
98
|
+
activeSidebar,
|
|
99
|
+
showFallbackBanner,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function findActiveSectionSidebar(sections: ResolvedSection[], slug: string): SidebarGroup[] | null {
|
|
104
|
+
for (const section of sections) {
|
|
105
|
+
if (section.path && (slug === section.path || slug.startsWith(section.path + "/"))) {
|
|
106
|
+
return section.sidebar ?? null;
|
|
107
|
+
}
|
|
108
|
+
if (section.children) {
|
|
109
|
+
const found = findActiveSectionSidebar(section.children, slug);
|
|
110
|
+
if (found) return found;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (sections.length > 0 && sections[0].sidebar && !sections[0].path) {
|
|
114
|
+
return sections[0].sidebar;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
@custom-variant dark (&:is(.dark *));
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--radius: 0.5rem;
|
|
5
|
+
--font-sans: 'Geist', ui-sans-serif, system-ui, sans-serif;
|
|
6
|
+
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
|
7
|
+
|
|
8
|
+
/* Zinc neutral palette */
|
|
9
|
+
--background: oklch(1 0 0);
|
|
10
|
+
--foreground: oklch(0.145 0 0);
|
|
11
|
+
--card: oklch(1 0 0);
|
|
12
|
+
--card-foreground: oklch(0.145 0 0);
|
|
13
|
+
--popover: oklch(1 0 0);
|
|
14
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
15
|
+
--primary: oklch(0.205 0 0);
|
|
16
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
17
|
+
--secondary: oklch(0.965 0.001 286.375);
|
|
18
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
19
|
+
--muted: oklch(0.965 0.001 286.375);
|
|
20
|
+
--muted-foreground: oklch(0.44 0.003 286.286);
|
|
21
|
+
--accent: oklch(0.965 0.001 286.375);
|
|
22
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
23
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
24
|
+
--border: oklch(0.922 0.004 286.32);
|
|
25
|
+
--input: oklch(0.922 0.004 286.32);
|
|
26
|
+
--ring: oklch(0.708 0.003 286.286);
|
|
27
|
+
|
|
28
|
+
/* Sidebar */
|
|
29
|
+
--sidebar: oklch(0.985 0 0);
|
|
30
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
31
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
32
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
33
|
+
--sidebar-accent: oklch(0.965 0.001 286.375);
|
|
34
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
35
|
+
--sidebar-border: oklch(0.922 0.004 286.32);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.dark {
|
|
39
|
+
--background: oklch(0.119 0 0);
|
|
40
|
+
--foreground: oklch(0.985 0 0);
|
|
41
|
+
--card: oklch(0.119 0 0);
|
|
42
|
+
--card-foreground: oklch(0.985 0 0);
|
|
43
|
+
--popover: oklch(0.119 0 0);
|
|
44
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
45
|
+
--primary: oklch(0.985 0 0);
|
|
46
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
47
|
+
--secondary: oklch(0.269 0.006 286.033);
|
|
48
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
49
|
+
--muted: oklch(0.269 0.006 286.033);
|
|
50
|
+
--muted-foreground: oklch(0.708 0.003 286.286);
|
|
51
|
+
--accent: oklch(0.269 0.006 286.033);
|
|
52
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
53
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
54
|
+
--border: oklch(0.24 0.005 286);
|
|
55
|
+
--input: oklch(0.24 0.005 286);
|
|
56
|
+
--ring: oklch(0.556 0.002 286.286);
|
|
57
|
+
|
|
58
|
+
--sidebar: oklch(0.15 0 0);
|
|
59
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
60
|
+
--sidebar-primary: oklch(0.985 0 0);
|
|
61
|
+
--sidebar-primary-foreground: oklch(0.205 0 0);
|
|
62
|
+
--sidebar-accent: oklch(0.269 0.006 286.033);
|
|
63
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
64
|
+
--sidebar-border: oklch(0.269 0.006 286.033);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* Tailwind v4 theme mapping */
|
|
68
|
+
@theme inline {
|
|
69
|
+
--color-background: var(--background);
|
|
70
|
+
--color-foreground: var(--foreground);
|
|
71
|
+
--color-card: var(--card);
|
|
72
|
+
--color-card-foreground: var(--card-foreground);
|
|
73
|
+
--color-popover: var(--popover);
|
|
74
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
75
|
+
--color-primary: var(--primary);
|
|
76
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
77
|
+
--color-secondary: var(--secondary);
|
|
78
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
79
|
+
--color-muted: var(--muted);
|
|
80
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
81
|
+
--color-accent: var(--accent);
|
|
82
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
83
|
+
--color-destructive: var(--destructive);
|
|
84
|
+
--color-border: var(--border);
|
|
85
|
+
--color-input: var(--input);
|
|
86
|
+
--color-ring: var(--ring);
|
|
87
|
+
--color-sidebar: var(--sidebar);
|
|
88
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
89
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
90
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
91
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
92
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
93
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
94
|
+
|
|
95
|
+
--font-family-sans: var(--font-sans);
|
|
96
|
+
--font-family-mono: var(--font-mono);
|
|
97
|
+
|
|
98
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
99
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
100
|
+
--radius-lg: var(--radius);
|
|
101
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Base styles */
|
|
105
|
+
* {
|
|
106
|
+
border-color: var(--border);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
body {
|
|
110
|
+
font-family: var(--font-sans);
|
|
111
|
+
background-color: var(--background);
|
|
112
|
+
color: var(--foreground);
|
|
113
|
+
-webkit-font-smoothing: antialiased;
|
|
114
|
+
-moz-osx-font-smoothing: grayscale;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Smooth scrolling with offset for anchors */
|
|
118
|
+
html {
|
|
119
|
+
scroll-behavior: smooth;
|
|
120
|
+
scroll-padding-top: 4rem;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* Custom scrollbar */
|
|
124
|
+
::-webkit-scrollbar {
|
|
125
|
+
width: 6px;
|
|
126
|
+
height: 6px;
|
|
127
|
+
}
|
|
128
|
+
::-webkit-scrollbar-track {
|
|
129
|
+
background: transparent;
|
|
130
|
+
}
|
|
131
|
+
::-webkit-scrollbar-thumb {
|
|
132
|
+
background: var(--border);
|
|
133
|
+
border-radius: 3px;
|
|
134
|
+
}
|
|
135
|
+
::-webkit-scrollbar-thumb:hover {
|
|
136
|
+
background: var(--muted-foreground);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* Selection */
|
|
140
|
+
::selection {
|
|
141
|
+
background: var(--primary);
|
|
142
|
+
color: var(--primary-foreground);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* Blockquote */
|
|
146
|
+
blockquote {
|
|
147
|
+
border-left: 3px solid var(--border);
|
|
148
|
+
margin: 1.5rem 0;
|
|
149
|
+
padding: 0.75rem 1rem;
|
|
150
|
+
color: var(--muted-foreground);
|
|
151
|
+
font-style: italic;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
blockquote footer {
|
|
155
|
+
margin-top: 0.5rem;
|
|
156
|
+
font-style: normal;
|
|
157
|
+
font-size: 0.875rem;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
blockquote cite {
|
|
161
|
+
color: var(--foreground);
|
|
162
|
+
font-weight: 500;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Task list */
|
|
166
|
+
.task-list {
|
|
167
|
+
list-style: none;
|
|
168
|
+
padding-left: 0;
|
|
169
|
+
margin: 1rem 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.task-list li {
|
|
173
|
+
display: flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
gap: 0.5rem;
|
|
176
|
+
padding: 0.25rem 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.task-checkbox {
|
|
180
|
+
display: inline-flex;
|
|
181
|
+
align-items: center;
|
|
182
|
+
justify-content: center;
|
|
183
|
+
width: 1rem;
|
|
184
|
+
height: 1rem;
|
|
185
|
+
border: 2px solid var(--border);
|
|
186
|
+
border-radius: 3px;
|
|
187
|
+
flex-shrink: 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.task-checkbox.checked {
|
|
191
|
+
background: var(--primary);
|
|
192
|
+
border-color: var(--primary);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.task-checkbox.checked::after {
|
|
196
|
+
content: "";
|
|
197
|
+
width: 0.375rem;
|
|
198
|
+
height: 0.625rem;
|
|
199
|
+
border: solid var(--primary-foreground);
|
|
200
|
+
border-width: 0 2px 2px 0;
|
|
201
|
+
transform: rotate(45deg) translateY(-1px);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* Columns */
|
|
205
|
+
.columns {
|
|
206
|
+
display: grid;
|
|
207
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
208
|
+
gap: 1.5rem;
|
|
209
|
+
margin: 1.5rem 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.column {
|
|
213
|
+
min-width: 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* Cards grid */
|
|
217
|
+
.cards-grid {
|
|
218
|
+
display: grid;
|
|
219
|
+
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
220
|
+
gap: 1rem;
|
|
221
|
+
margin: 1.5rem 0;
|
|
222
|
+
}
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ClassValue, clsx } from "clsx"
|
|
2
|
+
import { twMerge } from "tailwind-merge"
|
|
3
|
+
|
|
4
|
+
export function cn(...inputs: ClassValue[]) {
|
|
5
|
+
return twMerge(clsx(inputs))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type WithElementRef<T, El extends HTMLElement = HTMLElement> = T & {
|
|
9
|
+
ref?: El | null
|
|
10
|
+
}
|
package/src/mcp.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { VirtualDoc, SearchIndexEntry, SidebarGroup } from "./types.js"
|
|
2
|
+
|
|
3
|
+
export interface McpData {
|
|
4
|
+
docs: VirtualDoc[]
|
|
5
|
+
searchIndex: SearchIndexEntry[]
|
|
6
|
+
sidebar: SidebarGroup[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface JsonRpcRequest {
|
|
10
|
+
jsonrpc: "2.0"
|
|
11
|
+
id?: string | number
|
|
12
|
+
method: string
|
|
13
|
+
params?: Record<string, unknown>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface JsonRpcResponse {
|
|
17
|
+
jsonrpc: "2.0"
|
|
18
|
+
id?: string | number | null
|
|
19
|
+
result?: unknown
|
|
20
|
+
error?: { code: number; message: string; data?: unknown }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const TOOL_DEFINITIONS = [
|
|
24
|
+
{
|
|
25
|
+
name: "search_docs",
|
|
26
|
+
description: "Search documentation pages by keyword",
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: "object" as const,
|
|
29
|
+
properties: {
|
|
30
|
+
query: { type: "string", description: "Search query" },
|
|
31
|
+
limit: { type: "number", description: "Max results (default 10)" },
|
|
32
|
+
},
|
|
33
|
+
required: ["query"],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "read_doc",
|
|
38
|
+
description: "Read a specific documentation page by slug",
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: "object" as const,
|
|
41
|
+
properties: {
|
|
42
|
+
slug: { type: "string", description: "Page slug" },
|
|
43
|
+
},
|
|
44
|
+
required: ["slug"],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "list_pages",
|
|
49
|
+
description: "List documentation pages, optionally filtered by section",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object" as const,
|
|
52
|
+
properties: {
|
|
53
|
+
section: { type: "string", description: "Filter by section name" },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "list_sections",
|
|
59
|
+
description: "List all documentation sections with page counts",
|
|
60
|
+
inputSchema: { type: "object" as const, properties: {} },
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "get_table_of_contents",
|
|
64
|
+
description: "Get headings for a documentation page",
|
|
65
|
+
inputSchema: {
|
|
66
|
+
type: "object" as const,
|
|
67
|
+
properties: {
|
|
68
|
+
slug: { type: "string", description: "Page slug" },
|
|
69
|
+
},
|
|
70
|
+
required: ["slug"],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
function toolResult(data: unknown) {
|
|
76
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function toolError(message: string) {
|
|
80
|
+
return { content: [{ type: "text", text: message }], isError: true }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function callTool(name: string, args: Record<string, unknown>, data: McpData) {
|
|
84
|
+
switch (name) {
|
|
85
|
+
case "search_docs": {
|
|
86
|
+
const query = String(args.query ?? "")
|
|
87
|
+
const limit = Number(args.limit ?? 10)
|
|
88
|
+
const terms = query.toLowerCase().split(/\s+/)
|
|
89
|
+
const scored = data.searchIndex
|
|
90
|
+
.map((entry) => {
|
|
91
|
+
const haystack = `${entry.title} ${entry.headings} ${entry.text}`.toLowerCase()
|
|
92
|
+
const score = terms.reduce((s, t) => s + (haystack.includes(t) ? 1 : 0), 0)
|
|
93
|
+
return { entry, score }
|
|
94
|
+
})
|
|
95
|
+
.filter((r) => r.score > 0)
|
|
96
|
+
.sort((a, b) => b.score - a.score)
|
|
97
|
+
.slice(0, limit)
|
|
98
|
+
return toolResult(scored.map((r) => ({
|
|
99
|
+
title: r.entry.title,
|
|
100
|
+
slug: r.entry.slug,
|
|
101
|
+
snippet: r.entry.text.slice(0, 200),
|
|
102
|
+
})))
|
|
103
|
+
}
|
|
104
|
+
case "read_doc": {
|
|
105
|
+
const slug = String(args.slug ?? "")
|
|
106
|
+
const doc = data.docs.find((d) => d.slug === slug)
|
|
107
|
+
const idx = data.searchIndex.find((e) => e.slug === slug)
|
|
108
|
+
if (!doc || !idx) return toolError(`Page not found: ${slug}`)
|
|
109
|
+
return toolResult({
|
|
110
|
+
title: doc.meta.title ?? doc.slug,
|
|
111
|
+
description: doc.meta.description ?? "",
|
|
112
|
+
content: idx.text,
|
|
113
|
+
headings: doc.headings,
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
case "list_pages": {
|
|
117
|
+
const section = args.section ? String(args.section).toLowerCase() : ""
|
|
118
|
+
let filtered = data.searchIndex
|
|
119
|
+
if (section) {
|
|
120
|
+
filtered = filtered.filter((e) => e.group.toLowerCase().includes(section))
|
|
121
|
+
}
|
|
122
|
+
return toolResult(filtered.map((e) => ({
|
|
123
|
+
title: e.title,
|
|
124
|
+
slug: e.slug,
|
|
125
|
+
description: data.docs.find((d) => d.slug === e.slug)?.meta.description ?? "",
|
|
126
|
+
})))
|
|
127
|
+
}
|
|
128
|
+
case "list_sections": {
|
|
129
|
+
const groups = new Map<string, number>()
|
|
130
|
+
for (const entry of data.searchIndex) {
|
|
131
|
+
groups.set(entry.group, (groups.get(entry.group) ?? 0) + 1)
|
|
132
|
+
}
|
|
133
|
+
return toolResult(Array.from(groups.entries()).map(([label, pageCount]) => ({
|
|
134
|
+
label,
|
|
135
|
+
path: label.toLowerCase().replace(/ > /g, "/").replace(/ /g, "-"),
|
|
136
|
+
pageCount,
|
|
137
|
+
})))
|
|
138
|
+
}
|
|
139
|
+
case "get_table_of_contents": {
|
|
140
|
+
const slug = String(args.slug ?? "")
|
|
141
|
+
const doc = data.docs.find((d) => d.slug === slug)
|
|
142
|
+
if (!doc) return toolError(`Page not found: ${slug}`)
|
|
143
|
+
return toolResult(doc.headings.map((h) => ({ level: h.level, text: h.text, id: h.id })))
|
|
144
|
+
}
|
|
145
|
+
default:
|
|
146
|
+
return toolError(`Unknown tool: ${name}`)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function handleMcpRequest(body: JsonRpcRequest, data: McpData): JsonRpcResponse {
|
|
151
|
+
const { method, id, params } = body
|
|
152
|
+
|
|
153
|
+
switch (method) {
|
|
154
|
+
case "initialize":
|
|
155
|
+
return {
|
|
156
|
+
jsonrpc: "2.0",
|
|
157
|
+
id: id ?? null,
|
|
158
|
+
result: {
|
|
159
|
+
protocolVersion: "2024-11-05",
|
|
160
|
+
capabilities: { tools: {} },
|
|
161
|
+
serverInfo: { name: "tiramisu-docs", version: "0.1.0" },
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
case "notifications/initialized":
|
|
165
|
+
return { jsonrpc: "2.0", id: id ?? null, result: {} }
|
|
166
|
+
case "tools/list":
|
|
167
|
+
return { jsonrpc: "2.0", id: id ?? null, result: { tools: TOOL_DEFINITIONS } }
|
|
168
|
+
case "tools/call": {
|
|
169
|
+
const name = String((params as any)?.name ?? "")
|
|
170
|
+
const args = ((params as any)?.arguments ?? {}) as Record<string, unknown>
|
|
171
|
+
return { jsonrpc: "2.0", id: id ?? null, result: callTool(name, args, data) }
|
|
172
|
+
}
|
|
173
|
+
default:
|
|
174
|
+
return {
|
|
175
|
+
jsonrpc: "2.0",
|
|
176
|
+
id: id ?? null,
|
|
177
|
+
error: { code: -32601, message: `Method not found: ${method}` },
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
package/src/scan.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { compileTiramisu } from "@tiramisu-docs/core"
|
|
2
|
+
import type { DocMeta, Heading } from "@tiramisu-docs/core"
|
|
3
|
+
import fs from "node:fs"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
|
|
6
|
+
export interface ScannedDoc {
|
|
7
|
+
slug: string
|
|
8
|
+
meta: DocMeta
|
|
9
|
+
headings: Heading[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
import type { SearchIndexEntry } from "./types.js"
|
|
13
|
+
export type { SearchIndexEntry } from "./types.js"
|
|
14
|
+
|
|
15
|
+
export function findTiramisuFiles(dir: string): string[] {
|
|
16
|
+
const results: string[] = []
|
|
17
|
+
if (!fs.existsSync(dir)) return results
|
|
18
|
+
|
|
19
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const fullPath = path.join(dir, entry.name)
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
results.push(...findTiramisuFiles(fullPath))
|
|
24
|
+
} else if (entry.name.endsWith(".tiramisu")) {
|
|
25
|
+
results.push(fullPath)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return results
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function extractPlainText(html: string): string {
|
|
32
|
+
return html
|
|
33
|
+
.replace(/<script[\s\S]*?<\/script>/g, "")
|
|
34
|
+
.replace(/<[^>]+>/g, " ")
|
|
35
|
+
.replace(/&/g, "&")
|
|
36
|
+
.replace(/</g, "<")
|
|
37
|
+
.replace(/>/g, ">")
|
|
38
|
+
.replace(/"/g, '"')
|
|
39
|
+
.replace(/\s+/g, " ")
|
|
40
|
+
.trim()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function titleCase(slug: string): string {
|
|
44
|
+
return slug
|
|
45
|
+
.split("-")
|
|
46
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
47
|
+
.join(" ")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveSearchGroup(slug: string, meta: DocMeta): string {
|
|
51
|
+
const segments = slug.split("/")
|
|
52
|
+
if (segments.length === 1) return meta.group ?? "Docs"
|
|
53
|
+
return segments
|
|
54
|
+
.slice(0, -1)
|
|
55
|
+
.map(titleCase)
|
|
56
|
+
.join(" > ")
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function scanDocs(docsDir: string): {
|
|
60
|
+
docs: ScannedDoc[]
|
|
61
|
+
searchIndex: SearchIndexEntry[]
|
|
62
|
+
} {
|
|
63
|
+
const absDocsDir = path.resolve(docsDir)
|
|
64
|
+
const files = findTiramisuFiles(absDocsDir)
|
|
65
|
+
|
|
66
|
+
const docs: ScannedDoc[] = []
|
|
67
|
+
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
const source = fs.readFileSync(file, "utf-8")
|
|
70
|
+
const { meta, headings } = compileTiramisu(source)
|
|
71
|
+
const relativePath = path.relative(absDocsDir, file)
|
|
72
|
+
const slug = relativePath.replace(/\.tiramisu$/, "").replace(/\\/g, "/")
|
|
73
|
+
docs.push({ slug, meta, headings })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const searchIndex: SearchIndexEntry[] = docs.map((doc) => {
|
|
77
|
+
const file = path.resolve(absDocsDir, doc.slug + ".tiramisu")
|
|
78
|
+
const source = fs.readFileSync(file, "utf-8")
|
|
79
|
+
const { svelte } = compileTiramisu(source)
|
|
80
|
+
const text = extractPlainText(svelte)
|
|
81
|
+
return {
|
|
82
|
+
id: doc.slug,
|
|
83
|
+
title: doc.meta.title ?? doc.slug,
|
|
84
|
+
group: resolveSearchGroup(doc.slug, doc.meta),
|
|
85
|
+
slug: doc.slug,
|
|
86
|
+
headings: doc.headings.map((h) => h.text).join(" "),
|
|
87
|
+
text,
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
return { docs, searchIndex }
|
|
92
|
+
}
|