Collector

Collects comments from source files

See Also:

(defined in /src/collect.coffee:15)
class Collector

Parameters

(None)
(defined in /src/collect.coffee:18)
constructor: (@contents, @options = {}) ->
  @result =
    project_title: @options.title or 'croquis-jsdoc'
    ids: {}
    classes: {}
    guides: []
    pages: {}
    restapis: {}
    features: []
    files: []

Parameters

(None)

Returns

(Nothing)
(defined in /src/collect.coffee:43)
_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

Parameters

(None)

Returns

(Nothing)
(defined in /src/collect.coffee:62)
_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

Parameters

(None)

Returns

(Nothing)
(defined in /src/collect.coffee:31)
_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

Parameters

(None)

Returns

(Nothing)
(defined in /src/collect.coffee:128)
_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

Parameters

(None)

Returns

(Nothing)
(defined in /src/collect.coffee:136)
_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'

Parameters

NameTypeDescription
paramsArray<Object>
nameString

Returns

NameTypeDescription
(Returns)Object
(defined in /src/collect.coffee:103)
_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

Parameters

(None)

Returns

NameTypeDescription
(Returns)Array<Comment>
(defined in /src/collect.coffee:289)
_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

Parameters

(None)

Returns

(Nothing)
(defined in /src/collect.coffee:504)
_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'

Parameters

(None)

Returns

(Nothing)
(defined in /src/collect.coffee:114)
_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

Parameters

(None)

Returns

(Nothing)
(defined in /src/collect.coffee:524)
_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

Parameters

(None)

Returns

(Nothing)
(defined in /src/collect.coffee:343)
_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
  • '[' name ']' : optional
  • name '=' value : default value
  • '+' name : addable
  • '-' name : excludable

Parameters

NameTypeDescription
tagObject

Returns

NameTypeDescription
(Returns)Objectgiven tag
(defined in /src/collect.coffee:82)
_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
  • convert hash to sorted array
  • classes -> classes & modules

Parameters

(None)

Returns

(Nothing)
(defined in /src/collect.coffee:458)
_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

Parameters

(None)

Returns

(Nothing)
(defined in /src/collect.coffee:538)
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()
Fork me on GitHub