require 'optparse' require 'jsduck/meta_tag_registry' require 'jsduck/logger' require 'jsduck/json_duck' module JsDuck # Keeps command line options class Options attr_accessor :input_files attr_accessor :output_dir attr_accessor :ignore_global attr_accessor :external_classes attr_accessor :ext4_events # Customizing output attr_accessor :title attr_accessor :header attr_accessor :footer attr_accessor :head_html attr_accessor :body_html attr_accessor :welcome attr_accessor :guides attr_accessor :videos attr_accessor :examples attr_accessor :stats attr_accessor :categories_path attr_accessor :source attr_accessor :pretty_json attr_accessor :images attr_accessor :link_tpl attr_accessor :img_tpl attr_accessor :export attr_accessor :seo attr_accessor :eg_iframe attr_accessor :examples_base_url attr_accessor :tests # Debugging attr_accessor :processes attr_accessor :template_dir attr_accessor :template_links attr_accessor :extjs_path attr_accessor :local_storage_db attr_accessor :touch_examples_ui attr_accessor :ext_namespaces def initialize @input_files = [] @output_dir = nil @ignore_global = false @external_classes = [ # JavaScript builtins "Object", "String", "Number", "Boolean", "RegExp", "Function", "Array", "Arguments", "Date", "Error", # DOM "HTMLElement", "XMLElement", "NodeList", "TextNode", "CSSStyleSheet", "CSSStyleRule", "Event", # Special anything-goes type "Mixed", ] @ext4_events = nil @meta_tag_paths = [] @version = "3.11.2" # Customizing output @title = "Ext JS - Sencha Docs" @header = "Ext JS Sencha Docs" @footer = "Generated with JSDuck #{@version}." @head_html = "" @body_html = "" @welcome = nil @guides = nil @videos = nil @examples = nil @stats = false @categories_path = nil @source = true @pretty_json = false @images = [] @link_tpl = '%a' # Note that we wrap image template inside

because {@img} often # appears inline within text, but that just looks ugly in HTML @img_tpl = '

%a

' @export = nil @seo = false @eg_iframe = nil @examples_base_url = "extjs-build/examples/" @tests = false # Debugging # Turn multiprocessing off by default in Windows @processes = OS::windows? ? 0 : nil @root_dir = File.dirname(File.dirname(File.dirname(__FILE__))) @template_dir = @root_dir + "/template-min" @template_links = false @extjs_path = "extjs/ext-all.js" @local_storage_db = "docs" @touch_examples_ui = false @ext_namespaces = ["Ext"] # enable all warnings except :link_auto Logger.instance.set_warning(:all, true) Logger.instance.set_warning(:link_auto, false) end # Make options object behave like hash. # This allows us to substitute it with hash in unit tests. def [](key) send(key) end def parse!(argv) create_option_parser.parse!(argv).each do |fname| read_filenames(canonical(fname)) end validate reg = MetaTagRegistry.new reg.load([:builtins] + @meta_tag_paths) MetaTagRegistry.instance = reg end def create_option_parser optparser = 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 (unless --stdout).", "Use dash '-' to write docs to STDOUT (only export).", " ") do |path| @output_dir = path == "-" ? :stdout : canonical(path) end opts.on('--ignore-global', "Turns off the creation of global class.", " ") do @ignore_global = true end opts.on('--external=Foo,Bar,Baz', Array, "Declares list of external classes. These classes", "will then not generate warnings when used in type", "definitions or inherited from.", " ") do |classes| @external_classes += classes end opts.on('--[no-]ext4-events', "Appends extra options parameter that all Ext events have.", "The default is to auto-detect if we're using Ext JS 4", "based on whether the code uses Ext.define.", "Use this option to override the auto-detection.", " ") do |e| @ext4_events = e end opts.on('--builtin-classes', "Includes docs for JavaScript builtin classes.", " ") do read_filenames(@root_dir + "/js-classes") end opts.on('--meta-tags=PATH', "Path to Ruby file or directory with custom", "meta-tag implementations.", " ") do |path| @meta_tag_paths << canonical(path) end opts.on('--encoding=NAME', "Input encoding (defaults to UTF-8).", " ") do |encoding| JsDuck::IO.encoding = encoding end opts.on('-v', '--verbose', "This will fill up your console.", " ") do Logger.instance.verbose = true end opts.separator "Customizing output:" opts.separator "" opts.on('--title=TEXT', "Custom title text for the documentation.", "Defaults to 'Ext JS - Sencha Docs'", " ") do |text| @title = text @header = text.sub(/^(.*?) +- +/, "\\1 ") end opts.on('--footer=TEXT', "Custom footer text for the documentation.", "Defaults to: 'Generated with JSDuck {VERSION}.'", " ") do |text| @footer = text.gsub(/\{VERSION\}/, @version) end opts.on('--head-html=HTML', "HTML to append to the section of index.html.", " ") do |html| @head_html += html end opts.on('--body-html=HTML', "HTML to append to the section index.html.", " ") do |html| @body_html += html end opts.on('--welcome=PATH', "Path to HTML file with content for welcome page.", " ") do |path| @welcome = canonical(path) end opts.on('--guides=PATH', "Path to JSON file describing the guides. The file", "should be in a dir containing the actual guides.", "A guide is a dir containing README.md, icon.png,", "and other images referenced by the README.md file.", " ") do |path| @guides = canonical(path) end opts.on('--videos=PATH', "Path to JSON file describing the videos.", " ") do |path| @videos = canonical(path) end opts.on('--examples=PATH', "Path JSON file describing the examples.", " ") do |path| @examples = canonical(path) end opts.on('--categories=PATH', "Path to JSON file which defines categories for classes.", " ") do |path| @categories_path = canonical(path) end opts.on('--no-source', "Turns off the output of source files.", " ") do @source = false end opts.on('--pretty-json', "Turn on pretty-printing of JSON.", " ") do @pretty_json = true end opts.on('--images=PATH', "Search path for including images referenced by", "{@img} tag. Several paths can be specified by", "using the option multiple times.", " ") do |path| @images << canonical(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'", " ") 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: '

