// 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 marked = require('marked'); const path = require('path'); const preprocess = require('./preprocess.js'); const typeParser = require('./type-parser.js'); module.exports = toHTML; const STABILITY_TEXT_REG_EXP = /(.*:)\s*(\d)([\s\S]*)/; // customized heading without id attribute var renderer = new marked.Renderer(); renderer.heading = function(text, level) { return '' + text + '\n'; }; marked.setOptions({ renderer: renderer }); // TODO(chrisdickinson): never stop vomitting / fix this. var gtocPath = path.resolve(path.join( __dirname, '..', '..', 'doc', 'api', '_toc.md' )); var gtocLoading = null; var gtocData = null; /** * opts: input, filename, template, nodeVersion. */ function toHTML(opts, cb) { var template = opts.template; var nodeVersion = opts.nodeVersion || process.version; if (gtocData) { return onGtocLoaded(); } if (gtocLoading === null) { gtocLoading = [onGtocLoaded]; return loadGtoc(function(err, data) { if (err) throw err; gtocData = data; gtocLoading.forEach(function(xs) { xs(); }); }); } if (gtocLoading) { return gtocLoading.push(onGtocLoaded); } function onGtocLoaded() { var lexed = marked.lexer(opts.input); fs.readFile(template, 'utf8', function(er, template) { if (er) return cb(er); render({ lexed: lexed, filename: opts.filename, template: template, nodeVersion: nodeVersion, analytics: opts.analytics, }, cb); }); } } function loadGtoc(cb) { fs.readFile(gtocPath, 'utf8', function(err, data) { if (err) return cb(err); preprocess(gtocPath, data, function(err, data) { if (err) return cb(err); data = marked(data).replace(/', analyticsScript(opts.analytics) ); } // content has to be the last thing we do with // the lexed tokens, because it's destructive. const content = marked.parser(lexed); template = template.replace(/__CONTENT__/g, content); cb(null, template); }); } function analyticsScript(analytics) { return ` `; } // replace placeholders in text tokens function replaceInText(text) { return linkJsTypeDocs(linkManPages(text)); } // handle general body-text replacements // for example, link man page references to the actual page function parseText(lexed) { lexed.forEach(function(tok) { if (tok.type === 'table') { if (tok.cells) { tok.cells.forEach((row, x) => { row.forEach((_, y) => { if (tok.cells[x] && tok.cells[x][y]) { tok.cells[x][y] = replaceInText(tok.cells[x][y]); } }); }); } if (tok.header) { tok.header.forEach((_, i) => { if (tok.header[i]) { tok.header[i] = replaceInText(tok.header[i]); } }); } } else if (tok.text && tok.type !== 'code') { tok.text = replaceInText(tok.text); } }); } // just update the list item text in-place. // lists that come right after a heading are what we're after. function parseLists(input) { var state = null; var savedState = []; var depth = 0; var output = []; let headingIndex = -1; let heading = null; output.links = input.links; input.forEach(function(tok, index) { if (tok.type === 'blockquote_start') { savedState.push(state); state = 'MAYBE_STABILITY_BQ'; return; } if (tok.type === 'blockquote_end' && state === 'MAYBE_STABILITY_BQ') { state = savedState.pop(); return; } if ((tok.type === 'paragraph' && state === 'MAYBE_STABILITY_BQ') || tok.type === 'code') { if (tok.text.match(/Stability:.*/g)) { const stabilityMatch = tok.text.match(STABILITY_TEXT_REG_EXP); const stability = Number(stabilityMatch[2]); const isStabilityIndex = index - 2 === headingIndex || // general index - 3 === headingIndex; // with api_metadata block if (heading && isStabilityIndex) { heading.stability = stability; headingIndex = -1; heading = null; } tok.text = parseAPIHeader(tok.text).replace(/\n/g, ' '); output.push({ type: 'html', text: tok.text }); return; } else if (state === 'MAYBE_STABILITY_BQ') { output.push({ type: 'blockquote_start' }); state = savedState.pop(); } } if (state === null || (state === 'AFTERHEADING' && tok.type === 'heading')) { if (tok.type === 'heading') { headingIndex = index; heading = tok; state = 'AFTERHEADING'; } output.push(tok); return; } if (state === 'AFTERHEADING') { if (tok.type === 'list_start') { state = 'LIST'; if (depth === 0) { output.push({ type: 'html', text: '
' }); } depth++; output.push(tok); return; } if (tok.type === 'html' && common.isYAMLBlock(tok.text)) { tok.text = parseYAML(tok.text); } state = null; output.push(tok); return; } if (state === 'LIST') { if (tok.type === 'list_start') { depth++; output.push(tok); return; } if (tok.type === 'list_end') { depth--; output.push(tok); if (depth === 0) { state = null; output.push({ type: 'html', text: '
' }); } return; } } output.push(tok); }); return output; } function parseYAML(text) { const meta = common.extractAndParseYAML(text); const html = ['
']; const added = { description: '' }; const deprecated = { 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.changes.length > 0) { let changes = meta.changes.slice(); if (added.description) changes.push(added); if (deprecated.description) changes.push(deprecated); changes = changes.sort((a, b) => versionSort(a.version, b.version)); html.push('
History'); html.push(''); html.push(''); changes.forEach((change) => { html.push(``); html.push(``); }); html.push('
VersionChanges
${change.version}${marked(change.description)}
'); html.push('
'); } else { html.push(`${added.description}${deprecated.description}`); } html.push('
'); return html.join('\n'); } // Syscalls which appear in the docs, but which only exist in BSD / OSX var BSD_ONLY_SYSCALLS = new Set(['lchmod']); // Handle references to man pages, eg "open(2)" or "lchmod(2)" // Returns modified text, with such refs replace with HTML links, for example // '
open(2)' function linkManPages(text) { return text.replace( / ([a-z.]+)\((\d)([a-z]?)\)/gm, (match, name, number, optionalCharacter) => { // name consists of lowercase letters, number is a single digit var displayAs = `${name}(${number}${optionalCharacter})`; if (BSD_ONLY_SYSCALLS.has(name)) { return ` ${displayAs}`; } else { return ` ${displayAs}`; } }); } function linkJsTypeDocs(text) { var parts = text.split('`'); var i; var typeMatches; // Handle types, for example the source Markdown might say // "This argument should be a {Number} or {String}" for (i = 0; i < parts.length; i += 2) { typeMatches = parts[i].match(/\{([^}]+)\}/g); if (typeMatches) { typeMatches.forEach(function(typeMatch) { parts[i] = parts[i].replace(typeMatch, typeParser.toLink(typeMatch)); }); } } //XXX maybe put more stuff here? return parts.join('`'); } function parseAPIHeader(text) { const classNames = 'api_stability api_stability_$2'; const docsUrl = 'documentation.html#documentation_stability_index'; text = text.replace( STABILITY_TEXT_REG_EXP, `
$1 $2$3
` ); return text; } // section is just the first heading function getSection(lexed) { for (var i = 0, l = lexed.length; i < l; i++) { var tok = lexed[i]; if (tok.type === 'heading') return tok.text; } return ''; } function buildToc(lexed, filename, cb) { var toc = []; var depth = 0; const startIncludeRefRE = /^\s*\s*$/; const endIncludeRefRE = /^\s*\s*$/; const realFilenames = [filename]; lexed.forEach(function(tok) { // Keep track of the current filename along @include directives. if (tok.type === 'html') { let match; if ((match = tok.text.match(startIncludeRefRE)) !== null) realFilenames.unshift(match[1]); else if (tok.text.match(endIncludeRefRE)) realFilenames.shift(); } if (tok.type !== 'heading') return; if (tok.depth - depth > 1) { return cb(new Error('Inappropriate heading level\n' + JSON.stringify(tok))); } depth = tok.depth; const realFilename = path.basename(realFilenames[0], '.md'); const id = getId(realFilename + '_' + tok.text.trim()); toc.push(new Array((depth - 1) * 2 + 1).join(' ') + '* ' + '' + tok.text + ''); tok.text += '#'; }); toc = marked.parse(toc.join('\n')); cb(null, toc); } var idCounters = {}; function getId(text) { text = text.toLowerCase(); text = text.replace(/[^a-z0-9]+/g, '_'); text = text.replace(/^_+|_+$/, ''); text = text.replace(/^([^a-z])/, '_$1'); if (idCounters.hasOwnProperty(text)) { text += '_' + (++idCounters[text]); } else { idCounters[text] = 0; } return text; } const numberRe = /^(\d*)/; function versionSort(a, b) { a = a.trim(); b = 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)[1] - +a.match(numberRe)[1]; }