Commit b38c5063 authored by Rene Saarsoo's avatar Rene Saarsoo
Browse files

Refactor cmd line options to separate class.

Extracting a big chunk from the god class JsDuck::App.
The options object can now be easily passed around, unlike
with the previous system where every option needed to be
passed separately.

The bin/jsduck is now really short and readable.
parent 50fbba1c
Loading
Loading
Loading
Loading
+4 −200
Original line number Diff line number Diff line
@@ -18,204 +18,8 @@
$:.unshift File.dirname(File.dirname(__FILE__)) + "/lib"

require 'jsduck/app'
require 'optparse'
require 'jsduck/options'

app = JsDuck::App.new
app.template_dir = File.dirname(File.dirname(__FILE__)) + "/template"

opts = OptionParser.new do | opts |
  opts.banner = "Usage: jsduck [options] files/dirs...\n\n"

  opts.on('-o', '--output=PATH',
    "Directory to output all this amazing documentation.",
    "This option MUST be specified.", " ") do |path|
    app.output_dir = path
  end

  opts.on('--ignore-global', "Turns off the creation of global class.", " ") do
    app.ignore_global = true
  end

  opts.on('--private-classes', "Include private classes to docs.", " ") do
    app.show_private_classes = true
  end

  opts.on('--external=CLASSNAME',
    "Declares an external class.  When declared as",
    "external, inheriting from this class will not",
    "trigger warnings.  Useful when you are extending",
    "a class for which you can not supply source code.", " ") do |classname|
    app.external_classes << classname
  end

  opts.on('--no-warnings', "Turns off warnings.", " ") do
    app.warnings = false
  end

  opts.on('-v', '--verbose', "This will fill up your console.", " ") do
    app.verbose = true
  end

  opts.separator "Customizing output:"
  opts.separator ""

  opts.on('--title=TEXT',
    "Custom title for the documentation app.",
    "Defaults to 'ExtJS API Documentation'", " ") do |text|
    app.title = text
  end

  opts.on('--footer=TEXT',
    "Custom footer text for the documentation app.",
    "Defaults to: 'Generated with JSDuck.'", " ") do |text|
    app.footer = text
  end

  opts.on('--head-html=HTML', "HTML to append to the <head> section of index.html.", " ") do |html|
    app.head_html = html
  end

  opts.on('--body-html=HTML', "HTML to append to the <body> section index.html.", " ") do |html|
    app.body_html = html
  end

  opts.on('--guides=PATH', "Path to guides directory.",
    "Each subdirectory of that is treated as a guide",
    "and is expectd to contain a REAME.md file,",
    "which will be converted into a README.js.", " ") do |path|
    app.guides_dir = path
  end

  opts.on('--guides-order=a,b,c', Array,
    "The order in which the guides should appear. When",
    "a guide name is not specified here, it will be excluded.",
    "You don't have to write the whole name of the guide,",
    "just the beginning of it, as long as it's unique.", " ") do |list|
    app.guides_order = list
  end

  opts.on('--categories=PATH',
    "Path to JSON file which defines categories for classes.", " ") do |path|
    app.categories_path = path
  end

  opts.on('--examples=PATH', "Path to examples directory.", " ") do |path|
    app.examples_dir = path
  end

  opts.on('--link=TPL',
    "HTML template for replacing {@link}.",
    "Possible placeholders:",
    "%c - full class name (e.g. 'Ext.Panel')",
    "%m - class member name prefixed with member type",
    "     (e.g. 'method-urlEncode')",
    "%# - inserts '#' if member name present",
    "%- - inserts '-' if member name present",
    "%a - anchor text for link",
    "Default is: '<a href=\"#/api/%c%-%m\" rel=\"%c%-%m\" class=\"docClass\">%a</a>'", " ") do |tpl|
    app.link_tpl = tpl
  end

  opts.on('--img=TPL',
    "HTML template for replacing {@img}.",
    "Possible placeholders:",
    "%u - URL from @img tag (e.g. 'some/path.png')",
    "%a - alt text for image",
    "Default is: '<p><img src=\"doc-resources/%u\" alt=\"%a\"></p>'", " ") do |tpl|
    app.img_tpl = tpl
  end

  opts.on('--json', "Produces JSON export instead of HTML documentation.", " ") do
    app.export = :json
  end

  opts.on('--stdout', "Writes JSON export to STDOUT instead of writing to the filesystem", " ") do
    app.export = :stdout
  end

  opts.separator "Debugging:"
  opts.separator ""

  # For debugging it's often useful to set --processes=0 to get deterministic results.
  opts.on('-p', '--processes=COUNT',
    "The number of parallel processes to use.",
    "Defaults to the number of processors/cores.",
    "Set to 0 to disable parallel processing completely.", " ") do |count|
    app.processes = count.to_i
  end

  opts.on('--template=PATH',
    "Directory containing doc-browser UI template.", " ") do |path|
    app.template_dir = path
  end

  opts.on('--template-links',
    "Instead of copying template files, create symbolic",
    "links.  Useful for template files development.",
    "Only works on platforms supporting symbolic links.", " ") do
    app.template_links = true
  end

  opts.on('--extjs-path=PATH',
    "Path for main ExtJS JavaScript file.  Useful for specifying",
    "something different than extjs/ext.js", " ") do |path|
    app.extjs_path = path
  end

  opts.on('--local-storage-db=NAME',
    "Prefix for LocalStorage database names.",
    "Defaults to 'docs'.", " ") do |name|
    app.local_storage_db = name
  end

  opts.on('-h', '--help', "Prints this help message", " ") do
    puts opts
    exit
  end

