forked from github/docs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathget-mini-toc-items.js
149 lines (127 loc) · 4.86 KB
/
get-mini-toc-items.js
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
import cheerio from 'cheerio'
import { range } from 'lodash-es'
import renderContent from './render-content/index.js'
export default function getMiniTocItems(html, maxHeadingLevel = 2, headingScope = '') {
const $ = cheerio.load(html, { xmlMode: true })
// eg `h2, h3` or `h2, h3, h4` depending on maxHeadingLevel
const selector = range(2, maxHeadingLevel + 1)
.map((num) => `${headingScope} h${num}`)
.join(', ')
const headings = $(selector)
// return an array of objects containing each heading's contents, level, and optional platform.
// Article layout uses these as follows:
// - `title` and `link` to render the mini TOC headings
// - `headingLevel` the `2` in `h2`; used for determining required indentation
// - `platform` to show or hide platform-specific headings via client JS
// H1 = highest importance, H6 = lowest importance
let mostImportantHeadingLevel
const flatToc = headings
.get()
.filter((item) => {
if (!item.parent || !item.parent.attribs) return true
// Hide any items that belong to a hidden div
const { attribs } = item.parent
return !('hidden' in attribs)
})
.map((item) => {
// remove any <span> tags including their content
$('span', item).remove()
// Capture the anchor tag nested within the header, get its href and remove it
const anchor = $('a.doctocat-link', item)
const href = anchor.attr('href')
anchor.remove()
// remove any <strong> tags but leave content
$('strong', item).map((i, el) => $(el).replaceWith($(el).contents()))
const contents = { href, title: $(item).text().trim() }
const headingLevel = parseInt($(item)[0].name.match(/\d+/)[0], 10) || 0 // the `2` from `h2`
const platform = $(item).parent('.extended-markdown').attr('class') || ''
// track the most important heading level while we're looping through the items
if (headingLevel < mostImportantHeadingLevel || mostImportantHeadingLevel === undefined) {
mostImportantHeadingLevel = headingLevel
}
return { contents, headingLevel, platform }
})
.map((item) => {
// set the indentation level for each item based on the most important
// heading level in the current article
return {
...item,
indentationLevel: item.headingLevel - mostImportantHeadingLevel,
}
})
// convert the flatToc to a nested structure to simplify semantic rendering on the client
const nestedToc = buildNestedToc(flatToc)
return minimalMiniToc(nestedToc)
}
// Recursively build a tree from the list of allItems
function buildNestedToc(allItems, startIndex = 0) {
const startItem = allItems[startIndex]
if (!startItem) {
return []
}
let curLevelIndentation = startItem.indentationLevel
const currentLevel = []
for (let cursor = startIndex; cursor < allItems.length; cursor++) {
const cursorItem = allItems[cursor]
const nextItem = allItems[cursor + 1]
const nextItemIsNested = nextItem && nextItem.indentationLevel > cursorItem.indentationLevel
// if it's the current indentation level, push it on and keep going
if (curLevelIndentation === cursorItem.indentationLevel) {
currentLevel.push({
...cursorItem,
items: nextItemIsNested ? buildNestedToc(allItems, cursor + 1) : [],
})
continue
}
// these items were already handled via recursion
if (curLevelIndentation < cursorItem.indentationLevel) {
continue
}
// current root indentation is _greater_ than our current cursor item,
if (curLevelIndentation > cursorItem.indentationLevel) {
// special scenario where the initial list started with "less important" headers
// so we need to reset our expectations of what level to judge the indentation on
if (startIndex === 0) {
curLevelIndentation = cursorItem.indentationLevel
currentLevel.push({
...cursorItem,
items: nextItemIsNested ? buildNestedToc(allItems, cursor + 1) : [],
})
continue
}
break
}
}
return currentLevel
}
// Strip the bits and pieces from each object in the array that are
// not needed in the React component rendering.
function minimalMiniToc(toc) {
return toc.map(({ platform, contents, items }) => {
const minimal = { contents }
const subItems = minimalMiniToc(items)
if (subItems.length) minimal.items = subItems
if (platform) minimal.platform = platform
return minimal
})
}
export async function getAutomatedPageMiniTocItems(
items,
context,
depth = 2,
markdownHeading = ''
) {
const titles =
markdownHeading +
items
.map((item) => {
let title = ''
for (let i = 0; i < depth; i++) {
title += '#'
}
return title + ` ${item}\n`
})
.join('')
const toc = await renderContent(titles, context)
return getMiniTocItems(toc, depth, '')
}