@levino/shipyard-docs 0.0.0-rc-20251122114952
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/astro/DocsEntry.astro +37 -0
- package/astro/Layout.astro +79 -0
- package/astro/index.ts +1 -0
- package/package.json +38 -0
- package/src/index.ts +38 -0
- package/src/sidebarEntries.test.ts +173 -0
- package/src/sidebarEntries.ts +120 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { i18n } from 'astro:config/server'
|
|
3
|
+
import { getCollection, render } from 'astro:content'
|
|
4
|
+
import Layout from './Layout.astro'
|
|
5
|
+
|
|
6
|
+
export async function getStaticPaths() {
|
|
7
|
+
const docs = await getCollection('docs')
|
|
8
|
+
|
|
9
|
+
const getParams = (slug: string) => {
|
|
10
|
+
if (i18n) {
|
|
11
|
+
const [locale, ...rest] = slug.split('/')
|
|
12
|
+
return {
|
|
13
|
+
slug: rest.length ? rest.join('/') : undefined,
|
|
14
|
+
locale,
|
|
15
|
+
}
|
|
16
|
+
} else {
|
|
17
|
+
// For non-i18n, treat the entire slug as the path
|
|
18
|
+
return {
|
|
19
|
+
slug: slug || undefined,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return docs.map((entry) => ({
|
|
25
|
+
params: getParams(entry.id),
|
|
26
|
+
props: { entry },
|
|
27
|
+
}))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 2. For your template, you can get the entry directly from the prop
|
|
31
|
+
const { entry } = Astro.props
|
|
32
|
+
const { Content, headings } = await render(entry)
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
<Layout headings={headings}>
|
|
36
|
+
<Content />
|
|
37
|
+
</Layout>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { i18n } from 'astro:config/server'
|
|
3
|
+
import { getCollection, render } from 'astro:content'
|
|
4
|
+
import type { NavigationTree } from '@levino/shipyard-base'
|
|
5
|
+
import { Breadcrumbs, TableOfContents } from '@levino/shipyard-base/components'
|
|
6
|
+
import BaseLayout from '@levino/shipyard-base/layouts/Page.astro'
|
|
7
|
+
import { Array as EffectArray, Option } from 'effect'
|
|
8
|
+
import { toSidebarEntries } from '../src/sidebarEntries'
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
headings?: { depth: number; text: string; slug: string }[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { headings = [] } = Astro.props
|
|
15
|
+
|
|
16
|
+
const getPath = (id: string) =>
|
|
17
|
+
i18n ? `/${Astro.currentLocale}/docs/${id.slice(3)}` : `/docs/${id}`
|
|
18
|
+
|
|
19
|
+
const docs = await getCollection('docs')
|
|
20
|
+
.then(
|
|
21
|
+
EffectArray.map(async (doc) => {
|
|
22
|
+
const {
|
|
23
|
+
id,
|
|
24
|
+
data: {
|
|
25
|
+
title,
|
|
26
|
+
sidebar: { render: shouldBeRendered, label },
|
|
27
|
+
sidebar_position,
|
|
28
|
+
sidebar_label,
|
|
29
|
+
sidebar_class_name,
|
|
30
|
+
sidebar_custom_props,
|
|
31
|
+
},
|
|
32
|
+
} = doc
|
|
33
|
+
return {
|
|
34
|
+
id,
|
|
35
|
+
path: getPath(id),
|
|
36
|
+
title:
|
|
37
|
+
label ??
|
|
38
|
+
title ??
|
|
39
|
+
Option.getOrUndefined(
|
|
40
|
+
EffectArray.findFirst(
|
|
41
|
+
(await render(doc)).headings,
|
|
42
|
+
({ depth }) => depth === 1,
|
|
43
|
+
),
|
|
44
|
+
)?.text ??
|
|
45
|
+
id,
|
|
46
|
+
link: shouldBeRendered,
|
|
47
|
+
sidebarPosition: sidebar_position,
|
|
48
|
+
sidebarLabel: sidebar_label,
|
|
49
|
+
sidebarClassName: sidebar_class_name,
|
|
50
|
+
sidebarCustomProps: sidebar_custom_props,
|
|
51
|
+
}
|
|
52
|
+
}),
|
|
53
|
+
)
|
|
54
|
+
.then((promises) => Promise.all(promises))
|
|
55
|
+
|
|
56
|
+
const fullTree = toSidebarEntries(docs)
|
|
57
|
+
|
|
58
|
+
const entries: NavigationTree =
|
|
59
|
+
i18n && Astro.currentLocale
|
|
60
|
+
? (fullTree[Astro.currentLocale]?.subEntry ?? {})
|
|
61
|
+
: fullTree
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
<BaseLayout sidebarNavigation={entries}>
|
|
65
|
+
<div class="grid grid-cols-12 gap-6 max-w-7xl mx-auto">
|
|
66
|
+
<div class="col-span-12 xl:col-span-9">
|
|
67
|
+
<div class="prose max-w-none">
|
|
68
|
+
<Breadcrumbs navigation={entries} />
|
|
69
|
+
<TableOfContents links={headings} class="xl:hidden" />
|
|
70
|
+
<slot />
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="hidden xl:block col-span-3">
|
|
74
|
+
<div class="sticky top-20 max-h-[calc(100vh-6rem)] overflow-y-auto">
|
|
75
|
+
<TableOfContents links={headings} desktopOnly />
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</BaseLayout>
|
package/astro/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as DocsEntry } from './DocsEntry.astro'
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@levino/shipyard-docs",
|
|
3
|
+
"version": "0.0.0-rc-20251122114952",
|
|
4
|
+
"description": "",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"keywords": [],
|
|
8
|
+
"author": "",
|
|
9
|
+
"license": "ISC",
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"astro": "^5.7"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"effect": "^3.12.5",
|
|
15
|
+
"ramda": "^0.31",
|
|
16
|
+
"@levino/shipyard-base": "0.0.0-rc-20251122114952"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@tailwindcss/typography": "^0.5.16",
|
|
20
|
+
"@types/ramda": "^0.31",
|
|
21
|
+
"astro": "^5.7",
|
|
22
|
+
"vitest": "^2.1.8"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/levino/shipyard"
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./src/index.ts",
|
|
30
|
+
"./layout": "./astro/Layout.astro",
|
|
31
|
+
"./astro": "./astro/index.ts",
|
|
32
|
+
"./astro/*": "./astro/*"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"astro",
|
|
36
|
+
"src"
|
|
37
|
+
]
|
|
38
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AstroIntegration } from 'astro'
|
|
2
|
+
|
|
3
|
+
import { z } from 'astro/zod'
|
|
4
|
+
|
|
5
|
+
export const docsSchema = z.object({
|
|
6
|
+
sidebar: z
|
|
7
|
+
.object({
|
|
8
|
+
render: z.boolean().default(true),
|
|
9
|
+
label: z.string().optional(),
|
|
10
|
+
})
|
|
11
|
+
.default({ render: true }),
|
|
12
|
+
title: z.string().optional(),
|
|
13
|
+
description: z.string().optional(),
|
|
14
|
+
sidebar_position: z.number().optional(),
|
|
15
|
+
sidebar_label: z.string().optional(),
|
|
16
|
+
sidebar_class_name: z.string().optional(),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export default (): AstroIntegration => ({
|
|
20
|
+
name: 'shipyard-docs',
|
|
21
|
+
hooks: {
|
|
22
|
+
'astro:config:setup': ({ injectRoute, config }) => {
|
|
23
|
+
if (config.i18n) {
|
|
24
|
+
// With i18n: use locale prefix
|
|
25
|
+
injectRoute({
|
|
26
|
+
pattern: `/[locale]/docs/[...slug]`,
|
|
27
|
+
entrypoint: `@levino/shipyard-docs/astro/DocsEntry.astro`,
|
|
28
|
+
})
|
|
29
|
+
} else {
|
|
30
|
+
// Without i18n: direct path
|
|
31
|
+
injectRoute({
|
|
32
|
+
pattern: `/docs/[...slug]`,
|
|
33
|
+
entrypoint: `@levino/shipyard-docs/astro/DocsEntry.astro`,
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
})
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { DocsData } from './sidebarEntries'
|
|
3
|
+
import { toSidebarEntries } from './sidebarEntries'
|
|
4
|
+
|
|
5
|
+
describe('toSidebarEntries', () => {
|
|
6
|
+
it('should create a basic sidebar structure from flat docs', () => {
|
|
7
|
+
const docs: DocsData[] = [
|
|
8
|
+
{
|
|
9
|
+
id: 'guide/intro.md',
|
|
10
|
+
title: 'Introduction',
|
|
11
|
+
path: '/docs/guide/intro',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: 'guide/advanced/topic.md',
|
|
15
|
+
title: 'Topic',
|
|
16
|
+
path: '/docs/guide/advanced/topic',
|
|
17
|
+
},
|
|
18
|
+
{ id: 'api.md', title: 'API', path: '/docs/api' },
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
const entries = toSidebarEntries(docs)
|
|
22
|
+
|
|
23
|
+
expect(entries).toMatchObject({
|
|
24
|
+
guide: {
|
|
25
|
+
label: 'guide',
|
|
26
|
+
subEntry: {
|
|
27
|
+
intro: { label: 'Introduction', href: '/docs/guide/intro' },
|
|
28
|
+
advanced: {
|
|
29
|
+
label: 'advanced',
|
|
30
|
+
subEntry: {
|
|
31
|
+
topic: { label: 'Topic', href: '/docs/guide/advanced/topic' },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
api: { label: 'API', href: '/docs/api' },
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should respect sidebar_label', () => {
|
|
41
|
+
const docs: DocsData[] = [
|
|
42
|
+
{
|
|
43
|
+
id: 'guide/intro.md',
|
|
44
|
+
title: 'Introduction',
|
|
45
|
+
path: '/docs/guide/intro',
|
|
46
|
+
sidebarLabel: 'Intro',
|
|
47
|
+
},
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
const entries = toSidebarEntries(docs)
|
|
51
|
+
expect(entries.guide.subEntry?.intro.label).toBe('Intro')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should respect sidebar_position', () => {
|
|
55
|
+
const docs: DocsData[] = [
|
|
56
|
+
{
|
|
57
|
+
id: 'guide/b.md',
|
|
58
|
+
title: 'B',
|
|
59
|
+
path: '/docs/guide/b',
|
|
60
|
+
sidebarPosition: 2,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'guide/a.md',
|
|
64
|
+
title: 'A',
|
|
65
|
+
path: '/docs/guide/a',
|
|
66
|
+
sidebarPosition: 1,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'guide/c.md',
|
|
70
|
+
title: 'C',
|
|
71
|
+
path: '/docs/guide/c',
|
|
72
|
+
},
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
const entries = toSidebarEntries(docs)
|
|
76
|
+
const guideSub = entries.guide.subEntry
|
|
77
|
+
const keys = Object.keys(guideSub || {})
|
|
78
|
+
|
|
79
|
+
// Items with explicit positions come first (1, 2), then items without position (Infinity) are sorted alphabetically
|
|
80
|
+
expect(keys).toEqual(['a', 'b', 'c'])
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should apply sidebar_class_name', () => {
|
|
84
|
+
const docs: DocsData[] = [
|
|
85
|
+
{
|
|
86
|
+
id: 'page.md',
|
|
87
|
+
title: 'Page',
|
|
88
|
+
path: '/docs/page',
|
|
89
|
+
sidebarClassName: 'special-page',
|
|
90
|
+
},
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
const entries = toSidebarEntries(docs)
|
|
94
|
+
expect(entries.page.className).toBe('special-page')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should handle index files correctly', () => {
|
|
98
|
+
const docs: DocsData[] = [
|
|
99
|
+
{ id: 'guide/index.md', title: 'Guide Index', path: '/docs/guide' },
|
|
100
|
+
{ id: 'guide/other.md', title: 'Other', path: '/docs/guide/other' },
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
const entries = toSidebarEntries(docs)
|
|
104
|
+
expect(entries.guide.href).toBe('/docs/guide')
|
|
105
|
+
expect(entries.guide.label).toBe('Guide Index')
|
|
106
|
+
expect(entries.guide.subEntry).toBeDefined()
|
|
107
|
+
expect(entries.guide.subEntry?.other).toBeDefined()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should sort alphabetically when positions match', () => {
|
|
111
|
+
const docs: DocsData[] = [
|
|
112
|
+
{ id: 'z.md', title: 'Zebra', path: '/z' },
|
|
113
|
+
{ id: 'a.md', title: 'Apple', path: '/a' },
|
|
114
|
+
]
|
|
115
|
+
const entries = toSidebarEntries(docs)
|
|
116
|
+
const keys = Object.keys(entries)
|
|
117
|
+
expect(keys).toEqual(['a', 'z'])
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should apply index.md sidebar_position to parent category for top-level sorting', () => {
|
|
121
|
+
const docs: DocsData[] = [
|
|
122
|
+
// "zebra" folder with index.md that has sidebar_position: 1
|
|
123
|
+
{
|
|
124
|
+
id: 'zebra/index.md',
|
|
125
|
+
title: 'Zebra Section',
|
|
126
|
+
path: '/docs/zebra',
|
|
127
|
+
sidebarPosition: 1,
|
|
128
|
+
},
|
|
129
|
+
{ id: 'zebra/page.md', title: 'Zebra Page', path: '/docs/zebra/page' },
|
|
130
|
+
// "apple" folder with no explicit position (defaults to Infinity)
|
|
131
|
+
{
|
|
132
|
+
id: 'apple/index.md',
|
|
133
|
+
title: 'Apple Section',
|
|
134
|
+
path: '/docs/apple',
|
|
135
|
+
},
|
|
136
|
+
{ id: 'apple/page.md', title: 'Apple Page', path: '/docs/apple/page' },
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
const entries = toSidebarEntries(docs)
|
|
140
|
+
const keys = Object.keys(entries)
|
|
141
|
+
|
|
142
|
+
// zebra has explicit position 1, apple defaults to Infinity
|
|
143
|
+
// So zebra (position 1) comes before apple (Infinity)
|
|
144
|
+
expect(keys).toEqual(['zebra', 'apple'])
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('should position category first when index.md has explicit sidebar_position', () => {
|
|
148
|
+
const docs: DocsData[] = [
|
|
149
|
+
// A category with index.md having sidebar_position: 0
|
|
150
|
+
{
|
|
151
|
+
id: 'sidebar-demo/index.md',
|
|
152
|
+
title: 'Sidebar Demo',
|
|
153
|
+
path: '/docs/sidebar-demo',
|
|
154
|
+
sidebarPosition: 0,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: 'sidebar-demo/custom-class.md',
|
|
158
|
+
title: 'Custom Class',
|
|
159
|
+
path: '/docs/sidebar-demo/custom-class',
|
|
160
|
+
},
|
|
161
|
+
// Other top-level items with no position (default Infinity)
|
|
162
|
+
{ id: 'garden-beds.md', title: 'Garden Beds', path: '/docs/garden-beds' },
|
|
163
|
+
{ id: 'harvesting.md', title: 'Harvesting', path: '/docs/harvesting' },
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
const entries = toSidebarEntries(docs)
|
|
167
|
+
const keys = Object.keys(entries)
|
|
168
|
+
|
|
169
|
+
// sidebar-demo has explicit position 0, others default to Infinity
|
|
170
|
+
// So sidebar-demo comes first, then others alphabetically
|
|
171
|
+
expect(keys).toEqual(['sidebar-demo', 'garden-beds', 'harvesting'])
|
|
172
|
+
})
|
|
173
|
+
})
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { Entry } from '@levino/shipyard-base'
|
|
2
|
+
|
|
3
|
+
export interface DocsData {
|
|
4
|
+
id: string
|
|
5
|
+
title: string
|
|
6
|
+
path: string
|
|
7
|
+
link?: boolean
|
|
8
|
+
sidebarPosition?: number
|
|
9
|
+
sidebarLabel?: string
|
|
10
|
+
sidebarClassName?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface TreeNode {
|
|
14
|
+
readonly key: string
|
|
15
|
+
readonly label: string
|
|
16
|
+
readonly href?: string
|
|
17
|
+
readonly position: number
|
|
18
|
+
readonly className?: string
|
|
19
|
+
readonly children: Readonly<Record<string, TreeNode>>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEFAULT_POSITION = Number.POSITIVE_INFINITY
|
|
23
|
+
|
|
24
|
+
const createLeafNode = (key: string, doc: DocsData): TreeNode => ({
|
|
25
|
+
key,
|
|
26
|
+
label: doc.sidebarLabel ?? doc.title ?? key,
|
|
27
|
+
href: doc.link !== false ? doc.path : undefined,
|
|
28
|
+
position: doc.sidebarPosition ?? DEFAULT_POSITION,
|
|
29
|
+
className: doc.sidebarClassName,
|
|
30
|
+
children: {},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const createBranchNode = (key: string): TreeNode => ({
|
|
34
|
+
key,
|
|
35
|
+
label: key,
|
|
36
|
+
position: DEFAULT_POSITION,
|
|
37
|
+
children: {},
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const mergeNodeWithDoc = (node: TreeNode, doc: DocsData): TreeNode => ({
|
|
41
|
+
...node,
|
|
42
|
+
label: doc.sidebarLabel ?? doc.title ?? node.label,
|
|
43
|
+
href: doc.link !== false ? doc.path : node.href,
|
|
44
|
+
position: doc.sidebarPosition ?? node.position,
|
|
45
|
+
className: doc.sidebarClassName ?? node.className,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const insertAtPath = (
|
|
49
|
+
root: Readonly<Record<string, TreeNode>>,
|
|
50
|
+
pathParts: readonly string[],
|
|
51
|
+
doc: DocsData,
|
|
52
|
+
): Readonly<Record<string, TreeNode>> => {
|
|
53
|
+
if (pathParts.length === 0) return root
|
|
54
|
+
|
|
55
|
+
const [head, ...tail] = pathParts
|
|
56
|
+
const existingNode = root[head]
|
|
57
|
+
|
|
58
|
+
if (tail.length === 0) {
|
|
59
|
+
const newNode = existingNode
|
|
60
|
+
? mergeNodeWithDoc(existingNode, doc)
|
|
61
|
+
: createLeafNode(head, doc)
|
|
62
|
+
return { ...root, [head]: newNode }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const currentNode = existingNode ?? createBranchNode(head)
|
|
66
|
+
const updatedChildren = insertAtPath(currentNode.children, tail, doc)
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
...root,
|
|
70
|
+
[head]: { ...currentNode, children: updatedChildren },
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const sortByPositionThenLabel = (
|
|
75
|
+
nodes: readonly TreeNode[],
|
|
76
|
+
): readonly TreeNode[] =>
|
|
77
|
+
[...nodes].sort((a, b) =>
|
|
78
|
+
a.position !== b.position
|
|
79
|
+
? a.position - b.position
|
|
80
|
+
: a.label.localeCompare(b.label),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const treeNodeToEntry = (node: TreeNode): Entry[string] => {
|
|
84
|
+
const childNodes = Object.values(node.children)
|
|
85
|
+
const sortedChildren = sortByPositionThenLabel(childNodes)
|
|
86
|
+
|
|
87
|
+
const subEntry =
|
|
88
|
+
sortedChildren.length > 0
|
|
89
|
+
? Object.fromEntries(
|
|
90
|
+
sortedChildren.map((child) => [child.key, treeNodeToEntry(child)]),
|
|
91
|
+
)
|
|
92
|
+
: undefined
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
label: node.label,
|
|
96
|
+
...(node.href && { href: node.href }),
|
|
97
|
+
...(node.className && { className: node.className }),
|
|
98
|
+
...(subEntry && { subEntry }),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const parseDocPath = (id: string): readonly string[] => {
|
|
103
|
+
const cleanId = id.replace(/\.[^/.]+$/, '')
|
|
104
|
+
const parts = cleanId.split('/')
|
|
105
|
+
const filename = parts[parts.length - 1]
|
|
106
|
+
return filename === 'index' ? parts.slice(0, -1) : parts
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const toSidebarEntries = (docs: readonly DocsData[]): Entry => {
|
|
110
|
+
const rootTree = docs.reduce<Readonly<Record<string, TreeNode>>>(
|
|
111
|
+
(acc, doc) => insertAtPath(acc, parseDocPath(doc.id), doc),
|
|
112
|
+
{},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
const sortedNodes = sortByPositionThenLabel(Object.values(rootTree))
|
|
116
|
+
|
|
117
|
+
return Object.fromEntries(
|
|
118
|
+
sortedNodes.map((node) => [node.key, treeNodeToEntry(node)]),
|
|
119
|
+
)
|
|
120
|
+
}
|