whole_func_string: 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
    }
  }
func_code_string: 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
    }
  }
func_documentation_string: Parses comment block (array of String lines)