@@ -9,6 +9,7 @@ import type {
9
9
} from '@tutorialkit/types' ;
10
10
import { folderPathToFilesRef } from '@tutorialkit/types' ;
11
11
import { getCollection } from 'astro:content' ;
12
+ import { logger } from './logger' ;
12
13
import glob from 'fast-glob' ;
13
14
import path from 'node:path' ;
14
15
@@ -22,14 +23,13 @@ export async function getTutorial(): Promise<Tutorial> {
22
23
} ;
23
24
24
25
let tutorialMetaData : TutorialSchema | undefined ;
25
-
26
- const lessons : Lesson [ ] = [ ] ;
26
+ let lessons : Lesson [ ] = [ ] ;
27
27
28
28
for ( const entry of collection ) {
29
29
const { id, data } = entry ;
30
30
const { type } = data ;
31
31
32
- const [ partId , chapterId , lessonId ] = parseId ( id ) ;
32
+ const [ partId , chapterId , lessonId ] = id . split ( '/' ) ;
33
33
34
34
if ( type === 'tutorial' ) {
35
35
tutorialMetaData = data ;
@@ -41,6 +41,7 @@ export async function getTutorial(): Promise<Tutorial> {
41
41
} else if ( type === 'part' ) {
42
42
_tutorial . parts [ partId ] = {
43
43
id : partId ,
44
+ order : - 1 ,
44
45
data,
45
46
slug : getSlug ( entry ) ,
46
47
chapters : { } ,
@@ -52,6 +53,7 @@ export async function getTutorial(): Promise<Tutorial> {
52
53
53
54
_tutorial . parts [ partId ] . chapters [ chapterId ] = {
54
55
id : chapterId ,
56
+ order : - 1 ,
55
57
data,
56
58
slug : getSlug ( entry ) ,
57
59
lessons : { } ,
@@ -77,6 +79,7 @@ export async function getTutorial(): Promise<Tutorial> {
77
79
const lesson : Lesson = {
78
80
data,
79
81
id : lessonId ,
82
+ order : - 1 ,
80
83
part : {
81
84
id : partId ,
82
85
title : _tutorial . parts [ partId ] . data . title ,
@@ -97,20 +100,133 @@ export async function getTutorial(): Promise<Tutorial> {
97
100
}
98
101
}
99
102
103
+ if ( ! tutorialMetaData ) {
104
+ throw new Error ( `Could not find tutorial 'meta.md' file` ) ;
105
+ }
106
+
107
+ // let's now compute the order for everything
108
+ const partsOrder = getOrder ( tutorialMetaData . parts , _tutorial . parts ) ;
109
+
110
+ for ( let p = 0 ; p < partsOrder . length ; ++ p ) {
111
+ const partId = partsOrder [ p ] ;
112
+ const part = _tutorial . parts [ partId ] ;
113
+
114
+ if ( ! part ) {
115
+ logger . warn ( `Could not find '${ partId } ', it won't be part of the tutorial.` ) ;
116
+ continue ;
117
+ }
118
+
119
+ if ( ! _tutorial . firstPartId ) {
120
+ _tutorial . firstPartId = partId ;
121
+ }
122
+
123
+ part . order = p ;
124
+
125
+ const chapterOrder = getOrder ( part . data . chapters , part . chapters ) ;
126
+
127
+ for ( let c = 0 ; c < chapterOrder . length ; ++ c ) {
128
+ const chapterId = chapterOrder [ c ] ;
129
+ const chapter = part . chapters [ chapterId ] ;
130
+
131
+ if ( ! chapter ) {
132
+ logger . warn ( `Could not find '${ chapterId } ', it won't be part of the part '${ partId } '.` ) ;
133
+ continue ;
134
+ }
135
+
136
+ if ( ! part . firstChapterId ) {
137
+ part . firstChapterId = chapterId ;
138
+ }
139
+
140
+ chapter . order = c ;
141
+
142
+ const lessonOrder = getOrder ( chapter . data . lessons , chapter . lessons ) ;
143
+
144
+ for ( let l = 0 ; l < lessonOrder . length ; ++ l ) {
145
+ const lessonId = lessonOrder [ l ] ;
146
+ const lesson = chapter . lessons [ lessonId ] ;
147
+
148
+ if ( ! lesson ) {
149
+ logger . warn ( `Could not find '${ lessonId } ', it won't be part of the chapter '${ chapterId } '.` ) ;
150
+ continue ;
151
+ }
152
+
153
+ if ( ! chapter . firstLessonId ) {
154
+ chapter . firstLessonId = lessonId ;
155
+ }
156
+
157
+ lesson . order = l ;
158
+ }
159
+ }
160
+ }
161
+
162
+ // removed orphaned lessons
163
+ lessons = lessons . filter (
164
+ ( lesson ) =>
165
+ lesson . order !== - 1 &&
166
+ _tutorial . parts [ lesson . part . id ] . order !== - 1 &&
167
+ _tutorial . parts [ lesson . part . id ] . chapters [ lesson . chapter . id ] . order !== - 1 ,
168
+ ) ;
169
+
170
+ // find orphans discard them and print warnings
171
+ for ( const partId in _tutorial . parts ) {
172
+ const part = _tutorial . parts [ partId ] ;
173
+
174
+ if ( part . order === - 1 ) {
175
+ delete _tutorial . parts [ partId ] ;
176
+ logger . warn (
177
+ `An order was specified for the parts of the tutorial but '${ partId } ' is not included so it won't be visible.` ,
178
+ ) ;
179
+ continue ;
180
+ }
181
+
182
+ for ( const chapterId in part . chapters ) {
183
+ const chapter = part . chapters [ chapterId ] ;
184
+
185
+ if ( chapter . order === - 1 ) {
186
+ delete part . chapters [ chapterId ] ;
187
+ logger . warn (
188
+ `An order was specified for part '${ partId } ' but chapter '${ chapterId } ' is not included, so it won't be visible.` ,
189
+ ) ;
190
+ continue ;
191
+ }
192
+
193
+ for ( const lessonId in chapter . lessons ) {
194
+ const lesson = chapter . lessons [ lessonId ] ;
195
+
196
+ if ( lesson . order === - 1 ) {
197
+ delete chapter . lessons [ lessonId ] ;
198
+ logger . warn (
199
+ `An order was specified for chapter '${ chapterId } ' but lesson '${ lessonId } ' is not included, so it won't be visible.` ,
200
+ ) ;
201
+ continue ;
202
+ }
203
+ }
204
+ }
205
+ }
206
+
207
+ // sort lessons
100
208
lessons . sort ( ( a , b ) => {
101
- const partsA = [ a . part . id , a . chapter . id , a . id ] as const ;
102
- const partsB = [ b . part . id , b . chapter . id , b . id ] as const ;
209
+ const partsA = [
210
+ _tutorial . parts [ a . part . id ] . order ,
211
+ _tutorial . parts [ a . part . id ] . chapters [ a . chapter . id ] . order ,
212
+ a . order ,
213
+ ] as const ;
214
+ const partsB = [
215
+ _tutorial . parts [ b . part . id ] . order ,
216
+ _tutorial . parts [ b . part . id ] . chapters [ b . chapter . id ] . order ,
217
+ b . order ,
218
+ ] as const ;
103
219
104
220
for ( let i = 0 ; i < partsA . length ; i ++ ) {
105
221
if ( partsA [ i ] !== partsB [ i ] ) {
106
- return Number ( partsA [ i ] ) - Number ( partsB [ i ] ) ;
222
+ return partsA [ i ] - partsB [ i ] ;
107
223
}
108
224
}
109
225
110
226
return 0 ;
111
227
} ) ;
112
228
113
- // now we link all tutorials together
229
+ // now we link all lessons together
114
230
for ( const [ i , lesson ] of lessons . entries ( ) ) {
115
231
const prevLesson = i > 0 ? lessons . at ( i - 1 ) : undefined ;
116
232
const nextLesson = lessons . at ( i + 1 ) ;
@@ -167,43 +283,27 @@ function pick<T extends Record<any, any>>(objects: (T | undefined)[], properties
167
283
return newObject ;
168
284
}
169
285
170
- function sortCollection ( collection : CollectionEntryTutorial [ ] ) {
171
- return collection . sort ( ( a , b ) => {
172
- const splitA = a . id . split ( '/' ) ;
173
- const splitB = b . id . split ( '/' ) ;
174
-
175
- const depthA = splitA . length ;
176
- const depthB = splitB . length ;
177
-
178
- if ( depthA !== depthB ) {
179
- return depthA - depthB ;
180
- }
286
+ function getOrder ( order : string [ ] | undefined , fallbackSourceForOrder : Record < string , any > ) : string [ ] {
287
+ if ( order ) {
288
+ return order ;
289
+ }
181
290
182
- for ( let i = 0 ; i < splitA . length ; i ++ ) {
183
- const numA = parseInt ( splitA [ i ] , 10 ) ;
184
- const numB = parseInt ( splitB [ i ] , 10 ) ;
291
+ // default to an order based on having each folder prefixed by their order: `1-foo`, `2-bar`, ...
292
+ return Object . keys ( fallbackSourceForOrder ) . sort ( ( a , b ) => {
293
+ const numA = parseInt ( a , 10 ) ;
294
+ const numB = parseInt ( b , 10 ) ;
185
295
186
- if ( ! isNaN ( numA ) && ! isNaN ( numB ) && numA !== numB ) {
187
- return numA - numB ;
188
- } else {
189
- if ( splitA [ i ] !== splitB [ i ] ) {
190
- return splitA [ i ] . localeCompare ( splitB [ i ] ) ;
191
- }
192
- }
193
- }
194
-
195
- return 0 ;
296
+ return numA - numB ;
196
297
} ) ;
197
298
}
198
299
199
- function parseId ( id : string ) {
200
- const [ part , chapter , lesson ] = id . split ( '/' ) ;
201
-
202
- const [ partId ] = part . split ( '-' ) ;
203
- const [ chapterId ] = chapter ?. split ( '-' ) ?? [ ] ;
204
- const [ lessonId ] = lesson ?. split ( '-' ) ?? [ ] ;
300
+ function sortCollection ( collection : CollectionEntryTutorial [ ] ) {
301
+ return collection . sort ( ( a , b ) => {
302
+ const depthA = a . id . split ( '/' ) . length ;
303
+ const depthB = b . id . split ( '/' ) . length ;
205
304
206
- return [ partId , chapterId , lessonId ] ;
305
+ return depthA - depthB ;
306
+ } ) ;
207
307
}
208
308
209
309
function getSlug ( entry : CollectionEntryTutorial ) {
0 commit comments