ruh-roo 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
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