amp-front 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. data/.document +5 -0
  2. data/.gitignore +24 -0
  3. data/Ampfile +3 -0
  4. data/Gemfile +10 -0
  5. data/Gemfile.lock +36 -0
  6. data/LICENSE +20 -0
  7. data/README.md +50 -0
  8. data/Rakefile +64 -0
  9. data/VERSION +1 -0
  10. data/design_docs/commands.md +91 -0
  11. data/design_docs/dependencies.md +35 -0
  12. data/design_docs/plugins.md +47 -0
  13. data/features/amp.feature +8 -0
  14. data/features/amp_help.feature +36 -0
  15. data/features/amp_plugin_list.feature +10 -0
  16. data/features/step_definitions/amp-front_steps.rb +23 -0
  17. data/features/support/env.rb +4 -0
  18. data/lib/amp-front.rb +30 -0
  19. data/lib/amp-front/dispatch/commands/base.rb +158 -0
  20. data/lib/amp-front/dispatch/commands/builtin/help.rb +23 -0
  21. data/lib/amp-front/dispatch/commands/builtin/plugin.rb +24 -0
  22. data/lib/amp-front/dispatch/commands/validations.rb +171 -0
  23. data/lib/amp-front/dispatch/runner.rb +86 -0
  24. data/lib/amp-front/help/entries/__default__.erb +31 -0
  25. data/lib/amp-front/help/entries/ampfiles.md +42 -0
  26. data/lib/amp-front/help/entries/commands.erb +6 -0
  27. data/lib/amp-front/help/entries/new-commands.md +81 -0
  28. data/lib/amp-front/help/help.rb +312 -0
  29. data/lib/amp-front/plugins/base.rb +87 -0
  30. data/lib/amp-front/support/module_extensions.rb +92 -0
  31. data/lib/amp-front/third_party/maruku.rb +136 -0
  32. data/lib/amp-front/third_party/maruku/attributes.rb +227 -0
  33. data/lib/amp-front/third_party/maruku/defaults.rb +71 -0
  34. data/lib/amp-front/third_party/maruku/errors_management.rb +92 -0
  35. data/lib/amp-front/third_party/maruku/helpers.rb +260 -0
  36. data/lib/amp-front/third_party/maruku/input/charsource.rb +326 -0
  37. data/lib/amp-front/third_party/maruku/input/extensions.rb +69 -0
  38. data/lib/amp-front/third_party/maruku/input/html_helper.rb +189 -0
  39. data/lib/amp-front/third_party/maruku/input/linesource.rb +111 -0
  40. data/lib/amp-front/third_party/maruku/input/parse_block.rb +615 -0
  41. data/lib/amp-front/third_party/maruku/input/parse_doc.rb +234 -0
  42. data/lib/amp-front/third_party/maruku/input/parse_span_better.rb +746 -0
  43. data/lib/amp-front/third_party/maruku/input/rubypants.rb +225 -0
  44. data/lib/amp-front/third_party/maruku/input/type_detection.rb +147 -0
  45. data/lib/amp-front/third_party/maruku/input_textile2/t2_parser.rb +163 -0
  46. data/lib/amp-front/third_party/maruku/maruku.rb +33 -0
  47. data/lib/amp-front/third_party/maruku/output/to_ansi.rb +223 -0
  48. data/lib/amp-front/third_party/maruku/output/to_html.rb +991 -0
  49. data/lib/amp-front/third_party/maruku/output/to_markdown.rb +164 -0
  50. data/lib/amp-front/third_party/maruku/output/to_s.rb +56 -0
  51. data/lib/amp-front/third_party/maruku/string_utils.rb +191 -0
  52. data/lib/amp-front/third_party/maruku/structures.rb +167 -0
  53. data/lib/amp-front/third_party/maruku/structures_inspect.rb +87 -0
  54. data/lib/amp-front/third_party/maruku/structures_iterators.rb +61 -0
  55. data/lib/amp-front/third_party/maruku/textile2.rb +1 -0
  56. data/lib/amp-front/third_party/maruku/toc.rb +199 -0
  57. data/lib/amp-front/third_party/maruku/usage/example1.rb +33 -0
  58. data/lib/amp-front/third_party/maruku/version.rb +40 -0
  59. data/lib/amp-front/third_party/trollop.rb +766 -0
  60. data/spec/amp-front_spec.rb +25 -0
  61. data/spec/command_specs/base_spec.rb +123 -0
  62. data/spec/command_specs/command_spec.rb +97 -0
  63. data/spec/command_specs/help_spec.rb +33 -0
  64. data/spec/command_specs/spec_helper.rb +37 -0
  65. data/spec/command_specs/validations_spec.rb +267 -0
  66. data/spec/dispatch_specs/runner_spec.rb +116 -0
  67. data/spec/dispatch_specs/spec_helper.rb +15 -0
  68. data/spec/help_specs/help_entry_spec.rb +78 -0
  69. data/spec/help_specs/help_registry_spec.rb +77 -0
  70. data/spec/help_specs/spec_helper.rb +15 -0
  71. data/spec/plugin_specs/base_spec.rb +36 -0
  72. data/spec/plugin_specs/spec_helper.rb +15 -0
  73. data/spec/spec.opts +1 -0
  74. data/spec/spec_helper.rb +33 -0
  75. data/spec/support_specs/module_extensions_spec.rb +104 -0
  76. data/spec/support_specs/spec_helper.rb +15 -0
  77. data/test/third_party_tests/test_trollop.rb +1181 -0
  78. metadata +192 -0
