@levino/shipyard-docs 0.5.2 → 0.6.0-rc-20260106213612
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/Layout.astro +73 -23
- package/package.json +16 -6
- package/src/fallbacks.test.ts +136 -0
- package/src/fallbacks.ts +82 -0
- package/src/index.ts +177 -50
- package/src/pagination.test.ts +115 -15
- package/src/pagination.ts +44 -16
- package/src/schema.test.ts +467 -0
- package/src/sidebarEntries.test.ts +145 -7
- package/src/sidebarEntries.ts +30 -3
|
@@ -37,7 +37,7 @@ describe('toSidebarEntries', () => {
|
|
|
37
37
|
})
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
-
it('should respect
|
|
40
|
+
it('should respect sidebar.label', () => {
|
|
41
41
|
const docs: DocsData[] = [
|
|
42
42
|
{
|
|
43
43
|
id: 'guide/intro.md',
|
|
@@ -51,7 +51,7 @@ describe('toSidebarEntries', () => {
|
|
|
51
51
|
expect(entries.guide.subEntry?.intro.label).toBe('Intro')
|
|
52
52
|
})
|
|
53
53
|
|
|
54
|
-
it('should respect
|
|
54
|
+
it('should respect sidebar.position', () => {
|
|
55
55
|
const docs: DocsData[] = [
|
|
56
56
|
{
|
|
57
57
|
id: 'guide/b.md',
|
|
@@ -80,7 +80,7 @@ describe('toSidebarEntries', () => {
|
|
|
80
80
|
expect(keys).toEqual(['a', 'b', 'c'])
|
|
81
81
|
})
|
|
82
82
|
|
|
83
|
-
it('should apply
|
|
83
|
+
it('should apply sidebar.className', () => {
|
|
84
84
|
const docs: DocsData[] = [
|
|
85
85
|
{
|
|
86
86
|
id: 'page.md',
|
|
@@ -117,9 +117,9 @@ describe('toSidebarEntries', () => {
|
|
|
117
117
|
expect(keys).toEqual(['a', 'z'])
|
|
118
118
|
})
|
|
119
119
|
|
|
120
|
-
it('should apply index.md
|
|
120
|
+
it('should apply index.md sidebar.position to parent category for top-level sorting', () => {
|
|
121
121
|
const docs: DocsData[] = [
|
|
122
|
-
// "zebra" folder with index.md that has
|
|
122
|
+
// "zebra" folder with index.md that has sidebar.position: 1
|
|
123
123
|
{
|
|
124
124
|
id: 'zebra/index.md',
|
|
125
125
|
title: 'Zebra Section',
|
|
@@ -144,9 +144,9 @@ describe('toSidebarEntries', () => {
|
|
|
144
144
|
expect(keys).toEqual(['zebra', 'apple'])
|
|
145
145
|
})
|
|
146
146
|
|
|
147
|
-
it('should position category first when index.md has explicit
|
|
147
|
+
it('should position category first when index.md has explicit sidebar.position', () => {
|
|
148
148
|
const docs: DocsData[] = [
|
|
149
|
-
// A category with index.md having
|
|
149
|
+
// A category with index.md having sidebar.position: 0
|
|
150
150
|
{
|
|
151
151
|
id: 'sidebar-demo/index.md',
|
|
152
152
|
title: 'Sidebar Demo',
|
|
@@ -170,4 +170,142 @@ describe('toSidebarEntries', () => {
|
|
|
170
170
|
// So sidebar-demo comes first, then others alphabetically
|
|
171
171
|
expect(keys).toEqual(['sidebar-demo', 'garden-beds', 'harvesting'])
|
|
172
172
|
})
|
|
173
|
+
|
|
174
|
+
it('should include collapsible/collapsed in category entries', () => {
|
|
175
|
+
const docs: DocsData[] = [
|
|
176
|
+
{
|
|
177
|
+
id: 'guide/index.md',
|
|
178
|
+
title: 'Guide',
|
|
179
|
+
path: '/docs/guide',
|
|
180
|
+
collapsible: true,
|
|
181
|
+
collapsed: false,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: 'guide/intro.md',
|
|
185
|
+
title: 'Introduction',
|
|
186
|
+
path: '/docs/guide/intro',
|
|
187
|
+
},
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
const entries = toSidebarEntries(docs)
|
|
191
|
+
|
|
192
|
+
expect(entries.guide.collapsible).toBe(true)
|
|
193
|
+
expect(entries.guide.collapsed).toBe(false)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('should apply default collapsible/collapsed values when not specified', () => {
|
|
197
|
+
const docs: DocsData[] = [
|
|
198
|
+
{
|
|
199
|
+
id: 'guide/index.md',
|
|
200
|
+
title: 'Guide',
|
|
201
|
+
path: '/docs/guide',
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: 'guide/intro.md',
|
|
205
|
+
title: 'Introduction',
|
|
206
|
+
path: '/docs/guide/intro',
|
|
207
|
+
},
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
const entries = toSidebarEntries(docs)
|
|
211
|
+
|
|
212
|
+
// Default values: collapsible: true, collapsed: true
|
|
213
|
+
expect(entries.guide.collapsible).toBe(true)
|
|
214
|
+
expect(entries.guide.collapsed).toBe(true)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('should filter unlisted pages from sidebar', () => {
|
|
218
|
+
const docs: DocsData[] = [
|
|
219
|
+
{
|
|
220
|
+
id: 'guide/intro.md',
|
|
221
|
+
title: 'Introduction',
|
|
222
|
+
path: '/docs/guide/intro',
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: 'guide/hidden.md',
|
|
226
|
+
title: 'Hidden Page',
|
|
227
|
+
path: '/docs/guide/hidden',
|
|
228
|
+
unlisted: true,
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: 'guide/visible.md',
|
|
232
|
+
title: 'Visible Page',
|
|
233
|
+
path: '/docs/guide/visible',
|
|
234
|
+
},
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
const entries = toSidebarEntries(docs)
|
|
238
|
+
const guideSubKeys = Object.keys(entries.guide.subEntry || {})
|
|
239
|
+
|
|
240
|
+
// hidden should not be in the sidebar
|
|
241
|
+
expect(guideSubKeys).toContain('intro')
|
|
242
|
+
expect(guideSubKeys).toContain('visible')
|
|
243
|
+
expect(guideSubKeys).not.toContain('hidden')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('should create non-clickable entries for render: false (no href)', () => {
|
|
247
|
+
const docs: DocsData[] = [
|
|
248
|
+
{
|
|
249
|
+
id: 'guide/index.md',
|
|
250
|
+
title: 'Guide',
|
|
251
|
+
path: '/docs/guide',
|
|
252
|
+
link: false, // render: false translates to link: false
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
id: 'guide/intro.md',
|
|
256
|
+
title: 'Introduction',
|
|
257
|
+
path: '/docs/guide/intro',
|
|
258
|
+
},
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
const entries = toSidebarEntries(docs)
|
|
262
|
+
|
|
263
|
+
// Category should exist but not have an href
|
|
264
|
+
expect(entries.guide.label).toBe('Guide')
|
|
265
|
+
expect(entries.guide.href).toBeUndefined()
|
|
266
|
+
expect(entries.guide.subEntry?.intro.href).toBe('/docs/guide/intro')
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('should apply index.md metadata including collapsible state to parent category', () => {
|
|
270
|
+
const docs: DocsData[] = [
|
|
271
|
+
{
|
|
272
|
+
id: 'advanced/index.md',
|
|
273
|
+
title: 'Advanced Topics',
|
|
274
|
+
path: '/docs/advanced',
|
|
275
|
+
sidebarLabel: 'Advanced',
|
|
276
|
+
sidebarPosition: 5,
|
|
277
|
+
sidebarClassName: 'advanced-section',
|
|
278
|
+
collapsible: false,
|
|
279
|
+
collapsed: false,
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
id: 'advanced/topic1.md',
|
|
283
|
+
title: 'Topic 1',
|
|
284
|
+
path: '/docs/advanced/topic1',
|
|
285
|
+
},
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
const entries = toSidebarEntries(docs)
|
|
289
|
+
|
|
290
|
+
expect(entries.advanced.label).toBe('Advanced')
|
|
291
|
+
expect(entries.advanced.className).toBe('advanced-section')
|
|
292
|
+
expect(entries.advanced.collapsible).toBe(false)
|
|
293
|
+
expect(entries.advanced.collapsed).toBe(false)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('should not include collapsible/collapsed for leaf nodes', () => {
|
|
297
|
+
const docs: DocsData[] = [
|
|
298
|
+
{
|
|
299
|
+
id: 'page.md',
|
|
300
|
+
title: 'Page',
|
|
301
|
+
path: '/docs/page',
|
|
302
|
+
},
|
|
303
|
+
]
|
|
304
|
+
|
|
305
|
+
const entries = toSidebarEntries(docs)
|
|
306
|
+
|
|
307
|
+
// Leaf nodes should not have collapsible/collapsed properties
|
|
308
|
+
expect(entries.page.collapsible).toBeUndefined()
|
|
309
|
+
expect(entries.page.collapsed).toBeUndefined()
|
|
310
|
+
})
|
|
173
311
|
})
|
package/src/sidebarEntries.ts
CHANGED
|
@@ -2,14 +2,21 @@ import type { Entry } from '@levino/shipyard-base'
|
|
|
2
2
|
|
|
3
3
|
export interface DocsData {
|
|
4
4
|
id: string
|
|
5
|
+
/** Custom document ID from frontmatter (for referencing in pagination) */
|
|
6
|
+
customId?: string
|
|
5
7
|
title: string
|
|
6
8
|
path: string
|
|
7
9
|
link?: boolean
|
|
8
10
|
sidebarPosition?: number
|
|
9
11
|
sidebarLabel?: string
|
|
10
12
|
sidebarClassName?: string
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
sidebarCustomProps?: Record<string, unknown>
|
|
14
|
+
collapsible?: boolean
|
|
15
|
+
collapsed?: boolean
|
|
16
|
+
unlisted?: boolean
|
|
17
|
+
paginationLabel?: string
|
|
18
|
+
paginationNext?: string | null
|
|
19
|
+
paginationPrev?: string | null
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
interface TreeNode {
|
|
@@ -18,6 +25,9 @@ interface TreeNode {
|
|
|
18
25
|
readonly href?: string
|
|
19
26
|
readonly position: number
|
|
20
27
|
readonly className?: string
|
|
28
|
+
readonly customProps?: Record<string, unknown>
|
|
29
|
+
readonly collapsible: boolean
|
|
30
|
+
readonly collapsed: boolean
|
|
21
31
|
readonly children: Readonly<Record<string, TreeNode>>
|
|
22
32
|
}
|
|
23
33
|
|
|
@@ -29,6 +39,9 @@ const createLeafNode = (key: string, doc: DocsData): TreeNode => ({
|
|
|
29
39
|
href: doc.link !== false ? doc.path : undefined,
|
|
30
40
|
position: doc.sidebarPosition ?? DEFAULT_POSITION,
|
|
31
41
|
className: doc.sidebarClassName,
|
|
42
|
+
customProps: doc.sidebarCustomProps,
|
|
43
|
+
collapsible: doc.collapsible ?? true,
|
|
44
|
+
collapsed: doc.collapsed ?? true,
|
|
32
45
|
children: {},
|
|
33
46
|
})
|
|
34
47
|
|
|
@@ -36,6 +49,8 @@ const createBranchNode = (key: string): TreeNode => ({
|
|
|
36
49
|
key,
|
|
37
50
|
label: key,
|
|
38
51
|
position: DEFAULT_POSITION,
|
|
52
|
+
collapsible: true,
|
|
53
|
+
collapsed: true,
|
|
39
54
|
children: {},
|
|
40
55
|
})
|
|
41
56
|
|
|
@@ -45,6 +60,9 @@ const mergeNodeWithDoc = (node: TreeNode, doc: DocsData): TreeNode => ({
|
|
|
45
60
|
href: doc.link !== false ? doc.path : node.href,
|
|
46
61
|
position: doc.sidebarPosition ?? node.position,
|
|
47
62
|
className: doc.sidebarClassName ?? node.className,
|
|
63
|
+
customProps: doc.sidebarCustomProps ?? node.customProps,
|
|
64
|
+
collapsible: doc.collapsible ?? node.collapsible,
|
|
65
|
+
collapsed: doc.collapsed ?? node.collapsed,
|
|
48
66
|
})
|
|
49
67
|
|
|
50
68
|
const insertAtPath = (
|
|
@@ -93,11 +111,17 @@ const treeNodeToEntry = (node: TreeNode): Entry[string] => {
|
|
|
93
111
|
)
|
|
94
112
|
: undefined
|
|
95
113
|
|
|
114
|
+
// Only include collapsible/collapsed for nodes with children (category nodes)
|
|
115
|
+
const hasChildren = sortedChildren.length > 0
|
|
116
|
+
|
|
96
117
|
return {
|
|
97
118
|
label: node.label,
|
|
98
119
|
...(node.href && { href: node.href }),
|
|
99
120
|
...(node.className && { className: node.className }),
|
|
121
|
+
...(node.customProps && { customProps: node.customProps }),
|
|
100
122
|
...(subEntry && { subEntry }),
|
|
123
|
+
...(hasChildren && { collapsible: node.collapsible }),
|
|
124
|
+
...(hasChildren && { collapsed: node.collapsed }),
|
|
101
125
|
}
|
|
102
126
|
}
|
|
103
127
|
|
|
@@ -109,7 +133,10 @@ const parseDocPath = (id: string): readonly string[] => {
|
|
|
109
133
|
}
|
|
110
134
|
|
|
111
135
|
export const toSidebarEntries = (docs: readonly DocsData[]): Entry => {
|
|
112
|
-
|
|
136
|
+
// Filter out unlisted pages from the sidebar
|
|
137
|
+
const visibleDocs = docs.filter((doc) => !doc.unlisted)
|
|
138
|
+
|
|
139
|
+
const rootTree = visibleDocs.reduce<Readonly<Record<string, TreeNode>>>(
|
|
113
140
|
(acc, doc) => insertAtPath(acc, parseDocPath(doc.id), doc),
|
|
114
141
|
{},
|
|
115
142
|
)
|