xlsxrb 0.1.13 → 0.1.15

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -0
  3. data/README.md +17 -14
  4. data/lib/xlsxrb/colors.rb +118 -0
  5. data/lib/xlsxrb/elements/cell.rb +205 -16
  6. data/lib/xlsxrb/elements/column.rb +6 -4
  7. data/lib/xlsxrb/elements/coordinate_access.rb +32 -0
  8. data/lib/xlsxrb/elements/image.rb +128 -0
  9. data/lib/xlsxrb/elements/row.rb +206 -9
  10. data/lib/xlsxrb/elements/styles.rb +157 -0
  11. data/lib/xlsxrb/elements/types.rb +95 -1
  12. data/lib/xlsxrb/elements/workbook.rb +121 -4
  13. data/lib/xlsxrb/elements/worksheet.rb +519 -29
  14. data/lib/xlsxrb/elements.rb +2 -0
  15. data/lib/xlsxrb/number_formatter.rb +429 -0
  16. data/lib/xlsxrb/ooxml/cfb.rb +1 -1
  17. data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +7 -5
  18. data/lib/xlsxrb/ooxml/reader/listeners/drawing_listeners.rb +11 -4
  19. data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +7 -1
  20. data/lib/xlsxrb/ooxml/reader.rb +14 -17
  21. data/lib/xlsxrb/ooxml/shared_strings_parser.rb +7 -1
  22. data/lib/xlsxrb/ooxml/styles_parser.rb +39 -9
  23. data/lib/xlsxrb/ooxml/utils.rb +34 -18
  24. data/lib/xlsxrb/ooxml/workbook_parser.rb +74 -16
  25. data/lib/xlsxrb/ooxml/workbook_writer.rb +82 -23
  26. data/lib/xlsxrb/ooxml/worksheet_parser.rb +319 -79
  27. data/lib/xlsxrb/ooxml/worksheet_writer.rb +263 -66
  28. data/lib/xlsxrb/ooxml/writer/features_xml.rb +5 -3
  29. data/lib/xlsxrb/ooxml/writer.rb +14 -0
  30. data/lib/xlsxrb/ooxml/xml_builder.rb +14 -2
  31. data/lib/xlsxrb/ooxml/zip_reader.rb +144 -28
  32. data/lib/xlsxrb/stream_row.rb +243 -16
  33. data/lib/xlsxrb/stream_sheet.rb +647 -16
  34. data/lib/xlsxrb/stream_writer.rb +375 -34
  35. data/lib/xlsxrb/style_builder.rb +308 -22
  36. data/lib/xlsxrb/utils.rb +211 -0
  37. data/lib/xlsxrb/version.rb +1 -1
  38. data/lib/xlsxrb/workbook_builder.rb +72 -11
  39. data/lib/xlsxrb/worksheet_builder.rb +738 -24
  40. data/lib/xlsxrb.rb +1073 -83
  41. data/sig/generated/xlsxrb/colors.rbs +40 -0
  42. data/sig/generated/xlsxrb/elements/cell.rbs +113 -8
  43. data/sig/generated/xlsxrb/elements/column.rbs +3 -2
  44. data/sig/generated/xlsxrb/elements/coordinate_access.rbs +18 -0
  45. data/sig/generated/xlsxrb/elements/image.rbs +82 -0
  46. data/sig/generated/xlsxrb/elements/row.rbs +83 -4
  47. data/sig/generated/xlsxrb/elements/styles.rbs +79 -0
  48. data/sig/generated/xlsxrb/elements/types.rbs +43 -0
  49. data/sig/generated/xlsxrb/elements/workbook.rbs +79 -2
  50. data/sig/generated/xlsxrb/elements/worksheet.rbs +282 -18
  51. data/sig/generated/xlsxrb/number_formatter.rbs +132 -0
  52. data/sig/generated/xlsxrb/ooxml/reader.rbs +6 -0
  53. data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +2 -0
  54. data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +5 -2
  55. data/sig/generated/xlsxrb/ooxml/utils.rbs +17 -8
  56. data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +9 -3
  57. data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +1 -1
  58. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +28 -6
  59. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +13 -3
  60. data/sig/generated/xlsxrb/ooxml/writer.rbs +2 -0
  61. data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +3 -1
  62. data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +13 -0
  63. data/sig/generated/xlsxrb/stream_row.rbs +100 -6
  64. data/sig/generated/xlsxrb/stream_sheet.rbs +358 -14
  65. data/sig/generated/xlsxrb/stream_writer.rbs +220 -30
  66. data/sig/generated/xlsxrb/style_builder.rbs +55 -18
  67. data/sig/generated/xlsxrb/utils.rbs +151 -0
  68. data/sig/generated/xlsxrb/workbook_builder.rbs +26 -0
  69. data/sig/generated/xlsxrb/worksheet_builder.rbs +356 -18
  70. data/sig/generated/xlsxrb.rbs +223 -27
  71. metadata +11 -1