end

js_files = []
# scan directories for .js files
opts.parse!(ARGV).each do |fname|
  if File.exists?(fname)
    if File.directory?(fname)
      Dir[fname+"/**/*.{js,css,scss}"].each {|f| js_files << f }
    else
      js_files << fname
    end
  else
    $stderr.puts "Warning: File #{fname} not found"
  end
end
app.input_files = js_files

if app.input_files.length == 0
  puts "You should specify some input files, otherwise there's nothing I can do :("
  exit(1)
elsif app.export != :stdout
  if !app.output_dir
    puts "You should also specify an output directory, where I could write all this amazing documentation."
    exit(1)
  elsif File.exists?(app.output_dir) && !File.directory?(app.output_dir)
    puts "Oh noes!  The output directory is not really a directory at all :("
    exit(1)
  elsif !File.exists?(File.dirname(app.output_dir))
    puts "Oh noes!  The parent directory for #{app.output_dir} doesn't exist."
    exit(1)
  elsif !File.exists?(app.template_dir + "/extjs")
    puts "Oh noes!  The template directory does not contain extjs/ directory :("
    puts "Please copy ExtJS over to template/extjs or create symlink."
    puts "For example:"
    puts "    $ cp -r /path/to/ext-4.0.0 " + app.template_dir + "/extjs"
    exit(1)
  elsif !File.exists?(app.template_dir + "/resources/css")
    puts "Oh noes!  CSS files for custom ExtJS theme missing :("
    puts "Please compile SASS files in template/resources/sass with compass."
    puts "For example:"
    puts "    $ compass compile " + app.template_dir + "/resources/sass"
    exit(1)
  end
end

