amp-front 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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