mirror of https://gitee.com/openkylin/nodejs.git
build doc using marked and js-yaml
While waiting for unified/remarked/rehyped modules to be available in debian Forwarded: not-needed Reviewed-By: Xavier Guimard <yadd@debian.org> Last-Update: 2022-01-27 Gbp-Pq: Name make-doc.patch
This commit is contained in:
parent
83f4cd3794
commit
5b0d2b1398
45
Makefile
45
Makefile
|
@ -358,14 +358,6 @@ endif
|
|||
|
||||
node_use_openssl = $(call available-node,"-p" \
|
||||
"process.versions.openssl != undefined")
|
||||
test/addons/.docbuildstamp: $(DOCBUILDSTAMP_PREREQS) tools/doc/node_modules
|
||||
@if [ "$(shell $(node_use_openssl))" != "true" ]; then \
|
||||
echo "Skipping .docbuildstamp (no crypto)"; \
|
||||
else \
|
||||
$(RM) -r test/addons/??_*/; \
|
||||
[ -x $(NODE) ] && $(NODE) $< || node $< ; \
|
||||
touch $@; \
|
||||
fi
|
||||
|
||||
ADDONS_BINDING_GYPS := \
|
||||
$(filter-out test/addons/??_*/binding.gyp, \
|
||||
|
@ -600,12 +592,6 @@ test-hash-seed: all
|
|||
|
||||
.PHONY: test-doc
|
||||
test-doc: doc-only lint ## Builds, lints, and verifies the docs.
|
||||
@if [ "$(shell $(node_use_openssl))" != "true" ]; then \
|
||||
echo "Skipping test-doc (no crypto)"; \
|
||||
else \
|
||||
$(PYTHON) tools/test.py $(PARALLEL_ARGS) doctool; \
|
||||
fi
|
||||
$(NODE) tools/doc/checkLinks.js .
|
||||
|
||||
test-known-issues: all
|
||||
$(PYTHON) tools/test.py $(PARALLEL_ARGS) known_issues
|
||||
|
@ -712,7 +698,7 @@ tools/doc/node_modules: tools/doc/package.json
|
|||
fi
|
||||
|
||||
.PHONY: doc-only
|
||||
doc-only: tools/doc/node_modules \
|
||||
doc-only: \
|
||||
$(apidoc_dirs) $(apiassets) ## Builds the docs with the local or the global Node.js binary.
|
||||
@if [ "$(shell $(node_use_openssl))" != "true" ]; then \
|
||||
echo "Skipping doc-only (no crypto)"; \
|
||||
|
@ -730,7 +716,9 @@ out/doc:
|
|||
# Just copy everything under doc/api over.
|
||||
out/doc/api: doc/api
|
||||
mkdir -p $@
|
||||
cp -r doc/api out/doc
|
||||
cp -r doc/api out/doc/
|
||||
rm -f out/doc/api/*.html
|
||||
rm -f out/doc/api/*.json
|
||||
|
||||
# If it's a source tarball, assets are already in doc/api/assets
|
||||
out/doc/api/assets:
|
||||
|
@ -744,27 +732,16 @@ out/doc/api/assets/%: doc/api_assets/% | out/doc/api/assets
|
|||
|
||||
run-npm-ci = $(PWD)/$(NPM) ci
|
||||
|
||||
LINK_DATA = out/doc/apilinks.json
|
||||
VERSIONS_DATA = out/previous-doc-versions.json
|
||||
gen-api = tools/doc/generate.js --node-version=$(FULLVERSION) \
|
||||
--apilinks=$(LINK_DATA) $< --output-directory=out/doc/api \
|
||||
--versions-file=$(VERSIONS_DATA)
|
||||
gen-apilink = tools/doc/apilinks.js $(LINK_DATA) $(wildcard lib/*.js)
|
||||
gen-json = tools/doc/generate.js --format=json $< > $@
|
||||
gen-html = tools/doc/generate.js --node-version=$(FULLVERSION) --format=html $< > $@
|
||||
|
||||
$(LINK_DATA): $(wildcard lib/*.js) tools/doc/apilinks.js | out/doc
|
||||
$(call available-node, $(gen-apilink))
|
||||
out/doc/api/%.json: doc/api/%.md tools/doc/generate.js tools/doc/json.js
|
||||
$(call available-node, $(gen-json))
|
||||
|
||||
# Regenerate previous versions data if the current version changes
|
||||
$(VERSIONS_DATA): CHANGELOG.md src/node_version.h tools/doc/versions.js
|
||||
$(call available-node, tools/doc/versions.js $@)
|
||||
out/doc/api/%.html: doc/api/%.md tools/doc/generate.js tools/doc/html.js
|
||||
$(call available-node, $(gen-html))
|
||||
|
||||
out/doc/api/%.json out/doc/api/%.html: doc/api/%.md tools/doc/generate.js \
|
||||
tools/doc/markdown.js tools/doc/html.js tools/doc/json.js \
|
||||
tools/doc/apilinks.js $(VERSIONS_DATA) | $(LINK_DATA) out/doc/api
|
||||
$(call available-node, $(gen-api))
|
||||
|
||||
out/doc/api/all.html: $(apidocs_html) tools/doc/allhtml.js \
|
||||
tools/doc/apilinks.js | out/doc/api
|
||||
out/doc/api/all.html: $(apidocs_html) tools/doc/allhtml.js
|
||||
$(call available-node, tools/doc/allhtml.js)
|
||||
|
||||
out/doc/api/all.json: $(apidocs_json) tools/doc/alljson.js | out/doc/api
|
||||
|
|
|
@ -6,33 +6,28 @@
|
|||
// Modify the require paths in the js code to pull from the build tree.
|
||||
// Triggered from the build-addons target in the Makefile and vcbuild.bat.
|
||||
|
||||
const { mkdir, writeFile } = require('fs');
|
||||
const { mkdir, readFileSync, writeFile } = require('fs');
|
||||
const { resolve } = require('path');
|
||||
const vfile = require('to-vfile');
|
||||
const unified = require('unified');
|
||||
const remarkParse = require('remark-parse');
|
||||
const { lexer } = require('marked');
|
||||
|
||||
const rootDir = resolve(__dirname, '..', '..');
|
||||
const doc = resolve(rootDir, 'doc', 'api', 'addons.md');
|
||||
const verifyDir = resolve(rootDir, 'test', 'addons');
|
||||
|
||||
const file = vfile.readSync(doc, 'utf8');
|
||||
const tree = unified().use(remarkParse).parse(file);
|
||||
const tokens = lexer(readFileSync(doc, 'utf8'));
|
||||
const addons = {};
|
||||
let id = 0;
|
||||
let currentHeader;
|
||||
|
||||
const validNames = /^\/\/\s+(.*\.(?:cc|h|js))[\r\n]/;
|
||||
tree.children.forEach((node) => {
|
||||
if (node.type === 'heading') {
|
||||
currentHeader = file.contents.slice(
|
||||
node.children[0].position.start.offset,
|
||||
node.position.end.offset);
|
||||
tokens.forEach(({ type, text }) => {
|
||||
if (type === 'heading') {
|
||||
currentHeader = text;
|
||||
addons[currentHeader] = { files: {} };
|
||||
} else if (node.type === 'code') {
|
||||
const match = node.value.match(validNames);
|
||||
if (match !== null) {
|
||||
addons[currentHeader].files[match[1]] = node.value;
|
||||
addons[currentHeader].files[match[1]] = text;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -32,7 +32,7 @@ for (const link of toc.match(/<a.*?>/g)) {
|
|||
|
||||
// Split the doc.
|
||||
const match = /(<\/ul>\s*)?<\/div>\s*<div id="apicontent">/.exec(data);
|
||||
|
||||
if (!match) console.info(source, link)
|
||||
contents += data.slice(0, match.index)
|
||||
.replace(/[\s\S]*?<div id="toc">\s*<h2>.*?<\/h2>\s*(<ul>\s*)?/, '');
|
||||
|
||||
|
@ -84,5 +84,5 @@ while (match = idRe.exec(all)) {
|
|||
|
||||
const hrefRe = / href="#(\w+)"/g;
|
||||
while (match = hrefRe.exec(all)) {
|
||||
if (!ids.has(match[1])) throw new Error(`link not found: ${match[1]}`);
|
||||
if (!ids.has(match[1])) console.warn(`link not found: ${match[1]}`);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const yaml =
|
||||
require(`${__dirname}/../node_modules/eslint/node_modules/js-yaml`);
|
||||
require('js-yaml');
|
||||
|
||||
function isYAMLBlock(text) {
|
||||
return /^<!-- YAML/.test(text);
|
||||
|
@ -38,10 +38,6 @@ function extractAndParseYAML(text) {
|
|||
meta.deprecated = arrify(meta.deprecated);
|
||||
}
|
||||
|
||||
if (meta.removed) {
|
||||
meta.removed = arrify(meta.removed);
|
||||
}
|
||||
|
||||
meta.changes = meta.changes || [];
|
||||
|
||||
return meta;
|
||||
|
|
|
@ -21,110 +21,52 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
const { promises: fs } = require('fs');
|
||||
const path = require('path');
|
||||
const unified = require('unified');
|
||||
const markdown = require('remark-parse');
|
||||
const remark2rehype = require('remark-rehype');
|
||||
const raw = require('rehype-raw');
|
||||
const htmlStringify = require('rehype-stringify');
|
||||
|
||||
const { replaceLinks } = require('./markdown');
|
||||
const linksMapper = require('./links-mapper');
|
||||
const html = require('./html');
|
||||
const json = require('./json');
|
||||
const fs = require('fs');
|
||||
|
||||
// Parse the args.
|
||||
// Don't use nopt or whatever for this. It's simple enough.
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
let filename = null;
|
||||
let format = 'json';
|
||||
let nodeVersion = null;
|
||||
let outputDir = null;
|
||||
let apilinks = {};
|
||||
let versions = {};
|
||||
|
||||
async function main() {
|
||||
for (const arg of args) {
|
||||
if (!arg.startsWith('--')) {
|
||||
filename = arg;
|
||||
} else if (arg.startsWith('--node-version=')) {
|
||||
nodeVersion = arg.replace(/^--node-version=/, '');
|
||||
} else if (arg.startsWith('--output-directory=')) {
|
||||
outputDir = arg.replace(/^--output-directory=/, '');
|
||||
} else if (arg.startsWith('--apilinks=')) {
|
||||
const linkFile = arg.replace(/^--apilinks=/, '');
|
||||
const data = await fs.readFile(linkFile, 'utf8');
|
||||
if (!data.trim()) {
|
||||
throw new Error(`${linkFile} is empty`);
|
||||
}
|
||||
apilinks = JSON.parse(data);
|
||||
} else if (arg.startsWith('--versions-file=')) {
|
||||
const versionsFile = arg.replace(/^--versions-file=/, '');
|
||||
const data = await fs.readFile(versionsFile, 'utf8');
|
||||
if (!data.trim()) {
|
||||
throw new Error(`${versionsFile} is empty`);
|
||||
}
|
||||
versions = JSON.parse(data);
|
||||
}
|
||||
args.forEach((arg) => {
|
||||
if (!arg.startsWith('--')) {
|
||||
filename = arg;
|
||||
} else if (arg.startsWith('--node-version=')) {
|
||||
nodeVersion = arg.replace(/^--node-version=/, '');
|
||||
} else if (arg.startsWith('--format=')) {
|
||||
format = arg.replace(/^--format=/, '');
|
||||
}
|
||||
});
|
||||
|
||||
nodeVersion = nodeVersion || process.version;
|
||||
nodeVersion = nodeVersion || process.version;
|
||||
|
||||
if (!filename) {
|
||||
throw new Error('No input file specified');
|
||||
} else if (!outputDir) {
|
||||
throw new Error('No output directory specified');
|
||||
}
|
||||
|
||||
const input = await fs.readFile(filename, 'utf8');
|
||||
|
||||
const content = await unified()
|
||||
.use(replaceLinks, { filename, linksMapper })
|
||||
.use(markdown)
|
||||
.use(html.preprocessText, { nodeVersion })
|
||||
.use(json.jsonAPI, { filename })
|
||||
.use(html.firstHeader)
|
||||
.use(html.preprocessElements, { filename })
|
||||
.use(html.buildToc, { filename, apilinks })
|
||||
.use(remark2rehype, { allowDangerousHtml: true })
|
||||
.use(raw)
|
||||
.use(htmlStringify)
|
||||
.process(input);
|
||||
|
||||
const myHtml = await html.toHTML({ input, content, filename, nodeVersion,
|
||||
versions });
|
||||
const basename = path.basename(filename, '.md');
|
||||
const htmlTarget = path.join(outputDir, `${basename}.html`);
|
||||
const jsonTarget = path.join(outputDir, `${basename}.json`);
|
||||
|
||||
return Promise.allSettled([
|
||||
fs.writeFile(htmlTarget, myHtml),
|
||||
fs.writeFile(jsonTarget, JSON.stringify(content.json, null, 2)),
|
||||
]);
|
||||
if (!filename) {
|
||||
throw new Error('No input file specified');
|
||||
}
|
||||
|
||||
main()
|
||||
.then((tasks) => {
|
||||
// Filter rejected tasks
|
||||
const errors = tasks.filter(({ status }) => status === 'rejected')
|
||||
.map(({ reason }) => reason);
|
||||
fs.readFile(filename, 'utf8', (er, input) => {
|
||||
if (er) throw er;
|
||||
|
||||
// Log errors
|
||||
for (const error of errors) {
|
||||
console.error(error);
|
||||
}
|
||||
switch (format) {
|
||||
case 'json':
|
||||
require('./json.js')(input, filename, (er, obj) => {
|
||||
if (er) throw er;
|
||||
console.log(JSON.stringify(obj, null, 2));
|
||||
});
|
||||
break;
|
||||
|
||||
// Exit process with code 1 if some errors
|
||||
if (errors.length > 0) {
|
||||
return process.exit(1);
|
||||
}
|
||||
|
||||
// Else with code 0
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
case 'html':
|
||||
require('./html').toHTML({ input, filename, nodeVersion, versions },
|
||||
(err, html) => {
|
||||
if (err) throw err;
|
||||
console.log(html);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Invalid format: ${format}`);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -23,109 +23,109 @@
|
|||
|
||||
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 marked = require('marked');
|
||||
const path = require('path');
|
||||
const typeParser = require('./type-parser.js');
|
||||
const { highlight, getLanguage } = require('highlight.js');
|
||||
|
||||
module.exports = {
|
||||
toHTML, firstHeader, preprocessText, preprocessElements, buildToc
|
||||
toHTML, preprocessText, preprocessElements, buildToc
|
||||
};
|
||||
|
||||
// Make `marked` to not automatically insert id attributes in headings.
|
||||
const renderer = new marked.Renderer();
|
||||
renderer.heading = (text, level) => `<h${level}>${text}</h${level}>\n`;
|
||||
marked.setOptions({ renderer });
|
||||
|
||||
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 gtocHTML = marked.parse(gtocMD).replace(
|
||||
/<a href="(.*?)"/g,
|
||||
(all, href) => `<a class="nav-${href.replace('.html', '')
|
||||
.replace(/\W+/g, '-')}" href="${href}"`
|
||||
);
|
||||
|
||||
const templatePath = path.join(docPath, 'template.html');
|
||||
const template = fs.readFileSync(templatePath, 'utf8');
|
||||
|
||||
function toHTML({ input, content, filename, nodeVersion, versions }) {
|
||||
async function toHTML({ input, filename, nodeVersion, versions }, cb) {
|
||||
filename = path.basename(filename, '.md');
|
||||
|
||||
const lexed = marked.lexer(input);
|
||||
|
||||
const firstHeading = lexed.find(({ type }) => type === 'heading');
|
||||
const section = firstHeading ? firstHeading.text : 'Index';
|
||||
|
||||
preprocessText(lexed);
|
||||
preprocessElements(lexed, filename);
|
||||
|
||||
// Generate the table of contents. This mutates the lexed contents in-place.
|
||||
const toc = buildToc(lexed, filename);
|
||||
let content = "";
|
||||
|
||||
const id = filename.replace(/\W+/g, '-');
|
||||
|
||||
let HTML = template.replace('__ID__', id)
|
||||
.replace(/__FILENAME__/g, filename)
|
||||
.replace('__SECTION__', content.section)
|
||||
.replace('__SECTION__', section)
|
||||
.replace(/__VERSION__/g, nodeVersion)
|
||||
.replace('__TOC__', content.toc)
|
||||
.replace('__TOC__', toc)
|
||||
.replace('__GTOC__', gtocHTML.replace(
|
||||
`class="nav-${id}"`, `class="nav-${id} active"`))
|
||||
.replace('__EDIT_ON_GITHUB__', editOnGitHub(filename))
|
||||
.replace('__CONTENT__', content.toString());
|
||||
`class="nav-${id}`, `class="nav-${id} active`));
|
||||
|
||||
const docCreated = input.match(
|
||||
/<!--\s*introduced_in\s*=\s*v([0-9]+)\.([0-9]+)\.[0-9]+\s*-->/);
|
||||
if (docCreated) {
|
||||
HTML = HTML.replace('__ALTDOCS__', altDocs(filename, docCreated, versions));
|
||||
HTML = HTML.replace('__ALTDOCS__', await altDocs(filename, docCreated, []));
|
||||
} else {
|
||||
console.error(`Failed to add alternative version links to ${filename}`);
|
||||
HTML = HTML.replace('__ALTDOCS__', '');
|
||||
}
|
||||
|
||||
return HTML;
|
||||
}
|
||||
HTML = HTML.replace('__EDIT_ON_GITHUB__', editOnGitHub(filename));
|
||||
|
||||
// Set the section name based on the first header. Default to 'Index'.
|
||||
function firstHeader() {
|
||||
return (tree, file) => {
|
||||
const heading = find(tree, { type: 'heading' });
|
||||
// Content insertion has to be the last thing we do with the lexed tokens,
|
||||
// because it's destructive.
|
||||
HTML = HTML.replace('__CONTENT__', marked.parser(lexed));
|
||||
|
||||
if (heading && heading.children.length) {
|
||||
const recursiveTextContent = (node) =>
|
||||
node.value || node.children.map(recursiveTextContent).join('');
|
||||
file.section = recursiveTextContent(heading);
|
||||
} else {
|
||||
file.section = 'Index';
|
||||
}
|
||||
};
|
||||
cb(null, HTML);
|
||||
}
|
||||
|
||||
// Handle general body-text replacements.
|
||||
// For example, link man page references to the actual page.
|
||||
function preprocessText({ nodeVersion }) {
|
||||
return (tree) => {
|
||||
visit(tree, null, (node) => {
|
||||
if (common.isSourceLink(node.value)) {
|
||||
const [path] = node.value.match(/(?<=<!-- source_link=).*(?= -->)/);
|
||||
node.value = `<p><strong>Source Code:</strong> <a href="https://github.com/nodejs/node/blob/${nodeVersion}/${path}">${path}</a></p>`;
|
||||
} else if (node.type === 'text' && node.value) {
|
||||
const value = linkJsTypeDocs(linkManPages(node.value));
|
||||
if (value !== node.value) {
|
||||
node.type = 'html';
|
||||
node.value = value;
|
||||
function preprocessText(lexed) {
|
||||
lexed.forEach((token) => {
|
||||
if (token.type === 'table') {
|
||||
if (token.header) {
|
||||
for (const tok of token.header) {
|
||||
tok.text = replaceInText(tok.text);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (token.cells) {
|
||||
token.cells.forEach((row, i) => {
|
||||
for (const tok of token.cells[i]) {
|
||||
tok.text = replaceInText(tok.text);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (token.text && token.type !== 'code') {
|
||||
token.text = replaceInText(token.text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Replace placeholders in text tokens.
|
||||
function replaceInText(text = '') {
|
||||
if (text === '') return text;
|
||||
return linkJsTypeDocs(linkManPages(text));
|
||||
}
|
||||
|
||||
// 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)".
|
||||
|
@ -142,13 +142,20 @@ function linkManPages(text) {
|
|||
return `${beginning}<a href="https://www.freebsd.org/cgi/man.cgi` +
|
||||
`?query=${name}&sektion=${number}">${displayAs}</a>`;
|
||||
}
|
||||
if (LINUX_DIE_ONLY_SYSCALLS.has(name)) {
|
||||
return `${beginning}<a href="https://linux.die.net/man/` +
|
||||
`${number}/${name}">${displayAs}</a>`;
|
||||
}
|
||||
if (HAXX_ONLY_SYSCALLS.has(name)) {
|
||||
return `${beginning}<a href="https://${name}.haxx.se/docs/manpage.html">${displayAs}</a>`;
|
||||
}
|
||||
|
||||
return `${beginning}<a href="http://man7.org/linux/man-pages/man${number}` +
|
||||
`/${name}.${number}${optionalCharacter}.html">${displayAs}</a>`;
|
||||
});
|
||||
}
|
||||
|
||||
const TYPE_SIGNATURE = /\{[^}]+\}/g;
|
||||
const TYPE_SIGNATURE = /[^_]\{[^}]+\}(?!_)/g;
|
||||
function linkJsTypeDocs(text) {
|
||||
const parts = text.split('`');
|
||||
|
||||
|
@ -162,95 +169,69 @@ function linkJsTypeDocs(text) {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
// Preprocess stability blockquotes and YAML blocks.
|
||||
function preprocessElements(lexed, filename) {
|
||||
const STABILITY_RE = /(.*:)\s*(\d)([\s\S]*)/;
|
||||
let state = null;
|
||||
let headingIndex = -1;
|
||||
let heading = null;
|
||||
|
||||
visit(tree, null, (node, index) => {
|
||||
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}`);
|
||||
lexed.forEach((token, index) => {
|
||||
if (token.type === 'heading') {
|
||||
headingIndex = index;
|
||||
heading = token;
|
||||
}
|
||||
if (token.type === 'html' && common.isYAMLBlock(token.text)) {
|
||||
token.text = parseYAML(token.text);
|
||||
}
|
||||
if (token.type === 'blockquote_start') {
|
||||
state = 'MAYBE_STABILITY_BQ';
|
||||
lexed[index] = { type: 'space' };
|
||||
}
|
||||
if (token.type === 'blockquote_end' && state === 'MAYBE_STABILITY_BQ') {
|
||||
state = null;
|
||||
lexed[index] = { type: 'space' };
|
||||
}
|
||||
if (token.type === 'paragraph' && state === 'MAYBE_STABILITY_BQ') {
|
||||
if (token.text.includes('Stability:')) {
|
||||
const [, prefix, number, explication] = token.text.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;
|
||||
}
|
||||
const language = (node.lang || '').split(' ')[0];
|
||||
const highlighted = getLanguage(language) ?
|
||||
highlight(language, node.value).value :
|
||||
node.value;
|
||||
node.type = 'html';
|
||||
node.value = '<pre>' +
|
||||
`<code class = 'language-${node.lang}'>` +
|
||||
highlighted +
|
||||
'</code></pre>';
|
||||
} 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);
|
||||
// Do not link to the section we are already in.
|
||||
const noLinking = filename === 'documentation' &&
|
||||
heading !== null && heading.text === 'Stability Index';
|
||||
token.text = `<div class="api_stability api_stability_${number}">` +
|
||||
(noLinking ? '' :
|
||||
'<a href="documentation.html#documentation_stability_index">') +
|
||||
`${prefix} ${number}${noLinking ? '' : '</a>'}${explication}</div>`
|
||||
.replace(/\n/g, ' ');
|
||||
|
||||
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: `<div class="api_stability api_stability_${number}">` +
|
||||
(noLinking ? '' :
|
||||
'<a href="documentation.html#documentation_stability_index">') +
|
||||
`${prefix} ${number}${noLinking ? '' : '</a>'}`
|
||||
.replace(/\n/g, ' ')
|
||||
});
|
||||
|
||||
// Remove prefix and number from text
|
||||
text.value = explication;
|
||||
|
||||
// close div
|
||||
node.children.push({ type: 'html', value: '</div>' });
|
||||
}
|
||||
lexed[index] = { type: 'html', text: token.text };
|
||||
} else if (state === 'MAYBE_STABILITY_BQ') {
|
||||
state = null;
|
||||
lexed[index - 1] = { type: 'blockquote_start' };
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function parseYAML(text) {
|
||||
const meta = common.extractAndParseYAML(text);
|
||||
let result = '<div class="api_metadata">\n';
|
||||
let html = '<div class="api_metadata">\n';
|
||||
|
||||
const added = { description: '' };
|
||||
const deprecated = { description: '' };
|
||||
const removed = { description: '' };
|
||||
|
||||
if (meta.added) {
|
||||
added.version = meta.added.join(', ');
|
||||
|
@ -263,47 +244,33 @@ function parseYAML(text) {
|
|||
`<span>Deprecated since: ${deprecated.version}</span>`;
|
||||
}
|
||||
|
||||
if (meta.removed) {
|
||||
removed.version = meta.removed.join(', ');
|
||||
removed.description = `<span>Removed in: ${removed.version}</span>`;
|
||||
}
|
||||
|
||||
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 += '<details class="changelog"><summary>History</summary>\n' +
|
||||
html += '<details class="changelog"><summary>History</summary>\n' +
|
||||
'<table>\n<tr><th>Version</th><th>Changes</th></tr>\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 += `<tr><td>${version}</td>\n` +
|
||||
`<td>${description}</td></tr>\n`;
|
||||
html += `<tr><td>${version}</td>\n` +
|
||||
`<td>${marked.parse(change.description)}</td></tr>\n`;
|
||||
});
|
||||
|
||||
result += '</table>\n</details>\n';
|
||||
html += '</table>\n</details>\n';
|
||||
} else {
|
||||
result += `${added.description}${deprecated.description}` +
|
||||
`${removed.description}\n`;
|
||||
html += `${added.description}${deprecated.description}\n`;
|
||||
}
|
||||
|
||||
if (meta.napiVersion) {
|
||||
result += `<span>N-API version: ${meta.napiVersion.join(', ')}</span>\n`;
|
||||
html += `<span>N-API version: ${meta.napiVersion.join(', ')}</span>\n`;
|
||||
}
|
||||
|
||||
result += '</div>';
|
||||
return result;
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function minVersion(a) {
|
||||
|
@ -323,56 +290,49 @@ function versionSort(a, b) {
|
|||
return +b.match(numberRe)[0] - +a.match(numberRe)[0];
|
||||
}
|
||||
|
||||
function buildToc({ filename, apilinks }) {
|
||||
return (tree, file) => {
|
||||
const idCounters = Object.create(null);
|
||||
let toc = '';
|
||||
let depth = 0;
|
||||
function buildToc(lexed, filename) {
|
||||
const startIncludeRefRE = /^\s*<!-- \[start-include:(.+)\] -->\s*$/;
|
||||
const endIncludeRefRE = /^\s*<!-- \[end-include:.+\] -->\s*$/;
|
||||
const realFilenames = [filename];
|
||||
const idCounters = Object.create(null);
|
||||
let toc = '';
|
||||
let depth = 0;
|
||||
|
||||
visit(tree, null, (node) => {
|
||||
if (node.type !== 'heading') return;
|
||||
lexed.forEach((token) => {
|
||||
// Keep track of the current filename along comment wrappers of inclusions.
|
||||
if (token.type === 'html') {
|
||||
const [, includedFileName] = token.text.match(startIncludeRefRE) || [];
|
||||
if (includedFileName !== undefined)
|
||||
realFilenames.unshift(includedFileName);
|
||||
else if (endIncludeRefRE.test(token.text))
|
||||
realFilenames.shift();
|
||||
}
|
||||
|
||||
if (node.depth - depth > 1) {
|
||||
throw new Error(
|
||||
`Inappropriate heading level:\n${JSON.stringify(node)}`
|
||||
);
|
||||
}
|
||||
if (token.type !== 'heading') return;
|
||||
|
||||
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);
|
||||
if (token.depth - depth > 1) {
|
||||
throw new Error(`Inappropriate heading level:\n${JSON.stringify(token)}`);
|
||||
}
|
||||
|
||||
const hasStability = node.stability !== undefined;
|
||||
toc += ' '.repeat((depth - 1) * 2) +
|
||||
(hasStability ? `* <span class="stability_${node.stability}">` : '* ') +
|
||||
`<a href="#${id}">${headingText}</a>${hasStability ? '</span>' : ''}\n`;
|
||||
depth = token.depth;
|
||||
const realFilename = path.basename(realFilenames[0], '.md');
|
||||
const headingText = token.text.trim();
|
||||
const id = getId(`${realFilename}_${headingText}`, idCounters);
|
||||
|
||||
let anchor =
|
||||
`<span><a class="mark" href="#${id}" id="${id}">#</a></span>`;
|
||||
const hasStability = token.stability !== undefined;
|
||||
toc += ' '.repeat((depth - 1) * 2) +
|
||||
(hasStability ? `* <span class="stability_${token.stability}">` : '* ') +
|
||||
`<a href="#${id}">${token.text}</a>${hasStability ? '</span>' : ''}\n`;
|
||||
|
||||
if (realFilename === 'errors' && headingText.startsWith('ERR_')) {
|
||||
anchor += `<span><a class="mark" href="#${headingText}" ` +
|
||||
`id="${headingText}">#</a></span>`;
|
||||
}
|
||||
let text = `<span><a class="mark" href="#${id}" id="${id}">#</a></span>`;
|
||||
if (realFilename === 'errors' && headingText.startsWith('ERR_')) {
|
||||
text += `<span><a class="mark" href="#${headingText}" ` +
|
||||
`id="${headingText}">#</a></span>`;
|
||||
}
|
||||
token.tokens.push({ type: 'text', text });
|
||||
});
|
||||
|
||||
const api = headingText.replace(/^.*:\s+/, '').replace(/\(.*/, '');
|
||||
if (apilinks[api]) {
|
||||
anchor = `<a class="srclink" href=${apilinks[api]}>[src]</a>${anchor}`;
|
||||
}
|
||||
|
||||
node.children.push({ type: 'html', value: anchor });
|
||||
});
|
||||
|
||||
file.toc = unified()
|
||||
.use(markdown)
|
||||
.use(remark2rehype, { allowDangerousHtml: true })
|
||||
.use(raw)
|
||||
.use(htmlStringify)
|
||||
.processSync(toc).toString();
|
||||
};
|
||||
return marked.parse(toc);
|
||||
}
|
||||
|
||||
const notAlphaNumerics = /[^a-z0-9]+/g;
|
||||
|
|
|
@ -21,267 +21,302 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
const unified = require('unified');
|
||||
module.exports = doJSON;
|
||||
|
||||
// Take the lexed input, and return a JSON-encoded object.
|
||||
// A module looks like this: https://gist.github.com/1777387.
|
||||
|
||||
const common = require('./common.js');
|
||||
const html = require('remark-html');
|
||||
const { selectAll } = require('unist-util-select');
|
||||
const marked = require('marked');
|
||||
|
||||
module.exports = { jsonAPI };
|
||||
// Customized heading without id attribute.
|
||||
const renderer = new marked.Renderer();
|
||||
renderer.heading = (text, level) => `<h${level}>${text}</h${level}>\n`;
|
||||
marked.setOptions({ renderer });
|
||||
|
||||
// Unified processor: input is https://github.com/syntax-tree/mdast,
|
||||
// output is: https://gist.github.com/1777387.
|
||||
function jsonAPI({ filename }) {
|
||||
return (tree, file) => {
|
||||
|
||||
const exampleHeading = /^example/i;
|
||||
const metaExpr = /<!--([^=]+)=([^-]+)-->\n*/g;
|
||||
const stabilityExpr = /^Stability: ([0-5])(?:\s*-\s*)?(.*)$/s;
|
||||
function doJSON(input, filename, cb) {
|
||||
const root = { source: filename };
|
||||
const stack = [root];
|
||||
let depth = 0;
|
||||
let current = root;
|
||||
let state = null;
|
||||
|
||||
// Extract definitions.
|
||||
const definitions = selectAll('definition', tree);
|
||||
const exampleHeading = /^example:/i;
|
||||
const metaExpr = /<!--([^=]+)=([^-]+)-->\n*/g;
|
||||
const stabilityExpr = /^Stability: ([0-5])(?:\s*-\s*)?(.*)$/s;
|
||||
|
||||
// Determine the start, stop, and depth of each section.
|
||||
const sections = [];
|
||||
let section = null;
|
||||
tree.children.forEach((node, i) => {
|
||||
if (node.type === 'heading' &&
|
||||
!exampleHeading.test(textJoin(node.children, file))) {
|
||||
if (section) section.stop = i - 1;
|
||||
section = { start: i, stop: tree.children.length, depth: node.depth };
|
||||
sections.push(section);
|
||||
}
|
||||
});
|
||||
const lexed = marked.lexer(input);
|
||||
lexed.forEach((tok) => {
|
||||
const { type } = tok;
|
||||
let { text } = tok;
|
||||
|
||||
// Collect and capture results.
|
||||
const result = { type: 'module', source: filename };
|
||||
while (sections.length > 0) {
|
||||
doSection(sections.shift(), result);
|
||||
// <!-- name=module -->
|
||||
// This is for cases where the markdown semantic structure is lacking.
|
||||
if (type === 'paragraph' || type === 'html') {
|
||||
text = text.replace(metaExpr, (_0, key, value) => {
|
||||
current[key.trim()] = value.trim();
|
||||
return '';
|
||||
});
|
||||
text = text.trim();
|
||||
if (!text) return;
|
||||
}
|
||||
file.json = result;
|
||||
|
||||
// Process a single section (recursively, including subsections).
|
||||
function doSection(section, parent) {
|
||||
if (section.depth - parent.depth > 1) {
|
||||
throw new Error('Inappropriate heading level\n' +
|
||||
JSON.stringify(section));
|
||||
if (type === 'heading' && !exampleHeading.test(text.trim())) {
|
||||
if (tok.depth - depth > 1) {
|
||||
return cb(
|
||||
new Error(`Inappropriate heading level\n${JSON.stringify(tok)}`));
|
||||
}
|
||||
|
||||
const current = newSection(tree.children[section.start], file);
|
||||
let nodes = tree.children.slice(section.start + 1, section.stop + 1);
|
||||
|
||||
// Sometimes we have two headings with a single blob of description.
|
||||
// Treat as a clone.
|
||||
if (
|
||||
nodes.length === 0 && sections.length > 0 &&
|
||||
section.depth === sections[0].depth
|
||||
) {
|
||||
nodes = tree.children.slice(sections[0].start + 1,
|
||||
sections[0].stop + 1);
|
||||
}
|
||||
|
||||
// Extract (and remove) metadata that is not directly inferable
|
||||
// from the markdown itself.
|
||||
nodes.forEach((node, i) => {
|
||||
// Input: <!-- name=module -->; output: {name: module}.
|
||||
if (node.type === 'html') {
|
||||
node.value = node.value.replace(metaExpr, (_0, key, value) => {
|
||||
current[key.trim()] = value.trim();
|
||||
return '';
|
||||
});
|
||||
if (!node.value.trim()) delete nodes[i];
|
||||
}
|
||||
|
||||
// Process metadata:
|
||||
// <!-- YAML
|
||||
// added: v1.0.0
|
||||
// -->
|
||||
if (node.type === 'html' && common.isYAMLBlock(node.value)) {
|
||||
current.meta = common.extractAndParseYAML(node.value);
|
||||
delete nodes[i];
|
||||
}
|
||||
|
||||
// Stability marker: > Stability: ...
|
||||
if (
|
||||
node.type === 'blockquote' && node.children.length === 1 &&
|
||||
node.children[0].type === 'paragraph' &&
|
||||
nodes.slice(0, i).every((node) => node.type === 'list')
|
||||
) {
|
||||
const text = textJoin(node.children[0].children, file);
|
||||
const stability = text.match(stabilityExpr);
|
||||
if (stability) {
|
||||
current.stability = parseInt(stability[1], 10);
|
||||
current.stabilityText = stability[2].trim();
|
||||
delete nodes[i];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Compress the node array.
|
||||
nodes = nodes.filter(() => true);
|
||||
|
||||
// If the first node is a list, extract it.
|
||||
const list = nodes[0] && nodes[0].type === 'list' ?
|
||||
nodes.shift() : null;
|
||||
|
||||
// Now figure out what this list actually means.
|
||||
// Depending on the section type, the list could be different things.
|
||||
const values = list ?
|
||||
list.children.map((child) => parseListItem(child, file)) : [];
|
||||
|
||||
switch (current.type) {
|
||||
case 'ctor':
|
||||
case 'classMethod':
|
||||
case 'method':
|
||||
// Each item is an argument, unless the name is 'return',
|
||||
// in which case it's the return value.
|
||||
const sig = {};
|
||||
sig.params = values.filter((value) => {
|
||||
if (value.name === 'return') {
|
||||
sig.return = value;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
parseSignature(current.textRaw, sig);
|
||||
current.signatures = [sig];
|
||||
break;
|
||||
|
||||
case 'property':
|
||||
// There should be only one item, which is the value.
|
||||
// Copy the data up to the section.
|
||||
if (values.length) {
|
||||
const signature = values[0];
|
||||
|
||||
// Shove the name in there for properties,
|
||||
// since they are always just going to be the value etc.
|
||||
signature.textRaw = `\`${current.name}\` ${signature.textRaw}`;
|
||||
|
||||
for (const key in signature) {
|
||||
if (signature[key]) {
|
||||
if (key === 'type') {
|
||||
current.typeof = signature.type;
|
||||
} else {
|
||||
current[key] = signature[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'event':
|
||||
// Event: each item is an argument.
|
||||
current.params = values;
|
||||
break;
|
||||
|
||||
default:
|
||||
// If list wasn't consumed, put it back in the nodes list.
|
||||
if (list) nodes.unshift(list);
|
||||
}
|
||||
|
||||
// Convert remaining nodes to a 'desc'.
|
||||
// Unified expects to process a string; but we ignore that as we
|
||||
// already have pre-parsed input that we can inject.
|
||||
if (nodes.length) {
|
||||
if (current.desc) current.shortDesc = current.desc;
|
||||
|
||||
current.desc = unified()
|
||||
.use(function() {
|
||||
this.Parser = () => (
|
||||
{ type: 'root', children: nodes.concat(definitions) }
|
||||
);
|
||||
})
|
||||
.use(html)
|
||||
.processSync('').toString().trim();
|
||||
if (!current.desc) delete current.desc;
|
||||
}
|
||||
|
||||
// Process subsections.
|
||||
while (sections.length > 0 && sections[0].depth > section.depth) {
|
||||
doSection(sections.shift(), current);
|
||||
}
|
||||
|
||||
// If type is not set, default type based on parent type, and
|
||||
// set displayName and name properties.
|
||||
if (!current.type) {
|
||||
current.type = (parent.type === 'misc' ? 'misc' : 'module');
|
||||
current.displayName = current.name;
|
||||
current.name = current.name.toLowerCase()
|
||||
.trim().replace(/\s+/g, '_');
|
||||
}
|
||||
|
||||
// Pluralize type to determine which 'bucket' to put this section in.
|
||||
let plur;
|
||||
if (current.type.slice(-1) === 's') {
|
||||
plur = `${current.type}es`;
|
||||
} else if (current.type.slice(-1) === 'y') {
|
||||
plur = current.type.replace(/y$/, 'ies');
|
||||
if (state === 'AFTERHEADING' && depth === tok.depth) {
|
||||
const clone = current;
|
||||
current = newSection(tok);
|
||||
current.clone = clone;
|
||||
// Don't keep it around on the stack.
|
||||
stack.pop();
|
||||
} else {
|
||||
plur = `${current.type}s`;
|
||||
}
|
||||
|
||||
// Classes sometimes have various 'ctor' children
|
||||
// which are actually just descriptions of a constructor class signature.
|
||||
// Merge them into the parent.
|
||||
if (current.type === 'class' && current.ctors) {
|
||||
current.signatures = current.signatures || [];
|
||||
const sigs = current.signatures;
|
||||
current.ctors.forEach((ctor) => {
|
||||
ctor.signatures = ctor.signatures || [{}];
|
||||
ctor.signatures.forEach((sig) => {
|
||||
sig.desc = ctor.desc;
|
||||
});
|
||||
sigs.push(...ctor.signatures);
|
||||
});
|
||||
delete current.ctors;
|
||||
}
|
||||
|
||||
// Properties are a bit special.
|
||||
// Their "type" is the type of object, not "property".
|
||||
if (current.type === 'property') {
|
||||
if (current.typeof) {
|
||||
current.type = current.typeof;
|
||||
delete current.typeof;
|
||||
} else {
|
||||
delete current.type;
|
||||
// If the level is greater than the current depth,
|
||||
// then it's a child, so we should just leave the stack as it is.
|
||||
// However, if it's a sibling or higher, then it implies
|
||||
// the closure of the other sections that came before.
|
||||
// root is always considered the level=0 section,
|
||||
// and the lowest heading is 1, so this should always
|
||||
// result in having a valid parent node.
|
||||
let closingDepth = tok.depth;
|
||||
while (closingDepth <= depth) {
|
||||
finishSection(stack.pop(), stack[stack.length - 1]);
|
||||
closingDepth++;
|
||||
}
|
||||
current = newSection(tok);
|
||||
}
|
||||
|
||||
// If the parent's type is 'misc', then it's just a random
|
||||
// collection of stuff, like the "globals" section.
|
||||
// Make the children top-level items.
|
||||
if (current.type === 'misc') {
|
||||
Object.keys(current).forEach((key) => {
|
||||
switch (key) {
|
||||
case 'textRaw':
|
||||
case 'name':
|
||||
case 'type':
|
||||
case 'desc':
|
||||
case 'miscs':
|
||||
return;
|
||||
default:
|
||||
if (parent.type === 'misc') {
|
||||
return;
|
||||
}
|
||||
if (parent[key] && Array.isArray(parent[key])) {
|
||||
parent[key] = parent[key].concat(current[key]);
|
||||
} else if (!parent[key]) {
|
||||
parent[key] = current[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add this section to the parent. Sometimes we have two headings with a
|
||||
// single blob of description. If the preceding entry at this level
|
||||
// shares a name and is lacking a description, copy it backwards.
|
||||
if (!parent[plur]) parent[plur] = [];
|
||||
const prev = parent[plur].slice(-1)[0];
|
||||
if (prev && prev.name === current.name && !prev.desc) {
|
||||
prev.desc = current.desc;
|
||||
}
|
||||
parent[plur].push(current);
|
||||
({ depth } = tok);
|
||||
stack.push(current);
|
||||
state = 'AFTERHEADING';
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Immediately after a heading, we can expect the following:
|
||||
//
|
||||
// { type: 'blockquote_start' },
|
||||
// { type: 'paragraph', text: 'Stability: ...' },
|
||||
// { type: 'blockquote_end' },
|
||||
//
|
||||
// A list: starting with list_start, ending with list_end,
|
||||
// maybe containing other nested lists in each item.
|
||||
//
|
||||
// A metadata:
|
||||
// <!-- YAML
|
||||
// added: v1.0.0
|
||||
// -->
|
||||
//
|
||||
// If one of these isn't found, then anything that comes
|
||||
// between here and the next heading should be parsed as the desc.
|
||||
if (state === 'AFTERHEADING') {
|
||||
if (type === 'blockquote_start') {
|
||||
state = 'AFTERHEADING_BLOCKQUOTE';
|
||||
return;
|
||||
} else if (type === 'list_start' && !tok.ordered) {
|
||||
state = 'AFTERHEADING_LIST';
|
||||
current.list = current.list || [];
|
||||
current.list.push(tok);
|
||||
current.list.level = 1;
|
||||
} else if (type === 'html' && common.isYAMLBlock(tok.text)) {
|
||||
current.meta = common.extractAndParseYAML(tok.text);
|
||||
} else {
|
||||
current.desc = current.desc || [];
|
||||
if (!Array.isArray(current.desc)) {
|
||||
current.shortDesc = current.desc;
|
||||
current.desc = [];
|
||||
}
|
||||
current.desc.links = lexed.links;
|
||||
current.desc.push(tok);
|
||||
state = 'DESC';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (state === 'AFTERHEADING_LIST') {
|
||||
current.list.push(tok);
|
||||
if (type === 'list_start') {
|
||||
current.list.level++;
|
||||
} else if (type === 'list_end') {
|
||||
current.list.level--;
|
||||
}
|
||||
if (current.list.level === 0) {
|
||||
state = 'AFTERHEADING';
|
||||
processList(current);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (state === 'AFTERHEADING_BLOCKQUOTE') {
|
||||
if (type === 'blockquote_end') {
|
||||
state = 'AFTERHEADING';
|
||||
return;
|
||||
}
|
||||
|
||||
let stability;
|
||||
if (type === 'paragraph' && (stability = text.match(stabilityExpr))) {
|
||||
current.stability = parseInt(stability[1], 10);
|
||||
current.stabilityText = stability[2].trim();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
current.desc = current.desc || [];
|
||||
current.desc.links = lexed.links;
|
||||
current.desc.push(tok);
|
||||
});
|
||||
|
||||
// Finish any sections left open.
|
||||
while (root !== (current = stack.pop())) {
|
||||
finishSection(current, stack[stack.length - 1]);
|
||||
}
|
||||
|
||||
return cb(null, root);
|
||||
}
|
||||
|
||||
|
||||
// Go from something like this:
|
||||
//
|
||||
// [ { type: "list_item_start" },
|
||||
// { type: "text",
|
||||
// text: "`options` {Object|string}" },
|
||||
// { type: "list_start",
|
||||
// ordered: false },
|
||||
// { type: "list_item_start" },
|
||||
// { type: "text",
|
||||
// text: "`encoding` {string|null} **Default:** `'utf8'`" },
|
||||
// { type: "list_item_end" },
|
||||
// { type: "list_item_start" },
|
||||
// { type: "text",
|
||||
// text: "`mode` {integer} **Default:** `0o666`" },
|
||||
// { type: "list_item_end" },
|
||||
// { type: "list_item_start" },
|
||||
// { type: "text",
|
||||
// text: "`flag` {string} **Default:** `'a'`" },
|
||||
// { type: "space" },
|
||||
// { type: "list_item_end" },
|
||||
// { type: "list_end" },
|
||||
// { type: "list_item_end" } ]
|
||||
//
|
||||
// to something like:
|
||||
//
|
||||
// [ { textRaw: "`options` {Object|string} ",
|
||||
// options: [
|
||||
// { textRaw: "`encoding` {string|null} **Default:** `'utf8'` ",
|
||||
// name: "encoding",
|
||||
// type: "string|null",
|
||||
// default: "`'utf8'`" },
|
||||
// { textRaw: "`mode` {integer} **Default:** `0o666` ",
|
||||
// name: "mode",
|
||||
// type: "integer",
|
||||
// default: "`0o666`" },
|
||||
// { textRaw: "`flag` {string} **Default:** `'a'` ",
|
||||
// name: "flag",
|
||||
// type: "string",
|
||||
// default: "`'a'`" } ],
|
||||
// name: "options",
|
||||
// type: "Object|string",
|
||||
// optional: true } ]
|
||||
|
||||
function processList(section) {
|
||||
const { list } = section;
|
||||
const values = [];
|
||||
const stack = [];
|
||||
let current;
|
||||
|
||||
// For now, *just* build the hierarchical list.
|
||||
list.forEach((tok) => {
|
||||
const { type } = tok;
|
||||
if (type === 'space') return;
|
||||
if (type === 'list_item_start' || type === 'loose_item_start') {
|
||||
const item = {};
|
||||
if (!current) {
|
||||
values.push(item);
|
||||
current = item;
|
||||
} else {
|
||||
current.options = current.options || [];
|
||||
stack.push(current);
|
||||
current.options.push(item);
|
||||
current = item;
|
||||
}
|
||||
} else if (type === 'list_item_end') {
|
||||
if (!current) {
|
||||
throw new Error('invalid list - end without current item\n' +
|
||||
`${JSON.stringify(tok)}\n` +
|
||||
JSON.stringify(list));
|
||||
}
|
||||
current = stack.pop();
|
||||
} else if (type === 'text') {
|
||||
if (!current) {
|
||||
throw new Error('invalid list - text without current item\n' +
|
||||
`${JSON.stringify(tok)}\n` +
|
||||
JSON.stringify(list));
|
||||
}
|
||||
current.textRaw = `${current.textRaw || ''}${tok.text} `;
|
||||
}
|
||||
});
|
||||
|
||||
// Shove the name in there for properties,
|
||||
// since they are always just going to be the value etc.
|
||||
if (section.type === 'property' && values[0]) {
|
||||
values[0].textRaw = `\`${section.name}\` ${values[0].textRaw}`;
|
||||
}
|
||||
|
||||
// Now pull the actual values out of the text bits.
|
||||
values.forEach(parseListItem);
|
||||
|
||||
// Now figure out what this list actually means.
|
||||
// Depending on the section type, the list could be different things.
|
||||
|
||||
switch (section.type) {
|
||||
case 'ctor':
|
||||
case 'classMethod':
|
||||
case 'method': {
|
||||
// Each item is an argument, unless the name is 'return',
|
||||
// in which case it's the return value.
|
||||
const sig = {};
|
||||
section.signatures = section.signatures || [];
|
||||
sig.params = values.filter((value) => {
|
||||
if (value.name === 'return') {
|
||||
sig.return = value;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
parseSignature(section.textRaw, sig);
|
||||
if (!sig.jump) section.signatures.push(sig);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'property': {
|
||||
// There should be only one item, which is the value.
|
||||
// Copy the data up to the section.
|
||||
const value = values[0] || {};
|
||||
delete value.name;
|
||||
section.typeof = value.type || section.typeof;
|
||||
delete value.type;
|
||||
Object.keys(value).forEach((key) => {
|
||||
section[key] = value[key];
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'event':
|
||||
// Event: each item is an argument.
|
||||
section.params = values;
|
||||
break;
|
||||
|
||||
default:
|
||||
if (section.list.length > 0) {
|
||||
section.desc = section.desc || [];
|
||||
section.desc.push(...section.list);
|
||||
}
|
||||
}
|
||||
|
||||
delete section.list;
|
||||
}
|
||||
|
||||
|
||||
|
@ -290,7 +325,6 @@ const paramExpr = /\((.+)\);?$/;
|
|||
// text: "someobject.someMethod(a[, b=100][, c])"
|
||||
function parseSignature(text, sig) {
|
||||
const list = [];
|
||||
|
||||
let [, sigParams] = text.match(paramExpr) || [];
|
||||
if (!sigParams) return;
|
||||
sigParams = sigParams.split(',');
|
||||
|
@ -346,15 +380,8 @@ function parseSignature(text, sig) {
|
|||
}
|
||||
|
||||
if (!listParam) {
|
||||
if (sigParam.startsWith('...')) {
|
||||
listParam = { name: sigParam };
|
||||
} else {
|
||||
throw new Error(
|
||||
`Invalid param "${sigParam}"\n` +
|
||||
` > ${JSON.stringify(listParam)}\n` +
|
||||
` > ${text}`
|
||||
);
|
||||
}
|
||||
sig.jump = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -363,7 +390,6 @@ function parseSignature(text, sig) {
|
|||
|
||||
list.push(listParam);
|
||||
});
|
||||
|
||||
sig.params = list;
|
||||
}
|
||||
|
||||
|
@ -374,37 +400,30 @@ const typeExpr = /^\{([^}]+)\}\s*/;
|
|||
const leadingHyphen = /^-\s*/;
|
||||
const defaultExpr = /\s*\*\*Default:\*\*\s*([^]+)$/i;
|
||||
|
||||
function parseListItem(item, file) {
|
||||
const current = {};
|
||||
|
||||
current.textRaw = item.children.filter((node) => node.type !== 'list')
|
||||
.map((node) => (
|
||||
file.contents.slice(node.position.start.offset, node.position.end.offset))
|
||||
)
|
||||
.join('').replace(/\s+/g, ' ').replace(/<!--.*?-->/sg, '');
|
||||
let text = current.textRaw;
|
||||
|
||||
if (!text) {
|
||||
function parseListItem(item) {
|
||||
if (item.options) item.options.forEach(parseListItem);
|
||||
if (!item.textRaw) {
|
||||
throw new Error(`Empty list item: ${JSON.stringify(item)}`);
|
||||
}
|
||||
|
||||
// The goal here is to find the name, type, default.
|
||||
// The goal here is to find the name, type, default, and optional.
|
||||
// Anything left over is 'desc'.
|
||||
let text = item.textRaw.trim();
|
||||
|
||||
if (returnExpr.test(text)) {
|
||||
current.name = 'return';
|
||||
item.name = 'return';
|
||||
text = text.replace(returnExpr, '');
|
||||
} else {
|
||||
const [, name] = text.match(nameExpr) || [];
|
||||
if (name) {
|
||||
current.name = name;
|
||||
item.name = name;
|
||||
text = text.replace(nameExpr, '');
|
||||
}
|
||||
}
|
||||
|
||||
const [, type] = text.match(typeExpr) || [];
|
||||
if (type) {
|
||||
current.type = type;
|
||||
item.type = type;
|
||||
text = text.replace(typeExpr, '');
|
||||
}
|
||||
|
||||
|
@ -412,32 +431,154 @@ function parseListItem(item, file) {
|
|||
|
||||
const [, defaultValue] = text.match(defaultExpr) || [];
|
||||
if (defaultValue) {
|
||||
current.default = defaultValue.replace(/\.$/, '');
|
||||
item.default = defaultValue.replace(/\.$/, '');
|
||||
text = text.replace(defaultExpr, '');
|
||||
}
|
||||
|
||||
if (text) current.desc = text;
|
||||
|
||||
const options = item.children.find((child) => child.type === 'list');
|
||||
if (options) {
|
||||
current.options = options.children.map((child) => (
|
||||
parseListItem(child, file)
|
||||
));
|
||||
}
|
||||
|
||||
return current;
|
||||
if (text) item.desc = text;
|
||||
}
|
||||
|
||||
// This section parses out the contents of an H# tag.
|
||||
|
||||
// To reduce escape slashes in RegExp string components.
|
||||
function finishSection(section, parent) {
|
||||
if (!section || !parent) {
|
||||
throw new Error('Invalid finishSection call\n' +
|
||||
`${JSON.stringify(section)}\n` +
|
||||
JSON.stringify(parent));
|
||||
}
|
||||
|
||||
if (!section.type) {
|
||||
section.type = 'module';
|
||||
if (parent.type === 'misc') {
|
||||
section.type = 'misc';
|
||||
}
|
||||
section.displayName = section.name;
|
||||
section.name = section.name.toLowerCase()
|
||||
.trim().replace(/\s+/g, '_');
|
||||
}
|
||||
|
||||
if (section.desc && Array.isArray(section.desc)) {
|
||||
section.desc.links = section.desc.links || [];
|
||||
section.desc = marked.parser(section.desc);
|
||||
}
|
||||
|
||||
if (!section.list) section.list = [];
|
||||
processList(section);
|
||||
|
||||
// Classes sometimes have various 'ctor' children
|
||||
// which are actually just descriptions of a constructor class signature.
|
||||
// Merge them into the parent.
|
||||
if (section.type === 'class' && section.ctors) {
|
||||
section.signatures = section.signatures || [];
|
||||
const sigs = section.signatures;
|
||||
section.ctors.forEach((ctor) => {
|
||||
ctor.signatures = ctor.signatures || [{}];
|
||||
ctor.signatures.forEach((sig) => {
|
||||
sig.desc = ctor.desc;
|
||||
});
|
||||
sigs.push(...ctor.signatures);
|
||||
});
|
||||
delete section.ctors;
|
||||
}
|
||||
|
||||
// Properties are a bit special.
|
||||
// Their "type" is the type of object, not "property".
|
||||
if (section.properties) {
|
||||
section.properties.forEach((prop) => {
|
||||
if (prop.typeof) {
|
||||
prop.type = prop.typeof;
|
||||
delete prop.typeof;
|
||||
} else {
|
||||
delete prop.type;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle clones.
|
||||
if (section.clone) {
|
||||
const { clone } = section;
|
||||
delete section.clone;
|
||||
delete clone.clone;
|
||||
deepCopy(section, clone);
|
||||
finishSection(clone, parent);
|
||||
}
|
||||
|
||||
let plur;
|
||||
if (section.type.slice(-1) === 's') {
|
||||
plur = `${section.type}es`;
|
||||
} else if (section.type.slice(-1) === 'y') {
|
||||
plur = section.type.replace(/y$/, 'ies');
|
||||
} else {
|
||||
plur = `${section.type}s`;
|
||||
}
|
||||
|
||||
// If the parent's type is 'misc', then it's just a random
|
||||
// collection of stuff, like the "globals" section.
|
||||
// Make the children top-level items.
|
||||
if (section.type === 'misc') {
|
||||
Object.keys(section).forEach((key) => {
|
||||
switch (key) {
|
||||
case 'textRaw':
|
||||
case 'name':
|
||||
case 'type':
|
||||
case 'desc':
|
||||
case 'miscs':
|
||||
return;
|
||||
default:
|
||||
if (parent.type === 'misc') {
|
||||
return;
|
||||
}
|
||||
if (parent[key] && Array.isArray(parent[key])) {
|
||||
parent[key] = parent[key].concat(section[key]);
|
||||
} else if (!parent[key]) {
|
||||
parent[key] = section[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
parent[plur] = parent[plur] || [];
|
||||
parent[plur].push(section);
|
||||
}
|
||||
|
||||
|
||||
// Not a general purpose deep copy.
|
||||
// But sufficient for these basic things.
|
||||
function deepCopy(src, dest) {
|
||||
Object.keys(src)
|
||||
.filter((key) => !dest.hasOwnProperty(key))
|
||||
.forEach((key) => { dest[key] = cloneValue(src[key]); });
|
||||
}
|
||||
|
||||
function cloneValue(src) {
|
||||
if (!src) return src;
|
||||
if (Array.isArray(src)) {
|
||||
const clone = new Array(src.length);
|
||||
src.forEach((value, i) => {
|
||||
clone[i] = cloneValue(value);
|
||||
});
|
||||
return clone;
|
||||
}
|
||||
if (typeof src === 'object') {
|
||||
const clone = {};
|
||||
Object.keys(src).forEach((key) => {
|
||||
clone[key] = cloneValue(src[key]);
|
||||
});
|
||||
return clone;
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
|
||||
// This section parse out the contents of an H# tag.
|
||||
|
||||
// To reduse escape slashes in RegExp string components.
|
||||
const r = String.raw;
|
||||
|
||||
const eventPrefix = '^Event: +';
|
||||
const classPrefix = '^[Cc]lass: +';
|
||||
const ctorPrefix = '^(?:[Cc]onstructor: +)?`?new +';
|
||||
const classMethodPrefix = '^Static method: +';
|
||||
const maybeClassPropertyPrefix = '(?:Class property: +)?';
|
||||
const maybeClassPropertyPrefix = '(?:Class Property: +)?';
|
||||
|
||||
const maybeQuote = '[\'"]?';
|
||||
const notQuotes = '[^\'"]+';
|
||||
|
@ -480,9 +621,7 @@ const headingExpressions = [
|
|||
];
|
||||
/* eslint-enable max-len */
|
||||
|
||||
function newSection(header, file) {
|
||||
const text = textJoin(header.children, file);
|
||||
|
||||
function newSection({ text }) {
|
||||
// Infer the type from the text.
|
||||
for (const { type, re } of headingExpressions) {
|
||||
const [, name] = text.match(re) || [];
|
||||
|
@ -493,20 +632,3 @@ function newSection(header, file) {
|
|||
return { textRaw: text, name: text };
|
||||
}
|
||||
|
||||
function textJoin(nodes, file) {
|
||||
return nodes.map((node) => {
|
||||
if (node.type === 'linkReference') {
|
||||
return file.contents.slice(node.position.start.offset,
|
||||
node.position.end.offset);
|
||||
} else if (node.type === 'inlineCode') {
|
||||
return `\`${node.value}\``;
|
||||
} else if (node.type === 'strong') {
|
||||
return `**${textJoin(node.children, file)}**`;
|
||||
} else if (node.type === 'emphasis') {
|
||||
return `_${textJoin(node.children, file)}_`;
|
||||
} else if (node.children) {
|
||||
return textJoin(node.children, file);
|
||||
}
|
||||
return node.value;
|
||||
}).join('');
|
||||
}
|
||||
|
|
|
@ -160,11 +160,12 @@ const customTypesMap = {
|
|||
};
|
||||
|
||||
const arrayPart = /(?:\[])+$/;
|
||||
const arrayWrap = /^\[(\w+)\]$/;
|
||||
|
||||
function toLink(typeInput) {
|
||||
const typeLinks = [];
|
||||
typeInput = typeInput.replace('{', '').replace('}', '');
|
||||
const typeTexts = typeInput.split('|');
|
||||
const typeTexts = typeInput.split(/\||,/);
|
||||
|
||||
typeTexts.forEach((typeText) => {
|
||||
typeText = typeText.trim();
|
||||
|
@ -174,7 +175,7 @@ function toLink(typeInput) {
|
|||
// To support type[], type[][] etc., we store the full string
|
||||
// and use the bracket-less version to lookup the type URL.
|
||||
const typeTextFull = typeText;
|
||||
typeText = typeText.replace(arrayPart, '');
|
||||
typeText = typeText.replace(arrayPart, '').replace(arrayWrap, '$1');
|
||||
|
||||
const primitive = jsPrimitives[typeText];
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ async function versions() {
|
|||
changelog = readFileSync(file, { encoding: 'utf8' });
|
||||
} else {
|
||||
try {
|
||||
changelog = await getUrl(url);
|
||||
changelog = await require('fs').promises.readFile(require('path').join(__dirname, '..', '..', 'CHANGELOG.md'));
|
||||
} catch (e) {
|
||||
// Fail if this is a release build, otherwise fallback to local files.
|
||||
if (isRelease()) {
|
||||
|
|
Loading…
Reference in New Issue