roo-oxide 0.0.2-aarch64-linux-musl

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.
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roo
4
+ module Oxide
5
+ VERSION = "0.0.2"
6
+ end
7
+ end
data/lib/roo/oxide.rb ADDED
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roo"
4
+ require "roo/excelx"
5
+ require_relative "oxide/version"
6
+
7
+ if Roo.const_defined?(:Calamine, false) && !Roo::Calamine.equal?(Roo::Oxide)
8
+ raise LoadError, "Replace roo-calamine with roo-oxide in your Gemfile before loading roo/oxide"
9
+ end
10
+
11
+ Roo::Calamine = Roo::Oxide unless Roo.const_defined?(:Calamine, false)
12
+
13
+ module Roo
14
+ # Reads `.xlsx` and `.xlsm` worksheets for Roo::Excelx with a native
15
+ # extension. Loading this file is all it takes; `Roo::Oxide.enabled =
16
+ # false` or `ROO_OXIDE=0` switches back to roo's own reader at runtime.
17
+ module Oxide
18
+ # Raised by the native reader for XML it does not reproduce exactly the
19
+ # way roo reads it. Roo::Oxide rescues it and lets roo read the sheet.
20
+ class UnsupportedError < StandardError; end
21
+
22
+ DISABLING_ENV_VALUES = %w[0 false off].freeze
23
+ private_constant :DISABLING_ENV_VALUES
24
+
25
+ @enabled = true
26
+
27
+ class << self
28
+ attr_writer :enabled
29
+
30
+ # Whether the native extension is loaded. It is `false` on Ruby
31
+ # implementations other than CRuby, such as JRuby.
32
+ def available?
33
+ defined?(Native::Reader) ? true : false
34
+ end
35
+
36
+ # Whether the next worksheet read goes through the native reader.
37
+ def enabled?
38
+ environment = ENV.fetch("ROO_OXIDE") { ENV.fetch("ROO_CALAMINE", "") }
39
+ @enabled && available? && !DISABLING_ENV_VALUES.include?(environment.downcase)
40
+ end
41
+
42
+ # roo-oxide's own API, not roo's: yields each row of a worksheet as an
43
+ # Array of the values roo's cells return from `#value`, without building
44
+ # the cells. A row is what
45
+ # `Roo::Excelx#each_row_streaming(sheet:, pad_cells: true)` yields, with
46
+ # `cell&.value` in place of each cell: one entry per column from A up to
47
+ # the row's last cell, `nil` where the worksheet has no cell.
48
+ #
49
+ # `source` and `options` are what `Roo::Excelx.new` takes. The workbook
50
+ # is closed when the block returns. Without a block, returns an
51
+ # Enumerator.
52
+ def each_row_values(source, sheet: nil, **options, &block)
53
+ return enum_for(__method__, source, sheet:, **options) unless block
54
+
55
+ workbook = Roo::Excelx.new(source, options)
56
+ roo_sheet = workbook.sheet_for(sheet)
57
+ if available?
58
+ roo_sheet.instance_variable_get(:@sheet).each_row_values(roo_sheet, &block)
59
+ else
60
+ roo_sheet.each_row(pad_cells: true) { |cells| yield cells.map { |cell| cell&.value } }
61
+ end
62
+ nil
63
+ ensure
64
+ workbook&.close
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ if RUBY_ENGINE == "ruby"
71
+ begin
72
+ require "roo/oxide/#{RUBY_VERSION[/\d+\.\d+/]}/roo_oxide"
73
+ rescue LoadError
74
+ require "roo/oxide/roo_oxide"
75
+ end
76
+ require_relative "oxide/context"
77
+ require_relative "oxide/deferred_sheets"
78
+ require_relative "oxide/sheet_doc"
79
+ require_relative "oxide/sheet_parts"
80
+ require_relative "oxide/excelx"
81
+ require_relative "oxide/csv"
82
+ require_relative "oxide/formatters"
83
+ require_relative "oxide/row_index"
84
+ require_relative "oxide/row_iteration"
85
+ require_relative "oxide/sheet"
86
+ require_relative "oxide/shared_strings"
87
+ Roo::Excelx::SheetDoc.prepend(Roo::Oxide::DeferredSheetDoc)
88
+ Roo::Excelx::SheetDoc.prepend(Roo::Oxide::SheetDoc)
89
+ Roo::Excelx::SheetDoc.prepend(Roo::Oxide::SheetParts)
90
+ Roo::Excelx::Relationships.prepend(Roo::Oxide::HyperlinkRelationships)
91
+ Roo::Excelx::Sheet.prepend(Roo::Oxide::Sheet)
92
+ Roo::Excelx.prepend(Roo::Oxide::DeferredSheets)
93
+ Roo::Excelx.prepend(Roo::Oxide::Excelx)
94
+ Roo::Excelx.prepend(Roo::Oxide::SheetSelection)
95
+ Roo::Excelx::SharedStrings.prepend(Roo::Oxide::SharedStrings)
96
+ Roo::Excelx::Workbook.prepend(Roo::Oxide::MetadataDocument)
97
+ Roo::Excelx::Styles.prepend(Roo::Oxide::MetadataDocument)
98
+ Roo::Excelx.prepend(Roo::Oxide::CSV)
99
+ Roo::Excelx.prepend(Roo::Oxide::Formatters)
100
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: roo-oxide
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: aarch64-linux-musl
6
+ authors:
7
+ - Sebastian Sala
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-11-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: roo
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ description: An unofficial acceleration layer built on top of Roo. Add roo-oxide to
28
+ the Gemfile; Roo is installed as a dependency and its spreadsheet API stays the
29
+ same. A Rust extension speeds up XLSX and XLSM reads while preserving Roo's cell
30
+ and value rules. Unsupported input uses Roo's own reader. Not affiliated with roo-rb.
31
+ email:
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - CHANGELOG.md
37
+ - LICENSE
38
+ - README.md
39
+ - lib/roo/calamine.rb
40
+ - lib/roo/calamine/version.rb
41
+ - lib/roo/oxide.rb
42
+ - lib/roo/oxide/3.3/roo_oxide.so
43
+ - lib/roo/oxide/3.4/roo_oxide.so
44
+ - lib/roo/oxide/4.0/roo_oxide.so
45
+ - lib/roo/oxide/context.rb
46
+ - lib/roo/oxide/csv.rb
47
+ - lib/roo/oxide/deferred_sheets.rb
48
+ - lib/roo/oxide/excelx.rb
49
+ - lib/roo/oxide/formatters.rb
50
+ - lib/roo/oxide/row_index.rb
51
+ - lib/roo/oxide/row_iteration.rb
52
+ - lib/roo/oxide/shared_strings.rb
53
+ - lib/roo/oxide/sheet.rb
54
+ - lib/roo/oxide/sheet_doc.rb
55
+ - lib/roo/oxide/sheet_parts.rb
56
+ - lib/roo/oxide/version.rb
57
+ homepage: https://github.com/salasebas/roo-oxide
58
+ licenses:
59
+ - MIT
60
+ metadata:
61
+ homepage_uri: https://github.com/salasebas/roo-oxide
62
+ source_code_uri: https://github.com/salasebas/roo-oxide/tree/v0.0.2
63
+ changelog_uri: https://github.com/salasebas/roo-oxide/blob/v0.0.2/CHANGELOG.md
64
+ bug_tracker_uri: https://github.com/salasebas/roo-oxide/issues
65
+ rubygems_mfa_required: 'true'
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '3.3'
75
+ - - "<"
76
+ - !ruby/object:Gem::Version
77
+ version: 4.1.dev
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 3.3.11
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: 3.3.22
86
+ requirements: []
87
+ rubygems_version: 3.5.23
88
+ signing_key:
89
+ specification_version: 4
90
+ summary: Rust-powered native acceleration for Roo's XLSX and XLSM readers.
91
+ test_files: []