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
@@ -11,10 +11,12 @@ module Xlsxrb
11
11
  class WorksheetBuilder
12
12
  # @param name [String] The worksheet name.
13
13
  # @param strict_excel_mode [Boolean] Whether to enforce Microsoft Excel limits.
14
- #: (String name, ?strict_excel_mode: bool) -> void
15
- def initialize(name, strict_excel_mode: true)
14
+ # @param state [Symbol] Sheet visibility state (:visible, :hidden, or :very_hidden).
15
+ #: (String name, ?strict_excel_mode: bool, ?state: Symbol) -> void
16
+ def initialize(name, strict_excel_mode: true, state: :visible)
16
17
  @name = name
17
18
  @strict_excel_mode = strict_excel_mode
19
+ @state = state ? state.to_sym : :visible
18
20
  @rows = []
19
21
  @columns = []
20
22
  @charts = []
@@ -42,10 +44,17 @@ module Xlsxrb
42
44
  @shapes = []
43
45
  @sheet_properties = {}
44
46
  @sheet_view = {}
47
+ @sheet_format = {}
45
48
  @row_breaks = []
46
49
  @col_breaks = []
50
+ @protected_ranges = []
51
+ @cell_watches = []
52
+ @print_area = nil
53
+ @print_titles = nil
47
54
  end
48
55
 
56
+ attr_reader :name
57
+
49
58
  # Defines or configures a named cell style.
50
59
  #
51
60
  # @example Define a bold header style
@@ -81,11 +90,12 @@ module Xlsxrb
81
90
  # @param hidden [Boolean] Whether the row is hidden.
82
91
  # @param custom_height [Boolean] Whether custom row height is enforced.
83
92
  # @param outline_level [Integer, nil] Grouping/outline hierarchy level.
93
+ # @param collapsed [Boolean] Whether the row is collapsed.
84
94
  # @return [void]
85
95
  # @raise [ArgumentError] If limits are exceeded when strict_excel_mode is enabled.
86
96
  # @api public
87
- #: (Array[untyped] | Hash[untyped, untyped] values, ?styles: untyped, ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?outline_level: Integer | nil) -> void
88
- def row(values, styles: nil, height: nil, hidden: false, custom_height: false, outline_level: nil)
97
+ #: (Array[untyped] | Hash[untyped, untyped] values, ?styles: untyped, ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?collapsed: bool, ?outline_level: Integer | nil) -> void
98
+ def row(values, styles: nil, height: nil, hidden: false, custom_height: false, collapsed: false, outline_level: nil)
89
99
  row_index = @rows.size
90
100
  DslHelpers.validate_row_bounds!(row_index, height, strict_excel_mode: @strict_excel_mode)
91
101
 
@@ -149,6 +159,10 @@ module Xlsxrb
149
159
  col_index += 1
150
160
  next
151
161
  end
162
+ sb = @styles[style_name]
163
+ fmt_code = if sb&.num_fmt_id
164
+ sb.num_fmt_id.is_a?(String) ? sb.num_fmt_id : Ooxml::Utils::BUILTIN_NUM_FMT_CODES[sb.num_fmt_id]
165
+ end
152
166
  # If value is a Formula object or Hash with :formula, store it as the cell's formula
153
167
  cells[col_index] = if val.is_a?(Elements::Formula)
154
168
  Elements::Cell.new(
@@ -156,7 +170,9 @@ module Xlsxrb
156
170
  column_index: col_index,
157
171
  value: val.cached_value,
158
172
  formula: val,
159
- style_index: style_name
173
+ style_index: style_name,
174
+ raw_value: val.cached_value&.to_s,
175
+ format_code: fmt_code
160
176
  )
161
177
  elsif val.is_a?(Hash) && val.key?(:formula)
162
178
  f_obj = Elements::Formula.new(
@@ -169,14 +185,18 @@ module Xlsxrb
169
185
  column_index: col_index,
170
186
  value: val[:value],
171
187
  formula: f_obj,
172
- style_index: style_name
188
+ style_index: style_name,
189
+ raw_value: val[:value]&.to_s,
190
+ format_code: fmt_code
173
191
  )
174
192
  else
175
193
  Elements::Cell.new(
176
194
  row_index: row_index,
177
195
  column_index: col_index,
178
196
  value: val,
179
- style_index: style_name
197
+ style_index: style_name,
198
+ raw_value: val&.to_s,
199
+ format_code: fmt_code
180
200
  )
181
201
  end
182
202
  col_index += 1
@@ -189,9 +209,25 @@ module Xlsxrb
189
209
  height: height,
190
210
  hidden: hidden,
191
211
  custom_height: custom_height || !height.nil?,
212
+ collapsed: collapsed,
192
213
  outline_level: outline_level
193
214
  )
194
215
  end
