gerbil 1.0.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/LICENSE +23 -0
- data/README +1 -0
- data/Rakefile +48 -0
- data/bin/gerbil +275 -0
- data/doc/HelloWorld.input +37 -0
- data/doc/HelloWorld.spec +66 -0
- data/doc/gerbil.png +0 -0
- data/doc/gerbil.svg +330 -0
- data/doc/guide.erb +677 -0
- data/doc/guide.html +3416 -0
- data/fmt/html.icons/LICENSE +67 -0
- data/fmt/html.icons/README +30 -0
- data/fmt/html.icons/caution.png +0 -0
- data/fmt/html.icons/important.png +0 -0
- data/fmt/html.icons/note.png +0 -0
- data/fmt/html.icons/tip.png +0 -0
- data/fmt/html.icons/warning.png +0 -0
- data/fmt/html.yaml +897 -0
- data/fmt/latex.yaml +2 -0
- data/fmt/man.yaml +2 -0
- data/fmt/text.yaml +2 -0
- data/lib/gerbil.rb +11 -0
- data/lib/gerbil/html.rb +149 -0
- data/lib/gerbil/rdoc.rb +91 -0
- metadata +99 -0
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Copyright 2006 Suraj N. Kurapati <SNK at GNA dot ORG>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
4
|
+
copy of this software and associated documentation files (the "Software"),
|
5
|
+
to deal in the Software without restriction, including without limitation
|
6
|
+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
7
|
+
and/or sell copies of the Software, and to permit persons to whom the
|
8
|
+
Software is furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
* All copies and substantial portions of the Software (the "Derivatives")
|
11
|
+
and their corresponding machine-readable source code (the "Code") must
|
12
|
+
include the above copyright notice and this permission notice.
|
13
|
+
|
14
|
+
* Upon distribution, the Derivatives must be accompanied either by the Code
|
15
|
+
or--provided that the Code is obtainable for no more than the cost of
|
16
|
+
distribution plus a nominal fee--by information on how to obtain the Code.
|
17
|
+
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
20
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
21
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
22
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
23
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Please see doc/guide.html in your web browser.
|
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# Copyright 2007 Suraj N. Kurapati
|
2
|
+
# See the file named LICENSE for details.
|
3
|
+
|
4
|
+
require 'rake/clean'
|
5
|
+
require 'rake/gempackagetask'
|
6
|
+
|
7
|
+
# documentation
|
8
|
+
desc "Generate documentation."
|
9
|
+
task :doc => 'doc/guide.html'
|
10
|
+
|
11
|
+
file 'doc/guide.html' => 'doc/guide.erb' do |t|
|
12
|
+
sh "ruby bin/gerbil html #{t.prerequisites} > #{t.name}"
|
13
|
+
end
|
14
|
+
|
15
|
+
CLOBBER.include 'doc/guide.html'
|
16
|
+
|
17
|
+
# packaging
|
18
|
+
require 'lib/gerbil' # project info
|
19
|
+
|
20
|
+
spec = Gem::Specification.new do |s|
|
21
|
+
s.name = 'gerbil'
|
22
|
+
s.version = Gerbil[:version]
|
23
|
+
s.summary = 'Extensible document generator based on eRuby.'
|
24
|
+
s.description = s.summary
|
25
|
+
s.homepage = Gerbil[:website]
|
26
|
+
s.files = FileList['**/*'].exclude('_darcs')
|
27
|
+
s.executables = s.name
|
28
|
+
|
29
|
+
s.add_dependency 'RedCloth' # needed by the default 'html' format
|
30
|
+
s.add_dependency 'coderay' # needed by the default 'html' format
|
31
|
+
end
|
32
|
+
|
33
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
34
|
+
pkg.need_tar = true
|
35
|
+
pkg.need_zip = true
|
36
|
+
end
|
37
|
+
|
38
|
+
# releasing
|
39
|
+
desc 'Build packages for distribution.'
|
40
|
+
task :release => [:clobber, :doc] do
|
41
|
+
system 'rake package'
|
42
|
+
end
|
43
|
+
|
44
|
+
# utility
|
45
|
+
desc 'Upload to project website.'
|
46
|
+
task :upload => :doc do
|
47
|
+
sh 'rsync -av doc/ ~/www/lib/gerbil'
|
48
|
+
end
|
data/bin/gerbil
ADDED
@@ -0,0 +1,275 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Gerbil is an extensible document generator based on eRuby.
|
4
|
+
#
|
5
|
+
# * STDIN will be read if an input file is not specified.
|
6
|
+
#
|
7
|
+
# * If an error occurs, the input document will be printed to STDOUT
|
8
|
+
# so you can investigate line numbers in the error's stack trace.
|
9
|
+
#
|
10
|
+
# Otherwise, the final output document will be printed to STDOUT.
|
11
|
+
|
12
|
+
# Copyright 2007 Suraj N. Kurapati
|
13
|
+
# See the file named LICENSE for details.
|
14
|
+
|
15
|
+
require 'erb'
|
16
|
+
include ERB::Util
|
17
|
+
|
18
|
+
require 'digest/sha1'
|
19
|
+
require 'yaml'
|
20
|
+
require 'ostruct'
|
21
|
+
|
22
|
+
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
23
|
+
require 'gerbil'
|
24
|
+
|
25
|
+
# Prints the given message and raises the given error.
|
26
|
+
def hoist aMessage, aError = $!
|
27
|
+
STDERR.printf "%s:\n\n", aMessage
|
28
|
+
raise aError
|
29
|
+
end
|
30
|
+
|
31
|
+
class String
|
32
|
+
# Returns a SHA1 digest (in hex format) of this string.
|
33
|
+
def digest
|
34
|
+
Digest::SHA1.hexdigest(self)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Template < ERB
|
39
|
+
# Returns the result of template evaluation thus far.
|
40
|
+
attr_reader :buffer
|
41
|
+
|
42
|
+
# aName:: String that replaces the ambiguous '(erb)' identifier in stack
|
43
|
+
# traces, so that the user can better determine the source of an
|
44
|
+
# error.
|
45
|
+
#
|
46
|
+
# args:: Arguments for ERB::new
|
47
|
+
def initialize aName, *args
|
48
|
+
# silence the code-only <% ... %> directive, just like PHP does
|
49
|
+
args[0].gsub!(/^[ \t]*<%[^%=]((?!<%).)*?[^%]%>[ \t]*\r?\n/m) {|s| s.strip}
|
50
|
+
|
51
|
+
args[3] = '@buffer'
|
52
|
+
super(*args)
|
53
|
+
|
54
|
+
@filename = aName
|
55
|
+
end
|
56
|
+
|
57
|
+
# Renders this template within a fresh object that
|
58
|
+
# is populated with the given instance variables.
|
59
|
+
def render_with aInstVars = {}
|
60
|
+
context = Object.new.instance_eval do
|
61
|
+
aInstVars.each_pair do |var, val|
|
62
|
+
instance_variable_set var, val
|
63
|
+
end
|
64
|
+
|
65
|
+
binding
|
66
|
+
end
|
67
|
+
|
68
|
+
result(context)
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# Returns the content that the given block wants to append to
|
74
|
+
# the buffer. If the given block does not want to append to the
|
75
|
+
# buffer, then returns the result of invoking the given block.
|
76
|
+
def content_from_block *aBlockArgs
|
77
|
+
raise ArgumentError, 'block must be given' unless block_given?
|
78
|
+
|
79
|
+
start = @buffer.length
|
80
|
+
value = yield(*aBlockArgs) # this will do: buffer << content
|
81
|
+
finish = @buffer.length
|
82
|
+
|
83
|
+
if finish > start
|
84
|
+
@buffer.slice! start..-1
|
85
|
+
else
|
86
|
+
value
|
87
|
+
end.to_s
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class Node < OpenStruct
|
92
|
+
undef id, type # these are deprecated in Ruby 1.8 anyway
|
93
|
+
end
|
94
|
+
|
95
|
+
# the basename() is for being launched by a RubyGems executable
|
96
|
+
if __FILE__ == $0 or File.basename(__FILE__) == File.basename($0)
|
97
|
+
# parse command-line options
|
98
|
+
require 'optparse'
|
99
|
+
|
100
|
+
opts = OptionParser.new('')
|
101
|
+
|
102
|
+
opts.on '-h', '--help', 'show usage information' do
|
103
|
+
# show program description located at the top of this file
|
104
|
+
puts File.read(__FILE__).split(/^$\n/)[1].gsub(/^# ?/, '')
|
105
|
+
|
106
|
+
puts "\nUsage: #{File.basename $0} [Option...] Format|SpecFile [InputFile]\n"
|
107
|
+
|
108
|
+
puts "\nOption: #{opts}"
|
109
|
+
|
110
|
+
puts "\nFormat:"
|
111
|
+
Gerbil[:format_files].each do |file|
|
112
|
+
name = File.basename(file, '.yaml')
|
113
|
+
desc = YAML.load_file(file)['desc'] rescue nil
|
114
|
+
puts ' %-32s %s' % [name, desc]
|
115
|
+
end
|
116
|
+
|
117
|
+
exit
|
118
|
+
end
|
119
|
+
|
120
|
+
opts.on '-v', '--version', 'show version information' do
|
121
|
+
puts "%s %s (%s) %s %s" %
|
122
|
+
[:name, :version, :release, :website, :home].map { |m| Gerbil[m] }
|
123
|
+
exit
|
124
|
+
end
|
125
|
+
|
126
|
+
opts.parse! ARGV
|
127
|
+
|
128
|
+
# load format specification file
|
129
|
+
specFile = ARGV.shift or
|
130
|
+
raise ArgumentError, "Format was not specified. Try `{$0} -h` for help."
|
131
|
+
|
132
|
+
File.file? specFile or
|
133
|
+
specFile = File.join(Gerbil[:format_home], specFile + '.yaml')
|
134
|
+
|
135
|
+
begin
|
136
|
+
specData = YAML.load_file(specFile).merge! \
|
137
|
+
:file => File.expand_path(specFile),
|
138
|
+
:name => File.basename(specFile).sub(/\..*?$/, '')
|
139
|
+
|
140
|
+
rescue Exception
|
141
|
+
hoist "Error when loading the format specification file (#{specFile.inspect})"
|
142
|
+
end
|
143
|
+
|
144
|
+
if specData.key? 'code'
|
145
|
+
eval specData['code'].to_s, binding, "#{specFile}:code"
|
146
|
+
end
|
147
|
+
|
148
|
+
# load input document
|
149
|
+
input = ARGF.read
|
150
|
+
|
151
|
+
# expand all "include" directives in the input
|
152
|
+
begin end while input.gsub! %r/<%#\s*include\s+(.+?)\s*%>/ do
|
153
|
+
"<%#begin(#{name = $1.inspect})%>#{File.read $1}<%#end(#{name})%>"
|
154
|
+
end
|
155
|
+
|
156
|
+
# process input document
|
157
|
+
template = Template.new('INPUT', input)
|
158
|
+
|
159
|
+
templateVars = {
|
160
|
+
:@spec => specData,
|
161
|
+
:@roots => roots = [], # root nodes of all trees
|
162
|
+
:@nodes => nodes = [], # all nodes in the forest
|
163
|
+
:@types => types = Hash.new {|h,k| h[k] = []}, # nodes by type
|
164
|
+
}.each_pair { |k,v| template.instance_variable_set(k, v) }
|
165
|
+
|
166
|
+
nodeDefs = specData['nodes'].each_pair do |name, info|
|
167
|
+
template.instance_eval %{
|
168
|
+
#
|
169
|
+
# XXX: using a string because define_method()
|
170
|
+
# does not accept a block until Ruby 1.9
|
171
|
+
#
|
172
|
+
def #{name} *aArgs, &aBlock
|
173
|
+
node = Node.new(
|
174
|
+
:type => #{name.inspect},
|
175
|
+
:args => aArgs,
|
176
|
+
:trace => caller,
|
177
|
+
:children => []
|
178
|
+
)
|
179
|
+
@nodes << node
|
180
|
+
@types[node.type] << node
|
181
|
+
|
182
|
+
# calculate occurrence number for this node
|
183
|
+
if #{info['number']}
|
184
|
+
@count ||= Hash.new {|h,k| h[k] = []}
|
185
|
+
node.number = (@count[node.type] << node).length
|
186
|
+
end
|
187
|
+
|
188
|
+
@stack ||= []
|
189
|
+
|
190
|
+
# assign node family
|
191
|
+
if parent = @stack.last
|
192
|
+
parent.children << node
|
193
|
+
node.parent = parent
|
194
|
+
node.depth = parent.depth.next
|
195
|
+
|
196
|
+
# calculate latex-style index number for this node
|
197
|
+
if #{info['index']}
|
198
|
+
branches = parent.children.select {|n| n.index}
|
199
|
+
node.index = [parent.index, branches.length.next].join('.')
|
200
|
+
end
|
201
|
+
else
|
202
|
+
@roots << node
|
203
|
+
node.parent = nil
|
204
|
+
node.depth = 0
|
205
|
+
|
206
|
+
# calculate latex-style index number for this node
|
207
|
+
if #{info['index']}
|
208
|
+
branches = @roots.select {|n| n.index}
|
209
|
+
node.index = branches.length.next.to_s
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# assign node content
|
214
|
+
if block_given?
|
215
|
+
@stack << node
|
216
|
+
content = content_from_block(node, &aBlock)
|
217
|
+
@stack.pop
|
218
|
+
|
219
|
+
digest = content.digest
|
220
|
+
|
221
|
+
node.content = content
|
222
|
+
node.digest = digest
|
223
|
+
|
224
|
+
@buffer << digest
|
225
|
+
else
|
226
|
+
digest = node.object_id.to_s.digest
|
227
|
+
node.digest = digest
|
228
|
+
end
|
229
|
+
|
230
|
+
digest
|
231
|
+
end
|
232
|
+
}, __FILE__, Kernel.caller.first[/\d+/].to_i.next
|
233
|
+
end
|
234
|
+
|
235
|
+
begin
|
236
|
+
# build the document tree
|
237
|
+
document = template.instance_eval { result(binding) }
|
238
|
+
|
239
|
+
# replace nodes with output
|
240
|
+
expander = lambda do |n, buf|
|
241
|
+
# unindent content
|
242
|
+
if defined? $unindent and n.content
|
243
|
+
n.content.gsub! %r/^(#{Regexp.escape $unindent}){0,#{n.depth.next}}/, ''
|
244
|
+
end
|
245
|
+
|
246
|
+
# calculate node output
|
247
|
+
source = "#{specFile}:nodes:#{n.type}:output"
|
248
|
+
n.output = Template.new(source, nodeDefs[n.type]['output'].to_s.chomp).
|
249
|
+
render_with(templateVars.merge(:@node => n))
|
250
|
+
|
251
|
+
# replace node with output
|
252
|
+
if nodeDefs[n.type]['silent']
|
253
|
+
buf.sub! n.digest, ''
|
254
|
+
buf = n.output
|
255
|
+
else
|
256
|
+
buf.sub! n.digest, n.output
|
257
|
+
end
|
258
|
+
|
259
|
+
# repeat for all child nodes
|
260
|
+
n.children.each {|c| expander[c, buf]}
|
261
|
+
end
|
262
|
+
|
263
|
+
roots.each {|n| expander[n, document]}
|
264
|
+
|
265
|
+
rescue Exception
|
266
|
+
puts input # so the user can debug the line numbers in the stack trace
|
267
|
+
hoist 'Error when processing the input document (INPUT)'
|
268
|
+
end
|
269
|
+
|
270
|
+
# produce output document
|
271
|
+
output = Template.new("#{specFile}:output", specData['output'].to_s).
|
272
|
+
render_with(templateVars.merge(:@content => document))
|
273
|
+
|
274
|
+
puts output
|
275
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
<% $style = "border-left: thick solid salmon; padding-left: 1em;" %>
|
2
|
+
<% hello "Pretentious" do %>
|
3
|
+
<big>I'm</big> the very first node, oh _yes_ I am! *sneer*
|
4
|
+
|
5
|
+
<% hello "Bashful" do %>
|
6
|
+
Hi, I... *hide*
|
7
|
+
|
8
|
+
<% hello "Hopeful" do %>
|
9
|
+
*sigh*
|
10
|
+
|
11
|
+
<% hello "Confused" do %>
|
12
|
+
Huh?
|
13
|
+
<% end %>
|
14
|
+
<% end %>
|
15
|
+
|
16
|
+
<% hello "Raving" do %>
|
17
|
+
Oh it's *on* now! You're going *down*!
|
18
|
+
<% end %>
|
19
|
+
<% end %>
|
20
|
+
|
21
|
+
<% hello "Sleepy" do %>
|
22
|
+
*yawn* Just five more minutes...
|
23
|
+
|
24
|
+
<% hello "Peaceful" do %>
|
25
|
+
So _be_ happy my friend, *happy*!
|
26
|
+
|
27
|
+
<%= hello "Lonely (as you can see, I have no block)" %>
|
28
|
+
<% end %>
|
29
|
+
<% end %>
|
30
|
+
<% end %>
|
31
|
+
|
32
|
+
<% hello "Snappy" do %>
|
33
|
+
Zip! Zap! Wake up, you sap!
|
34
|
+
_Whoo I'm wild!_ ;-)
|
35
|
+
<% end %>
|
36
|
+
|
37
|
+
<%= hello "Independent (no block, no parents, I am _free_!)" %>
|
data/doc/HelloWorld.spec
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
desc: A illustrative format.
|
2
|
+
|
3
|
+
code: |
|
4
|
+
# Returns a random, but pronounceable, name.
|
5
|
+
def generate_name
|
6
|
+
letters = ('a'..'z').to_a - %w[ c q w x ] # redundant sounds
|
7
|
+
vowels = %w[a e i o u]
|
8
|
+
consonants = letters - vowels
|
9
|
+
sets = [consonants, vowels]
|
10
|
+
|
11
|
+
length = 3 + rand(5)
|
12
|
+
|
13
|
+
name = ''
|
14
|
+
length.times do |i|
|
15
|
+
set = sets[i % sets.length]
|
16
|
+
name << set[rand(set.length)]
|
17
|
+
end
|
18
|
+
|
19
|
+
name
|
20
|
+
end
|
21
|
+
|
22
|
+
class Node
|
23
|
+
def name
|
24
|
+
@name ||= generate_name
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
nodes:
|
29
|
+
hello:
|
30
|
+
index: true
|
31
|
+
number: true
|
32
|
+
silent: false
|
33
|
+
output: |
|
34
|
+
<h1><%= @node.type %> #<%= @node.index %>: <%= @node.name.inspect %></h1>
|
35
|
+
My name is <%= @node.name.inspect %> and these are my properties:
|
36
|
+
<table>
|
37
|
+
<tr>
|
38
|
+
<th>Property</th>
|
39
|
+
<th>Value</th>
|
40
|
+
</tr>
|
41
|
+
<tr>
|
42
|
+
<td>args</td>
|
43
|
+
<td><%= @node.args.inspect %></td>
|
44
|
+
</tr>
|
45
|
+
<tr>
|
46
|
+
<td>index</td>
|
47
|
+
<td><%= @node.index.inspect %></td>
|
48
|
+
</tr>
|
49
|
+
<tr>
|
50
|
+
<td>number</td>
|
51
|
+
<td><%= @node.number.inspect %></td>
|
52
|
+
</tr>
|
53
|
+
<tr>
|
54
|
+
<td>trace</td>
|
55
|
+
<td><ul><%= @node.trace.map {|s| "<li>#{s}</li>"} %></ul></td>
|
56
|
+
</tr>
|
57
|
+
<tr>
|
58
|
+
<td>content</td>
|
59
|
+
<td><%= @node.content %></td>
|
60
|
+
</tr>
|
61
|
+
</table>
|
62
|
+
|
63
|
+
output: |
|
64
|
+
Welcome to the "<%= @spec[:name] %>" format.
|
65
|
+
<div style="<%= $style %>"><%= @content %></div>
|
66
|
+
That's all folks!
|