-
Notifications
You must be signed in to change notification settings - Fork 66.9k
Expand file tree
/
Copy pathgeneric-toc.ts
More file actions
176 lines (155 loc) · 6.56 KB
/
generic-toc.ts
File metadata and controls
176 lines (155 loc) · 6.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
import type { Response, NextFunction } from 'express'
import type { ExtendedRequest, Context, Tree, ToC } from '@/types'
import findPageInSiteTree from '@/frame/lib/find-page-in-site-tree'
function isNewLandingPage(currentLayoutName: string): boolean {
return (
currentLayoutName === 'category-landing' ||
currentLayoutName === 'bespoke-landing' ||
currentLayoutName === 'discovery-landing' ||
currentLayoutName === 'journey-landing'
)
}
// This module adds either flatTocItems or nestedTocItems to the context object for
// product, category, and subcategory TOCs that don't have other layouts specified.
// They are rendered by includes/generic-toc-flat.html or includes/generic-toc-nested.html.
export default async function genericToc(req: ExtendedRequest, res: Response, next: NextFunction) {
if (!req.context) throw new Error('request not contextualized')
if (!req.context.page) return next()
if (
req.context.currentLayoutName !== 'default' &&
!isNewLandingPage(req.context.currentLayoutName || '')
)
return next()
// This middleware can only run on product, category, and subcategories.
if (
req.context.page.documentType === 'homepage' ||
req.context.page.documentType === 'article' ||
req.context.page.relativePath === 'search/index.md'
)
return next()
// This one product TOC is weird.
const isOneOffProductToc = req.context.page.relativePath === 'github/index.md'
// There are different types of TOC depending on the document type.
const tocTypes: Record<string, string> = {
product: 'flat',
category: 'nested',
subcategory: 'flat',
}
// Frontmatter can optionally be set on an Early Access product to show hidden child items.
// If so, this is a special case where we want to override the flat tocType and use a nested type.
const earlyAccessToc = req.context.page.earlyAccessToc
if (!req.context.currentProductTree) throw new Error('currentProductTree not in context')
if (!req.context.currentEnglishTree) throw new Error('currentEnglishTree not in context')
if (!req.pagePath) throw new Error('pagePath not in request')
// Find the part of the site tree that corresponds to the current path.
const treePage = findPageInSiteTree(
req.context.currentProductTree,
req.context.currentEnglishTree,
req.pagePath,
)
let fauxSubcategory = false
if (req.context.page.documentType === 'category' && req.context.page.autogenerated !== 'rest') {
// But does *have* children?
const hasGrandchildren = (treePage.childPages || []).some((child) => child.children)
fauxSubcategory = !hasGrandchildren
}
// Find the current TOC type based on the current document type.
const currentTocType = earlyAccessToc
? 'nested'
: fauxSubcategory
? 'flat'
: tocTypes[req.context.page.documentType]
// By default, only include hidden child items on a TOC page if it's an Early Access category or
// subcategory page, not a product or 'articles' fake category page (e.g., /early-access/github/articles).
// This is because we don't want entire EA product TOCs to be publicly browseable, but anything at the category
// or below level is fair game because that content is scoped to specific features.
const isCategoryOrSubcategory =
req.context.page.documentType === 'category' || req.context.page.documentType === 'subcategory'
if (!req.context.currentPath) throw new Error('currentPath not in context')
const isEarlyAccess = req.context.currentPath.includes('/early-access/')
const isArticlesCategory = req.context.currentPath.endsWith('/articles')
const includeHidden =
earlyAccessToc || (isCategoryOrSubcategory && isEarlyAccess && !isArticlesCategory)
// Conditionally run getTocItems() recursively.
let isRecursive
// Conditionally render intros.
let renderIntros
// Get an array of child links with intros and add it to the context object.
if (currentTocType === 'flat' && !isOneOffProductToc) {
isRecursive = false
renderIntros = true
req.context.genericTocFlat = await getTocItems(treePage, req.context, {
recurse: isRecursive,
renderIntros,
includeHidden,
textOnly: isNewLandingPage(req.context.currentLayoutName || ''),
})
}
// Get an array of child subcategories and their child articles and add it to the context object.
if (currentTocType === 'nested' || isOneOffProductToc) {
isRecursive = !isOneOffProductToc
renderIntros = false
req.context.genericTocNested = await getTocItems(treePage, req.context, {
recurse: isRecursive,
renderIntros: isNewLandingPage(req.context.currentLayoutName || '') ? true : false,
includeHidden,
textOnly: isNewLandingPage(req.context.currentLayoutName || ''),
})
}
return next()
}
// Return a nested object that contains the bits and pieces we need
// for the tree which is used for sidebars and listing
type Options = {
recurse: boolean
renderIntros: boolean
includeHidden: boolean
textOnly: boolean
}
async function getTocItems(node: Tree, context: Context, opts: Options): Promise<ToC[]> {
// Cleaner than trying to be too terse inside the `.filter()` inline callback.
function filterHidden(child: Tree): boolean {
return opts.includeHidden || !child.page.hidden
}
return await Promise.all(
(node.childPages || []).filter(filterHidden).map(async (child) => {
const { page } = child
const title = await page.renderProp('rawTitle', context, { textOnly: true })
const octicon = page.octicon ?? null
const category = page.category ? page.category : null
const complexity = page.complexity ? page.complexity : null
const industry = page.industry ? page.industry : null
let intro = null
if (opts.renderIntros) {
intro = ''
if (page.rawIntro) {
// The intro can contain Markdown even though it might not
// contain any Liquid.
// Use textOnly for new landing pages to strip HTML tags.
// For other pages, we intend to display the intro in a table of contents
// component with the HTML (dangerouslySetInnerHTML).
intro = await page.renderProp(
'rawIntro',
context,
opts.textOnly ? { textOnly: true } : {},
)
}
}
const childTocItems = []
if (child.childPages) {
childTocItems.push(...(await getTocItems(child, context, opts)))
}
const fullPath = child.href
return {
title,
fullPath,
intro,
octicon,
category,
complexity,
industry,
childTocItems,
} as ToC
}),
)
}