216
+ alias add_row row
217
+
218
+ # Appends a row to the worksheet and returns self for chaining.
219
+ #
220
+ # @example
221
+ # sheet << ["A", 1] << ["B", 2]
222
+ #
223
+ # @param values [Array, Hash] Row cell values.
224
+ # @return [self]
225
+ # @api public
226
+ #: (Array[untyped] | Hash[untyped, untyped] values) -> self
227
+ def <<(values)
228
+ row(values)
229
+ self
230
+ end
195
231
 
196
232
  # Sets column formatting and properties for one or multiple columns.
197
233
  #
@@ -203,23 +239,24 @@ module Xlsxrb
203
239
  #
204
240
  # @param index [Integer, String, Range, Array] Column index (0-based), letter ("A"), or range ("A".."D").
205
241
  # @param width [Float, Integer, nil] Column width in character units (0 to 255).
206
- # @param hidden [Boolean] Whether the column is hidden.
207
- # @param custom_width [Boolean] Whether custom width is explicitly set.
208
- # @param outline_level [Integer, nil] Grouping/outline level.
242
+ # @param style [Integer, Symbol, String, nil] Column style format or index.
209
243
  # @return [void]
210
244
  # @raise [ArgumentError] If width exceeds 255 in strict mode.
211
245
  # @api public
212
- #: (Integer | String | Range[Integer | String] | Array[Integer | String] index, ?width: Float | Integer | nil, ?hidden: bool, ?custom_width: bool, ?outline_level: Integer | nil) -> void
213
- def column(index, width: nil, hidden: false, custom_width: false, outline_level: nil)
246
+ #: (Integer | String | Range[Integer | String] | Array[Integer | String] index, ?width: Float | Integer | nil, ?hidden: bool, ?custom_width: bool, ?outline_level: Integer | nil, ?style: (Integer | Symbol | String)?) -> void
247
+ def column(index, width: nil, hidden: false, custom_width: false, outline_level: nil, style: nil)
214
248
  raise ArgumentError, "Column width #{width} must be between 0 and 255 characters (Excel limitation)" if @strict_excel_mode && width && (width.negative? || width > 255)
215
249
 
216
250
  DslHelpers.normalize_column_indices(index).each do |idx|
251
+ unmapped = style ? { style_index: style } : {}
217
252
  @columns << Elements::Column.new(
218
253
  index: idx,
219
254
  width: width,
220
255
  hidden: hidden,
221
256
  custom_width: custom_width || !width.nil?,
222
- outline_level: outline_level
257
+ outline_level: outline_level,
258
+ style_index: style,
259
+ unmapped_data: unmapped
223
260
  )
224
261
  end
225
262
  end
@@ -250,8 +287,17 @@ module Xlsxrb
250
287
  # @param location [String, nil] Optional internal sheet location (e.g. "Sheet2!A1").
251
288
  # @return [void]
252
289
  # @api public
253
- #: (String | Integer cell, ?String? url, ?display: String?, ?tooltip: String?, ?location: String?) -> void
290
+ #: (String | Integer cell, ?(String | Elements::Hyperlink)? url, ?display: String?, ?tooltip: String?, ?location: String?) -> void
254
291
  def hyperlink(cell, url = nil, display: nil, tooltip: nil, location: nil)
292
+ if url.is_a?(Elements::Hyperlink)
293
+ link = { cell: cell }
294
+ link[:url] = url.url if url.url
295
+ link[:display] = url.display || display if url.display || display
296
+ link[:tooltip] = url.tooltip || tooltip if url.tooltip || tooltip
297
+ link[:location] = url.location || location if url.location || location
298
+ @hyperlinks << link
299
+ return
300
+ end
255
301
  link = { cell: cell }
256
302
  link[:url] = url if url
257
303
  link[:display] = display if display
@@ -295,12 +341,21 @@ module Xlsxrb
295
341
 
296
342
  # Adds a data validation rule to a cell or range.
297
343
  #
298
- # @param sqref [String] Target cell or range reference (e.g. "B2:B100").
344
+ # @param sqref [String, nil] Target cell or range reference (e.g. "B2:B100").
299
345
  # @param opts [Hash] Validation configuration (e.g. type: :list, formula1: '"Option1,Option2"').
346
+ # @yield [data_validation_builder] Optional configuration block.
300
347
  # @return [void]
301
348
  # @api public
302
- #: (String sqref, **untyped opts) -> void
303
- def validate_data(sqref, **opts)
349
+ #: (?String? sqref, **untyped opts) ?{ (DataValidationBuilder) -> void } -> void
350
+ def validate_data(sqref = nil, **opts)
351
+ if block_given?
352
+ builder = DataValidationBuilder.new(sqref, **opts)
353
+ yield builder
354
+ opts = builder.to_h
355
+ sqref = opts.delete(:sqref) || sqref
356
+ end
357
+ raise ArgumentError, "data validation sqref must be provided" unless sqref
358
+
304
359
  @data_validations << opts.merge(sqref: sqref)