@@ -33,6 +33,16 @@ module Xlsxrb
33
33
  #: String
34
34
  attr_reader :name
35
35
 
36
+ # @return [Symbol] The sheet visibility state (:visible, :hidden, or :very_hidden).
37
+ # @api public
38
+ #: Symbol
39
+ attr_reader :state
40
+
41
+ # @return [Hash, nil] The parsed styles definition hash.
42
+ # @api public
43
+ #: Hash[untyped, untyped]?
44
+ attr_reader :styles
45
+
36
46
  # Initializes a streaming worksheet context.
37
47
  #
38
48
  # @param name [String] The sheet name.
@@ -41,15 +51,136 @@ module Xlsxrb
41
51
  # @param styles [Hash, nil] Optional parsed styles hash.
42
52
  # @param zip_reader [Ooxml::ZipReader, nil] Optional ZipReader context.
43
53
  # @param entry_name [String, nil] Archive entry name for this sheet.
44
- #: (String name, untyped sheet_source, Array[String] shared_strings, ?Hash[untyped, untyped]? styles, ?zip_reader: Ooxml::ZipReader?, ?entry_name: String?) -> void
45
- def initialize(name, sheet_source, shared_strings, styles = nil, zip_reader: nil, entry_name: nil)
54
+ # @param date1904 [Boolean] Whether the 1904 date system is active.
55
+ # @param dimension [String, nil] Sheet dimension reference string (e.g. "A1:Z50000").
56
+ # @param trim_empty_rows [Boolean] Whether to omit trailing empty rows during iteration.
57
+ # @param pad_empty_rows [Boolean] Whether to yield empty rows for skipped row numbers.
58
+ # @param pad_empty_cells [Boolean] Whether to pad missing cells within rows.
59
+ # @param images [Array<Elements::Image>, nil] Optional preloaded images array.
60
+ #: (String name, untyped sheet_source, Array[String] shared_strings, ?Hash[untyped, untyped]? styles, ?zip_reader: Ooxml::ZipReader?, ?entry_name: String?, ?state: Symbol, ?date1904: bool, ?dimension: String?, ?trim_empty_rows: bool, ?pad_empty_rows: bool, ?pad_empty_cells: bool, ?images: Array[Elements::Image]?) -> void
61
+ def initialize(name, sheet_source, shared_strings, styles = nil, zip_reader: nil, entry_name: nil, state: :visible, date1904: false, dimension: nil, trim_empty_rows: false, pad_empty_rows: false, pad_empty_cells: false, images: nil)
46
62
  @name = name
47
63
  @sheet_source = sheet_source
48
64
  @shared_strings = shared_strings
49
65
  @styles = styles
50
66
  @zip_reader = zip_reader
51
67
  @entry_name = entry_name
68
+ @state = state ? state.to_sym : :visible
69
+ @date1904 = date1904 ? true : false
70
+ @dimension = dimension
71
+ @dimension_checked = !dimension.nil?
52
72
  @sheet_xml = sheet_source if sheet_source.is_a?(String)