@@ -0,0 +1,86 @@
1
+ ##################################################################
2
+ # Licensing Information #
3
+ # #
4
+ # The following code is licensed, as standalone code, under #
5
+ # the Ruby License, unless otherwise directed within the code. #
6
+ # #
7
+ # For information on the license of this code when distributed #
8
+ # with and used in conjunction with the other modules in the #
9
+ # Amp project, please see the root-level LICENSE file. #
10
+ # #
11
+ # © Michael J. Edgar and Ari Brown, 2009-2010 #
12
+ # #
13
+ ##################################################################
14
+
15
+ module Amp
16
+ module Dispatch
17
+
18
+ # This class runs Amp as a binary. Create a new instance with the arguments
19
+ # to use, and call run! to run Amp.
20
+ class Runner
21
+ def initialize(args, opts={})
22
+ @args, @opts = args, opts
23
+ end
24
+
25
+ def run!
26
+ global_opts, arguments = collect_options(@args)
27
+ load_ampfile!
28
+ load_plugins!
29
+
30
+ command_class = Amp::Command.for_name(arguments.join(' '))
31
+ if command_class.nil?
32
+ command_class = Amp::Command::Help
33
+ else
34
+ arguments = trim_argv_for_command(arguments, command_class)
35
+ end
36
+ command = command_class.new
37
+ opts, arguments = command.collect_options(arguments)
38
+ command.call(opts.merge(global_opts), arguments)
39
+ end
40
+
41
+ # Loads the ampfile (or whatever it's specified as) from the
42
+ # current directory or a parent directory.
43
+ def load_ampfile!(in_dir = Dir.pwd)
44
+ file = @opts[:ampfile] || 'ampfile'
45
+ variations = [file, file[0,1].upcase + file[1..-1]] # include titlecase
46
+ to_load = variations.find {|x| File.exist?(File.join(in_dir, x))}
47
+ if to_load
48
+ load to_load
49
+ elsif File.dirname(in_dir) != in_dir
50
+ load_ampfile! File.dirname(in_dir)
51
+ end
52
+ end
53
+
54
+ def load_plugins!
55
+ Amp::Plugins::Base.all_plugins.each do |plugin|
56
+ instance = plugin.new(@opts)
57
+ instance.load!
58
+ Amp::Plugins::Base.loaded_plugins << instance
59
+ end
60
+ end
61
+
62
+ def trim_argv_for_command(arguments, command)
63
+ argv = arguments.dup
64
+ path_parts = command.inspect.gsub(/Amp::Command::/, '').gsub(/::/, ' ').split
65
+ path_parts.each do |part|
66
+ next_part = argv.shift
67
+ if next_part.downcase != part.downcase
68
+ raise ArgumentError.new(
69
+ "Failed to parse command line option for: #{command.inspect}")
70
+ end
71
+ end
72
+ argv
73
+ end
74
+
75
+ def collect_options(arguments)
76
+ argv = arguments.dup
77
+ _, hash = Trollop::options(argv) do
78
+ banner "Amp - some more crystal, sir?"
79
+ version "Amp version #{Amp::VERSION} (#{Amp::VERSION_TITLE})"
80
+ stop_on_unknown
81
+ end
82
+ [hash, argv]
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,31 @@
1
+ <%= "Amp Help" %>
2
+
3
+ Thanks for using Amp! This help file is a little sparse right now, but
4
+ here are some useful commands to help you get started:
5
+
6
+ <%= "add" %> add the specified files on the next commit
7
+ <%= "annotate" %> show changeset information per file line
8
+ <%= "clone" %> make a copy of an existing repository
9
+ <%= "commit" %> commit the specified files or all outstanding changes
10
+ <%= "diff" %> diff repository (or selected files)
11
+ <%= "export" %> dump the header and diffs for one or more changesets
12
+ <%= "init" %> create a new repository in the given directory
13
+ <%= "log" %> show revision history of entire repository or files
14
+ <%= "merge" %> merge working directory with another revision
15
+ <%= "parents" %> show the parents of the working dir or revision
16
+ <%= "pull" %> pull changes from the specified source
17
+ <%= "push" %> push changes to the specified destination
18
+ <%= "remove" %> remove the specified files on the next commit
19
+ <%= "serve" %> export the repository via HTTP
20
+ <%= "status" %> show changed files in the working directory
21
+ <%= "update" %> update working directory
22
+
23
+ Use <%= "amp help [command-name]" %> to get help on a particular
24
+ command. Also, we have other pages that you might find interesting - view them
25
+ with <%= "amp help [page-name]" %>!
26
+
27
+ <%= "ampfiles" %> - learn about your ampfile.rb
28
+ <%= "new-commands" %> - learn how to easily add commands to amp!
29
+ <%= "paths" %> - learn about how to pass in paths to files without going nuts!
30
+
31
+ Thanks again for using Amp!
@@ -0,0 +1,42 @@
1
+ # What's an Ampfile?
2
+
3
+ _Ampfiles_ are how you load your customizations into _amp_. INI files are handy, and we use them maintain compatibility with the #{hg_link} distribution. However, you can't put code in an INI file. And the files, quite frankly, aren't very pretty. That's why we have ampfiles.
4
+
5
+ For those familiar with #{ruby_link}, ampfiles are sort of similar to #{link_to "http://rake.rubyforge.org/", "Rake"}'s Rakefiles. When you run _amp_, amp will look in the current directory for a file called "Ampfile" (or "ampfile", "ampfile.rb", etc.). If it doesn't find one it looks in the folder containing the current one - and up and up until it gives up. If it doesn't find one, that's ok - you don't need an ampfile to use _amp_!
6
+
7
+ # So how does Amp use Ampfiles?
8
+ The cool thing about ampfiles is that they're just Ruby code. So _amp_ just runs your ampfile as Ruby. "But wait," you say, "what use is running a script every time I use amp?" Well, silly, we give you a bunch of Ruby methods you can use to modify _amp_, and when your ampfile is run, those changes happen!
9
+
10
+ # ~/.amprc
11
+ A quick note: _amp_ will also run the file located at `~/.amprc` (~ means "your user directory"), right before running any ampfiles. That way, the ampfile in your repository can override any global settings in `.amprc`.
12
+
13
+ # Example! Now!
14
+ Ok, ok. For starters, you can modify existing commands very simply. In Ruby, you can open a class up, add a method, and close it again. In Amp, you do that like this:
15
+
16
+
17
+ command "status" do |c|
18
+ c.default :"no-color", true
19
+ end
20
+
21
+ If you put that in your Ampfile, the `amp status` command will no longer use color. Why don't we create a new command entirely?
22
+
23
+
24
+ command "stats" do |c|
25
+ c.workflow :hg
26
+ c.desc "Prints how many commits each user has contributed"
27
+ c.on_call do |opts, args|
28
+ repo = opts[:repository]
29
+ users = Hash.new {|h, k| h[k] = 0}
30
+ repo.each do |changeset|
31
+ users[changeset.user] += 1
32
+ end
33
+ users.to_a.sort {|a,b| b[1] <=> a[1]}.each do |u,c|
34
+ puts "\#{u}: \#{c}"
35
+ end
36
+ end
37
+ end
38
+
39
+ You can put that in your ampfile and get commit statistics right away. In fact, it's in _amp_'s ampfile. If you run `amp help`, your new command will be in the list! Why does this work? Well, it's not much of a secret: **You're actually writing the exact same code used to create the built-in amp commands.** If you look at our code, each of the commands you know and love, such as `amp commit`, `amp move` are written using this exact same code style.
40
+ # Cool! What now?
41
+
42
+ Well, start hacking away! You might find the #{commands_link} section interesting, as well as our "Learn Amp" pages, where you can find the _amp_ API. We'll be setting up a place for useful snippets to be posted soon.
@@ -0,0 +1,6 @@
1
+ These are the following commands available:
2
+
3
+ <% Amp::Command::Base.all_commands.sort {|c1, c2| c1.name.to_s <=> c2.name.to_s}.each do |command| -%>
4
+ <%= command.name.to_s.downcase.ljust(30, " ")%><%= command.desc %>
5
+ <%- end %>
6
+ Run "amp help [command]" for more information.
@@ -0,0 +1,81 @@
1
+ # _Amp_ Commands
2
+
3
+ Amp's commands are the driving force behind what makes _amp_ unique. Most VCS users are programmers, and yet programming existing VCS systems isn't a simple, well-documented task. In _git_ you might write a shell script, yet with _hg_ you'll find yourself writing a python module and adding it through arcane INI file wizardry. With _amp_ you just add Ruby code directly to your _ampfile_, which is _not_ a hidden file in some strange directory.
4
+
5
+ All of _amp_'s commands have access to a first-class options parser and command line arguments. You don't have to parse options yourself, of course - you just declare them. Now, we've got some really simple commands littered throughout the site to illustrate how simple commands are. But if you're at this page, you probably want a little bit more. So here's the `amp log` command in _amp_ - unmodified:
6
+
7
+ command :log do |c|
8
+ c.workflow :hg
9
+ c.desc "Prints the commit history."
10
+ c.opt :verbose, "Verbose output", {:short => "-v"}
11
+ c.opt :limit, "Limit how many revisions to show",
12
+ {:short => "-l", :type => :integer, :default => -1}
13
+ c.opt :template, "Which template to use while printing",
14
+ {:short => "-t", :type => :string, :default => "default"}
15
+ c.opt :no_output, "Doesn't print output (useful for benchmarking)"
16
+ c.on_call do |options, args|
17
+ repo = options[:repository]
18
+ limit = options[:limit]
19
+ limit = repo.size if limit == -1
20
+
21
+ start = repo.size - 1
22
+ stop = start - limit + 1
23
+
24
+ options.merge! :template_type => :log
25
+ start.downto stop do |x|
26
+ puts repo[x].to_templated_s(options) unless options[:no_output]
27
+ end
28
+ end
29
+ end
30
+
31
+ There's a lot going on here. Let's break it down:
32
+
33
+ command :log do |c|
34
+
35
+ This line creates a command called `:log`, and passes it to the block as **c**. **If the command `:log` exists already, then the existing command is passed to the block as _c_**. The inside of the block is where we declare our command. This is where things get interesting!
36
+
37
+ c.workflow :hg
38
+
39
+ The `workflow` method specifies which `workflow` the command belongs to. We use it here to specify that the `:log` command we're defining belongs to the `:hg` workflow, and shouldn't appear if the user is using the git workflow (or any other). If you specify `:all` as the workflow (or don't specify one), the command will be available to all workflows.
40
+
41
+ c.desc "Prints the commit history."
42
+
43
+ The `desc` method declares a small description of the command, which shows up when the user runs `amp help` to get a list of all available commands. It should fit on one line and be succinct. You may also use `desc=` if you prefer that style.
44
+
45
+ c.opt :verbose, "Verbose output", {:short => "-v"}
46
+ c.opt :limit, "Limit how many revisions to show",
47
+ {:short => "-l", :type => :integer, :default => -1}
48
+ c.opt :template, "Which template to use while printing",
49
+ {:short => "-t", :type => :string, :default => "default"}
50
+
51
+ Now that's what we're talking about! We're declaring some options here, and can see a small portion of _amp_'s option parser at work. Take a look at the `:verbose` line.
52
+
53
+ By declaring `c.opt :verbose`, we create a `--verbose` option for our `amp log` command. The first argument is the name of the option - it can be a string, or a symbol. The second argument is a short description - these first two arguments are required. After that come the nifty options! (Note - _amp_ uses trollop under-the-hood, so its options are identical to trollop's.)
54
+
55
+ * short - By setting `:short` to "-v", we can now use `amp log -v` as well as `amp log --verbose`
56
+ * type - The type of the option. This defaults to `:flag`, which is a normal on-or-off switch, like `--verbose`. You'll notice the `:limit` option has `:type` => `:integer`. This makes the `:limit` option require an argument, and forces it into an integer during parsing. Easy, eh?
57
+ * default - The default value of the option. If the user doesn't specify the option, it will normally be parsed as `nil` - you can set a default value here.
58
+
59
+ For more ways to configure your options, see the documentation for the `opt` method in the Command class..
60
+
61
+ c.on_call do |options, args|
62
+ repo = options[:repository]
63
+ limit = options[:limit]
64
+ limit = repo.size if limit == -1
65
+
66
+ start = repo.size - 1
67
+ stop = start - limit + 1
68
+
69
+ options.merge! :template_type => :log
70
+ start.downto stop do |x|
71
+ puts repo[x].to_templated_s(options) unless options[:no_output]
72
+ end
73
+ end
74
+
75
+ Last, but not least, we have the `on_call` method. This is how we declare what _happens_ when our command is run - which is what we really care about! You specify what the command does in `on_call`'s block, which takes two arguments: `options` and `args`. These are, respectively, the command-line options passed in plus amp's additions, and the arguments provided by the user. For example: `amp log --verbose --limit=3 arg1 arg2` would provide `{:verbose => true, :limit => 3, :repository => repo}` as `options` and `["arg1", "arg2"]` as `args`.
76
+
77
+ Our command is getting changeset information, so it needs a repository to interact with. Unless told to do otherwise, _amp_ will look for a repository and store it in `options[:repository]`. This will always be an object of the class LocalRepository.
78
+
79
+ Once we extract our repository, we decide which changesets to print based on the options. To get a given changeset, we simply use `repo[x]`, where `x` can be a revision number, a partial node ID, "tip", and so on. See the documentation for `LocalRepository#[]` for more details.
80
+
81
+ This is just a quick once-over of some of the more obvious features of _Amp's_ command system - there are far more features to discuss. Until those pages are written, take a look at the Documentation for the Command class.
@@ -0,0 +1,312 @@
1
+ ##################################################################
2
+ # Licensing Information #
3
+ # #
4
+ # The following code is licensed, as standalone code, under #
5
+ # the Ruby License, unless otherwise directed within the code. #
6
+ # #
7
+ # For information on the license of this code when distributed #
8
+ # with and used in conjunction with the other modules in the #
9
+ # Amp project, please see the root-level LICENSE file. #
10
+ # #
11
+ # © Michael J. Edgar and Ari Brown, 2009-2010 #
12
+ # #
13
+ ##################################################################
14
+
15
+ module Amp
16
+ ##
17
+ # The module covering all the help subsystems for the Amp binary.
18
+ module Help
19
+
20
+ ##
21
+ # Module handling the registration and retrieval of entries in the
22
+ # help system.
23
+ #
24
+ # This is a singleton module. Don't mix it in anywhere. That'd be silly.
25
+ module HelpRegistry
26
+ extend self
27
+
28
+ ##
29
+ # Retrives the entries hash which stores all the help entrys
30
+ #
31
+ # @return [Hash{String => Array<HelpEntry>}] the entry table for the help system
32
+ def entries
33
+ @entries ||= Hash.new() {|h,k| h[k] = []}
34
+ end
35
+
36
+ ##
37
+ # Returns a list of HelpEntrys with the given name. Since we allow for
38
+ # the possibility of overlap by name, this returns an array.
39
+ #
40
+ # @param [String, #to_s] entry the name of the entry(ies) to retrieve
41
+ # @return [Array<HelpEntry>] the help entries stored under the given name
42
+ def [](entry)
43
+ entries[entry]
44
+ end
45
+
46
+ ##
47
+ # Adds an entry to the registry. We take a name and an entry, and store
48
+ # the entry under the list of entries with the given name.
49
+ #
50
+ # @param [String, #to_s] name the name of the help entry. Allowed to
51
+ # conflict with other entries.
52
+ # @param [HelpEntry] entry the entry to store in the registry
53
+ def register(name, entry)
54
+ entries[name] << entry
55
+ end
56
+
57
+ ##
58
+ # Unregisters the given entry from the registry. Not sure why you might
59
+ # use this, but it's a capability.
60
+ #
61
+ # @param [String, #to_s] name the name of the entry. Note - you will also
62
+ # need to provide the entry, because there might be naming conflicts.
63
+ # @param [HelpEntry] entry the entry to remove from the registry.
64
+ def unregister(name, entry=nil)
65
+ case entries[name].size
66
+ when 0
67
+ raise ArgumentError.new("No help entry named '#{name}' found.")
68
+ when 1
69
+ entries[name].clear
70
+ else
71
+ if entry.nil?
72
+ raise ArgumentError.new("Multiple help entries named '#{name}': " +
73
+ 'you must provide which one to remove to ' +
74
+ '#unregister.')
75
+ else
76
+ entries[name].delete entry
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ ##
83
+ # The generic HelpEntry class encapsulates a entry in the help system. The
84
+ # entry has text that it provides to the user, as well as a name. The base
85
+ # HelpEntry class does not track its own name, because well, that's not
86
+ # useful! All it needs to know how to do is present its text when asked for it.
87
+ class HelpEntry
88
+ class << self
89
+ ##
90
+ # Singleton method that opens a file and returns a HelpEntry representing it.
91
+ # What makes this method spiffy is that it tries to detect the type of file
92
+ # -- markdown, ERb, et cetera, based on the file's extension, and picks
93
+ # the appropriate class to represent that help entry.
94
+ #
95
+ # The entry is registered under the name of the file - without any extensions -
96
+ # and the file's full contents are provided as the initial text.
97
+ #
98
+ # @param [String] filename the path to the file to load
99
+ # @return [HelpEntry] a help entry representing the file as best as we can.
100
+ def from_file(filename)
101
+ klass = case File.extname(filename).downcase
102
+ when ".md", ".markdown"
103
+ MarkdownHelpEntry
104
+ when ".erb"
105
+ ErbHelpEntry
106
+ else
107
+ HelpEntry
108
+ end
109
+ name = File.basename(filename).split(".", 2).first
110
+ klass.new(name, File.read(filename))
111
+ end
112
+ end
113
+
114
+ ##
115
+ # Creates a new HelpEntry, and registers it in the Help system, making it
116
+ # immediately available. It is for this reason that all subclasses should
117
+ # call +super+, because that registration is important!
118
+ #
119
+ # @param [String, #to_s] name the name under which to register this help entry
120
+ # @param [String] text ("") the text for the entry.
121
+ def initialize(name, text = "")
122
+ @text = text
123
+ HelpRegistry.register(name, self)
124
+ end
125
+
126
+ ##
127
+ # Returns the help text to display for this entry.
128
+ #
129
+ # In the generic case, just return the @text variable.
130
+ #
131
+ # @param [Hash] options the options for the process - that way the help commands
132
+ # can access the user's run-time options and global configuration. For example,
133
+ # if the user passes in --verbose or --quiet, each help entry could handle that
134
+ # differently. Who are we to judge?
135
+ # @return [String] the help text for the entry.
136
+ def text(options = {})
137
+ @text
138
+ end
139
+
140
+ ##
141
+ # Describes the entry briefly, so if the user must pick, they have a decent
142
+ # shot at knowing what this entry is about. Hopefully.
143
+ #
144
+ # In the generic case, use the text and grab the first few words.
145
+ #
146
+ # @return a description of the entry based on its content
147
+ def desc
148
+ %Q{a regular help entry ("#{text.split[0..5].join(" ")} ...")}
149
+ end
150
+ end
151
+
152
+ ##
153
+ # Represents a help entry that filters its help text through a Markdown parser
154
+ # before returning.
155
+ #
156
+ # This makes it very easy to make very pretty help files, that are smart enough
157
+ # to look good in both HTML form and when printed to a terminal. This uses our
158
+ # additions to the markdown parser to provide an "ANSI" output format.
159
+ class MarkdownHelpEntry < HelpEntry
160
+ ##
161
+ # Returns the help text to display for this entry.
162
+ #
163
+ # For a markdown entry, we run this through Maruku and our special to_ansi
164
+ # output formatter. This will make things like *this* underlined and **these**
165
+ # bolded. Code blocks will be given a colored background, and headings are
166
+ # accentuated.
167
+ #
168
+ # @param [Hash] options the options for the process - that way the help commands
169
+ # can access the user's run-time options and global configuration. For example,
170
+ # if the user passes in --verbose or --quiet, each help entry could handle that
171
+ # differently. Who are we to judge?
172
+ # @return [String] the help text for the entry.
173
+ def text(options = {})
174
+ Maruku.new(super, {}).to_ansi
175
+ end
176
+ end
177
+
178
+ ##
179
+ # Represents a help entry that filters its help text through ERB before returning.
180
+ #
181
+ # This is useful because some entries might have programmatic logic to them -
182
+ # for example, the built in "commands" entry lists all the commands in the
183
+ # user's current workflow. That requires logic, and while we used to simply
184
+ # have that be its own class, we can now stuff it in an ERB file.
185
+ #
186
+ # Note: if you want to use pretty text in an ERB entry, you will have to use
187
+ # ruby code to do so. Use the following shortcuts:
188
+ #
189
+ # <%= "Ampfiles".bold.underline %> # bolds and underlines
190
+ # <%= "some.code()".black.on_green %> # changes to black and sets green bg color
191
+ #
192
+ # See our extensions to the String class for more.
193
+ class ErbHelpEntry < HelpEntry
194
+ ##
195
+ # Returns the help text to display for this entry.
196
+ #
197
+ # For an ERB entry, we run ERB on the text in the entry, while also exposing the
198
+ # options variable as local, so the ERB can access the user's runtime options.
199
+ #
200
+ # @param [Hash] options the options for the process - that way the help commands
201
+ # can access the user's run-time options and global configuration. For example,
202
+ # if the user passes in --verbose or --quiet, each help entry could handle that
203
+ # differently. Who are we to judge?
204
+ # @return [String] the help text for the entry.
205
+ def text(options = {})
206
+ full_text = super(options)
207
+
208
+ erb = ERB.new(full_text, 0, "-")
209
+ erb.result binding
210
+ end
211
+ end
212
+
213
+ ##
214
+ # Represents a command's help entry. All commands have one of these, and in fact,
215
+ # when the command is created, it creates a help entry to go with it.
216
+ #
217
+ # Commands are actually quite complicated, and themselves know how to educate
218
+ # users about their use, so we have surprisingly little logic in this class.
219
+ class CommandHelpEntry < HelpEntry
220
+ ##
221
+ # Creates a new command help entry. Differing arguments, because instead of
222
+ # text, we need the command itself. One might think: why not just pass in
223
+ # the command's help information instead? If you have a command object, you
224
+ # have command.help, no? Well, the reason is two-fold: the help information
225
+ # might be updated later, and there is more to printing a command's help entry
226
+ # than just the command.help() method.
227
+ #
228
+ # @param [String] name the name of the command
229
+ # @param [Amp::Command] command the command being represented.
230
+ def initialize(name, command)
231
+ super(name)
232
+ @command = command
233
+ end
234
+
235
+ ##
236
+ # Returns the help text to display for this entry.
237
+ #
238
+ # For a command-based entry, simply run its educate method, since commands know
239
+ # how to present their help information.
240
+ #
241
+ # @param [Hash] options the options for the process - that way the help commands
242
+ # can access the user's run-time options and global configuration. For example,
243
+ # if the user passes in --verbose or --quiet, each help entry could handle that
244
+ # differently. Who are we to judge?
245
+ # @return [String] the help text for the entry.
246
+ def text(options = {})
247
+ instantiated = @command.new
248
+ instantiated.collect_options([])
249
+ "#{@command.desc}\n#{instantiated.education}"
250
+ end
251
+
252
+ ##
253
+ # Describes the entry briefly, so if the user must pick, they have a decent
254
+ # shot at knowing what this entry is about. Hopefully.
255
+ #
256
+ # In the case of a command, grab the command's "desc" information.
257
+ #
258
+ # @return a description of the entry based on its content
259
+ def desc
260
+ %Q{a command help entry ("#{@command.desc}")}
261
+ end
262
+ end
263
+
264
+ ##
265
+ # The really public-facing part of the Help system - the Help's UI.
266
+ # This lets the outside world get at entries based on their names.
267
+ module HelpUI
268
+ extend self
269
+
270
+ ##
271
+ # Asks the UI system to print the entry with the given name, with the
272
+ # process's current options.
273
+ #
274
+ # This method is "smart" - it has to check to see what entries are
275
+ # available. If there's more than one with the provided name, it
276
+ # helps the user pick the appropriate entry.
277
+ #
278
+ # @param [String] name the name of the entry to print
279
+ # @param [Hash] options the process's options
280
+ def print_entry(name, options = {})
281
+ result = HelpRegistry[name.to_s]
282
+ case result.size
283
+ when 0
284
+ raise abort("Could not find help entry \"#{name}\"")
285
+ when 1
286
+ puts result.first.text(options)
287
+ when 2
288
+ UI.choose do |menu|
289
+ result.each do |entry|
290
+ menu.choice("#{name} - #{entry.desc}") { puts entry.text(options) }
291
+ end
292
+ end
293
+ end
294
+ end
295
+ end
296
+
297
+ ##
298
+ # A method that loads in the default entries for the help system.
299
+ # Normally, I'd just put this code in the module itself, or perhaps
300
+ # at the end of the file, but I'm experimenting with an approach
301
+ # where I try to minimize the bare code, leaving only the invocation
302
+ # of this method to sit in the module.
303
+ def self.load_default_entries
304
+ Dir[File.join(File.dirname(__FILE__), "entries", "**")].each do |file|
305
+ HelpEntry.from_file(file)
306
+ end
307
+ end
308
+
309
+ # Load the default entries.
310
+ self.load_default_entries
311
+ end
312
+ end