## # Module dependencies. 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 = (js, options = {}) -> js = js.replace /\r\n/gm, '\n' comments = [] skipSingleStar = options.skipSingleStar buf = '' withinMultiline = false withinSingle = false withinString = false linterPrefixes = options.skipPrefixes or ['jslint', 'jshint', 'eshint'] skipPattern = new RegExp('^' + (options.raw ? '' : '<p>') + '('+ linterPrefixes.join('|') + ')') lineNum = 1 lineNumStarting = 1 i = 0 len = js.length while i < len # start comment if not withinMultiline and not withinSingle and not withinString and '/' is js[i] and '*' is js[i+1] and (not skipSingleStar or js[i+2] is '*') 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 and comment.ctx comment.ctx.type = 'constructor' # starting a new namespace if comment.ctx and (comment.ctx.type is 'prototype' or comment.ctx.type is 'class') parentContext = comment.ctx # reasons to clear the namespace # new property/method in a different constructor else if not parentContext or not comment.ctx or not comment.ctx.constructor or not parentContext.constructor or parentContext.constructor isnt comment.ctx.constructor parentContext = null buf = '' i += 2 withinMultiline = true ignore = '!' is 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 ' ' isnt js[i] and '\n' isnt js[i] and '\t' isnt js[i] and '!' isnt js[i] i-- # end comment else if withinMultiline and not withinSingle and '*' is js[i] and '/' is 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 not comment.description.full.match(skipPattern) comments.push comment withinMultiline = ignore = false buf = '' else if not withinSingle and not withinMultiline and not withinString and '/' is js[i] and '/' is js[i+1] withinSingle = true buf += js[i] else if withinSingle and not withinMultiline and '\n' is js[i] withinSingle = false buf += js[i] else if not withinSingle and not withinMultiline and ('\'' is js[i] or '"' is js[i]) withinString = not withinString buf += js[i] else buf += js[i] if '\n' is js[i] lineNum++ i++ if comments.length is 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 comments ## # Removes excess indentation from string of code. # # @param {String} str # @return {String} # @api public exports.trimIndentation = (str) -> # 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') 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 = (str, options = {}) -> str = str.trim() comment = tags: [] raw = options.raw description = {} tags = str.split(/\n\s*@/) # A comment has no description if tags[0].charAt(0) is '@' 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 (tag) -> 'private' is tag.visibility comment.isConstructor = comment.tags.some (tag) -> 'constructor' is tag.type or 'augments' is tag.type comment.isClass = comment.tags.some (tag) -> 'class' is tag.type comment.isEvent = comment.tags.some (tag) -> 'event' is tag.type if not description.full or not description.full.trim() comment.tags.some (tag) -> if 'description' is tag.type description.full = tag.full description.summary = tag.summary description.body = tag.body true # markdown if not raw description.full = markdown description.full description.summary = markdown description.summary description.body = markdown description.body comment.tags.forEach (tag) -> if tag.description tag.description = markdown tag.description else tag.html = markdown tag.string 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 = (str) -> level = 0 extract = '' split = [] str.split('').forEach (c) -> if c.match(/\s/) and level is 0 split.push extract extract = '' else if c is '{' level++ else if c is '}' level-- extract += c split.push extract split.filter (str) -> str.length > 0 ## # Parse tag string "@param {Array} name description" etc. # # @param {String} # @return {Object} # @api public exports.parseTag = (str) -> 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 = -> description = parts.join ' ' if lines.length > 1 if description description += '\n' description += lines.slice(1).join('\n') description switch type when 'property', 'template', 'param' typeString = if matchTypeStr.test(parts[0]) then parts.shift() else '' tag.name = parts.shift() or '' tag.description = getMultilineDescription() exports.parseTagTypes typeString, tag when 'define', 'return', 'returns' typeString = if matchTypeStr.test(parts[0]) then parts.shift() else '' exports.parseTagTypes typeString, tag tag.description = getMultilineDescription() when 'see' if ~str.indexOf('http') tag.title = if parts.length > 1 then parts.shift() else '' tag.url = parts.join(' ') else tag.local = parts.join(' ') when 'api' tag.visibility = parts.shift() when 'public', 'private', 'protected' tag.visibility = type when 'enum', 'typedef', 'type' typeString = parts.shift() if not /{.*}/.test typeString typeString = '{' + typeString + '}' exports.parseTagTypes typeString, tag when 'lends', 'memberof' tag.parent = parts.shift() when 'extends', 'implements', 'augments' tag.otherClass = parts.shift() when 'borrows' tag.otherMemberName = parts.join(' ').split(' as ')[0] tag.thisMemberName = parts.join(' ').split(' as ')[1] when 'throws' if /{([^}]+)}\s*(.*)/.exec str tag.message = RegExp.$1 tag.description = RegExp.$2 else tag.message = '' tag.description = str when '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') else tag.string = getMultilineDescription().replace(/\s+$/, '') 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 = (str, tag) -> if not 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 is NodeType.OPTIONAL optional = true result = result.value transform = (ast) -> if ast.type is NodeType.NAME [ast.name] else if ast.type is NodeType.UNION left = transform ast.left right = transform ast.right [].push.apply left, right left else if ast.type is NodeType.RECORD [ast.entries.reduce (obj, entry) -> obj[entry.key] = transform entry.value obj , {}] else if ast.type is NodeType.GENERIC and ast.meta.syntax is SyntaxType.GenericTypeSyntax.ANGLE_BRACKET_WITH_DOT ast = {...ast, meta: {...ast.meta, syntax: SyntaxType.GenericTypeSyntax.ANGLE_BRACKET}} [publish ast] else [publish ast] types = transform result if tag tag.types = types tag.optional = (tag.name and tag.name.slice(0,1) is '[') or optional 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 = (str, parentContext) -> if not parentContext parentContext = {} ctx = undefined # loop through all context matchers, returning the first successful match exports.contextPatternMatchers.some((matcher) -> ctx = matcher(str, parentContext) ) and ctx exports.contextPatternMatchers = [ # class, possibly exported by name or as a default (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 (str, parentContext) -> if /^\s*constructor\s*\(/.exec(str) return { type: 'method' constructor: parentContext.name cons: parentContext.name name: 'constructor' string: (if parentContext?.name then parentContext.name + '.prototype.' else '') + 'constructor()' is_constructor: true } # class method (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: (if parentContext?.name then parentContext.name + (if RegExp.$1 then '.' else '.prototype.') else '') + RegExp.$2 + RegExp.$3 + '()' } # named function statementpossibly exported by name or as a default (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 (str) -> if /^\s*export\s+default\s+function\s*\(/.exec(str) return { type: 'function' name: RegExp.$1 # undefined string: RegExp.$1 + '()' } # function expression (str) -> if /^return\s+function(?:\s+([\w$]+))?\s*\(/.exec(str) return { type: 'function' name: RegExp.$1 string: RegExp.$1 + '()' } # function expression (str) -> if /^\s*(?:const|let|var)\s+([\w$]+)\s*=\s*function/.exec(str) return { type: 'function' name: RegExp.$1 string: RegExp.$1 + '()' } # prototype method (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 (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 (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 (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 (str, parentContext) -> if /^\s*([\w$.]+)\s*:\s*function/.exec(str) return { type: 'method' constructor: parentContext.name cons: parentContext.name name: RegExp.$1 string: (if parentContext?.name then parentContext.name + '.prototype.' else '') + RegExp.$1 + '()' } # inline property (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: (if parentContext?.name then parentContext.name + '.' else '') + RegExp.$1 } # inline getter/setter (str, parentContext) -> if /^\s*(get|set)\s*([\w$.]+)\s*\(/.exec(str) return { type: 'property' constructor: parentContext.name cons: parentContext.name name: RegExp.$2 string: (if parentContext?.name then parentContext.name + '.prototype.' else '') + RegExp.$2 } # method (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 (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 (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 } ]