app.run()
opts = JsDuck::Options.new
opts.parse!(ARGV)
JsDuck::App.new(opts).run
+43 −101
Original line number Diff line number Diff line
@@ -24,117 +24,59 @@ module JsDuck

  # The main application logic of jsduck
  class App
    # These are basically input parameters for app
    attr_accessor :output_dir
    attr_accessor :template_dir
    attr_accessor :guides_dir
    attr_accessor :examples_dir
    attr_accessor :guides_order
    attr_accessor :categories_path
    attr_accessor :template_links
    attr_accessor :input_files
    attr_accessor :export
    attr_accessor :link_tpl
    attr_accessor :img_tpl
    attr_accessor :ignore_global
    attr_accessor :external_classes
    attr_accessor :show_private_classes
    attr_accessor :title
    attr_accessor :footer
    attr_accessor :extjs_path
    attr_accessor :local_storage_db
    attr_accessor :head_html
    attr_accessor :body_html

    def initialize
      @output_dir = nil
      @template_dir = nil
      @guides_dir = nil
      @examples_dir = nil
      @guides_order = nil
      @categories_path = nil
      @template_links = false
      @input_files = []
      @warnings = true
      @export = nil
      @link_tpl = nil
      @img_tpl = nil
      @ignore_global = false
      @external_classes = []
      @show_private_classes = false
      @title = "Ext JS API Documentation"
      @footer = 'Generated with <a href="https://github.com/senchalabs/jsduck">JSDuck</a>.'
      @extjs_path = "extjs/ext.js"
      @local_storage_db = "docs"
      @head_html = ""
      @body_html = ""
    # Initializes app with JsDuck::Options object
    def initialize(opts)
      @opts = opts
      @timer = Timer.new
      @parallel = ParallelWrap.new
    end

      # Sets the nr of parallel processes to use.
      # Set to 0 to disable parallelization completely.
    def processes=(count)
      @parallel = ParallelWrap.new(:in_processes => count)
    end

    # Sets warnings on or off
    def warnings=(enabled)
      Logger.instance.warnings = enabled
    end

    # Sets verbose mode on or off
    def verbose=(enabled)
      Logger.instance.verbose = enabled
      @parallel = ParallelWrap.new(:in_processes => @opts.processes)
      # Sets warnings and verbose mode on or off
      Logger.instance.warnings = @opts.warnings
      Logger.instance.verbose = @opts.verbose
    end

    # Call this after input parameters set
    def run
      # Set default templates
      @link_tpl ||= '<a href="#/api/%c%-%m" rel="%c%-%m" class="docClass">%a</a>'
      # Note that we wrap image template inside <p> because {@img} often
      # appears inline within text, but that just looks ugly in HTML
      @img_tpl ||= '<p><img src="doc-resources/%u" alt="%a"></p>'

      parsed_files = @timer.time(:parsing) { parallel_parse(@input_files) }
      parsed_files = @timer.time(:parsing) { parallel_parse(@opts.input_files) }
      result = @timer.time(:aggregating) { aggregate(parsed_files) }
      relations = @timer.time(:aggregating) { filter_classes(result) }
      Aliases.new(relations).resolve_all
      Lint.new(relations).run

      @guides = Guides.new(get_doc_formatter(relations), @guides_order)
      if @guides_dir
        @timer.time(:parsing) { @guides.parse_dir(@guides_dir) }
      @guides = Guides.new(get_doc_formatter(relations), @opts.guides_order)
      if @opts.guides_dir
        @timer.time(:parsing) { @guides.parse_dir(@opts.guides_dir) }
      end

      @categories = Categories.new(get_doc_formatter(relations), relations)
      if @categories_path
      if @opts.categories_path
        @timer.time(:parsing) do
          @categories.parse(@categories_path)
          @categories.parse(@opts.categories_path)
          @categories.validate
        end
      end

      clear_dir(@output_dir) unless @export == :stdout
      if @export == :stdout
      clear_dir(@opts.output_dir) unless @opts.export == :stdout
      if @opts.export == :stdout
        @timer.time(:generating) { puts JSON.generate(relations.classes) }
      elsif @export == :json
        FileUtils.mkdir(@output_dir)
        init_output_dirs(@output_dir)
        @timer.time(:generating) { write_src(@output_dir+"/source", parsed_files) }
        @timer.time(:generating) { write_classes(@output_dir+"/output", relations) }
      elsif @opts.export == :json
        FileUtils.mkdir(@opts.output_dir)
        init_output_dirs(@opts.output_dir)
        @timer.time(:generating) { write_src(@opts.output_dir+"/source", parsed_files) }
        @timer.time(:generating) { write_classes(@opts.output_dir+"/output", relations) }
      else
        if @template_links
          link_template(@template_dir, @output_dir)
        if @opts.template_links
          link_template(@opts.template_dir, @opts.output_dir)
        else
          copy_template(@template_dir, @output_dir)
          copy_template(@opts.template_dir, @opts.output_dir)
        end
        create_index_html(@template_dir, @output_dir)
        @timer.time(:generating) { write_src(@output_dir+"/source", parsed_files) }
        @timer.time(:generating) { write_tree(@output_dir+"/output/tree.js", relations) }
        @timer.time(:generating) { write_search_data(@output_dir+"/output/searchData.js", relations) }
        @timer.time(:generating) { write_classes(@output_dir+"/output", relations) }
        @timer.time(:generating) { @guides.write(@output_dir+"/guides") }
        create_index_html(@opts.template_dir, @opts.output_dir)
        @timer.time(:generating) { write_src(@opts.output_dir+"/source", parsed_files) }
        @timer.time(:generating) { write_tree(@opts.output_dir+"/output/tree.js", relations) }
        @timer.time(:generating) { write_search_data(@opts.output_dir+"/output/searchData.js", relations) }
        @timer.time(:generating) { write_classes(@opts.output_dir+"/output", relations) }
        @timer.time(:generating) { @guides.write(@opts.output_dir+"/guides") }
      end

      @timer.report
