inochi 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/doc/setup.erb ADDED
@@ -0,0 +1,51 @@
1
+ <% chapter "Setup" do %>
2
+ <% section "Requirements" do %>
3
+ Your system needs the following software to run **<%= $project %>**.
4
+
5
+ | Software | Description | Notes |
6
+ | -------- | ----------- | ----- |
7
+ | [Ruby](http://ruby-lang.org) | Ruby language interpreter | Version 1.8.6 or 1.8.7 is required. |
8
+ | [RubyGems](http://rubygems.org) | Ruby packaging system | Version 1.0.0 or newer is required. |
9
+ | [Lynx](http://lynx.isc.org) | Text-mode web browser | Version 2.8.6 or newer is required to convert HTML into plain text. |
10
+ <% end %>
11
+
12
+ <% section "Installation" do %>
13
+ You can install **<%= $project %>** by running this command:
14
+
15
+ gem install <%= $program %>
16
+
17
+ To check whether the installation was sucessful, run this command:
18
+
19
+ <%= $program %> --version
20
+
21
+ If the installation was successful, you will see output like this:
22
+
23
+ <pre><%= `ruby bin/#{$program} --version` %></pre>
24
+
25
+ If you do not see such output, you may
26
+ <%= xref "License", "ask the author(s)" %> for help.
27
+ <% end %>
28
+
29
+ <% section "Manifest" do %>
30
+ You will see the following items inside **<%= $project %>**'s installation
31
+ directory, whose path you can determine by running this command:
32
+
33
+ <%= $program %> --version
34
+
35
+ * <tt>bin/</tt>
36
+
37
+ * <tt><%= $program %></tt> --- the main **<%= $project %>** executable.
38
+
39
+ * <tt>lib/</tt>
40
+
41
+ * <tt><%= $program %>.rb</tt> --- the main **<%= $project %>** library.
42
+
43
+ * <tt>doc/</tt>
44
+
45
+ * <tt>api/</tt> --- API reference documentation.
46
+
47
+ * <tt>index.erb</tt> --- source of this user manual.
48
+
49
+ * <tt>LICENSE</tt> --- copyright notice and legal conditions.
50
+ <% end %>
51
+ <% end %>
data/doc/theory.erb ADDED
@@ -0,0 +1,3 @@
1
+ <% chapter "Theory of operation" do %>
2
+ > TODO: explain how your project does what it does
3
+ <% end %>
data/doc/usage.erb ADDED
@@ -0,0 +1,367 @@
1
+ <% part "Usage" do %>
2
+ <% section "Command-line interface" do %>
3
+ When you run this command:
4
+
5
+ <%= $program %> --help
6
+
7
+ You will see this output:
8
+
9
+ <pre><%= verbatim `ruby bin/#{$program} --help` %></pre>
10
+
11
+ <% tip "Merging files with **kdiff3**" do %>
12
+ Instead of merging files by hand, you can transfer wanted changes between files semi-automatically using [kdiff3](http://kdiff3.sourceforge.net). Simply follow these instructions:
13
+
14
+ 1. Create a file named <tt>merge2</tt> with the following content:
15
+
16
+ #!/bin/sh
17
+
18
+ old_file=$1
19
+ shift
20
+
21
+ new_file=$1
22
+ shift
23
+
24
+ output_file=$1
25
+ shift
26
+
27
+ kdiff3 --auto "$old_file" "$new_file" --output "$output_file"
28
+
29
+ 2. Make the file executable:
30
+
31
+ chmod +x merge2
32
+
33
+ 3. Place the file in a directory that is listed in your `PATH` environment variable.
34
+
35
+ 4. Run **<%= $project %>** like this:
36
+
37
+ <%= $program %> -m merge2
38
+
39
+ Now **kdiff3** will be invoked to help you transfer your changes. When you are finished transferring changes, save the file and quit **kdiff3**. If you do not want to transfer any changes, simply quit **kdiff3** _without_ saving the file.
40
+ <% end %>
41
+ <% end %>
42
+
43
+ <% section "Ruby library interface" do %>
44
+ The [`Inochi` module](api/Inochi.html) has several class methods which provide a common configuration for various aspects of your project. These aspects, and their interactions with the `Inochi` module, are as follows:
45
+ * Your project's main library invokes the [`Inochi.init()` method](api/Inochi.html#init-class_method).
46
+ * Your project's main executable invokes the [`Inochi.main()` method](api/Inochi.html#main-class_method).
47
+ * Your project's <tt>Rakefile</tt> invokes the [`Inochi.rake()` method](api/Inochi.html#rake-class_method).
48
+ * Your project's user manual invokes the [`Inochi.book()` method](api/Inochi.html#book-class_method).
49
+ <% end %>
50
+
51
+ <% section "Tutorial" do %>
52
+ This tutorial shows how **<%= $project %>** is used to manage a hypothetical `WordCount` project throughout the various stages of its life.
53
+
54
+ <% section "Have a brilliant idea" do %>
55
+ It is 4am on Sunday morning. Unwilling to sleep, you have spent the past few hours programming obsessively.. Though your eyes grow heavy and your stomach churns from hunger, your mind charges forth with haste.
56
+
57
+ > Push on! Keep on!
58
+
59
+ Until at last, pushed far beyond its limit, your body overpowers your will and drags you into black unconsciousness.
60
+
61
+ *BEEP* *BEEP* *B*---
62
+
63
+ Half-asleep and violent from the sudden disturbance, you silence the bleeting alarm clock with vengeance. It is 2pm on Sunday afternoon.
64
+
65
+ Red beams of sunlight slip through the gaps in your curtains. It is a beautiful day, outside. *Outside*--- you think,
66
+
67
+ > What am I doing to myself?
68
+ >
69
+ > I've got to get *outside*.
70
+ >
71
+ > I've got to get *away*...
72
+ >
73
+ > Away from this computer... this... mental prison in which I toil night after night, like a souless machine.
74
+
75
+ Venturing into the courtyard outside your quarters, you find peace. A warm breeze graces you, sweeping your hair gently as a mother would. The bright sunlight penetrates your mind's eye as your thoughts fade...
76
+
77
+ Thoughts of tests to write, units to refactor, bugs to fix, options to document. They melt and mix and flow into nothingness.
78
+
79
+ All is clear. No thoughts. No more.
80
+
81
+ > No!
82
+
83
+ You collapse heavily onto the grassy earth beneath you. Breathing deeply, you sink into yourself and whisper
84
+
85
+ > It's okay.
86
+ >
87
+ > Just, let go.
88
+
89
+ and fall asleep.
90
+
91
+ You awaken that evening relaxed and refreshed. A brilliant idea for a new project enters your mind: the project will be a tool that counts the number of words in text file. And, the project can be accessed from Ruby via the `WordCount` module.
92
+
93
+ *However*, you must go to work the next morning, so there isn't much time. What can you do? Let's see how **<%= $project %>** can help us meet this challenge.
94
+ <% end %>
95
+
96
+ <%
97
+ require 'tempfile'
98
+ tmp = Tempfile.new($project).path
99
+ File.delete tmp
100
+ mkdir_p tmp
101
+
102
+ begin
103
+ old = Dir.pwd
104
+ cd tmp
105
+
106
+ main_executable = 'bin/word_count'
107
+ %>
108
+ <% section "Generate your project" do %>
109
+ Give life to your new project:
110
+
111
+ <pre>
112
+ # inochi WordCount
113
+ <%= verbatim `ruby #{$install}/bin/inochi WordCount` %>
114
+ </pre>
115
+
116
+ Enter the <tt>word_count</tt> directory:
117
+
118
+ <pre>
119
+ # cd word_count
120
+ <% cd "word_count" %>
121
+ </pre>
122
+
123
+ View the available Rake tasks:
124
+
125
+ <pre>
126
+ # rake -T
127
+ <%= verbatim `rake -T` %>
128
+ </pre>
129
+
130
+ Try the main project executable:
131
+
132
+ <pre>
133
+ <% command = main_executable %>
134
+ # ruby <%= command %>
135
+ <%= verbatim `ruby #{command}` %>
136
+ </pre>
137
+
138
+ See usage information:
139
+
140
+ <pre>
141
+ <% command = "#{main_executable} --help" %>
142
+ # ruby <%= command %>
143
+ <%= verbatim `ruby #{command}` %>
144
+ </pre>
145
+
146
+ See project & version information:
147
+
148
+ <pre>
149
+ <% command = "#{main_executable} --version" %>
150
+ # ruby <%= command %>
151
+ <%= verbatim `ruby #{command}` %>
152
+ </pre>
153
+
154
+ See the user manual:
155
+
156
+ <pre>
157
+ # rake doc:man 2>/dev/null
158
+
159
+ <% command = "#{main_executable} --manual" %>
160
+ # ruby <%= command %>
161
+ </pre>
162
+
163
+ The manual will now appear in your default web browser.
164
+ <% end %>
165
+
166
+ <% section "Configure your project" do %>
167
+ <%= xref "Ruby library interface" %> lists and documents the interactions between your project and **<%= $project %>**. These points of interaction are illustrated in the following sections.
168
+
169
+ <% section "Project information" do %>
170
+ <% license_file = 'LICENSE' %>
171
+
172
+ Open the <tt><%= license_file %></tt> file, which contains the open source [ISC license](http://www.opensource.org/licenses/isc-license.txt) by default, and add a copyright notice with your name and (optional) email address:
173
+
174
+ <pre>
175
+ <%= verbatim File.read(license_file) %>
176
+ </pre>
177
+
178
+ <% main_library = 'lib/word_count.rb' %>
179
+
180
+ Open the main project library file <tt><%= main_library %></tt> and fill in the blanks:
181
+
182
+ <code>
183
+ <%= verbatim File.read(main_library) %>
184
+ </code>
185
+ <% end %>
186
+
187
+ <% section "Project executable" do %>
188
+ Open the <tt><%= main_executable %></tt> file and fill in the blanks:
189
+
190
+ <code>
191
+ <%= verbatim File.read(main_executable) %>
192
+ </code>
193
+ <% end %>
194
+
195
+ <% section "Rake tasks" do %>
196
+ <% rake_file = 'Rakefile' %>
197
+
198
+ Open the <tt><%= rake_file %></tt> and fill in the blanks:
199
+
200
+ <code>
201
+ <%= verbatim File.read(rake_file) %>
202
+ </code>
203
+ <% end %>
204
+
205
+ <% section "User manual" do %>
206
+ <%
207
+ whole = 'doc/index.erb'
208
+ parts = File.read(whole).
209
+ scan(/<%#\s*include\s+(\S+)/).flatten.map {|s| "doc/#{s}" }
210
+
211
+ files = [whole, *parts]
212
+ %>
213
+
214
+ The user manual's source file <tt><%= whole %></tt> subdivides its content into several smaller files, according to topic, for easier editing and maintenance. These files are processed by the [<%= ERBook::PROJECT %>](<%= ERBook::WEBSITE %>) program's [XHTML format](<%= ERBook::DOCSITE %>#xhtml) to produce the <tt>doc/index.xhtml</tt> file.
215
+
216
+ Open these source files and fill in the blanks:
217
+
218
+ <% files.each do |f| %>
219
+ <% paragraph "<tt>#{f}</tt>" do %>
220
+ <code lang="rhtml"><%= verbatim File.read(f) %></code>
221
+ <% end %>
222
+ <% end %>
223
+ <% end %>
224
+ <% end %>
225
+
226
+ <% section "Implement your project" do %>
227
+ Add the following code to the bottom of the main project library:
228
+
229
+ <code>
230
+ module WordCount
231
+ # Returns the number of words in the given input.
232
+ def WordCount.count input
233
+ input.to_s.split(/\W+/).length
234
+ end
235
+ end
236
+ </code>
237
+
238
+ Add the following code to the bottom of the main project executable:
239
+
240
+ <code>
241
+ input = ARGF.read
242
+ total = WordCount.count(input)
243
+ puts "There are #{total} words in the input."
244
+ </code>
245
+
246
+ <% paragraph "Goodbye `$LOAD_PATH`, hello `require()`" do %>
247
+ Notice that, in the Ruby files that you modified so far, there were no `$LOAD_PATH` manipulations and no explicit `require()` statements to pull in the various parts of your project. That is because **<%= $project %>** does this for you automatically.
248
+
249
+ Furthermore, you can always `require()` a sub-library anywhere in your project using its canonical path because **<%= $project %>** puts your main project libraries on the Ruby load path.
250
+
251
+ <% sub_library = 'word_count/odf/text' %>
252
+
253
+ For example, if your project has a sub-library, say, <tt>lib/<%= sub_library %>.rb</tt> that counts the number of words in an [OpenDocument Text](http://en.wikipedia.org/wiki/OpenDocument) document, then it would be loaded into the main project executable like this:
254
+
255
+ <code>
256
+ require '<%= sub_library %>'
257
+ </code>
258
+
259
+ Regardless of whether a sub-library is used within your project itself or from within an external application, we always `require()` the sub-library using the same canonical path.
260
+ <% end %>
261
+ <% end %>
262
+
263
+ <% section "Test your project" do %>
264
+ > TODO: show how to write a unit test for the code
265
+
266
+ > TODO: integrate minitest tasks into Inochi.rake()
267
+ <% end %>
268
+
269
+ <% section "Publish your project" do %>
270
+ This command performs all of the automated steps described in the following sections:
271
+
272
+ <pre>
273
+ # rake pub
274
+ </pre>
275
+
276
+ <% section "Build a RubyGem" do %>
277
+ Build a RubyGem by running:
278
+
279
+ <pre>
280
+ # rake pak
281
+ <%= `rake pak` %>
282
+ </pre>
283
+
284
+ See the RubyGem contents:
285
+
286
+ <pre>
287
+ # gem spec pkg/*.gem
288
+ <code lang="yaml"><%= `gem spec pkg/*.gem`.rstrip %></code>
289
+ </pre>
290
+ <% end %>
291
+
292
+ <% section "Publish a RubyGem" do %>
293
+ You must first register your project on [RubyForge](http://rubyforge.org) before you can publish a RubyGem. If your RubyForge project name is different from your actual project name, then you should pass the `:rubyforge_project` and `:rubyforge_section` options to the [`Inochi.rake()` method](api/Inochi.html#rake-class_method)).
294
+
295
+ Publish a RubyGem by running:
296
+
297
+ <pre>
298
+ # rake pub:pak
299
+ </pre>
300
+ <% end %>
301
+
302
+ <% section "Announce a release" do %>
303
+ You must first provide your <%= xref "Login information" %> to **<%= $project %>**. If you do not want to do this, then see <%= xref "Manual release announcement" %>.
304
+
305
+ Announce a release by running:
306
+
307
+ <pre>
308
+ # rake pub:ann
309
+ </pre>
310
+
311
+ <% paragraph "Login information" do %>
312
+ In order to automate the announcement of releases, **<%= $project %>** needs to know your login information for the [RAA (Ruby Application Archive)](http://raa.ruby-lang.org) and [RubyForum](http://www.ruby-forum.com/forum/4), which serves as a gateway to the [ruby-talk mailing list](http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/).
313
+
314
+ <% logins_file = "~/.config/inochi/logins.yaml" %>
315
+
316
+ This information is expected to be stored in a <tt><%= logins_file %></tt> file (this location can be overridden by passing the `:logins_file` option to the [`Inochi.rake()` method](api/Inochi.html#rake-class_method)), where <tt>~</tt> denotes the path to your home directory. This file is a [YAML](http://www.yaml.org) document containing the following parameters:
317
+
318
+ <code lang="yaml">
319
+ www.ruby-forum.com:
320
+ user: YOUR_USERNAME_HERE
321
+ pass: YOUR_PASSWORD_HERE
322
+
323
+ raa.ruby-lang.org:
324
+ pass: YOUR_PASSWORD_HERE
325
+ </code>
326
+
327
+ For better security, you should ensure that this file is only readable and writable by you and is not accessible by anyone else. In a UNIX environment, this can be accomplished by running the following command:
328
+
329
+ # chmod 0600 <%= logins_file %>
330
+
331
+ <% end %>
332
+
333
+ <% section "Manual release announcement" do %>
334
+ Build release announcements by running:
335
+
336
+ <pre>
337
+ # rake ann
338
+ <%= `rake ann` %>
339
+ </pre>
340
+
341
+ This produces the following files in your project directory:
342
+ <% Dir['ANN*'].each do |f| %>
343
+ * <tt><%= f %></tt>
344
+ <% end %>
345
+
346
+ Now you can manually announce your release using these files.
347
+ <% end %>
348
+ <% end %>
349
+
350
+ <% section "Publish the documentation" do %>
351
+ Publish the user manual and API documentation by running:
352
+
353
+ <pre>
354
+ # rake pub:doc
355
+ </pre>
356
+
357
+ If your documentation website (see the `:docsite` option for the [`Inochi.init()` method](api/Inochi.html#init-class_method)) is hosted on RubyForge, then the above command will automatically upload your project's documentation to the correct place.
358
+ <% end %>
359
+ <% end %>
360
+ <%
361
+ ensure
362
+ cd old
363
+ rm_rf tmp
364
+ end
365
+ %>
366
+ <% end %>
367
+ <% end %>
data/lib/inochi.rb ADDED
@@ -0,0 +1,16 @@
1
+ require File.join(File.dirname(__FILE__), 'inochi', 'inochi')
2
+
3
+ Inochi.init :Inochi,
4
+ :version => '0.0.0',
5
+ :release => '2009-01-19',
6
+ :tagline => 'Gives life to RubyGems-based software',
7
+ :website => 'http://snk.tuxfamily.org/lib/inochi',
8
+ :require => {
9
+ 'rubyforge' => '~> 1', # for publishing gems to RubyForge
10
+ 'mechanize' => '~> 0', # for automating web browsing
11
+ 'trollop' => '~> 1', # for parsing command-line options
12
+ 'erbook' => '~> 6', # for processing the user manual
13
+ 'launchy' => '~> 0', # for launching a web browser
14
+ 'yard' => nil, # for generating API documentation
15
+ 'addressable' => '~> 2', # for parsing URIs properly
16
+ }
@@ -0,0 +1,1000 @@
1
+ require 'rubygems'
2
+
3
+ module Inochi
4
+ class << self
5
+ ##
6
+ # Establishes your project in Ruby's runtime environment by defining
7
+ # the project module (which serves as a namespace for all code in the
8
+ # project) and providing a common configuration for the project module:
9
+ #
10
+ # * Adds the project lib/ directory to the Ruby load path.
11
+ #
12
+ # * Defines the INOCHI constant in the project module. This constant
13
+ # contains the effective configuration parameters (@see project_config).
14
+ #
15
+ # * Defines all configuration parameters as constants in the project module.
16
+ #
17
+ # This method must be invoked from immediately within (that is, not from
18
+ # within any of its descendant directories) the project lib/ directory.
19
+ # Ideally, this method would be invoked from the main project library.
20
+ #
21
+ # @param [Symbol] project_symbol
22
+ # Name of the Ruby constant which serves
23
+ # as a namespace for the entire project.
24
+ #
25
+ # @param [Hash] project_config
26
+ # Project configuration parameters:
27
+ #
28
+ # [String] :project =>
29
+ # Name of the project.
30
+ #
31
+ # The default value is the value of the project_symbol parameter.
32
+ #
33
+ # [String] :tagline =>
34
+ # An enticing, single line description of the project.
35
+ #
36
+ # The default value is an empty string.
37
+ #
38
+ # [String] :website =>
39
+ # URL of the published project website.
40
+ #
41
+ # The default value is an empty string.
42
+ #
43
+ # [String] :docsite =>
44
+ # URL of the published user manual.
45
+ #
46
+ # The default value is the same value as the :website parameter.
47
+ #
48
+ # [String] :program =>
49
+ # Name of the main project executable.
50
+ #
51
+ # The default value is the value of the :project parameter
52
+ # in lowercase and CamelCase converted into snake_case.
53
+ #
54
+ # [String] :version =>
55
+ # Version of the project.
56
+ #
57
+ # The default value is "0.0.0".
58
+ #
59
+ # [String] :release =>
60
+ # Date when this version was released.
61
+ #
62
+ # The default value is the current time.
63
+ #
64
+ # [String] :display =>
65
+ # How the project name should be displayed.
66
+ #
67
+ # The default value is the project name and version together.
68
+ #
69
+ # [String] :install =>
70
+ # Path to the directory which contains the project.
71
+ #
72
+ # The default value is one directory above the parent
73
+ # directory of the file from which this method was called.
74
+ #
75
+ # [Hash] :require =>
76
+ # The names and version constraints of ruby gems required by
77
+ # this project. This information must be expressed as follows:
78
+ #
79
+ # * Each hash key must be the name of a ruby gem.
80
+ #
81
+ # * Each hash value must be either +nil+, a single version number
82
+ # requirement string (see Gem::Requirement) or an Array thereof.
83
+ #
84
+ # The default value is an empty Hash.
85
+ #
86
+ # @return [Module] The newly configured project module.
87
+ #
88
+ def init project_symbol, project_config = {}
89
+ project_module = fetch_project_module(project_symbol)
90
+
91
+ # this method is not re-entrant
92
+ @already_seen ||= []
93
+ return project_module if @already_seen.include? project_module
94
+ @already_seen << project_module
95
+
96
+ # put project on Ruby load path
97
+ project_file = File.expand_path(first_caller_file)
98
+ project_libs = File.dirname(project_file)
99
+ $LOAD_PATH.unshift project_libs
100
+
101
+ # supply configuration defaults
102
+ project_config[:project] ||= project_symbol.to_s
103
+ project_config[:tagline] ||= ''
104
+ project_config[:version] ||= '0.0.0'
105
+ project_config[:release] ||= Time.now.strftime('%F')
106
+ project_config[:website] ||= ''
107
+ project_config[:docsite] ||= project_config[:website]
108
+ project_config[:display] ||= "#{project_config[:project]} #{project_config[:version]}"
109
+ project_config[:program] ||= calc_program_name(project_symbol)
110
+ project_config[:install] ||= File.dirname(project_libs)
111
+ project_config[:require] ||= {}
112
+
113
+ # establish gem version dependencies and
114
+ # sanitize the values while we're at it
115
+ src = project_config[:require].dup
116
+ dst = project_config[:require].clear
117
+
118
+ src.each_pair do |gem_name, version_reqs|
119
+ gem_name = gem_name.to_s
120
+ version_reqs = [version_reqs].flatten.compact
121
+
122
+ dst[gem_name] = version_reqs
123
+ gem gem_name, *version_reqs
124
+ end
125
+
126
+ # make configuration parameters available as constants
127
+ project_config[:inochi] = project_config
128
+
129
+ project_config.each_pair do |param, value|
130
+ project_module.const_set param.to_s.upcase, value
131
+ end
132
+
133
+ project_module
134
+ end
135
+
136
+ ##
137
+ # Provides a common configuration for the main project executable:
138
+ #
139
+ # * The program description (the sequence of non-blank lines at the
140
+ # top of the file in which this method is invoked) is properly
141
+ # formatted and displayed at the top of program's help information.
142
+ #
143
+ # * The program version information is fetched from the project module
144
+ # and formatted in YAML fashion for easy consumption by other tools.
145
+ #
146
+ # * A list of command-line options is displayed at
147
+ # the bottom of the program's help information.
148
+ #
149
+ # @param [Symbol] project_symbol
150
+ # Name of the Ruby constant which serves
151
+ # as a namespace for the entire project.
152
+ #
153
+ # @param trollop_args
154
+ # Optional arguments for Trollop::options().
155
+ #
156
+ # @param trollop_config
157
+ # Optional block argument for Trollop::options().
158
+ #
159
+ # @return The result of Trollop::options().
160
+ #
161
+ def main project_symbol, *trollop_args, &trollop_config
162
+ program_file = first_caller_file
163
+ program_home = File.dirname(File.dirname(program_file))
164
+
165
+ # load the project module
166
+ program_name = File.basename(program_home)
167
+
168
+ require File.join(program_home, 'lib', program_name)
169
+ project_module = fetch_project_module(project_symbol)
170
+
171
+ # parse command-line options
172
+ require 'trollop'
173
+
174
+ options = Trollop.options(*trollop_args) do
175
+
176
+ # show project description
177
+ text "#{project_module::PROJECT} - #{project_module::TAGLINE}"
178
+ text ''
179
+
180
+ # show program description
181
+ text File.read(program_file)[/\A.*?^$\n/m]. # grab the header
182
+ gsub(/^# ?/, ''). # strip the comment markers
183
+ sub(/\A!.*?\n/, '').lstrip # omit the shebang line
184
+ text ''
185
+
186
+ instance_eval(&trollop_config) if trollop_config
187
+
188
+ # show version information
189
+ version %w[PROJECT VERSION RELEASE WEBSITE INSTALL].map {|c|
190
+ "#{c.downcase}: #{project_module.const_get c}"
191
+ }.join("\n")
192
+
193
+ opt :manual, 'Show the user manual'
194
+ end
195
+
196
+ if options[:manual]
197
+ require 'launchy'
198
+ Launchy::Browser.run "#{project_module::INSTALL}/doc/index.xhtml"
199
+ exit
200
+ end
201
+
202
+ options
203
+ end
204
+
205
+ ##
206
+ # Provides Rake tasks for packaging, publishing, and announcing your project.
207
+ #
208
+ # * An AUTHORS constant (which has the form "[[name, info]]"
209
+ # where "name" is the name of a copyright holder and "info" is
210
+ # their contact information) is added to the project module.
211
+ #
212
+ # This information is extracted from copyright notices in
213
+ # the project license file. NOTE that the first copyright
214
+ # notice must correspond to the primary project maintainer.
215
+ #
216
+ # Copyright notices must be in the following form:
217
+ #
218
+ # Copyright YEAR HOLDER <EMAIL>
219
+ #
220
+ # Where HOLDER is the name of the copyright holder, YEAR is the year
221
+ # when the copyright holder first began working on the project, and
222
+ # EMAIL is (optional) the email address of the copyright holder.
223
+ #
224
+ # @param [Symbol] project_symbol
225
+ # Name of the Ruby constant which serves
226
+ # as a namespace for the entire project.
227
+ #
228
+ # @param [Hash] options
229
+ # Additional method parameters, which are all optional:
230
+ #
231
+ # [String] :license_file =>
232
+ # Path (relative to the main project directory which contains the
233
+ # project Rakefile) to the file which contains the project license.
234
+ #
235
+ # The default value is "LICENSE".
236
+ #
237
+ # [String] :logins_file =>
238
+ # Path to the YAML file which contains login
239
+ # information for publishing release announcements.
240
+ #
241
+ # The default value is "~/.config/inochi/logins.yaml"
242
+ # where "~" is the path to your home directory.
243
+ #
244
+ # [String] :rubyforge_project =>
245
+ # Name of the RubyForge project where
246
+ # release packages will be published.
247
+ #
248
+ # The default value is the value of the PROGRAM constant.
249
+ #
250
+ # [String] :rubyforge_section =>
251
+ # Name of the RubyForge project's File Release System
252
+ # section where release packages will be published.
253
+ #
254
+ # The default value is the value of the :rubyforge_project parameter.
255
+ #
256
+ # [String] :raa_project =>
257
+ # Name of the RAA (Ruby Application Archive) entry for this project.
258
+ #
259
+ # The default value is the value of the PROGRAM constant.
260
+ #
261
+ # [String] :upload_target =>
262
+ # Where to upload the project documentation.
263
+ # See "destination" in the rsync manual.
264
+ #
265
+ # The default value is nil.
266
+ #
267
+ # [String] :upload_delete =>
268
+ # Delete unknown files at the upload target location?
269
+ #
270
+ # The default value is false.
271
+ #
272
+ # [Array] :upload_options =>
273
+ # Additional command-line arguments to the rsync command.
274
+ #
275
+ # The default value is an empty array.
276
+ #
277
+ # @param gem_config
278
+ # Block that is passed to Gem::specification.new()
279
+ # for additonal gem configuration.
280
+ #
281
+ # @yieldparam [Gem::Specification] gem_spec the gem specification
282
+ #
283
+ def rake project_symbol, options = {}, &gem_config
284
+ program_file = first_caller_file
285
+ program_home = File.dirname(program_file)
286
+
287
+ # load the project module
288
+ program_name = File.basename(program_home)
289
+
290
+ require File.join('lib', program_name)
291
+ project_module = fetch_project_module(project_symbol)
292
+
293
+ # supply default options
294
+ options[:rubyforge_project] ||= program_name
295
+ options[:rubyforge_section] ||= program_name
296
+ options[:raa_project] ||= program_name
297
+ options[:license_file] ||= 'LICENSE'
298
+ options[:logins_file] ||= File.join(ENV['HOME'], '.config', 'inochi', 'logins.yaml')
299
+ options[:upload_delete] ||= false
300
+ options[:upload_options] ||= []
301
+
302
+ # add AUTHORS constant to the project module
303
+ license = File.read(options[:license_file])
304
+
305
+ copyright_holders =
306
+ license.scan(/Copyright.*?\d+\s+(.*)/).flatten.
307
+ map {|s| (s =~ /\s*<(.*?)>/) ? [$`, $1] : [s, ''] }
308
+
309
+ project_module.const_set :AUTHORS, copyright_holders
310
+
311
+ require 'rake/clean'
312
+
313
+ hide_rake_task = lambda do |name|
314
+ Rake::Task[name].instance_variable_set :@comment, nil
315
+ end
316
+
317
+ # documentation
318
+ desc 'Build all documentation.'
319
+ task :doc => %w[ doc:api doc:man ]
320
+
321
+ # user manual
322
+ doc_man_src = 'doc/index.erb'
323
+ doc_man_dst = 'doc/index.xhtml'
324
+ doc_man_deps = FileList['doc/*.erb']
325
+
326
+ doc_man_doc = nil
327
+ task :doc_man_doc => doc_man_src do
328
+ unless doc_man_doc
329
+ require 'erbook' unless defined? ERBook
330
+ doc_man_txt = File.read(doc_man_src)
331
+ doc_man_doc = ERBook::Document.new(:xhtml, doc_man_txt, doc_man_src, :unindent => true)
332
+ end
333
+ end
334
+
335
+ desc 'Build the user manual.'
336
+ task 'doc:man' => doc_man_dst
337
+
338
+ file doc_man_dst => doc_man_deps do
339
+ Rake::Task[:doc_man_doc].invoke
340
+ File.write doc_man_dst, doc_man_doc
341
+ end
342
+
343
+ CLOBBER.include doc_man_dst
344
+
345
+ # API reference
346
+ doc_api_dst = 'doc/api'
347
+
348
+ desc 'Build API reference.'
349
+ task 'doc:api' => doc_api_dst
350
+
351
+ require 'yard'
352
+ YARD::Rake::YardocTask.new doc_api_dst do |t|
353
+ t.options.push '--protected',
354
+ '--output-dir', doc_api_dst,
355
+ '--readme', options[:license_file]
356
+
357
+ task doc_api_dst => options[:license_file]
358
+ end
359
+
360
+ hide_rake_task[doc_api_dst]
361
+
362
+ CLEAN.include '.yardoc'
363
+ CLOBBER.include doc_api_dst
364
+
365
+ # announcements
366
+ desc 'Build all release announcements.'
367
+ task :ann => %w[ ann:feed ann:html ann:text ann:mail ]
368
+
369
+ # it has long been a tradition to use an "[ANN]" prefix
370
+ # when announcing things on the ruby-talk mailing list
371
+ ann_prefix = '[ANN] '
372
+ ann_subject = ann_prefix + project_module::DISPLAY
373
+ ann_project = ann_prefix + project_module::PROJECT
374
+
375
+ # fetch the project summary from user manual
376
+ ann_nfo_doc = nil
377
+ task :ann_nfo_doc => :doc_man_doc do
378
+ ann_nfo_doc = $project_summary_node
379
+ end
380
+
381
+ # fetch release notes from user manual
382
+ ann_rel_doc = nil
383
+ task :ann_rel_doc => :doc_man_doc do
384
+ unless ann_rel_doc
385
+ if parent = $project_history_node
386
+ if child = parent.children.first
387
+ ann_rel_doc = child
388
+ else
389
+ raise 'The "project_history" node in the user manual lacks child nodes.'
390
+ end
391
+ else
392
+ raise 'The user manual lacks a "project_history" node.'
393
+ end
394
+ end
395
+ end
396
+
397
+ # build release notes in HTML and plain text
398
+ # converts the given HTML into plain text. we do this using
399
+ # lynx because (1) it outputs a list of all hyperlinks used
400
+ # in the HTML document and (2) it runs on all major platforms
401
+ convert_html_to_text = lambda do |html|
402
+ require 'tempfile'
403
+
404
+ begin
405
+ # lynx's -dump option requires a .html file
406
+ tmp_file = Tempfile.new(Inochi::PROGRAM).path + '.html'
407
+
408
+ File.write tmp_file, html
409
+ text = `lynx -dump #{tmp_file} -width 70`
410
+ ensure
411
+ File.delete tmp_file
412
+ end
413
+
414
+ # improve readability of list items that span multiple
415
+ # lines by adding a blank line between such items
416
+ text.gsub! %r{^( *[^\*\s].*)(\r?\n)( *\* \S)}, '\1\2\2\3'
417
+
418
+ text
419
+ end
420
+
421
+ # binds relative addresses in the given HTML to the project docsite
422
+ resolve_html_links = lambda do |html|
423
+ # resolve relative URLs into absolute URLs
424
+ # see http://en.wikipedia.org/wiki/URI_scheme#Generic_syntax
425
+ require 'addressable/uri'
426
+ uri = Addressable::URI.parse(project_module::DOCSITE)
427
+ doc_url = uri.to_s
428
+ dir_url = uri.path =~ %r{/$|^$} ? doc_url : File.dirname(doc_url)
429
+
430
+ html.to_s.gsub %r{(href=|src=)(.)(.*?)(\2)} do |match|
431
+ a, b = $1 + $2, $3.to_s << $4
432
+
433
+ case $3
434
+ when %r{^[[:alpha:]][[:alnum:]\+\.\-]*://} # already absolute
435
+ match
436
+
437
+ when /^#/
438
+ a << File.join(doc_url, b)
439
+
440
+ else
441
+ a << File.join(dir_url, b)
442
+ end
443
+ end
444
+ end
445
+
446
+ ann_html = nil
447
+ task :ann_html => [:doc_man_doc, :ann_nfo_doc, :ann_rel_doc] do
448
+ unless ann_html
449
+ ann_html = %{
450
+ <center>
451
+ <h1>#{project_module::DISPLAY}</h1>
452
+ <p>#{project_module::TAGLINE}</p>
453
+ <p>#{project_module::WEBSITE}</p>
454
+ </center>
455
+ #{ann_nfo_doc}
456
+ #{ann_rel_doc}
457
+ }
458
+
459
+ # remove heading navigation menus
460
+ ann_html.gsub! %r{<div class="nav"[^>]*>(.*?)</div>}, ''
461
+
462
+ ann_html = resolve_html_links[ann_html]
463
+ end
464
+ end
465
+
466
+ ann_text = nil
467
+ task :ann_text => :ann_html do
468
+ unless ann_text
469
+ ann_text = convert_html_to_text[ann_html]
470
+ end
471
+ end
472
+
473
+ ann_nfo_text = nil
474
+ task :ann_nfo_text => :ann_nfo_doc do
475
+ unless ann_nfo_text
476
+ ann_nfo_html = resolve_html_links[ann_nfo_doc]
477
+ ann_nfo_text = convert_html_to_text[ann_nfo_html]
478
+ end
479
+ end
480
+
481
+ # HTML
482
+ ann_html_dst = 'ANN.html'
483
+
484
+ desc "Build HTML announcement: #{ann_html_dst}"
485
+ task 'ann:html' => ann_html_dst
486
+
487
+ file ann_html_dst => doc_man_deps do
488
+ Rake::Task[:ann_html].invoke
489
+ File.write ann_html_dst, ann_html
490
+ end
491
+
492
+ CLEAN.include ann_html_dst
493
+
494
+ # RSS feed
495
+ ann_feed_dst = 'doc/ann.xml'
496
+
497
+ desc "Build RSS announcement: #{ann_feed_dst}"
498
+ task 'ann:feed' => ann_feed_dst
499
+
500
+ file ann_feed_dst => doc_man_deps do
501
+ require 'time'
502
+ require 'rss/maker'
503
+
504
+ feed = RSS::Maker.make('2.0') do |feed|
505
+ feed.channel.title = ann_project
506
+ feed.channel.link = project_module::WEBSITE
507
+ feed.channel.description = project_module::TAGLINE
508
+
509
+ Rake::Task[:ann_rel_doc].invoke
510
+ Rake::Task[:ann_html].invoke
511
+
512
+ item = feed.items.new_item
513
+ item.title = ann_rel_doc.title
514
+ item.link = project_module::DOCSITE + '#' + ann_rel_doc.here_frag
515
+ item.date = Time.parse(item.title)
516
+ item.description = ann_html
517
+ end
518
+
519
+ File.write ann_feed_dst, feed
520
+ end
521
+
522
+ CLOBBER.include ann_feed_dst
523
+
524
+ # plain text
525
+ ann_text_dst = 'ANN.txt'
526
+
527
+ desc "Build plain text announcement: #{ann_text_dst}"
528
+ task 'ann:text' => ann_text_dst
529
+
530
+ file ann_text_dst => doc_man_deps do
531
+ Rake::Task[:ann_text].invoke
532
+ File.write ann_text_dst, ann_text
533
+ end
534
+
535
+ CLEAN.include ann_text_dst
536
+
537
+ # e-mail
538
+ ann_mail_dst = 'ANN.eml'
539
+
540
+ desc "Build e-mail announcement: #{ann_mail_dst}"
541
+ task 'ann:mail' => ann_mail_dst
542
+
543
+ file ann_mail_dst => doc_man_deps do
544
+ File.open ann_mail_dst, 'w' do |f|
545
+ require 'time'
546
+ f.puts "Date: #{Time.now.rfc822}"
547
+
548
+ f.puts 'To: ruby-talk@ruby-lang.org'
549
+ f.puts 'From: "%s" <%s>' % project_module::AUTHORS.first
550
+ f.puts "Subject: #{ann_subject}"
551
+
552
+ Rake::Task[:ann_text].invoke
553
+ f.puts '', ann_text
554
+ end
555
+ end
556
+
557
+ CLEAN.include ann_mail_dst
558
+
559
+ # packaging
560
+ desc 'Build a release.'
561
+ task :pak => [:clobber, :doc] do
562
+ sh $0, 'package'
563
+ end
564
+ CLEAN.include 'pkg'
565
+
566
+ # ruby gem
567
+ require 'rake/gempackagetask'
568
+
569
+ gem = Gem::Specification.new do |gem|
570
+ authors = project_module::AUTHORS
571
+
572
+ if author = authors.first
573
+ gem.author, gem.email = author
574
+ end
575
+
576
+ if authors.length > 1
577
+ gem.authors = authors.map {|name, mail| name }
578
+ end
579
+
580
+ gem.rubyforge_project = options[:rubyforge_project]
581
+
582
+ # XXX: In theory, `gem.name` should be assigned to
583
+ # ::PROJECT instead of ::PROGRAM
584
+ #
585
+ # In practice, PROJECT may contain non-word
586
+ # characters and may also contain a mixture
587
+ # of lowercase and uppercase letters.
588
+ #
589
+ # This makes it difficult for people to
590
+ # install the project gem because they must
591
+ # remember the exact spelling used in
592
+ # `gem.name` when running `gem install ____`.
593
+ #
594
+ # For example, consider the "RedCloth" gem.
595
+ #
596
+ gem.name = project_module::PROGRAM
597
+
598
+ gem.version = project_module::VERSION
599
+ gem.summary = project_module::TAGLINE
600
+ gem.description = gem.summary
601
+ gem.homepage = project_module::WEBSITE
602
+ gem.files = FileList['**/*'].exclude('_darcs') - CLEAN
603
+ gem.executables = project_module::PROGRAM
604
+ gem.has_rdoc = true
605
+
606
+ unless project_module == Inochi
607
+ gem.add_dependency 'inochi', Inochi::VERSION
608
+ end
609
+
610
+ project_module::REQUIRE.each_pair do |gem_name, version_reqs|
611
+ gem.add_dependency gem_name, *version_reqs
612
+ end
613
+
614
+ # additional configuration is done by user
615
+ yield gem if gem_config
616
+ end
617
+
618
+ Rake::GemPackageTask.new(gem).define
619
+
620
+ # XXX: hide the tasks defined by the above gem packaging library
621
+ %w[gem package repackage clobber_package].each {|t| hide_rake_task[t] }
622
+
623
+ # releasing
624
+ desc 'Publish a release.'
625
+ task 'pub' => %w[ pub:pak pub:doc pub:ann ]
626
+
627
+ # connect to RubyForge services
628
+ pub_forge = nil
629
+ pub_forge_project = options[:rubyforge_project]
630
+ pub_forge_section = options[:rubyforge_section]
631
+
632
+ task :pub_forge do
633
+ require 'rubyforge'
634
+ pub_forge = RubyForge.new
635
+ pub_forge.configure('release_date' => project_module::RELEASE)
636
+
637
+ unless pub_forge.autoconfig['group_ids'].key? pub_forge_project
638
+ raise "The #{pub_forge_project.inspect} project was not recognized by the RubyForge client. Either specify a different RubyForge project by passing the :rubyforge_project option to Inochi.rake(), or ensure that the client is configured correctly (see `rubyforge --help` for help) and try again."
639
+ end
640
+
641
+ pub_forge.login
642
+ end
643
+
644
+ # documentation
645
+ desc 'Publish documentation to project website.'
646
+ task 'pub:doc' => [:doc, 'ann:feed'] do
647
+ target = options[:upload_target]
648
+
649
+ unless target
650
+ require 'addressable/uri'
651
+ docsite = Addressable::URI.parse(project_module::DOCSITE)
652
+
653
+ # provide uploading capability to websites hosted on RubyForge
654
+ if docsite.host.include? '.rubyforge.org'
655
+ target = "#{pub_forge.userconfig['username']}@rubyforge.org:#{File.join '/var/www/gforge-projects', options[:rubyforge_project], docsite.path}"
656
+ end
657
+ end
658
+
659
+ if target
660
+ cmd = ['rsync', '-auvz', 'doc/', "#{target}/"]
661
+ cmd.push '--delete' if options[:upload_delete]
662
+ cmd.concat options[:upload_options]
663
+
664
+ p cmd
665
+ sh(*cmd)
666
+ end
667
+ end
668
+
669
+ # announcement
670
+ desc 'Publish all announcements.'
671
+ task 'pub:ann' => %w[ pub:ann:forge pub:ann:raa pub:ann:talk ]
672
+
673
+ # login information
674
+ ann_logins_file = options[:logins_file]
675
+ ann_logins = nil
676
+
677
+ task :ann_logins do
678
+ ann_logins = begin
679
+ require 'yaml'
680
+ YAML.load_file ann_logins_file
681
+ rescue => e
682
+ warn "Could not read login information from #{ann_logins_file.inspect}:"
683
+ warn e
684
+ warn "** You will NOT be able to publish release announcements! **"
685
+ {}
686
+ end
687
+ end
688
+
689
+ desc 'Announce to RubyForge news.'
690
+ task 'pub:ann:forge' => :pub_forge do
691
+ project = options[:rubyforge_project]
692
+
693
+ if group_id = pub_forge.autoconfig['group_ids'][project]
694
+ # check if this release was already announced
695
+ require 'mechanize'
696
+ www = WWW::Mechanize.new
697
+ page = www.get "http://rubyforge.org/news/?group_id=#{group_id}"
698
+
699
+ posts = (page/'//a[starts-with(./@href, "/forum/forum.php?forum_id=")]/text()').map {|e| e.to_s.gsub("\302\240", '').strip }
700
+
701
+ already_announced = posts.include? ann_subject
702
+
703
+ if already_announced
704
+ warn "This release was already announced to RubyForge news, so I will NOT announce it there again."
705
+ else
706
+ # make the announcement
707
+ Rake::Task[:ann_text].invoke
708
+ pub_forge.post_news project, ann_subject, ann_text
709
+
710
+ puts "Successfully announced to RubyForge news:"
711
+ puts page.uri
712
+ end
713
+ else
714
+ raise "Could not determine the group_id of the #{project.inspect} RubyForge project. Run `rubyforge config` and try again."
715
+ end
716
+ end
717
+
718
+ desc 'Announce to ruby-talk mailing list.'
719
+ task 'pub:ann:talk' => :ann_logins do
720
+ host = 'http://ruby-forum.com'
721
+ ruby_talk = 4 # ruby-talk forum ID
722
+
723
+ require 'mechanize'
724
+ www = WWW::Mechanize.new
725
+
726
+ # check if this release was already announced
727
+ already_announced =
728
+ begin
729
+ page = www.get "#{host}/forum/#{ruby_talk}", :filter => %{"#{ann_subject}"}
730
+
731
+ posts = (page/'//div[@class="forum"]//a[starts-with(./@href, "/topic/")]/text()').map {|e| e.to_s.strip }
732
+ posts.include? ann_subject
733
+ rescue
734
+ false
735
+ end
736
+
737
+ if already_announced
738
+ warn "This release was already announced to the ruby-talk mailing list, so I will NOT announce it there again."
739
+ else
740
+ # log in to RubyForum
741
+ page = www.get "#{host}/user/login"
742
+ form = page.forms.first
743
+
744
+ if login = ann_logins['www.ruby-forum.com']
745
+ form['name'] = login['user']
746
+ form['password'] = login['pass']
747
+ end
748
+
749
+ page = form.click_button # use the first submit button
750
+
751
+ if (page/'//a[@href="/user/logout"]').empty?
752
+ warn "Could not log in to RubyForum using the login information in #{ann_logins_file.inspect}, so I can NOT announce this release to the ruby-talk mailing list."
753
+ else
754
+ # make the announcement
755
+ page = www.get "#{host}/topic/new?forum_id=#{ruby_talk}"
756
+ form = page.forms.first
757
+
758
+ Rake::Task[:ann_text].invoke
759
+ form['post[subject]'] = ann_subject
760
+ form['post[text]'] = ann_text
761
+
762
+ form.checkboxes.first.check # enable email notification
763
+ page = form.submit
764
+
765
+ errors = [page/'//div[@class="error"]/text()'].flatten
766
+ if errors.empty?
767
+ puts "Successfully announced to ruby-talk mailing list:"
768
+ puts page.uri
769
+ else
770
+ warn "Could not announce to ruby-talk mailing list:"
771
+ warn errors.join("\n")
772
+ end
773
+ end
774
+ end
775
+ end
776
+
777
+ desc 'Announce to RAA (Ruby Application Archive).'
778
+ task 'pub:ann:raa' => :ann_logins do
779
+ show_page_error = lambda do |page, message|
780
+ warn "#{message}, so I can NOT announce this release to RAA:"
781
+ warn "#{(page/'h2').text} -- #{(page/'p').first.text.strip}"
782
+ end
783
+
784
+ resource = "#{options[:raa_project].inspect} project entry on RAA"
785
+
786
+ require 'mechanize'
787
+ www = WWW::Mechanize.new
788
+ page = www.get "http://raa.ruby-lang.org/update.rhtml?name=#{options[:raa_project]}"
789
+
790
+ if form = page.forms[1]
791
+ resource << " (owned by #{form.owner.inspect})"
792
+
793
+ Rake::Task[:ann_nfo_text].invoke
794
+ form['description'] = ann_nfo_text
795
+ form['description_style'] = 'Pre-formatted'
796
+ form['short_description'] = project_module::TAGLINE
797
+ form['version'] = project_module::VERSION
798
+ form['url'] = project_module::WEBSITE
799
+ form['pass'] = ann_logins['raa.ruby-lang.org']['pass']
800
+
801
+ page = form.submit
802
+
803
+ if page.title =~ /error/i
804
+ show_page_error[page, "Could not update #{resource}"]
805
+ else
806
+ puts "Successfully announced to RAA (Ruby Application Archive)."
807
+ end
808
+ else
809
+ show_page_error[page, "Could not access #{resource}"]
810
+ end
811
+ end
812
+
813
+ # release packages
814
+ desc 'Publish release packages to RubyForge.'
815
+ task 'pub:pak' => :pub_forge do
816
+ # check if this release was already published
817
+ version = project_module::VERSION
818
+ packages = pub_forge.autoconfig['release_ids'][pub_forge_section]
819
+
820
+ if packages and packages.key? version
821
+ warn "The release packages were already published, so I will NOT publish them again."
822
+ else
823
+ # create the FRS package section
824
+ unless pub_forge.autoconfig['package_ids'].key? pub_forge_section
825
+ pub_forge.create_package pub_forge_project, pub_forge_section
826
+ end
827
+
828
+ # publish the package to the section
829
+ uploader = lambda do |command, *files|
830
+ pub_forge.__send__ command, pub_forge_project, pub_forge_section, version, *files
831
+ end
832
+
833
+ Rake::Task[:pak].invoke
834
+ packages = Dir['pkg/*.[a-z]*']
835
+
836
+ unless packages.empty?
837
+ # NOTE: use the 'add_release' command ONLY for the first
838
+ # file because it creates a new sub-section on the
839
+ # RubyForge download page; we do not want one package
840
+ # per sub-section on the RubyForge download page!
841
+ #
842
+ uploader[:add_release, packages.shift]
843
+
844
+ unless packages.empty?
845
+ uploader[:add_file, *packages]
846
+ end
847
+
848
+ puts "Successfully published release packages to RubyForge."
849
+ end
850
+ end
851
+ end
852
+ end
853
+
854
+ ##
855
+ # Provides a common configuration for the project's user manual:
856
+ #
857
+ # * Assigns the title, subtitle, date, and authors for the document.
858
+ #
859
+ # You may override these assignments by reassigning these
860
+ # document parameters AFTER this method is invoked.
861
+ #
862
+ # Refer to the "document parameters" for the XHTML
863
+ # format in the "erbook" user manual for details.
864
+ #
865
+ # * Provides the project's configuration as global variables in the document.
866
+ #
867
+ # For example, <%= $version %> is the same as
868
+ # <%= project_module::VERSION %> in the document.
869
+ #
870
+ # * Defines a "project_summary" node for use in the document. The body
871
+ # of this node should contain a brief introduction to the project.
872
+ #
873
+ # * Defines a "project_history" node for use in the document. The body
874
+ # of this node should contain other nodes, each of which represent a
875
+ # single set of release notes for one of the project's releases.
876
+ #
877
+ # It is assumed that this method is called
878
+ # from within the Inochi.rake() environment.
879
+ #
880
+ # @param [Symbol] project_symbol
881
+ # Name of the Ruby constant which serves
882
+ # as a namespace for the entire project.
883
+ #
884
+ # @param [ERBook::Document::Template] book_template
885
+ # The eRuby template which serves as the documentation for the project.
886
+ #
887
+ def book project_symbol, book_template
888
+ project_module = fetch_project_module(project_symbol)
889
+
890
+ # provide project constants as global variables to the user manual
891
+ project_module::INOCHI.each_pair do |param, value|
892
+ eval "$#{param} = value", binding
893
+ end
894
+
895
+ # set document parameters for the user manual
896
+ $title = project_module::DISPLAY
897
+ $subtitle = project_module::TAGLINE
898
+ $feeds = { File.join(project_module::DOCSITE, 'ann.xml') => :rss }
899
+ $authors = Hash[
900
+ *project_module::AUTHORS.map do |name, addr|
901
+ # convert raw e-mail addresses into URLs for the erbook XHTML format
902
+ addr = "mailto:#{addr}" unless addr =~ /^\w+:/
903
+
904
+ [name, addr]
905
+ end.flatten
906
+ ]
907
+
908
+ class << book_template
909
+ def project_summary
910
+ raise ArgumentError, 'block must be given' unless block_given?
911
+ node do
912
+ $project_summary_node = @nodes.last
913
+ yield
914
+ end
915
+ end
916
+
917
+ def project_history
918
+ raise ArgumentError, 'block must be given' unless block_given?
919
+ node do
920
+ $project_history_node = @nodes.last
921
+ yield
922
+ end
923
+ end
924
+ end
925
+ end
926
+
927
+ ##
928
+ # Returns the name of the main program executable, which
929
+ # is the same as the project name fully in lowercase.
930
+ #
931
+ def calc_program_name project_symbol
932
+ camel_to_snake_case(project_symbol).downcase
933
+ end
934
+
935
+ ##
936
+ # Calculates the name of the project module from the given project name.
937
+ #
938
+ def calc_project_symbol project_name
939
+ name = project_name.to_s.gsub(/\W+/, '_').squeeze('_').gsub(/^_|_$/, '')
940
+ (name[0,1].upcase + name[1..-1]).to_sym
941
+ end
942
+
943
+ ##
944
+ # Transforms the given input from CamelCase to snake_case.
945
+ #
946
+ def camel_to_snake_case input
947
+ input = input.to_s.dup
948
+
949
+ # handle camel case like FooBar => Foo_Bar
950
+ while input.gsub!(/([a-z]+)([A-Z])(\w+)/) { $1 + '_' + $2 + $3 }
951
+ end
952
+
953
+ # handle abbreviations like XMLParser => XML_Parser
954
+ while input.gsub!(/([A-Z]+)([A-Z])([a-z]+)/) { $1 + '_' + $2 + $3 }
955
+ end
956
+
957
+ input
958
+ end
959
+
960
+ private
961
+
962
+ ##
963
+ # Returns the path of the file in which this method was called. Calls
964
+ # to this method from within *THIS* file are excluded from the search.
965
+ #
966
+ def first_caller_file
967
+ File.expand_path caller.each {|s| !s.include? __FILE__ and s =~ /^(.*?):\d+/ and break $1 }
968
+ end
969
+
970
+ ##
971
+ # Returns the project module corresponding to the given symbol.
972
+ # A new module is created if none already exists.
973
+ #
974
+ def fetch_project_module project_symbol
975
+ if Object.const_defined? project_symbol
976
+ project_module = Object.const_get(project_symbol)
977
+ else
978
+ project_module = Module.new
979
+ Object.const_set project_symbol, project_module
980
+ end
981
+
982
+ project_module
983
+ end
984
+ end
985
+ end
986
+
987
+ ##
988
+ # utility methods
989
+ #
990
+
991
+ unless File.respond_to? :write
992
+ ##
993
+ # Writes the given content to the given file.
994
+ #
995
+ # @return number of bytes written
996
+ #
997
+ def File.write path, content
998
+ File.open(path, 'wb') {|f| f.write content.to_s }
999
+ end
1000
+ end