From 0db0bc3b6f0bc616fa9b1319be3083e4ea6affce Mon Sep 17 00:00:00 2001 From: elijah Date: Sun, 9 Dec 2012 22:24:57 -0800 Subject: initial experiments with auto doc creation --- Rakefile | 108 ++++++++++++++++++++++++++ lib/leap_cli/app.rb | 57 ++++++++++++++ lib/lib_ext/markdown_document_listener.rb | 122 ++++++++++++++++++++++++++++++ 3 files changed, 287 insertions(+) create mode 100644 lib/leap_cli/app.rb create mode 100644 lib/lib_ext/markdown_document_listener.rb diff --git a/Rakefile b/Rakefile index 21d1c2d..54ae637 100644 --- a/Rakefile +++ b/Rakefile @@ -27,6 +27,8 @@ $spec_path = 'leap_cli.gemspec' $base_dir = File.dirname(__FILE__) $spec = eval(File.read(File.join($base_dir, $spec_path))) $gem_path = File.join($base_dir, 'pkg', "#{$spec.name}-#{$spec.version}.gem") +$lib_dir = "#{$base_dir}/lib" +$LOAD_PATH.unshift $lib_dir def built_gem_path Dir[File.join($base_dir, "#{$spec.name}-*.gem")].sort_by{|f| File.mtime(f)}.last @@ -122,3 +124,109 @@ end # rd.rdoc_files.include("README.rdoc","lib/**/*.rb","bin/**/*") # rd.title = 'Your application title' # end + +desc "Generate documentation" +task 'doc' do + require 'leap_cli' + require 'leap_cli/app' + + class DocMaker < GLI::Command + def initialize(app) + @app = app + @listener = GLI::Commands::RdocDocumentListener.new([],[],[]) + end + + def create + @listener.beginning + @listener.program_desc(@app.program_desc) unless @app.program_desc.nil? + @listener.program_long_desc(@app.program_long_desc) unless @app.program_long_desc.nil? + @listener.version(@app.version_string) + if any_options?(@app) + @listener.options + end + document_flags_and_switches(@listener, @app.flags.values.sort(&by_name), @app.switches.values.sort(&by_name)) + if any_options?(@app) + @listener.end_options + end + @listener.commands + document_commands(@listener, @app) + @listener.end_commands + @listener.ending + end + + private + + def document_commands(document_listener,context) + context.commands.values.reject {|_| _.nodoc }.sort(&by_name).each do |command| + call_command_method_being_backwards_compatible(document_listener,command) + document_listener.options if any_options?(command) + document_flags_and_switches(document_listener,command_flags(command),command_switches(command)) + document_listener.end_options if any_options?(command) + document_listener.commands if any_commands?(command) + document_commands(document_listener,command) + document_listener.end_commands if any_commands?(command) + document_listener.end_command(command.name) + end + document_listener.default_command(context.get_default_command) + end + + def call_command_method_being_backwards_compatible(document_listener,command) + command_args = [command.name, + Array(command.aliases), + command.description, + command.long_description, + command.arguments_description] + if document_listener.method(:command).arity == 6 + command_args << command.arguments_options + end + document_listener.command(*command_args) + end + + def by_name + lambda { |a,b| a.name.to_s <=> b.name.to_s } + end + + def command_flags(command) + command.topmost_ancestor.flags.values.select { |flag| flag.associated_command == command }.sort(&by_name) + end + + def command_switches(command) + command.topmost_ancestor.switches.values.select { |switch| switch.associated_command == command }.sort(&by_name) + end + + def document_flags_and_switches(document_listener,flags,switches) + flags.each do |flag| + document_listener.flag(flag.name, + Array(flag.aliases), + flag.description, + flag.long_description, + flag.safe_default_value, + flag.argument_name, + flag.must_match, + flag.type) + end + switches.each do |switch| + document_listener.switch(switch.name, + Array(switch.aliases), + switch.description, + switch.long_description, + switch.negatable) + end + end + + def any_options?(context) + options = if context.kind_of?(GLI::Command) + command_flags(context) + command_switches(context) + else + context.flags.values + context.switches.values + end + !options.empty? + end + + def any_commands?(command) + !command.commands.empty? + end + end + + puts DocMaker.new(LeapCli::Commands).create +end diff --git a/lib/leap_cli/app.rb b/lib/leap_cli/app.rb new file mode 100644 index 0000000..90c4ae9 --- /dev/null +++ b/lib/leap_cli/app.rb @@ -0,0 +1,57 @@ +require 'gli' +require 'highline' +require 'forwardable' +require 'lib_ext/gli' # our custom extensions to gli + +# +# Typically, GLI and Highline methods are loaded into the global namespace. +# Instead, here we load these into the module LeapCli::Commands in order to +# ensure that the cli logic and code is kept isolated to leap_cli/commands/*.rb +# +# no cheating! +# +module LeapCli::Commands + extend GLI::App + extend Forwardable + + # + # delegate highline methods to make them available to sub-commands + # + @terminal = HighLine.new + def_delegator :@terminal, :ask, 'self.ask' + def_delegator :@terminal, :agree, 'self.agree' + def_delegator :@terminal, :choose, 'self.choose' + def_delegator :@terminal, :say, 'self.say' + def_delegator :@terminal, :color, 'self.color' + def_delegator :@terminal, :list, 'self.list' + + # + # make config manager available as 'manager' + # + def self.manager + @manager ||= begin + manager = LeapCli::Config::Manager.new + manager.load + manager + end + end + + # + # info about leap command line suite + # + program_desc LeapCli::SUMMARY + program_long_desc LeapCli::DESCRIPTION + + # + # handle --version ourselves + # + if ARGV.grep(/--version/).any? + puts "leap #{LeapCli::VERSION}, ruby #{RUBY_VERSION}" + exit(0) + end + + # + # load commands and run + # + commands_from('leap_cli/commands') +end diff --git a/lib/lib_ext/markdown_document_listener.rb b/lib/lib_ext/markdown_document_listener.rb new file mode 100644 index 0000000..55026e9 --- /dev/null +++ b/lib/lib_ext/markdown_document_listener.rb @@ -0,0 +1,122 @@ +require 'stringio' +require 'gli/commands/help_modules/arg_name_formatter' + +# +# adaption of RdocDocumentListener to use Markdown +# see http://rtomayko.github.com/ronn/ronn-format.7 +# + +module GLI + module Commands + class MarkdownDocumentListener + + def initialize(global_options,options,arguments) + @io = STDOUT #File.new(File.basename($0) + ".rdoc",'w') + @nest = '' + @arg_name_formatter = GLI::Commands::HelpModules::ArgNameFormatter.new + end + + def beginning + end + + # Called when processing has completed + def ending + #@io.close + end + + # Gives you the program description + def program_desc(desc) + @io.puts "== #{File.basename($0)} - #{desc}" + @io.puts + end + + def program_long_desc(desc) + @io.puts desc + @io.puts + end + + # Gives you the program version + def version(version) + @io.puts "v#{version}" + @io.puts + end + + def options + if @nest.size == 0 + @io.puts "=== Global Options" + else + @io.puts "#{@nest}=== Options" + end + end + + # Gives you a flag in the current context + def flag(name,aliases,desc,long_desc,default_value,arg_name,must_match,type) + invocations = ([name] + Array(aliases)).map { |_| add_dashes(_) }.join('|') + usage = "#{invocations} #{arg_name || 'arg'}" + @io.puts "#{@nest}=== #{usage}" + @io.puts + @io.puts String(desc).strip + @io.puts + @io.puts "[Default Value] #{default_value || 'None'}" + @io.puts "[Must Match] #{must_match.to_s}" unless must_match.nil? + @io.puts String(long_desc).strip + @io.puts + end + + # Gives you a switch in the current context + def switch(name,aliases,desc,long_desc,negetable) + if negetable + name = "[no-]#{name}" if name.to_s.length > 1 + aliases = aliases.map { |_| _.to_s.length > 1 ? "[no-]#{_}" : _ } + end + invocations = ([name] + aliases).map { |_| add_dashes(_) }.join('|') + @io.puts "#{@nest}=== #{invocations}" + @io.puts String(desc).strip + @io.puts + @io.puts String(long_desc).strip + @io.puts + end + + def end_options + end + + def commands + @io.puts "#{@nest}=== Commands" + @nest = "#{@nest}=" + end + + # Gives you a command in the current context and creates a new context of this command + def command(name,aliases,desc,long_desc,arg_name,arg_options) + @io.puts "#{@nest}=== Command: #{([name] + aliases).join('|')} #{@arg_name_formatter.format(arg_name,arg_options)}" + @io.puts String(desc).strip + @io.puts + @io.puts String(long_desc).strip + @nest = "#{@nest}=" + end + + # Ends a command, and "pops" you back up one context + def end_command(name) + @nest.gsub!(/=$/,'') + end + + # Gives you the name of the current command in the current context + def default_command(name) + @io.puts "[Default Command] #{name}" unless name.nil? + end + + def end_commands + @nest.gsub!(/=$/,'') + end + + private + + def add_dashes(name) + name = "-#{name}" + name = "-#{name}" if name.length > 2 + name + end + + + end + end +end -- cgit v1.2.3