73
+ @trim_empty_rows = trim_empty_rows ? true : false
74
+ @pad_empty_rows = pad_empty_rows ? true : false
75
+ @pad_empty_cells = pad_empty_cells ? true : false
76
+ @images = images
77
+ end
78
+
79
+ # Returns whether trailing empty rows are omitted during iteration.
80
+ #
81
+ # @return [Boolean]
82
+ # @api public
83
+ #: () -> bool
84
+ def trim_empty_rows?
85
+ @trim_empty_rows ? true : false
86
+ end
87
+ alias trim_empty_rows trim_empty_rows?
88
+
89
+ # Returns whether skipped rows are padded during iteration.
90
+ #
91
+ # @return [Boolean]
92
+ # @api public
93
+ #: () -> bool
94
+ def pad_empty_rows?
95
+ @pad_empty_rows ? true : false
96
+ end
97
+ alias pad_empty_rows pad_empty_rows?
98
+
99
+ # Returns whether missing cells within rows are padded during iteration.
100
+ #
101
+ # @return [Boolean]
102
+ # @api public
103
+ #: () -> bool
104
+ def pad_empty_cells?
105
+ @pad_empty_cells ? true : false
106
+ end
107
+ alias pad_empty_cells pad_empty_cells?
108
+
109
+ # Returns the dimension reference string (e.g. "A1:Z50000"), or nil if not present.
110
+ #
111
+ # @return [String, nil]
112
+ # @api public
113
+ #: () -> String?
114
+ def dimension
115
+ return @dimension if @dimension_checked
116
+
117
+ @dimension_checked = true
118
+ @dimension = extract_dimension
119
+ end
120
+
121
+ # Returns the 1-based index of the first row defined in the sheet dimension, or nil.
122
+ #
123
+ # @return [Integer, nil]
124
+ # @api public
125
+ #: () -> Integer?
126
+ def first_row
127
+ dimension_bounds&.[](0)
128
+ end
129
+
130
+ # Returns the 1-based index of the first column defined in the sheet dimension, or nil.
131
+ #
132
+ # @return [Integer, nil]
133
+ # @api public
134
+ #: () -> Integer?
135
+ def first_column
136
+ dimension_bounds&.[](1)
137
+ end
138
+ alias first_col first_column
139
+
140
+ # Returns the 1-based index of the last row defined in the sheet dimension, or nil.
141
+ #
142
+ # @return [Integer, nil]
143
+ # @api public
144
+ #: () -> Integer?
145
+ def last_row
146
+ dimension_bounds&.[](2)
147
+ end
148
+
149
+ # Returns the 1-based index of the last column defined in the sheet dimension, or nil.
150
+ #
151
+ # @return [Integer, nil]
152
+ # @api public
153
+ #: () -> Integer?
154
+ def last_column
155
+ dimension_bounds&.[](3)
156
+ end
157
+ alias last_col last_column
158
+
159
+ # Returns whether the sheet uses the 1904 date system.
160
+ #
161
+ # @return [Boolean]
162
+ # @api public
163
+ #: () -> bool
164
+ def date1904?
165
+ @date1904
166
+ end
167
+
168
+ # Returns whether the sheet is hidden (:hidden or :very_hidden).
169
+ #
170
+ # @return [Boolean]
171
+ # @api public
172
+ #: () -> bool
173
+ def hidden?
174
+ @state == :hidden || @state == :very_hidden
175
+ end
176
+
177
+ # Returns whether the sheet is visible.
178
+ #
179
+ # @return [Boolean]
180
+ # @api public
181
+ #: () -> bool
182
+ def visible?
183
+ @state == :visible
53
184
  end
54
185
 
55
186
  # Iterates over rows in this streaming worksheet with O(1) memory.
@@ -57,16 +188,41 @@ module Xlsxrb
57
188
  # @overload each_row(&block)
58
189
  # @yield [row]
59
190
  # @yieldparam row [StreamRow, Elements::Row] The current row.
191
+ # @overload each_row(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &block)
192
+ # @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows (defaults to sheet setting).
193
+ # @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers (defaults to sheet setting).
194
+ # @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows (defaults to sheet setting).
195
+ # @yield [row]
196
+ # @yieldparam row [StreamRow, Elements::Row] The current row.
60
197
  # @return [void]
61
198
  #
62
- # @overload each_row
199
+ # @overload each_row(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil)
200
+ # @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows (defaults to sheet setting).
201
+ # @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers (defaults to sheet setting).
202
+ # @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows (defaults to sheet setting).
63
203
  # @return [Enumerator<StreamRow | Elements::Row, void>]
64
204
  #
65
205
  # @api public
66
- #: () { (StreamRow | Elements::Row) -> void } -> void
67
- #: () -> Enumerator[StreamRow | Elements::Row, void]
68
- def each_row(&)
69
- return enum_for(:each_row) unless block_given?
206
+ #: (?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) { (StreamRow | Elements::Row) -> void } -> void
207
+ #: (?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) -> Enumerator[StreamRow | Elements::Row, void]
208
+ def each_row(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &)
209
+ return enum_for(:each_row, trim_empty_rows: trim_empty_rows, pad_empty_rows: pad_empty_rows, pad_empty_cells: pad_empty_cells) unless block_given?
210
+
211
+ trim = if trim_empty_rows.nil?
212
+ @trim_empty_rows
213
+ else
214
+ (trim_empty_rows ? true : false)
215
+ end
216
+ pad_rows = if pad_empty_rows.nil?
217
+ @pad_empty_rows
218
+ else
219
+ (pad_empty_rows ? true : false)
220
+ end
221
+ pad_cells = if pad_empty_cells.nil?
222
+ @pad_empty_cells
223
+ else
224
+ (pad_empty_cells ? true : false)
225
+ end
70
226
 
71
227
  source = if @sheet_source
72
228
  @sheet_source
