ruh-roo 3.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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +677 -0
  3. data/Gemfile +24 -0
  4. data/LICENSE +24 -0
  5. data/README.md +315 -0
  6. data/lib/roo/base.rb +607 -0
  7. data/lib/roo/constants.rb +7 -0
  8. data/lib/roo/csv.rb +141 -0
  9. data/lib/roo/errors.rb +11 -0
  10. data/lib/roo/excelx/cell/base.rb +108 -0
  11. data/lib/roo/excelx/cell/boolean.rb +30 -0
  12. data/lib/roo/excelx/cell/date.rb +28 -0
  13. data/lib/roo/excelx/cell/datetime.rb +107 -0
  14. data/lib/roo/excelx/cell/empty.rb +20 -0
  15. data/lib/roo/excelx/cell/number.rb +89 -0
  16. data/lib/roo/excelx/cell/string.rb +19 -0
  17. data/lib/roo/excelx/cell/time.rb +44 -0
  18. data/lib/roo/excelx/cell.rb +110 -0
  19. data/lib/roo/excelx/comments.rb +55 -0
  20. data/lib/roo/excelx/coordinate.rb +19 -0
  21. data/lib/roo/excelx/extractor.rb +39 -0
  22. data/lib/roo/excelx/format.rb +71 -0
  23. data/lib/roo/excelx/images.rb +26 -0
  24. data/lib/roo/excelx/relationships.rb +33 -0
  25. data/lib/roo/excelx/shared.rb +39 -0
  26. data/lib/roo/excelx/shared_strings.rb +151 -0
  27. data/lib/roo/excelx/sheet.rb +151 -0
  28. data/lib/roo/excelx/sheet_doc.rb +248 -0
  29. data/lib/roo/excelx/styles.rb +64 -0
  30. data/lib/roo/excelx/workbook.rb +63 -0
  31. data/lib/roo/excelx.rb +480 -0
  32. data/lib/roo/font.rb +17 -0
  33. data/lib/roo/formatters/base.rb +15 -0
  34. data/lib/roo/formatters/csv.rb +84 -0
  35. data/lib/roo/formatters/matrix.rb +23 -0
  36. data/lib/roo/formatters/xml.rb +31 -0
  37. data/lib/roo/formatters/yaml.rb +40 -0
  38. data/lib/roo/helpers/default_attr_reader.rb +20 -0
  39. data/lib/roo/helpers/weak_instance_cache.rb +41 -0
  40. data/lib/roo/libre_office.rb +4 -0
  41. data/lib/roo/link.rb +34 -0
  42. data/lib/roo/open_office.rb +628 -0
  43. data/lib/roo/spreadsheet.rb +39 -0
  44. data/lib/roo/tempdir.rb +21 -0
  45. data/lib/roo/utils.rb +128 -0
  46. data/lib/roo/version.rb +3 -0
  47. data/lib/roo.rb +36 -0
  48. data/roo.gemspec +28 -0
  49. metadata +189 -0
