roo-oxide 0.0.2
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +63 -0
- data/Cargo.lock +341 -0
- data/Cargo.toml +7 -0
- data/LICENSE +21 -0
- data/README.md +220 -0
- data/ext/roo_oxide/Cargo.toml +19 -0
- data/ext/roo_oxide/extconf.rb +14 -0
- data/ext/roo_oxide/src/archive.rs +546 -0
- data/ext/roo_oxide/src/convert/cells.rs +516 -0
- data/ext/roo_oxide/src/convert/csv.rs +209 -0
- data/ext/roo_oxide/src/convert.rs +687 -0
- data/ext/roo_oxide/src/lib.rs +306 -0
- data/ext/roo_oxide/src/numeric.rs +174 -0
- data/ext/roo_oxide/src/reader/attributes.rs +199 -0
- data/ext/roo_oxide/src/reader/tests.rs +475 -0
- data/ext/roo_oxide/src/reader.rs +944 -0
- data/ext/roo_oxide/src/shared_strings.rs +684 -0
- data/ext/roo_oxide/src/sheet_parts.rs +312 -0
- data/ext/roo_oxide/src/store/formatters.rs +177 -0
- data/ext/roo_oxide/src/store.rs +894 -0
- data/lib/roo/calamine/version.rb +5 -0
- data/lib/roo/calamine.rb +3 -0
- data/lib/roo/oxide/context.rb +74 -0
- data/lib/roo/oxide/csv.rb +157 -0
- data/lib/roo/oxide/deferred_sheets.rb +284 -0
- data/lib/roo/oxide/excelx.rb +156 -0
- data/lib/roo/oxide/formatters.rb +114 -0
- data/lib/roo/oxide/row_index.rb +93 -0
- data/lib/roo/oxide/row_iteration.rb +33 -0
- data/lib/roo/oxide/shared_strings.rb +46 -0
- data/lib/roo/oxide/sheet.rb +99 -0
- data/lib/roo/oxide/sheet_doc.rb +138 -0
- data/lib/roo/oxide/sheet_parts.rb +78 -0
- data/lib/roo/oxide/version.rb +7 -0
- data/lib/roo/oxide.rb +100 -0
- metadata +110 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Roo
|
|
4
|
+
module Oxide
|
|
5
|
+
# Roo 4.0.0's output formatting, using the native records while the
|
|
6
|
+
# sheet's public cells hash has not been materialized.
|
|
7
|
+
module Formatters
|
|
8
|
+
def formatted_value(row, col, sheet = nil)
|
|
9
|
+
row, col = normalize(row, col) if row.is_a?(String) || col.is_a?(String)
|
|
10
|
+
cell = sheet_for(sheet).cell_at(row, col)
|
|
11
|
+
return formatter_temporal_value(cell) if native_formatters? && cell.is_a?(Roo::Excelx::Cell::DateTime)
|
|
12
|
+
|
|
13
|
+
cell.formatted_value if cell.respond_to?(:formatted_value)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_yaml(prefix = {}, from_row = nil, from_column = nil, to_row = nil, to_column = nil, sheet = default_sheet) # rubocop:disable Metrics/ParameterLists
|
|
17
|
+
return super unless native_formatters?
|
|
18
|
+
return "" unless first_row
|
|
19
|
+
|
|
20
|
+
area = formatter_area(sheet, [from_row, from_column, to_row, to_column])
|
|
21
|
+
store = sheet_for(sheet).lazy_cells
|
|
22
|
+
return super unless store && area.all?(Integer)
|
|
23
|
+
|
|
24
|
+
store.to_yaml(self, prefix, area)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_matrix(from_row = nil, from_column = nil, to_row = nil, to_column = nil, sheet = default_sheet) # rubocop:disable Metrics/ParameterLists
|
|
28
|
+
return super unless native_formatters?
|
|
29
|
+
|
|
30
|
+
require "matrix"
|
|
31
|
+
return ::Matrix.empty unless first_row
|
|
32
|
+
|
|
33
|
+
area = formatter_rectangle(sheet, [from_row, from_column, to_row, to_column])
|
|
34
|
+
store = sheet_for(sheet).lazy_cells
|
|
35
|
+
return super unless store && area.all?(Integer)
|
|
36
|
+
|
|
37
|
+
::Matrix.rows(store.matrix_rows(area))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_xml
|
|
41
|
+
return super unless native_formatters?
|
|
42
|
+
return super if sheets.empty?
|
|
43
|
+
|
|
44
|
+
targets = sheets.map { |name| sheet_for(name) }
|
|
45
|
+
return super unless targets.all?(&:lazy_cells)
|
|
46
|
+
|
|
47
|
+
result = +"<?xml version=\"1.0\"?>\n<spreadsheet>\n"
|
|
48
|
+
document = Nokogiri::XML::Document.new
|
|
49
|
+
sheets.zip(targets).each do |name, target|
|
|
50
|
+
self.default_sheet = name
|
|
51
|
+
result << formatter_xml_sheet(document, name, target)
|
|
52
|
+
end
|
|
53
|
+
result << "</spreadsheet>\n"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def formatter_temporal_value(cell)
|
|
59
|
+
if cell.is_a?(Roo::Excelx::Cell::Time)
|
|
60
|
+
@oxide_time_formats ||= Hash.new { |formats, encoding| formats[encoding] = {} }
|
|
61
|
+
formatter = @oxide_time_formats[cell.format.encoding][cell.format] ||=
|
|
62
|
+
cell.format.gsub(/#{Roo::Excelx::Cell::DateTime::TIME_FORMATS.keys.join("|")}/,
|
|
63
|
+
Roo::Excelx::Cell::DateTime::TIME_FORMATS)
|
|
64
|
+
return cell.instance_variable_get(:@datetime).strftime(formatter)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
@oxide_datetime_formats ||= Hash.new { |formats, encoding| formats[encoding] = {} }
|
|
68
|
+
formatter = @oxide_datetime_formats[cell.format.encoding][cell.format] ||= formatter_datetime(cell)
|
|
69
|
+
formatter ? cell.value.strftime(formatter) : cell.formatted_value
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def formatter_datetime(cell)
|
|
73
|
+
parts = cell.format.downcase.split(" ", 0).map do |part|
|
|
74
|
+
parsed = cell.send(:parse_date_or_time_format, part)
|
|
75
|
+
return nil unless parsed
|
|
76
|
+
|
|
77
|
+
parsed
|
|
78
|
+
end
|
|
79
|
+
parts.join(" ")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def native_formatters?
|
|
83
|
+
Oxide.enabled? && Roo::VERSION == "4.0.0"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def formatter_area(sheet, rectangle)
|
|
87
|
+
target = sheet_for(sheet)
|
|
88
|
+
formatter_rectangle(sheet, rectangle) +
|
|
89
|
+
[target.first_row, target.first_column, target.last_row, target.last_column]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def formatter_rectangle(sheet, rectangle)
|
|
93
|
+
bounds = %i[first_row first_column last_row last_column]
|
|
94
|
+
rectangle.zip(bounds).map { |requested, bound| requested || public_send(bound, sheet) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def formatter_xml_sheet(document, name, target)
|
|
98
|
+
element = document.create_element("sheet", name:)
|
|
99
|
+
cells = first_row ? target.lazy_cells.xml_cells(self, formatter_area(name, [nil] * 4)) : ""
|
|
100
|
+
" #{element.to_xml.delete_suffix("/>")}#{cells.empty? ? "/>\n" : ">\n#{cells} </sheet>\n"}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def formatter_yaml_prefix(prefix)
|
|
104
|
+
text = +""
|
|
105
|
+
prefix.each { |key, value| text << " #{key}: #{value} \n" }
|
|
106
|
+
text
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def formatter_xml_cell(value, row, column, type)
|
|
110
|
+
Nokogiri::XML::Document.new.create_element("cell", value, row:, column:, type:).to_xml
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Roo
|
|
4
|
+
module Oxide
|
|
5
|
+
# Answers Excelx#row for the default sheet from its cells in
|
|
6
|
+
# first_row..last_row and first_column..last_column, laid out in one pass
|
|
7
|
+
# over Sheet#cells: in one flat Array with a slot per column of each row
|
|
8
|
+
# (an Array per row costs a major GC on a large sheet), or, on a sheet
|
|
9
|
+
# with fewer than one cell per MAX_SLOTS_PER_CELL slots, in a Hash of an
|
|
10
|
+
# Array per row of its column offsets and cells. roo answers the first ROO_ROWS
|
|
11
|
+
# requests, so a caller that stops early never pays for that pass, and
|
|
12
|
+
# any other row, a sheet too short to repay the pass (fewer than
|
|
13
|
+
# 3 * ROO_ROWS rows), an empty sheet, and every request once the default
|
|
14
|
+
# sheet changes or Excelx#set writes to the workbook.
|
|
15
|
+
class RowIndex
|
|
16
|
+
ROO_ROWS = 1_000
|
|
17
|
+
MAX_SLOTS_PER_CELL = 4
|
|
18
|
+
|
|
19
|
+
def initialize(workbook)
|
|
20
|
+
@workbook = workbook
|
|
21
|
+
@requests = 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def row(line, set_calls)
|
|
25
|
+
index = index(line) if current?(set_calls)
|
|
26
|
+
return @workbook.row(line) unless index
|
|
27
|
+
|
|
28
|
+
@lines ? sparse_row(@lines[index]) : @cells[index * @width, @width].map! { |cell| cell&.value }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def index(line)
|
|
34
|
+
line - @first_row if line.is_a?(Integer) && line.between?(@first_row, @last_row)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def current?(set_calls)
|
|
38
|
+
return false if (@requests += 1) <= ROO_ROWS
|
|
39
|
+
|
|
40
|
+
sheet = @workbook.sheet_for(nil)
|
|
41
|
+
build(sheet, set_calls) unless @sheet
|
|
42
|
+
(@cells || @lines) && @sheet.equal?(sheet) && @set_calls == set_calls
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def sparse_row(entries)
|
|
46
|
+
values = Array.new(@width)
|
|
47
|
+
return values unless entries
|
|
48
|
+
|
|
49
|
+
i = 0
|
|
50
|
+
while i < entries.size
|
|
51
|
+
values[entries[i]] = entries[i + 1]&.value
|
|
52
|
+
i += 2
|
|
53
|
+
end
|
|
54
|
+
values
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def build(sheet, set_calls)
|
|
58
|
+
@sheet = sheet
|
|
59
|
+
@set_calls = set_calls
|
|
60
|
+
return unless sheet.first_row && sheet.last_row >= 3 * ROO_ROWS
|
|
61
|
+
|
|
62
|
+
@first_row = sheet.first_row
|
|
63
|
+
@last_row = sheet.last_row
|
|
64
|
+
@first = sheet.first_column
|
|
65
|
+
@last = sheet.last_column
|
|
66
|
+
@width = @last - @first + 1
|
|
67
|
+
lay_out(sheet.cells, @last_row - @first_row + 1)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def lay_out(cells, rows)
|
|
71
|
+
if rows * @width > MAX_SLOTS_PER_CELL * cells.size
|
|
72
|
+
@lines = {}
|
|
73
|
+
each_cell(cells) { |index, column, cell| (@lines[index] ||= []).push(column, cell) }
|
|
74
|
+
else
|
|
75
|
+
@cells = Array.new(rows * @width)
|
|
76
|
+
each_cell(cells) { |index, column, cell| @cells[(index * @width) + column] = cell }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def each_cell(cells) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
81
|
+
cells.each_pair do |key, cell|
|
|
82
|
+
next unless key.is_a?(Array) && key.size == 2
|
|
83
|
+
|
|
84
|
+
line, column = key
|
|
85
|
+
next unless line.is_a?(Integer) && column.is_a?(Integer) &&
|
|
86
|
+
line >= @first_row && line <= @last_row && column >= @first && column <= @last
|
|
87
|
+
|
|
88
|
+
yield line - @first_row, column - @first, cell
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Roo
|
|
4
|
+
module Oxide
|
|
5
|
+
# Roo::Base#each reads the current sheet's row after every block call.
|
|
6
|
+
module Excelx
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def native_rows?
|
|
10
|
+
Oxide.enabled? && method(:row).owner.equal?(::Roo::Excelx)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def each_without_options(&)
|
|
14
|
+
sheet = sheet_for(nil)
|
|
15
|
+
lazy = sheet.lazy_cells
|
|
16
|
+
return each_lazy_row(sheet, lazy, &) if lazy && sheet.first_column
|
|
17
|
+
|
|
18
|
+
index = RowIndex.new(self)
|
|
19
|
+
1.upto(last_row) { |line| yield index.row(line, @oxide_set_calls) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def each_lazy_row(sheet, lazy)
|
|
23
|
+
last = last_row
|
|
24
|
+
line = lazy.each_row(last, sheet.first_column, sheet.last_column) do |values|
|
|
25
|
+
yield values
|
|
26
|
+
sheet_for(nil).equal?(sheet) && sheet.lazy_cells.equal?(lazy)
|
|
27
|
+
end
|
|
28
|
+
line.upto(last) { |number| yield row(number) }
|
|
29
|
+
1
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Roo
|
|
4
|
+
module Oxide
|
|
5
|
+
# Prepended to Roo::Excelx::SharedStrings: the native extension builds the
|
|
6
|
+
# arrays #to_a and #to_html memoize, deferring HTML objects until used.
|
|
7
|
+
# Roo's own code and the native reader share them. A file the extension
|
|
8
|
+
# does not read exactly the way roo does is left to roo.
|
|
9
|
+
module SharedStrings
|
|
10
|
+
# roo versions whose shared-string extraction the extension reproduces.
|
|
11
|
+
ROO_VERSIONS = %w[4.0.0].freeze
|
|
12
|
+
|
|
13
|
+
def to_a
|
|
14
|
+
oxide_read(html: !@options[:disable_html_wrapper] && !(@html || @oxide_html), plain: true) unless @array
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_html
|
|
19
|
+
unless @html
|
|
20
|
+
oxide_read(html: true, plain: false) unless @oxide_html
|
|
21
|
+
@html = @oxide_html.to_a if @oxide_html && (@oxide_html_used || doc_exists?)
|
|
22
|
+
end
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def oxide_html
|
|
27
|
+
return to_html unless @oxide_html && (@oxide_html_used || doc_exists?)
|
|
28
|
+
|
|
29
|
+
@oxide_html_used = true
|
|
30
|
+
@html || @oxide_html
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def oxide_read(html:, plain:)
|
|
36
|
+
return unless Oxide.enabled? && ROO_VERSIONS.include?(Roo::VERSION) && doc_exists?
|
|
37
|
+
|
|
38
|
+
plain, html = Native.shared_strings(@path, html, plain)
|
|
39
|
+
@array ||= plain
|
|
40
|
+
@oxide_html ||= html
|
|
41
|
+
rescue UnsupportedError
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Roo
|
|
4
|
+
module Oxide
|
|
5
|
+
# Prepended to Roo::Excelx::Sheet: single-cell lookups, rows, columns, and
|
|
6
|
+
# the sheet's bounds read the lazy cells of SheetDoc#lazy_cells while
|
|
7
|
+
# #cells has not been built, so they build only the cells they read. Rows
|
|
8
|
+
# and columns build only the cells whose value needs one.
|
|
9
|
+
module Sheet
|
|
10
|
+
NO_CELLS = [].freeze
|
|
11
|
+
private_constant :NO_CELLS
|
|
12
|
+
|
|
13
|
+
# cells[[row, column]]
|
|
14
|
+
def cell_at(row, column)
|
|
15
|
+
(lazy = lazy_cells) ? lazy.cell_at(row, column) : cells[[row, column]]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def row(row_number)
|
|
19
|
+
lazy = lazy_cells
|
|
20
|
+
return lazy.row(row_number, first_column, last_column) if lazy && first_column
|
|
21
|
+
|
|
22
|
+
first_column.upto(last_column).map do |col|
|
|
23
|
+
cell_at(row_number, col)&.value
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def column(col_number)
|
|
28
|
+
lazy = lazy_cells
|
|
29
|
+
return lazy.column(col_number, first_row, last_row) if lazy && first_row
|
|
30
|
+
|
|
31
|
+
first_row.upto(last_row).map do |row|
|
|
32
|
+
cell_at(row, col_number)&.value
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def excelx_format(key)
|
|
37
|
+
cell = (lazy = lazy_cells) ? lazy[key] : cells[key]
|
|
38
|
+
styles.style_format(cell.style).to_s if cell
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# For Excelx#parse: a Hash per line of `lines`, holding the value of the
|
|
42
|
+
# cell at [line, column] under `key` for each [key, column] of `fields`.
|
|
43
|
+
def parse_rows(lines, fields)
|
|
44
|
+
lazy = lazy_cells
|
|
45
|
+
if lazy && fields.all? { |_, column| column.is_a?(Integer) }
|
|
46
|
+
lazy_parse_rows(lazy, lines, fields)
|
|
47
|
+
else
|
|
48
|
+
cells_parse_rows(lines, fields)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The Native::Store behind the lazy cells while #cells is not built.
|
|
53
|
+
def lazy_cells
|
|
54
|
+
@sheet.lazy_cells(@rels) unless @cells
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def lazy_parse_rows(lazy, lines, fields)
|
|
60
|
+
first, last = fields.map(&:last).minmax
|
|
61
|
+
slots = fields.map { |key, column| [key, column - first] }.freeze
|
|
62
|
+
lines.map { |line| lazy.hash_row(line, first || 1, last || 0, slots) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def cells_parse_rows(lines, fields)
|
|
66
|
+
slots = fields.map(&:last).uniq.each_with_index.to_h
|
|
67
|
+
by_line = cells_by_line(slots)
|
|
68
|
+
fields = fields.map { |key, column| [key, slots.fetch(column)] }
|
|
69
|
+
lines.map do |line|
|
|
70
|
+
line_cells = by_line.fetch(line, NO_CELLS)
|
|
71
|
+
fields.each_with_object({}) do |(key, slot), row|
|
|
72
|
+
cell = line_cells[slot]
|
|
73
|
+
row[key] = (cell.value if cell.respond_to?(:value)) # Roo::Excelx#safe_send, inline
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def cells_by_line(slots)
|
|
79
|
+
by_line = {}
|
|
80
|
+
cells.each do |(line, column), cell|
|
|
81
|
+
slot = slots[column] or next
|
|
82
|
+
(by_line[line] ||= Array.new(slots.size))[slot] = cell
|
|
83
|
+
end
|
|
84
|
+
by_line
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Custom padding stays in roo's loop over #each_cell.
|
|
88
|
+
def cells_for_row_element(row_element, options = {})
|
|
89
|
+
return super if options[:pad_cells] && !method(:pad_cells).owner.equal?(::Roo::Excelx::Sheet)
|
|
90
|
+
|
|
91
|
+
@sheet.native_row_cells(row_element, pad_cells: options[:pad_cells]) || super
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def first_last_row_col
|
|
95
|
+
@first_last_row_col ||= lazy_cells&.bounds || super
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Roo
|
|
4
|
+
module Oxide
|
|
5
|
+
# Prepended to Roo::Excelx::SheetDoc, where every worksheet read in roo
|
|
6
|
+
# starts: Sheet#cells (and with it #row, #each, #parse), the lazy cells
|
|
7
|
+
# behind Excelx#cell and the other single-cell lookups, and Sheet#each_row
|
|
8
|
+
# (#each_row_streaming). Offsets and row limits stay roo's code.
|
|
9
|
+
module SheetDoc
|
|
10
|
+
# Cells the native reader parses ahead of Ruby, per batch.
|
|
11
|
+
BATCH_CELLS = 16_384
|
|
12
|
+
|
|
13
|
+
# A sheet already read into a Native::Store is built from it even when
|
|
14
|
+
# the native reader has been turned off since, so the cells handed out
|
|
15
|
+
# stay the ones #cells holds.
|
|
16
|
+
def cells(relationships)
|
|
17
|
+
return super if @cells || !(native? || @oxide_store)
|
|
18
|
+
|
|
19
|
+
store = @oxide_store || nil
|
|
20
|
+
@oxide_store = nil
|
|
21
|
+
@cells = store ? store.cells : read_cells(relationships)
|
|
22
|
+
rescue UnsupportedError
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The cells of #cells, each built when it is first read. nil when they
|
|
27
|
+
# have to be built all at once: when #cells already exists, when merged
|
|
28
|
+
# ranges are expanded, and when building some cell would raise, because
|
|
29
|
+
# roo raises for the whole sheet then.
|
|
30
|
+
def lazy_cells(relationships)
|
|
31
|
+
if @oxide_store.nil? && !@cells && !@options[:expand_merged_ranges] && native?
|
|
32
|
+
@oxide_store = read_store(relationships)
|
|
33
|
+
end
|
|
34
|
+
@oxide_store || nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Yields Native::Row objects; #each_cell turns one into cells when roo
|
|
38
|
+
# asks for it, so rows roo skips are never converted, as in roo.
|
|
39
|
+
def each_row_streaming(&)
|
|
40
|
+
return super unless native?
|
|
41
|
+
|
|
42
|
+
yielded = 0
|
|
43
|
+
reader = Native::Reader.new(sheet_source, true, BATCH_CELLS)
|
|
44
|
+
while (row = reader.next_row)
|
|
45
|
+
yield row
|
|
46
|
+
yielded += 1
|
|
47
|
+
end
|
|
48
|
+
nil
|
|
49
|
+
rescue UnsupportedError
|
|
50
|
+
# roo reads the sheet again; skip the rows already yielded.
|
|
51
|
+
skip = yielded
|
|
52
|
+
super { |row| skip.zero? ? yield(row) : skip -= 1 }
|
|
53
|
+
ensure
|
|
54
|
+
# Also when the caller stops early, as roo does for max_rows.
|
|
55
|
+
reader&.close
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def each_cell(row_xml, &)
|
|
59
|
+
return super unless row_xml.is_a?(Native::Row)
|
|
60
|
+
|
|
61
|
+
oxide_stream_converter.row_cells(row_xml, false).each(&)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The cells #each_cell yields for a Native::Row, as a new Array; nil for
|
|
65
|
+
# rows roo reads itself, custom #each_cell, or roo-only padding.
|
|
66
|
+
def native_row_cells(row, pad_cells: false)
|
|
67
|
+
return unless row.is_a?(Native::Row) && method(:each_cell).owner.equal?(SheetDoc)
|
|
68
|
+
|
|
69
|
+
oxide_stream_converter.row_cells(row, pad_cells)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Roo::Oxide.each_row_values. `sheet` is the Roo::Excelx::Sheet that
|
|
73
|
+
# owns this document; it turns rows that roo reads itself into cells.
|
|
74
|
+
def each_row_values(sheet, &)
|
|
75
|
+
return each_roo_row_values(sheet, 0, &) unless native?
|
|
76
|
+
|
|
77
|
+
yielded = 0
|
|
78
|
+
reader = Native::Reader.new(sheet_source, true, BATCH_CELLS)
|
|
79
|
+
while (values = reader.next_values(oxide_stream_converter))
|
|
80
|
+
yield values
|
|
81
|
+
yielded += 1
|
|
82
|
+
end
|
|
83
|
+
nil
|
|
84
|
+
rescue UnsupportedError
|
|
85
|
+
each_roo_row_values(sheet, yielded, &)
|
|
86
|
+
ensure
|
|
87
|
+
reader&.close
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# The values of the rows roo's own #each_row_streaming yields, after the
|
|
93
|
+
# first `skip`.
|
|
94
|
+
def each_roo_row_values(sheet, skip)
|
|
95
|
+
method(:each_row_streaming).super_method.call do |row|
|
|
96
|
+
next skip -= 1 unless skip.zero?
|
|
97
|
+
|
|
98
|
+
yield sheet.send(:cells_for_row_element, row, pad_cells: true).map { |cell| cell&.value }
|
|
99
|
+
end
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def oxide_stream_converter
|
|
104
|
+
@oxide_stream_converter ||= Native::Converter.new(Context.new(@shared, hyperlinks(@relationships), true),
|
|
105
|
+
true)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def native?
|
|
109
|
+
@path && Oxide.enabled?
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Every cell, in roo's order.
|
|
113
|
+
def read_cells(relationships)
|
|
114
|
+
cells = {}
|
|
115
|
+
reader = Native::Reader.new(sheet_source, false, BATCH_CELLS)
|
|
116
|
+
converter = Native::Converter.new(context(relationships), false)
|
|
117
|
+
nil while reader.fill_cells(cells, converter)
|
|
118
|
+
expand_merged_ranges(cells) if @options[:expand_merged_ranges]
|
|
119
|
+
cells
|
|
120
|
+
ensure
|
|
121
|
+
reader&.close
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# A Native::Store, or false when the cells have to be built all at once.
|
|
125
|
+
# roo applies the hyperlinks to every cell on the first read, so later
|
|
126
|
+
# changes to the hash it hands out do not reach the cells.
|
|
127
|
+
def read_store(relationships)
|
|
128
|
+
Native::Store.read(sheet_source, context(relationships, hyperlinks(relationships).dup)) || false
|
|
129
|
+
rescue UnsupportedError
|
|
130
|
+
false
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def context(relationships, links = hyperlinks(relationships))
|
|
134
|
+
Context.new(@shared, links, @options[:empty_cell])
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Roo
|
|
4
|
+
module Oxide
|
|
5
|
+
# Prepended to Roo::Excelx::SheetDoc. roo finds a sheet's hyperlinks
|
|
6
|
+
# (#hyperlinks, behind every read of a sheet with a hyperlink
|
|
7
|
+
# relationship) and its merged ranges with XPath on the whole sheet.
|
|
8
|
+
# Native.sheet_parts resolves links without a Nokogiri document.
|
|
9
|
+
module SheetParts
|
|
10
|
+
# roo versions whose hyperlink and merge rules these methods reproduce.
|
|
11
|
+
ROO_VERSIONS = %w[4.0.0].freeze
|
|
12
|
+
|
|
13
|
+
def hyperlinks(relationships)
|
|
14
|
+
return super unless native? && ROO_VERSIONS.include?(Roo::VERSION)
|
|
15
|
+
return @hyperlinks if @hyperlinks
|
|
16
|
+
return @hyperlinks = {} if @options[:no_hyperlinks]
|
|
17
|
+
|
|
18
|
+
targets = relationships.oxide_hyperlink_targets
|
|
19
|
+
return @hyperlinks = {} unless targets
|
|
20
|
+
return super unless (parts = oxide_parts(targets))
|
|
21
|
+
|
|
22
|
+
@hyperlinks = parts.first
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def expand_merged_ranges(cells)
|
|
28
|
+
return super unless (parts = oxide_parts)
|
|
29
|
+
|
|
30
|
+
merges = {}
|
|
31
|
+
parts.last.each { |ref| merge_range(merges, ref, cells) }
|
|
32
|
+
merges.each { |dst, src| cells[dst] = cells[src] }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def merge_range(merges, ref, cells)
|
|
36
|
+
src, dst = ref.split(":").map { |key| ::Roo::Utils.ref_to_key(key) }
|
|
37
|
+
return unless cells[src]
|
|
38
|
+
|
|
39
|
+
(src[0]..dst[0]).each do |row|
|
|
40
|
+
(src[1]..dst[1]).each do |col|
|
|
41
|
+
merges[[row, col]] = src unless row == src[0] && col == src[1]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# [hyperlinks, merge_cells] from Native.sheet_parts, or nil when roo
|
|
47
|
+
# reads them from the whole document.
|
|
48
|
+
def oxide_parts(targets = {})
|
|
49
|
+
return unless native? && ROO_VERSIONS.include?(Roo::VERSION)
|
|
50
|
+
|
|
51
|
+
@oxide_parts = Native.sheet_parts(sheet_source, targets) if @oxide_parts.nil?
|
|
52
|
+
@oxide_parts || nil
|
|
53
|
+
rescue UnsupportedError
|
|
54
|
+
@oxide_parts = false
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# roo's relationship nodes stay lazy; hyperlink reads need only targets.
|
|
60
|
+
module HyperlinkRelationships
|
|
61
|
+
def oxide_hyperlink_targets
|
|
62
|
+
return targets_from_roo if @relationships || !doc_exists?
|
|
63
|
+
|
|
64
|
+
Native.hyperlink_targets(@path)
|
|
65
|
+
rescue UnsupportedError
|
|
66
|
+
targets_from_roo
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def targets_from_roo
|
|
72
|
+
return unless include_type?("hyperlink")
|
|
73
|
+
|
|
74
|
+
to_a.transform_values { |relationship| relationship["Target"] }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|