@@ -74,7 +230,7 @@ module Xlsxrb
74
230
  ->(&blk) { @zip_reader.each_entry_chunk(@entry_name, &blk) }
75
231
  end
76
232
 
77
- Ooxml::WorksheetParser.each_row(source, shared_strings: @shared_strings, &)
233
+ Ooxml::WorksheetParser.each_row(source, shared_strings: @shared_strings, styles: @styles, date1904: @date1904, trim_empty_rows: trim, pad_empty_rows: pad_rows, pad_empty_cells: pad_cells, &)
78
234
  end
79
235
 
80
236
  # Iterates over all cells across all rows continuously with O(1) memory.
@@ -98,21 +254,56 @@ module Xlsxrb
98
254
  end
99
255
  end
100
256
 
257
+ # Iterates over row values directly as Arrays without wrapping cells in Elements::Cell objects.
258
+ #
259
+ # @overload each_row_values(type_cast: false, trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &block)
260
+ # @param type_cast [Boolean] Whether to coerce date/time serial numbers into Date/Time instances.
261
+ # @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows (defaults to sheet setting).
262
+ # @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers (defaults to sheet setting).
263
+ # @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows (defaults to sheet setting).
264
+ # @yield [values]
265
+ # @yieldparam values [Array<Object>] Row values array.
266
+ # @return [void]
267
+ #
268
+ # @overload each_row_values(type_cast: false, trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil)
269
+ # @param type_cast [Boolean] Whether to coerce date/time serial numbers into Date/Time instances.
270
+ # @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows (defaults to sheet setting).
271
+ # @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers (defaults to sheet setting).
272
+ # @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows (defaults to sheet setting).
273
+ # @return [Enumerator<Array<Object>, void>]
274
+ #
275
+ # @api public
276
+ #: (?type_cast: bool, ?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) { (Array[untyped]) -> void } -> void
277
+ #: (?type_cast: bool, ?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) -> Enumerator[Array[untyped], void]
278
+ def each_row_values(type_cast: false, trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &block)
279
+ return enum_for(:each_row_values, type_cast: type_cast, trim_empty_rows: trim_empty_rows, pad_empty_rows: pad_empty_rows, pad_empty_cells: pad_empty_cells) unless block
280
+
281
+ each_row(trim_empty_rows: trim_empty_rows, pad_empty_rows: pad_empty_rows, pad_empty_cells: pad_empty_cells) do |row|
282
+ block.call(row.values(type_cast: type_cast))
283
+ end
284
+ end
285
+
101
286
  # Default Enumerable iteration delegates to {#each_row}.
102
287
  #
103
- # @overload each(&block)
288
+ # @overload each(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &block)
289
+ # @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows.
290
+ # @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers.
291
+ # @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows.
104
292
  # @yield [row]
105
293
  # @yieldparam row [StreamRow, Elements::Row]
106
294
  # @return [void]
107
295
  #
108
- # @overload each
296
+ # @overload each(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil)
297
+ # @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows.
298
+ # @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers.
299
+ # @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows.
109
300
  # @return [Enumerator<StreamRow | Elements::Row, void>]
110
301
  #
111
302
  # @api public
112
- #: () { (StreamRow | Elements::Row) -> void } -> void
113
- #: () -> Enumerator[StreamRow | Elements::Row, void]
114
- def each(&)
115
- each_row(&)
303
+ #: (?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) { (StreamRow | Elements::Row) -> void } -> void
304
+ #: (?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) -> Enumerator[StreamRow | Elements::Row, void]
305
+ def each(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &)
306
+ each_row(trim_empty_rows: trim_empty_rows, pad_empty_rows: pad_empty_rows, pad_empty_cells: pad_empty_cells, &)
116
307
  end
117
308
 
118
309
  # Loads this sheet completely into an in-memory {Elements::Worksheet},
@@ -123,10 +314,185 @@ module Xlsxrb
123
314
  # @api public
124
315
  #: () -> Elements::Worksheet
125
316
  def load
