UNPKG

crojsdoc

Version:

A documentation generator for JavaScript and CoffeeScript

683 lines (655 loc) 19.8 kB
// Generated by CoffeeScript 2.4.1 //# // Module dependencies. var markdown, markedOptions, renderer; markdown = require('marked'); renderer = new markdown.Renderer(); //renderer.heading = (text, level) -> // '<h' + level + '>' + text + '</h' + level + '>\n' //renderer.paragraph = (text) -> // '<p>' + text + '</p>' //renderer.br = () -> // '<br />' markedOptions = { renderer: renderer, gfm: true, tables: true, // breaks: true pedantic: false, sanitize: false, smartLists: true, smartypants: false }; markdown.setOptions(markedOptions); //# // 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 = {}) { var buf, code, comment, comments, i, ignore, len, lineNum, lineNumStarting, linterPrefixes, parentContext, ref, skipPattern, skipSingleStar, withinMultiline, withinSingle, withinString; js = js.replace(/\r\n/gm, '\n'); comments = []; skipSingleStar = options.skipSingleStar; buf = ''; withinMultiline = false; withinSingle = false; withinString = false; linterPrefixes = options.skipPrefixes || ['jslint', 'jshint', 'eshint']; skipPattern = new RegExp('^' + ((ref = options.raw) != null ? ref : { '': '<p>' }) + '(' + linterPrefixes.join('|') + ')'); lineNum = 1; lineNumStarting = 1; i = 0; len = js.length; while (i < len) { // 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 && ('\'' === js[i] || '"' === js[i])) { withinString = !withinString; buf += js[i]; } else { buf += js[i]; } if ('\n' === js[i]) { lineNum++; } i++; } 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) { var indent; // Find indentation from first line of code. 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 = {}) { var comment, description, raw, tags; str = str.trim(); 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(description.full); description.summary = markdown(description.summary); description.body = markdown(description.body); comment.tags.forEach(function(tag) { if (tag.description) { return tag.description = markdown(tag.description); } else { return tag.html = markdown(tag.string); } }); } 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 extract, level, split; level = 0; extract = ''; split = []; str.split('').forEach(function(c) { if (c.match(/\s/) && level === 0) { split.push(extract); return extract = ''; } else { if (c === '{') { level++; } else if (c === '}') { level--; } return 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 getMultilineDescription, lines, matchType, matchTypeStr, parts, tag, type, typeString; tag = {}; lines = str.split('\n'); parts = exports.extractTagParts(lines[0]); type = tag.type = parts.shift().replace('@', '').toLowerCase(); matchType = new RegExp('^@?' + type + ' *'); matchTypeStr = /^\{.+\}$/; tag.string = str.replace(matchType, ''); getMultilineDescription = function() { var description; description = parts.join(' '); if (lines.length > 1) { if (description) { description += '\n'; } description += lines.slice(1).join('\n'); } return description; }; switch (type) { case 'property': case 'template': case 'param': typeString = matchTypeStr.test(parts[0]) ? parts.shift() : ''; tag.name = parts.shift() || ''; tag.description = getMultilineDescription(); exports.parseTagTypes(typeString, tag); break; case 'define': case 'return': case 'returns': typeString = matchTypeStr.test(parts[0]) ? parts.shift() : ''; exports.parseTagTypes(typeString, tag); tag.description = getMultilineDescription(); 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': typeString = parts.shift(); if (!/{.*}/.test(typeString)) { typeString = '{' + typeString + '}'; } exports.parseTagTypes(typeString, 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': if (/{([^}]+)}\s*(.*)/.exec(str)) { tag.message = RegExp.$1; tag.description = RegExp.$2; } else { tag.message = ''; tag.description = str; } 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 = getMultilineDescription().replace(/\s+$/, ''); } 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) { var NodeType, SyntaxType, optional, parse, publish, result, transform, types; if (!str) { if (tag) { tag.types = []; tag.optional = false; } return []; } ({parse, publish, NodeType, SyntaxType} = require('jsdoctypeparser')); result = parse(str.substr(1, str.length - 2)); optional = false; if (result.type === NodeType.OPTIONAL) { optional = true; result = result.value; } transform = function(ast) { var left, right; if (ast.type === NodeType.NAME) { return [ast.name]; } else if (ast.type === NodeType.UNION) { left = transform(ast.left); right = transform(ast.right); [].push.apply(left, right); return left; } else if (ast.type === NodeType.RECORD) { return [ ast.entries.reduce(function(obj, entry) { obj[entry.key] = transform(entry.value); return obj; }, {}) ]; } else if (ast.type === NodeType.GENERIC && ast.meta.syntax === SyntaxType.GenericTypeSyntax.ANGLE_BRACKET_WITH_DOT) { ast = { ...ast, meta: { ...ast.meta, syntax: SyntaxType.GenericTypeSyntax.ANGLE_BRACKET } }; return [publish(ast)]; } else { return [publish(ast)]; } }; types = transform(result); if (tag) { tag.types = types; tag.optional = (tag.name && tag.name.slice(0, 1) === '[') || optional; } return types; }; //# // 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) { var ctx; if (!parentContext) { parentContext = {}; } ctx = void 0; // loop through all context matchers, returning the first successful match return exports.contextPatternMatchers.some(function(matcher) { return ctx = matcher(str, parentContext); }) && ctx; }; exports.contextPatternMatchers = [ // class, possibly exported by name or as a default function(str) { 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 + '()' }; } }, // class constructor function(str, parentContext) { if (/^\s*constructor\s*\(/.exec(str)) { return { type: 'method', constructor: parentContext.name, cons: parentContext.name, name: 'constructor', string: ((parentContext != null ? parentContext.name : void 0) ? parentContext.name + '.prototype.' : '') + 'constructor()', is_constructor: true }; } }, // 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 != null ? parentContext.name : void 0) ? parentContext.name + (RegExp.$1 ? '.' : '.prototype.') : '') + RegExp.$2 + RegExp.$3 + '()' }; } }, // named function statementpossibly 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 + '()' }; } }, // anonymous function expression exported as a default function(str) { if (/^\s*export\s+default\s+function\s*\(/.exec(str)) { return { type: 'function', name: RegExp.$1, // undefined string: RegExp.$1 + '()' }; } }, // function expression function(str) { if (/^return\s+function(?:\s+([\w$]+))?\s*\(/.exec(str)) { return { type: 'function', name: RegExp.$1, string: RegExp.$1 + '()' }; } }, // function expression function(str) { if (/^\s*(?:const|let|var)\s+([\w$]+)\s*=\s*function/.exec(str)) { return { type: 'function', name: RegExp.$1, string: RegExp.$1 + '()' }; } }, // prototype method function(str, parentContext) { 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 + '()' }; } }, // prototype property function(str) { 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 }; } }, // prototype property without assignment function(str) { 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 }; } }, // inline prototype function(str) { 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' }; } }, // inline method function(str, parentContext) { if (/^\s*([\w$.]+)\s*:\s*function/.exec(str)) { return { type: 'method', constructor: parentContext.name, cons: parentContext.name, name: RegExp.$1, string: ((parentContext != null ? parentContext.name : void 0) ? parentContext.name + '.prototype.' : '') + RegExp.$1 + '()' }; } }, // inline property function(str, parentContext) { 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 != null ? parentContext.name : void 0) ? parentContext.name + '.' : '') + RegExp.$1 }; } }, // inline getter/setter function(str, parentContext) { if (/^\s*(get|set)\s*([\w$.]+)\s*\(/.exec(str)) { return { type: 'property', constructor: parentContext.name, cons: parentContext.name, name: RegExp.$2, string: ((parentContext != null ? parentContext.name : void 0) ? parentContext.name + '.prototype.' : '') + RegExp.$2 }; } }, // method function(str) { 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 + '()' }; } }, // property function(str) { 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 }; } }, // declaration function(str) { 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 }; } } ];