@@ -156,7 +98,7 @@ module JsDuck
        agr.aggregate(file)
      end
      agr.classify_orphans
      agr.create_global_class unless @ignore_global
      agr.create_global_class unless @opts.ignore_global
      agr.append_ext4_event_options
      agr.result
    end
@@ -167,7 +109,7 @@ module JsDuck
      classes = []
      docs.each do |d|
        if d[:tagname] == :class
          classes << Class.new(d) if !d[:private] || @show_private_classes
          classes << Class.new(d) if !d[:private] || @opts.show_private_classes
        else
          type = d[:tagname].to_s
          name = d[:name]
@@ -176,7 +118,7 @@ module JsDuck
          Logger.instance.warn("Ignoring #{type}: #{name} in #{file} line #{line}")
        end
      end
      Relations.new(classes, @external_classes)
      Relations.new(classes, @opts.external_classes)
    end

    # Given all classes, generates namespace tree and writes it
@@ -209,7 +151,7 @@ module JsDuck

    # Writes formatted HTML source code for each input file
    def write_src(path, parsed_files)
      src = SourceWriter.new(path, @export ? nil : :page)
      src = SourceWriter.new(path, @opts.export ? nil : :page)
      # Can't be done in parallel, because file.html_filename= method
      # updates all the doc-objects related to the file
      parsed_files.each do |file|
@@ -222,10 +164,10 @@ module JsDuck
    # Creates and initializes DocFormatter
    def get_doc_formatter(relations)
      formatter = DocFormatter.new
      formatter.link_tpl = @link_tpl if @link_tpl
      formatter.img_tpl = @img_tpl if @img_tpl
      formatter.link_tpl = @opts.link_tpl if @opts.link_tpl
      formatter.img_tpl = @opts.img_tpl if @opts.img_tpl
      formatter.relations = relations
      formatter.get_example = lambda {|path| IO.read(@examples_dir + "/" + path) } if @examples_dir
      formatter.get_example = lambda {|path| IO.read(@opts.examples_dir + "/" + path) } if @opts.examples_dir
      formatter
    end

@@ -258,14 +200,14 @@ module JsDuck
    def create_index_html(template_dir, dir)
      Logger.instance.log("Creating #{dir}/index.html...")
      html = IO.read(template_dir+"/index.html")
      html.gsub!("{title}", @title)
      html.gsub!("{title}", @opts.title)
      html.gsub!("{footer}", "<div id='footer-content' style='display: none'>#{@footer}</div>")
      html.gsub!("{extjs_path}", @extjs_path)
      html.gsub!("{local_storage_db}", @local_storage_db)
      html.gsub!("{extjs_path}", @opts.extjs_path)
      html.gsub!("{local_storage_db}", @opts.local_storage_db)
      html.gsub!("{guides}", @guides.to_html)
      html.gsub!("{categories}", @categories.to_html)
      html.gsub!("{head_html}", @head_html)
      html.gsub!("{body_html}", @body_html)
      html.gsub!("{head_html}", @opts.head_html)
      html.gsub!("{body_html}", @opts.body_html)
      FileUtils.rm(dir+"/index.html")
      File.open(dir+"/index.html", 'w') {|f| f.write(html) }
    end

lib/jsduck/options.rb

0 → 100644
+273 −0
Original line number Diff line number Diff line
require 'optparse'