126
- Xlsxrb.send(:build_worksheet, @name, raw_sheet_xml, @shared_strings, @styles)
317
+ Xlsxrb.send(
318
+ :build_worksheet,
319
+ @name,
320
+ raw_sheet_xml,
321
+ @shared_strings,
322
+ @styles,
323
+ state: @state,
324
+ zip_reader: @zip_reader,
325
+ entry_name: @entry_name,
326
+ hyperlinks: hyperlinks,
327
+ comments: comments,
328
+ images: images,
329
+ date1904: @date1904,
330
+ dimension: dimension,
331
+ trim_empty_rows: @trim_empty_rows,
332
+ pad_empty_rows: @pad_empty_rows,
333
+ pad_empty_cells: @pad_empty_cells,
334
+ merged_cells: merged_cells,
335
+ auto_filter: auto_filter,
336
+ tables: tables,
337
+ shapes: shapes,
338
+ freeze_pane: freeze_pane,
339
+ page_setup: page_setup,
340
+ page_margins: page_margins,
341
+ header_footer: header_footer,
342
+ print_options: print_options,
343
+ sheet_protection: sheet_protection,
344
+ sheet_view: sheet_view,
345
+ sheet_properties: sheet_properties,
346
+ sheet_format: sheet_format,
347
+ row_breaks: row_breaks,
348
+ col_breaks: col_breaks,
349
+ protected_ranges: protected_ranges,
350
+ cell_watches: cell_watches
351
+ )
127
352
  end
128
353
  alias to_worksheet load
129
354
 
355
+ # Returns hyperlinks configured for this worksheet as a Hash of cell references to hyperlink hashes.
356
+ #
357
+ # @return [Hash<String, Hash[Symbol, untyped]>]
358
+ # @api public
359
+ #: () -> Hash[String, Hash[Symbol, untyped]]
360
+ def hyperlinks
361
+ @hyperlinks ||= Xlsxrb.send(:resolve_hyperlinks, raw_sheet_xml, zip_reader: @zip_reader, entry_name: @entry_name)
362
+ end
363
+
364
+ # Returns hyperlink metadata for a specific cell reference, or nil.
365
+ #
366
+ # @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
367
+ # @param col [Integer, nil] Optional 0-based column index.
368
+ # @return [Hash, nil]
369
+ # @api public
370
+ #: (String | Symbol | Integer ref_or_row, ?Integer? col) -> Hash[Symbol, untyped]?
371
+ def hyperlink(ref_or_row, col = nil)
372
+ ref = if col
373
+ "#{Elements::Cell.column_letter(col)}#{ref_or_row.to_i + 1}"
374
+ else
375
+ ref_or_row.to_s.upcase
376
+ end
377
+ hyperlinks[ref]
378
+ end
379
+
380
+ # Returns comments configured for this worksheet as an Array of comment hashes.
381
+ #
382
+ # @return [Array<Hash[Symbol, untyped]>]
383
+ # @api public
384
+ #: () -> Array[Hash[Symbol, untyped]]
385
+ def comments
386
+ @comments ||= Xlsxrb.send(:resolve_comments, zip_reader: @zip_reader, entry_name: @entry_name)
387
+ end
388
+
389
+ # Returns comment metadata for a specific cell reference, or nil.
390
+ #
391
+ # @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
392
+ # @param col [Integer, nil] Optional 0-based column index.
393
+ # @return [Hash, nil]
394
+ # @api public
395
+ #: (String | Symbol | Integer ref_or_row, ?Integer? col) -> Hash[Symbol, untyped]?
396
+ def comment(ref_or_row, col = nil)
397
+ ref = if col
398
+ "#{Elements::Cell.column_letter(col)}#{ref_or_row.to_i + 1}"
399
+ else
400
+ ref_or_row.to_s.upcase
401
+ end
402
+ comments_by_ref[ref]
403
+ end
404
+
405
+ # Returns comments indexed by cell reference.
406
+ #
407
+ # @return [Hash<String, Hash[Symbol, untyped]>]
408
+ # @api public
409
+ #: () -> Hash[String, Hash[Symbol, untyped]]
410
+ def comments_by_ref
411
+ @comments_by_ref ||= comments.each_with_object({}) do |c, acc|
412
+ r = c[:ref] || c[:cell]
413
+ acc[r.to_s.upcase] = c if r
414
+ end
415
+ end
416
+
417
+ # Returns all embedded images in this worksheet.
418
+ #
419
+ # @return [Array<Elements::Image>]
420
+ # @api public
421
+ #: () -> Array[Elements::Image]
422
+ def images
423
+ @images ||= Xlsxrb.send(:resolve_images, zip_reader: @zip_reader, entry_name: @entry_name)
424
+ end
425
+
426
+ # Returns all images anchored at a specific cell reference or 0-based coordinate.
427
+ #
428
+ # @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
429
+ # @param col [Integer, nil] Optional 0-based column index.
430
+ # @return [Array<Elements::Image>]
431
+ # @api public
432
+ #: (String | Symbol | Integer ref_or_row, ?Integer? col) -> Array[Elements::Image]
433
+ def images_at(ref_or_row, col = nil)
434
+ ref = if col
435
+ "#{Elements::Cell.column_letter(col)}#{ref_or_row.to_i + 1}"
436
+ else
437
+ ref_or_row.to_s.upcase
438
+ end
439
+ images.select { |img| img.cell_ref == ref }
440
+ end
441
+
442
+ # Loads and returns images for this worksheet, optionally yielding self for block syntax.
443
+ #
444
+ # @yield [sheet]
445
+ # @yieldparam sheet [StreamSheet]
446
+ # @return [StreamSheet, void]
447
+ # @api public
448
+ #: () { (StreamSheet) -> void } -> void
449
+ #: () -> StreamSheet
450
+ def with_images
451
+ images
452
+ if block_given?
453
+ yield self
454
+ nil
455
+ else
456
+ self
457
+ end
458
+ end
459
+
460
+ # Access a cell by reference or 0-based coordinates.
461
+ #
462
+ # @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
463
+ # @param col [Integer, nil] Optional 0-based column index.
464
+ # @return [Elements::Cell, nil]
465
+ # @api public
466
+ #: (String | Symbol | Integer ref_or_row, ?Integer? col) -> Elements::Cell?
467
+ def cell(ref_or_row, col = nil)
468
+ target_row, target_col = if col
469
+ [ref_or_row.to_i, col.to_i]
470
+ else
471
+ parsed = Elements::Cell.parse_ref(ref_or_row.to_s)
472
+ return nil unless parsed
473
+
474
+ parsed
475
+ end
476
+
477
+ each_row do |row|
478
+ next if row.index < target_row
479
+ return row.cell_at(target_col) if row.index == target_row
480
+ break if row.index > target_row
481
+ end
482
+ nil
483
+ end
484
+
485
+ # Returns the formatted string representation of a cell's value.
486
+ #
487
+ # @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
488
+ # @param col [Integer, nil] Optional 0-based column index.
489
+ # @return [String, nil]
490
+ # @api public
491
+ #: (String | Symbol | Integer ref_or_row, ?Integer? col) -> String?
492
+ def formatted_value(ref_or_row, col = nil)
493
+ cell(ref_or_row, col)&.formatted_value
494
+ end
495
+
130
496
  # Returns merged cell ranges (e.g. ["A1:B2"]) for this worksheet.
