Skip to content

Commit 7bff6d1

Browse files
vsemozhetbytaddaleax
authored andcommittedMay 14, 2018
tools: overhaul tools/doc/html.js
PR-URL: #20613 Reviewed-By: Trivikram Kamat <[email protected]>
1 parent d11a435 commit 7bff6d1

File tree

1 file changed

+213
-307
lines changed

1 file changed

+213
-307
lines changed
 

‎tools/doc/html.js

+213-307
Original file line numberDiff line numberDiff line change
@@ -29,109 +29,50 @@ const typeParser = require('./type-parser.js');
2929

3030
module.exports = toHTML;
3131

32-
const STABILITY_TEXT_REG_EXP = /(.*:)\s*(\d)([\s\S]*)/;
33-
const DOC_CREATED_REG_EXP = /<!--\s*introduced_in\s*=\s*v([0-9]+)\.([0-9]+)\.([0-9]+)\s*-->/;
34-
35-
// Customized heading without id attribute.
32+
// Make `marked` to not automatically insert id attributes in headings.
3633
const renderer = new marked.Renderer();
37-
renderer.heading = function(text, level) {
38-
return `<h${level}>${text}</h${level}>\n`;
39-
};
40-
marked.setOptions({
41-
renderer: renderer
42-
});
34+
renderer.heading = (text, level) => `<h${level}>${text}</h${level}>\n`;
35+
marked.setOptions({ renderer });
4336

4437
const docPath = path.resolve(__dirname, '..', '..', 'doc');
4538

4639
const gtocPath = path.join(docPath, 'api', '_toc.md');
4740
const gtocMD = fs.readFileSync(gtocPath, 'utf8').replace(/^@\/\/.*$/gm, '');
4841
const gtocHTML = marked(gtocMD).replace(
4942
/<a href="(.*?)"/g,
50-
(all, href) => `<a class="nav-${toID(href)}" href="${href}"`
43+
(all, href) => `<a class="nav-${href.replace('.html', '')
44+
.replace(/\W+/g, '-')}" href="${href}"`
5145
);
5246

5347
const templatePath = path.join(docPath, 'template.html');
5448
const template = fs.readFileSync(templatePath, 'utf8');
5549

