// 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. import fs from 'fs'; import path from 'path'; import highlightJs from 'highlight.js'; import raw from 'rehype-raw'; import htmlStringify from 'rehype-stringify'; import gfm from 'remark-gfm'; import markdown from 'remark-parse'; import remark2rehype from 'remark-rehype'; import unified from 'unified'; import { visit } from 'unist-util-visit'; import * as common from './common.mjs'; import * as typeParser from './type-parser.mjs'; const { highlight, getLanguage } = highlightJs; const docPath = new URL('../../doc/', import.meta.url); // 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 = new URL('./api/index.md', docPath); const gtocMD = fs.readFileSync(gtocPath, 'utf8') .replace(/\(([^#?]+?)\.md\)/ig, (_, filename) => `(${filename}.html)`) .replace(/^/gms, ''); const gtocHTML = unified() .use(markdown) .use(gfm) .use(remark2rehype, { allowDangerousHtml: true }) .use(raw) .use(navClasses) .use(htmlStringify) .processSync(gtocMD).toString(); const templatePath = new URL('./template.html', docPath); const template = fs.readFileSync(templatePath, 'utf8'); function processContent(content) { content = content.toString(); // Increment header tag levels to avoid multiple h1 tags in a doc. // This means we can't already have an
. if (content.includes('
')) { throw new Error('Cannot increment a level 6 header'); } // `++level` to convert the string to a number and increment it. content = content.replace(/(?<=<\/?h)[1-5](?=[^<>]*>)/g, (level) => ++level); // Wrap h3 tags in section tags. let firstTime = true; return content .replace(/

{ if (firstTime) { firstTime = false; return '
' + heading; } return '
' + heading; }) + (firstTime ? '' : '
'); } export function toHTML({ input, content, filename, nodeVersion, versions }) { 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__', processContent(content)); const docCreated = input.match( //); if (docCreated) { HTML = HTML.replace('__ALTDOCS__', altDocs(filename, docCreated, versions)); } else { console.error(`Failed to add alternative version links to ${filename}`); HTML = HTML.replace('__ALTDOCS__', ''); } return HTML; } // Set the section name based on the first header. Default to 'Index'. export function firstHeader() { return (tree, file) => { let heading; visit(tree, (node) => { if (node.type === 'heading') { heading = node; return false; } }); if (heading && heading.children.length) { const recursiveTextContent = (node) => node.value || node.children.map(recursiveTextContent).join(''); file.section = recursiveTextContent(heading); } else { file.section = 'Index'; } }; } // Handle general body-text replacements. // For example, link man page references to the actual page. export function preprocessText({ nodeVersion }) { return (tree) => { visit(tree, null, (node) => { if (common.isSourceLink(node.value)) { const [path] = node.value.match(/(?<=)/); node.value = `

Source Code: ${path}

`; } else 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 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}`; } 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('`'); } const isJSFlavorSnippet = (node) => node.lang === 'cjs' || node.lang === 'mjs'; // Preprocess headers, stability blockquotes, and YAML blocks. export function preprocessElements({ filename }) { return (tree) => { const STABILITY_RE = /(.*:)\s*(\d)([\s\S]*)/; let headingIndex = -1; let heading = null; visit(tree, null, (node, index, parent) => { if (node.type === 'heading') { headingIndex = index; heading = node; } else if (node.type === 'code') { if (!node.lang) { console.warn( `No language set in ${filename}, line ${node.position.start.line}` ); } const className = isJSFlavorSnippet(node) ? `language-js ${node.lang}` : `language-${node.lang}`; const highlighted = `${(getLanguage(node.lang || '') ? highlight(node.value, { language: node.lang }) : node).value}`; node.type = 'html'; if (isJSFlavorSnippet(node)) { const previousNode = parent.children[index - 1] || {}; const nextNode = parent.children[index + 1] || {}; if (!isJSFlavorSnippet(previousNode) && isJSFlavorSnippet(nextNode) && nextNode.lang !== node.lang) { // Saving the highlight code as value to be added in the next node. node.value = highlighted; } else if (isJSFlavorSnippet(previousNode)) { node.value = '
' +
              '' +
              previousNode.value +
              highlighted +
              '
'; node.lang = null; previousNode.value = ''; previousNode.lang = null; } else { // Isolated JS snippet, no need to add the checkbox. node.value = `
${highlighted}
`; } } else { node.value = `
${highlighted}
`; } } 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); // Stability indices are never more than 3 nodes away from their // heading. const isStabilityIndex = index - headingIndex <= 3; 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(gfm) .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]; } const DEPRECATION_HEADING_PATTERN = /^DEP\d+:/; export function buildToc({ filename, apilinks }) { return (tree, file) => { const idCounters = Object.create(null); let toc = ''; let depth = 0; visit(tree, null, (node) => { 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(filename, '.md'); const headingText = file.contents.slice( node.children[0].position.start.offset, node.position.end.offset).trim(); const id = getId(`${realFilename}_${headingText}`, idCounters); const isDeprecationHeading = DEPRECATION_HEADING_PATTERN.test(headingText); if (isDeprecationHeading) { if (!node.data) node.data = {}; if (!node.data.hProperties) node.data.hProperties = {}; node.data.hProperties.id = headingText.substring(0, headingText.indexOf(':')); } 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 }); }); if (toc !== '') { file.toc = '
Table of contents' + unified() .use(markdown) .use(gfm) .use(remark2rehype, { allowDangerousHtml: true }) .use(raw) .use(htmlStringify) .processSync(toc).toString() + '
'; } else { file.toc = ''; } }; } 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; } function altDocs(filename, docCreated, versions) { const [, docCreatedMajor, docCreatedMinor] = docCreated.map(Number); const host = 'https://nodejs.org'; 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}
  • ` : ''; } function editOnGitHub(filename) { return `
  • Edit on GitHub
  • `; }