305
360
  end
306
361
 
@@ -317,17 +372,32 @@ module Xlsxrb
317
372
 
318
373
  # Adds an Excel Table (ListObject) to the sheet.
319
374
  #
320
- # @param ref [String] The table range (e.g. "A1:D20").
321
- # @param columns [Array<String>] Column header names.
375
+ # @param ref [String, nil] The table range (e.g. "A1:D20").
376
+ # @param columns [Array<String>, nil] Column header names.
322
377
  # @param name [String, nil] Table name identifier.
323
378
  # @param display_name [String, nil] Table display name.
324
379
  # @param style [String, nil] Built-in table style name (e.g. "TableStyleMedium2").
325
380
  # @param opts [Hash] Additional table properties.
381
+ # @yield [table_builder] Optional configuration block.
326
382
  # @return [void]
327
383
  # @api public
328
- #: (String ref, columns: Array[String | Hash[Symbol, untyped]], ?name: String?, ?display_name: String?, ?style: String?, **untyped opts) -> void
329
- def table(ref, columns:, name: nil, display_name: nil, style: nil, **)
330
- @tables << DslHelpers.normalize_table_options(ref, columns: columns, name: name, display_name: display_name, style: style, **)
384
+ #: (?String? ref, ?columns: Array[String | Hash[Symbol, untyped]]?, ?name: String?, ?display_name: String?, ?style: String?, **untyped opts) ?{ (TableBuilder) -> void } -> void
385
+ def table(ref = nil, columns: nil, name: nil, display_name: nil, style: nil, **opts)
386
+ opts = opts.dup
387
+ if block_given?
388
+ builder = TableBuilder.new(ref, columns: columns, name: name, display_name: display_name, style: style, **opts)
389
+ yield builder
390
+ opts = builder.to_h
391
+ ref = opts.delete(:ref) || ref
392
+ columns = opts.delete(:columns) || columns
393
+ name = opts.delete(:name) || name
394
+ display_name = opts.delete(:display_name) || display_name
395
+ style = opts.delete(:style) || style
396
+ end
397
+ raise ArgumentError, "table ref must be provided" unless ref
398
+ raise ArgumentError, "table columns must be provided" if columns.nil?
399
+
400
+ @tables << DslHelpers.normalize_table_options(ref, columns: columns, name: name, display_name: display_name, style: style, **opts)
331
401
  end
332
402
 
333
403
  # Adds a Pivot Table to the sheet.
@@ -380,6 +450,38 @@ module Xlsxrb
380
450
  @sparkline_groups << group
381
451
  end
382
452
 
453
+ # Adds a single sparkline to the sheet.
454
+ #
455
+ # @example Add a line sparkline
456
+ # sheet.add_sparkline(location: "F1", range: "A1:E1")
457
+ #
458
+ # @example Add a column sparkline with high/low markers
459
+ # sheet.add_sparkline(location: "F2", range: "A2:E2", type: :column, high: true, low: true)
460
+ #
461
+ # @example Add a win/loss sparkline
462
+ # sheet.add_sparkline(location: "F3", range: "A3:E3", type: :win_loss)
463
+ #
464
+ # @param location [String, nil] Target cell for the sparkline (e.g. "F1").
465
+ # @param range [String, nil] Data range for the sparkline (e.g. "A1:E1").
466
+ # @param type [Symbol, String, nil] Sparkline type (:line, :column, :win_loss / :stacked).
467
+ # @param opts [Hash] Additional sparkline options.
468
+ # @return [void]
469
+ # @api public
470
+ #: (?location: String?, ?range: String?, ?type: (String | Symbol)?, **untyped opts) -> void
471
+ def add_sparkline(location: nil, range: nil, type: nil, **opts)
472
+ loc = location || opts.delete(:location_ref)
473
+ rng = range || opts.delete(:data_ref) || opts.delete(:data)
474
+ raise ArgumentError, "Missing required location: parameter for sparkline" unless loc
475
+ raise ArgumentError, "Missing required range: parameter for sparkline" unless rng
476
+
477
+ mapped_type = case type&.to_s
478
+ when "win_loss" then "stacked"
479
+ when nil, "" then nil
480
+ else type.to_s
481
+ end
482
+ sparkline_group(sparklines: [{ data_ref: rng.to_s, location_ref: loc.to_s }], type: mapped_type, **opts)
483
+ end
484
+
383
485
  # Merges a range of cells into a single cell.
384
486
  #
385
487
  # @example Merge with string range
@@ -492,6 +594,98 @@ module Xlsxrb
492
594
  @print_options[name] = value
493
595
  end
494
596
 
