'use strict';
* Module dependencies
*/
const dox = require('dox');
const fs = require('fs');
const md = require('marked');
const files = [
'lib/mongoose.js',
'lib/schema.js',
'lib/connection.js',
'lib/document.js',
'lib/model.js',
'lib/query.js',
'lib/cursor/queryCursor.js',
'lib/aggregate.js',
'lib/cursor/aggregationCursor.js',
'lib/schemaType.js',
'lib/virtualType.js',
'lib/error/index.js',
'lib/schema/array.js',
'lib/schema/documentArray.js',
'lib/schema/subdocument.js',
'lib/schema/boolean.js',
'lib/schema/buffer.js',
'lib/schema/number.js',
'lib/schema/objectId.js',
'lib/schema/string.js',
'lib/schema/uuid.js',
'lib/options/schemaTypeOptions.js',
'lib/options/schemaArrayOptions.js',
'lib/options/schemaBufferOptions.js',
'lib/options/schemaDateOptions.js',
'lib/options/schemaNumberOptions.js',
'lib/options/schemaObjectIdOptions.js',
'lib/options/schemaStringOptions.js',
'lib/types/documentArray/methods/index.js',
'lib/types/subdocument.js',
'lib/types/arraySubdocument.js',
'lib/types/buffer.js',
'lib/types/decimal128.js',
'lib/types/map.js',
'lib/types/array/methods/index.js',
'lib/types/uuid.js'
];
const out = module.exports.docs = new Map();
{
dox.contextPatternMatchers.unshift(function(str) {
const match = /^\s*([\w$.]+)\s*\.\s*([\w$]+)\s*=\s*(?:async\s+)?function/.exec(str);
if (match) {
return {
type: 'method',
receiver: match[1],
name: match[2],
string: match[1] + '.' + match[2] + '()'
};
}
});
dox.contextPatternMatchers.unshift(function(str) {
const match = /^\s*([\w$.]+)\s*\.\s*prototype\s*\.\s*([\w$]+)\s*=\s*(?:async\s+)?function/.exec(str);
if (match) {
return {
type: 'method',
constructor: match[1],
cons: match[1],
name: match[2],
string: match[1] + '.prototype.' + match[2] + '()'
};
}
});
dox.contextPatternMatchers.unshift(function(str) {
const match = /^\s*(export(\s+default)?\s+)?(?:async\s+)?function\s+([\w$]+)\s*\(/.exec(str);
if (match) {
return {
type: 'function',
name: match[3],
string: match[3] + '()'
};
}
});
}
parseAllFiles();
* @typedef {Object} TagObject
* @property {String} name The Processed name of the Tag (already includes all processing)
* @property {String} description The Description of this Tag
* @property {String} [descriptionMarkdown] The raw markdown description for markdown output
* @property {Boolean} optional Defines wheter the Tag is optional or not (already included in `name`)
* @property {Boolean} nullable Defines wheter the Tag is nullable (dox does not add "null" by default)
* @property {Boolean} nonNullable Unknown (invert of `nullable`?)
* @property {Boolean} variable Defines wheter the type is spreadable ("...Type")
* @property {String[]} types Collection of all types this Tag has
* @property {String} type The Type of the Tag
* @property {String} string The full string of types plus name plus description (unused in mongoose)
* @property {String} typesDescription Processed `types` into markdown code (unused in mongoose)
*/
* @typedef {Object} SeeObject
* @property {String} text The text to display the link as
* @property {String} [url] The link the text should have as href
*/
* @typedef {Object} PropContext
* @property {boolean} [isStatic] Defines wheter the current property is a static property (not mutually exlusive with "isInstance")
* @property {boolean} [isInstance] Defines wheter the current property is a instance property (not mutually exlusive with "isStatic")
* @property {boolean} [isFunction] Defines wheter the current property is meant to be a function
* @property {string} [constructor] Defines the Constructor (or rather path) the current property is on
* @property {boolean} [constructorWasUndefined] Defined wheter the "constructor" property was defined by "dox", but was set to "undefined"
* @property {string} [type] Defines the type the property is meant to be
* @property {string} [name] Defines the current Properties name
* @property {TagObject} [return] The full object for a "@return" jsdoc tag
* @property {string} [string] Defines the full string the property will be listed as
* @property {string} [anchorId] Defines the Anchor ID to be used for linking
* @property {string} [description] Defines the Description the property will be listed with
* @property {string} [descriptionMarkdown] Defines the raw markdown description for markdown output
* @property {string} [deprecated] Defines wheter the current Property is signaled as deprecated
* @property {SeeObject[]} [see] Defines all "@see" references
* @property {TagObject[]} [param] Defines all "@param" references
* @property {SeeObject} [inherits] Defines the string for "@inherits"
*/
* @typedef {Object} NameObj
* @property {string} docName
* @property {string} filePath
* @property {string} fullName
* @property {string} docFileName
*/
* @typedef {Object} DocsObj
* @property {string} title The Title of the page
* @property {string} fileName The name of the resulting file
* @property {PropContext[]} props All the functions and values
* @property {string} file The original file (relative to the root of the repository)
* @property {string} editLink The link used for edits
* @property {string} markdownSource The raw markdown docs for this API page
* @property {boolean} [hideFromNav] Indicate that the entry should not be listed in the navigation
*/
* Process a file name to a documentation name
* @param {string} input
* @returns {NameObj}
*/
function processName(input) {
let name = input.
replace('lib/', '').
replace('.js', '').
replace('/index', '').
replace('/methods', '');
const lastSlash = name.lastIndexOf('/');
const fullName = name;
const basename = name.substr(lastSlash === -1 ? 0 : lastSlash + 1);
name = basename;
if (basename === 'core_array') {
name = 'array';
}
if (fullName.startsWith('schema/')) {
name = 'Schema';
if (basename.charAt(0) !== basename.charAt(0).toUpperCase()) {
name += basename.charAt(0).toUpperCase() + basename.substring(1);
} else {
name += basename;
}
}
if (fullName === 'types/array/methods/index') {
name = 'Array';
}
if (basename === 'SubdocumentPath') {
name = 'SubdocumentPath';
}
if (basename === 'documentarray') {
name = 'DocumentArrayPath';
}
if (basename === 'DocumentArray') {
name = 'MongooseDocumentArray';
}
if (basename === 'index') {
name = 'Mongoose';
}
const docName = name.charAt(0).toUpperCase() === name.charAt(0) ? name : name.charAt(0).toUpperCase() + name.substr(1);
return {
docName: docName,
fullName: fullName,
filePath: input,
docFileName: name.toLowerCase()
};
}
function convertTypesToString(types, typesDescription) {
if (!Array.isArray(types)) {
return types;
}
const result = types.join('|');
if (result.includes('[object Object]') && typesDescription) {
return typesDescription.replace(/<\/?code>/g, '');
}
return result;
}
* Convert API doc HTML links in a string to their equivalent Markdown file paths.
* @param {String} [str] The string containing API documentation links to convert
* @returns {String|undefined}
*/
function apiLinksToMarkdown(str) {
if (!str) {
return str;
}
return str.
replace(/https:\/\/mongoosejs\.com\/docs\/api\/\S+\.html/g, url => url.replace(/\.html$/, '.md')).
replace(/^([^/#]+)\.html(#.*)?$/, '$1.md$2');
}
* Clean a dox description for Markdown output.
* @param {String} [description] The raw dox description
* @returns {String}
*/
function normalizeMarkdownDescription(description) {
return apiLinksToMarkdown((description || '').
replace(/<br \/>/ig, '\n').
replace(/>/ig, '>')).
trim();
}
* Clean a dox description for HTML output.
* @param {String} [description] The raw dox description
* @returns {String}
*/
function normalizeHtmlDescription(description) {
return (description || '').
replace(/<br \/>/ig, ' ').
replace(/>/ig, '>');
}
* Format an API type string for Markdown output.
* @param {String} [types] The API type string to format
* @returns {String}
*/
function formatApiType(types) {
return types ? `\\<${types}\\> ` : '';
}
* Build the Markdown source for a parsed API page.
* @param {DocsObj} data The parsed API page data
* @returns {String}
*/
function buildMarkdown(data) {
const lines = [
`# ${data.title}`,
'',
...data.props.map(prop => `- [\`${prop.string}\`](#${prop.anchorId})`),
''
];
for (const prop of data.props) {
lines.push(`## \`${prop.string}\``);
lines.push('');
if (prop.deprecated) {
lines.push('Deprecated.');
lines.push('');
}
if (prop.param != null) {
lines.push('### Parameters');
lines.push('');
for (const param of prop.param) {
lines.push(`- \`${param.name}\` ${formatApiType(param.types)}${param.descriptionMarkdown || ''}`.trim());
}
lines.push('');
}
if (prop.return != null) {
lines.push('### Returns');
lines.push('');
lines.push(`- ${formatApiType(prop.return.types)}${prop.return.descriptionMarkdown || ''}`.trim());
lines.push('');
}
if (prop.type != null && prop.type !== 'method' && prop.type !== 'function') {
lines.push('### Type');
lines.push('');
lines.push(`- ${formatApiType(prop.type)}`.trim());
lines.push('');
}
if (prop.inherits != null) {
lines.push('### Inherits');
lines.push('');
lines.push(`- [${prop.inherits.text}](${apiLinksToMarkdown(prop.inherits.url)})`);
lines.push('');
}
if (prop.see != null && prop.see.length > 0) {
lines.push('### See');
lines.push('');
for (const see of prop.see) {
lines.push(`- [${see.text}](${apiLinksToMarkdown(see.url)})`);
}
lines.push('');
}
if (prop.descriptionMarkdown) {
lines.push(prop.descriptionMarkdown);
lines.push('');
}
}
return `${lines.join('\n').trim()}\n`;
}
* Parse all files defined in "files"
*/
function parseAllFiles() {
for (const file of files) {
parseFile(file, true);
}
}
* Parse a specific file
* @param {String} file The file to parse
* @param {Boolean} throwErr throw the error if one is encountered?
*/
function parseFile(file, throwErr = true) {
try {
const comments = dox.parseComments(fs.readFileSync(file, 'utf8'), { raw: true });
comments.file = file;
processFile(comments);
} catch (err) {
console.error('Error while trying to parseComments for ', file);
if (throwErr) {
throw err;
}
}
}
function processFile(props) {
const { docName: name, docFileName } = processName(props.file);
const data = {
title: name,
fileName: docFileName,
props: []
};
for (const prop of props) {
if (prop.ignore || prop.isPrivate) {
continue;
}
const ctx = prop.ctx || {};
if ('receiver' in ctx) {
ctx.constructor = ctx.receiver;
delete ctx.receiver;
}
if ('constructor' in ctx && ctx.constructor === undefined) {
ctx.constructorWasUndefined = true;
}
if (!prop.tags) continue;
for (const __tag of prop.tags) {
const tag = __tag;
switch (tag.type) {
case 'see':
if (!Array.isArray(ctx.see)) {
ctx.see = [];
}
ctx.see.push(extractTextUrlFromTag(tag, ctx, true));
break;
case 'receiver':
console.warn(`Found "@receiver" tag in ${ctx.constructor} ${ctx.name}`);
break;
case 'property':
ctx.type = 'property';
ctx.name = tag.name;
if (tag.types.length > 0) {
ctx.type = convertTypesToString(tag.types, tag.typesDescription);
}
break;
case 'type':
ctx.type = convertTypesToString(tag.types, tag.typesDescription);
break;
case 'static':
ctx.type = 'property';
ctx.isStatic = true;
break;
case 'function':
ctx.type = 'function';
ctx.isStatic = true;
ctx.name = tag.string;
ctx.isFunction = true;
break;
case 'return':
tag.descriptionMarkdown = normalizeMarkdownDescription(tag.description);
tag.description = tag.description ?
md.parse(tag.description).replace(/^<p>/, '').replace(/<\/p>\n?$/, '') :
'';
if (tag.string.includes('void') || tag.string.includes('undefined')) {
tag.types.push('void');
}
ctx.return = tag;
break;
case 'inherits': {
const obj = extractTextUrlFromTag(tag, ctx);
if (!obj.url || obj.url === obj.text) {
let match = undefined;
for (const file of files) {
const { docName, docFileName } = processName(file);
if (docName.toLowerCase().includes(obj.text.toLowerCase())) {
match = docFileName;
break;
}
}
if (match) {
obj.url = match + '.html';
} else {
console.warn(`no match found in files for inherits "${obj.text}" on "${ctx.constructor}.${ctx.name}"`);
}
}
ctx.inherits = obj;
break;
}
case 'event':
case 'param':
ctx[tag.type] = (ctx[tag.type] || []);
if (tag.nullable) {
tag.types.push('null');
}
if (tag.types) {
tag.types = convertTypesToString(tag.types, tag.typesDescription);
}
ctx[tag.type].push(tag);
if (tag.name != null && tag.name.startsWith('[') && tag.name.endsWith(']') && tag.name.includes('.')) {
tag.nested = true;
}
if (tag.variable) {
if (tag.name.startsWith('[')) {
tag.name = '[...' + tag.name.slice(1);
} else {
tag.name = '...' + tag.name;
}
}
tag.descriptionMarkdown = normalizeMarkdownDescription(tag.description);
tag.description = tag.description ?
md.parse(tag.description).replace(/^<p>/, '').replace(/<\/p>$/, '') :
'';
break;
case 'method':
ctx.type = 'method';
ctx.name = tag.string;
ctx.isFunction = true;
break;
case 'memberOf':
ctx.constructor = tag.parent;
break;
case 'constructor':
ctx.string = tag.string;
ctx.name = tag.string;
ctx.isFunction = true;
break;
case 'instance':
ctx.isInstance = true;
break;
case 'deprecated':
ctx.deprecated = true;
break;
}
}
if (ctx.isInstance && ctx.isStatic) {
console.warn(`Property "${ctx.name}" in "${ctx.constructor}" has both instance and static JSDOC markings (most likely both @instance and @static)! (File: "${props.file}")`);
}
if (ctx.isInstance || (!ctx.isStatic && !ctx.isInstance && (!ctx.string || ctx.constructorWasUndefined))) {
if (ctx.name.startsWith('[')) {
ctx.string = `${ctx.constructor}.prototype${ctx.name}`;
} else {
ctx.string = `${ctx.constructor}.prototype.${ctx.name}`;
}
} else if (ctx.isStatic) {
ctx.string = `${ctx.constructor}.${ctx.name}`;
}
if ((ctx.isFunction || ctx.type === 'method') && !ctx.string.endsWith('()')) {
ctx.string = ctx.string + '()';
}
ctx.anchorId = ctx.string;
ctx.descriptionMarkdown = normalizeMarkdownDescription(prop.description.full);
ctx.description = md.parse(normalizeHtmlDescription(prop.description.full));
data.props.push(ctx);
}
data.props.sort(function(a, b) {
if (a.string < b.string) {
return -1;
} else {
return 1;
}
});
if (props.file.startsWith('lib/options')) {
data.hideFromNav = true;
}
data.file = props.file;
data.editLink = 'https://github.com/Automattic/mongoose/edit/master/' +
props.file;
data.markdownSource = buildMarkdown(data);
out.set(data.file, data);
}
* Extract the Text and Url from a description if any
* @param {Tag} tag The tag to process the resulting object from
* @param {PropContext} ctx The current ctx for warnings
* @param {Boolean} warnOnMissingUrl Warn if the url is missing, false by default
* @returns {{ text: string, url: string }}
*/
function extractTextUrlFromTag(tag, ctx, warnOnMissingUrl = false) {
const textMatches = /^(.*? (?=#|\/|(?:https?:)|\.\/|$))/i.exec(tag.string);
let text = undefined;
let url = undefined;
if (textMatches === null || textMatches === undefined) {
if (warnOnMissingUrl) {
console.warn(`No Text Matches found in tag for "${ctx.constructor}.${ctx.name}"`);
}
url = tag.string;
text = tag.string;
} else {
text = textMatches[1].trim();
url = tag.string.slice(text.length).trim();
}
return {
text: text || 'No Description',
url: url || undefined
};
}
module.exports.parseFile = parseFile;
module.exports.parseAllFiles = parseAllFiles;