131
497
  #
132
498
  # @return [Array<String>]
@@ -179,6 +545,169 @@ module Xlsxrb
179
545
  end
180
546
  end
181
547
 
548
+ # Returns tables defined in this worksheet.
549
+ #
550
+ # @return [Array<Hash[Symbol, untyped]>]
551
+ # @api public
552
+ #: () -> Array[Hash[Symbol, untyped]]
553
+ def tables
554
+ @tables ||= Xlsxrb.send(:resolve_tables, zip_reader: @zip_reader, entry_name: @entry_name)
555
+ end
556
+
557
+ # Returns shapes defined in this worksheet.
558
+ #
559
+ # @return [Array<Hash[Symbol, untyped]>]
560
+ # @api public
561
+ #: () -> Array[Hash[Symbol, untyped]]
562
+ def shapes
563
+ @shapes ||= Xlsxrb.send(:resolve_shapes, zip_reader: @zip_reader, entry_name: @entry_name)
564
+ end
565
+
566
+ # Returns freeze pane settings for this worksheet, or nil if none.
567
+ #
568
+ # @return [Hash[Symbol, untyped], nil]
569
+ # @api public
570
+ #: () -> Hash[Symbol, untyped]?
571
+ def freeze_pane
572
+ sheet_view_data[:pane]
573
+ end
574
+
575
+ # Returns sheet view properties for this worksheet.
576
+ #
577
+ # @return [Hash[Symbol, untyped]]
578
+ # @api public
579
+ #: () -> Hash[Symbol, untyped]
580
+ def sheet_view
581
+ sheet_view_data[:view] || {}
582
+ end
583
+
584
+ # Returns sheet protection settings as a hash, or nil if unprotected.
585
+ #
586
+ # @return [Hash[Symbol, untyped], nil]
587
+ # @api public
588
+ #: () -> Hash[Symbol, untyped]?
589
+ def sheet_protection
590
+ @sheet_protection ||= if raw_sheet_xml.include?("<sheetProtection")
591
+ listener = Ooxml::Reader::SheetProtectionListener.new
592
+ Ooxml::XmlParser.parse(raw_sheet_xml, listener)
593
+ listener.protection
594
+ end
595
+ end
596
+
597
+ # Returns sheet-level properties (tabColor, outlinePr) for this worksheet.
598
+ #
599
+ # @return [Hash[Symbol, untyped]]
600
+ # @api public
601
+ #: () -> Hash[Symbol, untyped]
602
+ def sheet_properties
603
+ @sheet_properties ||= if raw_sheet_xml.include?("<sheetPr")
604
+ listener = Ooxml::Reader::SheetPropertiesListener.new
605
+ Ooxml::XmlParser.parse(raw_sheet_xml, listener)
606
+ listener.properties
607
+ else
608
+ {}
609
+ end
610
+ end
611
+
612
+ # Returns sheet format properties (defaultRowHeight, defaultColWidth, baseColWidth) for this worksheet.
613
+ #
614
+ # @return [Hash[Symbol, untyped]]
615
+ # @api public
616
+ #: () -> Hash[Symbol, untyped]
617
+ def sheet_format
618
+ @sheet_format ||= if raw_sheet_xml.include?("<sheetFormatPr")
619
+ listener = Ooxml::Reader::SheetFormatListener.new
620
+ Ooxml::XmlParser.parse(raw_sheet_xml, listener)
621
+ listener.properties
622
+ else
623
+ {}
624
+ end
625
+ end
626
+
627
+ # Returns page setup configurations for this worksheet.
628
+ #
629
+ # @return [Hash[Symbol, untyped]]
630
+ # @api public
631
+ #: () -> Hash[Symbol, untyped]
632
+ def page_setup
633
+ print_page_data[:page_setup] || {}
634
+ end
635
+
636
+ # Returns page margins for this worksheet, or nil if none.
637
+ #
638
+ # @return [Hash[Symbol, untyped], nil]
639
+ # @api public
640
+ #: () -> Hash[Symbol, untyped]?
641
+ def page_margins
642
+ print_page_data[:page_margins]
643
+ end
644
+
645
+ # Returns header/footer configurations for this worksheet.
646
+ #
647
+ # @return [Hash[Symbol, untyped]]
648
+ # @api public
649
+ #: () -> Hash[Symbol, untyped]
650
+ def header_footer
651
+ print_page_data[:header_footer] || {}
652
+ end
653
+
654
+ # Returns print options for this worksheet.
655
+ #
656
+ # @return [Hash[Symbol, untyped]]
657
+ # @api public
658
+ #: () -> Hash[Symbol, untyped]
659
+ def print_options
660
+ print_page_data[:print_options] || {}
661
+ end
662
+
663
+ # Returns explicit row page breaks for this worksheet.
664
+ #
665
+ # @return [Array<untyped>]
666
+ # @api public
667
+ #: () -> Array[untyped]
668
+ def row_breaks
669
+ print_page_data[:row_breaks] || []
670
+ end
671
+
672
+ # Returns explicit column page breaks for this worksheet.
673
+ #
674
+ # @return [Array<untyped>]
675
+ # @api public
676
+ #: () -> Array[untyped]
677
+ def col_breaks
678
+ print_page_data[:col_breaks] || []
679
+ end
680
+
681
+ # Returns protected ranges defined for this worksheet.
682
+ #
683
+ # @return [Array<Hash[Symbol, untyped]>]
684
+ # @api public
685
+ #: () -> Array[Hash[Symbol, untyped]]
686
+ def protected_ranges
687
+ @protected_ranges ||= if raw_sheet_xml.include?("<protectedRanges")
688
+ listener = Ooxml::Reader::ProtectedRangesListener.new
689
+ Ooxml::XmlParser.parse(raw_sheet_xml, listener)
690
+ listener.ranges
691
+ else
692
+ []
693
+ end
694
+ end
695
+
696
+ # Returns cell watch cell references defined for this worksheet.
697
+ #
698
+ # @return [Array<String>]
699
+ # @api public
700
+ #: () -> Array[String]
701
+ def cell_watches
702
+ @cell_watches ||= if raw_sheet_xml.include?("<cellWatches")
703
+ listener = Ooxml::Reader::CellWatchesListener.new
704
+ Ooxml::XmlParser.parse(raw_sheet_xml, listener)
705
+ listener.watches
706
+ else
707
+ []
708
+ end
709
+ end
710
+
182
711
  # Closes any underlying reader resources.