56-
var docCreated = null;
57-
var nodeVersion = null;
58-
59-
/**
60-
* opts: input, filename, nodeVersion.
61-
*/
62-
function toHTML(opts, cb) {
63-
nodeVersion = opts.nodeVersion || process.version;
64-
docCreated = opts.input.match(DOC_CREATED_REG_EXP);
65-
66-
const lexed = marked.lexer(opts.input);
67-
render({
68-
lexed: lexed,
69-
filename: opts.filename,
70-
template: template,
71-
nodeVersion: nodeVersion,
72-
analytics: opts.analytics,
73-
}, cb);
74-
}
75-
76-
function toID(filename) {
77-
return filename
78-
.replace('.html', '')
79-
.replace(/[^\w-]/g, '-')
80-
.replace(/-+/g, '-');
81-
}
82-
83-
/**
84-
* opts: lexed, filename, template, nodeVersion.
85-
*/
86-
function render(opts, cb) {
87-
var { lexed, filename, template } = opts;
88-
const nodeVersion = opts.nodeVersion || process.version;
50+
function toHTML({ input, filename, nodeVersion, analytics }, cb) {
51+
filename = path.basename(filename, '.md');
8952

90-
// Get the section.
91-
const section = getSection(lexed);
53+
const lexed = marked.lexer(input);
9254

93-
filename = path.basename(filename, '.md');
55+
const firstHeading = lexed.find(({ type }) => type === 'heading');
56+
const section = firstHeading ? firstHeading.text : 'Index';
9457

95-
parseText(lexed);
96-
lexed = preprocessElements(lexed);
97-
98-
// Generate the table of contents.
99-
// This mutates the lexed contents in-place.
100-
buildToc(lexed, filename, function(er, toc) {
101-
if (er) return cb(er);
102-
103-
const id = toID(path.basename(filename));
104-
105-
template = template.replace(/__ID__/g, id);
106-
template = template.replace(/__FILENAME__/g, filename);
107-
template = template.replace(/__SECTION__/g, section || 'Index');
108-
template = template.replace(/__VERSION__/g, nodeVersion);
109-
template = template.replace(/__TOC__/g, toc);
110-
template = template.replace(
111-
/__GTOC__/g,
112-
gtocHTML.replace(`class="nav-${id}`, `class="nav-${id} active`)
113-
);
114-
115-
if (opts.analytics) {
116-
template = template.replace(
117-
'<!-- __TRACKING__ -->',
118-
analyticsScript(opts.analytics)
119-
);
120-
}
58+
preprocessText(lexed);
59+
preprocessElements(lexed);
12160

122-
template = template.replace(/__ALTDOCS__/, altDocs(filename));
61+
// Generate the table of contents. This mutates the lexed contents in-place.
62+
const toc = buildToc(lexed, filename);
12363

124-
// Content has to be the last thing we do with the lexed tokens,
125-
// because it's destructive.
126-
const content = marked.parser(lexed);
127-
template = template.replace(/__CONTENT__/g, content);
64+
const id = filename.replace(/\W+/g, '-');
12865

129-
cb(null, template);
130-
});
131-
}
66+
let HTML = template.replace('__ID__', id)
67+
.replace(/__FILENAME__/g, filename)
68+
.replace('__SECTION__', section)
69+
.replace(/__VERSION__/g, nodeVersion)
70+
.replace('__TOC__', toc)
71+
.replace('__GTOC__', gtocHTML.replace(
72+
`class="nav-${id}`, `class="nav-${id} active`));
13273

133-
function analyticsScript(analytics) {
134-
return `
74+
if (analytics) {
75+
HTML = HTML.replace('<!-- __TRACKING__ -->', `
13576
<script src="assets/dnt_helper.js"></script>
13677
<script>
13778
if (!_dntEnabled()) {
@@ -143,149 +84,143 @@ function analyticsScript(analytics) {
14384
ga('create', '${analytics}', 'auto');
14485
ga('send', 'pageview');
14586
}
146-
</script>
147-
`;
148-
}
149-
150-
// Replace placeholders in text tokens.
151-
function replaceInText(text) {
152-
return linkJsTypeDocs(linkManPages(text));
153-
}
154-
155-
function altDocs(filename) {
156-
if (!docCreated) {
157-
console.error(`Failed to add alternative version links to ${filename}`);
158-
return '';
159-
}
160-
161-
function lte(v) {
162-
const ns = v.num.split('.');
163-
if (docCreated[1] > +ns[0])
164-
return false;
165-
if (docCreated[1] < +ns[0])
166-
return true;
167-
return docCreated[2] <= +ns[1];
87+
</script>`);
16888
}
16989

170-
const versions = [
171-
{ num: '10.x' },
172-
{ num: '9.x' },
173-
{ num: '8.x', lts: true },
174-
{ num: '7.x' },
175-
{ num: '6.x', lts: true },
176-
{ num: '5.x' },
177-
{ num: '4.x', lts: true },
178-
{ num: '0.12.x' },
179-
{ num: '0.10.x' }
180-
];
181-
182-
const host = 'https://nodejs.org';
183-
const href = (v) => `${host}/docs/latest-v${v.num}/api/${filename}.html`;
184-
185-
function li(v) {
186-
let html = `<li><a href="${href(v)}">${v.num}`;
187-
188-
if (v.lts)
189-
html += ' <b>LTS</b>';
190-
191-
return html + '</a></li>';
90+
const docCreated = input.match(
91+
/<!--\s*introduced_in\s*=\s*v([0-9]+)\.([0-9]+)\.[0-9]+\s*-->/);
92+
if (docCreated) {
93+
HTML = HTML.replace('__ALTDOCS__', altDocs(filename, docCreated));
94+
} else {
95+
console.error(`Failed to add alternative version links to ${filename}`);
96+
HTML = HTML.replace('__ALTDOCS__', '');
19297
}
19398

194-
const lis = versions.filter(lte).map(li).join('\n');
99+
// Content insertion has to be the last thing we do with the lexed tokens,
100+
// because it's destructive.
101+
HTML = HTML.replace('__CONTENT__', marked.parser(lexed));
195102

196-
if (!lis.length)
197-
return '';
198-
199-
return `
200-
<li class="version-picker">
201-
<a href="#">View another version <span>&#x25bc;</span></a>
202-
<ol class="version-picker">${lis}</ol>
203-
</li>
204-
`;
103+
cb(null, HTML);
205104
}
206105

207106
// Handle general body-text replacements.
208107
// For example, link man page references to the actual page.
209-
function parseText(lexed) {
210-
lexed.forEach(function(tok) {
211-
if (tok.type === 'table') {
212-
if (tok.cells) {
213-
tok.cells.forEach((row, x) => {
214-
row.forEach((_, y) => {
215-
if (tok.cells[x] && tok.cells[x][y]) {
216-
tok.cells[x][y] = replaceInText(tok.cells[x][y]);
217-
}
218-
});
219-
});
108+
function preprocessText(lexed) {
109+
lexed.forEach((token) => {
110+
if (token.type === 'table') {
111+
if (token.header) {
112+
token.header = token.header.map(replaceInText);
220113
}
221114

222-
if (tok.header) {
223-
tok.header.forEach((_, i) => {
224-
if (tok.header[i]) {
225-
tok.header[i] = replaceInText(tok.header[i]);
226-
}
115+
if (token.cells) {
116+
token.cells.forEach((row, i) => {
117+
token.cells[i] = row.map(replaceInText);
227118
});
228119
}
229-
} else if (tok.text && tok.type !== 'code') {
230-
tok.text = replaceInText(tok.text);
120+
} else if (token.text && token.type !== 'code') {
121+
token.text = replaceInText(token.text);
231122
}
232123
});
233124
}
234125

126+
// Replace placeholders in text tokens.
127+
function replaceInText(text) {
128+
if (text === '') return text;
129+
return linkJsTypeDocs(linkManPages(text));
130+
}
131+
132+
// Syscalls which appear in the docs, but which only exist in BSD / macOS.
133+
const BSD_ONLY_SYSCALLS = new Set(['lchmod']);
134+
const MAN_PAGE = /(^|\s)([a-z.]+)\((\d)([a-z]?)\)/gm;
135+
136+
// Handle references to man pages, eg "open(2)" or "lchmod(2)".
137+
// Returns modified text, with such refs replaced with HTML links, for example
138+
// '<a href="http://man7.org/linux/man-pages/man2/open.2.html">open(2)</a>'.
139+
function linkManPages(text) {
140+
return text.replace(
141+
MAN_PAGE, (match, beginning, name, number, optionalCharacter) => {
142+
// Name consists of lowercase letters,
143+
// number is a single digit with an optional lowercase letter.
144+
const displayAs = `${name}(${number}${optionalCharacter})`;
145+
146+
if (BSD_ONLY_SYSCALLS.has(name)) {
147+
return `${beginning}<a href="https://www.freebsd.org/cgi/man.cgi` +
148+
`?query=${name}&sektion=${number}">${displayAs}</a>`;
149+
}
150+
return `${beginning}<a href="http://man7.org/linux/man-pages/man${number}` +
151+
`/${name}.${number}${optionalCharacter}.html">${displayAs}</a>`;
152+
});
153+
}
154+
155+
const TYPE_SIGNATURE = /\{[^}]+\}/g;
156+
function linkJsTypeDocs(text) {
157+
const parts = text.split('`');
158+
159+
// Handle types, for example the source Markdown might say
160+
// "This argument should be a {number} or {string}".
161+
for (let i = 0; i < parts.length; i += 2) {
162+
const typeMatches = parts[i].match(TYPE_SIGNATURE);
163+
if (typeMatches) {
164+
typeMatches.forEach((typeMatch) => {
165+
parts[i] = parts[i].replace(typeMatch, typeParser.toLink(typeMatch));
166+
});
167+
}
168+
}
169+
170+
return parts.join('`');
171+
}
172+
235173
// Preprocess stability blockquotes and YAML blocks.
236-
function preprocessElements(input) {
237-
var state = null;
238-
const output = [];
174+
function preprocessElements(lexed) {
175+
const STABILITY_RE = /(.*:)\s*(\d)([\s\S]*)/;
176+
let state = null;
239177
let headingIndex = -1;
240178
let heading = null;
241179

242-
output.links = input.links;
243-
input.forEach(function(tok, index) {
244-
if (tok.type === 'heading') {
180+
lexed.forEach((token, index) => {
181+
if (token.type === 'heading') {
245182
headingIndex = index;
246-
heading = tok;
183+
heading = token;
247184
}
248-
if (tok.type === 'html' && common.isYAMLBlock(tok.text)) {
249-
tok.text = parseYAML(tok.text);
185+
if (token.type === 'html' && common.isYAMLBlock(token.text)) {
186+
token.text = parseYAML(token.text);
250187
}
251-
if (tok.type === 'blockquote_start') {
188+
if (token.type === 'blockquote_start') {
252189
state = 'MAYBE_STABILITY_BQ';
253-
return;
190+
lexed[index] = { type: 'space' };
254191
}
255-
if (tok.type === 'blockquote_end' && state === 'MAYBE_STABILITY_BQ') {
192+
if (token.type === 'blockquote_end' && state === 'MAYBE_STABILITY_BQ') {
256193
state = null;
257-
return;
194+
lexed[index] = { type: 'space' };
258195
}
259-
if (tok.type === 'paragraph' && state === 'MAYBE_STABILITY_BQ') {
260-
if (tok.text.match(/Stability:.*/g)) {
261-
const stabilityMatch = tok.text.match(STABILITY_TEXT_REG_EXP);
262-
const stability = Number(stabilityMatch[2]);
196+
if (token.type === 'paragraph' && state === 'MAYBE_STABILITY_BQ') {
197+
if (token.text.includes('Stability:')) {
198+
const [, prefix, number, explication] = token.text.match(STABILITY_RE);
263199
const isStabilityIndex =
264200
index - 2 === headingIndex || // General.
265201
index - 3 === headingIndex; // With api_metadata block.
266202

267203
if (heading && isStabilityIndex) {
268-
heading.stability = stability;
204+
heading.stability = number;
269205
headingIndex = -1;
270206
heading = null;
271207
}
272-
tok.text = parseAPIHeader(tok.text).replace(/\n/g, ' ');
273-
output.push({ type: 'html', text: tok.text });
274-
return;
208+
token.text = `<div class="api_stability api_stability_${number}">` +
209+
'<a href="documentation.html#documentation_stability_index">' +
210+
`${prefix} ${number}</a>${explication}</div>`
211+
.replace(/\n/g, ' ');
212+
lexed[index] = { type: 'html', text: token.text };
275213
} else if (state === 'MAYBE_STABILITY_BQ') {
276-
output.push({ type: 'blockquote_start' });
277214
state = null;
215+
lexed[index - 1] = { type: 'blockquote_start' };
278216
}
279217
}
280-
output.push(tok);
281218
});
282-
283-
return output;
284219
}
285220

286221
function parseYAML(text) {
287222
const meta = common.extractAndParseYAML(text);
288-
const html = ['<div class="api_metadata">'];
223+
let html = '<div class="api_metadata">\n';
289224

290225
const added = { description: '' };
291226
const deprecated = { description: '' };
@@ -302,159 +237,130 @@ function parseYAML(text) {
302237
}
303238

304239
if (meta.changes.length > 0) {
305-
let changes = meta.changes.slice();
306-
if (added.description) changes.push(added);
307-
if (deprecated.description) changes.push(deprecated);
240+
if (added.description) meta.changes.push(added);
241+
if (deprecated.description) meta.changes.push(deprecated);
308242

309-
changes = changes.sort((a, b) => versionSort(a.version, b.version));
243+
meta.changes.sort((a, b) => versionSort(a.version, b.version));
310244

311-
html.push('<details class="changelog"><summary>History</summary>');
312-
html.push('<table>');
313-
html.push('<tr><th>Version</th><th>Changes</th></tr>');
245+
html += '<details class="changelog"><summary>History</summary>\n' +
246+
'<table>\n<tr><th>Version</th><th>Changes</th></tr>\n';
314247

315-
changes.forEach((change) => {
316-
html.push(`<tr><td>${change.version}</td>`);
317-
html.push(`<td>${marked(change.description)}</td></tr>`);
248+
meta.changes.forEach((change) => {
249+
html += `<tr><td>${change.version}</td>\n` +
250+
`<td>${marked(change.description)}</td></tr>\n`;
318251
});
319252

320-
html.push('</table>');
321-
html.push('</details>');
253+
html += '</table>\n</details>\n';
322254
} else {
323-
html.push(`${added.description}${deprecated.description}`);
324-
}
325-
326-
html.push('</div>');
327-
return html.join('\n');
328-
}
329-
330-
// Syscalls which appear in the docs, but which only exist in BSD / macOS.
331-
const BSD_ONLY_SYSCALLS = new Set(['lchmod']);
332-
333-
// Handle references to man pages, eg "open(2)" or "lchmod(2)".
334-
// Returns modified text, with such refs replaced with HTML links, for example
335-
// '<a href="http://man7.org/linux/man-pages/man2/open.2.html">open(2)</a>'.
336-
function linkManPages(text) {
337-
return text.replace(
338-
/(^|\s)([a-z.]+)\((\d)([a-z]?)\)/gm,
339-
(match, beginning, name, number, optionalCharacter) => {
340-
// Name consists of lowercase letters, number is a single digit.
341-
const displayAs = `${name}(${number}${optionalCharacter})`;
342-
if (BSD_ONLY_SYSCALLS.has(name)) {
343-
return `${beginning}<a href="https://www.freebsd.org/cgi/man.cgi?query=${name}` +
344-
`&sektion=${number}">${displayAs}</a>`;
345-
} else {
346-
return `${beginning}<a href="http://man7.org/linux/man-pages/man${number}` +
347-
`/${name}.${number}${optionalCharacter}.html">${displayAs}</a>`;
348-
}
349-
});
350-
}
351-
352-
function linkJsTypeDocs(text) {
353-
const parts = text.split('`');
354-
var i;
355-
var typeMatches;
356-
357-
// Handle types, for example the source Markdown might say
358-
// "This argument should be a {Number} or {String}".
359-
for (i = 0; i < parts.length; i += 2) {
360-
typeMatches = parts[i].match(/\{([^}]+)\}/g);
361-
if (typeMatches) {
362-
typeMatches.forEach(function(typeMatch) {
363-
parts[i] = parts[i].replace(typeMatch, typeParser.toLink(typeMatch));
364-
});
365-
}
255+
html += `${added.description}${deprecated.description}\n`;
366256
}
367257

368-
// TODO: maybe put more stuff here?
369-
return parts.join('`');
258+
html += '</div>';
259+
return html;
370260
}
371261

372-
function parseAPIHeader(text) {
373-
const classNames = 'api_stability api_stability_$2';
374-
const docsUrl = 'documentation.html#documentation_stability_index';
375-
376-
text = text.replace(
377-
STABILITY_TEXT_REG_EXP,
378-
`<div class="${classNames}"><a href="${docsUrl}">$1 $2</a>$3</div>`
379-
);
380-
return text;
381-
}
382-
383-
// Section is just the first heading.
384-
function getSection(lexed) {
385-
for (var i = 0, l = lexed.length; i < l; i++) {
386-
var tok = lexed[i];
387-
if (tok.type === 'heading') return tok.text;
388-
}
389-
return '';
390-
}
391-
392-
function getMark(anchor) {
393-
return `<span><a class="mark" href="#${anchor}" id="${anchor}">#</a></span>`;
262+
const numberRe = /^\d*/;
263+
function versionSort(a, b) {
264+
a = a.trim();
265+
b = b.trim();
266+
let i = 0; // Common prefix length.
267+
while (i < a.length && i < b.length && a[i] === b[i]) i++;
268+
a = a.substr(i);
269+
b = b.substr(i);
270+
return +b.match(numberRe)[0] - +a.match(numberRe)[0];
394271
}
395272

396-
function buildToc(lexed, filename, cb) {
397-
var toc = [];
398-
var depth = 0;
399-
273+
function buildToc(lexed, filename) {
400274
const startIncludeRefRE = /^\s*<!-- \[start-include:(.+)\] -->\s*$/;
401-
const endIncludeRefRE = /^\s*<!-- \[end-include:(.+)\] -->\s*$/;
275+
const endIncludeRefRE = /^\s*<!-- \[end-include:.+\] -->\s*$/;
402276
const realFilenames = [filename];
403-
404-
lexed.forEach(function(tok) {
405-
// Keep track of the current filename along @include directives.
406-
if (tok.type === 'html') {
407-
let match;
408-
if ((match = tok.text.match(startIncludeRefRE)) !== null)
409-
realFilenames.unshift(match[1]);
410-
else if (tok.text.match(endIncludeRefRE))
277+
const idCounters = Object.create(null);
278+
let toc = '';
279+
let depth = 0;
280+
281+
lexed.forEach((token) => {
282+
// Keep track of the current filename along comment wrappers of inclusions.
283+
if (token.type === 'html') {
284+
const [, includedFileName] = token.text.match(startIncludeRefRE) || [];
285+
if (includedFileName !== undefined)
286+
realFilenames.unshift(includedFileName);
287+
else if (endIncludeRefRE.test(token.text))
411288
realFilenames.shift();
412289
}
413290

414-
if (tok.type !== 'heading') return;
415-
if (tok.depth - depth > 1) {
416-
return cb(new Error('Inappropriate heading level\n' +
417-
JSON.stringify(tok)));
291+
if (token.type !== 'heading') return;
292+
293+
if (token.depth - depth > 1) {
294+
throw new Error(`Inappropriate heading level:\n${JSON.stringify(token)}`);
418295
}
419296

420-
depth = tok.depth;
297+
depth = token.depth;
421298
const realFilename = path.basename(realFilenames[0], '.md');
422-
const apiName = tok.text.trim();
423-
const id = getId(`${realFilename}_${apiName}`);
424-
toc.push(new Array((depth - 1) * 2 + 1).join(' ') +
425-
`* <span class="stability_${tok.stability}">` +
426-
`<a href="#${id}">${tok.text}</a></span>`);
427-
tok.text += getMark(id);
428-
if (realFilename === 'errors' && apiName.startsWith('ERR_')) {
429-
tok.text += getMark(apiName);
299+
const headingText = token.text.trim();
300+
const id = getId(`${realFilename}_${headingText}`, idCounters);
301+
toc += ' '.repeat((depth - 1) * 2) +
302+
`* <span class="stability_${token.stability}">` +
303+
`<a href="#${id}">${token.text}</a></span>\n`;
304+
token.text += `<span><a class="mark" href="#${id}" id="${id}">#</a></span>`;
305+
if (realFilename === 'errors' && headingText.startsWith('ERR_')) {
306+
token.text += `<span><a class="mark" href="#${headingText}" ` +
307+
`id="${headingText}">#</a></span>`;
430308
}
431309
});
432310

433-
toc = marked.parse(toc.join('\n'));
434-
cb(null, toc);
311+
return marked(toc);
435312
}
436313

437-
const idCounters = {};
438-
function getId(text) {
439-
text = text.toLowerCase();
440-
text = text.replace(/[^a-z0-9]+/g, '_');
441-
text = text.replace(/^_+|_+$/, '');
442-
text = text.replace(/^([^a-z])/, '_$1');
443-
if (idCounters.hasOwnProperty(text)) {
444-
text += `_${++idCounters[text]}`;
445-
} else {
446-
idCounters[text] = 0;
314+
const notAlphaNumerics = /[^a-z0-9]+/g;
315+
const edgeUnderscores = /^_+|_+$/g;
316+
const notAlphaStart = /^[^a-z]/;
317+
function getId(text, idCounters) {
318+
text = text.toLowerCase()
319+
.replace(notAlphaNumerics, '_')
320+
.replace(edgeUnderscores, '')
321+
.replace(notAlphaStart, '_$&');
322+
if (idCounters[text] !== undefined) {
323+
return `${text}_${++idCounters[text]}`;
447324
}
325+
idCounters[text] = 0;
448326
return text;
449327
}
450328

451-
const numberRe = /^(\d*)/;
452-
function versionSort(a, b) {
453-
a = a.trim();
454-
b = b.trim();
455-
let i = 0; // Common prefix length.
456-
while (i < a.length && i < b.length && a[i] === b[i]) i++;
457-
a = a.substr(i);
458-
b = b.substr(i);
459-
return +b.match(numberRe)[1] - +a.match(numberRe)[1];
329+
function altDocs(filename, docCreated) {
330+
const [, docCreatedMajor, docCreatedMinor] = docCreated.map(Number);
331+
const host = 'https://nodejs.org';
332+
const versions = [
333+
{ num: '10.x' },
334+
{ num: '9.x' },
335+
{ num: '8.x', lts: true },
336+
{ num: '7.x' },
337+
{ num: '6.x', lts: true },
338+
{ num: '5.x' },
339+
{ num: '4.x', lts: true },
340+
{ num: '0.12.x' },
341+
{ num: '0.10.x' }
342+
];
343+
344+
const getHref = (versionNum) =>
345+
`${host}/docs/latest-v${versionNum}/api/${filename}.html`;
346+
347+
const wrapInListItem = (version) =>
348+
`<li><a href="${getHref(version.num)}">${version.num}` +
349+
`${version.lts ? ' <b>LTS</b>' : ''}</a></li>`;
350+
351+
function isDocInVersion(version) {
352+
const [versionMajor, versionMinor] = version.num.split('.').map(Number);
353+
if (docCreatedMajor > versionMajor) return false;
354+
if (docCreatedMajor < versionMajor) return true;
355+
return docCreatedMinor <= versionMinor;
356+
}
357+
358+
const list = versions.filter(isDocInVersion).map(wrapInListItem).join('\n');
359+
360+
return list ? `
361+
<li class="version-picker">
362+
<a href="#">View another version <span>&#x25bc;</span></a>
363+
<ol class="version-picker">${list}</ol>
364+
</li>
365+
` : '';
460366
}

0 commit comments

Comments
 (0)
Please sign in to comment.