data/lib/roo/base.rb ADDED
@@ -0,0 +1,607 @@
1
+ require "tmpdir"
2
+ require "stringio"
3
+ require "nokogiri"
4
+ require "roo/utils"
5
+ require "roo/formatters/base"
6
+ require "roo/formatters/csv"
7
+ require "roo/formatters/matrix"
8
+ require "roo/formatters/xml"
9
+ require "roo/formatters/yaml"
10
+
11
+ # Base class for all other types of spreadsheets
12
+ class Roo::Base
13
+ include Enumerable
14
+ include Roo::Formatters::Base
15
+ include Roo::Formatters::CSV
16
+ include Roo::Formatters::Matrix
17
+ include Roo::Formatters::XML
18
+ include Roo::Formatters::YAML
19
+
20
+ MAX_ROW_COL = 999_999
21
+ MIN_ROW_COL = 0
22
+
23
+ attr_reader :headers
24
+
25
+ # sets the line with attribute names (default: 1)
26
+ attr_accessor :header_line
27
+
28
+ def self.TEMP_PREFIX
29
+ warn "[DEPRECATION] please access TEMP_PREFIX via Roo::TEMP_PREFIX"
30
+ Roo::TEMP_PREFIX
31
+ end
32
+
33
+ def self.finalize(object_id)
34
+ proc { finalize_tempdirs(object_id) }
35
+ end
36
+
37
+ def initialize(filename, options = {}, _file_warning = :error, _tmpdir = nil)
38
+ @filename = filename
39
+ @options = options
40
+
41
+ @cell = {}
42
+ @cell_type = {}
43
+ @cells_read = {}
44
+
45
+ @first_row = {}
46
+ @last_row = {}
47
+ @first_column = {}
48
+ @last_column = {}
49
+
50
+ @header_line = 1
51
+ end
52
+
53
+ def close
54
+ if self.class.respond_to?(:finalize_tempdirs)
55
+ self.class.finalize_tempdirs(object_id)
56
+ end
57
+
58
+ instance_variables.each do |instance_variable|
59
+ instance_variable_set(instance_variable, nil)
60
+ end
61
+
62
+ nil
63
+ end
64
+
65
+ def default_sheet
66
+ @default_sheet ||= sheets.first
67
+ end
68
+
69
+ # sets the working sheet in the document
70
+ # 'sheet' can be a number (0 = first sheet) or the name of a sheet.
71
+ def default_sheet=(sheet)
72
+ validate_sheet!(sheet)
73
+ @default_sheet = sheet.is_a?(String) ? sheet : sheets[sheet]
74
+ @first_row[sheet] = @last_row[sheet] = @first_column[sheet] = @last_column[sheet] = nil
75
+ @cells_read[sheet] = false
76
+ end
77
+
78
+ # first non-empty column as a letter
79
+ def first_column_as_letter(sheet = default_sheet)
80
+ ::Roo::Utils.number_to_letter(first_column(sheet))
81
+ end
82
+
83
+ # last non-empty column as a letter
84
+ def last_column_as_letter(sheet = default_sheet)
85
+ ::Roo::Utils.number_to_letter(last_column(sheet))
86
+ end
87
+
88
+ # Set first/last row/column for sheet
89
+ def first_last_row_col_for_sheet(sheet)
90
+ @first_last_row_cols ||= {}
91
+ @first_last_row_cols[sheet] ||= begin
92
+ result = collect_last_row_col_for_sheet(sheet)
93
+ {
94
+ first_row: result[:first_row] == MAX_ROW_COL ? nil : result[:first_row],
95
+ first_column: result[:first_column] == MAX_ROW_COL ? nil : result[:first_column],
96
+ last_row: result[:last_row] == MIN_ROW_COL ? nil : result[:last_row],
97
+ last_column: result[:last_column] == MIN_ROW_COL ? nil : result[:last_column]
98
+ }
99
+ end
100
+ end
101
+
102
+ # Collect first/last row/column from sheet
103
+ def collect_last_row_col_for_sheet(sheet)
104
+ first_row = first_column = MAX_ROW_COL
105
+ last_row = last_column = MIN_ROW_COL
106
+ @cell[sheet].each_pair do |key, value|
107
+ next unless value
108
+ first_row = [first_row, key.first.to_i].min
109
+ last_row = [last_row, key.first.to_i].max
110
+ first_column = [first_column, key.last.to_i].min
111
+ last_column = [last_column, key.last.to_i].max
112
+ end if @cell[sheet]
113
+ { first_row: first_row, first_column: first_column, last_row: last_row, last_column: last_column }
114
+ end
115
+
116
+ %i(first_row last_row first_column last_column).each do |key|
117
+ ivar = "@#{key}".to_sym
118
+ define_method(key) do |sheet = default_sheet|
119
+ read_cells(sheet)
120
+ instance_variable_get(ivar)[sheet] ||= first_last_row_col_for_sheet(sheet)[key]
121
+ end
122
+ end
123
+
124
+ def inspect
125
+ "<##{self.class}:#{object_id.to_s(8)} #{instance_variables.join(' ')}>"
126
+ end
127
+
128
+ # find a row either by row number or a condition
129
+ # Caution: this works only within the default sheet -> set default_sheet before you call this method
130
+ # (experimental. see examples in the test_roo.rb file)
131
+ def find(*args) # :nodoc
132
+ options = (args.last.is_a?(Hash) ? args.pop : {})
133
+
134
+ case args[0]
135
+ when Integer
136
+ find_by_row(args[0])
137
+ when :all
138
+ find_by_conditions(options)
139
+ else
140
+ fail ArgumentError, "unexpected arg #{args[0].inspect}, pass a row index or :all"
141
+ end
142
+ end
143
+
144
+ # returns all values in this row as an array
145
+ # row numbers are 1,2,3,... like in the spreadsheet
146
+ def row(row_number, sheet = default_sheet)
147
+ read_cells(sheet)
148
+ first_column(sheet).upto(last_column(sheet)).map do |col|
149
+ cell(row_number, col, sheet)
150
+ end
151
+ end
152
+
153
+ # returns all values in this column as an array
154
+ # column numbers are 1,2,3,... like in the spreadsheet
155
+ def column(column_number, sheet = default_sheet)
156
+ if column_number.is_a?(::String)
157
+ column_number = ::Roo::Utils.letter_to_number(column_number)
158
+ end
159
+ read_cells(sheet)
160
+ first_row(sheet).upto(last_row(sheet)).map do |row|
161
+ cell(row, column_number, sheet)
162
+ end
163
+ end
164
+
165
+ # set a cell to a certain value
166
+ # (this will not be saved back to the spreadsheet file!)
167
+ def set(row, col, value, sheet = default_sheet) #:nodoc:
168
+ read_cells(sheet)
169
+ row, col = normalize(row, col)
170
+ cell_type = cell_type_by_value(value)
171
+ set_value(row, col, value, sheet)
172
+ set_type(row, col, cell_type, sheet)
173
+ end
174
+
175
+ def cell_type_by_value(value)
176
+ case value
177
+ when Integer then :float
178
+ when String, Float then :string
179
+ else
180
+ fail ArgumentError, "Type for #{value} not set"
181
+ end
182
+ end
183
+
184
+ # reopens and read a spreadsheet document
185
+ def reload
186
+ ds = default_sheet
187
+ reinitialize
188
+ self.default_sheet = ds
189
+ end
190
+
191
+ # true if cell is empty
192
+ def empty?(row, col, sheet = default_sheet)
193
+ read_cells(sheet)
194
+ row, col = normalize(row, col)
195
+ contents = cell(row, col, sheet)
196
+ !contents || (celltype(row, col, sheet) == :string && contents.empty?) \
197
+ || (row < first_row(sheet) || row > last_row(sheet) || col < first_column(sheet) || col > last_column(sheet))
198
+ end
199
+
200
+ # returns information of the spreadsheet document and all sheets within
201
+ # this document.
202
+ def info
203
+ without_changing_default_sheet do
204
+ result = "File: #{File.basename(@filename)}\n"\
205
+ "Number of sheets: #{sheets.size}\n"\
206
+ "Sheets: #{sheets.join(', ')}\n"
207
+ n = 1
208
+ sheets.each do |sheet|
209
+ self.default_sheet = sheet
210
+ result << "Sheet " + n.to_s + ":\n"
211
+ if first_row
212
+ result << " First row: #{first_row}\n"
213
+ result << " Last row: #{last_row}\n"
214
+ result << " First column: #{::Roo::Utils.number_to_letter(first_column)}\n"
215
+ result << " Last column: #{::Roo::Utils.number_to_letter(last_column)}"
216
+ else
217
+ result << " - empty -"
218
+ end
219
+ result << "\n" if sheet != sheets.last
220
+ n += 1
221
+ end
222
+ result
223
+ end
224
+ end
225
+
226
+ # when a method like spreadsheet.a42 is called
227
+ # convert it to a call of spreadsheet.cell('a',42)
228
+ def method_missing(m, *args)
229
+ # #aa42 => #cell('aa',42)
230
+ # #aa42('Sheet1') => #cell('aa',42,'Sheet1')
231
+ if m =~ /^([a-z]+)(\d+)$/
232
+ col = ::Roo::Utils.letter_to_number(Regexp.last_match[1])
233
+ row = Regexp.last_match[2].to_i
234
+ if args.empty?
235
+ cell(row, col)
236
+ else
237
+ cell(row, col, args.first)
238
+ end
239
+ else
240
+ super
241
+ end
242
+ end
243
+
244
+ # access different worksheets by calling spreadsheet.sheet(1)
245
+ # or spreadsheet.sheet('SHEETNAME')
246
+ def sheet(index, name = false)
247
+ self.default_sheet = index.is_a?(::String) ? index : sheets[index]
248
+ name ? [default_sheet, self] : self
249
+ end
250
+
251
+ # iterate through all worksheets of a document
252
+ def each_with_pagename
253
+ sheets.each do |s|
254
+ yield sheet(s, true)
255
+ end
256
+ end
257
+
258
+ # by passing in headers as options, this method returns
259
+ # specific columns from your header assignment
260
+ # for example:
261
+ # xls.sheet('New Prices').parse(:upc => 'UPC', :price => 'Price') would return:
262
+ # [{:upc => 123456789012, :price => 35.42},..]
263
+
264
+ # the queries are matched with regex, so regex options can be passed in
265
+ # such as :price => '^(Cost|Price)'
266
+ # case insensitive by default
267
+
268
+ # by using the :header_search option, you can query for headers
269
+ # and return a hash of every row with the keys set to the header result
270
+ # for example:
271
+ # xls.sheet('New Prices').parse(:header_search => ['UPC*SKU','^Price*\sCost\s'])
272
+
273
+ # that example searches for a column titled either UPC or SKU and another
274
+ # column titled either Price or Cost (regex characters allowed)
275
+ # * is the wildcard character
276
+
277
+ # you can also pass in a :clean => true option to strip the sheet of
278
+ # control characters and white spaces around columns
279
+
280
+ def each(options = {})
281
+ return to_enum(:each, options) unless block_given?
282
+
283
+ if options.empty?
284
+ 1.upto(last_row) do |line|
285
+ yield row(line)
286
+ end
287
+ else
288
+ clean_sheet_if_need(options)
289
+ search_or_set_header(options)
290
+ headers = @headers ||
291
+ (first_column..last_column).each_with_object({}) do |col, hash|
292
+ hash[cell(@header_line, col)] = col
293
+ end
294
+
295
+ @header_line.upto(last_row) do |line|
296
+ yield(headers.each_with_object({}) { |(k, v), hash| hash[k] = cell(line, v) })
297
+ end
298
+ end
299
+ end
300
+
301
+ def parse(options = {})
302
+ results = each(options).map do |row|
303
+ block_given? ? yield(row) : row
304
+ end
305
+
306
+ options[:headers] == true ? results : results.drop(1)
307
+ end
308
+
309
+ def row_with(query, return_headers = false)
310
+ line_no = 0
311
+ closest_mismatched_headers = []
312
+ each do |row|
313
+ line_no += 1
314
+ headers = query.map { |q| row.grep(q)[0] }.compact
315
+ if headers.length == query.length
316
+ @header_line = line_no
317
+ return return_headers ? headers : line_no
318
+ else
319
+ closest_mismatched_headers = headers if headers.length > closest_mismatched_headers.length
320
+ if line_no > 100
321
+ break
322
+ end
323
+ end
324
+ end
325
+ missing_headers = query.select { |q| closest_mismatched_headers.grep(q).empty? }
326
+ raise Roo::HeaderRowNotFoundError, missing_headers
327
+ end
328
+
329
+ protected
330
+
331
+ def file_type_check(filename, exts, name, warning_level, packed = nil)
332
+ if packed == :zip
333
+ # spreadsheet.ods.zip => spreadsheet.ods
334
+ # Decompression is not performed here, only the 'zip' extension
335
+ # is removed from the file.
336
+ filename = File.basename(filename, File.extname(filename))
337
+ end
338
+
339
+ if uri?(filename) && (qs_begin = filename.rindex("?"))
340
+ filename = filename[0..qs_begin - 1]
341
+ end
342
+ exts = Array(exts)
343
+
344
+ return if exts.include?(File.extname(filename).downcase)
345
+
346
+ case warning_level
347
+ when :error
348
+ warn file_type_warning_message(filename, exts)
349
+ fail TypeError, "#{filename} is not #{name} file"
350
+ when :warning
351
+ warn "are you sure, this is #{name} spreadsheet file?"
352
+ warn file_type_warning_message(filename, exts)
353
+ when :ignore
354
+ # ignore
355
+ else
356
+ fail "#{warning_level} illegal state of file_warning"
357
+ end
358
+ end
359
+
360
+ # konvertiert einen Key in der Form "12,45" (=row,column) in
361
+ # ein Array mit numerischen Werten ([12,45])
362
+ # Diese Methode ist eine temp. Loesung, um zu erforschen, ob der
363
+ # Zugriff mit numerischen Keys schneller ist.
364
+ def key_to_num(str)
365
+ r, c = str.split(",")
366
+ [r.to_i, c.to_i]
367
+ end
368
+
369
+ # see: key_to_num
370
+ def key_to_string(arr)
371
+ "#{arr[0]},#{arr[1]}"
372
+ end
373
+
374
+ def is_stream?(filename_or_stream)
375
+ filename_or_stream.respond_to?(:seek)
376
+ end
377
+
378
+ private
379
+
380
+ def clean_sheet_if_need(options)
381
+ return unless options[:clean]
382
+ options.delete(:clean)
383
+ @cleaned ||= {}
384
+ clean_sheet(default_sheet) unless @cleaned[default_sheet]
385
+ end
386
+
387
+ def search_or_set_header(options)
388
+ if options[:header_search]
389
+ @headers = nil
390
+ @header_line = row_with(options[:header_search])
391
+ elsif [:first_row, true].include?(options[:headers])
392
+ @headers = []
393
+ row(first_row).each_with_index { |x, i| @headers << [x, i + 1] }
394
+ else
395
+ set_headers(options)
396
+ end
397
+ end
398
+
399
+ def local_filename(filename, tmpdir, packed)
400
+ return if is_stream?(filename)
401
+ filename = download_uri(filename, tmpdir) if uri?(filename)
402
+ filename = unzip(filename, tmpdir) if packed == :zip
403
+
404
+ fail IOError, "file #{filename} does not exist" unless File.file?(filename)
405
+
406
+ filename
407
+ end
408
+
409
+ def file_type_warning_message(filename, exts)
410
+ *rest, last_ext = exts
411
+ ext_list = rest.any? ? "#{rest.join(', ')} or #{last_ext}" : last_ext
412
+ "use #{Roo::CLASS_FOR_EXTENSION.fetch(last_ext.sub('.', '').to_sym)}.new to handle #{ext_list} spreadsheet files. This has #{File.extname(filename).downcase}"
413
+ rescue KeyError
414
+ raise "unknown file types: #{ext_list}"
415
+ end
416
+
417
+ def find_by_row(row_index)
418
+ row_index += (header_line - 1) if @header_line
419
+
420
+ row(row_index).size.times.map do |cell_index|
421
+ cell(row_index, cell_index + 1)
422
+ end
423
+ end
424
+
425
+ def find_by_conditions(options)
426
+ rows = first_row.upto(last_row)
427
+ header_for = 1.upto(last_column).each_with_object({}) do |col, hash|
428
+ hash[col] = cell(@header_line, col)
429
+ end
430
+
431
+ # are all conditions met?
432
+ conditions = options[:conditions]
433
+ if conditions && !conditions.empty?
434
+ column_with = header_for.invert
435
+ rows = rows.select do |i|
436
+ conditions.all? { |key, val| cell(i, column_with[key]) == val }
437
+ end
438
+ end
439
+
440
+ if options[:array]
441
+ rows.map { |i| row(i) }
442
+ else
443
+ rows.map do |i|
444
+ 1.upto(row(i).size).each_with_object({}) do |j, hash|
445
+ hash[header_for.fetch(j)] = cell(i, j)
446
+ end
447
+ end
448
+ end
449
+ end
450
+
451
+ def without_changing_default_sheet
452
+ original_default_sheet = default_sheet
453
+ yield
454
+ ensure
455
+ self.default_sheet = original_default_sheet
456
+ end
457
+
458
+ def reinitialize
459
+ initialize(@filename)
460
+ end
461
+
462
+ def find_basename(filename)
463
+ if uri?(filename)
464
+ require "uri"
465
+ uri = URI.parse filename
466
+ File.basename(uri.path)
467
+ elsif !is_stream?(filename)
468
+ File.basename(filename)
469
+ end
470
+ end
471
+
472
+ def make_tmpdir(prefix = nil, root = nil, &block)
473
+ warn "[DEPRECATION] extend Roo::Tempdir and use its .make_tempdir instead"
474
+ prefix = "#{Roo::TEMP_PREFIX}#{prefix}"
475
+ root ||= ENV["ROO_TMP"]
476
+
477
+ if block_given?
478
+ # folder is deleted at end of block
479
+ ::Dir.mktmpdir(prefix, root, &block)
480
+ else
481
+ self.class.make_tempdir(self, prefix, root)
482
+ end
483
+ end
484
+
485
+ def clean_sheet(sheet)
486
+ read_cells(sheet)
487
+ @cell[sheet].each_pair do |coord, value|
488
+ @cell[sheet][coord] = sanitize_value(value) if value.is_a?(::String)
489
+ end
490
+ @cleaned[sheet] = true
491
+ end
492
+
493
+ def sanitize_value(v)
494
+ v.gsub(/[[:cntrl:]]|^[\p{Space}]+|[\p{Space}]+$/, "")
495
+ end
496
+
497
+ def set_headers(hash = {})
498
+ # try to find header row with all values or give an error
499
+ # then create new hash by indexing strings and keeping integers for header array
500
+ header_row = row_with(hash.values, true)
501
+ @headers = {}
502
+ hash.each_with_index do |(key, _), index|
503
+ @headers[key] = header_index(header_row[index])
504
+ end
505
+ end
506
+
507
+ def header_index(query)
508
+ row(@header_line).index(query) + first_column
509
+ end
510
+
511
+ def set_value(row, col, value, sheet = default_sheet)
512
+ @cell[sheet][[row, col]] = value
513
+ end
514
+
515
+ def set_type(row, col, type, sheet = default_sheet)
516
+ @cell_type[sheet][[row, col]] = type
517
+ end
518
+
519
+ # converts cell coordinate to numeric values of row,col
520
+ def normalize(row, col)
521
+ if row.is_a?(::String)
522
+ if col.is_a?(::Integer)
523
+ # ('A',1):
524
+ # ('B', 5) -> (5, 2)
525
+ row, col = col, row
526
+ else
527
+ fail ArgumentError
528
+ end
529
+ end
530
+
531
+ col = ::Roo::Utils.letter_to_number(col) if col.is_a?(::String)
532
+
533
+ [row, col]
534
+ end
535
+
536
+ def uri?(filename)
537
+ filename.start_with?("http://", "https://", "ftp://")
538
+ rescue
539
+ false
540
+ end
541
+
542
+ def download_uri(uri, tmpdir)
543
+ require "open-uri"
544
+ tempfilename = File.join(tmpdir, find_basename(uri))
545
+ begin
546
+ File.open(tempfilename, "wb") do |file|
547
+ URI.open(uri, "User-Agent" => "Ruby/#{RUBY_VERSION}") do |net|
548
+ file.write(net.read)
549
+ end
550
+ end
551
+ rescue OpenURI::HTTPError
552
+ raise "could not open #{uri}"
553
+ end
554
+ tempfilename
555
+ end
556
+
557
+ def open_from_stream(stream, tmpdir)
558
+ tempfilename = File.join(tmpdir, "spreadsheet")
559
+ File.open(tempfilename, "wb") do |file|
560
+ file.write(stream[7..-1])
561
+ end
562
+ File.join(tmpdir, "spreadsheet")
563
+ end
564
+
565
+ def unzip(filename, tmpdir)
566
+ require "zip/filesystem"
567
+
568
+ Zip::File.open(filename) do |zip|
569
+ process_zipfile_packed(zip, tmpdir)
570
+ end
571
+ end
572
+
573
+ # check if default_sheet was set and exists in sheets-array
574
+ def validate_sheet!(sheet)
575
+ case sheet
576
+ when nil
577
+ fail ArgumentError, "Error: sheet 'nil' not valid"
578
+ when Integer
579
+ sheets.fetch(sheet) do
580
+ fail RangeError, "sheet index #{sheet} not found"
581
+ end
582
+ when String
583
+ unless sheets.include?(sheet)
584
+ fail RangeError, "sheet '#{sheet}' not found"
585
+ end
586
+ else
587
+ fail TypeError, "not a valid sheet type: #{sheet.inspect}"
588
+ end
589
+ end
590
+
591
+ def process_zipfile_packed(zip, tmpdir, path = "")
592
+ if zip.file.file? path
593
+ # extract and return filename
594
+ File.open(File.join(tmpdir, path), "wb") do |file|
595
+ file.write(zip.read(path))
596
+ end
597
+ File.join(tmpdir, path)
598
+ else
599
+ ret = nil
600
+ path += "/" unless path.empty?
601
+ zip.dir.foreach(path) do |filename|
602
+ ret = process_zipfile_packed(zip, tmpdir, path + filename)
603
+ end
604
+ ret
605
+ end
606
+ end
607
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roo
4
+ ROO_EXCEL_NOTICE = "Excel support has been extracted to roo-xls due to its dependency on the GPL'd spreadsheet gem. Install roo-xls to use Roo::Excel."
5
+ ROO_EXCELML_NOTICE = "Excel SpreadsheetML support has been extracted to roo-xls. Install roo-xls to use Roo::Excel2003XML."
6
+ ROO_GOOGLE_NOTICE = "Google support has been extracted to roo-google. Install roo-google to use Roo::Google."
7
+ end