597
+ # Sets or gets the print area for the worksheet.
598
+ #
599
+ # @example Set print area range
600
+ # sheet.print_area("A1:H50")
601
+ #
602
+ # @example Get print area range
603
+ # sheet.print_area #=> "A1:H50"
604
+ #
605
+ # @param range [String, nil] Cell range (e.g. "A1:H50").
606
+ # @return [String, nil]
607
+ # @api public
608
+ #: (?String? range) -> String?
609
+ def print_area(range = nil)
610
+ return @print_area if range.nil?
611
+
612
+ @print_area = range
613
+ end
614
+
615
+ # Sets or gets repeating print titles (rows and/or columns) for pagination.
616
+ #
617
+ # @example Set repeating header rows
618
+ # sheet.print_titles(rows: "1:2")
619
+ #
620
+ # @example Get repeating print titles
621
+ # sheet.print_titles #=> { rows: "1:2", cols: nil }
622
+ #
623
+ # @param rows [String, nil] Repeating row range (e.g. "1:2").
624
+ # @param cols [String, nil] Repeating column range (e.g. "A:B").
625
+ # @return [Hash, nil]
626
+ # @api public
627
+ #: (?rows: String?, ?cols: String?) -> Hash[Symbol, untyped]?
628
+ def print_titles(rows: nil, cols: nil)
629
+ return @print_titles if rows.nil? && cols.nil?
630
+
631
+ @print_titles = { rows: rows, cols: cols }
632
+ end
633
+
634
+ # Sets repeating title rows to appear at the top of each printed page.
635
+ # Accepts 0-indexed row numbers (WriteXLSX / xlsxrb convention) or Excel range string ("1:2").
636
+ #
637
+ # @example Repeat first row
638
+ # sheet.repeat_rows(0)
639
+ #
640
+ # @example Repeat rows 1 through 3
641
+ # sheet.repeat_rows(0, 2)
642
+ #
643
+ # @param first [Integer, String] First row index (0-based) or range string (e.g. "1:2").
644
+ # @param last [Integer, String, nil] Optional last row index (0-based, defaults to first).
645
+ # @return [void]
646
+ # @api public
647
+ #: (Integer | String first, ?(Integer | String)? last) -> void
648
+ def repeat_rows(first, last = nil)
649
+ rows_str = if first.is_a?(String)
650
+ first.include?(":") ? first : "#{first}:#{last || first}"
651
+ else
652
+ last ||= first
653
+ "#{first + 1}:#{last + 1}"
654
+ end
655
+ @print_titles = (@print_titles || {}).merge(rows: rows_str)
656
+ end
657
+
658
+ # Sets repeating title columns to appear at the left of each printed page.
659
+ # Accepts 0-indexed column numbers (0 => "A"), column names ("A", "B"), or range string ("A:B").
660
+ #
661
+ # @example Repeat column A
662
+ # sheet.repeat_columns(0)
663
+ #
664
+ # @example Repeat columns A through C
665
+ # sheet.repeat_columns("A", "C")
666
+ #
667
+ # @param first [Integer, String] First column index (0-based) or column name/range.
668
+ # @param last [Integer, String, nil] Optional last column index/name (defaults to first).
669
+ # @return [void]
670
+ # @api public
671
+ #: (Integer | String first, ?(Integer | String)? last) -> void
672
+ def repeat_columns(first, last = nil)
673
+ cols_str = if first.is_a?(String)
674
+ first.include?(":") ? first : "#{first}:#{last || first}"
675
+ else
676
+ first_name = Utils.col_index_to_name(first)
677
+ last_name = if last.is_a?(Integer)
678
+ Utils.col_index_to_name(last)
679
+ elsif last
680
+ last.to_s
681
+ else
682
+ first_name
683
+ end
684
+ "#{first_name}:#{last_name}"
685
+ end
686
+ @print_titles = (@print_titles || {}).merge(cols: cols_str)
687
+ end
688
+
495
689
  # Sets sheet-level protection with optional password hashing.
496
690
  #
497
691
  # @param opts [Hash] Protection options (e.g. password: "secret", select_locked_cells: true).
@@ -561,6 +755,30 @@ module Xlsxrb
561
755
  @sheet_view[name] = value
562
756
  end
563
757
 
