# frozen_string_literal: true require 'digest/sha1' require 'fileutils' module YARD module CLI # Default options used in +yard doc+ command. class YardocOptions < Templates::TemplateOptions # @return [Array] # the list of extra files rendered along with objects default_attr :files, lambda { [] } # @return [String] the default title appended to each generated page default_attr :title, "Documentation by YARD #{YARD::VERSION}" # @return [Verifier] the default verifier object to filter queries default_attr :verifier, lambda { Verifier.new } # @return [Serializers::Base] the default serializer for generating output # to disk. default_attr :serializer, lambda { Serializers::FileSystemSerializer.new } # @return [Symbol] the default output format (:html). default_attr :format, :html # @return [Boolean] whether the data should be rendered in a single page, # if the template supports it. default_attr :onefile, false # @return [CodeObjects::ExtraFileObject] the README file object rendered # along with objects attr_accessor :readme # @return [Array] the list of code objects to render # the templates with. attr_accessor :objects # @return [Numeric] An index value for rendering sequentially related templates attr_accessor :index # @return [CodeObjects::Base] an extra item to send to a template that is not # the main rendered object attr_accessor :item # @return [CodeObjects::ExtraFileObject] the file object being rendered. # The +object+ key is not used so that a file may be rendered in the context # of an object's namespace (for generating links). attr_accessor :file # @return [String] the current locale attr_accessor :locale end # Yardoc is the default YARD CLI command (+yard doc+ and historic +yardoc+ # executable) used to generate and output (mainly) HTML documentation given # a set of source files. # # == Usage # # Main usage for this command is: # # $ yardoc [options] [source_files [- extra_files]] # # See +yardoc --help+ for details on valid options. # # == Options File (+.yardopts+) # # If a +.yardopts+ file is found in the source directory being processed, # YARD will use the contents of the file as arguments to the command, # treating newlines as spaces. You can use shell-style quotations to # group space delimited arguments, just like on the command line. # # A valid +.yardopts+ file might look like: # # --no-private # --title "My Title" # --exclude foo --exclude bar # lib/**/*.erb # lib/**/*.rb - # HACKING.rdoc LEGAL COPYRIGHT # # Note that Yardoc also supports the legacy RDoc style +.document+ file, # though this file can only specify source globs to parse, not options. # # == Queries (+--query+) # # Yardoc supports queries to select specific code objects for which to # generate documentation. For example, you might want to generate # documentation only for your public API. If you've documented your public # methods with +@api public+, you can use the following query to select # all of these objects: # # --query '@api.text == "public"' # # Note that the syntax for queries is mostly Ruby with a few syntactic # simplifications for meta-data tags. See the {Verifier} class for an # overview of this syntax. # # == Adding Custom Ad-Hoc Meta-data Tags (+--tag+) # # YARD allows specification of {file:docs/Tags.md meta-data tags} # programmatically via the {YARD::Tags::Library} class, but often this is not # practical for users writing documentation. To make adding custom tags # easier, Yardoc has a few command-line switches for creating basic tags # and displaying them in generated HTML output. # # To specify a custom tag to be displayed in output, use any of the # following: # # * +--tag+ TAG:TITLE # * +--name-tag+ TAG:TITLE # * +--type-tag+ TAG:TITLE # * +--type-name-tag+ TAG:TITLE # * +--title-tag+ TAG:TITLE # # "TAG:TITLE" is of the form: name:"Display Title", for example: # # --tag overload:"Overloaded Method" # # See +yard help doc+ for a description of the various options. # # Tags added in this way are automatically displayed in output. To add # a meta-data tag that does not show up in output, use +--hide-tag TAG+. # Note that you can also use this option on existing tags to hide # builtin tags, for instance. # # == Processed Data Storage (+.yardoc+ directory) # # When Yardoc parses a source directory, it creates a +.yardoc+ directory # (by default, override with +-b+) at the root of the project. This directory # contains marshal dumps for all raw object data in the source, so that # you can access it later for various commands (+stats+, +graph+, etc.). # This directory is also used as a cache for any future calls to +yardoc+ # so as to process only the files which have changed since the last call. # # When Yardoc uses the cache in subsequent calls to +yardoc+, methods # or classes that have been deleted from source since the last parsing # will not be erased from the cache (YARD never deletes objects). In such # a case, you should wipe the cache and do a clean parsing of the source tree. # You can do this by deleting the +.yardoc+ directory manually, or running # Yardoc without +--use-cache+ (+-c+). # # @since 0.2.1 # @see Verifier class Yardoc < YardoptsCommand # @return [Hash] the hash of options passed to the template. # @see Templates::Engine#render attr_reader :options # @return [Array] list of Ruby source files to process attr_accessor :files # @return [Array] list of excluded paths (regexp matches) # @since 0.5.3 attr_accessor :excluded # @return [Boolean] whether to use the existing yardoc db if the # .yardoc already exists. Also makes use of file checksums to # parse only changed files. attr_accessor :use_cache # @return [Boolean] whether objects should be serialized to .yardoc db attr_accessor :save_yardoc # @return [Boolean] whether to generate output attr_accessor :generate # @return [Boolean] whether to print a list of objects # @since 0.5.5 attr_accessor :list # Keep track of which visibilities are to be shown # @return [Array] a list of visibilities # @since 0.5.6 attr_accessor :visibilities # Keep track of which APIs are to be shown # @return [Array] a list of APIs # @since 0.8.1 attr_accessor :apis # Keep track of which APIs are to be hidden # @return [Array] a list of APIs to be hidden # @since 0.8.7 attr_accessor :hidden_apis # @return [Array] a list of tags to hide from templates # @since 0.6.0 attr_accessor :hidden_tags # @return [Boolean] whether to print statistics after parsing # @since 0.6.0 attr_accessor :statistics # @return [Array] a list of assets to copy after generation # @since 0.6.0 attr_accessor :assets # @return [Boolean] whether markup option was specified # @since 0.7.0 attr_accessor :has_markup # @return [Boolean] whether yard exits with error status code if a warning occurs attr_accessor :fail_on_warning # Creates a new instance of the commandline utility def initialize super @options = YardocOptions.new @options.reset_defaults @visibilities = [:public] @apis = [] @hidden_apis = [] @assets = {} @excluded = [] @files = [] @hidden_tags = [] @use_cache = false @generate = true @statistics = true @list = false @save_yardoc = true @has_markup = false @fail_on_warning = false if defined?(::Encoding) && ::Encoding.respond_to?(:default_external=) utf8 = ::Encoding.find('utf-8') ::Encoding.default_external = utf8 unless ::Encoding.default_external == utf8 ::Encoding.default_internal = utf8 unless ::Encoding.default_internal == utf8 end end def description "Generates documentation" end # Runs the commandline utility, parsing arguments and generating # output if set. # # @param [Array] args the list of arguments. If the list only # contains a single nil value, skip calling of {#parse_arguments} # @return [void] def run(*args) log.show_progress = true if args.empty? || !args.first.nil? # fail early if arguments are not valid return unless parse_arguments(*args) end checksums = nil if use_cache Registry.load checksums = Registry.checksums.dup end if save_yardoc Registry.lock_for_writing do YARD.parse(files, excluded) Registry.save(use_cache) end else YARD.parse(files, excluded) end if generate run_generate(checksums) copy_assets elsif list print_list end if !list && statistics && log.level < Logger::ERROR Registry.load_all log.enter_level(Logger::ERROR) do Stats.new(false).run(*args) end end abort if fail_on_warning && log.warned true ensure log.show_progress = false end # Parses commandline arguments # @param [Array] args the list of arguments # @return [Boolean] whether or not arguments are valid # @since 0.5.6 def parse_arguments(*args) super(*args) # Last minute modifications self.files = Parser::SourceParser::DEFAULT_PATH_GLOB if files.empty? files.delete_if {|x| x =~ /\A\s*\Z/ } # remove empty ones readme = Dir.glob('README{,*[^~]}').first readme ||= Dir.glob(files.first).first if options.onefile options.readme ||= CodeObjects::ExtraFileObject.new(readme) if readme options.files.unshift(options.readme).uniq! if options.readme Tags::Library.visible_tags -= hidden_tags add_visibility_verifier add_api_verifier apply_locale # US-ASCII is invalid encoding for onefile if defined?(::Encoding) && options.onefile if ::Encoding.default_internal == ::Encoding::US_ASCII log.warn "--one-file is not compatible with US-ASCII encoding, using ASCII-8BIT" ::Encoding.default_external, ::Encoding.default_internal = ['ascii-8bit'] * 2 end end if generate && !verify_markup_options false else true end end # The list of all objects to process. Override this method to change # which objects YARD should generate documentation for. # # @deprecated To hide methods use the +@private+ tag instead. # @return [Array] a list of code objects to process def all_objects Registry.all(:root, :module, :class) end private # Generates output for objects # @param [Hash, nil] checksums if supplied, a list of checkums for files. # @return [void] # @since 0.5.1 def run_generate(checksums) if checksums changed_files = [] Registry.checksums.each do |file, hash| changed_files << file if checksums[file] != hash end end Registry.load_all if use_cache objects = run_verifier(all_objects).reject do |object| serialized = !options.serializer || options.serializer.exists?(object) if checksums && serialized && !object.files.any? {|f, _line| changed_files.include?(f) } true else log.debug "Re-generating object #{object.path}..." false end end Templates::Engine.generate(objects, options) end # Verifies that the markup options are valid before parsing any code. # Failing early is better than failing late. # # @return (see YARD::Templates::Helpers::MarkupHelper#load_markup_provider) def verify_markup_options result = false lvl = has_markup ? log.level : Logger::FATAL obj = Struct.new(:options).new(options) obj.extend(Templates::Helpers::MarkupHelper) options.files.each do |file| markup = file.attributes[:markup] || obj.markup_for_file('', file.filename) result = obj.load_markup_provider(markup) return false if !result && markup != :rdoc end options.markup = :rdoc unless has_markup log.enter_level(lvl) { result = obj.load_markup_provider } if !result && !has_markup log.warn "Could not load default RDoc formatter, " \ "ignoring any markup (install RDoc to get default formatting)." options.markup = :none true else result end end # Copies any assets to the output directory # @return [void] # @since 0.6.0 def copy_assets return unless options.serializer outpath = options.serializer.basepath assets.each do |from, to| to = File.join(outpath, to) log.debug "Copying asset '#{from}' to '#{to}'" from += '/.' if File.directory?(from) FileUtils.cp_r(from, to) end end # Prints a list of all objects # @return [void] # @since 0.5.5 def print_list Registry.load_all run_verifier(Registry.all). sort_by {|item| [item.file || '', item.line || 0] }.each do |item| log.puts "#{item.file}:#{item.line}: #{item.path}" end end # Adds a set of extra documentation files to be processed # @param [Array] files the set of documentation files def add_extra_files(*files) files.map! {|f| f.include?("*") ? Dir.glob(f) : f }.flatten! files.each do |file| if File.file?(file) options.files << CodeObjects::ExtraFileObject.new(file) else log.warn "Could not find extra file: #{file}" end end end # Parses the file arguments into Ruby files and extra files, which are # separated by a '-' element. # # @example Parses a set of Ruby source files # parse_files %w(file1 file2 file3) # @example Parses a set of Ruby files with a separator and extra files # parse_files %w(file1 file2 - extrafile1 extrafile2) # @param [Array] files the list of files to parse # @return [void] def parse_files(*files) seen_extra_files_marker = false files.each do |file| if file == "-" seen_extra_files_marker = true next end if seen_extra_files_marker add_extra_files(file) else self.files << file end end end # Adds verifier rule for visibilities # @return [void] # @since 0.5.6 def add_visibility_verifier vis_expr = "#{visibilities.uniq.inspect}.include?(object.visibility)" options.verifier.add_expressions(vis_expr) end # Adds verifier rule for APIs # @return [void] # @since 0.8.1 def add_api_verifier no_api = true if apis.delete('') exprs = [] exprs << "#{apis.uniq.inspect}.include?(@api.text)" unless apis.empty? unless hidden_apis.empty? exprs << "!#{hidden_apis.uniq.inspect}.include?(@api.text)" end exprs = !exprs.empty? ? [exprs.join(' && ')] : [] exprs << "!@api" if no_api expr = exprs.join(' || ') options.verifier.add_expressions(expr) unless expr.empty? end # Applies the specified locale to collected objects # @return [void] # @since 0.8.3 def apply_locale YARD::I18n::Locale.default = options.locale options.files.each do |file| file.locale = options.locale end end # (see Templates::Helpers::BaseHelper#run_verifier) def run_verifier(list) options.verifier ? options.verifier.run(list) : list end # @since 0.6.0 def add_tag(tag_data, factory_method = nil) tag, title = *tag_data.split(':') title ||= tag.capitalize Tags::Library.define_tag(title, tag.to_sym, factory_method) Tags::Library.visible_tags |= [tag.to_sym] end # Parses commandline options. # @param [Array] args each tokenized argument def optparse(*args) opts = OptionParser.new opts.banner = "Usage: yard doc [options] [source_files [- extra_files]]" opts.separator "(if a list of source files is omitted, " opts.separator " {lib,app}/**/*.rb ext/**/*.c is used.)" opts.separator "" opts.separator "Example: yardoc -o documentation/ - FAQ LICENSE" opts.separator " The above example outputs documentation for files in" opts.separator " lib/**/*.rb to documentation/ including the extra files" opts.separator " FAQ and LICENSE." opts.separator "" opts.separator "A base set of options can be specified by adding a .yardopts" opts.separator "file to your base path containing all extra options separated" opts.separator "by whitespace." general_options(opts) output_options(opts) tag_options(opts) common_options(opts) parse_options(opts, args) parse_files(*args) unless args.empty? end # Adds general options def general_options(opts) opts.separator "" opts.separator "General Options:" opts.on('-b', '--db FILE', 'Use a specified .yardoc db to load from or save to', ' (defaults to .yardoc)') do |yfile| YARD::Registry.yardoc_file = yfile end opts.on('--[no-]single-db', 'Whether code objects should be stored to single', ' database file (advanced)') do |use_single_db| Registry.single_object_db = use_single_db end opts.on('-n', '--no-output', 'Only generate .yardoc database, no documentation.') do self.generate = false end opts.on('-c', '--use-cache [FILE]', "Use the cached .yardoc db to generate documentation.", " (defaults to no cache)") do |file| YARD::Registry.yardoc_file = file if file self.use_cache = true end opts.on('--no-cache', "Clear .yardoc db before parsing source.") do self.use_cache = false end yardopts_options(opts) opts.on('--no-save', 'Do not save the parsed data to the yardoc db') do self.save_yardoc = false end opts.on('--exclude REGEXP', 'Ignores a file if it matches path match (regexp)') do |path| excluded << path end opts.on('--fail-on-warning', 'Exit with error status code if a warning occurs') do self.fail_on_warning = true end end # Adds output options def output_options(opts) opts.separator "" opts.separator "Output options:" opts.on('--one-file', 'Generates output as a single file') do options.onefile = true end opts.on('--list', 'List objects to standard out (implies -n)') do |_format| self.generate = false self.list = true end opts.on('--no-public', "Don't show public methods. (default shows public)") do visibilities.delete(:public) end opts.on('--protected', "Show protected methods. (default hides protected)") do visibilities.push(:protected) end opts.on('--private', "Show private methods. (default hides private)") do visibilities.push(:private) end opts.on('--no-private', "Hide objects with @private tag") do options.verifier.add_expressions '!object.tag(:private) && (object.namespace.is_a?(CodeObjects::Proxy) || !object.namespace.tag(:private))' end opts.on('--[no-]api API', 'Generates documentation for a given API', '(objects which define the correct @api tag).', 'If --no-api is given, displays objects with', 'no @api tag.') do |api| api = '' if api == false apis.push(api) end opts.on('--hide-api API', 'Hides given @api tag from documentation') do |api| hidden_apis.push(api) end opts.on('--embed-mixins', "Embeds mixin methods into class documentation") do options.embed_mixins << '*' end opts.on('--embed-mixin [MODULE]', "Embeds mixin methods from a particular", " module into class documentation") do |mod| options.embed_mixins << mod end opts.on('--no-highlight', "Don't highlight code blocks in output.") do options.highlight = false end opts.on('--default-return TYPE', "Shown if method has no return type. ", " (defaults to 'Object')") do |type| options.default_return = type end opts.on('--hide-void-return', "Hides return types specified as 'void'. ", " (default is shown)") do options.hide_void_return = true end opts.on('--query QUERY', "Only show objects that match a specific query") do |query| next if YARD::Config.options[:safe_mode] options.verifier.add_expressions(query.taint) end opts.on('--title TITLE', 'Add a specific title to HTML documents') do |title| options.title = title end opts.on('-r', '--readme FILE', '--main FILE', 'The readme file used as the title page', ' of documentation.') do |readme| if File.file?(readme) options.readme = CodeObjects::ExtraFileObject.new(readme) else log.warn "Could not find readme file: #{readme}" end end opts.on('--files FILE1,FILE2,...', 'Any extra comma separated static files to be ', ' included (eg. FAQ)') do |files| add_extra_files(*files.split(",")) end opts.on('--asset FROM[:TO]', 'A file or directory to copy over to output ', ' directory after generating') do |asset| re = %r{^(?:\.\./|/)} from, to = *asset.split(':').map {|f| File.cleanpath(f, true) } to ||= from if from =~ re || to =~ re log.warn "Invalid file '#{asset}'" else assets[from] = to end end opts.on('-o', '--output-dir PATH', 'The output directory. (defaults to ./doc)') do |dir| options.serializer.basepath = dir end opts.on('-m', '--markup MARKUP', 'Markup style used in documentation, like textile, ', ' markdown or rdoc. (defaults to rdoc)') do |markup| self.has_markup = true options.markup = markup.to_sym end opts.on('-M', '--markup-provider MARKUP_PROVIDER', 'Overrides the library used to process markup ', ' formatting (specify the gem name)') do |markup_provider| options.markup_provider = markup_provider.to_sym end opts.on('--charset ENC', 'Character set to use when parsing files ', ' (default is system locale)') do |encoding| begin if defined?(Encoding) && Encoding.respond_to?(:default_external=) Encoding.default_external = encoding Encoding.default_internal = encoding end rescue ArgumentError => e raise OptionParser::InvalidOption, e end end opts.on('-t', '--template TEMPLATE', 'The template to use. (defaults to "default")') do |template| options.template = template.to_sym end opts.on('-p', '--template-path PATH', 'The template path to look for templates in.', ' (used with -t).') do |path| next if YARD::Config.options[:safe_mode] YARD::Templates::Engine.register_template_path(File.expand_path(path)) end opts.on('-f', '--format FORMAT', 'The output format for the template.', ' (defaults to html)') do |format| options.format = format.to_sym end opts.on('--no-stats', 'Don\'t print statistics') do self.statistics = false end opts.on('--no-progress', 'Don\'t show progress bar') do log.show_progress = false end opts.on('--locale LOCALE', 'The locale for generated documentation.', ' (defaults to en)') do |locale| options.locale = locale end opts.on('--po-dir DIR', 'The directory that has .po files.', " (defaults to #{YARD::Registry.po_dir})") do |dir| YARD::Registry.po_dir = dir end end # Adds tag options # @since 0.6.0 def tag_options(opts) opts.separator "" opts.separator "Tag options: (TAG:TITLE looks like: 'overload:Overloaded Method')" opts.on('--tag TAG:TITLE', 'Registers a new free-form metadata @tag') do |tag| add_tag(tag) end opts.on('--type-tag TAG:TITLE', 'Tag with an optional types field') do |tag| add_tag(tag, :with_types) end opts.on('--type-name-tag TAG:TITLE', 'Tag with optional types and a name field') do |tag| add_tag(tag, :with_types_and_name) end opts.on('--name-tag TAG:TITLE', 'Tag with a name field') do |tag| add_tag(tag, :with_name) end opts.on('--title-tag TAG:TITLE', 'Tag with first line as title field') do |tag| add_tag(tag, :with_title_and_text) end opts.on('--hide-tag TAG', 'Hides a previously defined tag from templates') do |tag| self.hidden_tags |= [tag.to_sym] end opts.on('--transitive-tag TAG', 'Marks a tag as transitive') do |tag| Tags::Library.transitive_tags |= [tag.to_sym] end opts.on('--non-transitive-tag TAG', 'Marks a tag as not transitive') do |tag| Tags::Library.transitive_tags -= [tag.to_sym] end end end end end