slideshow 1.1.0.beta8 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +1 -1
- data/lib/slideshow/commands/gen.rb +35 -69
- data/lib/slideshow/config.rb +3 -0
- data/lib/slideshow/filters/slide_filter.rb +16 -0
- data/lib/slideshow/opts.rb +9 -0
- data/lib/slideshow/runner.rb +77 -18
- data/lib/slideshow/slide.rb +83 -25
- data/lib/slideshow/version.rb +1 -1
- metadata +11 -15
data/Rakefile
CHANGED
@@ -96,7 +96,11 @@ class Gen
|
|
96
96
|
if (@markup_type == :markdown && Markdown.lib == 'pandoc-ruby') || @markup_type == :rest
|
97
97
|
content = add_slide_directive_before_div_h1( content )
|
98
98
|
else
|
99
|
-
|
99
|
+
if config.header_level == 2
|
100
|
+
content = add_slide_directive_before_h2( content )
|
101
|
+
else # assume level 1
|
102
|
+
content = add_slide_directive_before_h1( content )
|
103
|
+
end
|
100
104
|
end
|
101
105
|
|
102
106
|
dump_content_to_file_debug_html( content )
|
@@ -126,65 +130,24 @@ class Gen
|
|
126
130
|
end
|
127
131
|
|
128
132
|
|
129
|
-
## split slide source into header (optional) and content/body
|
130
|
-
## plus check for (css style) classes and data attributes
|
131
|
-
|
132
133
|
slides2 = []
|
133
134
|
slides.each do |slide_source|
|
134
|
-
|
135
|
-
|
136
|
-
## check for css style classes
|
137
|
-
from = 0
|
138
|
-
while (pos = slide_source.index( /<!-- _S9(SLIDE|STYLE)_(.*?)-->/m, from ))
|
139
|
-
logger.debug " adding css classes from pi #{$1.downcase}: #{$2.strip}"
|
140
|
-
|
141
|
-
from = Regexp.last_match.end(0) # continue search later from here
|
142
|
-
|
143
|
-
values = $2.strip.dup
|
144
|
-
|
145
|
-
# remove data values (eg. x=-20 scale=4) and store in data hash
|
146
|
-
values.gsub!( /([-\w]+)[ \t]*=[ \t]*([-\w\.]+)/ ) do |_|
|
147
|
-
logger.debug " adding data pair: key=>#{$1.downcase}< value=>#{$2}<"
|
148
|
-
slide.data[ $1.downcase.dup ] = $2.dup
|
149
|
-
" " # replace w/ space
|
150
|
-
end
|
151
|
-
|
152
|
-
values.strip! # remove spaces # todo: use squish or similar and check for empty string
|
153
|
-
|
154
|
-
if slide.classes.nil?
|
155
|
-
slide.classes = values
|
156
|
-
else
|
157
|
-
slide.classes << " #{values}"
|
158
|
-
end
|
159
|
-
end
|
160
|
-
|
161
|
-
# try to cut off header using non-greedy .+? pattern; tip test regex online at rubular.com
|
162
|
-
# note/fix: needs to get improved to also handle case for h1 wrapped into div
|
163
|
-
# (e.g. extract h1 - do not assume it starts slide source)
|
164
|
-
if slide_source =~ /^\s*(<h1.*?>.*?<\/h\d>)\s*(.*)/m
|
165
|
-
slide.header = $1
|
166
|
-
slide.content = ($2 ? $2 : "")
|
167
|
-
logger.debug " adding slide with header:\n#{slide.header}"
|
168
|
-
else
|
169
|
-
slide.content = slide_source
|
170
|
-
logger.debug " adding slide with *no* header:\n#{slide.content}"
|
171
|
-
end
|
172
|
-
slides2 << slide
|
135
|
+
slides2 << Slide.new( slide_source, config )
|
173
136
|
end
|
174
137
|
|
175
138
|
# for convenience create a string w/ all in-one-html
|
176
139
|
# no need to wrap slides in divs etc.
|
177
140
|
|
178
141
|
content2 = ""
|
179
|
-
slides2.each do |slide|
|
142
|
+
slides2.each do |slide|
|
180
143
|
content2 << slide.to_classic_html
|
181
144
|
end
|
182
145
|
|
183
146
|
# make content2 and slide2 available to erb template
|
184
147
|
# -- todo: cleanup variable names and use attr_readers for content and slide
|
185
148
|
|
186
|
-
@slides = slides2 # strutured content
|
187
|
-
@content = content2 # content all-in-one
|
149
|
+
@slides = slides2 # strutured content
|
150
|
+
@content = content2 # content all-in-one
|
188
151
|
end
|
189
152
|
|
190
153
|
|
@@ -207,7 +170,7 @@ class Gen
|
|
207
170
|
matches = manifests.select { |m| m[0] == manifest_path_or_name }
|
208
171
|
|
209
172
|
if matches.empty?
|
210
|
-
puts "*** error: unknown template manifest '#{manifest_path_or_name}'"
|
173
|
+
puts "*** error: unknown template manifest '#{manifest_path_or_name}'"
|
211
174
|
# todo: list installed manifests
|
212
175
|
exit 2
|
213
176
|
end
|
@@ -215,6 +178,7 @@ class Gen
|
|
215
178
|
manifest = load_manifest( matches[0][1] )
|
216
179
|
end
|
217
180
|
|
181
|
+
|
218
182
|
# expand output path in current dir and make sure output path exists
|
219
183
|
outpath = File.expand_path( opts.output_path )
|
220
184
|
logger.debug "outpath=#{outpath}"
|
@@ -235,29 +199,16 @@ class Gen
|
|
235
199
|
logger.debug "oldcwd=#{oldcwd}"
|
236
200
|
logger.debug "newcwd=#{newcwd}"
|
237
201
|
Dir.chdir newcwd
|
238
|
-
end
|
202
|
+
end
|
239
203
|
|
240
204
|
puts "Preparing slideshow '#{basename}'..."
|
241
|
-
|
242
|
-
if
|
243
|
-
|
244
|
-
|
245
|
-
config.known_extnames.each do |e|
|
246
|
-
logger.debug "File.exists? #{dirname}/#{basename}#{e}"
|
247
|
-
if File.exists?( "#{dirname}/#{basename}#{e}" ) then
|
248
|
-
extname = e
|
249
|
-
logger.debug "extname=#{extname}"
|
250
|
-
break
|
251
|
-
end
|
252
|
-
end
|
253
|
-
end
|
254
|
-
|
255
|
-
if config.known_markdown_extnames.include?( extname )
|
256
|
-
@markup_type = :markdown
|
205
|
+
|
206
|
+
if config.known_textile_extnames.include?( extname )
|
207
|
+
@markup_type = :textile
|
257
208
|
elsif config.known_rest_extnames.include?( extname )
|
258
209
|
@markup_type = :rest
|
259
|
-
else
|
260
|
-
@markup_type = :
|
210
|
+
else # default/fallback use markdown
|
211
|
+
@markup_type = :markdown
|
261
212
|
end
|
262
213
|
|
263
214
|
# shared variables for templates (binding)
|
@@ -268,7 +219,7 @@ class Gen
|
|
268
219
|
|
269
220
|
@session = {} # reset session hash for plugins/helpers
|
270
221
|
|
271
|
-
inname = "#{
|
222
|
+
inname = "#{basename}#{extname}"
|
272
223
|
|
273
224
|
logger.debug "inname=#{inname}"
|
274
225
|
|
@@ -299,6 +250,10 @@ class Gen
|
|
299
250
|
end
|
300
251
|
|
301
252
|
|
253
|
+
#### fix/todo:
|
254
|
+
##
|
255
|
+
## check for .erb file extension for trigger for erb processing
|
256
|
+
|
302
257
|
manifest.each do |entry|
|
303
258
|
outname = entry[0]
|
304
259
|
if outname.include? '__file__' # process
|
@@ -320,11 +275,22 @@ class Gen
|
|
320
275
|
else # just copy verbatim if target/dest has no __file__ in name
|
321
276
|
dest = entry[0]
|
322
277
|
source = entry[1]
|
323
|
-
|
324
|
-
|
278
|
+
|
279
|
+
#### fix/todo:
|
280
|
+
##
|
281
|
+
## check for .erb file extension for trigger for erb processing
|
282
|
+
|
283
|
+
puts "Copying to #{dest} from #{source}..."
|
325
284
|
FileUtils.copy( source, with_output_path( dest, outpath ) )
|
326
285
|
end
|
327
286
|
end
|
287
|
+
|
288
|
+
|
289
|
+
## pop/restore working folder/dir
|
290
|
+
unless newcwd == oldcwd
|
291
|
+
logger.debug "oldcwd=>#{oldcwd}<, newcwd=>#{newcwd}<"
|
292
|
+
Dir.chdir( oldcwd )
|
293
|
+
end
|
328
294
|
|
329
295
|
puts "Done."
|
330
296
|
end
|
data/lib/slideshow/config.rb
CHANGED
@@ -28,6 +28,22 @@ def add_slide_directive_before_h1( content )
|
|
28
28
|
content
|
29
29
|
end
|
30
30
|
|
31
|
+
def add_slide_directive_before_h2( content )
|
32
|
+
|
33
|
+
slide_count = 0
|
34
|
+
|
35
|
+
content.gsub!( /<h2/ ) do |match|
|
36
|
+
slide_count += 1
|
37
|
+
"\n<!-- _S9SLIDE_ -->\n#{Regexp.last_match(0)}"
|
38
|
+
end
|
39
|
+
|
40
|
+
puts " Adding #{slide_count} slide breaks (using h2 rule)..."
|
41
|
+
|
42
|
+
content
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
|
31
47
|
# add slide directive before div h1 (for pandoc-generated html)
|
32
48
|
#
|
33
49
|
# e.g. changes:
|
data/lib/slideshow/opts.rb
CHANGED
data/lib/slideshow/runner.rb
CHANGED
@@ -35,44 +35,103 @@ def load_plugins
|
|
35
35
|
end
|
36
36
|
|
37
37
|
|
38
|
+
def find_file_with_known_extension( fn )
|
39
|
+
dirname = File.dirname( fn )
|
40
|
+
basename = File.basename( fn, '.*' )
|
41
|
+
extname = File.extname( fn )
|
42
|
+
logger.debug "dirname=#{dirname}, basename=#{basename}, extname=#{extname}"
|
43
|
+
|
44
|
+
config.known_extnames.each do |e|
|
45
|
+
newname = File.join( dirname, "#{basename}#{e}" )
|
46
|
+
logger.debug "File.exists? #{newname}"
|
47
|
+
return newname if File.exists?( newname )
|
48
|
+
end # each extension (e)
|
49
|
+
|
50
|
+
nil # not found; return nil
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
def find_files( file_or_dir_or_pattern )
|
55
|
+
|
56
|
+
filtered_files = []
|
57
|
+
|
58
|
+
## for now process/assume only single file
|
59
|
+
|
60
|
+
## (check for missing extension)
|
61
|
+
if File.exists?( file_or_dir_or_pattern )
|
62
|
+
file = file_or_dir_or_pattern
|
63
|
+
logger.debug " adding file '#{file}'..."
|
64
|
+
filtered_files << file
|
65
|
+
else # check for existing file w/ missing extension
|
66
|
+
file = find_file_with_known_extension( file_or_dir_or_pattern )
|
67
|
+
if file.nil?
|
68
|
+
puts " skipping missing file '#{file_or_dir_or_pattern}{#{config.known_extnames.join(',')}}'..."
|
69
|
+
else
|
70
|
+
logger.debug " adding file '#{file}'..."
|
71
|
+
filtered_files << file
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
filtered_files
|
76
|
+
|
77
|
+
end # method find_files
|
78
|
+
|
79
|
+
|
80
|
+
|
38
81
|
def run( args )
|
39
82
|
|
40
83
|
opt=OptionParser.new do |cmd|
|
41
84
|
|
42
85
|
cmd.banner = "Usage: slideshow [options] name"
|
43
|
-
|
86
|
+
|
44
87
|
cmd.on( '-o', '--output PATH', 'Output Path' ) { |path| opts.output_path = path }
|
45
|
-
|
46
|
-
cmd.on( '-g', '--generate', 'Generate Slide Show Templates (Using Built-In S6 Pack)' ) { opts.generate = true }
|
47
88
|
|
48
|
-
cmd.on( "-t", "--template MANIFEST", "Template Manifest" ) do |t|
|
89
|
+
cmd.on( "-t", "--template MANIFEST", "Template Manifest (default is s6.txt)" ) do |t|
|
49
90
|
# todo: do some checks on passed in template argument
|
50
91
|
opts.manifest = t
|
51
92
|
end
|
52
93
|
|
94
|
+
|
95
|
+
# cmd.on( '--header NUM', 'Header Level (default is 1)' ) do |n|
|
96
|
+
# opts.header_level = n.to_i
|
97
|
+
# end
|
98
|
+
|
99
|
+
cmd.on( '--h1', 'Set Header Level to 1 (default)' ) { opts.header_level = 1 }
|
100
|
+
cmd.on( '--h2', 'Set Header Level to 2' ) { opts.header_level = 2 }
|
101
|
+
|
102
|
+
|
53
103
|
# ?? opts.on( "-s", "--style STYLE", "Select Stylesheet" ) { |s| $options[:style]=s }
|
54
|
-
# ?? opts.on( "--version", "Show version" ) {}
|
55
104
|
|
56
105
|
# ?? cmd.on( '-i', '--include PATH', 'Load Path' ) { |s| opts.put( 'include', s ) }
|
57
106
|
|
58
|
-
cmd.on( '-f', '--fetch URI', 'Fetch Templates' ) do |uri|
|
59
|
-
opts.fetch_uri = uri
|
60
|
-
end
|
61
|
-
|
62
107
|
cmd.on( '-c', '--config PATH', 'Configuration Path (default is ~/.slideshow)' ) do |path|
|
63
108
|
opts.config_path = path
|
64
109
|
end
|
110
|
+
|
111
|
+
cmd.on( '-f', '--fetch URI', 'Fetch Templates' ) do |uri|
|
112
|
+
opts.fetch_uri = uri
|
113
|
+
end
|
114
|
+
|
115
|
+
cmd.on( '-g', '--generate', 'Generate Slide Show Templates (Using Built-In S6 Pack)' ) { opts.generate = true }
|
65
116
|
|
66
117
|
cmd.on( '-l', '--list', 'List Installed Templates' ) { opts.list = true }
|
67
118
|
|
68
119
|
# todo: find different letter for debug trace switch (use v for version?)
|
69
120
|
cmd.on( "-v", "--verbose", "Show debug trace" ) do
|
70
121
|
logger.datetime_format = "%H:%H:%S"
|
71
|
-
logger.level = Logger::DEBUG
|
122
|
+
logger.level = Logger::DEBUG
|
72
123
|
end
|
73
|
-
|
74
|
-
|
124
|
+
|
125
|
+
## todo: add --version
|
75
126
|
|
127
|
+
cmd.on( '--version', "Show version" ) do
|
128
|
+
puts Slideshow.generator
|
129
|
+
exit
|
130
|
+
end
|
131
|
+
|
132
|
+
cmd.on_tail( "-h", "--help", "Show this message" ) do
|
133
|
+
puts <<EOS
|
134
|
+
|
76
135
|
Slide Show (S9) is a free web alternative to PowerPoint or KeyNote in Ruby
|
77
136
|
|
78
137
|
#{cmd.help}
|
@@ -95,10 +154,6 @@ Further information:
|
|
95
154
|
http://slideshow.rubyforge.org
|
96
155
|
|
97
156
|
EOS
|
98
|
-
|
99
|
-
|
100
|
-
cmd.on_tail( "-h", "--help", "Show this message" ) do
|
101
|
-
puts usage
|
102
157
|
exit
|
103
158
|
end
|
104
159
|
end
|
@@ -118,8 +173,12 @@ EOS
|
|
118
173
|
else
|
119
174
|
load_plugins # check for optional plugins/extension in ./lib folder
|
120
175
|
|
121
|
-
args.each do |
|
122
|
-
|
176
|
+
args.each do |arg|
|
177
|
+
files = find_files( arg )
|
178
|
+
files.each do |file|
|
179
|
+
### fix/todo: reset/clean headers
|
180
|
+
Gen.new( logger, opts, config, headers ).create_slideshow( file )
|
181
|
+
end
|
123
182
|
end
|
124
183
|
end
|
125
184
|
end
|
data/lib/slideshow/slide.rb
CHANGED
@@ -1,19 +1,84 @@
|
|
1
1
|
module Slideshow
|
2
2
|
|
3
3
|
class Slide
|
4
|
-
|
5
|
-
attr_accessor :
|
4
|
+
|
5
|
+
attr_accessor :logger
|
6
|
+
|
7
|
+
# NB: unparsed html source (use content_without_header
|
8
|
+
# for content without option header)
|
9
|
+
|
6
10
|
attr_accessor :content
|
11
|
+
attr_accessor :content_without_header
|
12
|
+
attr_accessor :header
|
7
13
|
attr_accessor :classes
|
8
|
-
attr_accessor :data
|
9
14
|
|
10
|
-
def initialize
|
11
|
-
@
|
12
|
-
@
|
13
|
-
|
15
|
+
def initialize( content, options )
|
16
|
+
@logger = options.logger
|
17
|
+
@header_level = options.header_level
|
18
|
+
|
19
|
+
@content = content
|
20
|
+
|
21
|
+
@header = nil
|
22
|
+
@content_without_header = nil
|
23
|
+
@classes = nil # NB: style classes as all in one string
|
14
24
|
@data = {}
|
25
|
+
|
26
|
+
parse()
|
15
27
|
end
|
16
28
|
|
29
|
+
def parse
|
30
|
+
## pass 1) check for (css style) classes and data attributes
|
31
|
+
## check for css style classes
|
32
|
+
from = 0
|
33
|
+
while( pos = @content.index( /<!-- _S9(SLIDE|STYLE)_(.*?)-->/m, from ))
|
34
|
+
logger.debug " adding css classes from pi >#{$1.downcase}<: >#{$2.strip}<"
|
35
|
+
|
36
|
+
from = Regexp.last_match.end(0) # continue search later from here
|
37
|
+
|
38
|
+
values = $2.strip.dup
|
39
|
+
|
40
|
+
# remove data values (eg. x=-20 scale=4) and store in data hash
|
41
|
+
values.gsub!( /([-\w]+)[ \t]*=[ \t]*([-\w\.]+)/ ) do |_|
|
42
|
+
logger.debug " adding data pair: key=>#{$1.downcase}< value=>#{$2}<"
|
43
|
+
@data[ $1.downcase.dup ] = $2.dup
|
44
|
+
" " # replace w/ space
|
45
|
+
end
|
46
|
+
|
47
|
+
values.strip! # remove spaces # todo: use squish or similar and check for empty string
|
48
|
+
|
49
|
+
if @classes.nil?
|
50
|
+
@classes = values
|
51
|
+
else
|
52
|
+
@classes << " #{values}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
## pass 2) split slide source into header (optional) and content/body
|
57
|
+
|
58
|
+
## todo: add option split on h1/h2 etc.
|
59
|
+
|
60
|
+
# try to extract first header using non-greedy .+? pattern;
|
61
|
+
# tip test regex online at rubular.com
|
62
|
+
# note/fix: needs to get improved to also handle case for h1 wrapped into div
|
63
|
+
|
64
|
+
if @header_level == 2
|
65
|
+
pattern = /^(.*?)(<h2.*?>.*?<\/h2>)(.*)/m
|
66
|
+
else # assume header level 1
|
67
|
+
pattern = /^(.*?)(<h1.*?>.*?<\/h1>)(.*)/m
|
68
|
+
end
|
69
|
+
|
70
|
+
if @content =~ pattern
|
71
|
+
@header = $2
|
72
|
+
@content_without_header = ($1 ? $1 : '') + ($3 ? $3 : '')
|
73
|
+
logger.debug " adding slide with header (h1):\n#{@header}"
|
74
|
+
else
|
75
|
+
@header = nil # todo: set to '' empty string? why not?
|
76
|
+
@content_without_header = @content
|
77
|
+
logger.debug " adding slide with *no* header:\n#{@content}"
|
78
|
+
end
|
79
|
+
end # method parse
|
80
|
+
|
81
|
+
|
17
82
|
def data_attributes
|
18
83
|
buf = ""
|
19
84
|
@data.each do | key,value |
|
@@ -21,40 +86,33 @@ module Slideshow
|
|
21
86
|
end
|
22
87
|
buf
|
23
88
|
end
|
89
|
+
|
90
|
+
################################
|
91
|
+
#### convenience helpers
|
24
92
|
|
25
93
|
def to_classic_html
|
26
|
-
|
27
94
|
buf = ""
|
28
95
|
buf << "<div class='slide "
|
29
96
|
buf << classes if classes
|
30
|
-
buf << "'>\n"
|
31
|
-
|
32
|
-
buf << header if header
|
33
|
-
buf << content if content
|
34
|
-
|
97
|
+
buf << "'>\n"
|
98
|
+
buf << content
|
35
99
|
buf << "</div>\n"
|
36
|
-
buf
|
100
|
+
buf
|
37
101
|
end
|
38
102
|
|
39
103
|
def to_google_html5
|
40
|
-
|
41
104
|
buf = ""
|
42
105
|
buf << "<div class='slide'>\n"
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
end
|
47
|
-
|
48
|
-
buf << "<section class='"
|
49
|
-
buf << classes if classes
|
106
|
+
buf << "<header>#{header}</header>\n" if header
|
107
|
+
buf << "<section "
|
108
|
+
buf << "class='#{classes}'" if classes
|
50
109
|
buf << "'>\n"
|
51
|
-
|
52
|
-
buf << content if content
|
110
|
+
buf << content_without_header if content_without_header
|
53
111
|
buf << "</section>\n"
|
54
112
|
buf << "</div>\n"
|
55
113
|
buf
|
56
114
|
end
|
57
115
|
|
58
|
-
end # class
|
116
|
+
end # class Slide
|
59
117
|
|
60
118
|
end # module Slideshow
|
data/lib/slideshow/version.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: slideshow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
5
|
-
prerelease:
|
4
|
+
hash: 19
|
5
|
+
prerelease:
|
6
6
|
segments:
|
7
7
|
- 1
|
8
8
|
- 1
|
9
9
|
- 0
|
10
|
-
|
11
|
-
- 8
|
12
|
-
version: 1.1.0.beta8
|
10
|
+
version: 1.1.0
|
13
11
|
platform: ruby
|
14
12
|
authors:
|
15
13
|
- Gerald Bauer
|
@@ -17,7 +15,7 @@ autorequire:
|
|
17
15
|
bindir: bin
|
18
16
|
cert_chain: []
|
19
17
|
|
20
|
-
date: 2012-06-
|
18
|
+
date: 2012-06-17 00:00:00 Z
|
21
19
|
dependencies:
|
22
20
|
- !ruby/object:Gem::Dependency
|
23
21
|
name: RedCloth
|
@@ -43,12 +41,12 @@ dependencies:
|
|
43
41
|
requirements:
|
44
42
|
- - ">="
|
45
43
|
- !ruby/object:Gem::Version
|
46
|
-
hash:
|
44
|
+
hash: 15
|
47
45
|
segments:
|
48
46
|
- 0
|
49
|
-
-
|
47
|
+
- 4
|
50
48
|
- 0
|
51
|
-
version: 0.
|
49
|
+
version: 0.4.0
|
52
50
|
type: :runtime
|
53
51
|
version_requirements: *id002
|
54
52
|
- !ruby/object:Gem::Dependency
|
@@ -222,14 +220,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
222
220
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
223
221
|
none: false
|
224
222
|
requirements:
|
225
|
-
- - "
|
223
|
+
- - ">="
|
226
224
|
- !ruby/object:Gem::Version
|
227
|
-
hash:
|
225
|
+
hash: 3
|
228
226
|
segments:
|
229
|
-
-
|
230
|
-
|
231
|
-
- 1
|
232
|
-
version: 1.3.1
|
227
|
+
- 0
|
228
|
+
version: "0"
|
233
229
|
requirements: []
|
234
230
|
|
235
231
|
rubyforge_project: slideshow
|