// 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 getVersions = require('./versions.js'); 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'); async 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__', await 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: `
` + (noLinking ? '' : '') + `${prefix} ${number}${noLinking ? '' : ''}` .replace(/\n/g, ' ') }); // Remove prefix and number from text text.value = explication; // close div node.children.push({ type: 'html', value: '
' }); } } }); }; } function parseYAML(text) { const meta = common.extractAndParseYAML(text); let result = '
\n'; const added = { description: '' }; const deprecated = { description: '' }; const removed = { description: '' }; if (meta.added) { added.version = meta.added.join(', '); added.description = `Added in: ${added.version}`; } if (meta.deprecated) { deprecated.version = meta.deprecated.join(', '); deprecated.description = `Deprecated since: ${deprecated.version}`; } if (meta.removed) { removed.version = meta.removed.join(', '); removed.description = `Removed in: ${removed.version}`; } if (meta.changes.length > 0) { if (added.description) meta.changes.push(added); if (deprecated.description) meta.changes.push(deprecated); if (removed.description) meta.changes.push(removed); meta.changes.sort((a, b) => versionSort(a.version, b.version)); result += '
History\n' + '\n\n'; meta.changes.forEach((change) => { const description = unified() .use(markdown) .use(remark2rehype, { allowDangerousHTML: true }) .use(raw) .use(htmlStringify) .processSync(change.description).toString(); const version = common.arrify(change.version).join(', '); result += `\n` + `\n`; }); result += '
VersionChanges
${version}${description}
\n
\n'; } else { result += `${added.description}${deprecated.description}` + `${removed.description}\n`; } if (meta.napiVersion) { result += `N-API version: ${meta.napiVersion.join(', ')}\n`; } result += '
'; return result; } function minVersion(a) { return common.arrify(a).reduce((min, e) => { return !min || versionSort(min, e) < 0 ? e : min; }); } const numberRe = /^\d*/; function versionSort(a, b) { a = minVersion(a).trim(); b = minVersion(b).trim(); let i = 0; // Common prefix length. while (i < a.length && i < b.length && a[i] === b[i]) i++; a = a.substr(i); b = b.substr(i); return +b.match(numberRe)[0] - +a.match(numberRe)[0]; } function buildToc({ filename, apilinks }) { return (tree, file) => { const startIncludeRefRE = /^\s*\s*$/; const endIncludeRefRE = /^\s*\s*$/; const realFilenames = [filename]; const idCounters = Object.create(null); let toc = ''; let depth = 0; visit(tree, null, (node) => { // Keep track of the current filename for comment wrappers of inclusions. if (node.type === 'html') { const [, includedFileName] = node.value.match(startIncludeRefRE) || []; if (includedFileName !== undefined) realFilenames.unshift(includedFileName); else if (endIncludeRefRE.test(node.value)) realFilenames.shift(); } if (node.type !== 'heading') return; if (node.depth - depth > 1) { throw new Error( `Inappropriate heading level:\n${JSON.stringify(node)}` ); } depth = node.depth; const realFilename = path.basename(realFilenames[0], '.md'); const headingText = file.contents.slice( node.children[0].position.start.offset, node.position.end.offset).trim(); const id = getId(`${realFilename}_${headingText}`, idCounters); const hasStability = node.stability !== undefined; toc += ' '.repeat((depth - 1) * 2) + (hasStability ? `* ` : '* ') + `${headingText}${hasStability ? '' : ''}\n`; let anchor = `#`; if (realFilename === 'errors' && headingText.startsWith('ERR_')) { anchor += `#`; } const api = headingText.replace(/^.*:\s+/, '').replace(/\(.*/, ''); if (apilinks[api]) { anchor = `[src]${anchor}`; } node.children.push({ type: 'html', value: anchor }); }); file.toc = unified() .use(markdown) .use(remark2rehype, { allowDangerousHTML: true }) .use(raw) .use(htmlStringify) .processSync(toc).toString(); }; } const notAlphaNumerics = /[^a-z0-9]+/g; const edgeUnderscores = /^_+|_+$/g; const notAlphaStart = /^[^a-z]/; function getId(text, idCounters) { text = text.toLowerCase() .replace(notAlphaNumerics, '_') .replace(edgeUnderscores, '') .replace(notAlphaStart, '_$&'); if (idCounters[text] !== undefined) { return `${text}_${++idCounters[text]}`; } idCounters[text] = 0; return text; } async function altDocs(filename, docCreated) { const [, docCreatedMajor, docCreatedMinor] = docCreated.map(Number); const host = 'https://nodejs.org'; const versions = await getVersions.versions(); const getHref = (versionNum) => `${host}/docs/latest-v${versionNum}/api/${filename}.html`; const wrapInListItem = (version) => `
  • ${version.num}` + `${version.lts ? ' LTS' : ''}
  • `; function isDocInVersion(version) { const [versionMajor, versionMinor] = version.num.split('.').map(Number); if (docCreatedMajor > versionMajor) return false; if (docCreatedMajor < versionMajor) return true; if (Number.isNaN(versionMinor)) return true; return docCreatedMinor <= versionMinor; } const list = versions.filter(isDocInVersion).map(wrapInListItem).join('\n'); return list ? `
  • View another version
      ${list}
  • ` : ''; } // eslint-disable-next-line max-len const githubLogo = ''; function editOnGitHub(filename) { return `
  • ${githubLogo}Edit on GitHub
  • `; }