758
+ # Configures sheet format properties (e.g. default_row_height, default_col_width).
759
+ #
760
+ # @example Using keyword arguments
761
+ # sheet.sheet_format(default_row_height: 24, default_col_width: 15)
762
+ #
763
+ # @example Using property name and value
764
+ # sheet.sheet_format(:default_row_height, 24)
765
+ #
766
+ # @param name [Symbol, String, Hash, nil] Property name or hash of properties.
767
+ # @param value [Object, nil] Property value when name is provided.
768
+ # @param opts [Hash] Keyword arguments for sheet format properties.
769
+ # @return [Hash]
770
+ # @api public
771
+ #: (?Symbol | String | Hash[Symbol, untyped] | nil name, ?untyped value, **untyped opts) -> Hash[Symbol, untyped]
772
+ def sheet_format(name = nil, value = nil, **opts)
773
+ if name.is_a?(Hash)
774
+ @sheet_format.merge!(name)
775
+ elsif name && value
776
+ @sheet_format[name.to_sym] = value
777
+ end
778
+ @sheet_format.merge!(opts) unless opts.empty?
779
+ @sheet_format
780
+ end
781
+
564
782
  # Inserts a horizontal page break before the specified row.
565
783
  #
566
784
  # @param row_num [Integer] 1-based row number.
@@ -582,6 +800,34 @@ module Xlsxrb
582
800
  @col_breaks << col_index
583
801
  end
584
802
 
803
+ # Adds a protected range to the worksheet.
804
+ #
805
+ # @param name [String] Name of the protected range.
806
+ # @param sqref [String] Cell range (e.g. "A1:B10").
807
+ # @param password [String, nil] Optional password or hash.
808
+ # @param opts [Hash] Additional options.
809
+ # @return [void]
810
+ # @api public
811
+ #: (String name, String sqref, ?password: String?, **untyped opts) -> void
812
+ def protected_range(name, sqref, password: nil, **opts)
813
+ entry = { name: name.to_s, sqref: sqref.to_s }
814
+ entry[:password] = password if password
815
+ entry.merge!(opts) unless opts.empty?
816
+ @protected_ranges << DslHelpers.normalize_protection_options(entry)
817
+ end
818
+ alias add_protected_range protected_range
819
+
820
+ # Adds a cell watch to the worksheet.
821
+ #
822
+ # @param ref [String] Cell reference (e.g. "A1").
823
+ # @return [void]
824
+ # @api public
825
+ #: (String ref) -> void
826
+ def cell_watch(ref)
827
+ @cell_watches << ref.to_s
828
+ end
829
+ alias add_cell_watch cell_watch
830
+
585
831
  # Builds and returns the in-memory {Elements::Worksheet}.
586
832
  #
587
833
  # @return [Elements::Worksheet]
@@ -611,13 +857,77 @@ module Xlsxrb
611
857
  facade_meta[:images] = @images unless @images.empty?
612
858
  facade_meta[:shapes] = @shapes unless @shapes.empty?
613
859
  facade_meta[:sheet_properties] = @sheet_properties unless @sheet_properties.empty?
860
+ facade_meta[:sheet_format] = @sheet_format unless @sheet_format.empty?
614
861
  facade_meta[:sheet_view] = @sheet_view unless @sheet_view.empty?
615
862
  facade_meta[:row_breaks] = @row_breaks unless @row_breaks.empty?
616
863
  facade_meta[:col_breaks] = @col_breaks unless @col_breaks.empty?
864
+ facade_meta[:protected_ranges] = @protected_ranges unless @protected_ranges.empty?
865
+ facade_meta[:cell_watches] = @cell_watches unless @cell_watches.empty?
866
+ facade_meta[:print_area] = @print_area if @print_area
867
+ facade_meta[:print_titles] = @print_titles if @print_titles
868
+
869
+ hl_hash = {}
870
+ @hyperlinks.each do |h|
871
+ ref = h[:cell].to_s.upcase
872
+ entry = {}
873
+ entry[:url] = h[:url] if h[:url]
874
+ entry[:display] = h[:display] if h[:display]
875
+ entry[:tooltip] = h[:tooltip] if h[:tooltip]
876
+ entry[:location] = h[:location] if h[:location]
877
+ hl_hash[ref] = entry unless entry.empty?
878
+ end
879
+
880
+ cm_hash = {}
881
+ @comments.each do |c|
882
+ ref = (c[:ref] || c[:cell]).to_s.upcase
883
+ cm_hash[ref] = c
884
+ end
885
+
886
+ rows = if hl_hash.empty? && cm_hash.empty?
887
+ @rows
888
+ else
889
+ @rows.map do |row|
890
+ new_cells = row.cells.map do |cell|
891
+ hl = hl_hash[cell.ref]
892
+ cm = cm_hash[cell.ref]
893
+ if hl || cm
894
+ cell.with(hyperlink: hl || cell.hyperlink, comment: cm || cell.comment)
895
+ else
896
+ cell
897
+ end
898
+ end
899
+ row.with(cells: new_cells)
900
+ end
901
+ end
617
902
 