183
712
  #: () -> void
184
713
  def close
@@ -188,7 +717,42 @@ module Xlsxrb
188
717
 
189
718
  private
190
719
 
191
- # Returns the full raw XML string for this worksheet, loaded on demand.
720
+ #: () -> Hash[Symbol, untyped]
721
+ def sheet_view_data
722
+ @sheet_view_data ||= if raw_sheet_xml.include?("<sheetView")
723
+ listener = Ooxml::Reader::SheetViewListener.new
724
+ Ooxml::XmlParser.parse(raw_sheet_xml, listener)
725
+ { view: listener.view, pane: listener.pane, selection: listener.selection }
726
+ else
727
+ { view: {}, pane: nil, selection: nil }
728
+ end
729
+ end
730
+
731
+ #: () -> Hash[Symbol, untyped]
732
+ def print_page_data
733
+ @print_page_data ||= if raw_sheet_xml.match?(/<(?:pageSetup|pageMargins|headerFooter|printOptions|rowBreaks|colBreaks)\b/)
734
+ listener = Ooxml::Reader::PrintPageListener.new
735
+ Ooxml::XmlParser.parse(raw_sheet_xml, listener)
736
+ {
737
+ page_margins: listener.page_margins,
738
+ page_setup: listener.page_setup,
739
+ header_footer: listener.header_footer,
740
+ print_options: listener.print_options,
741
+ row_breaks: listener.row_breaks,
742
+ col_breaks: listener.col_breaks
743
+ }
744
+ else
745
+ {
746
+ page_margins: nil,
747
+ page_setup: {},
748
+ header_footer: {},
749
+ print_options: {},
750
+ row_breaks: [],
751
+ col_breaks: []
752
+ }
753
+ end
754
+ end
755
+
192
756
  #: () -> String
