forked from github/docs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpage.js
211 lines (170 loc) · 7.63 KB
/
page.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
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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
const assert = require('assert')
const fs = require('fs')
const path = require('path')
const cheerio = require('cheerio')
const patterns = require('./patterns')
const getMapTopicContent = require('./get-map-topic-content')
const rewriteAssetPathsToS3 = require('./rewrite-asset-paths-to-s3')
const rewriteLocalLinks = require('./rewrite-local-links')
const encodeBracketedParentheticals = require('./encode-bracketed-parentheticals')
const generateRedirectsForPermalinks = require('./redirects/permalinks')
const getEnglishHeadings = require('./get-english-headings')
const useEnglishHeadings = require('./use-english-headings')
const getTocItems = require('./get-toc-items')
const pathUtils = require('./path-utils')
const Permalink = require('./permalink')
const languages = require('./languages')
const renderContent = require('./render-content')
const frontmatter = require('./frontmatter')
const products = require('./all-products')
const slash = require('slash')
class Page {
constructor (opts) {
assert(opts.relativePath, 'relativePath is required')
assert(opts.basePath, 'basePath is required')
assert(opts.languageCode, 'languageCode is required')
Object.assign(this, { ...opts })
this.relativePath = slash(this.relativePath)
this.fullPath = slash(path.join(this.basePath, this.relativePath))
this.raw = fs.readFileSync(this.fullPath, 'utf8')
// TODO remove this when https://github.com/github/crowdin-support/issues/66 has been resolved
if (this.languageCode !== 'en' && this.raw.includes(': verdadero')) {
this.raw = this.raw.replace(': verdadero', ': true')
}
// parse fronmatter and save any errors for validation in the test suite
const { content, data, errors: frontmatterErrors } = frontmatter(this.raw, { filepath: this.fullPath })
this.frontmatterErrors = frontmatterErrors
if (this.frontmatterErrors.length) {
throw new Error(JSON.stringify(this.frontmatterErrors, null, 2))
}
// preserve the frontmatter-free markdown content,
this.markdown = content
// prevent `[foo] (bar)` strings with a space between from being interpreted as markdown links
this.markdown = encodeBracketedParentheticals(this.markdown)
Object.assign(this, data)
// Store raw data so we can cache parsed versions
this.rawIntro = this.intro
this.rawTitle = this.title
this.rawShortTitle = this.shortTitle
this.rawProduct = this.product
this.rawPermissions = this.permissions
// Store raw data so we can access/modify it later in middleware
this.rawGettingStartedLinks = this.gettingStartedLinks
this.rawPopularLinks = this.popularLinks
this.rawGuideLinks = this.guideLinks
// Do not need to keep the original props
delete this.gettingStartedLinks
delete this.popularLinks
delete this.guideLinks
// derive array of Permalink objects
this.permalinks = Permalink.derive(this.languageCode, this.relativePath, this.title, this.versions)
// create backwards-compatible old paths for page permalinks and frontmatter redirects
this.redirects = generateRedirectsForPermalinks(this.permalinks, this.redirect_from)
// get an array of linked items in product and category TOCs
this.tocItems = getTocItems(this)
// if this is an article and it doesn't have showMiniToc = false, set mini TOC to true
if (!this.relativePath.endsWith('index.md') && !this.mapTopic) {
this.showMiniToc = this.showMiniToc === false
? this.showMiniToc
: true
}
return this
}
// Infer the parent product ID from the page's relative file path
get parentProductId () {
// Each page's top-level content directory matches its product ID
const id = this.relativePath.split('/')[0]
// ignore top-level content/index.md
if (id === 'index.md') return null
// make sure the ID is valid
assert(
Object.keys(products).includes(id),
`page ${this.fullPath} has an invalid product ID: ${id}`
)
return id
}
get parentProduct () {
return products[this.parentProductId]
}
async renderTitle (context, opts = {}) {
return this.shortTitle
? this.renderProp('shortTitle', context, opts)
: this.renderProp('title', context, opts)
}
async render (context) {
this.intro = await renderContent(this.rawIntro, context)
this.introPlainText = await renderContent(this.rawIntro, context, { textOnly: true })
this.title = await renderContent(this.rawTitle, context, { textOnly: true, encodeEntities: true })
this.shortTitle = await renderContent(this.shortTitle, context, { textOnly: true, encodeEntities: true })
const markdown = this.mapTopic
? getMapTopicContent(this, context.pages, context.redirects)
: this.markdown
const html = await renderContent(markdown, context)
// product frontmatter may contain liquid
if (this.product) {
this.product = await renderContent(this.rawProduct, context)
}
// permissions frontmatter may contain liquid
if (this.permissions) {
this.permissions = await renderContent(this.rawPermissions, context)
}
const $ = cheerio.load(html)
// set a flag so layout knows whether to render a mac/windows/linux switcher element
this.includesPlatformSpecificContent = $('[class^="platform-"], .mac, .windows, .linux, .all').length > 0
// rewrite asset paths to s3 if it's a dotcom article on any GHE version
// or if it's an enterprise article on any GHE version EXCEPT latest version
rewriteAssetPathsToS3($, context.currentVersion, this.relativePath)
// use English IDs/anchors for translated headings, so links don't break (see #8572)
if (this.languageCode !== 'en') {
const englishHeadings = getEnglishHeadings(this, context.pages)
if (englishHeadings) useEnglishHeadings($, englishHeadings)
}
// rewrite local links to include current language code and GHE version if needed
rewriteLocalLinks($, context.currentVersion, context.currentLanguage)
// wrap ordered list images in a container div
$('ol > li img').each((i, el) => {
$(el).wrap('<div class="procedural-image-wrapper" />')
})
const cleanedHTML = $('body').html()
return cleanedHTML
}
// Allow other modules (like custom liquid tags) to make one-off requests
// for a page's rendered properties like `title` and `intro`
async renderProp (propName, context, opts = { unwrap: false }) {
let prop
if (propName === 'title') {
prop = this.rawTitle
} else if (propName === 'shortTitle') {
prop = this.rawShortTitle || this.rawTitle // fall back to title
} else if (propName === 'intro') {
prop = this.rawIntro
} else {
prop = this[propName]
}
const html = await renderContent(prop, context, opts)
if (!opts.unwrap) return html
// The unwrap option removes surrounding tags from a string, preserving any inner HTML
const $ = cheerio.load(html, { xmlMode: true })
return $.root().contents().html()
}
// infer current page's corresponding homepage
// /en/articles/foo -> /en
// /en/enterprise/2.14/user/articles/foo -> /en/enterprise/2.14/user
static getHomepage (requestPath) {
return requestPath.replace(/\/articles.*/, '')
}
// given a page path, return an array of objects containing hrefs
// for that page in all languages
static getLanguageVariants (href) {
const suffix = pathUtils.getPathWithoutLanguage(href)
return Object.values(languages).map(({ name, code, hreflang }) => { // eslint-disable-line
return {
name,
code,
hreflang,
href: `/${code}${suffix}`.replace(patterns.trailingSlash, '$1')
}
})
}
}
module.exports = Page