module JsDuck

  # Keeps command line options
  class Options
    attr_accessor :input_files

    attr_accessor :output_dir
    attr_accessor :ignore_global
    attr_accessor :show_private_classes
    attr_accessor :external_classes
    attr_accessor :warnings
    attr_accessor :verbose

    # Customizing output
    attr_accessor :title
    attr_accessor :footer
    attr_accessor :head_html
    attr_accessor :body_html
    attr_accessor :guides_dir
    attr_accessor :guides_order
    attr_accessor :categories_path
    attr_accessor :examples_dir
    attr_accessor :link_tpl
    attr_accessor :img_tpl
    attr_accessor :export

    # Debugging
    attr_accessor :processes
    attr_accessor :template_dir
    attr_accessor :template_links
    attr_accessor :extjs_path
    attr_accessor :local_storage_db

    def initialize
      @input_files = []

      @output_dir = nil
      @ignore_global = false
      @show_private_classes = false
      @external_classes = []
      @warnings = true
      @verbose = false

      # Customizing output
      @title = "Ext JS API Documentation"
      @footer = 'Generated with <a href="https://github.com/senchalabs/jsduck">JSDuck</a>.'
      @head_html = ""
      @body_html = ""
      @guides_dir = nil
      @guides_order = nil
      @categories_path = nil
      @examples_dir = nil
      @link_tpl = '<a href="#/api/%c%-%m" rel="%c%-%m" class="docClass">%a</a>'
      # Note that we wrap image template inside <p> because {@img} often
      # appears inline within text, but that just looks ugly in HTML
      @img_tpl = '<p><img src="doc-resources/%u" alt="%a"></p>'
      @export = nil

      # Debugging
      @processes = nil
      @template_dir = File.dirname(File.dirname(File.dirname(__FILE__))) + "/template"
      @template_links = false
      @extjs_path = "extjs/ext.js"
      @local_storage_db = "docs"
    end

    def parse!(argv)
      create_option_parser.parse!(argv).each {|fname| read_filenames(fname) }
      validate
    end

    def create_option_parser
      return OptionParser.new do | opts |
        opts.banner = "Usage: jsduck [options] files/dirs...\n\n"

        opts.on('-o', '--output=PATH',
          "Directory to output all this amazing documentation.",
          "This option MUST be specified.", " ") do |path|
          @output_dir = path
        end

        opts.on('--ignore-global', "Turns off the creation of global class.", " ") do
          @ignore_global = true
        end

        opts.on('--private-classes', "Include private classes to docs.", " ") do
          @show_private_classes = true
        end

        opts.on('--external=CLASSNAME',
          "Declares an external class.  When declared as",
          "external, inheriting from this class will not",
          "trigger warnings.  Useful when you are extending",
          "a class for which you can not supply source code.", " ") do |classname|
          @external_classes << classname
        end

        opts.on('--no-warnings', "Turns off warnings.", " ") do
          @warnings = false
        end

        opts.on('-v', '--verbose', "This will fill up your console.", " ") do
          @verbose = true
        end

        opts.separator "Customizing output:"
        opts.separator ""

        opts.on('--title=TEXT',
          "Custom title for the documentation @",
          "Defaults to 'ExtJS API Documentation'", " ") do |text|
          @title = text
        end

        opts.on('--footer=TEXT',
          "Custom footer text for the documentation @",
          "Defaults to: 'Generated with JSDuck.'", " ") do |text|
          @footer = text
        end

        opts.on('--head-html=HTML', "HTML to append to the <head> section of index.html.", " ") do |html|
          @head_html = html
        end

        opts.on('--body-html=HTML', "HTML to append to the <body> section index.html.", " ") do |html|
          @body_html = html
        end

        opts.on('--guides=PATH', "Path to guides directory.",
          "Each subdirectory of that is treated as a guide",
          "and is expectd to contain a REAME.md file,",
          "which will be converted into a README.js.", " ") do |path|
          @guides_dir = path
        end

        opts.on('--guides-order=a,b,c', Array,
          "The order in which the guides should appear. When",
          "a guide name is not specified here, it will be excluded.",
          "You don't have to write the whole name of the guide,",
          "just the beginning of it, as long as it's unique.", " ") do |list|
          @guides_order = list
        end

        opts.on('--categories=PATH',
          "Path to JSON file which defines categories for classes.", " ") do |path|
          @categories_path = path
        end

        opts.on('--examples=PATH', "Path to examples directory.", " ") do |path|
          @examples_dir = path
        end

        opts.on('--link=TPL',
          "HTML template for replacing {@link}.",
          "Possible placeholders:",
          "%c - full class name (e.g. 'Ext.Panel')",
          "%m - class member name prefixed with member type",
          "     (e.g. 'method-urlEncode')",
          "%# - inserts '#' if member name present",
          "%- - inserts '-' if member name present",
          "%a - anchor text for link",
          "Default is: '<a href=\"#/api/%c%-%m\" rel=\"%c%-%m\" class=\"docClass\">%a</a>'", " ") do |tpl|
          @link_tpl = tpl
        end

        opts.on('--img=TPL',
          "HTML template for replacing {@img}.",
          "Possible placeholders:",
          "%u - URL from @img tag (e.g. 'some/path.png')",
          "%a - alt text for image",
          "Default is: '<p><img src=\"doc-resources/%u\" alt=\"%a\"></p>'", " ") do |tpl|
          @img_tpl = tpl
        end

        opts.on('--json', "Produces JSON export instead of HTML documentation.", " ") do
          @export = :json
        end

        opts.on('--stdout', "Writes JSON export to STDOUT instead of writing to the filesystem", " ") do
          @export = :stdout
        end

        opts.separator "Debugging:"
        opts.separator ""

        # For debugging it's often useful to set --processes=0 to get deterministic results.
        opts.on('-p', '--processes=COUNT',
          "The number of parallel processes to use.",
          "Defaults to the number of processors/cores.",
          "Set to 0 to disable parallel processing completely.", " ") do |count|
          @processes = count.to_i
        end

        opts.on('--template=PATH',
          "Directory containing doc-browser UI template.", " ") do |path|
          @template_dir = path
        end

        opts.on('--template-links',
          "Instead of copying template files, create symbolic",
          "links.  Useful for template files development.",
          "Only works on platforms supporting symbolic links.", " ") do
          @template_links = true
        end

        opts.on('--extjs-path=PATH',
          "Path for main ExtJS JavaScript file.  Useful for specifying",
          "something different than extjs/ext.js", " ") do |path|
          @extjs_path = path
        end

        opts.on('--local-storage-db=NAME',
          "Prefix for LocalStorage database names.",
          "Defaults to 'docs'.", " ") do |name|
          @local_storage_db = name
        end

        opts.on('-h', '--help', "Prints this help message", " ") do
          puts opts
          exit
        end
      end
    end

    # scans directory for .js files or simply adds file to input files list
    def read_filenames(fname)
      if File.exists?(fname)
        if File.directory?(fname)
          Dir[fname+"/**/*.{js,css,scss}"].each {|f| @input_files << f }
        else
          @input_files << fname
        end
      else
        $stderr.puts "Warning: File #{fname} not found"
      end
    end

    # Runs checks on the options
    def validate
      if @input_files.length == 0
        puts "You should specify some input files, otherwise there's nothing I can do :("
        exit(1)
      elsif @export != :stdout
        if !@output_dir
          puts "You should also specify an output directory, where I could write all this amazing documentation."
          exit(1)
        elsif File.exists?(@output_dir) && !File.directory?(@output_dir)
          puts "Oh noes!  The output directory is not really a directory at all :("
          exit(1)
        elsif !File.exists?(File.dirname(@output_dir))
          puts "Oh noes!  The parent directory for #{@output_dir} doesn't exist."
          exit(1)
        elsif !File.exists?(@template_dir + "/extjs")
          puts "Oh noes!  The template directory does not contain extjs/ directory :("
          puts "Please copy ExtJS over to template/extjs or create symlink."
          puts "For example:"
          puts "    $ cp -r /path/to/ext-4.0.0 " + @template_dir + "/extjs"
          exit(1)
        elsif !File.exists?(@template_dir + "/resources/css")
          puts "Oh noes!  CSS files for custom ExtJS theme missing :("
          puts "Please compile SASS files in template/resources/sass with compass."
          puts "For example:"
          puts "    $ compass compile " + @template_dir + "/resources/sass"
          exit(1)
        end
      end
    end

  end

end