193
757
  def raw_sheet_xml
194
758
  @raw_sheet_xml ||= if @sheet_source.is_a?(String)
@@ -203,5 +767,72 @@ module Xlsxrb
203
767
  ""
204
768
  end
205
769
  end
770
+
771
+ # Returns 1-based bounds [first_row, first_col, last_row, last_col] from dimension ref.
772
+ #: () -> Array[Integer]?
773
+ def dimension_bounds
774
+ return @dimension_bounds if defined?(@dimension_bounds)
775
+
776
+ ref = dimension
777
+ return @dimension_bounds = nil unless ref
778
+
779
+ parts = ref.split(":", 2)
780
+ first_coords = Elements::Cell.parse_ref(parts[0])
781
+ return @dimension_bounds = nil unless first_coords
782
+
783
+ last_coords = parts[1] ? Elements::Cell.parse_ref(parts[1]) : first_coords
784
+ return @dimension_bounds = nil unless last_coords
785
+
786
+ @dimension_bounds = [
787
+ first_coords[0] + 1,
788
+ first_coords[1] + 1,
789
+ last_coords[0] + 1,
790
+ last_coords[1] + 1
791
+ ].freeze
792
+ end
793
+
794
+ # Extracts dimension reference from XML source lazily without parsing the entire file.
795
+ #: () -> String?
796
+ def extract_dimension
797
+ if @sheet_source.is_a?(String)
798
+ @sheet_source.slice(/<(?:[a-zA-Z0-9_]+:)?dimension\b[^>]*\bref=["']([^"']+)["']/, 1)
799
+ elsif @zip_reader && @entry_name
800
+ stream = @zip_reader.open_entry_io(@entry_name)
801
+ buf = +""
802
+ dim = nil
803
+ begin
804
+ while (chunk = stream.read(65_536))
805
+ buf << chunk
806
+ if (m = buf.match(/<(?:[a-zA-Z0-9_]+:)?dimension\b[^>]*\bref=["']([^"']+)["']/))
807
+ dim = m[1]
808
+ break
809
+ end
810
+ break if buf.include?("<sheetData") || buf.include?(":sheetData")
811
+ end
812
+ ensure
813
+ stream.close
814
+ end
815
+ dim
816
+ elsif @sheet_source.respond_to?(:call)
817
+ buf = +""
818
+ dim = nil
819
+ @sheet_source.call do |chunk|
820
+ buf << chunk
821
+ if (m = buf.match(/<(?:[a-zA-Z0-9_]+:)?dimension\b[^>]*\bref=["']([^"']+)["']/))
822
+ dim = m[1]
823
+ break
824
+ end
825
+ break if buf.include?("<sheetData") || buf.include?(":sheetData")
826
+ end
827
+ dim
828
+ elsif @sheet_source.respond_to?(:seek) && @sheet_source.respond_to?(:pos)
829
+ pos = @sheet_source.pos
830
+ buf = @sheet_source.read(65_536) || ""
831
+ @sheet_source.seek(pos, IO::SEEK_SET)
832
+ buf.slice(/<(?:[a-zA-Z0-9_]+:)?dimension\b[^>]*\bref=["']([^"']+)["']/, 1)
833
+ else
834
+ raw_sheet_xml.slice(/<(?:[a-zA-Z0-9_]+:)?dimension\b[^>]*\bref=["']([^"']+)["']/, 1)
835
+ end
836
+ end
206
837
  end
207
838
  end