UNPKG

dox

Version:

Markdown / JSdoc documentation generator

739 lines (664 loc) 21 kB
/*! * Module dependencies. */ var markdown = require('markdown-it')({ html: true, xhtmlOut: true, breaks: true, langPrefix: 'lang-' }); /** * Expose api. */ exports.api = require('./api'); /** * Parse comments in the given string of `js`. * * @param {String} js * @param {Object} options * @return {Array} * @see exports.parseComment * @api public */ exports.parseComments = function(js, options){ options = options || {}; js = js.replace(/\r\n/gm, '\n'); var comments = [] , skipSingleStar = options.skipSingleStar , comment , buf = '' , ignore , withinMultiline = false , withinSingle = false , withinString = false , code , linterPrefixes = options.skipPrefixes || ['jslint', 'jshint', 'eshint'] , skipPattern = new RegExp('^' + (options.raw ? '' : '<p>') + '('+ linterPrefixes.join('|') + ')') , lineNum = 1 , lineNumStarting = 1 , parentContext , withinEscapeChar , currentStringQuoteChar; for (var i = 0, len = js.length; i < len; ++i) { withinEscapeChar = withinString && !withinEscapeChar && js[i - 1] == '\\'; // start comment if (!withinMultiline && !withinSingle && !withinString && '/' == js[i] && '*' == js[i+1] && (!skipSingleStar || js[i+2] == '*')) { lineNumStarting = lineNum; // code following the last comment if (buf.trim().length) { comment = comments[comments.length - 1]; if(comment) { // Adjust codeStart for any vertical space between comment and code comment.codeStart += buf.match(/^(\s*)/)[0].split('\n').length - 1; comment.code = code = exports.trimIndentation(buf).trim(); comment.ctx = exports.parseCodeContext(code, parentContext); if (comment.isConstructor && comment.ctx){ comment.ctx.type = "constructor"; } // starting a new namespace if (comment.ctx && (comment.ctx.type === 'prototype' || comment.ctx.type === 'class')){ parentContext = comment.ctx; } // reasons to clear the namespace // new property/method in a different constructor else if (!parentContext || !comment.ctx || !comment.ctx.constructor || !parentContext.constructor || parentContext.constructor !== comment.ctx.constructor){ parentContext = null; } } buf = ''; } i += 2; withinMultiline = true; ignore = '!' == js[i]; // if the current character isn't whitespace and isn't an ignored comment, // back up one character so we don't clip the contents if (' ' !== js[i] && '\n' !== js[i] && '\t' !== js[i] && '!' !== js[i]) i--; // end comment } else if (withinMultiline && !withinSingle && '*' == js[i] && '/' == js[i+1]) { i += 2; buf = buf.replace(/^[ \t]*\* ?/gm, ''); comment = exports.parseComment(buf, options); comment.ignore = ignore; comment.line = lineNumStarting; comment.codeStart = lineNum + 1; if (!comment.description.full.match(skipPattern)) { comments.push(comment); } withinMultiline = ignore = false; buf = ''; } else if (!withinSingle && !withinMultiline && !withinString && '/' == js[i] && '/' == js[i+1]) { withinSingle = true; buf += js[i]; } else if (withinSingle && !withinMultiline && '\n' == js[i]) { withinSingle = false; buf += js[i]; } else if (!withinSingle && !withinMultiline && !withinEscapeChar && ('\'' == js[i] || '"' == js[i] || '`' == js[i])) { if(withinString) { if(js[i] == currentStringQuoteChar) { withinString = false; } } else { withinString = true; currentStringQuoteChar = js[i]; } buf += js[i]; } else { buf += js[i]; } if('\n' == js[i]) { lineNum++; } } if (comments.length === 0) { comments.push({ tags: [], description: {full: '', summary: '', body: ''}, isPrivate: false, isConstructor: false, line: lineNumStarting }); } // trailing code if (buf.trim().length) { comment = comments[comments.length - 1]; // Adjust codeStart for any vertical space between comment and code comment.codeStart += buf.match(/^(\s*)/)[0].split('\n').length - 1; comment.code = code = exports.trimIndentation(buf).trim(); comment.ctx = exports.parseCodeContext(code, parentContext); } return comments; }; /** * Removes excess indentation from string of code. * * @param {String} str * @return {String} * @api public */ exports.trimIndentation = function (str) { // Find indentation from first line of code. var indent = str.match(/(?:^|\n)([ \t]*)[^\s]/); if (indent) { // Replace indentation on all lines. str = str.replace(new RegExp('(^|\n)' + indent[1], 'g'), '$1'); } return str; }; /** * Parse the given comment `str`. * * The comment object returned contains the following * * - `tags` array of tag objects * - `description` the first line of the comment * - `body` lines following the description * - `content` both the description and the body * - `isPrivate` true when "@api private" is used * * @param {String} str * @param {Object} options * @return {Object} * @see exports.parseTag * @api public */ exports.parseComment = function(str, options) { str = str.trim(); options = options || {}; var comment = { tags: [] } , raw = options.raw , description = {} , tags = str.split(/\n\s*@/); // A comment has no description if (tags[0].charAt(0) === '@') { tags.unshift(''); } // parse comment body description.full = tags[0]; description.summary = description.full.split('\n\n')[0]; description.body = description.full.split('\n\n').slice(1).join('\n\n'); comment.description = description; // parse tags if (tags.length) { comment.tags = tags.slice(1).map(exports.parseTag); comment.isPrivate = comment.tags.some(function(tag){ return 'private' == tag.visibility; }); comment.isConstructor = comment.tags.some(function(tag){ return 'constructor' == tag.type || 'augments' == tag.type; }); comment.isClass = comment.tags.some(function(tag){ return 'class' == tag.type; }); comment.isEvent = comment.tags.some(function(tag){ return 'event' == tag.type; }); if (!description.full || !description.full.trim()) { comment.tags.some(function(tag){ if ('description' == tag.type) { description.full = tag.full; description.summary = tag.summary; description.body = tag.body; return true; } }); } } // markdown if (!raw) { description.full = markdown.render(description.full).trim(); description.summary = markdown.render(description.summary).trim(); description.body = markdown.render(description.body).trim(); comment.tags.forEach(function (tag) { if (tag.description) tag.description = markdown.render(tag.description).trim(); else tag.html = markdown.render(tag.string).trim(); }); } return comment; }; //TODO: Find a smarter way to do this /** * Extracts different parts of a tag by splitting string into pieces separated by whitespace. If the white spaces are * somewhere between curly braces (which is used to indicate param/return type in JSDoc) they will not be used to split * the string. This allows to specify jsdoc tags without the need to eliminate all white spaces i.e. {number | string} * * @param str The tag line as a string that needs to be split into parts * @returns {Array.<string>} An array of strings containing the parts */ exports.extractTagParts = function(str) { var level = 0, extract = '', split = []; str.split('').forEach(function(c) { if(c.match(/\s/) && level === 0) { split.push(extract); extract = ''; } else { if(c === '{') { level++; } else if (c === '}') { level--; } extract += c; } }); split.push(extract); return split.filter(function(str) { return str.length > 0; }); }; /** * Parse tag string "@param {Array} name description" etc. * * @param {String} * @return {Object} * @api public */ exports.parseTag = function(str) { var tag = {} , lines = str.split('\n') , parts = exports.extractTagParts(lines[0]) , type = tag.type = parts.shift().replace('@', '') , matchType = new RegExp('^@?' + type + ' *') , matchTypeStr = /^\{.+\}$/; tag.string = str.replace(matchType, ''); if (lines.length > 1) { parts.push(lines.slice(1).join('\n')); } switch (type) { case 'property': case 'template': case 'param': var typeString = matchTypeStr.test(parts[0]) ? parts.shift() : ""; tag.name = parts.shift() || ''; tag.description = parts.join(' '); exports.parseTagTypes(typeString, tag); break; case 'define': case 'return': case 'returns': var typeString = matchTypeStr.test(parts[0]) ? parts.shift() : ""; exports.parseTagTypes(typeString, tag); tag.description = parts.join(' '); break; case 'see': if (~str.indexOf('http')) { tag.title = parts.length > 1 ? parts.shift() : ''; tag.url = parts.join(' '); } else { tag.local = parts.join(' '); } break; case 'api': tag.visibility = parts.shift(); break; case 'public': case 'private': case 'protected': tag.visibility = type; break; case 'enum': case 'typedef': case 'type': exports.parseTagTypes(parts.shift(), tag); break; case 'lends': case 'memberOf': tag.parent = parts.shift(); break; case 'extends': case 'implements': case 'augments': tag.otherClass = parts.shift(); break; case 'borrows': tag.otherMemberName = parts.join(' ').split(' as ')[0]; tag.thisMemberName = parts.join(' ').split(' as ')[1]; break; case 'throws': var typeString = matchTypeStr.test(parts[0]) ? parts.shift() : ""; tag.types = exports.parseTagTypes(typeString); tag.description = parts.join(' '); break; case 'description': tag.full = parts.join(' ').trim(); tag.summary = tag.full.split('\n\n')[0]; tag.body = tag.full.split('\n\n').slice(1).join('\n\n'); break; default: tag.string = parts.join(' ').replace(/\s+$/, ''); break; } return tag; }; /** * Parse tag type string "{Array|Object}" etc. * This function also supports complex type descriptors like in jsDoc or even the enhanced syntax used by the * [google closure compiler](https://developers.google.com/closure/compiler/docs/js-for-compiler#types) * * The resulting array from the type descriptor `{number|string|{name:string,age:number|date}}` would look like this: * * [ * 'number', * 'string', * { * age: ['number', 'date'], * name: ['string'] * } * ] * * @param {String} str * @return {Array} * @api public */ exports.parseTagTypes = function(str, tag) { if (!str) { if(tag) { tag.types = []; tag.typesDescription = ""; tag.optional = tag.nullable = tag.nonNullable = tag.variable = false; } return []; } var {parse, publish, createDefaultPublisher, NodeType, SyntaxType} = require('jsdoctypeparser'); var result = parse(str.substring(1, str.length - 1)); var customPublisher = Object.assign({}, createDefaultPublisher(), { NAME(nameNode) { var output = '<code>' + nameNode.name + '</code>'; if (result.type === NodeType.OPTIONAL) { output += '|<code>undefined</code>'; } else if (result.type === NodeType.NULLABLE) { output += '|<code>null</code>'; } return output; } }); var types = (function transform(type) { if (type && type.type === NodeType.UNION) { return [transform(type.left), transform(type.right)].flat(); } else if (type && type.type === NodeType.NAME) { return [type.name]; } else if (type && type.type === NodeType.RECORD) { return [type.entries.reduce(function (obj, entry) { obj[entry.key] = transform(entry.value); return obj; }, {})]; } else if (type && type.type === NodeType.GENERIC) { if (type.meta.syntax === SyntaxType.GenericTypeSyntax.ANGLE_BRACKET) { return [type.subject.name + '<' + transform(type.objects[0]).join('|') + '>']; } else if (type.meta.syntax === SyntaxType.GenericTypeSyntax.ANGLE_BRACKET_WITH_DOT) { return [type.subject.name + '.<' + transform(type.objects[0]).join('|') + '>']; } else if (type.meta.syntax === SyntaxType.GenericTypeSyntax.SQUARE_BRACKET) { return [type.subject.name + '[' + transform(type.objects[0]).join('|') + ']']; } else if (type.meta.syntax === SyntaxType.VariadicTypeSyntax.PREFIX_DOTS) { return [`...${type.subject.name}`]; } else if (type.meta.syntax === SyntaxType.VariadicTypeSyntax.SUFFIX_DOTS) { return [`${type.subject.name}...`]; } else if (type.meta.syntax === SyntaxType.VariadicTypeSyntax.ONLY_DOTS) { return ['...']; } return [type.subject.name] } else if (type && type.value) { return transform(type.value); } else { return type.toString(); } }(result)); if(tag) { tag.types = types; tag.typesDescription = publish(result, customPublisher).replace(/^\?|=$/, ''); tag.optional = (tag.name && tag.name.slice(0,1) === '[') || result.type === NodeType.OPTIONAL; tag.nullable = result.type === NodeType.NULLABLE; tag.nonNullable = result.meta ? result.meta.syntax === 'SUFFIX_QUESTION_MARK' || result.meta.syntax === 'PREFIX_BANG': false; tag.variable = result.type === NodeType.VARIADIC; } return types; }; /** * Determine if a parameter is optional. * * Examples: * JSDoc: {Type} [name] * Google: {Type=} name * TypeScript: {Type?} name * * @param {Object} tag * @return {Boolean} * @api public */ exports.parseParamOptional = function(tag) { var lastTypeChar = tag.types.slice(-1)[0].slice(-1); return tag.name.slice(0,1) === '[' || lastTypeChar === '=' || lastTypeChar === '?'; }; /** * Parse the context from the given `str` of js. * * This method attempts to discover the context * for the comment based on it's code. Currently * supports: * * - classes * - class constructors * - class methods * - function statements * - function expressions * - prototype methods * - prototype properties * - methods * - properties * - declarations * * @param {String} str * @param {Object=} parentContext An indication if we are already in something. Like a namespace or an inline declaration. * @return {Object} * @api public */ exports.parseCodeContext = function(str, parentContext) { parentContext = parentContext || {}; var ctx; // loop through all context matchers, returning the first successful match return exports.contextPatternMatchers.some(function (matcher) { return ctx = matcher(str, parentContext); }) && ctx; }; exports.contextPatternMatchers = [ function (str) { // class, possibly exported by name or as a default if (/^\s*(export(\s+default)?\s+)?class\s+([\w$]+)(\s+extends\s+([\w$.]+(?:\(.*\))?))?\s*{/.exec(str)) { return { type: 'class' , constructor: RegExp.$3 , cons: RegExp.$3 , name: RegExp.$3 , extends: RegExp.$5 , string: 'new ' + RegExp.$3 + '()' }; } }, function (str, parentContext) { // class constructor if (/^\s*constructor\s*\(/.exec(str)) { return { type: 'constructor' , constructor: parentContext.name , cons: parentContext.name , name: 'constructor' , string: (parentContext && parentContext.name && parentContext.name + '.prototype.' || '') + 'constructor()' }; // class method } }, function (str, parentContext) { if (/^\s*(static)?\s*(\*)?\s*([\w$]+|\[.*\])\s*\(/.exec(str)) { return { type: 'method' , constructor: parentContext.name , cons: parentContext.name , name: RegExp.$2 + RegExp.$3 , string: (parentContext && parentContext.name && parentContext.name + (RegExp.$1 ? '.' : '.prototype.') || '') + RegExp.$2 + RegExp.$3 + '()' }; // named function statement, possibly exported by name or as a default } }, function (str) { if (/^\s*(export(\s+default)?\s+)?function\s+([\w$]+)\s*\(/.exec(str)) { return { type: 'function' , name: RegExp.$3 , string: RegExp.$3 + '()' }; } }, function (str) { // anonymous function expression exported as a default if (/^\s*export\s+default\s+function\s*\(/.exec(str)) { return { type: 'function' , name: RegExp.$1 // undefined , string: RegExp.$1 + '()' }; } }, function (str) { // function expression if (/^return\s+function(?:\s+([\w$]+))?\s*\(/.exec(str)) { return { type: 'function' , name: RegExp.$1 , string: RegExp.$1 + '()' }; } }, function (str) { // function expression if (/^\s*(?:const|let|var)\s+([\w$]+)\s*=\s*function/.exec(str)) { return { type: 'function' , name: RegExp.$1 , string: RegExp.$1 + '()' }; } }, function (str, parentContext) { // prototype method if (/^\s*([\w$.]+)\s*\.\s*prototype\s*\.\s*([\w$]+)\s*=\s*function/.exec(str)) { return { type: 'method' , constructor: RegExp.$1 , cons: RegExp.$1 , name: RegExp.$2 , string: RegExp.$1 + '.prototype.' + RegExp.$2 + '()' }; } }, function (str) { // prototype property if (/^\s*([\w$.]+)\s*\.\s*prototype\s*\.\s*([\w$]+)\s*=\s*([^\n;]+)/.exec(str)) { return { type: 'property' , constructor: RegExp.$1 , cons: RegExp.$1 , name: RegExp.$2 , value: RegExp.$3.trim() , string: RegExp.$1 + '.prototype.' + RegExp.$2 }; } }, function (str) { // prototype property without assignment if (/^\s*([\w$]+)\s*\.\s*prototype\s*\.\s*([\w$]+)\s*/.exec(str)) { return { type: 'property' , constructor: RegExp.$1 , cons: RegExp.$1 , name: RegExp.$2 , string: RegExp.$1 + '.prototype.' + RegExp.$2 }; } }, function (str) { // inline prototype if (/^\s*([\w$.]+)\s*\.\s*prototype\s*=\s*{/.exec(str)) { return { type: 'prototype' , constructor: RegExp.$1 , cons: RegExp.$1 , name: RegExp.$1 , string: RegExp.$1 + '.prototype' }; } }, function (str, parentContext) { // inline method if (/^\s*([\w$.]+)\s*:\s*function/.exec(str)) { return { type: 'method' , constructor: parentContext.name , cons: parentContext.name , name: RegExp.$1 , string: (parentContext && parentContext.name && parentContext.name + '.prototype.' || '') + RegExp.$1 + '()' }; } }, function (str, parentContext) { // inline property if (/^\s*([\w$.]+)\s*:\s*([^\n;]+)/.exec(str)) { return { type: 'property' , constructor: parentContext.name , cons: parentContext.name , name: RegExp.$1 , value: RegExp.$2.trim() , string: (parentContext && parentContext.name && parentContext.name + '.' || '') + RegExp.$1 }; } }, function (str, parentContext) { // inline getter/setter if (/^\s*(get|set)\s*([\w$.]+)\s*\(/.exec(str)) { return { type: 'property' , constructor: parentContext.name , cons: parentContext.name , name: RegExp.$2 , string: (parentContext && parentContext.name && parentContext.name + '.prototype.' || '') + RegExp.$2 }; } }, function (str) { // method if (/^\s*([\w$.]+)\s*\.\s*([\w$]+)\s*=\s*function/.exec(str)) { return { type: 'method' , receiver: RegExp.$1 , name: RegExp.$2 , string: RegExp.$1 + '.' + RegExp.$2 + '()' }; } }, function (str) { // property if (/^\s*([\w$.]+)\s*\.\s*([\w$]+)\s*=\s*([^\n;]+)/.exec(str)) { return { type: 'property' , receiver: RegExp.$1 , name: RegExp.$2 , value: RegExp.$3.trim() , string: RegExp.$1 + '.' + RegExp.$2 }; } }, function (str) { // declaration if (/^\s*(?:const|let|var)\s+([\w$]+)\s*=\s*([^\n;]+)/.exec(str)) { return { type: 'declaration' , name: RegExp.$1 , value: RegExp.$2.trim() , string: RegExp.$1 }; } } ];