lib/jazzy/config.rb in jazzy-0.3.2 vs lib/jazzy/config.rb in jazzy-0.4.0

- old
+ new

@@ -7,237 +7,397 @@ require 'jazzy/source_declaration/access_control_level' module Jazzy # rubocop:disable Metrics/ClassLength class Config - attr_accessor :output - attr_accessor :xcodebuild_arguments - attr_accessor :author_name - attr_accessor :module_name - attr_accessor :github_url - attr_accessor :github_file_prefix - attr_accessor :author_url - attr_accessor :dash_url - attr_accessor :sourcekitten_sourcefile - attr_accessor :clean - attr_accessor :readme_path - attr_accessor :docset_platform - attr_accessor :root_url - attr_accessor :version - attr_accessor :min_acl - attr_accessor :skip_undocumented - attr_accessor :hide_documentation_coverage - attr_accessor :podspec - attr_accessor :docset_icon - attr_accessor :docset_path - attr_accessor :source_directory - attr_accessor :excluded_files - attr_accessor :custom_categories - attr_accessor :template_directory - attr_accessor :swift_version - attr_accessor :assets_directory - attr_accessor :copyright + # rubocop:disable Style/AccessorMethodName + class Attribute + attr_reader :name, :description, :command_line, :default, :parse - def initialize - PodspecDocumenter.configure(self, Dir['*.podspec{,.json}'].first) - self.output = Pathname('docs') - self.xcodebuild_arguments = [] - self.author_name = '' - self.module_name = '' - self.author_url = URI('') - self.clean = false - self.docset_platform = 'jazzy' - self.version = '1.0' - self.min_acl = SourceDeclaration::AccessControlLevel.public - self.skip_undocumented = false - self.hide_documentation_coverage = false - self.source_directory = Pathname.pwd - self.excluded_files = [] - self.custom_categories = {} - self.template_directory = Pathname(__FILE__).parent + 'templates' - self.swift_version = '2.0' - self.assets_directory = Pathname(__FILE__).parent + 'assets' + def initialize(name, description: nil, command_line: nil, + default: nil, parse: ->(x) { x }) + @name = name + @description = Array(description) + @command_line = Array(command_line) + @default = default + @parse = parse + end + + def get(config) + config.method(name).call + end + + def set_raw(config, val) + config.method("#{name}=").call(val) + end + + def set(config, val, mark_configured: true) + set_raw(config, parse.call(val)) + config.method("#{name}_configured=").call(true) if mark_configured + end + + def set_to_default(config) + set(config, default, mark_configured: false) if default + end + + def set_if_unconfigured(config, val) + set(config, val) unless configured?(config) + end + + def configured?(config) + config.method("#{name}_configured").call + end + + def attach_to_option_parser(config, opt) + return if command_line.empty? + opt.on(*command_line, *description) do |val| + set(config, val) + end + end end + # rubocop:enable Style/AccessorMethodName - def podspec=(podspec) - @podspec = PodspecDocumenter.configure(self, podspec) + def self.config_attr(name, **opts) + attr_accessor name + attr_accessor "#{name}_configured" + @all_config_attrs ||= [] + @all_config_attrs << Attribute.new(name, **opts) end - def template_directory=(template_directory) - @template_directory = template_directory - Doc.template_path = template_directory + class << self + attr_reader :all_config_attrs end - # rubocop:disable Metrics/MethodLength - def self.parse! - config = new - OptionParser.new do |opt| - opt.banner = 'Usage: jazzy' - opt.separator '' - opt.separator 'Options' + # ──────── Build ──────── - opt.on('-o', '--output FOLDER', - 'Folder to output the HTML docs to') do |output| - config.output = Pathname(output) - end + # rubocop:disable Style/AlignParameters - opt.on('-c', '--[no-]clean', - 'Delete contents of output directory before running.', - 'WARNING: If --output is set to ~/Desktop, this will delete the \ - ~/Desktop directory.') do |clean| - config.clean = clean - end + config_attr :output, + description: 'Folder to output the HTML docs to', + command_line: ['-o', '--output FOLDER'], + default: 'docs', + parse: ->(o) { Pathname(o) } - opt.on('-x', '--xcodebuild-arguments arg1,arg2,…argN', Array, - 'Arguments to forward to xcodebuild') do |args| - config.xcodebuild_arguments = args - end + config_attr :clean, + command_line: ['-c', '--[no-]clean'], + description: ['Delete contents of output directory before running. ', + 'WARNING: If --output is set to ~/Desktop, this will '\ + 'delete the ~/Desktop directory.'], + default: false - opt.on('-a', '--author AUTHOR_NAME', - 'Name of author to attribute in docs (i.e. Realm)') do |a| - config.author_name = a - end + config_attr :objc_mode, + command_line: '--[no-]objc', + description: 'Generate docs for Objective-C.', + default: false - opt.on('-u', '--author_url URL', - 'Author URL of this project (i.e. http://realm.io)') do |u| - config.author_url = URI(u) - end + config_attr :umbrella_header, + command_line: '--umbrella-header PATH', + description: 'Umbrella header for your Objective-C framework.', + parse: ->(uh) { Pathname(uh) } - opt.on('-m', '--module MODULE_NAME', - 'Name of module being documented. (i.e. RealmSwift)') do |m| - config.module_name = m - end + config_attr :framework_root, + command_line: '--framework-root PATH', + description: 'The root path to your Objective-C framework.', + parse: ->(fr) { Pathname(fr) } - opt.on('-d', '--dash_url URL', - 'Location of the dash XML feed \ - (i.e. http://realm.io/docsets/realm.xml') do |d| - config.dash_url = URI(d) - end + config_attr :config_file, + command_line: '--config PATH', + description: ['Configuration file (.yaml or .json)', + 'Default: .jazzy.yaml in source directory or ancestor'], + parse: ->(cf) { Pathname(cf) } - opt.on('-g', '--github_url URL', - 'GitHub URL of this project (i.e. \ - https://github.com/realm/realm-cocoa)') do |g| - config.github_url = URI(g) - end + config_attr :xcodebuild_arguments, + command_line: ['-x', '--xcodebuild-arguments arg1,arg2,…argN', Array], + description: 'Arguments to forward to xcodebuild', + default: [] - opt.on('--github-file-prefix PREFIX', - 'GitHub URL file prefix of this project (i.e. \ - https://github.com/realm/realm-cocoa/tree/v0.87.1)') do |g| - config.github_file_prefix = g - end + config_attr :sourcekitten_sourcefile, + command_line: ['-s', '--sourcekitten-sourcefile FILEPATH'], + description: 'File generated from sourcekitten output to parse', + parse: ->(s) { Pathname(s) } - opt.on('-s', '--sourcekitten-sourcefile FILEPATH', - 'File generated from sourcekitten output to parse') do |s| - config.sourcekitten_sourcefile = Pathname(s) - end + config_attr :source_directory, + command_line: '--source-directory DIRPATH', + description: 'The directory that contains the source to be documented', + default: Pathname.pwd, + parse: ->(sd) { Pathname(sd) } - opt.on('-r', '--root-url URL', - 'Absolute URL root where these docs will be stored') do |r| - config.root_url = URI(r) - if !config.dash_url && config.root_url - config.dash_url = URI.join(r, "docsets/#{config.module_name}.xml") - end - end + config_attr :excluded_files, + command_line: ['-e', '--exclude file1,file2,…fileN', Array], + description: 'Files to be excluded from documentation', + default: [], + parse: ->(files) do + files.map { |f| File.expand_path(f) } + end - opt.on('--module-version VERSION', - 'module version. will be used when generating docset') do |mv| - config.version = mv - end + config_attr :swift_version, + command_line: '--swift-version VERSION', + default: '2.1' - opt.on('--min-acl [private | internal | public]', - 'minimum access control level to document \ - (default is public)') do |acl| - if acl == 'private' - config.min_acl = SourceDeclaration::AccessControlLevel.private - elsif acl == 'internal' - config.min_acl = SourceDeclaration::AccessControlLevel.internal - end - end + # ──────── Metadata ──────── - opt.on('--[no-]skip-undocumented', - "Don't document declarations that have no documentation \ - comments.", - ) do |skip_undocumented| - config.skip_undocumented = skip_undocumented - end + config_attr :author_name, + command_line: ['-a', '--author AUTHOR_NAME'], + description: 'Name of author to attribute in docs (e.g. Realm)', + default: '' - opt.on('--[no-]hide-documentation-coverage', - "Hide \"(X\% documented)\" from the generated documents", - ) do |hide_documentation_coverage| - config.hide_documentation_coverage = hide_documentation_coverage - end + config_attr :author_url, + command_line: ['-u', '--author_url URL'], + description: 'Author URL of this project (e.g. http://realm.io)', + default: '', + parse: ->(u) { URI(u) } - opt.on('--podspec FILEPATH') do |podspec| - config.podspec = Pathname(podspec) - end + config_attr :module_name, + command_line: ['-m', '--module MODULE_NAME'], + description: 'Name of module being documented. (e.g. RealmSwift)', + default: '' - opt.on('--docset-icon FILEPATH') do |docset_icon| - config.docset_icon = Pathname(docset_icon) - end + config_attr :version, + command_line: '--module-version VERSION', + description: 'module version. will be used when generating docset', + default: '1.0' - opt.on('--docset-path DIRPATH', 'The relative path for the generated ' \ - 'docset') do |docset_path| - config.docset_path = docset_path - end + config_attr :copyright, + command_line: '--copyright COPYRIGHT_MARKDOWN', + description: 'copyright markdown rendered at the bottom of the docs pages' - opt.on('--source-directory DIRPATH', 'The directory that contains ' \ - 'the source to be documented') do |source_directory| - config.source_directory = Pathname(source_directory) - end + config_attr :readme_path, + command_line: '--readme FILEPATH', + description: 'The path to a markdown README file', + parse: ->(rp) { Pathname(rp) } - opt.on('t', '--template-directory DIRPATH', 'The directory that ' \ - 'contains the mustache templates to use') do |template_directory| - config.template_directory = Pathname(template_directory) - end + config_attr :podspec, + command_line: '--podspec FILEPATH', + parse: ->(ps) { PodspecDocumenter.create_podspec(Pathname(ps)) if ps }, + default: Dir['*.podspec{,.json}'].first - opt.on('--swift-version VERSION') do |swift_version| - config.swift_version = swift_version - end + config_attr :docset_platform, default: 'jazzy' - opt.on('--assets-directory DIRPATH', 'The directory that contains ' \ - 'the assets (CSS, JS, images) to use') do |assets_directory| - config.assets_directory = Pathname(assets_directory) - end + config_attr :docset_icon, + command_line: '--docset-icon FILEPATH', + parse: ->(di) { Pathname(di) } - opt.on('--readme FILEPATH', - 'The path to a markdown README file') do |readme| - config.readme_path = Pathname(readme) - end + config_attr :docset_path, + command_line: '--docset-path DIRPATH', + description: 'The relative path for the generated docset' - opt.on('-e', '--exclude file1,file2,…fileN', Array, - 'Files to be excluded from documentation') do |files| - config.excluded_files = files.map { |f| File.expand_path(f) } - end + # ──────── URLs ──────── - opt.on('--categories file', - 'JSON or YAML file with custom groupings') do |file| - config.custom_categories = parse_config_file(file) + config_attr :root_url, + command_line: ['-r', '--root-url URL'], + description: 'Absolute URL root where these docs will be stored', + parse: ->(r) { URI(r) } + + config_attr :dash_url, + command_line: ['-d', '--dash_url URL'], + description: 'Location of the dash XML feed '\ + 'e.g. http://realm.io/docsets/realm.xml)', + parse: ->(d) { URI(d) } + + config_attr :github_url, + command_line: ['-g', '--github_url URL'], + description: 'GitHub URL of this project (e.g. '\ + 'https://github.com/realm/realm-cocoa)', + parse: ->(g) { URI(g) } + + config_attr :github_file_prefix, + command_line: '--github-file-prefix PREFIX', + description: 'GitHub URL file prefix of this project (e.g. '\ + 'https://github.com/realm/realm-cocoa/tree/v0.87.1)' + + # ──────── Doc generation options ──────── + + config_attr :min_acl, + command_line: '--min-acl [private | internal | public]', + description: 'minimum access control level to document', + default: 'public', + parse: ->(acl) do + SourceDeclaration::AccessControlLevel.from_human_string(acl) + end + + config_attr :skip_undocumented, + command_line: '--[no-]skip-undocumented', + description: "Don't document declarations that have no documentation '\ + 'comments.", + default: false + + config_attr :hide_documentation_coverage, + command_line: '--[no-]hide-documentation-coverage', + description: "Hide \"(X\% documented)\" from the generated documents", + default: false + + config_attr :custom_categories, + description: ['Custom navigation categories to replace the standard '\ + '“Classes, Protocols, etc.”', 'Types not explicitly named '\ + 'in a custom category appear in generic groups at the end.', + 'Example: http://git.io/vcTZm'], + default: [] + + config_attr :template_directory, + command_line: ['-t', '--template-directory DIRPATH'], + description: 'The directory that contains the mustache templates to use', + default: Pathname(__FILE__).parent + 'templates', + parse: ->(td) { Pathname(td) } + + config_attr :assets_directory, + command_line: '--assets-directory DIRPATH', + description: 'The directory that contains the assets (CSS, JS, images) '\ + 'used by the templates', + default: Pathname(__FILE__).parent + 'assets', + parse: ->(ad) { Pathname(ad) } + + # rubocop:enable Style/AlignParameters + + def initialize + self.class.all_config_attrs.each do |attr| + attr.set_to_default(self) + end + end + + def template_directory=(template_directory) + @template_directory = template_directory + Doc.template_path = template_directory + end + + # rubocop:disable Metrics/MethodLength + def self.parse! + config = new + config.parse_command_line + config.parse_config_file + PodspecDocumenter.apply_config_defaults(config.podspec, config) + + if config.root_url + config.dash_url ||= URI.join( + config.root_url, + "docsets/#{config.module_name}.xml") + end + + config + end + + def parse_command_line + OptionParser.new do |opt| + opt.banner = 'Usage: jazzy' + opt.separator '' + opt.separator 'Options' + + self.class.all_config_attrs.each do |attr| + attr.attach_to_option_parser(self, opt) end opt.on('-v', '--version', 'Print version number') do puts 'jazzy version: ' + Jazzy::VERSION exit end - opt.on('--copyright COPYRIGHT_MARKDOWN', 'copyright markdown ' \ - 'rendered at the bottom of the docs pages') do |copyright| - config.copyright = copyright - end - - opt.on('-h', '--help', 'Print this help message') do - puts opt + opt.on('-h', '--help [TOPIC]', 'Available topics:', + ' usage Command line options (this help message)', + ' config Configuration file options', + '...or an option keyword, e.g. "dash"') do |topic| + case topic + when 'usage', nil + puts opt + when 'config' + print_config_file_help + else + print_option_help(topic) + end exit end end.parse! - config + expand_paths(Pathname.pwd) end - def self.parse_config_file(file) + def parse_config_file + config_path = locate_config_file + return unless config_path + + puts "Using config file #{config_path}" + config_file = read_config_file(config_path) + self.class.all_config_attrs.each do |attr| + key = attr.name.to_s + if config_file.key?(key) + attr.set_if_unconfigured(self, config_file[key]) + end + end + + expand_paths(config_path.parent) + end + + def locate_config_file + return config_file if config_file + + source_directory.ascend do |dir| + candidate = dir.join('.jazzy.yaml') + return candidate if candidate.exist? + end + + nil + end + + def read_config_file(file) case File.extname(file) - when '.json' then JSON.parse(File.read(file)) - when '.yaml', '.yml' then YAML.load(File.read(file)) - else raise "Config file must be .yaml or .json, but got #{file.inspect}" + when '.json' then JSON.parse(File.read(file)) + when '.yaml', '.yml' then YAML.load(File.read(file)) + else raise "Config file must be .yaml or .json, but got #{file.inspect}" + end + end + + def expand_paths(base_path) + self.class.all_config_attrs.each do |attr| + val = attr.get(self) + if val.respond_to?(:expand_path) + attr.set_raw(self, val.expand_path(base_path)) + end + end + end + + def print_config_file_help + puts <<-_EOS_ + + By default, jazzy looks for a file named ".jazzy.yaml" in the source + directory and its ancestors. You can override the config file location + with --config. + + (The source directory is the current working directory by default. + You can override that with --source-directory.) + + The config file can be in YAML or JSON format. Available options are: + + _EOS_ + .gsub(/^ +/, '') + + print_option_help + end + + def print_option_help(topic = '') + found = false + self.class.all_config_attrs.each do |attr| + match = ([attr.name] + attr.command_line).any? do |opt| + opt.to_s.include?(topic) + end + if match + found = true + puts + puts attr.name.to_s.tr('_', ' ').upcase + puts + puts " Config file: #{attr.name}" + cmd_line_forms = attr.command_line.select { |opt| opt.is_a?(String) } + if cmd_line_forms.any? + puts " Command line: #{cmd_line_forms.join(', ')}" + end + puts + print_attr_description(attr) + end + end + warn "Unknown help topic #{topic.inspect}" unless found + end + + def print_attr_description(attr) + attr.description.each { |line| puts " #{line}" } + if attr.default && attr.default != '' + puts " Default: #{attr.default}" end end #-------------------------------------------------------------------------#