xlsxrb 0.1.7 → 0.1.9

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -2
  3. data/README.md +125 -36
  4. data/Rakefile +36 -7
  5. data/benchmark.rb +32 -4
  6. data/docs/ARCHITECTURE.md +32 -30
  7. data/docs/DEVELOPMENT.md +11 -3
  8. data/docs/QUALITY_ASSURANCE.md +3 -4
  9. data/docs/SPEC_SOURCES.md +7 -1
  10. data/docs/visual/VisualGallery.md +157 -157
  11. data/docs/wasm/ruby.wasm +0 -0
  12. data/lib/ruby_lsp/xlsxrb/addon.rb +1 -1
  13. data/lib/ruby_lsp/xlsxrb/completion_listener.rb +11 -13
  14. data/lib/xlsxrb/elements/coordinate_access.rb +101 -0
  15. data/lib/xlsxrb/elements/row.rb +2 -2
  16. data/lib/xlsxrb/elements/workbook.rb +20 -2
  17. data/lib/xlsxrb/elements/worksheet.rb +94 -124
  18. data/lib/xlsxrb/elements.rb +1 -0
  19. data/lib/xlsxrb/ooxml/cfb.rb +490 -0
  20. data/lib/xlsxrb/ooxml/crypto/agile.rb +285 -0
  21. data/lib/xlsxrb/ooxml/crypto/standard.rb +152 -0
  22. data/lib/xlsxrb/ooxml/crypto.rb +60 -0
  23. data/lib/xlsxrb/ooxml/worksheet_parser.rb +21 -15
  24. data/lib/xlsxrb/stream_row.rb +181 -0
  25. data/lib/xlsxrb/version.rb +1 -1
  26. data/lib/xlsxrb.rb +271 -122
  27. data/sig/generated/xlsxrb/elements/coordinate_access.rbs +70 -0
  28. data/sig/generated/xlsxrb/elements/worksheet.rbs +119 -14
  29. data/sig/generated/xlsxrb/ooxml/cfb.rbs +121 -0
  30. data/sig/generated/xlsxrb/ooxml/crypto/agile.rbs +46 -0
  31. data/sig/generated/xlsxrb/ooxml/crypto/standard.rbs +28 -0
  32. data/sig/generated/xlsxrb/ooxml/crypto.rbs +17 -0
  33. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +2 -0
  34. data/sig/generated/xlsxrb/stream_row.rbs +127 -0
  35. data/sig/generated/xlsxrb.rbs +135 -71
  36. data/vendor/sdk_runner/Program.cs +3 -1
  37. metadata +13 -1
data/docs/wasm/ruby.wasm CHANGED
Binary file
@@ -12,7 +12,7 @@ module RubyLsp
12
12
  # This add-on serves as a bridge/polyfill for current Ruby LSP environments.
13
13
  # While xlsxrb ships with complete RBS signatures (`sig/generated/`), Ruby LSP's
14
14
  # type inferrer does not yet perform automatic static type inference from method
15
- # block signatures to block parameters (e.g., `Xlsxrb.generate do |wb|`).
15
+ # block signatures to block parameters (e.g., `Xlsxrb.write do |wb|`).
16
16
  #
17
17
  # This add-on enables immediate out-of-the-box autocompletion and rich Markdown
18
18
  # documentation across all public block arguments.
@@ -693,9 +693,9 @@ module RubyLsp
693
693
 
694
694
  if caller_receiver_is_xlsxrb?(caller_receiver)
695
695
  case method_name
696
- when :generate then return :stream_writer
696
+ when :write then return :stream_writer
697
+ when :read then return :stream_sheet
697
698
  when :build then return :workbook_builder
698
- when :foreach then return :stream_sheet
699
699
  when :modify then return :workbook
700
700
  end
701
701
  end
@@ -718,25 +718,23 @@ module RubyLsp
718
718
  def infer_each_block_target(caller_receiver)
719
719
  if receiver_matches?(caller_receiver, %i[workbook wb])
720
720
  :worksheet
721
- elsif receiver_matches?(caller_receiver, %i[row r])
721
+ elsif receiver_matches?(caller_receiver, %i[row r stream_row])
722
722
  :cell
723
723
  else
724
724
  :row
725
725
  end
726
726
  end
727
727
 
728
- def infer_from_variable_name(receiver_name)
729
- case receiver_name
730
- when :wb, :stream_writer, :w
731
- :stream_writer
732
- when :workbook
733
- :workbook
734
- when :s, :ws, :sheet
728
+ def infer_from_variable_name(name)
729
+ case name
730
+ when :wb, :workbook
731
+ :workbook_builder
732
+ when :s, :sheet, :ws, :worksheet
735
733
  :worksheet_proxy
736
- when :worksheet
737
- :worksheet
738
- when :stream_sheet
734
+ when :stream_sheet, :ss
739
735
  :stream_sheet
736
+ when :stream_writer, :sw
737
+ :stream_writer
740
738
  when :r, :row
741
739
  :row
742
740
  when :c, :cell
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module Xlsxrb
6
+ module Elements
7
+ # Mixin providing coordinate-based and random-access cell/row lookups
8
+ # for in-memory worksheet structures.
9
+ #
10
+ # Expects the including class to provide a `#rows` method returning an `Array<Elements::Row>`.
11
+ #
12
+ # @api public
13
+ module CoordinateAccess
14
+ # Returns a Hash mapping Excel cell references (e.g. "A1") to Cell objects.
15
+ #
16
+ # @return [Hash<String, Elements::Cell>]
17
+ #: () -> Hash[String, Elements::Cell]
18
+ def cells_hash
19
+ h = {}
20
+ rows.each do |r|
21
+ r.cells.each do |c|
22
+ ref = "#{Cell.column_letter(c.column_index)}#{c.row_index + 1}"
23
+ h[ref] = c
24
+ end
25
+ end
26
+ h
27
+ end
28
+
29
+ # Returns all cells ordered by row and column index.
30
+ #
31
+ # @return [Array<Elements::Cell>]
32
+ # @api public
33
+ #: () -> Array[Elements::Cell]
34
+ def cells
35
+ cells_hash.values.sort_by { |c| [c.row_index, c.column_index] }
36
+ end
37
+
38
+ # Access a cell by its Excel-style reference (e.g. "A1").
39
+ #
40
+ # @example
41
+ # sheet["A1"] #=> #<Elements::Cell value="Hello">
42
+ #
43
+ # @param ref [String, Symbol] Cell reference (e.g. "A1" or :A1).
44
+ # @return [Elements::Cell, nil]
45
+ # @api public
46
+ #: (String | Symbol ref) -> Elements::Cell?
47
+ def [](ref)
48
+ cells_hash[ref.to_s.upcase]
49
+ end
50
+
51
+ # Returns the row at the given 0-based index, or nil.
52
+ #
53
+ # @param index [Integer] 0-based row index.
54
+ # @return [Elements::Row, nil]
55
+ # @api public
56
+ #: (Integer index) -> Elements::Row?
57
+ def row_at(index)
58
+ rows.find { |r| r.index == index }
59
+ end
60
+
61
+ # Returns the first row in the sheet, or nil.
62
+ #
63
+ # @return [Elements::Row, nil]
64
+ # @api public
65
+ #: () -> Elements::Row?
66
+ def first_row
67
+ rows.min_by(&:index)
68
+ end
69
+
70
+ # Returns the last row in the sheet, or nil.
71
+ #
72
+ # @return [Elements::Row, nil]
73
+ # @api public
74
+ #: () -> Elements::Row?
75
+ def last_row
76
+ rows.max_by(&:index)
77
+ end
78
+
79
+ # Returns the raw cell value at the given Excel-style reference (e.g. "A1").
80
+ #
81
+ # @example
82
+ # sheet.cell_value("A1") #=> "Sales Report"
83
+ #
84
+ # @param ref [String] Cell reference (e.g. "A1").
85
+ # @return [Object, nil]
86
+ # @api public
87
+ #: (String ref) -> untyped
88
+ def cell_value(ref)
89
+ parsed = Cell.parse_ref(ref)
90
+ return nil unless parsed
91
+
92
+ row_idx, col_idx = parsed
93
+ row = row_at(row_idx)
94
+ return nil unless row
95
+
96
+ cell = row.cell_at(col_idx)
97
+ cell&.value
98
+ end
99
+ end
100
+ end
101
+ end
@@ -75,7 +75,7 @@ module Xlsxrb
75
75
  # @return [Enumerator, void]
76
76
  # @api public
77
77
  #: () { (Elements::Cell) -> void } -> void
78
- #: | () -> Enumerator[Elements::Cell, void]
78
+ #: () -> Enumerator[Elements::Cell, void]
79
79
  def each(&)
80
80
  return to_enum(:each) unless block_given?
81
81
 
@@ -94,7 +94,7 @@ module Xlsxrb
94
94
  # @return [Enumerator, void]
95
95
  # @api public
96
96
  #: () { (Elements::Cell) -> void } -> void
97
- #: | () -> Enumerator[Elements::Cell, void]
97
+ #: () -> Enumerator[Elements::Cell, void]
98
98
  def each_cell(&)
99
99
  return to_enum(:each_cell) unless block_given?
100
100
 
@@ -38,10 +38,11 @@ module Xlsxrb
38
38
  # @yieldparam sheet [Elements::Worksheet]
39
39
  # @return [Enumerator, void]
40
40
  #: () { (Elements::Worksheet) -> void } -> void
41
- #: | () -> Enumerator[Elements::Worksheet, void]
41
+ #: () -> Enumerator[Elements::Worksheet, void]
42
42
  def each(&)
43
43
  sheets.each(&)
44
44
  end
45
+ alias_method :each_sheet, :each
45
46
 
46
47
  # Returns whether the workbook is valid according to ECMA-376 rules.
47
48
  #
@@ -71,6 +72,22 @@ module Xlsxrb
71
72
  end
72
73
  alias_method :[], :sheet
73
74
 
75
+ # Loads all sheets into memory, returning an Elements::Workbook where every
76
+ # worksheet is a fully-parsed Elements::Worksheet supporting coordinate random access.
77
+ #
78
+ # @example
79
+ # wb = Xlsxrb.read("file.xlsx").load
80
+ # puts wb["Sheet1"]["A1"].value
81
+ #
82
+ # @return [Elements::Workbook]
83
+ # @api public
84
+ #: () -> Elements::Workbook
85
+ def load
86
+ loaded_sheets = sheets.map { |s| s.respond_to?(:load) ? s.load : s }
87
+ with(sheets: loaded_sheets)
88
+ end
89
+ alias_method :to_workbook, :load
90
+
74
91
  # Returns a new Workbook with the specified sheet updated.
75
92
  # Yields the matched worksheet to the block, which must return a new Worksheet.
76
93
  #
@@ -92,10 +109,11 @@ module Xlsxrb
92
109
  sheet_to_update = sheet(identifier)
93
110
  raise ArgumentError, "sheet not found: #{identifier}" unless sheet_to_update
94
111
 
112
+ sheet_to_update = sheet_to_update.load if sheet_to_update.respond_to?(:load)
95
113
  new_sheet = yield sheet_to_update
96
114
  raise TypeError, "block must return a Worksheet" unless new_sheet.is_a?(Worksheet)
97
115
 
98
- new_sheets = sheets.map { |s| s == sheet_to_update ? new_sheet : s }
116
+ new_sheets = sheets.map { |s| s.name == sheet_to_update.name ? new_sheet : s }
99
117
  with(sheets: new_sheets)
100
118
  end
101
119
 
@@ -4,16 +4,20 @@
4
4
 
5
5
  module Xlsxrb
6
6
  module Elements
7
- # Represents a single worksheet in a workbook.
7
+ # Represents a single fully parsed, in-memory worksheet in a workbook.
8
+ # Provides coordinate random access (sheet["A1"]), row lookups (row_at),
9
+ # and immutable cell updates (update_cell).
8
10
  #
9
11
  # @example Access cells and rows
10
- # sheet = workbook.sheet(0)
12
+ # sheet = workbook.sheet(0).load
11
13
  # cell = sheet["A1"]
12
- # sheet.each_row { |row| puts row.to_a.inspect }
14
+ # row = sheet.row_at(0)
13
15
  #
14
16
  # @api public
15
- Worksheet = Data.define(:name, :rows, :columns, :charts, :unmapped_data, :errors) do
16
- include Enumerable
17
+ class Worksheet
18
+ [Enumerable, CoordinateAccess].each { |m| include m }
19
+
20
+ attr_reader :name, :rows, :columns, :charts, :unmapped_data, :errors
17
21
 
18
22
  # @param name [String] The worksheet name (max 31 characters).
19
23
  # @param rows [Array<Elements::Row>] Rows in the sheet.
@@ -21,87 +25,34 @@ module Xlsxrb
21
25
  # @param charts [Array<Hash>] Charts in the sheet.
22
26
  # @param unmapped_data [Hash] Additional metadata for round-tripping.
23
27
  # @param errors [Array<String>, nil] Validation errors.
24
- #: (name: String, ?rows: Array[Elements::Row], ?columns: Array[Elements::Column], ?charts: Array[Hash[Symbol, untyped]], ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?) -> void
28
+ #: (name: String?, ?rows: Array[Elements::Row], ?columns: Array[Elements::Column], ?charts: Array[Hash[Symbol, untyped]], ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?) -> void
25
29
  def initialize(name:, rows: [], columns: [], charts: [], unmapped_data: {}, errors: nil)
26
- computed_errors = errors || self.class.validate(name, rows)
27
- super(name: name, rows: rows.freeze, columns: columns.freeze, charts: charts.freeze,
28
- unmapped_data: unmapped_data, errors: computed_errors.freeze)
29
- end
30
-
31
- # Returns a Hash mapping Excel cell references (e.g. "A1") to Cell objects.
32
- #
33
- # @return [Hash<String, Elements::Cell>]
34
- #: () -> Hash[String, Elements::Cell]
35
- def cells_hash
36
- h = {}
37
- rows.each do |r|
38
- r.cells.each do |c|
39
- ref = "#{Cell.column_letter(c.column_index)}#{c.row_index + 1}"
40
- h[ref] = c
41
- end
42
- end
43
- h
30
+ @name = name
31
+ @rows = (rows || []).freeze
32
+ @columns = (columns || []).freeze
33
+ @charts = (charts || []).freeze
34
+ @unmapped_data = (unmapped_data || {}).freeze
35
+ computed_errors = errors || self.class.validate(@name, @rows)
36
+ @errors = computed_errors.freeze
44
37
  end
45
38
 
46
- # Returns all cells ordered by row and column index.
47
- #
48
- # @return [Array<Elements::Cell>]
49
- # @api public
50
- #: () -> Array[Elements::Cell]
51
- def cells
52
- # Ensure ordered traversal
53
- cells_hash.values.sort_by { |c| [c.row_index, c.column_index] }
54
- end
55
-
56
- # Access a cell by its Excel-style reference (e.g. "A1").
57
- #
58
- # @example
59
- # sheet["A1"] #=> #<Elements::Cell value="Hello">
60
- #
61
- # @param ref [String, Symbol] Cell reference (e.g. "A1" or :A1).
62
- # @return [Elements::Cell, nil]
63
- # @api public
64
- #: (String | Symbol ref) -> Elements::Cell?
65
- def [](ref)
66
- cells_hash[ref.to_s.upcase]
67
- end
68
-
69
- # Iterate over cells in the worksheet.
39
+ # Iterate over rows in the worksheet.
70
40
  #
71
41
  # @example
72
- # sheet.each do |cell|
73
- # puts cell.value
42
+ # sheet.each do |row|
43
+ # puts row.to_a.inspect
74
44
  # end
75
45
  #
76
- # @yield [cell]
77
- # @yieldparam cell [Elements::Cell]
46
+ # @yield [row]
47
+ # @yieldparam row [Elements::Row]
78
48
  # @return [Enumerator, void]
79
49
  # @api public
80
- #: () { (Elements::Cell) -> void } -> void
81
- #: | () -> Enumerator[Elements::Cell, void]
50
+ #: () { (Elements::Row) -> void } -> void
51
+ #: () -> Enumerator[Elements::Row, void]
82
52
  def each(&)
83
53
  return to_enum(:each) unless block_given?
84
54
 
85
- cells.each(&)
86
- end
87
-
88
- # Iterate over cells in the worksheet.
89
- #
90
- # @example
91
- # sheet.each_cell do |cell|
92
- # puts "#{cell.ref}: #{cell.value}"
93
- # end
94
- #
95
- # @yield [cell]
96
- # @yieldparam cell [Elements::Cell]
97
- # @return [Enumerator, void]
98
- # @api public
99
- #: () { (Elements::Cell) -> void } -> void
100
- #: | () -> Enumerator[Elements::Cell, void]
101
- def each_cell(&)
102
- return to_enum(:each_cell) unless block_given?
103
-
104
- cells.each(&)
55
+ rows.each(&)
105
56
  end
106
57
 
107
58
  # Iterate over rows in the worksheet.
@@ -116,68 +67,33 @@ module Xlsxrb
116
67
  # @return [Enumerator, void]
117
68
  # @api public
118
69
  #: () { (Elements::Row) -> void } -> void
119
- #: | () -> Enumerator[Elements::Row, void]
70
+ #: () -> Enumerator[Elements::Row, void]
120
71
  def each_row(&)
121
72
  return to_enum(:each_row) unless block_given?
122
73
 
123
74
  rows.each(&)
124
75
  end
125
76
 
126
- # Returns whether the worksheet is valid according to OOXML specifications.
77
+ # Iterate over all cells across rows.
127
78
  #
128
- # @return [Boolean]
129
- #: () -> bool
130
- def valid?
131
- errors.empty?
132
- end
133
-
134
- # Returns the row at the given 0-based index, or nil.
135
- #
136
- # @param index [Integer] 0-based row index.
137
- # @return [Elements::Row, nil]
138
- # @api public
139
- #: (Integer index) -> Elements::Row?
140
- def row_at(index)
141
- rows.find { |r| r.index == index }
142
- end
143
-
144
- # Returns the first row in the sheet, or nil.
145
- #
146
- # @return [Elements::Row, nil]
79
+ # @yield [cell]
80
+ # @yieldparam cell [Elements::Cell]
81
+ # @return [Enumerator, void]
147
82
  # @api public
148
- #: () -> Elements::Row?
149
- def first_row
150
- rows.min_by(&:index)
151
- end
83
+ #: () { (Elements::Cell) -> void } -> void
84
+ #: () -> Enumerator[Elements::Cell, void]
85
+ def each_cell(&)
86
+ return to_enum(:each_cell) unless block_given?
152
87
 
153
- # Returns the last row in the sheet, or nil.
154
- #
155
- # @return [Elements::Row, nil]
156
- # @api public
157
- #: () -> Elements::Row?
158
- def last_row
159
- rows.max_by(&:index)
88
+ cells.each(&)
160
89
  end
161
90
 
162
- # Returns the raw cell value at the given Excel-style reference (e.g. "A1").
163
- #
164
- # @example
165
- # sheet.cell_value("A1") #=> "Sales Report"
91
+ # Returns whether the worksheet is valid according to OOXML specifications.
166
92
  #
167
- # @param ref [String] Cell reference (e.g. "A1").
168
- # @return [Object, nil]
169
- # @api public
170
- #: (String ref) -> untyped
171
- def cell_value(ref)
172
- parsed = Cell.parse_ref(ref)
173
- return nil unless parsed
174
-
175
- row_idx, col_idx = parsed
176
- row = row_at(row_idx)
177
- return nil unless row
178
-
179
- cell = row.cell_at(col_idx)
180
- cell&.value
93
+ # @return [Boolean]
94
+ #: () -> bool
95
+ def valid?
96
+ errors.empty?
181
97
  end
182
98
 
183
99
  # Returns a new Worksheet with the specified cell updated.
@@ -227,6 +143,60 @@ module Xlsxrb
227
143
  with(rows: new_rows)
228
144
  end
229
145
 
146
+ # Returns a new Worksheet with attributes replaced (Data-like behavior).
147
+ #
148
+ # @param changes [Hash]
149
+ # @return [Worksheet]
150
+ # @api public
151
+ #: (**untyped) -> Elements::Worksheet
152
+ def with(**changes)
153
+ new_name = changes.key?(:name) ? changes[:name] : name
154
+ new_rows = changes.key?(:rows) ? changes[:rows] : rows
155
+ new_cols = changes.key?(:columns) ? changes[:columns] : columns
156
+ new_charts = changes.key?(:charts) ? changes[:charts] : charts
157
+ new_unmapped = changes.key?(:unmapped_data) ? changes[:unmapped_data] : unmapped_data
158
+ new_errors = changes.key?(:errors) ? changes[:errors] : errors
159
+
160
+ self.class.new(
161
+ name: new_name,
162
+ rows: new_rows,
163
+ columns: new_cols,
164
+ charts: new_charts,
165
+ unmapped_data: new_unmapped,
166
+ errors: new_errors
167
+ )
168
+ end
169
+
170
+ # Support pattern matching.
171
+ #: (Array[Symbol]?) -> Hash[Symbol, untyped]
172
+ def deconstruct_keys(_keys)
173
+ { name: name, rows: rows, columns: columns, charts: charts, unmapped_data: unmapped_data, errors: errors }
174
+ end
175
+
176
+ # Compare worksheets for equality.
177
+ #: (untyped other) -> bool
178
+ def ==(other)
179
+ return false unless other.is_a?(Worksheet)
180
+
181
+ name == other.name && rows == other.rows && columns == other.columns && charts == other.charts
182
+ end
183
+ alias eql? ==
184
+
185
+ #: () -> Integer
186
+ def hash
187
+ [self.class, name, rows, columns, charts].hash
188
+ end
189
+
190
+ # Returns self when load is called on an already in-memory Worksheet.
191
+ #
192
+ # @return [Elements::Worksheet]
193
+ # @api public
194
+ #: () -> Elements::Worksheet
195
+ def load
196
+ self
197
+ end
198
+ alias to_worksheet load
199
+
230
200
  # Validates worksheet name and rows against OOXML limits.
231
201
  #
232
202
  # @param name [String]
@@ -6,6 +6,7 @@ require_relative "elements/types"
6
6
  require_relative "elements/cell"
7
7
  require_relative "elements/row"
8
8
  require_relative "elements/column"
9
+ require_relative "elements/coordinate_access"
9
10
  require_relative "elements/worksheet"
10
11
  require_relative "elements/workbook"
11
12