diff --git a/Makefile b/Makefile index 9a5bbf2f79c497..7dd885f02d0791 100644 --- a/Makefile +++ b/Makefile @@ -284,7 +284,7 @@ out/doc/api/%.json: doc/api/%.md [ -x $(NODE) ] && $(NODE) $(gen-json) || node $(gen-json) # check if ./node is actually set, else use user pre-installed binary -gen-html = tools/doc/generate.js --node-version=$(FULLVERSION) --format=html --template=doc/template.html $< > $@ +gen-html = tools/doc/generate.js --node-version=$(FULLVERSION) --format=html --template=doc/template.html $< out/doc/api/%.html: doc/api/%.md [ -x $(NODE) ] && $(NODE) $(gen-html) || node $(gen-html) diff --git a/doc/guides/_toc.md b/doc/guides/_toc.md new file mode 100644 index 00000000000000..3c0c22ede141fd --- /dev/null +++ b/doc/guides/_toc.md @@ -0,0 +1,11 @@ +* [About these Docs](documentation.html) +* [Usage & Example](synopsis.html) + +
+ +* [Building Node With Ninja](building-node-with-ninja.html) + +
+ +* [GitHub Repo & Issue Tracker](https://github.com/nodejs/node) +* [Mailing List](http://groups.google.com/group/nodejs) diff --git a/test/doctool/test-doctool-html.js b/test/doctool/test-doctool-html.js index a8476b7234c9fa..75360362dcf144 100644 --- a/test/doctool/test-doctool-html.js +++ b/test/doctool/test-doctool-html.js @@ -5,7 +5,10 @@ const assert = require('assert'); const fs = require('fs'); const path = require('path'); -const html = require('../../tools/doc/html.js'); +common.globalCheck = false; + +const processIncludes = require('../../tools/doc/preprocess.js'); +const html = require('../../tools/doc/html.js').toHTML; // Test data is a list of objects with two properties. // The file property is the file path. @@ -53,22 +56,43 @@ const testData = [ '

Describe Something in more detail here. ' + '

' }, + { + file: path.join(common.fixturesDir, 'doc_with_includes.md'), + html: '' + + '

Look here!

' + + '' + + '' + + '

foobar#

' + + '

I exist and am being linked to.

' + + '' + }, ]; testData.forEach(function(item) { // Normalize expected data by stripping whitespace const expected = item.html.replace(/\s/g, ''); - fs.readFile(item.file, 'utf8', common.mustCall(function(err, input) { + fs.readFile(item.file, 'utf8', common.mustCall((err, input) => { assert.ifError(err); - html(input, 'foo', 'doc/template.html', - common.mustCall(function(err, output) { - assert.ifError(err); + processIncludes(item.file, input, common.mustCall((err, preprocessed) => { + assert.ifError(err); + + html( + { + input: preprocessed, + filename: 'foo', + template: 'doc/template.html', + nodeVersion: process.version, + }, + common.mustCall((err, output) => { + assert.ifError(err); - const actual = output.replace(/\s/g, ''); - // Assert that the input stripped of all whitespace contains the - // expected list - assert.notEqual(actual.indexOf(expected), -1); - })); + const actual = output.replace(/\s/g, ''); + // Assert that the input stripped of all whitespace contains the + // expected list + assert.notEqual(actual.indexOf(expected), -1); + })); + })); })); }); diff --git a/tools/doc/generate.js b/tools/doc/generate.js index 7df987e1cf78f9..29bb36442d7572 100644 --- a/tools/doc/generate.js +++ b/tools/doc/generate.js @@ -2,6 +2,8 @@ const processIncludes = require('./preprocess.js'); const fs = require('fs'); +const path = require('path'); +const toHTML = require('./html.js').toHTML; // parse the args. // Don't use nopt or whatever for this. It's simple enough. @@ -48,11 +50,15 @@ function next(er, input) { break; case 'html': - require('./html.js')(input, inputFile, template, nodeVersion, - function(er, html) { - if (er) throw er; - console.log(html); + toHTML(input, inputFile, template, nodeVersion, (er, html) => { + if (er) throw er; + const filename = `./out/doc/api/${path.basename(inputFile, 'md')}html`; + fs.writeFile(filename, html, (err) => { + if (err) + console.log(err); + console.log('generated:', filename); }); + }); break; default: diff --git a/tools/doc/html.js b/tools/doc/html.js index ef7d78d5b70ab3..048fa65be9ec63 100644 --- a/tools/doc/html.js +++ b/tools/doc/html.js @@ -1,312 +1,2 @@ '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; - -// 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; - -function toHTML(input, filename, template, nodeVersion, cb) { - if (typeof nodeVersion === 'function') { - cb = nodeVersion; - nodeVersion = null; - } - nodeVersion = 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(input); - fs.readFile(template, 'utf8', function(er, template) { - if (er) return cb(er); - render(lexed, filename, template, nodeVersion, 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(/' }); - } - 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 = ['
']; - - if (meta.added) { - html.push(`Added in: ${meta.added.join(', ')}`); - } - - if (meta.deprecated) { - html.push(`Deprecated since: ${meta.deprecated.join(', ')} `); - } - - 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)\)/gm, function(match, name, number) { - // name consists of lowercase letters, number is a single digit - var displayAs = name + '(' + number + ')'; - 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) { - text = text.replace( - /(.*:)\s(\d)([\s\S]*)/, - '
$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; - lexed.forEach(function(tok) { - if (tok.type !== 'heading') return; - if (tok.depth - depth > 1) { - return cb(new Error('Inappropriate heading level\n' + - JSON.stringify(tok))); - } - - depth = tok.depth; - var id = getId(filename + '_' + 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; -} +module.exports.toHTML = require('./lib/toHTML'); diff --git a/tools/doc/lib/buildToc.js b/tools/doc/lib/buildToc.js new file mode 100644 index 00000000000000..2a62eff7368bd4 --- /dev/null +++ b/tools/doc/lib/buildToc.js @@ -0,0 +1,41 @@ +'use strict'; +const marked = require('marked'); +const getId = require('./getId.js'); + +module.exports = 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); +}; diff --git a/tools/doc/lib/getId.js b/tools/doc/lib/getId.js new file mode 100644 index 00000000000000..0642262a8188dc --- /dev/null +++ b/tools/doc/lib/getId.js @@ -0,0 +1,14 @@ +'use strict'; +var idCounters = {}; +module.exports = 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; +}; diff --git a/tools/doc/lib/getSection.js b/tools/doc/lib/getSection.js new file mode 100644 index 00000000000000..0754766c6af47b --- /dev/null +++ b/tools/doc/lib/getSection.js @@ -0,0 +1,9 @@ +'use strict'; +// section is just the first heading +module.exports = 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 ''; +}; diff --git a/tools/doc/lib/linkJsTypeDocs.js b/tools/doc/lib/linkJsTypeDocs.js new file mode 100644 index 00000000000000..7066333c9006ef --- /dev/null +++ b/tools/doc/lib/linkJsTypeDocs.js @@ -0,0 +1,22 @@ +'use strict'; +const typeParser = require('../type-parser.js'); + +module.exports = 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('`'); +}; diff --git a/tools/doc/lib/linkManPages.js b/tools/doc/lib/linkManPages.js new file mode 100644 index 00000000000000..b221f7fa3a8a02 --- /dev/null +++ b/tools/doc/lib/linkManPages.js @@ -0,0 +1,20 @@ +'use strict'; +// 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)' +module.exports = function linkManPages(text) { + return text.replace(/ ([a-z]+)\((\d)\)/gm, function(match, name, number) { + // name consists of lowercase letters, number is a single digit + var displayAs = name + '(' + number + ')'; + if (BSD_ONLY_SYSCALLS.has(name)) { + return ' ' + displayAs + ''; + } else { + return ' ' + displayAs + ''; + } + }); +}; diff --git a/tools/doc/lib/loadGtoc.js b/tools/doc/lib/loadGtoc.js new file mode 100644 index 00000000000000..44aae27195c73f --- /dev/null +++ b/tools/doc/lib/loadGtoc.js @@ -0,0 +1,34 @@ +'use strict'; +const path = require('path'); +const fs = require('fs'); +const marked = require('marked'); +const preprocess = require('../preprocess.js'); +const toID = require('./toID.js'); +// TODO(chrisdickinson): never stop vomitting / fix this. +var gtocPath = path.resolve(path.join( + __dirname, + '..', + '..', + '..', + 'doc', + 'api', + '_toc.md' +)); +global.global.gtocLoading = null; +global.gtocData = null; + + +exports.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(/$1 $2$3' + ); + return text; +}; diff --git a/tools/doc/lib/parseLists.js b/tools/doc/lib/parseLists.js new file mode 100644 index 00000000000000..f737443afef2d8 --- /dev/null +++ b/tools/doc/lib/parseLists.js @@ -0,0 +1,63 @@ +'use strict'; +const common = require('../common.js'); +const parseAPIHeader = require('./parseAPIHeader.js'); +const parseYAML = require('./parseYAML.js'); +// just update the list item text in-place. +// lists that come right after a heading are what we're after. +module.exports = function parseLists(input) { + var state = null; + var depth = 0; + var output = []; + output.links = input.links; + input.forEach(function(tok) { + if (tok.type === 'code' && tok.text.match(/Stability:.*/g)) { + tok.text = parseAPIHeader(tok.text); + output.push({ type: 'html', text: tok.text }); + return; + } + if (state === null || + (state === 'AFTERHEADING' && tok.type === 'heading')) { + if (tok.type === 'heading') { + 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; +}; diff --git a/tools/doc/lib/parseText.js b/tools/doc/lib/parseText.js new file mode 100644 index 00000000000000..09d07ef611121b --- /dev/null +++ b/tools/doc/lib/parseText.js @@ -0,0 +1,13 @@ +'use strict'; +const linkManPages = require('./linkManPages'); +const linkJsTypeDocs = require('./linkJsTypeDocs'); +// handle general body-text replacements +// for example, link man page references to the actual page +module.exports = function parseText(lexed) { + lexed.forEach(function(tok) { + if (tok.text && tok.type !== 'code') { + tok.text = linkManPages(tok.text); + tok.text = linkJsTypeDocs(tok.text); + } + }); +}; diff --git a/tools/doc/lib/parseYAML.js b/tools/doc/lib/parseYAML.js new file mode 100644 index 00000000000000..4f161f3bd7ccf8 --- /dev/null +++ b/tools/doc/lib/parseYAML.js @@ -0,0 +1,18 @@ +'use strict'; +const common = require('../common.js'); + +module.exports = function parseYAML(text) { + const meta = common.extractAndParseYAML(text); + const html = ['
']; + + if (meta.added) { + html.push(`Added in: ${meta.added.join(', ')}`); + } + + if (meta.deprecated) { + html.push(`Deprecated since: ${meta.deprecated.join(', ')} `); + } + + html.push('
'); + return html.join('\n'); +}; diff --git a/tools/doc/lib/render.js b/tools/doc/lib/render.js new file mode 100644 index 00000000000000..6d1d190968c5da --- /dev/null +++ b/tools/doc/lib/render.js @@ -0,0 +1,50 @@ +'use strict'; +const path = require('path'); +const marked = require('marked'); +const getSection = require('./getSection'); +const parseText = require('./parseText'); +const parseLists = require('./parseLists'); +const buildToc = require('./buildToc'); +const toID = require('./toID.js'); + +module.exports = function render(lexed, filename, template, nodeVersion, cb) { + if (typeof nodeVersion === 'function') { + cb = nodeVersion; + nodeVersion = null; + } + + nodeVersion = nodeVersion || process.version; + + // get the section + var section = getSection(lexed); + + filename = path.basename(filename, '.md'); + + parseText(lexed); + lexed = parseLists(lexed); + + // generate the table of contents. + // this mutates the lexed contents in-place. + buildToc(lexed, filename, function(er, toc) { + if (er) return cb(er); + + var id = toID(path.basename(filename)); + + template = template.replace(/__ID__/g, id); + template = template.replace(/__FILENAME__/g, filename); + template = template.replace(/__SECTION__/g, section); + template = template.replace(/__VERSION__/g, nodeVersion); + template = template.replace(/__TOC__/g, toc); + template = template.replace( + /__GTOC__/g, + global.gtocData.replace(`class="nav-${id}`, `class="nav-${id} active`) + ); + + // 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); + }); +}; diff --git a/tools/doc/lib/toHTML.js b/tools/doc/lib/toHTML.js new file mode 100644 index 00000000000000..ca74b1c66d1bd8 --- /dev/null +++ b/tools/doc/lib/toHTML.js @@ -0,0 +1,50 @@ +'use strict'; +const fs = require('fs'); +const loadGtoc = require('./loadGtoc').loadGtoc; +const render = require('./render'); +const marked = require('marked'); + +// customized heading without id attribute +var renderer = new marked.Renderer(); +renderer.heading = function(text, level) { + return '' + text + '\n'; +}; +marked.setOptions({ + renderer: renderer +}); + + +module.exports = function toHTML(input, filename, template, nodeVersion, cb) { + if (typeof nodeVersion === 'function') { + cb = nodeVersion; + nodeVersion = null; + } + nodeVersion = nodeVersion || process.version; + + if (global.gtocData) { + return onGtocLoaded(); + } + + if (global.gtocLoading === null) { + global.gtocLoading = [onGtocLoaded]; + return loadGtoc(function(err, data) { + if (err) throw err; + global.gtocData = data; + global.gtocLoading.forEach(function(xs) { + xs(); + }); + }); + } + + if (global.gtocLoading) { + return global.gtocLoading.push(onGtocLoaded); + } + + function onGtocLoaded() { + var lexed = marked.lexer(input); + fs.readFile(template, 'utf8', function(er, template) { + if (er) return cb(er); + render(lexed, filename, template, nodeVersion, cb); + }); + } +}; diff --git a/tools/doc/lib/toID.js b/tools/doc/lib/toID.js new file mode 100644 index 00000000000000..3c92eb33950497 --- /dev/null +++ b/tools/doc/lib/toID.js @@ -0,0 +1,7 @@ +'use strict'; +module.exports = function toID(filename) { + return filename + .replace('.html', '') + .replace(/[^\w\-]/g, '-') + .replace(/-+/g, '-'); +}; diff --git a/tools/doc/preprocess.js b/tools/doc/preprocess.js index 295737a2a53aee..ad5a19af799fc0 100644 --- a/tools/doc/preprocess.js +++ b/tools/doc/preprocess.js @@ -48,7 +48,10 @@ function processIncludes(inputFile, input, cb) { if (errState) return; if (er) return cb(errState = er); incCount--; - includeData[fname] = inc; + // Add comments to let the HTML generator know how the anchors for + // headings should look like. + includeData[fname] = `\n` + + inc + `\n\n`; input = input.split(include + '\n').join(includeData[fname] + '\n'); if (incCount === 0) { return cb(null, input);