nishidayuya-rd2odt 0.0.0 → 0.0.1

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/lib/rd2odt.rb ADDED
@@ -0,0 +1,610 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require "pp"
4
+ require "optparse"
5
+ require "find"
6
+ require "tmpdir"
7
+ require "cgi"
8
+ require "rd/rdvisitor"
9
+ require "rd/rdfmt"
10
+ require "rexml/document"
11
+ require "zip/zip"
12
+
13
+ module RD2ODT
14
+ @@options = {
15
+ :backtrace => false,
16
+ :template => nil,
17
+ }
18
+ OPTION_PARSER = OptionParser.new
19
+ OPTION_PARSER.banner = "Usage: #{OPTION_PARSER.program_name} [options] input-file-path.rd [output-file-path.odt]"
20
+ OPTION_PARSER.on("--backtrace", "print backtrace") do
21
+ @@options[:backtrace] = true
22
+ end
23
+ OPTION_PARSER.on("--template=TEMPLATE", "specify template filename") do |arg|
24
+ @@options[:template] = arg
25
+ end
26
+
27
+ def options
28
+ return @@options
29
+ end
30
+ module_function :options
31
+
32
+ def parse_option(argv)
33
+ begin
34
+ OPTION_PARSER.parse!(argv)
35
+ rescue OptionParser::ParseError => e
36
+ raise ProgramOptionParseError, e
37
+ end
38
+
39
+ @@input_path = argv.shift
40
+ if @@input_path.nil?
41
+ raise ProgramOptionError, "no input file path."
42
+ end
43
+
44
+ @@output_path =
45
+ argv.shift ||
46
+ (@@input_path == "-" ? "output.odt" : @@input_path + ".odt")
47
+
48
+ if options[:template].nil?
49
+ options[:template] = @@input_path + ".ott"
50
+ end
51
+ @@input_path
52
+ end
53
+ module_function :parse_option
54
+
55
+ def main(argv)
56
+ parse_option(argv)
57
+
58
+ include_paths = [
59
+ File.dirname(@@input_path),
60
+ File.dirname(@@output_path),
61
+ ]
62
+
63
+ puts("input_path: " + @@input_path.inspect) if $DEBUG
64
+ puts("output_path: " + @@output_path.inspect) if $DEBUG
65
+ puts("options: " + options.inspect) if $DEBUG
66
+ puts("include_paths: " + include_paths.inspect) if $DEBUG
67
+
68
+ input_lines = treat_input(File.readlines(@@input_path))
69
+ tree = RD::RDTree.new(input_lines, include_paths, nil)
70
+ tree.parse
71
+ visitor = RD2ODTVisitor.new
72
+ doc = visitor.visit(tree)
73
+ create_odt(visitor, doc, @@output_path, options[:template])
74
+ rescue Error => e
75
+ e.process
76
+ end
77
+ module_function :main
78
+
79
+ def self.treat_input(lines)
80
+ result = lines.dup
81
+
82
+ if lines.grep(/^=begin\b/).empty? &&
83
+ lines.grep(/^=end\b/).empty?
84
+ result.unshift("=begin\n")
85
+
86
+ if !(/\n\z/ === result[-1])
87
+ result[-1] = result[-1] + "\n"
88
+ end
89
+ result.push("=end\n")
90
+ end
91
+
92
+ return result
93
+ end
94
+
95
+ def self.create_odt(visitor, doc, output_path, template_path)
96
+ current_path = Dir.pwd
97
+ output_absolute_path = File.expand_path(output_path)
98
+ template_absolute_path = File.expand_path(template_path)
99
+ Dir.mktmpdir do |tmpdir|
100
+ Dir.chdir(tmpdir) do
101
+ unzip(template_absolute_path)
102
+ open("styles.xml", "r+") do |f|
103
+ operate_styles_xml(f, visitor.additional_styles)
104
+ end
105
+ open("content.xml", "w") do |f|
106
+ f.puts('<?xml version="1.0" encoding="UTF-8"?>')
107
+ f.puts
108
+ f.puts(ah_to_xml(doc))
109
+ end
110
+ # todo: test
111
+ # todo: extract only inner_object.href for more optimizing.
112
+ visitor.inner_objects.each do |inner_object|
113
+ Dir.mktmpdir do |dir|
114
+ Dir.chdir(dir) do
115
+ unzip(File.join(current_path, inner_object.path))
116
+ from = inner_object.href
117
+ to = File.join(tmpdir, inner_object.fixed_href)
118
+ FileUtils.mkdir_p(File.dirname(to))
119
+ FileUtils.mv(from, to)
120
+ end
121
+ end
122
+ end
123
+ zip(output_absolute_path)
124
+ end
125
+ end
126
+ end
127
+
128
+ # very lazy formatter
129
+ def self.ah_to_xml(o)
130
+ return __send__("ah_to_xml_by_" + o.class.name.downcase, o)
131
+ end
132
+
133
+ def self.ah_to_xml_by_array(ary)
134
+ if ary.first.is_a?(Array) ||
135
+ ary.first.is_a?(Symbol) && /<.*>/ === ary.first.to_s
136
+ # This case is:
137
+ # [[:tag], [:tag]]
138
+ # |
139
+ # v
140
+ # <tag></tag>
141
+ # <tag></tag>
142
+ return ary.map { |item|
143
+ ah_to_xml(item)
144
+ }.join("\n")
145
+ end
146
+
147
+ ary = ary.dup
148
+ result = "<"
149
+
150
+ tag_name = ah_to_xml(ary.shift)
151
+ result << tag_name
152
+
153
+ if Hash === ary.first
154
+ h = ary.shift
155
+ result << ah_to_xml_by_hash(h)
156
+ end
157
+
158
+ if ary.empty?
159
+ result << " />"
160
+ return result
161
+ end
162
+
163
+ result << ">"
164
+
165
+ ary.each do |item|
166
+ case item
167
+ when Array
168
+ result << "\n"
169
+ result << ah_to_xml_by_array(item).gsub(/^/, " ")
170
+ result << "\n"
171
+ else
172
+ result << ah_to_xml(item)
173
+ end
174
+ end
175
+
176
+ result << "</" + tag_name + ">"
177
+
178
+ return result
179
+ end
180
+
181
+ def self.ah_to_xml_by_symbol(symbol)
182
+ return symbol.to_s.gsub("__", ":").gsub("_", "-")
183
+ end
184
+
185
+ def self.ah_to_xml_by_hash(h)
186
+ return h.keys.sort_by { |item|
187
+ item.to_s
188
+ }.map { |key|
189
+ converted_key = ah_to_xml_by_symbol(key)
190
+
191
+ value = h[key]
192
+ converted_value = ah_to_xml_by_string(value)
193
+
194
+ " " + converted_key + "=" + '"' + converted_value + '"'
195
+ }.join
196
+ end
197
+
198
+ def self.ah_to_xml_by_string(s)
199
+ return CGI.escapeHTML(s.to_s)
200
+ end
201
+
202
+ def self.operate_styles_xml(io, additional_styles)
203
+ parser = REXML::Document.new(io.read)
204
+ office_styles = parser.elements["/office:document-styles/office:styles"]
205
+ additional_styles.each do |element|
206
+ office_styles.add_element(element)
207
+ end
208
+
209
+ io.rewind
210
+ io.truncate(0)
211
+ io.write(parser.to_s)
212
+ end
213
+
214
+ # create zip file by current directory.
215
+ def self.zip(output_path)
216
+ # if !system("zip", "-9qr", output_path, ".")
217
+ # raise "zip failure: #{output_path.inspect}"
218
+ # end
219
+ FileUtils.rm_f(output_path)
220
+ Zip::ZipFile.open(output_path, Zip::ZipFile::CREATE) do |zip_file|
221
+ Find.find(".") do |path_orig|
222
+ path = path_orig.sub(/\A\.\//, "") # remove "./"
223
+ if File.file?(path)
224
+ zip_file.get_output_stream(path) do |f|
225
+ f.write(File.read(path))
226
+ end
227
+ elsif File.directory?(path)
228
+ zip_file.mkdir(path)
229
+ end
230
+ end
231
+ end
232
+ end
233
+
234
+ # unzip to current directory.
235
+ def self.unzip(input_path)
236
+ # if !system("unzip", "-q", input_path)
237
+ # raise "unzip failure: #{input_path.inspect}"
238
+ # end
239
+ Zip::ZipFile.foreach(input_path) do |zip_entry|
240
+ path = zip_entry.name
241
+ if zip_entry.directory?
242
+ FileUtils.mkdir_p(path)
243
+ elsif zip_entry.file?
244
+ FileUtils.mkdir_p(File.dirname(path))
245
+ zip_entry.get_input_stream do |input|
246
+ open(path, "w") do |output|
247
+ output.write(input.read)
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end
253
+
254
+ class RD2ODTVisitor < RD::RDVisitor
255
+ attr_accessor :continue_numbering_headline
256
+
257
+ # for content.xml#/office:document-content/office:automatic-styles
258
+ attr_accessor :automatic_styles
259
+
260
+ # for styles.xml#/office:document-styles/office:styles
261
+ attr_accessor :additional_styles
262
+
263
+ attr_accessor :number_of_include_files
264
+
265
+ # included OLE objects
266
+ attr_accessor :inner_objects
267
+
268
+ #
269
+ attr_accessor :list_stack
270
+
271
+ def initialize(*args)
272
+ super
273
+
274
+ self.number_of_include_files = 0
275
+ self.additional_styles = []
276
+ self.automatic_styles = []
277
+ self.inner_objects = []
278
+ self.list_stack = []
279
+ end
280
+
281
+ def apply_to_DocumentElement(element, sub_content)
282
+ result =
283
+ [:office__document_content,
284
+ {
285
+ :xmlns__office =>
286
+ "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
287
+ :xmlns__style => "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
288
+ :xmlns__text => "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
289
+ :xmlns__table => "urn:oasis:names:tc:opendocument:xmlns:table:1.0",
290
+ :xmlns__draw => "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
291
+ :xmlns__fo =>
292
+ "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
293
+ :xmlns__xlink => "http://www.w3.org/1999/xlink",
294
+ :xmlns__dc => "http://purl.org/dc/elements/1.1/",
295
+ :xmlns__meta => "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
296
+ :xmlns__number =>
297
+ "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0",
298
+ :xmlns__svg =>
299
+ "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0",
300
+ :xmlns__chart => "urn:oasis:names:tc:opendocument:xmlns:chart:1.0",
301
+ :xmlns__dr3d => "urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0",
302
+ :xmlns__math => "http://www.w3.org/1998/Math/MathML",
303
+ :xmlns__form => "urn:oasis:names:tc:opendocument:xmlns:form:1.0",
304
+ :xmlns__script =>
305
+ "urn:oasis:names:tc:opendocument:xmlns:script:1.0",
306
+ :xmlns__ooo => "http://openoffice.org/2004/office",
307
+ :xmlns__ooow => "http://openoffice.org/2004/writer",
308
+ :xmlns__oooc => "http://openoffice.org/2004/calc",
309
+ :xmlns__dom => "http://www.w3.org/2001/xml-events",
310
+ :xmlns__xforms => "http://www.w3.org/2002/xforms",
311
+ :xmlns__xsd => "http://www.w3.org/2001/XMLSchema",
312
+ :xmlns__xsi => "http://www.w3.org/2001/XMLSchema-instance",
313
+ :xmlns__field =>
314
+ "urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:field:1.0",
315
+ :office__version => "1.1",
316
+ },
317
+ [:office__scripts],
318
+ [:office__font_face_decls,
319
+ [:style__font_face,
320
+ {
321
+ :style__name => "さざなみ明朝",
322
+ :svg__font_family => "さざなみ明朝",
323
+ :style__font_family_generic => "roman",
324
+ :style__font_pitch => "variable",
325
+ }],
326
+ [:style__font_face,
327
+ {
328
+ :style__name => "IPAゴシック",
329
+ :svg__font_family => "IPAゴシック",
330
+ :style__font_family_generic => "swiss",
331
+ :style__font_pitch => "variable",
332
+ }],
333
+ [:style__font_face,
334
+ {
335
+ :style__name => "IPAゴシック1",
336
+ :svg__font_family => "IPAゴシック",
337
+ :style__font_family_generic => "system",
338
+ :style__font_pitch => "variable",
339
+ }],
340
+ ], # :office__font_face_decls
341
+ [:office__automatic_styles,
342
+ *self.automatic_styles.map { |element|
343
+ element.to_s.to_sym
344
+ }],
345
+ [:office__body,
346
+ [:office__text,
347
+ [:text__sequence_decls,
348
+ [:text__sequence_decl,
349
+ {
350
+ :text__display_outline_level => "0",
351
+ :text__name => "Illustration",
352
+ }],
353
+ [:text__sequence_decl,
354
+ {
355
+ :text__display_outline_level => "0",
356
+ :text__name => "Table",
357
+ }],
358
+ [:text__sequence_decl,
359
+ {
360
+ :text__display_outline_level => "0",
361
+ :text__name => "Text",
362
+ }],
363
+ [:text__sequence_decl,
364
+ {
365
+ :text__display_outline_level => "0",
366
+ :text__name => "Drawing",
367
+ }],
368
+ ], # :text__sequence_decls
369
+ *sub_content
370
+ ], # :office__text
371
+ ], # :office__body
372
+ ] # :office__document_content
373
+ return result
374
+ end
375
+
376
+ def apply_to_TextBlock(element, sub_contents)
377
+ return [:text__p,
378
+ {:text__style_name => "Text_20_body"},
379
+ *sub_contents
380
+ ]
381
+ end
382
+
383
+ def apply_to_StringElement(element)
384
+ return element.content.gsub(/[\r\n]+/m, "")
385
+ end
386
+
387
+ def create_headline_result(title, original_level, current_level)
388
+ if current_level.zero?
389
+ return [:text__p,
390
+ {:text__style_name => "Heading_20_#{original_level}"},
391
+ *title
392
+ ]
393
+ else
394
+ return [:text__list,
395
+ {:text__continue_numbering => "true"},
396
+ [:text__list_item,
397
+ create_headline_result(title, original_level,
398
+ current_level - 1)
399
+ ],
400
+ ]
401
+ end
402
+ end
403
+ private :create_headline_result
404
+
405
+ def apply_to_Headline(element, title)
406
+ level = element.level
407
+ result = create_headline_result(title, level, level)
408
+ result[1][:text__style_name] = "Numbering_20_2"
409
+ if level == 1 && !continue_numbering_headline
410
+ result[1].delete(:text__continue_numbering)
411
+ end
412
+ self.continue_numbering_headline = true
413
+ return result
414
+ end
415
+
416
+ [:enum, :item].each do |s|
417
+ method_name = "visit_#{s.to_s.capitalize}List"
418
+ define_method(method_name) do |*args|
419
+ list_stack.push(s)
420
+ result = super
421
+ list_stack.pop
422
+ return result
423
+ end
424
+ end
425
+
426
+ def apply_to_EnumList(element, items)
427
+ return apply_to_list(items,
428
+ :text__style_name => "Numbering_20_1",
429
+ :text__continue_numbering => "false")
430
+ end
431
+
432
+ def apply_to_ItemList(element, items)
433
+ return apply_to_list(items, :text__style_name => "List_20_1")
434
+ end
435
+
436
+ def apply_to_list(items, attributes)
437
+ return [:text__list, attributes, *items]
438
+ end
439
+ private :apply_to_list
440
+
441
+ def apply_to_EnumListItem(element, sub_contents)
442
+ return apply_to_list_item(sub_contents)
443
+ end
444
+
445
+ def apply_to_ItemListItem(element, sub_contents)
446
+ return apply_to_list_item(sub_contents)
447
+ end
448
+
449
+ def apply_to_list_item(sub_contents)
450
+ return [:text__list_item, *sub_contents]
451
+ end
452
+ private :apply_to_list_item
453
+
454
+ def apply_to_Verbatim(element)
455
+ lines = element.content.map { |line|
456
+ escape_text(line.chomp)
457
+ }
458
+ return [:text__p,
459
+ {:text__style_name=>"Preformatted_20_Text"},
460
+ lines.join("<text:line-break />").to_sym,
461
+ ]
462
+ end
463
+
464
+ def escape_text(text)
465
+ return CGI.escapeHTML(text).gsub(/ {2,}/) {
466
+ num_space_chars = Regexp.last_match.to_s.length
467
+ %Q'<text:s text:c="#{num_space_chars}" />'
468
+ }.gsub("\t", "<text:tab />")
469
+ end
470
+ private :escape_text
471
+
472
+ DO_NOT_INCLUDE_TAG_NAMES = ["office:forms", "text:sequence-decls"]
473
+
474
+ def apply_to_Include(element)
475
+ self.number_of_include_files += 1
476
+ name_prefix = create_name_prefix
477
+ path = search_file(element.tree.include_paths, element.filename)
478
+
479
+ append_children = []
480
+ content_xml = read_file_in_zip(path, "content.xml")
481
+ parser = REXML::Document.new(content_xml)
482
+ office_text =
483
+ parser.elements["/office:document-content/office:body/office:text"]
484
+ apply_prefix_to_xlink_href(path, office_text, name_prefix) # todo: test
485
+ [
486
+ "text:style-name",
487
+ "table:style-name",
488
+ "table:name",
489
+ "draw:style-name",
490
+ ].each do |attribute_key|
491
+ apply_prefix_to_all_of_style_name(office_text, attribute_key,
492
+ name_prefix)
493
+ end
494
+ office_text.each_element do |child|
495
+ # may use XPath.
496
+ next if DO_NOT_INCLUDE_TAG_NAMES.include?(child.expanded_name)
497
+ append_children << child.to_s.to_sym
498
+ end
499
+
500
+ office_automatic_styles =
501
+ parser.elements["/office:document-content/office:automatic-styles"]
502
+ apply_prefix_to_all_of_style_name(office_automatic_styles,
503
+ "style:name", name_prefix)
504
+ office_automatic_styles.each_element do |child|
505
+ self.automatic_styles << child.deep_clone
506
+ end
507
+
508
+ styles_xml = read_file_in_zip(path, "styles.xml")
509
+ parser = REXML::Document.new(styles_xml)
510
+ office_styles = parser.elements["/office:document-styles/office:styles"]
511
+ [
512
+ "style:name",
513
+ "style:parent-style-name",
514
+ "style:display-name",
515
+ ].each do |attribute_key|
516
+ apply_prefix_to_all_of_style_name(office_styles, attribute_key,
517
+ name_prefix)
518
+ end
519
+ office_styles.elements.each("style:style") do |element|
520
+ self.additional_styles << element
521
+ end
522
+
523
+ return append_children
524
+ end
525
+
526
+ def search_file(include_paths, filename)
527
+ include_paths.each do |d|
528
+ path = File.join(d, filename)
529
+ return path if File.exist?(path)
530
+ end
531
+ raise "file not found: #{filename.inspect}, #{include_paths.inspect}"
532
+ end
533
+ private :search_file
534
+
535
+ def read_file_in_zip(zip_path, path_in_zip)
536
+ # return `unzip -c #{zip_path} #{path_in_zip}`
537
+ Zip::ZipFile.open(zip_path) do |zip_file|
538
+ return zip_file.read(path_in_zip)
539
+ end
540
+ end
541
+ private :read_file_in_zip
542
+
543
+ def create_name_prefix
544
+ t = Time.now
545
+ return sprintf("rd2odt:%d:%06d:%d:",
546
+ t.tv_sec, t.tv_usec, number_of_include_files)
547
+ end
548
+ private :create_name_prefix
549
+
550
+ def apply_prefix_to_all_of_style_name(start_element, attribute_key,
551
+ name_prefix)
552
+ start_element.elements.each("//*[@#{attribute_key}]") do |element|
553
+ element.attributes[attribute_key] =
554
+ name_prefix + element.attributes[attribute_key]
555
+ end
556
+ end
557
+ private :apply_prefix_to_all_of_style_name
558
+
559
+ # todo: test
560
+ def apply_prefix_to_xlink_href(path, office_text, name_prefix)
561
+ # <draw:object> and <draw:image>
562
+ office_text.elements.each("//*[@xlink:href]") do |element|
563
+ href = element.attributes["xlink:href"]
564
+ fixed_href = File.join(File.dirname(href),
565
+ name_prefix + File.basename(href))
566
+ element.attributes["xlink:href"] = fixed_href
567
+ self.inner_objects << InnerObject.new(path, href, fixed_href)
568
+ end
569
+ end
570
+ private :apply_prefix_to_xlink_href
571
+ end
572
+
573
+ InnerObject = Struct.new(:path, :href, :fixed_href)
574
+
575
+ class Error < StandardError
576
+ def process
577
+ if RD2ODT.options[:backtrace]
578
+ STDERR.puts("backtrace:")
579
+ STDERR.puts(backtrace.map { |l|
580
+ " " + l
581
+ })
582
+ end
583
+ STDERR.puts(message)
584
+ exit(1)
585
+ end
586
+ end
587
+
588
+ class ProgramOptionError < Error
589
+ ADDITIONAL_MESSAGE = [RD2ODT::OPTION_PARSER.banner,
590
+ "use #{RD2ODT::OPTION_PARSER.program_name} --help for more help."]
591
+
592
+ def message
593
+ return [super, "", *ADDITIONAL_MESSAGE]
594
+ end
595
+ end
596
+
597
+ class ProgramOptionParseError < ProgramOptionError
598
+ def initialize(e)
599
+ @e = e
600
+ end
601
+
602
+ def message
603
+ return [@e.message, "", *ADDITIONAL_MESSAGE]
604
+ end
605
+
606
+ def backtrace
607
+ return @e.backtrace
608
+ end
609
+ end
610
+ end
data/rd2odt.gemspec ADDED
@@ -0,0 +1,63 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "rd2odt"
3
+ s.version = "0.0.1"
4
+
5
+ s.authors = "Yuya.Nishida."
6
+ s.email = "yuyaAT@ATj96DOT.DOTorg"
7
+
8
+ s.rubyforge_project = s.name
9
+ s.homepage = "http://rubyforge.org/projects/#{s.name}/"
10
+ s.platform = Gem::Platform::RUBY
11
+ s.required_ruby_version = ">= 1.8.7"
12
+ s.requirements << "rubyzip"
13
+ s.require_path = "lib"
14
+ # s.autorequire = "rake"
15
+ # s.has_rdoc = true
16
+ # s.extra_rdoc_files = ["README"]
17
+ s.executable = "rd2odt"
18
+
19
+ s.summary = "RD(Ruby Document) to OpenDocument converter."
20
+ s.description = <<EOF
21
+ #{s.summary}
22
+ EOF
23
+
24
+ s.files = ["bin/rd2odt",
25
+
26
+ # "lib/**/*.rb",
27
+ "lib/rd2odt.rb",
28
+
29
+ # "doc/**/[a-z]*.rd*",
30
+ "doc/sample.rd.ja",
31
+ "doc/sample.rd.ja.ott",
32
+ "doc/sample.rd.ja.pdf",
33
+ "doc/sample/body-text.rd",
34
+ "doc/sample/enum-list-over-headline-multi-level.rd",
35
+ "doc/sample/enum-list-over-headline.rd",
36
+ "doc/sample/enum-list-over-item-list-multi-level-2.rd",
37
+ "doc/sample/enum-list-over-item-list-multi-level.rd",
38
+ "doc/sample/enum-list-over-item-list.rd",
39
+ "doc/sample/headline.rd",
40
+ "doc/sample/include.rd",
41
+ "doc/sample/list.rd",
42
+ "doc/sample/multi-paragraph.rd",
43
+ "doc/sample/verbatim.rd",
44
+ "doc/specification.ja.rd",
45
+
46
+ # "test/**/*.rb",
47
+ "test/functional/rd2odt-spec.rb",
48
+ "test/test-helper.rb",
49
+ "test/unit/rd2odt-spec.rb",
50
+ "rd2odt.gemspec",
51
+ "Rakefile",
52
+ "README",
53
+ "FUTURE",
54
+ "NEWS",
55
+ "LICENSE",
56
+ "setup.rb"]
57
+ end
58
+
59
+ # Editor settings
60
+ # - Emacs -
61
+ # local variables:
62
+ # mode: Ruby
63
+ # end: