@@ -19,27 +19,111 @@ import { renderContentWithFallback } from '@/languages/lib/render-with-fallback'
19
19
import { deprecated , supported } from '@/versions/lib/enterprise-server-releases'
20
20
import { allPlatforms } from '@/tools/lib/all-platforms'
21
21
22
+ import type { Context , FrontmatterVersions } from '@/types'
23
+
22
24
// We're going to check a lot of pages' "ID" (the first part of
23
25
// the relativePath) against `productMap` to make sure it's valid.
24
26
// To avoid having to do `Object.keys(productMap).includes(id)`
25
27
// every single time, we turn it into a Set once.
26
28
const productMapKeysAsSet = new Set ( Object . keys ( productMap ) )
27
29
30
+ type ReadFileContentsResult = {
31
+ data ?: any
32
+ content ?: string
33
+ errors ?: any [ ]
34
+ }
35
+
36
+ type PageInitOptions = {
37
+ languageCode : string
38
+ relativePath : string
39
+ basePath : string
40
+ }
41
+
42
+ type PageReadResult = PageInitOptions & {
43
+ fullPath : string
44
+ markdown : string
45
+ frontmatterErrors ?: any [ ]
46
+ } & any
47
+
48
+ type RenderOptions = {
49
+ preferShort ?: boolean
50
+ unwrap ?: boolean
51
+ textOnly ?: boolean
52
+ throwIfEmpty ?: boolean
53
+ }
54
+
55
+ type CommunityRedirect = {
56
+ name : string
57
+ href : string
58
+ }
59
+
60
+ type GuideWithType = {
61
+ href : string
62
+ title : string
63
+ type ?: string
64
+ topics ?: string [ ]
65
+ }
66
+
28
67
export class FrontmatterErrorsError extends Error {
29
- constructor ( message , frontmatterErrors ) {
68
+ public frontmatterErrors : string [ ]
69
+
70
+ constructor ( message : string , frontmatterErrors : string [ ] ) {
30
71
super ( message )
31
72
this . frontmatterErrors = frontmatterErrors
32
73
}
33
74
}
34
75
35
76
class Page {
36
- static async init ( opts ) {
37
- opts = await Page . read ( opts )
38
- if ( ! opts ) return
39
- return new Page ( opts )
77
+ // Core properties from PageFrontmatter
78
+ public title : string = ''
79
+ public rawTitle : string = ''
80
+ public shortTitle ?: string
81
+ public rawShortTitle ?: string
82
+ public intro : string = ''
83
+ public rawIntro ?: string
84
+ public product ?: string
85
+ public rawProduct ?: string
86
+ public permissions ?: string
87
+ public rawPermissions ?: string
88
+ public versions : FrontmatterVersions = { }
89
+ public showMiniToc ?: boolean
90
+ public hidden ?: boolean
91
+ public redirect_from ?: string [ ]
92
+ public learningTracks ?: any [ ]
93
+ public rawLearningTracks ?: string [ ]
94
+ public includeGuides ?: GuideWithType [ ]
95
+ public rawIncludeGuides ?: string [ ]
96
+ public introLinks ?: Record < string , string >
97
+ public rawIntroLinks ?: Record < string , string >
98
+
99
+ // Derived properties
100
+ public languageCode ! : string
101
+ public relativePath ! : string
102
+ public basePath ! : string
103
+ public fullPath ! : string
104
+ public markdown ! : string
105
+ public documentType : string
106
+ public applicableVersions : string [ ]
107
+ public permalinks : Permalink [ ]
108
+ public tocItems ?: any [ ]
109
+ public communityRedirect ?: CommunityRedirect
110
+ public detectedPlatforms : string [ ] = [ ]
111
+ public includesPlatformSpecificContent : boolean = false
112
+ public detectedTools : string [ ] = [ ]
113
+ public includesToolSpecificContent : boolean = false
114
+ public allToolsParsed : typeof allTools = allTools
115
+ public introPlainText ?: string
116
+
117
+ // Bound method
118
+ public render : ( context : Context ) => Promise < string >
119
+
120
+ static async init ( opts : PageInitOptions ) : Promise < Page | undefined > {
121
+ const readResult = await Page . read ( opts )
122
+ if ( ! readResult ) return
123
+ return new Page ( readResult )
40
124
}
41
125
42
- static async read ( opts ) {
126
+ static async read ( opts : PageInitOptions ) : Promise < PageReadResult | false > {
43
127
assert ( opts . languageCode , 'languageCode is required' )
44
128
assert ( opts . relativePath , 'relativePath is required' )
45
129
assert ( opts . basePath , 'basePath is required' )
@@ -50,7 +134,11 @@ class Page {
50
134
// Per https://nodejs.org/api/fs.html#fs_fs_exists_path_callback
51
135
// its better to read and handle errors than to check access/stats first
52
136
try {
53
- const { data, content, errors : frontmatterErrors } = await readFileContents ( fullPath )
137
+ const {
138
+ data,
139
+ content,
140
+ errors : frontmatterErrors ,
141
+ } : ReadFileContentsResult = await readFileContents ( fullPath )
54
142
55
143
// The `|| ''` is for pages that are purely frontmatter.
56
144
// So the `content` property will be `undefined`.
@@ -72,11 +160,11 @@ class Page {
72
160
// where as notations like `__GHES_DEPRECATED__[3]`
73
161
// or `__GHES_SUPPORTED__[0]` are static.
74
162
if ( opts . basePath . split ( path . sep ) . includes ( 'fixtures' ) ) {
75
- supported . forEach ( ( version , i , arr ) => {
163
+ supported . forEach ( ( version : string , i : number , arr : string [ ] ) => {
76
164
markdown = markdown . replaceAll ( `__GHES_SUPPORTED__[${ i } ]` , version )
77
165
markdown = markdown . replaceAll ( `__GHES_SUPPORTED__[-${ arr . length - i } ]` , version )
78
166
} )
79
- deprecated . forEach ( ( version , i , arr ) => {
167
+ deprecated . forEach ( ( version : string , i : number , arr : string [ ] ) => {
80
168
markdown = markdown . replaceAll ( `__GHES_DEPRECATED__[${ i } ]` , version )
81
169
markdown = markdown . replaceAll ( `__GHES_DEPRECATED__[-${ arr . length - i } ]` , version )
82
170
} )
@@ -86,25 +174,29 @@ class Page {
86
174
...opts ,
87
175
relativePath,
88
176
fullPath,
89
- ...data ,
177
+ ...( data || { } ) ,
90
178
markdown,
91
179
frontmatterErrors,
92
- }
93
- } catch ( err ) {
180
+ } as PageReadResult
181
+ } catch ( err : any ) {
94
182
if ( err . code === 'ENOENT' ) return false
95
183
console . error ( err )
184
+ return false
96
185
}
97
186
}
98
187
99
- constructor ( opts ) {
188
+ constructor ( opts : PageReadResult ) {
100
189
if ( opts . frontmatterErrors && opts . frontmatterErrors . length ) {
101
190
throw new FrontmatterErrorsError (
102
191
`${ opts . frontmatterErrors . length } frontmatter errors trying to load ${ opts . fullPath } ` ,
103
192
opts . frontmatterErrors ,
104
193
)
105
194
}
106
- delete opts . frontmatterErrors
107
- Object . assign ( this , { ...opts } )
195
+
196
+ // Remove frontmatter errors before assignment
197
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
198
+ const { frontmatterErrors : _ , ...cleanOpts } = opts
199
+ Object . assign ( this , cleanOpts )
108
200
109
201
// Store raw data so we can cache parsed versions
110
202
this . rawIntro = this . intro
@@ -113,7 +205,7 @@ class Page {
113
205
this . rawProduct = this . product
114
206
this . rawPermissions = this . permissions
115
207
this . rawLearningTracks = this . learningTracks
116
- this . rawIncludeGuides = this . includeGuides
208
+ this . rawIncludeGuides = this . includeGuides as any
117
209
this . rawIntroLinks = this . introLinks
118
210
119
211
// Is this the Homepage or a Product, Category, Topic, or Article?
@@ -130,7 +222,7 @@ class Page {
130
222
const versionsParentProductIsNotAvailableIn = this . applicableVersions
131
223
// only the homepage will not have this.parentProduct
132
224
. filter (
133
- ( availableVersion ) =>
225
+ ( availableVersion : string ) =>
134
226
this . parentProduct && ! this . parentProduct . versions . includes ( availableVersion ) ,
135
227
)
136
228
@@ -164,12 +256,15 @@ class Page {
164
256
return this
165
257
}
166
258
167
- buildRedirects ( ) {
168
- return generateRedirectsForPermalinks ( this . permalinks , this . redirect_from || [ ] )
259
+ buildRedirects ( ) : Record < string , string > {
260
+ return generateRedirectsForPermalinks ( this . permalinks , this . redirect_from || [ ] ) as Record <
261
+ string ,
262
+ string
263
+ >
169
264
}
170
265
171
266
// Infer the parent product ID from the page's relative file path
172
- get parentProductId ( ) {
267
+ get parentProductId ( ) : string | null {
173
268
// Each page's top-level content directory matches its product ID
174
269
const id = this . relativePath . split ( '/' ) [ 0 ]
175
270
@@ -184,17 +279,21 @@ class Page {
184
279
return id
185
280
}
186
281
187
- get parentProduct ( ) {
188
- return productMap [ this . parentProductId ]
282
+ get parentProduct ( ) : any {
283
+ const id = this . parentProductId
284
+ return id ? productMap [ id ] : undefined
189
285
}
190
286
191
- async renderTitle ( context , opts = { preferShort : true } ) {
287
+ async renderTitle (
288
+ context : Context ,
289
+ opts : RenderOptions = { preferShort : true } ,
290
+ ) : Promise < string > {
192
291
return opts . preferShort && this . shortTitle
193
292
? this . renderProp ( 'shortTitle' , context , opts )
194
293
: this . renderProp ( 'title' , context , opts )
195
294
}
196
295
197
- async _render ( context ) {
296
+ private async _render ( context : Context ) : Promise < string > {
198
297
// use English IDs/anchors for translated headings, so links don't break (see #8572)
199
298
if ( this . languageCode !== 'en' ) {
200
299
const englishHeadings = getEnglishHeadings ( this , context )
@@ -246,7 +345,7 @@ class Page {
246
345
247
346
// introLinks may contain Liquid and need to have versioning processed.
248
347
if ( this . rawIntroLinks ) {
249
- const introLinks = { }
348
+ const introLinks : Record < string , string > = { }
250
349
for ( const [ rawKey , value ] of Object . entries ( this . rawIntroLinks ) ) {
251
350
introLinks [ rawKey ] = await renderContent ( value , context , {
252
351
textOnly : true ,
@@ -257,8 +356,8 @@ class Page {
257
356
}
258
357
259
358
if ( this . rawIncludeGuides ) {
260
- this . includeGuides = await getLinkData ( this . rawIncludeGuides , context )
261
- this . includeGuides . map ( ( guide ) => {
359
+ this . includeGuides = ( await getLinkData ( this . rawIncludeGuides , context ) ) as GuideWithType [ ]
360
+ this . includeGuides ? .map ( ( guide : any ) => {
262
361
const { page } = guide
263
362
guide . type = page . type
264
363
if ( page . topics ) {
@@ -272,7 +371,7 @@ class Page {
272
371
// set a flag so layout knows whether to render a mac/windows/linux switcher element
273
372
// Remember, the values of platform is matched in
274
373
// the handleInvalidQuerystringValues shielding middleware.
275
- this . detectedPlatforms = allPlatforms . filter ( ( platform ) => {
374
+ this . detectedPlatforms = allPlatforms . filter ( ( platform : string ) => {
276
375
// This matches `ghd-tool mac` but not `ghd-tool macos`
277
376
// Whereas `html.includes('ghd-tool mac')` would match both.
278
377
const regex = new RegExp ( `ghd-tool ${ platform } \\b|platform-${ platform } \\b` )
@@ -281,7 +380,7 @@ class Page {
281
380
this . includesPlatformSpecificContent = this . detectedPlatforms . length > 0
282
381
283
382
// set flags for webui, cli, etc switcher element
284
- this . detectedTools = Object . keys ( allTools ) . filter ( ( tool ) => {
383
+ this . detectedTools = Object . keys ( allTools ) . filter ( ( tool : string ) => {
285
384
// This matches `ghd-tool jetbrain` but not `ghd-tool jetbrain_beta`
286
385
// Whereas `html.includes('ghd-tool jetbrain')` would match both.
287
386
const regex = new RegExp ( `ghd-tool ${ tool } \\b|tool-${ tool } \\b` )
@@ -298,8 +397,12 @@ class Page {
298
397
299
398
// Allow other modules (like custom liquid tags) to make one-off requests
300
399
// for a page's rendered properties like `title` and `intro`
301
- async renderProp ( propName , context , opts = { unwrap : false } ) {
302
- let prop
400
+ async renderProp (
401
+ propName : string ,
402
+ context : Context ,
403
+ opts : RenderOptions = { unwrap : false } ,
404
+ ) : Promise < string > {
405
+ let prop : string
303
406
if ( propName === 'title' ) {
304
407
prop = 'rawTitle'
305
408
} else if ( propName === 'shortTitle' ) {
@@ -316,13 +419,13 @@ class Page {
316
419
317
420
// The unwrap option removes surrounding tags from a string, preserving any inner HTML
318
421
const $ = cheerio . load ( html , { xmlMode : true } )
319
- return $ . root ( ) . contents ( ) . html ( )
422
+ return $ . root ( ) . contents ( ) . html ( ) || ''
320
423
}
321
424
322
425
// infer current page's corresponding homepage
323
426
// /en/articles/foo -> /en
324
427
// /en/enterprise/2.14/user/articles/foo -> /en/enterprise/2.14/user
325
- static getHomepage ( requestPath ) {
428
+ static getHomepage ( requestPath : string ) : string {
326
429
return requestPath . replace ( / \/ a r t i c l e s .* / , '' )
327
430
}
328
431
}
0 commit comments