cli.coffee

##
# Provides CroJSDoc command line interface
# @module cli

fs = require 'fs'
glob = require 'glob'
walkdir = require 'walkdir'
{basename,dirname,join,resolve} = require 'path'

isWindows = process.platform is 'win32'

##
# Reads a config file(crojsdoc.yaml) to build options
# @param {Options} options
# @memberOf cli
_readConfig = (options) ->
  {safeLoad} = require 'js-yaml'
  try
    config = safeLoad fs.readFileSync(join(process.cwd(), 'crojsdoc.yaml'), 'utf-8')
    if config.hasOwnProperty 'output'
      options.output = config.output
    if config.hasOwnProperty 'title'
      options.title = config.title
    if config.hasOwnProperty 'quiet' or config.hasOwnProperty 'quite'
      options.quiet = config.quiet is true
    if  config.hasOwnProperty 'files'
      options.files = config.files is true
    if config.hasOwnProperty('readme') and typeof config.readme is 'string'
      options._readme = config.readme
    if config.hasOwnProperty 'external-types'
      options['external-types'] = config['external-types']
    if config.hasOwnProperty 'sources'
      if Array.isArray config.sources
        [].push.apply options._sources, config.sources
      else
        options._sources.push config.sources
    if config.hasOwnProperty 'github'
      options.github = config.github
      if options.github.branch is undefined
        options.github.branch = 'master'
    if config.hasOwnProperty 'reverse_see_also'
      options.reverse_see_also = config.reverse_see_also is true
    if config.hasOwnProperty 'plugins'
      plugins = config.plugins
      if not Array.isArray plugins
        plugins = [plugins]
      options.plugins = plugins.map (plugin) ->
        try
          require plugin
        catch e
          console.log "Plugin '#{plugin}' not found"
      .filter (plugin) -> plugin
    return

##
# Parses the command line arguments to build options
# @param {Options} options
# @memberOf cli
_parseArguments = (options) ->
  {OptionParser} = require 'optparse'
  switches = [
    [ '-h', '--help', 'show help' ]
    [ '-o', '--output DIRECTORY', 'Output directory' ]
    [ '-t', '--title TITLE', 'Document Title' ]
    [ '-q', '--quiet', 'less output' ]
    [ '-r', '--readme DIRECTORY', 'README.md directory path']
    [ '-f', '--files', 'included source files' ]
    [ '--external-types JSONFILE', 'external type definitions' ]
  ]
  parser = new OptionParser switches
  parser.banner = 'Usage: crojsdoc [-o DIRECTORY] [-t TITLE] [-q] [options..] SOURCES...'
  parser.on 'help', ->
    console.log parser.toString()
    process.exit(1)
  parser.on '*', (opt, value) ->
    if value is undefined
      value = true
    options[opt] = value
  [].push.apply options._sources, parser.parse process.argv.slice 2

##
# Reads additional type definitions from a file or a object
# @param {String|Object} external_types
# @param {Object} types
# @memberOf cli
_readExternalTypes = (external_types, types) ->
  return if not external_types

  if typeof external_types is 'string'
    try
      content = fs.readFileSync(external_types, 'utf-8').trim()
      try
        external_types = JSON.parse content
      catch e
        console.log "external-types: Invalid JSON file"
    catch e
      console.log "external-types: Cannot open #{external_types}"

  if typeof external_types is 'object'
    for type, url of external_types
      types[type] = url

##
# Builds options from a config file(crojsdoc.yaml) or command line arguments
# @return {Options}
# @memberOf cli
_buildOptions = ->
  options =
    _project_dir: process.cwd()
    types:
      # Links for pre-known types
      Object: 'https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object'
      Boolean: 'https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Boolean'
      String: 'https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/String'
      Array: 'https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array'
      Number: 'https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Number'
      Date: 'https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date'
      Function: 'https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function'
      RegExp: 'https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/RegExp'
      Error: 'https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error'
      undefined: 'https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/undefined'
    _sources: []

  _readConfig options
  _parseArguments options

  if options.plugins
    options.plugins.forEach (plugin) ->
      if plugin.externalTypes
        _readExternalTypes plugin.externalTypes, options.types
      return

  # process options.external-types after plugins' externalTypes
  # for user to override plugins' configurations
  _readExternalTypes options['external-types'], options.types

  options.output_dir = resolve options._project_dir, options.output or 'doc'

  return options

##
# Reads source files
# @param {Options} options
# @return {Array<Content>}
# @memberOf cli
_readSourceFiles = (options) ->
  if isWindows
    project_dir_re = new RegExp("^" + options._project_dir.replace(/\\/g, '\\\\'))
  else
    project_dir_re = new RegExp("^" + options._project_dir)
  contents = []
  for path in options._sources
    base_path = path = resolve options._project_dir, path
    base_path = dirname base_path while /[*?]/.test basename(base_path)
    glob.sync(path).forEach (path) =>
      if fs.statSync(path).isDirectory()
        list = walkdir.sync path
      else
        list = [path]
      for file in list
        continue if fs.statSync(file).isDirectory()
        data = fs.readFileSync(file, 'utf-8').trim()
        continue if not data
        if isWindows
          contents.push full_path: file.replace(project_dir_re, '').replace(/\\/g, '/'), path: file.substr(base_path.length+1).replace(/\\/g, '/'), data: data
        else
          contents.push full_path: file.replace(project_dir_re, ''), path: file.substr(base_path.length+1), data: data
      return
  try
    data = fs.readFileSync "#{options._readme or options._project_dir}/README.md", 'utf-8'
    contents.push full_path: '', path: 'README', data: data
  return contents

##
# Runs CroJSDoc via CLI
# @memberOf cli
exports.run = ->
  options = _buildOptions()
  contents = _readSourceFiles options
  result = require('./collect') contents, options
  require('./render') result, options
Fork me on GitHub