// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
const common = require('./common.js');
const fs = require('fs');
const unified = require('unified');
const find = require('unist-util-find');
const visit = require('unist-util-visit');
const markdown = require('remark-parse');
const remark2rehype = require('remark-rehype');
const raw = require('rehype-raw');
const htmlStringify = require('rehype-stringify');
const path = require('path');
const typeParser = require('./type-parser.js');
module.exports = {
toHTML, firstHeader, preprocessText, preprocessElements, buildToc
};
const docPath = path.resolve(__dirname, '..', '..', 'doc');
// Add class attributes to index navigation links.
function navClasses() {
return (tree) => {
visit(tree, { type: 'element', tagName: 'a' }, (node) => {
node.properties.class = 'nav-' +
node.properties.href.replace('.html', '').replace(/\W+/g, '-');
});
};
}
const gtocPath = path.join(docPath, 'api', 'index.md');
const gtocMD = fs.readFileSync(gtocPath, 'utf8').replace(/^/gms, '');
const gtocHTML = unified()
.use(markdown)
.use(remark2rehype, { allowDangerousHTML: true })
.use(raw)
.use(navClasses)
.use(htmlStringify)
.processSync(gtocMD).toString();
const templatePath = path.join(docPath, 'template.html');
const template = fs.readFileSync(templatePath, 'utf8');
function toHTML({ input, content, filename, nodeVersion }, cb) {
filename = path.basename(filename, '.md');
const id = filename.replace(/\W+/g, '-');
let HTML = template.replace('__ID__', id)
.replace(/__FILENAME__/g, filename)
.replace('__SECTION__', content.section)
.replace(/__VERSION__/g, nodeVersion)
.replace('__TOC__', content.toc)
.replace('__GTOC__', gtocHTML.replace(
`class="nav-${id}`, `class="nav-${id} active`))
.replace('__EDIT_ON_GITHUB__', editOnGitHub(filename))
.replace('__CONTENT__', content.toString());
const docCreated = input.match(
//);
if (docCreated) {
HTML = HTML.replace('__ALTDOCS__', altDocs(filename, docCreated));
} else {
console.error(`Failed to add alternative version links to ${filename}`);
HTML = HTML.replace('__ALTDOCS__', '');
}
cb(null, HTML);
}
// Set the section name based on the first header. Default to 'Index'.
function firstHeader() {
return (tree, file) => {
file.section = 'Index';
const heading = find(tree, { type: 'heading' });
if (heading) {
const text = find(heading, { type: 'text' });
if (text) file.section = text.value;
}
};
}
// Handle general body-text replacements.
// For example, link man page references to the actual page.
function preprocessText() {
return (tree) => {
visit(tree, null, (node) => {
if (node.type === 'text' && node.value) {
const value = linkJsTypeDocs(linkManPages(node.value));
if (value !== node.value) {
node.type = 'html';
node.value = value;
}
}
});
};
}
// Syscalls which appear in the docs, but which only exist in BSD / macOS.
const BSD_ONLY_SYSCALLS = new Set(['lchmod']);
const LINUX_DIE_ONLY_SYSCALLS = new Set(['uname']);
const HAXX_ONLY_SYSCALLS = new Set(['curl']);
const MAN_PAGE = /(^|\s)([a-z.]+)\((\d)([a-z]?)\)/gm;
// Handle references to man pages, eg "open(2)" or "lchmod(2)".
// Returns modified text, with such refs replaced with HTML links, for example
// 'open(2)'.
function linkManPages(text) {
return text.replace(
MAN_PAGE, (match, beginning, name, number, optionalCharacter) => {
// Name consists of lowercase letters,
// number is a single digit with an optional lowercase letter.
const displayAs = `${name}(${number}${optionalCharacter})
`;
if (BSD_ONLY_SYSCALLS.has(name)) {
return `${beginning}${displayAs}`;
}
if (LINUX_DIE_ONLY_SYSCALLS.has(name)) {
return `${beginning}${displayAs}`;
}
if (HAXX_ONLY_SYSCALLS.has(name)) {
return `${beginning}${displayAs}`;
}
return `${beginning}${displayAs}`;
});
}
const TYPE_SIGNATURE = /\{[^}]+\}/g;
function linkJsTypeDocs(text) {
const parts = text.split('`');
// Handle types, for example the source Markdown might say
// "This argument should be a {number} or {string}".
for (let i = 0; i < parts.length; i += 2) {
const typeMatches = parts[i].match(TYPE_SIGNATURE);
if (typeMatches) {
typeMatches.forEach((typeMatch) => {
parts[i] = parts[i].replace(typeMatch, typeParser.toLink(typeMatch));
});
}
}
return parts.join('`');
}
// Preprocess headers, stability blockquotes, and YAML blocks.
function preprocessElements({ filename }) {
return (tree) => {
const STABILITY_RE = /(.*:)\s*(\d)([\s\S]*)/;
let headingIndex = -1;
let heading = null;
visit(tree, null, (node, index) => {
if (node.type === 'heading') {
headingIndex = index;
heading = node;
} else if (node.type === 'html' && common.isYAMLBlock(node.value)) {
node.value = parseYAML(node.value);
} else if (node.type === 'blockquote') {
const paragraph = node.children[0].type === 'paragraph' &&
node.children[0];
const text = paragraph && paragraph.children[0].type === 'text' &&
paragraph.children[0];
if (text && text.value.includes('Stability:')) {
const [, prefix, number, explication] =
text.value.match(STABILITY_RE);
const isStabilityIndex =
index - 2 === headingIndex || // General.
index - 3 === headingIndex; // With api_metadata block.
if (heading && isStabilityIndex) {
heading.stability = number;
headingIndex = -1;
heading = null;
}
// Do not link to the section we are already in.
const noLinking = filename.includes('documentation') &&
heading !== null && heading.children[0].value === 'Stability Index';
// Collapse blockquote and paragraph into a single node
node.type = 'paragraph';
node.children.shift();
node.children.unshift(...paragraph.children);
// Insert div with prefix and number
node.children.unshift({
type: 'html',
value: `