@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,174 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test"
|
|
2
|
+
import { generateSitemap, generateLlmsTxt, generateLlmsFullTxt, generateSkillMd, buildCanonicalUrl, buildPageJsonLd } from "../src/seo"
|
|
3
|
+
|
|
4
|
+
describe("generateSitemap", () => {
|
|
5
|
+
it("generates valid XML with doc URLs", () => {
|
|
6
|
+
const docs = [{ slug: "index" }, { slug: "getting-started" }]
|
|
7
|
+
const xml = generateSitemap(docs, { baseUrl: "https://example.com" })
|
|
8
|
+
expect(xml).toContain('<?xml version="1.0"')
|
|
9
|
+
expect(xml).toContain("<urlset")
|
|
10
|
+
expect(xml).toContain("<loc>https://example.com/docs</loc>")
|
|
11
|
+
expect(xml).toContain("<loc>https://example.com/docs/getting-started</loc>")
|
|
12
|
+
expect(xml).toContain("<lastmod>")
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it("strips trailing slash from baseUrl", () => {
|
|
16
|
+
const docs = [{ slug: "intro" }]
|
|
17
|
+
const xml = generateSitemap(docs, { baseUrl: "https://example.com/" })
|
|
18
|
+
expect(xml).toContain("<loc>https://example.com/docs/intro</loc>")
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it("adds xhtml:link alternates for i18n", () => {
|
|
22
|
+
const docs = [{ slug: "index" }]
|
|
23
|
+
const xml = generateSitemap(docs, {
|
|
24
|
+
baseUrl: "https://example.com",
|
|
25
|
+
locales: [{ code: "en" }, { code: "fr" }],
|
|
26
|
+
defaultLocale: "en",
|
|
27
|
+
})
|
|
28
|
+
expect(xml).toContain('hreflang="en"')
|
|
29
|
+
expect(xml).toContain('hreflang="fr"')
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe("generateLlmsTxt", () => {
|
|
34
|
+
it("produces llms.txt format", () => {
|
|
35
|
+
const docs = [
|
|
36
|
+
{ slug: "intro", meta: { title: "Introduction", description: "Getting started", group: "Guide" } },
|
|
37
|
+
{ slug: "api", meta: { title: "API Reference", group: "Reference" } },
|
|
38
|
+
]
|
|
39
|
+
const txt = generateLlmsTxt(docs, {
|
|
40
|
+
title: "My Docs",
|
|
41
|
+
description: "Project documentation",
|
|
42
|
+
baseUrl: "https://example.com",
|
|
43
|
+
})
|
|
44
|
+
expect(txt).toContain("# My Docs")
|
|
45
|
+
expect(txt).toContain("> Project documentation")
|
|
46
|
+
expect(txt).toContain("## Guide")
|
|
47
|
+
expect(txt).toContain("- [Introduction](https://example.com/docs/intro): Getting started")
|
|
48
|
+
expect(txt).toContain("## Reference")
|
|
49
|
+
expect(txt).toContain("- [API Reference](https://example.com/docs/api)")
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("defaults group to Docs", () => {
|
|
53
|
+
const docs = [{ slug: "readme", meta: { title: "README" } }]
|
|
54
|
+
const txt = generateLlmsTxt(docs, {
|
|
55
|
+
title: "T",
|
|
56
|
+
description: "D",
|
|
57
|
+
baseUrl: "https://example.com",
|
|
58
|
+
})
|
|
59
|
+
expect(txt).toContain("## Docs")
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe("generateLlmsFullTxt", () => {
|
|
64
|
+
it("includes full text content", () => {
|
|
65
|
+
const searchIndex = [
|
|
66
|
+
{ slug: "intro", title: "Intro", group: "Guide", text: "Full content of the intro page." },
|
|
67
|
+
]
|
|
68
|
+
const txt = generateLlmsFullTxt(searchIndex, {
|
|
69
|
+
title: "My Docs",
|
|
70
|
+
description: "Docs",
|
|
71
|
+
baseUrl: "https://example.com",
|
|
72
|
+
})
|
|
73
|
+
expect(txt).toContain("# My Docs")
|
|
74
|
+
expect(txt).toContain("---")
|
|
75
|
+
expect(txt).toContain("## Intro")
|
|
76
|
+
expect(txt).toContain("URL: https://example.com/docs/intro")
|
|
77
|
+
expect(txt).toContain("Full content of the intro page.")
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe("buildCanonicalUrl", () => {
|
|
82
|
+
it("builds a canonical URL from baseUrl and slug", () => {
|
|
83
|
+
expect(buildCanonicalUrl("https://example.com", "getting-started")).toBe(
|
|
84
|
+
"https://example.com/docs/getting-started"
|
|
85
|
+
)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it("strips trailing slash from baseUrl", () => {
|
|
89
|
+
expect(buildCanonicalUrl("https://example.com/", "intro")).toBe(
|
|
90
|
+
"https://example.com/docs/intro"
|
|
91
|
+
)
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe("buildPageJsonLd", () => {
|
|
96
|
+
it("returns valid JSON with required fields", () => {
|
|
97
|
+
const json = buildPageJsonLd({
|
|
98
|
+
title: "Getting Started",
|
|
99
|
+
slug: "getting-started",
|
|
100
|
+
baseUrl: "https://example.com",
|
|
101
|
+
})
|
|
102
|
+
const ld = JSON.parse(json)
|
|
103
|
+
expect(ld["@context"]).toBe("https://schema.org")
|
|
104
|
+
expect(ld["@type"]).toBe("TechArticle")
|
|
105
|
+
expect(ld.headline).toBe("Getting Started")
|
|
106
|
+
expect(ld.url).toBe("https://example.com/docs/getting-started")
|
|
107
|
+
expect(ld.mainEntityOfPage).toEqual({
|
|
108
|
+
"@type": "WebPage",
|
|
109
|
+
"@id": "https://example.com/docs/getting-started",
|
|
110
|
+
})
|
|
111
|
+
expect(ld.isPartOf).toEqual({ "@type": "WebSite", url: "https://example.com" })
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("includes optional description and dateModified", () => {
|
|
115
|
+
const json = buildPageJsonLd({
|
|
116
|
+
title: "API",
|
|
117
|
+
slug: "api",
|
|
118
|
+
baseUrl: "https://example.com",
|
|
119
|
+
description: "API reference",
|
|
120
|
+
lastEdited: "2026-03-01T00:00:00Z",
|
|
121
|
+
})
|
|
122
|
+
const ld = JSON.parse(json)
|
|
123
|
+
expect(ld.description).toBe("API reference")
|
|
124
|
+
expect(ld.dateModified).toBe("2026-03-01T00:00:00Z")
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it("includes image when provided", () => {
|
|
128
|
+
const json = buildPageJsonLd({
|
|
129
|
+
title: "Intro",
|
|
130
|
+
slug: "intro",
|
|
131
|
+
baseUrl: "https://example.com",
|
|
132
|
+
image: "https://example.com/images/intro.png",
|
|
133
|
+
})
|
|
134
|
+
const ld = JSON.parse(json)
|
|
135
|
+
expect(ld.image).toBe("https://example.com/images/intro.png")
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it("omits image when not provided", () => {
|
|
139
|
+
const json = buildPageJsonLd({
|
|
140
|
+
title: "Intro",
|
|
141
|
+
slug: "intro",
|
|
142
|
+
baseUrl: "https://example.com",
|
|
143
|
+
})
|
|
144
|
+
const ld = JSON.parse(json)
|
|
145
|
+
expect(ld.image).toBeUndefined()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it("includes siteName in isPartOf when provided", () => {
|
|
149
|
+
const json = buildPageJsonLd({
|
|
150
|
+
title: "Intro",
|
|
151
|
+
slug: "intro",
|
|
152
|
+
baseUrl: "https://example.com",
|
|
153
|
+
siteName: "My Docs",
|
|
154
|
+
})
|
|
155
|
+
const ld = JSON.parse(json)
|
|
156
|
+
expect(ld.isPartOf.name).toBe("My Docs")
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe("generateSkillMd", () => {
|
|
161
|
+
it("generates skill.md with sections and MCP reference", () => {
|
|
162
|
+
const docs = [
|
|
163
|
+
{ slug: "intro", meta: { title: "Intro", group: "Guide" } },
|
|
164
|
+
{ slug: "api", meta: { title: "API", group: "Reference" } },
|
|
165
|
+
]
|
|
166
|
+
const md = generateSkillMd(docs, { title: "My Docs", description: "Project docs" })
|
|
167
|
+
expect(md).toContain("name: my-docs-docs")
|
|
168
|
+
expect(md).toContain("# My Docs Documentation")
|
|
169
|
+
expect(md).toContain("**Guide** (1 pages)")
|
|
170
|
+
expect(md).toContain("**Reference** (1 pages)")
|
|
171
|
+
expect(md).toContain("`search_docs`")
|
|
172
|
+
expect(md).toContain("`read_doc`")
|
|
173
|
+
})
|
|
174
|
+
})
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test"
|
|
2
|
+
import { tiramisuPlugin, buildSidebarTree, buildSectionSidebars, buildLocaleData } from "../src/vite"
|
|
3
|
+
import type { SidebarItem, SidebarSubgroup } from "../src/vite"
|
|
4
|
+
|
|
5
|
+
describe("tiramisuPlugin", () => {
|
|
6
|
+
it("returns a Vite plugin object", () => {
|
|
7
|
+
const plugin = tiramisuPlugin({ docsDir: "src/docs" })
|
|
8
|
+
expect(plugin.name).toBe("tiramisu-docs")
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it("has required hooks", () => {
|
|
12
|
+
const plugin = tiramisuPlugin({ docsDir: "src/docs" })
|
|
13
|
+
expect(typeof plugin.resolveId).toBe("function")
|
|
14
|
+
expect(typeof plugin.load).toBe("function")
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe("buildSidebarTree", () => {
|
|
19
|
+
it("flat docs with meta.group produce flat groups (backward compat)", () => {
|
|
20
|
+
const docs = [
|
|
21
|
+
{ slug: "intro", meta: { title: "Introduction", order: 1, group: "Getting Started" } },
|
|
22
|
+
{ slug: "install", meta: { title: "Installation", order: 2, group: "Getting Started" } },
|
|
23
|
+
{ slug: "basics", meta: { title: "Basics", order: 1, group: "Guide" } },
|
|
24
|
+
]
|
|
25
|
+
const result = buildSidebarTree(docs)
|
|
26
|
+
expect(result).toHaveLength(2)
|
|
27
|
+
expect(result[0].label).toBe("Getting Started")
|
|
28
|
+
expect(result[0].items).toHaveLength(2)
|
|
29
|
+
expect(result[0].items[0].type).toBe("item")
|
|
30
|
+
expect((result[0].items[0] as SidebarItem).title).toBe("Introduction")
|
|
31
|
+
expect((result[0].items[1] as SidebarItem).title).toBe("Installation")
|
|
32
|
+
expect(result[1].label).toBe("Guide")
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("folder-based grouping from nested files", () => {
|
|
36
|
+
const docs = [
|
|
37
|
+
{ slug: "getting-started/index", meta: { title: "Introduction", order: 1 } },
|
|
38
|
+
{ slug: "getting-started/quick-start", meta: { title: "Quick Start", order: 2 } },
|
|
39
|
+
]
|
|
40
|
+
const result = buildSidebarTree(docs)
|
|
41
|
+
expect(result).toHaveLength(1)
|
|
42
|
+
// Index page title becomes the group label; group has no slug
|
|
43
|
+
expect(result[0].label).toBe("Introduction")
|
|
44
|
+
expect(result[0]).not.toHaveProperty("slug")
|
|
45
|
+
// Non-index files are direct items in the group
|
|
46
|
+
expect(result[0].items).toHaveLength(1)
|
|
47
|
+
expect((result[0].items[0] as SidebarItem).title).toBe("Quick Start")
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("nested subgroups from deep folder structure", () => {
|
|
51
|
+
const docs = [
|
|
52
|
+
{ slug: "writing/markdown/math", meta: { title: "Math", order: 1 } },
|
|
53
|
+
{ slug: "writing/markdown/mermaid", meta: { title: "Mermaid", order: 2 } },
|
|
54
|
+
{ slug: "writing/page-meta", meta: { title: "Page Meta", order: 3 } },
|
|
55
|
+
]
|
|
56
|
+
const result = buildSidebarTree(docs)
|
|
57
|
+
expect(result).toHaveLength(1)
|
|
58
|
+
expect(result[0].label).toBe("Writing")
|
|
59
|
+
expect(result[0].items).toHaveLength(2)
|
|
60
|
+
|
|
61
|
+
// Subgroup should come first (lower order from children)
|
|
62
|
+
const subgroup = result[0].items[0] as SidebarSubgroup
|
|
63
|
+
expect(subgroup.type).toBe("subgroup")
|
|
64
|
+
expect(subgroup.label).toBe("Markdown")
|
|
65
|
+
expect(subgroup.items).toHaveLength(2)
|
|
66
|
+
expect((subgroup.items[0] as SidebarItem).title).toBe("Math")
|
|
67
|
+
|
|
68
|
+
// Direct item
|
|
69
|
+
const item = result[0].items[1] as SidebarItem
|
|
70
|
+
expect(item.type).toBe("item")
|
|
71
|
+
expect(item.title).toBe("Page Meta")
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("merges root file with meta.group into folder group (case-insensitive)", () => {
|
|
75
|
+
const docs = [
|
|
76
|
+
{ slug: "page-conventions", meta: { title: "Page Conventions", order: 1, group: "Writing" } },
|
|
77
|
+
{ slug: "writing/page-meta", meta: { title: "Page Meta", order: 2 } },
|
|
78
|
+
]
|
|
79
|
+
const result = buildSidebarTree(docs)
|
|
80
|
+
expect(result).toHaveLength(1)
|
|
81
|
+
expect(result[0].label).toBe("Writing")
|
|
82
|
+
expect(result[0].items).toHaveLength(2)
|
|
83
|
+
expect((result[0].items[0] as SidebarItem).title).toBe("Page Conventions")
|
|
84
|
+
expect((result[0].items[1] as SidebarItem).title).toBe("Page Meta")
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it("sorts by order at each level", () => {
|
|
88
|
+
const docs = [
|
|
89
|
+
{ slug: "guide/z-last", meta: { title: "Last", order: 10 } },
|
|
90
|
+
{ slug: "guide/a-first", meta: { title: "First", order: 1 } },
|
|
91
|
+
]
|
|
92
|
+
const result = buildSidebarTree(docs)
|
|
93
|
+
expect((result[0].items[0] as SidebarItem).title).toBe("First")
|
|
94
|
+
expect((result[0].items[1] as SidebarItem).title).toBe("Last")
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it("respects groupOrder config", () => {
|
|
98
|
+
const docs = [
|
|
99
|
+
{ slug: "alpha/page", meta: { title: "A Page", order: 1 } },
|
|
100
|
+
{ slug: "beta/page", meta: { title: "B Page", order: 1 } },
|
|
101
|
+
]
|
|
102
|
+
const result = buildSidebarTree(docs, ["Beta", "Alpha"])
|
|
103
|
+
expect(result[0].label).toBe("Beta")
|
|
104
|
+
expect(result[1].label).toBe("Alpha")
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it("propagates icon from meta to sidebar items", () => {
|
|
108
|
+
const docs = [
|
|
109
|
+
{ slug: "intro", meta: { title: "Intro", order: 1, group: "Guide", icon: "book-open" } },
|
|
110
|
+
]
|
|
111
|
+
const result = buildSidebarTree(docs)
|
|
112
|
+
expect((result[0].items[0] as SidebarItem).icon).toBe("book-open")
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("propagates icon from folder index to group", () => {
|
|
116
|
+
const docs = [
|
|
117
|
+
{ slug: "guide/index", meta: { title: "Guide", order: 1, icon: "compass" } },
|
|
118
|
+
{ slug: "guide/setup", meta: { title: "Setup", order: 2 } },
|
|
119
|
+
]
|
|
120
|
+
const result = buildSidebarTree(docs)
|
|
121
|
+
expect(result[0].icon).toBe("compass")
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it("propagates icon from subfolder index to subgroup", () => {
|
|
125
|
+
const docs = [
|
|
126
|
+
{ slug: "guide/advanced/index", meta: { title: "Advanced", order: 1, icon: "zap" } },
|
|
127
|
+
{ slug: "guide/advanced/tips", meta: { title: "Tips", order: 2 } },
|
|
128
|
+
]
|
|
129
|
+
const result = buildSidebarTree(docs)
|
|
130
|
+
const subgroup = result[0].items[0] as SidebarSubgroup
|
|
131
|
+
expect(subgroup.icon).toBe("zap")
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it("items without icon have undefined icon", () => {
|
|
135
|
+
const docs = [
|
|
136
|
+
{ slug: "readme", meta: { title: "README" } },
|
|
137
|
+
]
|
|
138
|
+
const result = buildSidebarTree(docs)
|
|
139
|
+
expect((result[0].items[0] as SidebarItem).icon).toBeUndefined()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it("defaults root files without meta.group to 'Docs'", () => {
|
|
143
|
+
const docs = [
|
|
144
|
+
{ slug: "readme", meta: { title: "README" } },
|
|
145
|
+
]
|
|
146
|
+
const result = buildSidebarTree(docs)
|
|
147
|
+
expect(result[0].label).toBe("Docs")
|
|
148
|
+
expect(result[0].items).toHaveLength(1)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it("subgroup index becomes the subgroup slug and label", () => {
|
|
152
|
+
const docs = [
|
|
153
|
+
{ slug: "guide/advanced/index", meta: { title: "Advanced Guide", order: 1 } },
|
|
154
|
+
{ slug: "guide/advanced/tips", meta: { title: "Tips", order: 2 } },
|
|
155
|
+
]
|
|
156
|
+
const result = buildSidebarTree(docs)
|
|
157
|
+
const subgroup = result[0].items[0] as SidebarSubgroup
|
|
158
|
+
expect(subgroup.type).toBe("subgroup")
|
|
159
|
+
expect(subgroup.slug).toBe("guide/advanced/index")
|
|
160
|
+
expect(subgroup.label).toBe("Advanced Guide")
|
|
161
|
+
expect(subgroup.order).toBe(1)
|
|
162
|
+
expect(subgroup.items).toHaveLength(1)
|
|
163
|
+
expect((subgroup.items[0] as SidebarItem).title).toBe("Tips")
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe("buildSectionSidebars", () => {
|
|
168
|
+
it("groups docs by section path", () => {
|
|
169
|
+
const docs = [
|
|
170
|
+
{ slug: "guides/intro", meta: { title: "Intro", order: 1 } },
|
|
171
|
+
{ slug: "guides/advanced", meta: { title: "Advanced", order: 2 } },
|
|
172
|
+
{ slug: "api/endpoints", meta: { title: "Endpoints", order: 1 } },
|
|
173
|
+
]
|
|
174
|
+
const sections = [
|
|
175
|
+
{ label: "Guides", path: "guides" },
|
|
176
|
+
{ label: "API", path: "api" },
|
|
177
|
+
]
|
|
178
|
+
const result = buildSectionSidebars(docs, sections)
|
|
179
|
+
expect(result).toHaveLength(2)
|
|
180
|
+
expect(result[0].label).toBe("Guides")
|
|
181
|
+
expect(result[0].sidebar).toHaveLength(1)
|
|
182
|
+
expect(result[0].sidebar![0].items).toHaveLength(2)
|
|
183
|
+
expect(result[1].label).toBe("API")
|
|
184
|
+
expect(result[1].sidebar).toHaveLength(1)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it("root-level docs go into implicit section", () => {
|
|
188
|
+
const docs = [
|
|
189
|
+
{ slug: "index", meta: { title: "Home", order: 1 } },
|
|
190
|
+
{ slug: "guides/intro", meta: { title: "Intro", order: 1 } },
|
|
191
|
+
]
|
|
192
|
+
const sections = [{ label: "Guides", path: "guides" }]
|
|
193
|
+
const result = buildSectionSidebars(docs, sections, "My Docs")
|
|
194
|
+
expect(result[0].label).toBe("My Docs")
|
|
195
|
+
expect(result[0].sidebar).toHaveLength(1)
|
|
196
|
+
expect(result[1].label).toBe("Guides")
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it("dropdown sections pass through with children sidebars", () => {
|
|
200
|
+
const docs = [
|
|
201
|
+
{ slug: "api/endpoints", meta: { title: "Endpoints", order: 1 } },
|
|
202
|
+
{ slug: "cli/commands", meta: { title: "Commands", order: 1 } },
|
|
203
|
+
]
|
|
204
|
+
const sections = [
|
|
205
|
+
{ label: "Reference", children: [
|
|
206
|
+
{ label: "API", path: "api" },
|
|
207
|
+
{ label: "CLI", path: "cli" },
|
|
208
|
+
]},
|
|
209
|
+
]
|
|
210
|
+
const result = buildSectionSidebars(docs, sections)
|
|
211
|
+
expect(result).toHaveLength(1)
|
|
212
|
+
expect(result[0].label).toBe("Reference")
|
|
213
|
+
expect(result[0].children).toHaveLength(2)
|
|
214
|
+
expect(result[0].children![0].sidebar).toHaveLength(1)
|
|
215
|
+
expect(result[0].children![1].sidebar).toHaveLength(1)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it("nested docs within a section preserve full hierarchy (no prefix stripping)", () => {
|
|
219
|
+
const docs = [
|
|
220
|
+
{ slug: "writing/index", meta: { title: "Writing", order: 1 } },
|
|
221
|
+
{ slug: "writing/markup-basics", meta: { title: "Markup Basics", order: 2 } },
|
|
222
|
+
{ slug: "writing/content/index", meta: { title: "Content", order: 3 } },
|
|
223
|
+
{ slug: "writing/content/code-blocks", meta: { title: "Code Blocks", order: 4 } },
|
|
224
|
+
{ slug: "writing/page-meta", meta: { title: "Page Meta", order: 5 } },
|
|
225
|
+
]
|
|
226
|
+
const sections = [{ label: "Writing", path: "writing" }]
|
|
227
|
+
const result = buildSectionSidebars(docs, sections)
|
|
228
|
+
expect(result).toHaveLength(1)
|
|
229
|
+
expect(result[0].label).toBe("Writing")
|
|
230
|
+
|
|
231
|
+
const sidebar = result[0].sidebar!
|
|
232
|
+
expect(sidebar).toHaveLength(1)
|
|
233
|
+
|
|
234
|
+
const group = sidebar[0]
|
|
235
|
+
expect(group.label).toBe("Writing")
|
|
236
|
+
// Should have: Markup Basics (item), Content (subgroup), Page Meta (item)
|
|
237
|
+
expect(group.items).toHaveLength(3)
|
|
238
|
+
|
|
239
|
+
const markupBasics = group.items[0] as SidebarItem
|
|
240
|
+
expect(markupBasics.type).toBe("item")
|
|
241
|
+
expect(markupBasics.title).toBe("Markup Basics")
|
|
242
|
+
expect(markupBasics.slug).toBe("writing/markup-basics")
|
|
243
|
+
|
|
244
|
+
const contentSubgroup = group.items[1] as SidebarSubgroup
|
|
245
|
+
expect(contentSubgroup.type).toBe("subgroup")
|
|
246
|
+
expect(contentSubgroup.label).toBe("Content")
|
|
247
|
+
expect(contentSubgroup.slug).toBe("writing/content/index")
|
|
248
|
+
expect(contentSubgroup.items).toHaveLength(1)
|
|
249
|
+
expect((contentSubgroup.items[0] as SidebarItem).slug).toBe("writing/content/code-blocks")
|
|
250
|
+
|
|
251
|
+
const pageMeta = group.items[2] as SidebarItem
|
|
252
|
+
expect(pageMeta.type).toBe("item")
|
|
253
|
+
expect(pageMeta.title).toBe("Page Meta")
|
|
254
|
+
expect(pageMeta.slug).toBe("writing/page-meta")
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it("external href sections have no sidebar", () => {
|
|
258
|
+
const docs: { slug: string; meta: any }[] = []
|
|
259
|
+
const sections = [{ label: "Blog", href: "https://blog.example.com" }]
|
|
260
|
+
const result = buildSectionSidebars(docs, sections)
|
|
261
|
+
expect(result).toHaveLength(1)
|
|
262
|
+
expect(result[0].href).toBe("https://blog.example.com")
|
|
263
|
+
expect(result[0].sidebar).toBeUndefined()
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
describe("buildLocaleData", () => {
|
|
268
|
+
it("groups docs by locale prefix", () => {
|
|
269
|
+
const allDocs = [
|
|
270
|
+
{ slug: "en/intro", meta: { title: "Intro" }, headings: [], lastEdited: "2026-03-01T00:00:00.000Z" },
|
|
271
|
+
{ slug: "en/guide", meta: { title: "Guide" }, headings: [], lastEdited: "2026-03-01T00:00:00.000Z" },
|
|
272
|
+
{ slug: "fr/intro", meta: { title: "Introduction" }, headings: [], lastEdited: "2026-03-01T00:00:00.000Z" },
|
|
273
|
+
]
|
|
274
|
+
const locales = [
|
|
275
|
+
{ code: "en", label: "English" },
|
|
276
|
+
{ code: "fr", label: "Français" },
|
|
277
|
+
]
|
|
278
|
+
const result = buildLocaleData(allDocs, locales)
|
|
279
|
+
expect(result.en.docs).toHaveLength(2)
|
|
280
|
+
expect(result.en.docs[0].slug).toBe("intro")
|
|
281
|
+
expect(result.fr.docs).toHaveLength(1)
|
|
282
|
+
})
|
|
283
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"baseUrl": ".",
|
|
13
|
+
"paths": {
|
|
14
|
+
"$lib": ["./src/lib"],
|
|
15
|
+
"$lib/*": ["./src/lib/*"]
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"include": ["src"]
|
|
19
|
+
}
|