618
903
  Elements::Worksheet.new(
619
- name: @name, rows: @rows, columns: @columns, charts: @charts,
620
- unmapped_data: facade_meta.empty? ? {} : { facade: facade_meta }
904
+ name: @name, rows: rows, columns: @columns, charts: @charts,
905
+ conditional_formatting: @conditional_formats,
906
+ data_validations: @data_validations,
907
+ unmapped_data: facade_meta.empty? ? {} : { facade: facade_meta },
908
+ state: @state,
909
+ hyperlinks: hl_hash,
910
+ comments: @comments,
911
+ print_area: @print_area,
912
+ print_titles: @print_titles,
913
+ sparkline_groups: @sparkline_groups,
914
+ merged_cells: @merge_cells_ranges,
915
+ auto_filter: @auto_filter,
916
+ tables: @tables,
917
+ shapes: @shapes,
918
+ freeze_pane: @freeze_pane,
919
+ page_setup: @page_setup,
920
+ page_margins: @page_margins,
921
+ header_footer: @header_footer,
922
+ print_options: @print_options,
923
+ sheet_protection: @sheet_protection,
924
+ sheet_view: @sheet_view,
925
+ sheet_properties: @sheet_properties,
926
+ sheet_format: @sheet_format,
927
+ row_breaks: @row_breaks,
928
+ col_breaks: @col_breaks,
929
+ protected_ranges: @protected_ranges,
930
+ cell_watches: @cell_watches
621
931
  )
622
932
  end
623
933
 
@@ -626,4 +936,408 @@ module Xlsxrb
626
936
  #: Hash[String, StyleBuilder]
627
937
  attr_reader :styles
628
938
  end
