parser.js
function parse_block (source, opts) {
  var trim = opts.trim
    ? function trim (s) { return s.trim() }
    : function trim (s) { return s }

  var source_str = source
    .map(function (line) { return trim(line.source) })
    .join('\n')

  source_str = trim(source_str)

  var start = source[0].number

  // merge source lines into tags
  // we assume tag starts with "@"
  source = source
    .reduce(function (tags, line) {
      line.source = trim(line.source)

      if (line.source.match(/^\s*@(\w+)/)) {
        tags.push({source: [line.source], line: line.number})
      } else {
        var tag = tags[tags.length - 1]
        if (opts.join !== undefined && opts.join !== false && opts.join !== 0 &&
            !line.startWithStar && tag.source.length > 0) {
          var source
          if (typeof opts.join === 'string') {
            source = opts.join + line.source.replace(/^\s+/, '')
          } else if (typeof opts.join === 'number') {
            source = line.source
          } else {
            source = ' ' + line.source.replace(/^\s+/, '')
          }
          tag.source[tag.source.length - 1] += source
        } else {
          tag.source.push(line.source)
        }
      }

      return tags
    }, [{source: []}])
    .map(function (tag) {
      tag.source = trim(tag.source.join('\n'))
      return tag
    })

  // Block description
  var description = source.shift()

  // skip if no descriptions and no tags
  if (description.source === '' && source.length === 0) {
    return null
  }

  var tags = source.reduce(function (tags, tag) {
    var tag_node = parse_tag(tag.source, opts.parsers)

    if (!tag_node) { return tags }

    tag_node.line = tag.line
    tag_node.source = tag.source

    if (opts.dotted_names && tag_node.name.indexOf('.') !== -1) {
      var parent_name
      var parent_tag
      var parent_tags = tags
      var parts = tag_node.name.split('.')

      while (parts.length > 1) {
        parent_name = parts.shift()
        parent_tag = find(parent_tags, {
          tag: tag_node.tag,
          name: parent_name
        })

        if (!parent_tag) {
          parent_tag = {
            tag: tag_node.tag,
            line: Number(tag_node.line),
            name: parent_name,
            type: '',
            description: ''
          }
          parent_tags.push(parent_tag)
        }

        parent_tag.tags = parent_tag.tags || []
        parent_tags = parent_tag.tags
      }

      tag_node.name = parts[0]
      parent_tags.push(tag_node)
      return tags
    }

    return tags.concat(tag_node)
  }, [])

  return {
    tags: tags,
    line: start,
    description: description.source,
    source: source_str
  }
}
function parse_block (source, opts) {
  var trim = opts.trim
    ? function trim (s) { return s.trim() }
    : function trim (s) { return s }

  var source_str = source
    .map(function (line) { return trim(line.source) })
    .join('\n')

  source_str = trim(source_str)

  var start = source[0].number

  // merge source lines into tags
  // we assume tag starts with "@"
  source = source
    .reduce(function (tags, line) {
      line.source = trim(line.source)

      if (line.source.match(/^\s*@(\w+)/)) {
        tags.push({source: [line.source], line: line.number})
      } else {
        var tag = tags[tags.length - 1]
        if (opts.join !== undefined && opts.join !== false && opts.join !== 0 &&
            !line.startWithStar && tag.source.length > 0) {
          var source
          if (typeof opts.join === 'string') {
            source = opts.join + line.source.replace(/^\s+/, '')
          } else if (typeof opts.join === 'number') {
            source = line.source
          } else {
            source = ' ' + line.source.replace(/^\s+/, '')
          }
          tag.source[tag.source.length - 1] += source
        } else {
          tag.source.push(line.source)
        }
      }

      return tags
    }, [{source: []}])
    .map(function (tag) {
      tag.source = trim(tag.source.join('\n'))
      return tag
    })

  // Block description
  var description = source.shift()

  // skip if no descriptions and no tags
  if (description.source === '' && source.length === 0) {
    return null
  }

  var tags = source.reduce(function (tags, tag) {
    var tag_node = parse_tag(tag.source, opts.parsers)

    if (!tag_node) { return tags }

    tag_node.line = tag.line
    tag_node.source = tag.source

    if (opts.dotted_names && tag_node.name.indexOf('.') !== -1) {
      var parent_name
      var parent_tag
      var parent_tags = tags
      var parts = tag_node.name.split('.')

      while (parts.length > 1) {
        parent_name = parts.shift()
        parent_tag = find(parent_tags, {
          tag: tag_node.tag,
          name: parent_name
        })

        if (!parent_tag) {
          parent_tag = {
            tag: tag_node.tag,
            line: Number(tag_node.line),
            name: parent_name,
            type: '',
            description: ''
          }
          parent_tags.push(parent_tag)
        }

        parent_tag.tags = parent_tag.tags || []
        parent_tags = parent_tag.tags
      }

      tag_node.name = parts[0]
      parent_tags.push(tag_node)
      return tags
    }

    return tags.concat(tag_node)
  }, [])

  return {
    tags: tags,
    line: start,
    description: description.source,
    source: source_str
  }
}
Parses comment block (array of String lines)