##
# 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
}
]