\"%a\"

'", " ") do |tpl| @img_tpl = tpl end opts.on('--export=TYPE', "Exports docs in JSON. TYPE is one of:", "* full - full class docs.", "* api - only class- and member names.", "* examples - extracts inline examples from classes.", " ") do |format| @export = format.to_sym end opts.on('--seo', "Creates index.php that handles search engine traffic.", " ") do @seo = true end opts.on('--eg-iframe=PATH', "An HTML file to use inside an iframe", "to display inline examples.", " ") do |path| @eg_iframe = canonical(path) end opts.on('--examples-base-url=URL', "Base URL for examples with relative URL-s.", " ") do |path| @examples_base_url = path end opts.on('--tests', "Creates page for testing inline examples.", " ") do @tests = true end opts.on('--stats', "Creates page with all kinds of statistics. Experimental!", " ") do @stats = true end opts.separator "Debugging:" opts.separator "" opts.on('--warnings=+A,-B,+C', Array, "Turns warnings selectively on/off.", " ", " +all - to turn on all warnings", " ", "By default these warnings are turned +on/-off:", " ", *Logger.instance.doc_warnings) do |warnings| warnings.each do |op| if op =~ /^([-+]?)(.*)$/ enable = !($1 == "-") name = $2.to_sym Logger.instance.set_warning(name, enable) end end end # 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.", OS::windows? ? "Defaults to off in Windows." : "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 = canonical(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 # NB! must be relative 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('--touch-examples-ui', "Use phone/tablet UI for examples.", " ") do @touch_examples_ui = true end opts.on('--ext-namespaces=Ext,Foo', Array, "Namespace(s) of ExtJS. Defaults to 'Ext'.", " ") do |ns| @ext_namespaces = ns end opts.on('--config=PATH', "Loads config options from JSON file.", " ") do |path| path = canonical(path) if File.exists?(path) config = read_json_config(path) else puts "Oh noes! The config file #{path} doesn't exist." exit(1) end # treat paths inside JSON config relative to the location of # config file. When done, switch back to current working dir. @working_dir = File.dirname(path) optparser.parse!(config).each {|fname| read_filenames(canonical(fname)) } @working_dir = nil end opts.on('-h', '--help[=full]', "Short help or --help=full for all available options.", " ") do |v| if v == 'full' puts opts else puts opts.banner puts "For example:" puts puts " # Documentation for builtin JavaScript classes like Array and String" puts " jsduck --output output/dir --builtin-classes" puts puts " # Documentation for your own JavaScript" puts " jsduck --output output/dir input-file.js some/input/dir" puts puts "The main options:" puts show_help = false main_opts = [ /--output/, /--builtin-classes/, /--encoding/, /--verbose/, /--help/, /--version/, ] opts.summarize([], opts.summary_width) do |helpline| if main_opts.any? {|re| helpline =~ re } puts helpline show_help = true elsif helpline =~ /^\s*$/ && show_help == true puts helpline show_help = false elsif show_help == true puts helpline end end end exit end opts.on('--version', "Prints JSDuck version", " ") do puts "JSDuck " + @version exit end end return optparser end # Reads JSON configuration from file and returns an array of # config options that can be feeded into optparser. def read_json_config(fname) config = [] json = JsonDuck.read(fname) json.each_pair do |key, value| if key == "--" # filenames config += Array(value).map(&:to_s) elsif value == true # simple switch config += [key.to_s] else # An option with value or with multiple values. # In the latter case, add the option multiple times. Array(value).each do |v| config += [key.to_s, v.to_s] end end end config 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 } elsif fname =~ /\.jsb3$/ extract_jsb_files(fname).each {|fn| read_filenames(fn) } else @input_files << fname end else Logger.instance.warn(nil, "File #{fname} not found") end end # Extracts files of first build in jsb file def extract_jsb_files(jsb_file) json = JsonDuck::read(jsb_file) basedir = File.dirname(jsb_file) return json["builds"][0]["packages"].map do |package_id| package = json["packages"].find {|p| p["id"] == package_id } (package ? package["files"] : []).map do |file| File.expand_path(basedir + "/" + file["path"] + file["name"]) end end.flatten end # Converts relative path to full path # # Especially important for running on Windows where C:\foo\bar # pathnames are converted to C:/foo/bar which ruby can work on # more easily. def canonical(path) File.expand_path(path, @working_dir) end # Runs checks on the options def validate if @input_files.length == 0 && !@welcome && !@guides && !@videos && !@examples puts "You should specify some input files, otherwise there's nothing I can do :(" exit(1) elsif @output_dir == :stdout && !@export puts "Output to STDOUT only works when using --export option." exit(1) elsif ![nil, :full, :api, :examples].include?(@export) puts "Unknown export format: #{@export}" exit(1) elsif @output_dir != :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 !@export && !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 !@export && !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