@@ -19,27 +19,111 @@ import { renderContentWithFallback } from '@/languages/lib/render-with-fallback'
1919import { deprecated , supported } from '@/versions/lib/enterprise-server-releases'
2020import { allPlatforms } from '@/tools/lib/all-platforms'
2121
22+ import type { Context , FrontmatterVersions } from '@/types'
23+
2224// We're going to check a lot of pages' "ID" (the first part of
2325// the relativePath) against `productMap` to make sure it's valid.
2426// To avoid having to do `Object.keys(productMap).includes(id)`
2527// every single time, we turn it into a Set once.
2628const productMapKeysAsSet = new Set ( Object . keys ( productMap ) )
2729
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+
2867export class FrontmatterErrorsError extends Error {
29- constructor ( message , frontmatterErrors ) {
68+ public frontmatterErrors : string [ ]
69+
70+ constructor ( message : string , frontmatterErrors : string [ ] ) {
3071 super ( message )
3172 this . frontmatterErrors = frontmatterErrors
3273 }
3374}
3475
3576class 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 )
40124 }
41125
42- static async read ( opts ) {
126+ static async read ( opts : PageInitOptions ) : Promise < PageReadResult | false > {
43127 assert ( opts . languageCode , 'languageCode is required' )
44128 assert ( opts . relativePath , 'relativePath is required' )
45129 assert ( opts . basePath , 'basePath is required' )
@@ -50,7 +134,11 @@ class Page {
50134 // Per https://nodejs.org/api/fs.html#fs_fs_exists_path_callback
51135 // its better to read and handle errors than to check access/stats first
52136 try {
53- const { data, content, errors : frontmatterErrors } = await readFileContents ( fullPath )
137+ const {
138+ data,
139+ content,
140+ errors : frontmatterErrors ,
141+ } : ReadFileContentsResult = await readFileContents ( fullPath )
54142
55143 // The `|| ''` is for pages that are purely frontmatter.
56144 // So the `content` property will be `undefined`.
@@ -72,11 +160,11 @@ class Page {
72160 // where as notations like `__GHES_DEPRECATED__[3]`
73161 // or `__GHES_SUPPORTED__[0]` are static.
74162 if ( opts . basePath . split ( path . sep ) . includes ( 'fixtures' ) ) {
75- supported . forEach ( ( version , i , arr ) => {
163+ supported . forEach ( ( version : string , i : number , arr : string [ ] ) => {
76164 markdown = markdown . replaceAll ( `__GHES_SUPPORTED__[${ i } ]` , version )
77165 markdown = markdown . replaceAll ( `__GHES_SUPPORTED__[-${ arr . length - i } ]` , version )
78166 } )
79- deprecated . forEach ( ( version , i , arr ) => {
167+ deprecated . forEach ( ( version : string , i : number , arr : string [ ] ) => {
80168 markdown = markdown . replaceAll ( `__GHES_DEPRECATED__[${ i } ]` , version )
81169 markdown = markdown . replaceAll ( `__GHES_DEPRECATED__[-${ arr . length - i } ]` , version )
82170 } )
@@ -86,25 +174,29 @@ class Page {
86174 ...opts ,
87175 relativePath,
88176 fullPath,
89- ...data ,
177+ ...( data || { } ) ,
90178 markdown,
91179 frontmatterErrors,
92- }
93- } catch ( err ) {
180+ } as PageReadResult
181+ } catch ( err : any ) {
94182 if ( err . code === 'ENOENT' ) return false
95183 console . error ( err )
184+ return false
96185 }
97186 }
98187
99- constructor ( opts ) {
188+ constructor ( opts : PageReadResult ) {
100189 if ( opts . frontmatterErrors && opts . frontmatterErrors . length ) {
101190 throw new FrontmatterErrorsError (
102191 `${ opts . frontmatterErrors . length } frontmatter errors trying to load ${ opts . fullPath } ` ,
103192 opts . frontmatterErrors ,
104193 )
105194 }
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 )
108200
109201 // Store raw data so we can cache parsed versions
110202 this . rawIntro = this . intro
@@ -113,7 +205,7 @@ class Page {
113205 this . rawProduct = this . product
114206 this . rawPermissions = this . permissions
115207 this . rawLearningTracks = this . learningTracks
116- this . rawIncludeGuides = this . includeGuides
208+ this . rawIncludeGuides = this . includeGuides as any
117209 this . rawIntroLinks = this . introLinks
118210
119211 // Is this the Homepage or a Product, Category, Topic, or Article?
@@ -130,7 +222,7 @@ class Page {
130222 const versionsParentProductIsNotAvailableIn = this . applicableVersions
131223 // only the homepage will not have this.parentProduct
132224 . filter (
133- ( availableVersion ) =>
225+ ( availableVersion : string ) =>
134226 this . parentProduct && ! this . parentProduct . versions . includes ( availableVersion ) ,
135227 )
136228
@@ -164,12 +256,15 @@ class Page {
164256 return this
165257 }
166258
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+ >
169264 }
170265
171266 // Infer the parent product ID from the page's relative file path
172- get parentProductId ( ) {
267+ get parentProductId ( ) : string | null {
173268 // Each page's top-level content directory matches its product ID
174269 const id = this . relativePath . split ( '/' ) [ 0 ]
175270
@@ -184,17 +279,21 @@ class Page {
184279 return id
185280 }
186281
187- get parentProduct ( ) {
188- return productMap [ this . parentProductId ]
282+ get parentProduct ( ) : any {
283+ const id = this . parentProductId
284+ return id ? productMap [ id ] : undefined
189285 }
190286
191- async renderTitle ( context , opts = { preferShort : true } ) {
287+ async renderTitle (
288+ context : Context ,
289+ opts : RenderOptions = { preferShort : true } ,
290+ ) : Promise < string > {
192291 return opts . preferShort && this . shortTitle
193292 ? this . renderProp ( 'shortTitle' , context , opts )
194293 : this . renderProp ( 'title' , context , opts )
195294 }
196295
197- async _render ( context ) {
296+ private async _render ( context : Context ) : Promise < string > {
198297 // use English IDs/anchors for translated headings, so links don't break (see #8572)
199298 if ( this . languageCode !== 'en' ) {
200299 const englishHeadings = getEnglishHeadings ( this , context )
@@ -246,7 +345,7 @@ class Page {
246345
247346 // introLinks may contain Liquid and need to have versioning processed.
248347 if ( this . rawIntroLinks ) {
249- const introLinks = { }
348+ const introLinks : Record < string , string > = { }
250349 for ( const [ rawKey , value ] of Object . entries ( this . rawIntroLinks ) ) {
251350 introLinks [ rawKey ] = await renderContent ( value , context , {
252351 textOnly : true ,
@@ -257,8 +356,8 @@ class Page {
257356 }
258357
259358 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 ) => {
262361 const { page } = guide
263362 guide . type = page . type
264363 if ( page . topics ) {
@@ -272,7 +371,7 @@ class Page {
272371 // set a flag so layout knows whether to render a mac/windows/linux switcher element
273372 // Remember, the values of platform is matched in
274373 // the handleInvalidQuerystringValues shielding middleware.
275- this . detectedPlatforms = allPlatforms . filter ( ( platform ) => {
374+ this . detectedPlatforms = allPlatforms . filter ( ( platform : string ) => {
276375 // This matches `ghd-tool mac` but not `ghd-tool macos`
277376 // Whereas `html.includes('ghd-tool mac')` would match both.
278377 const regex = new RegExp ( `ghd-tool ${ platform } \\b|platform-${ platform } \\b` )
@@ -281,7 +380,7 @@ class Page {
281380 this . includesPlatformSpecificContent = this . detectedPlatforms . length > 0
282381
283382 // 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 ) => {
285384 // This matches `ghd-tool jetbrain` but not `ghd-tool jetbrain_beta`
286385 // Whereas `html.includes('ghd-tool jetbrain')` would match both.
287386 const regex = new RegExp ( `ghd-tool ${ tool } \\b|tool-${ tool } \\b` )
@@ -298,8 +397,12 @@ class Page {
298397
299398 // Allow other modules (like custom liquid tags) to make one-off requests
300399 // 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
303406 if ( propName === 'title' ) {
304407 prop = 'rawTitle'
305408 } else if ( propName === 'shortTitle' ) {
@@ -316,13 +419,13 @@ class Page {
316419
317420 // The unwrap option removes surrounding tags from a string, preserving any inner HTML
318421 const $ = cheerio . load ( html , { xmlMode : true } )
319- return $ . root ( ) . contents ( ) . html ( )
422+ return $ . root ( ) . contents ( ) . html ( ) || ''
320423 }
321424
322425 // infer current page's corresponding homepage
323426 // /en/articles/foo -> /en
324427 // /en/enterprise/2.14/user/articles/foo -> /en/enterprise/2.14/user
325- static getHomepage ( requestPath ) {
428+ static getHomepage ( requestPath : string ) : string {
326429 return requestPath . replace ( / \/ a r t i c l e s .* / , '' )
327430 }
328431}
0 commit comments