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.
- data/.document +5 -0
- data/.gitignore +24 -0
- data/Ampfile +3 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +36 -0
- data/LICENSE +20 -0
- data/README.md +50 -0
- data/Rakefile +64 -0
- data/VERSION +1 -0
- data/design_docs/commands.md +91 -0
- data/design_docs/dependencies.md +35 -0
- data/design_docs/plugins.md +47 -0
- data/features/amp.feature +8 -0
- data/features/amp_help.feature +36 -0
- data/features/amp_plugin_list.feature +10 -0
- data/features/step_definitions/amp-front_steps.rb +23 -0
- data/features/support/env.rb +4 -0
- data/lib/amp-front.rb +30 -0
- data/lib/amp-front/dispatch/commands/base.rb +158 -0
- data/lib/amp-front/dispatch/commands/builtin/help.rb +23 -0
- data/lib/amp-front/dispatch/commands/builtin/plugin.rb +24 -0
- data/lib/amp-front/dispatch/commands/validations.rb +171 -0
- data/lib/amp-front/dispatch/runner.rb +86 -0
- data/lib/amp-front/help/entries/__default__.erb +31 -0
- data/lib/amp-front/help/entries/ampfiles.md +42 -0
- data/lib/amp-front/help/entries/commands.erb +6 -0
- data/lib/amp-front/help/entries/new-commands.md +81 -0
- data/lib/amp-front/help/help.rb +312 -0
- data/lib/amp-front/plugins/base.rb +87 -0
- data/lib/amp-front/support/module_extensions.rb +92 -0
- data/lib/amp-front/third_party/maruku.rb +136 -0
- data/lib/amp-front/third_party/maruku/attributes.rb +227 -0
- data/lib/amp-front/third_party/maruku/defaults.rb +71 -0
- data/lib/amp-front/third_party/maruku/errors_management.rb +92 -0
- data/lib/amp-front/third_party/maruku/helpers.rb +260 -0
- data/lib/amp-front/third_party/maruku/input/charsource.rb +326 -0
- data/lib/amp-front/third_party/maruku/input/extensions.rb +69 -0
- data/lib/amp-front/third_party/maruku/input/html_helper.rb +189 -0
- data/lib/amp-front/third_party/maruku/input/linesource.rb +111 -0
- data/lib/amp-front/third_party/maruku/input/parse_block.rb +615 -0
- data/lib/amp-front/third_party/maruku/input/parse_doc.rb +234 -0
- data/lib/amp-front/third_party/maruku/input/parse_span_better.rb +746 -0
- data/lib/amp-front/third_party/maruku/input/rubypants.rb +225 -0
- data/lib/amp-front/third_party/maruku/input/type_detection.rb +147 -0
- data/lib/amp-front/third_party/maruku/input_textile2/t2_parser.rb +163 -0
- data/lib/amp-front/third_party/maruku/maruku.rb +33 -0
- data/lib/amp-front/third_party/maruku/output/to_ansi.rb +223 -0
- data/lib/amp-front/third_party/maruku/output/to_html.rb +991 -0
- data/lib/amp-front/third_party/maruku/output/to_markdown.rb +164 -0
- data/lib/amp-front/third_party/maruku/output/to_s.rb +56 -0
- data/lib/amp-front/third_party/maruku/string_utils.rb +191 -0
- data/lib/amp-front/third_party/maruku/structures.rb +167 -0
- data/lib/amp-front/third_party/maruku/structures_inspect.rb +87 -0
- data/lib/amp-front/third_party/maruku/structures_iterators.rb +61 -0
- data/lib/amp-front/third_party/maruku/textile2.rb +1 -0
- data/lib/amp-front/third_party/maruku/toc.rb +199 -0
- data/lib/amp-front/third_party/maruku/usage/example1.rb +33 -0
- data/lib/amp-front/third_party/maruku/version.rb +40 -0
- data/lib/amp-front/third_party/trollop.rb +766 -0
- data/spec/amp-front_spec.rb +25 -0
- data/spec/command_specs/base_spec.rb +123 -0
- data/spec/command_specs/command_spec.rb +97 -0
- data/spec/command_specs/help_spec.rb +33 -0
- data/spec/command_specs/spec_helper.rb +37 -0
- data/spec/command_specs/validations_spec.rb +267 -0
- data/spec/dispatch_specs/runner_spec.rb +116 -0
- data/spec/dispatch_specs/spec_helper.rb +15 -0
- data/spec/help_specs/help_entry_spec.rb +78 -0
- data/spec/help_specs/help_registry_spec.rb +77 -0
- data/spec/help_specs/spec_helper.rb +15 -0
- data/spec/plugin_specs/base_spec.rb +36 -0
- data/spec/plugin_specs/spec_helper.rb +15 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +33 -0
- data/spec/support_specs/module_extensions_spec.rb +104 -0
- data/spec/support_specs/spec_helper.rb +15 -0
- data/test/third_party_tests/test_trollop.rb +1181 -0
- 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
|