## # Collects comments from source files # @module collect # @see Collector _ = require 'lodash' dox = require './dox' inflect = require 'inflect' markdown = require 'marked' is_test_mode = process.env.NODE_ENV is 'test' ## # Collects comments from source files class Collector ## # Create a Collector instance constructor: (@contents, @options = {}) -> @result = project_title: @options.title or 'croquis-jsdoc' ids: {} classes: {} guides: [] pages: {} restapis: {} features: [] files: [] ## # Adds a guide file to the result _addGuide: (path, data) -> id = path.substr(0, path.length-3) name = path.substr(0, path.length-8).replace(/\//g, '.') item = name: inflect.humanize inflect.underscore name filename: 'guides/' + name content: markdown data @result.guides.push item @result.ids[id] = item ## # Adds a feature file to the result _addFeature: (path, data) -> name = path.substr 0, path.length-8 namespace = '' name = name.replace /(.*)\//, (_, $1) -> namespace = $1 + '/' return '' feature = '' data = data.replace /Feature: (.*)/, (_, $1) -> feature = $1 return '' @result.features.push name: namespace + name namespace: namespace filename: 'features/' + namespace.replace(/\//g, '.') + name feature: feature content: data ## # Adds a source file to the result _addFile: (path, data) -> namespace = '' name = path.replace /(.*)\//, (_, $1) -> namespace = $1 + '/' return '' @result.files.push name: namespace + name namespace: namespace filename: 'files/' + namespace.replace(/\//g, '.') + name content: data ## # Checks flags of parameter # # * '[' name ']' : optional # * name '=' value : default value # * '+' name : addable # * '-' name : excludable # @param {Object} tag # @return {Object} given tag _processParamFlags: (tag) -> # is optional parameter? if tag.name[0] is '[' and tag.name[tag.name.length-1] is ']' tag.name = tag.name.substr 1, tag.name.length-2 if (pos = tag.name.indexOf '=')>=0 tag.default_value = tag.name.substr pos+1 tag.name = tag.name.substr 0, pos tag.optional = true if tag.name.substr(0, 1) is '+' tag.name = tag.name.substr(1) tag.addable = true if tag.name.substr(0, 1) is '-' tag.name = tag.name.substr(1) tag.excludable = true return tag ## # Finds a parameter in the list # @param {Array<Object>} params # @param {String} name # @return {Object} _findParam: (params, name) -> for param in params if param.name is name return param if param.params found = @_findParam param.params, name return found if found return ## # Makes parameters(or returnprops) nested _makeNested: (comment, targetName) -> i = comment[targetName].length while i-->0 param = comment[targetName][i] if match = param.name.match /\[?([^=]*)\.([^\]]*)\]?/ parentParam = @_findParam comment[targetName], match[1] if parentParam comment[targetName].splice i, 1 parentParam[targetName] = parentParam[targetName] or [] param.name = match[2] parentParam[targetName].unshift param ## # Apply markdown _applyMarkdown: (str) -> # we cannot use '###' for header level 3 or above in CoffeeScript, instead web use '##\#', ''##\##', ... # recover this for markdown str = str.replace /#\\#/g, '##' return markdown str ## # Classifies type and collect id _classifyComments: (comments) -> current_class = undefined current_module = undefined comments.forEach (comment) => comment.ctx or comment.ctx = {} comment.params = [] comment.returnprops = [] comment.throws = [] comment.resterrors = [] comment.sees = [] comment.reverse_sees = [] comment.todos = [] comment.extends = [] comment.subclasses = [] comment.uses = [] comment.usedbys = [] comment.properties = [] comment.examples = [] if comment.ctx.type is 'property' or comment.ctx.type is 'method' id = comment.ctx.string.replace('()', '') else id = comment.ctx.name comment.ctx.fullname = id comment.namespace or= '' if comment.ctx.type is 'property' or comment.ctx.type is 'method' if comment.ctx.cons? comment.isStatic = false comment.ctx.class_name = comment.ctx.cons else if comment.ctx.receiver? comment.isStatic = true comment.ctx.class_name = comment.ctx.receiver last = 0 for tag, i in comment.tags if tag.type is '' comment.tags[last].string += "\n#{tag.string}" continue last = i switch tag.type when 'page', 'restapi', 'class' comment.ctx.type = tag.type if tag.string comment.ctx.name = tag.string comment.ctx.fullname = id = comment.ctx.name when 'module' comment.ctx.type = 'class' comment.is_module = true if tag.string comment.ctx.name = tag.string comment.ctx.fullname = id = comment.ctx.name comment.code = null when 'memberof' if /(::|#|\.prototype)$/.test tag.parent comment.isStatic = false comment.ctx.class_name = tag.parent.replace /(::|#|\.prototype)$/, '' else comment.isStatic = true comment.ctx.class_name = tag.parent when 'namespace' comment.namespace = if tag.string then tag.string else '' when 'property' comment.ctx.type = tag.type comment.ctx.name = tag.name when 'method' comment.ctx.type = tag.type if tag.string comment.ctx.name = tag.string when 'static' comment.isStatic = true when 'private' comment.isPrivate = true when 'abstract' comment.isAbstract = true when 'async' comment.isAsync = true when 'promise' comment.doesReturnPromise = true when 'nodejscallback' comment.doesReturnNodejscallback = true when 'chainable' comment.isChainable = true when 'type' if not tag.types and tag.typeString typeString = tag.typeString if not /{.*}/.test typeString typeString = '{' + typeString + '}' dox.parseTagTypes typeString, tag when 'apimethod' comment.apimethod = tag.string.toUpperCase() id += '_' + comment.apimethod when 'param', 'return', 'returns', 'returnprop', 'throws', 'resterror', 'see' , 'extends', 'todo', 'api', 'uses', 'override', 'example', 'internal' else console.log "Unknown tag : #{tag.type} in #{comment.full_path}" if comment.namespace comment.namespace += '.' if comment.ctx.class_name if comment.ctx.type is 'function' comment.ctx.type = 'method' else if comment.ctx.type is 'declaration' comment.ctx.type = 'property' seperator = if comment.isStatic then '.' else '::' id = comment.ctx.class_name + seperator + comment.ctx.name comment.ctx.fullname = comment.ctx.class_name.replace(/.*[\./](\w+)/, '$1') + seperator + comment.ctx.name if comment.ctx.type is 'class' current_class = comment if comment.is_module current_module = comment if (comment.ctx.type is 'property' or comment.ctx.type is 'method') and not comment.namespace if current_class comment.namespace = current_class.namespace if current_module and not comment.ctx.class_name comment.ctx.class_name = current_module.ctx.name if id comment.id = id if @result.ids.hasOwnProperty id @result.ids[id] = 'DUPLICATED ENTRY' else @result.ids[id] = comment if comment.namespace and @result.ids.hasOwnProperty comment.namespace+id @result.ids[comment.namespace+id] = 'DUPLICATED ENTRY' else @result.ids[comment.namespace+id] = comment comment.html_id = (comment.namespace+id).replace(/[^A-Za-z0-9_]/g, '_') switch comment.ctx.type when 'class' comment.ctx.name = comment.namespace + comment.ctx.name comment.ctx.fullname = comment.namespace + comment.ctx.fullname @result.classes[comment.ctx.name] = comment if comment.is_module comment.filename = 'modules/' + comment.ctx.name.replace(/\//g, '.') else comment.filename = 'classes/' + comment.ctx.name.replace(/\//g, '.') when 'property', 'method' comment.ctx.class_name = comment.namespace + comment.ctx.class_name comment.filename = 'classes/' + comment.ctx.class_name.replace(/\//g, '.') when 'page' comment.filename = 'pages' when 'restapi' comment.filename = 'restapis' ## # Returns list of comments of the given file # @return {Array<Comment>} _getComments: (type, full_path, path, data) -> if type is 'coffeescript' comments = dox.parseCommentsCoffee data, { raw: true } comments.forEach (comment) -> comment.language = 'coffeescript' else if type is 'javascript' comments = dox.parseComments data, { raw: true } comments.forEach (comment) -> comment.language = 'javascript' else if type is 'typescript' comments = dox.parseCommentsTS data comments.forEach (comment) -> comment.language = 'typescript' else if type is 'page' namespace = '' name = path.substr(0, path.length-3).replace(/[^A-Za-z0-9]*Page$/, '') name = name.replace /(.*)\//, (_, $1) -> namespace = $1 return '' comments = [ { description: summary: '' body: data full: '' tags: [ { type: 'page', string: name } { type: 'namespace', string: namespace } ] } ] return if not comments? # filter out empty comments comments = comments.filter (comment) -> return comment.description.full or comment.description.summary or comment.description.body or comment.tags?.length > 0 comments.forEach (comment) -> comment.full_path = full_path comment.path = path return if @options.plugins comments.forEach (comment) => @options.plugins.forEach (plugin) -> plugin.onComment comment return return @_classifyComments comments return comments ## # Structuralizes comments _processComments: (comments) -> comments.forEach (comment) => desc = comment.description if desc desc.full = @_applyMarkdown desc.full desc.summary = @_applyMarkdown desc.summary desc.body = @_applyMarkdown desc.body for tag in comment.tags switch tag.type when 'param' tag = @_processParamFlags tag for type, i in tag.types tag.types[i] = type tag.description = tag.description comment.params.push tag when 'return', 'returns' for type, i in tag.types tag.types[i] = type tag.description = tag.description comment.return = tag when 'returnprop' tag = dox.parseTag '@param ' + tag.string tag = @_processParamFlags tag for type, i in tag.types tag.types[i] = type tag.description = tag.description comment.returnprops.push tag when 'throws' comment.throws.push message: tag.message, description: tag.description when 'resterror' if /{(\d+)\/([A-Za-z0-9_ ]+)}\s*(.*)/.exec tag.string comment.resterrors.push code: RegExp.$1, message: RegExp.$2, description: RegExp.$3 when 'see' str = tag.local or tag.url comment.sees.push str when 'todo' comment.todos.push tag.string when 'extends' comment.extends.push tag.string @result.ids[tag.string]?.subclasses.push comment.ctx.name when 'uses' comment.uses.push tag.string @result.ids[tag.string]?.usedbys.push comment.ctx.name when 'type' for type, i in tag.types tag.types[i] = type comment.types = tag.types when 'example' comment.examples.push tag when 'override' if @result.ids[tag.string] and @result.ids[tag.string] isnt 'DUPLICATED ENTRY' comment.override = @result.ids[tag.string] comment.override_link = tag.string if comment.ctx.type is 'class' if /^class +\w+ +extends +([\w\.]+)/.exec comment.class_code comment.extends.push RegExp.$1 @result.ids[RegExp.$1]?.subclasses.push comment.ctx.name # make parameters nested @_makeNested comment, 'params' @_makeNested comment, 'returnprops' if comment.doesReturnNodejscallback callback_params = [ { name: 'error' types: ['Error'] description: 'See throws' } ] if comment.return callback_params.push name: 'result' types: comment.return.types description: 'See returns' comment.params.push name: 'callback' types: ['Function'] optional: comment.doesReturnPromise description: 'NodeJS style\'s callback' params: callback_params if comment.isChainable and not comment.return comment.return = types: [comment.ctx.class_name] description: 'this' switch comment.ctx.type when 'property', 'method' class_name = comment.ctx.class_name if class_name and class_comment = @result.classes[class_name] if comment.ctx.is_constructor # merge to class comment class_comment.code = comment.code class_comment.codeStart = comment.codeStart class_comment.params = comment.params else class_comment.properties.push comment if class_comment.is_module comment.filename = comment.filename.replace('classes/', 'modules/') when 'page' @result.pages[comment.ctx.name] = comment when 'restapi' if comment.apimethod @result.restapis[comment.ctx.name+comment.apimethod] = comment return if /^(GET|POST|PATCH|PUT|DELETE|HEAD)/i.test comment.ctx.name comment.apimethod = RegExp.$1.toUpperCase() @result.restapis[comment.ctx.name] = comment ## # Refines result. # # - convert hash to sorted array # - classes -> classes & modules _refineResult: -> result = @result result.classes = Object.keys(result.classes).sort( (a,b) -> a_ns = result.classes[a].namespace b_ns = result.classes[b].namespace return -1 if a_ns < b_ns return 1 if a_ns > b_ns if a<b then -1 else 1 ).map (name) -> result.classes[name] result.pages = Object.keys(result.pages).sort( (a,b) -> a_ns = result.pages[a].namespace b_ns = result.pages[b].namespace return -1 if a_ns < b_ns return 1 if a_ns > b_ns if a<b then -1 else 1 ).map (name) -> result.pages[name] result.restapis = Object.keys(result.restapis).sort( (a,b) -> a_ns = result.restapis[a].namespace b_ns = result.restapis[b].namespace return -1 if a_ns < b_ns return 1 if a_ns > b_ns a = a.replace /([A-Z]+) \/(.*)/, '-$2 $1' b = b.replace /([A-Z]+) \/(.*)/, '-$2 $1' if a<b then -1 else 1 ).map (name) -> result.restapis[name] result.guides = result.guides.sort (a,b) -> if a.name < b.name then -1 else 1 result.features = result.features.sort (a,b) -> if a.name < b.name then -1 else 1 result.files = result.files.sort (a,b) -> a_ns = a.namespace b_ns = b.namespace return -1 if a_ns < b_ns return 1 if a_ns > b_ns if a.name < b.name then -1 else 1 result.classes.forEach (klass) -> klass.properties.sort (a, b) -> if a.ctx.name < b.ctx.name then -1 else 1 for property in klass.properties property.ctx = _.pick property.ctx, 'type', 'name', 'fullname' result.modules = result.classes.filter (klass) -> klass.is_module result.classes = result.classes.filter (klass) -> not klass.is_module ## # Returns the type of a file _getType: (path) -> if /\.coffee$/.test path return 'coffeescript' else if /\.js$/.test path return 'javascript' else if /\.ts$/.test path return 'typescript' else if /Page\.md$/.test path return 'page' else if /Guide\.md$/.test path return 'guide' else if /\.feature$/.test path return 'feature' else if path is 'README' return 'readme' else return 'unknown' ## # Makes reverse see alsos _makeReverseSeeAlso: (comments) -> for comment in comments for see in comment.sees other = @result.ids[see] if other and other isnt 'DUPLICATED ENTRY' me = @result.ids[comment.id] if me and me is 'DUPLICATED ENTRY' other.reverse_sees?.push comment.namespace+comment.id else other.reverse_sees?.push comment.id return ## # Runs run: -> all_comments = [] file_count_read = 0 for {full_path, path, data} in @contents type = @_getType path switch type when 'guide' @_addGuide path, data when 'feature' @_addFeature path, data when 'coffeescript', 'javascript', 'typescript', 'page' comments = @_getComments type, full_path, path, data if comments? [].push.apply all_comments, comments when 'readme' @result.readme = markdown data if type is 'coffeescript' or type is 'javascript' or type is 'typescript' @_addFile path, data file_count_read++ console.log path + ' is processed' if not (@options.quiet or is_test_mode) console.log 'Total ' + file_count_read + ' files processed' if not is_test_mode @_processComments all_comments if @options.reverse_see_also @_makeReverseSeeAlso all_comments if not @options.files @result.files = [] @_refineResult() ## # Collects # @param {Array<Content>} contents # @param {Options} options # @return {Result} # @memberOf collect collect = (contents, options) -> collector = new Collector contents, options collector.run() return collector.result module.exports = collect