939
+
940
+ # Builder context for configuring an Excel Table (ListObject) in block form.
941
+ #
942
+ # @api public
943
+ class TableBuilder
944
+ # @param ref [String, nil] Table range (e.g. "A1:D10").
945
+ # @param columns [Array, nil] Column header names or definition hashes.
946
+ # @param name [String, nil] Table name identifier.
947
+ # @param display_name [String, nil] Table display name.
948
+ # @param style [String, nil] Built-in table style name (e.g. "TableStyleMedium2").
949
+ # @param opts [Hash] Additional table properties.
950
+ #: (?String? ref, ?columns: untyped, ?name: String?, ?display_name: String?, ?style: String?, **untyped opts) -> void
951
+ def initialize(ref = nil, columns: nil, name: nil, display_name: nil, style: nil, **opts)
952
+ @options = opts.dup
953
+ @options[:ref] = ref if ref
954
+ @options[:columns] = columns.is_a?(Array) ? columns.dup : [columns] if columns
955
+ @options[:name] = name if name
956
+ @options[:display_name] = display_name if display_name
957
+ @options[:style] = style if style
958
+ end
959
+
960
+ #: (untyped key) -> untyped
961
+ def [](key)
962
+ @options[key.to_sym]
963
+ end
964
+
965
+ #: (untyped key, untyped value) -> untyped
966
+ def []=(key, value)
967
+ @options[key.to_sym] = value
968
+ end
969
+
970
+ #: (?String? val) -> String?
971
+ def ref(val = nil)
972
+ if val
973
+ @options[:ref] = val.to_s
974
+ else
975
+ @options[:ref]
976
+ end
977
+ end
978
+
979
+ #: (String val) -> String
980
+ def ref=(val)
981
+ @options[:ref] = val.to_s
982
+ end
983
+
984
+ #: (*untyped cols) -> untyped
985
+ def columns(*cols)
986
+ if cols.empty?
987
+ @options[:columns]
988
+ elsif cols.size == 1 && cols.first.is_a?(Array)
989
+ @options[:columns] = cols.first
990
+ else
991
+ @options[:columns] = cols
992
+ end
993
+ end
994
+
995
+ #: (untyped cols) -> untyped
996
+ def columns=(cols)
997
+ @options[:columns] = cols.is_a?(Array) ? cols : [cols]
998
+ end
999
+
1000
+ # Appends a single column name or column definition.
1001
+ #
1002
+ # @param col [String, Hash] Column header name or definition.
1003
+ # @return [void]
1004
+ #: (String | Hash[Symbol, untyped] col) -> void
1005
+ def column(col)
1006
+ (@options[:columns] ||= []) << col
1007
+ end
1008
+ alias add_column column
1009
+
1010
+ #: (?String? val) -> String?
1011
+ def name(val = nil)
1012
+ if val
1013
+ @options[:name] = val.to_s
1014
+ else
1015
+ @options[:name]
1016
+ end
1017
+ end
1018
+
1019
+ #: (String val) -> String
1020
+ def name=(val)
1021
+ @options[:name] = val.to_s
1022
+ end
1023
+
1024
+ #: (?String? val) -> String?
1025
+ def display_name(val = nil)
1026
+ if val
1027
+ @options[:display_name] = val.to_s
1028
+ else
1029
+ @options[:display_name]
1030
+ end
1031
+ end
1032
+
1033
+ #: (String val) -> String
1034
+ def display_name=(val)
1035
+ @options[:display_name] = val.to_s
1036
+ end
1037
+
1038
+ #: (?String? val) -> String?
1039
+ def style(val = nil)
1040
+ if val
1041
+ @options[:style] = val.to_s
1042
+ else
1043
+ @options[:style]
1044
+ end
1045
+ end
1046
+
1047
+ #: (String val) -> String
1048
+ def style=(val)
1049
+ @options[:style] = val.to_s
1050
+ end
1051
+
1052
+ #: (?bool? val) -> bool?
1053
+ def show_row_stripes(val = nil)
1054
+ if val.nil?
1055
+ @options[:show_row_stripes]
1056
+ else
1057
+ @options[:show_row_stripes] = val ? true : false
1058
+ end
1059
+ end
1060
+
1061
+ #: (bool val) -> bool
1062
+ def show_row_stripes=(val)
1063
+ @options[:show_row_stripes] = val ? true : false
1064
+ end
1065
+
1066
+ #: (?bool? val) -> bool?
1067
+ def show_column_stripes(val = nil)
1068
+ if val.nil?
1069
+ @options[:show_column_stripes]
1070
+ else
1071
+ @options[:show_column_stripes] = val ? true : false
1072
+ end
1073
+ end
1074
+
1075
+ #: (bool val) -> bool
1076
+ def show_column_stripes=(val)
1077
+ @options[:show_column_stripes] = val ? true : false
1078
+ end
1079
+
1080
+ #: (?bool? val) -> bool?
1081
+ def show_first_column(val = nil)
1082
+ if val.nil?
1083
+ @options[:show_first_column]
1084
+ else
1085
+ @options[:show_first_column] = val ? true : false
1086
+ end
1087
+ end
1088
+
1089
+ #: (bool val) -> bool
1090
+ def show_first_column=(val)
1091
+ @options[:show_first_column] = val ? true : false
1092
+ end
1093
+
1094
+ #: (?bool? val) -> bool?
1095
+ def show_last_column(val = nil)
1096
+ if val.nil?
1097
+ @options[:show_last_column]
1098
+ else
1099
+ @options[:show_last_column] = val ? true : false
1100
+ end
1101
+ end
1102
+
1103
+ #: (bool val) -> bool
1104
+ def show_last_column=(val)
1105
+ @options[:show_last_column] = val ? true : false
1106
+ end
1107
+
1108
+ #: (?Integer? val) -> Integer?
1109
+ def total_row_count(val = nil)
1110
+ if val.nil?
1111
+ @options[:total_row_count]
1112
+ else
1113
+ @options[:total_row_count] = val.to_i
1114
+ end
1115
+ end
1116
+
1117
+ #: (Integer val) -> Integer
1118
+ def total_row_count=(val)
1119
+ @options[:total_row_count] = val.to_i
1120
+ end
1121
+
1122
+ #: () -> Hash[Symbol, untyped]
1123
+ def to_h
1124
+ @options.dup
1125
+ end
1126
+ end
1127
+
1128
+ # Builder context for configuring data validation rules in block form.
1129
+ #
1130
+ # @api public
1131
+ class DataValidationBuilder
1132
+ # @param sqref [String, nil] Target cell or range reference.
1133
+ # @param opts [Hash] Additional validation properties.
1134
+ #: (?String? sqref, **untyped opts) -> void
1135
+ def initialize(sqref = nil, **opts)
1136
+ @options = opts.dup
1137
+ @options[:sqref] = sqref if sqref
1138
+ end
1139
+
1140
+ #: (untyped key) -> untyped
1141
+ def [](key)
1142
+ @options[key.to_sym]
1143
+ end
1144
+
1145
+ #: (untyped key, untyped value) -> untyped
1146
+ def []=(key, value)
1147
+ @options[key.to_sym] = value
1148
+ end
1149
+
1150
+ #: (?String? val) -> String?
1151
+ def sqref(val = nil)
1152
+ if val
1153
+ @options[:sqref] = val.to_s
1154
+ else
1155
+ @options[:sqref]
1156
+ end
1157
+ end
1158
+
1159
+ #: (String val) -> String
1160
+ def sqref=(val)
1161
+ @options[:sqref] = val.to_s
1162
+ end
1163
+ alias range sqref
1164
+ alias range= sqref=
1165
+
1166
+ #: (?untyped val) -> untyped
1167
+ def type(val = nil)
1168
+ if val
1169
+ @options[:type] = val
1170
+ else
1171
+ @options[:type]
1172
+ end
1173
+ end
1174
+
1175
+ #: (untyped val) -> untyped
1176
+ def type=(val)
1177
+ @options[:type] = val
1178
+ end
1179
+
1180
+ #: (?untyped val) -> untyped
1181
+ def operator(val = nil)
1182
+ if val
1183
+ @options[:operator] = val
1184
+ else
1185
+ @options[:operator]
1186
+ end
1187
+ end
1188
+
1189
+ #: (untyped val) -> untyped
1190
+ def operator=(val)
1191
+ @options[:operator] = val
1192
+ end
1193
+
1194
+ #: (?untyped val) -> untyped
1195
+ def formula1(val = nil)
1196
+ if val
1197
+ @options[:formula1] = val
1198
+ else
1199
+ @options[:formula1]
1200
+ end
1201
+ end
1202
+
1203
+ #: (untyped val) -> untyped
1204
+ def formula1=(val)
1205
+ @options[:formula1] = val
1206
+ end
1207
+
1208
+ #: (?untyped val) -> untyped
1209
+ def formula2(val = nil)
1210
+ if val
1211
+ @options[:formula2] = val
1212
+ else
1213
+ @options[:formula2]
1214
+ end
1215
+ end
1216
+
1217
+ #: (untyped val) -> untyped
1218
+ def formula2=(val)
1219
+ @options[:formula2] = val
1220
+ end
1221
+
1222
+ #: (?bool? val) -> bool?
1223
+ def allow_blank(val = nil)
1224
+ if val.nil?
1225
+ @options[:allow_blank]
1226
+ else
1227
+ @options[:allow_blank] = val ? true : false
1228
+ end
1229
+ end
1230
+
1231
+ #: (bool val) -> bool
1232
+ def allow_blank=(val)
1233
+ @options[:allow_blank] = val ? true : false
1234
+ end
1235
+
1236
+ #: (?bool? val) -> bool?
1237
+ def show_input_message(val = nil)
1238
+ if val.nil?
1239
+ @options[:show_input_message]
1240
+ else
1241
+ @options[:show_input_message] = val ? true : false
1242
+ end
1243
+ end
1244
+
1245
+ #: (bool val) -> bool
1246
+ def show_input_message=(val)
1247
+ @options[:show_input_message] = val ? true : false
1248
+ end
1249
+
1250
+ #: (?bool? val) -> bool?
1251
+ def show_error_message(val = nil)
1252
+ if val.nil?
1253
+ @options[:show_error_message]
1254
+ else
1255
+ @options[:show_error_message] = val ? true : false
1256
+ end
1257
+ end
1258
+
1259
+ #: (bool val) -> bool
1260
+ def show_error_message=(val)
1261
+ @options[:show_error_message] = val ? true : false
1262
+ end
1263
+
1264
+ #: (?String? val) -> String?
1265
+ def error_title(val = nil)
1266
+ if val
1267
+ @options[:error_title] = val.to_s
1268
+ else
1269
+ @options[:error_title]
1270
+ end
1271
+ end
1272
+
1273
+ #: (String val) -> String
1274
+ def error_title=(val)
1275
+ @options[:error_title] = val.to_s
1276
+ end
1277
+
1278
+ #: (?String? val) -> String?
1279
+ def error_message(val = nil)
1280
+ if val
1281
+ @options[:error] = val.to_s
1282
+ else
1283
+ @options[:error]
1284
+ end
1285
+ end
1286
+
1287
+ #: (String val) -> String
1288
+ def error_message=(val)
1289
+ @options[:error] = val.to_s
1290
+ end
1291
+ alias error error_message
1292
+ alias error= error_message=
1293
+
1294
+ #: (?String? val) -> String?
1295
+ def prompt_title(val = nil)
1296
+ if val
1297
+ @options[:prompt_title] = val.to_s
1298
+ else
1299
+ @options[:prompt_title]
1300
+ end
1301
+ end
1302
+
1303
+ #: (String val) -> String
1304
+ def prompt_title=(val)
1305
+ @options[:prompt_title] = val.to_s
1306
+ end
1307
+
1308
+ #: (?String? val) -> String?
1309
+ def prompt_message(val = nil)
1310
+ if val
1311
+ @options[:prompt] = val.to_s
1312
+ else
1313
+ @options[:prompt]
1314
+ end
1315
+ end
1316
+
1317
+ #: (String val) -> String
1318
+ def prompt_message=(val)
1319
+ @options[:prompt] = val.to_s
1320
+ end
1321
+ alias prompt prompt_message
1322
+ alias prompt= prompt_message=
1323
+
1324
+ #: (?untyped val) -> untyped
1325
+ def error_style(val = nil)
1326
+ if val
1327
+ @options[:error_style] = val
1328
+ else
1329
+ @options[:error_style]
1330
+ end
1331
+ end
1332
+
1333
+ #: (untyped val) -> untyped
1334
+ def error_style=(val)
1335
+ @options[:error_style] = val
1336
+ end
1337
+
1338
+ #: () -> Hash[Symbol, untyped]
1339
+ def to_h
1340
+ @options.dup
1341
+ end
1342
+ end
629
1343
  end