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
@@ -6,6 +6,7 @@ require "tempfile"
6
6
  require_relative "style_builder"
7
7
  require_relative "chart_builder"
8
8
  require_relative "dsl_helpers"
9
+ require_relative "worksheet_builder"
9
10
  require_relative "ooxml/writer"
10
11
  require_relative "ooxml/worksheet_writer"
11
12
  require_relative "ooxml/workbook_writer"
@@ -70,9 +71,12 @@ module Xlsxrb
70
71
  @current_images = []
71
72
  @current_shapes = []
72
73
  @current_sheet_properties = {}
74
+ @current_sheet_format = {}
73
75
  @current_sheet_view = {}
74
76
  @current_row_breaks = []
75
77
  @current_col_breaks = []
78
+ @current_protected_ranges = []
79
+ @current_cell_watches = []
76
80
  @current_cells = {}
77
81
  @styles = {} # { style_name => StyleBuilder }
78
82
  @style_writer = Ooxml::Writer.new
@@ -220,15 +224,30 @@ module Xlsxrb
220
224
  # @param hidden [Boolean] Whether the row is hidden.
221
225
  # @param custom_height [Boolean] Whether to flag as custom height.
222
226
  # @param outline_level [Integer, nil] Grouping/outline level (0 - 7).
227
+ # @param collapsed [Boolean] Whether the row is collapsed.
223
228
  # @return [void]
224
229
  # @api public
225
- #: (Array[untyped] | Hash[untyped, untyped] values, ?styles: untyped, ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?outline_level: Integer | nil) -> void
226
- def row(values, styles: nil, height: nil, hidden: false, custom_height: false, outline_level: nil)
230
+ #: (Array[untyped] | Hash[untyped, untyped] values, ?styles: untyped, ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?collapsed: bool, ?outline_level: Integer | nil) -> void
231
+ def row(values, styles: nil, height: nil, hidden: false, custom_height: false, collapsed: false, outline_level: nil)
227
232
  raise Error, "Sheet '#{@sheet_name}' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
228
233
 
229
- @writer.row(values, styles: styles, height: height, hidden: hidden, custom_height: custom_height, outline_level: outline_level)
234
+ @writer.row(values, styles: styles, height: height, hidden: hidden, custom_height: custom_height, collapsed: collapsed, outline_level: outline_level)
235
+ end
236
+ alias add_row row
237
+
238
+ # Appends a row to the worksheet and returns self for chaining.
239
+ #
240
+ # @example
241
+ # sheet << ["A", 1] << ["B", 2]
242
+ #
243
+ # @param values [Array, Hash] Row cell values.
244
+ # @return [self]
245
+ # @api public
246
+ #: (Array[untyped] | Hash[untyped, untyped] values) -> self
247
+ def <<(values)
248
+ row(values)
249
+ self
230
250
  end
231
- alias << row
232
251
 
233
252
  # Configures column width and properties.
234
253
  #
@@ -287,7 +306,7 @@ module Xlsxrb
287
306
  # @param location [String, nil] Destination location / URL (keyword alternative).
288
307
  # @return [void]
289
308
  # @api public
290
- #: (String cell, ?String? url, ?display: String?, ?tooltip: String?, ?location: String?) -> void
309
+ #: (String | Integer cell, ?(String | Elements::Hyperlink)? url, ?display: String?, ?tooltip: String?, ?location: String?) -> void
291
310
  def hyperlink(...)
292
311
  raise Error, "Sheet '#{@sheet_name}' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
293
312
 
@@ -344,15 +363,24 @@ module Xlsxrb
344
363
 
345
364
  # Adds data validation rules to a range.
346
365
  #
366
+ # Adds data validation to a cell or range.
367
+ #
347
368
  # @example Dropdown list validation
348
369
  # s.validate_data("B2:B100", type: "list", formula1: '"High,Medium,Low"')
349
370
  #
350
- # @param range [String] The cell range (e.g. "B2:B10").
351
- # @param type [String, Symbol] Validation type ("list", "whole", "decimal", "date", "time", "textLength", "custom").
371
+ # @example Block syntax
372
+ # s.validate_data("B2:B100") do |dv|
373
+ # dv.type :list
374
+ # dv.formula1 '"High,Medium,Low"'
375
+ # end
376
+ #
377
+ # @param range [String, nil] The cell range (e.g. "B2:B10").
378
+ # @param type [String, Symbol, nil] Validation type ("list", "whole", "decimal", "date", "time", "textLength", "custom").
352
379
  # @param opts [Hash] Validation options.
380
+ # @yield [data_validation_builder] Optional configuration block.
353
381
  # @return [void]
354
382
  # @api public
355
- #: (String range, ?type: String | Symbol, **untyped opts) -> void
383
+ #: (?String? range, ?type: (String | Symbol)?, **untyped opts) ?{ (DataValidationBuilder) -> void } -> void
356
384
  def validate_data(...)
357
385
  raise Error, "Sheet '#{@sheet_name}' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
358
386
 
@@ -378,18 +406,25 @@ module Xlsxrb
378
406
 
379
407
  # Adds a formatted Excel Table to the sheet.
380
408
  #
381
- # @example
409
+ # @example Standard keyword arguments
382
410
  # s.table("A1:C10", columns: ["ID", "Name", "Total"], name: "SalesTable", style: "TableStyleMedium9")
383
411
  #
384
- # @param ref [String] The cell range for the table (e.g. "A1:D10").
385
- # @param columns [Array<String>, Array<Hash>] Column names or definitions.
412
+ # @example Block syntax
413
+ # s.table("A1:C10") do |t|
414
+ # t.columns ["ID", "Name", "Total"]
415
+ # t.style "TableStyleMedium9"
416
+ # end
417
+ #
418
+ # @param ref [String, nil] The cell range for the table (e.g. "A1:D10").
419
+ # @param columns [Array<String>, Array<Hash>, nil] Column names or definitions.
386
420
  # @param name [String, nil] Table name.
387
421
  # @param display_name [String, nil] Display name.
388
422
  # @param style [String, nil] Table style name.
389
423
  # @param opts [Hash] Additional options.
424
+ # @yield [table_builder] Optional configuration block.
390
425
  # @return [void]
391
426
  # @api public
392
- #: (String ref, columns: untyped, ?name: String?, ?display_name: String?, ?style: String?, **untyped opts) -> void
427
+ #: (?String? ref, ?columns: untyped, ?name: String?, ?display_name: String?, ?style: String?, **untyped opts) ?{ (TableBuilder) -> void } -> void
393
428
  def table(...)
394
429
  raise Error, "Sheet '#{@sheet_name}' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
395
430
 
@@ -460,6 +495,30 @@ module Xlsxrb
460
495
  @writer.sparkline_group(...)
461
496
  end
462
497
 
498
+ # Adds a single sparkline to the sheet.
499
+ #
500
+ # @example Add a line sparkline
501
+ # s.add_sparkline(location: "F1", range: "A1:E1")
502
+ #
503
+ # @example Add a column sparkline with high/low markers
504
+ # s.add_sparkline(location: "F2", range: "A2:E2", type: :column, high: true, low: true)
505
+ #
506
+ # @example Add a win/loss sparkline
507
+ # s.add_sparkline(location: "F3", range: "A3:E3", type: :win_loss)
508
+ #
509
+ # @param location [String, nil] Target cell for the sparkline (e.g. "F1").
510
+ # @param range [String, nil] Data range for the sparkline (e.g. "A1:E1").
511
+ # @param type [Symbol, String, nil] Sparkline type (:line, :column, :win_loss / :stacked).
512
+ # @param opts [Hash] Additional sparkline options.
513
+ # @return [void]
514
+ # @api public
515
+ #: (?location: String?, ?range: String?, ?type: (String | Symbol)?, **untyped opts) -> void
516
+ def add_sparkline(...)
517
+ raise Error, "Sheet '#{@sheet_name}' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
518
+
519
+ @writer.add_sparkline(...)
520
+ end
521
+
463
522
  # Sets workbook-level properties.
464
523
  #
465
524
  # @param opts [Hash] Workbook property options.
@@ -554,6 +613,32 @@ module Xlsxrb
554
613
 
555
614
  @writer.print_titles(...)
556
615
  end
616
+
617
+ # Sets repeating title rows to appear at the top of each printed page.
618
+ #
619
+ # @param first [Integer, String] First row index (0-based) or range string (e.g. "1:2").
620
+ # @param last [Integer, String, nil] Optional last row index (0-based).
621
+ # @return [void]
622
+ # @api public
623
+ #: (Integer | String first, ?(Integer | String)? last) -> void
624
+ def repeat_rows(first, last = nil)
625
+ raise Error, "Sheet '#{@sheet_name}' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
626
+
627
+ @writer.repeat_rows(first, last, sheet: @sheet_name)
628
+ end
629
+
630
+ # Sets repeating title columns to appear at the left of each printed page.
631
+ #
632
+ # @param first [Integer, String] First column index (0-based) or column name/range.
633
+ # @param last [Integer, String, nil] Optional last column index/name.
634
+ # @return [void]
635
+ # @api public
636
+ #: (Integer | String first, ?(Integer | String)? last) -> void
637
+ def repeat_columns(first, last = nil)
638
+ raise Error, "Sheet '#{@sheet_name}' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
639
+
640
+ @writer.repeat_columns(first, last, sheet: @sheet_name)
641
+ end
557
642
  # simplecov:enable
558
643
 
559
644
  # Splits sheet view into panes.
@@ -765,6 +850,20 @@ module Xlsxrb
765
850
  @writer.sheet_view(...)
766
851
  end
767
852
 
853
+ # Configures sheet format properties (e.g. default_row_height, default_col_width).
854
+ #
855
+ # @param name [Symbol, String, Hash, nil] Property name or hash of properties.
856
+ # @param value [Object, nil] Property value when name is provided.
857
+ # @param opts [Hash] Keyword arguments for sheet format properties.
858
+ # @return [Hash[Symbol, untyped]]
859
+ # @api public
860
+ #: (?Symbol | String | Hash[Symbol, untyped] | nil name, ?untyped value, **untyped opts) -> Hash[Symbol, untyped]
861
+ def sheet_format(...)
862
+ raise Error, "Sheet '#{@sheet_name}' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
863
+
864
+ @writer.sheet_format(...)
865
+ end
866
+
768
867
  # simplecov:disable
769
868
  # Edge case / untested delegation block
770
869
  # Adds a horizontal page break after the given row index.
@@ -794,6 +893,35 @@ module Xlsxrb
794
893
  @writer.page_break_col(...)
795
894
  end
796
895
  # simplecov:enable
896
+
897
+ # Adds a protected range to the sheet.
898
+ #
899
+ # @param name [String] Name of the protected range.
900
+ # @param sqref [String] Cell range (e.g. "A1:B10").
901
+ # @param password [String, nil] Optional password or hash.
902
+ # @param opts [Hash] Additional options.
903
+ # @return [void]
904
+ # @api public
905
+ #: (String name, String sqref, ?password: String?, **untyped opts) -> void
906
+ def protected_range(...)
907
+ raise Error, "Sheet '#{@sheet_name}' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
908
+
909
+ @writer.protected_range(...)
910
+ end
911
+ alias add_protected_range protected_range
912
+
913
+ # Adds a cell watch to the sheet.
914
+ #
915
+ # @param ref [String] Cell reference (e.g. "A1").
916
+ # @return [void]
917
+ # @api public
918
+ #: (String ref) -> void
919
+ def cell_watch(...)
920
+ raise Error, "Sheet '#{@sheet_name}' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
921
+
922
+ @writer.cell_watch(...)
923
+ end
924
+ alias add_cell_watch cell_watch
797
925
  end
798
926
 
799
927
  # Adds a new sheet to the workbook and starts streaming rows into it.
@@ -829,7 +957,8 @@ module Xlsxrb
829
957
  @sheet_entry_started = false
830
958
  @current_row_buffer = String.new(capacity: 65_536)
831
959
  @current_sheet_io = StringIO.new(@current_row_buffer)
832
- @current_row_writer = Ooxml::WorksheetWriter.new(@current_sheet_io)
960
+ date1904 = @workbook_properties&.[](:date1904) ? true : false
961
+ @current_row_writer = Ooxml::WorksheetWriter.new(@current_sheet_io, date1904: date1904)
833
962
  @current_row_writer.instance_variable_set(:@started, true)
834
963
 
835
964
  @current_columns = []
@@ -856,9 +985,12 @@ module Xlsxrb
856
985
  @current_images = []
857
986
  @current_shapes = []
858
987
  @current_sheet_properties = {}
988
+ @current_sheet_format = {}
859
989
  @current_sheet_view = {}
860
990
  @current_row_breaks = []
861
991
  @current_col_breaks = []
992
+ @current_protected_ranges = []
993
+ @current_cell_watches = []
862
994
  @current_cells = {}
863
995
 
864
996
  return unless block_given?
@@ -883,14 +1015,26 @@ module Xlsxrb
883
1015
  row_buf.clear
884
1016
  end
885
1017
 
886
- @current_row_writer = Ooxml::WorksheetWriter.new(zip_io)
1018
+ if @current_columns && !@current_columns.empty? && @style_name_to_id
1019
+ @current_columns.each do |col|
1020
+ s = col[:style] || col[:style_index]
1021
+ if s.is_a?(Symbol) || s.is_a?(String)
1022
+ resolved = @style_name_to_id[s] || @style_name_to_id[s.to_s]
1023
+ col[:style] = resolved if resolved
1024
+ end
1025
+ end
1026
+ end
1027
+
1028
+ date1904 = @workbook_properties&.[](:date1904) ? true : false
1029
+ @current_row_writer = Ooxml::WorksheetWriter.new(zip_io, date1904: date1904)
887
1030
  @current_row_writer.start(
888
1031
  columns: @current_columns,
889
1032
  sheet_properties: @current_sheet_properties.empty? ? nil : @current_sheet_properties,
890
1033
  freeze_pane: @current_freeze_pane,
891
1034
  split_pane: @current_split_pane,
892
1035
  selection: @current_selection,
893
- sheet_view: @current_sheet_view.empty? ? nil : @current_sheet_view
1036
+ sheet_view: @current_sheet_view.empty? ? nil : @current_sheet_view,
1037
+ sheet_format: @current_sheet_format.empty? ? nil : @current_sheet_format
894
1038
  )
895
1039
  @zip.write_data(@current_row_buffer) unless @current_row_buffer.empty?
896
1040
  @current_row_buffer.clear
@@ -906,8 +1050,8 @@ module Xlsxrb
906
1050
  # @param outline_level [Integer, nil] Grouping/outline level (0 - 7).
907
1051
  # @return [void]
908
1052
  # @api public
909
- #: (Array[untyped] | Hash[untyped, untyped] values, ?styles: untyped, ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?outline_level: Integer | nil) -> void
910
- def row(values, styles: nil, height: nil, hidden: false, custom_height: false, outline_level: nil)
1053
+ #: (Array[untyped] | Hash[untyped, untyped] values, ?styles: untyped, ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?collapsed: bool, ?outline_level: Integer | nil) -> void
1054
+ def row(values, styles: nil, height: nil, hidden: false, custom_height: false, collapsed: false, outline_level: nil)
911
1055
  sheet if @current_sheet.nil?
912
1056
 
913
1057
  row_index = @current_row_index
@@ -931,36 +1075,60 @@ module Xlsxrb
931
1075
  raise ArgumentError, "Row contains #{values.length} columns, exceeding Excel limit of 16_384 columns" if @strict_excel_mode && values.length > 16_384
932
1076
 
933
1077
  attrs = nil
934
- if height || hidden || outline_level
1078
+ if height || hidden || outline_level || collapsed
935
1079
  attrs = {}
936
1080
  attrs[:height] = height if height
937
1081
  attrs[:hidden] = true if hidden
938
1082
  attrs[:custom_height] = custom_height || !height.nil?
1083
+ attrs[:collapsed] = true if collapsed
939
1084
  attrs[:outline_level] = outline_level if outline_level
940
1085
  end
941
1086
 
942
1087
  @current_row_writer.write_row_values(row_index, values, styles: styles, style_map: @style_name_to_id, sst: @sst, sst_index: @sst_index, attrs: attrs)
943
1088
  start_sheet_entry if !@sheet_entry_started && @current_row_buffer.bytesize >= 65_536
944
1089
  end
945
- alias << row
1090
+ alias add_row row
1091
+
1092
+ # Appends a row to the worksheet and returns self for chaining.
1093
+ #
1094
+ # @example
1095
+ # writer << ["A", 1] << ["B", 2]
1096
+ #
1097
+ # @param values [Array, Hash] Row cell values.
1098
+ # @return [self]
1099
+ # @api public
1100
+ #: (Array[untyped] | Hash[untyped, untyped] values) -> self
1101
+ def <<(values)
1102
+ row(values)
1103
+ self
1104
+ end
946
1105
 
947
1106
  # Sets column formatting and properties for one or multiple columns.
948
1107
  #
949
1108
  # @param index [Integer, String, Range, Array] Column index (0-based) or letter ("A".."D").
950
1109
  # @param width [Float, Integer, nil] Column width in character units (0 - 255).
951
1110
  # @param hidden [Boolean] Whether the column is hidden.
952
- # @param custom_width [Boolean] Whether custom width is set.
953
- # @param outline_level [Integer, nil] Grouping/outline level.
1111
+ # @param style [Integer, Symbol, String, nil] Column style format or index.
954
1112
  # @return [void]
955
1113
  # @api public
956
- #: (Integer | String | Range[Integer | String] | Array[Integer | String] index, ?width: Float | Integer | nil, ?hidden: bool, ?custom_width: bool, ?outline_level: Integer | nil) -> void
957
- def column(index, width: nil, hidden: false, custom_width: false, outline_level: nil)
1114
+ #: (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
1115
+ def column(index, width: nil, hidden: false, custom_width: false, outline_level: nil, style: nil)
958
1116
  raise ArgumentError, "Column width #{width} must be between 0 and 255 characters (Excel limitation)" if @strict_excel_mode && width && (width.negative? || width > 255)
959
1117
 
960
1118
  sheet if @current_sheet.nil?
961
1119
 
1120
+ style_id = if @style_name_to_id && style
1121
+ @style_name_to_id[style] || @style_name_to_id[style.to_s] || style
1122
+ else
1123
+ style
1124
+ end
962
1125
  DslHelpers.normalize_column_indices(index).each do |idx|
963
- @current_columns << { index: idx, width: width, hidden: hidden, custom_width: custom_width || !width.nil?, outline_level: outline_level }
1126
+ entry = { index: idx, width: width, hidden: hidden, custom_width: custom_width || !width.nil?, outline_level: outline_level }
1127
+ if style_id
1128
+ entry[:style] = style_id
1129
+ @current_row_writer&.set_column_style(idx, style_id)
1130
+ end
1131
+ @current_columns << entry
964
1132
  end
965
1133
  end
966
1134
 
@@ -993,9 +1161,18 @@ module Xlsxrb
993
1161
  # @param location [String, nil] Internal location.
994
1162
  # @return [void]
995
1163
  # @api public
996
- #: (String | Integer cell, ?String? url, ?display: String?, ?tooltip: String?, ?location: String?) -> void
1164
+ #: (String | Integer cell, ?(String | Elements::Hyperlink)? url, ?display: String?, ?tooltip: String?, ?location: String?) -> void
997
1165
  def hyperlink(cell, url = nil, display: nil, tooltip: nil, location: nil)
998
1166
  sheet if @current_sheet.nil?
1167
+ if url.is_a?(Elements::Hyperlink)
1168
+ link = { cell: cell }
1169
+ link[:url] = url.url if url.url
1170
+ link[:display] = url.display || display if url.display || display
1171
+ link[:tooltip] = url.tooltip || tooltip if url.tooltip || tooltip
1172
+ link[:location] = url.location || location if url.location || location
1173
+ @current_hyperlinks << link
1174
+ return
1175
+ end
999
1176
  link = { cell: cell }
1000
1177
  link[:url] = url if url
1001
1178
  link[:display] = display if display
@@ -1042,13 +1219,22 @@ module Xlsxrb
1042
1219
 
1043
1220
  # Adds a data validation rule.
1044
1221
  #
1045
- # @param sqref [String] Cell range.
1222
+ # @param sqref [String, nil] Cell range.
1046
1223
  # @param opts [Hash] Validation options.
1224
+ # @yield [data_validation_builder] Optional configuration block.
1047
1225
  # @return [void]
1048
1226
  # @api public
1049
- #: (untyped sqref, **untyped opts) -> void
1050
- def validate_data(sqref, **opts)
1227
+ #: (?String? sqref, **untyped opts) ?{ (DataValidationBuilder) -> void } -> void
1228
+ def validate_data(sqref = nil, **opts)
1051
1229
  sheet if @current_sheet.nil?
1230
+ if block_given?
1231
+ builder = DataValidationBuilder.new(sqref, **opts)
1232
+ yield builder
1233
+ opts = builder.to_h
1234
+ sqref = opts.delete(:sqref) || sqref
1235
+ end
1236
+ raise ArgumentError, "data validation sqref must be provided" unless sqref
1237
+
1052
1238
  @current_data_validations << opts.merge(sqref: sqref)
1053
1239
  end
1054
1240
 
@@ -1066,18 +1252,33 @@ module Xlsxrb
1066
1252
 
1067
1253
  # Adds an Excel Table (ListObject).
1068
1254
  #
1069
- # @param ref [String] Table range.
1070
- # @param columns [Array<String>] Column names.
1255
+ # @param ref [String, nil] Table range.
1256
+ # @param columns [Array<String>, nil] Column names.
1071
1257
  # @param name [String, nil] Table name.
1072
1258
  # @param display_name [String, nil] Display name.
1073
1259
  # @param style [String, nil] Table style.
1074
1260
  # @param opts [Hash] Additional options.
1261
+ # @yield [table_builder] Optional configuration block.
1075
1262
  # @return [void]
1076
1263
  # @api public
1077
- #: (untyped ref, columns: untyped, ?name: untyped, ?display_name: untyped, ?style: untyped, **untyped opts) -> void
1078
- def table(ref, columns:, name: nil, display_name: nil, style: nil, **)
1264
+ #: (?String? ref, ?columns: untyped, ?name: untyped, ?display_name: untyped, ?style: untyped, **untyped opts) ?{ (TableBuilder) -> void } -> void
1265
+ def table(ref = nil, columns: nil, name: nil, display_name: nil, style: nil, **opts)
1079
1266
  sheet if @current_sheet.nil?
1080
- @current_tables << DslHelpers.normalize_table_options(ref, columns: columns, name: name, display_name: display_name, style: style, **)
1267
+ opts = opts.dup
1268
+ if block_given?
1269
+ builder = TableBuilder.new(ref, columns: columns, name: name, display_name: display_name, style: style, **opts)
1270
+ yield builder
1271
+ opts = builder.to_h
1272
+ ref = opts.delete(:ref) || ref
1273
+ columns = opts.delete(:columns) || columns
1274
+ name = opts.delete(:name) || name
1275
+ display_name = opts.delete(:display_name) || display_name
1276
+ style = opts.delete(:style) || style
1277
+ end
1278
+ raise ArgumentError, "table ref must be provided" unless ref
1279
+ raise ArgumentError, "table columns must be provided" if columns.nil?
1280
+
1281
+ @current_tables << DslHelpers.normalize_table_options(ref, columns: columns, name: name, display_name: display_name, style: style, **opts)
1081
1282
  end
1082
1283
 
1083
1284
  # Adds a Pivot Table.
@@ -1134,6 +1335,38 @@ module Xlsxrb
1134
1335
  @current_sparkline_groups << group
1135
1336
  end
1136
1337
 
1338
+ # Adds a single sparkline to the sheet.
1339
+ #
1340
+ # @example Add a line sparkline
1341
+ # writer.add_sparkline(location: "F1", range: "A1:E1")
1342
+ #
1343
+ # @example Add a column sparkline with high/low markers
1344
+ # writer.add_sparkline(location: "F2", range: "A2:E2", type: :column, high: true, low: true)
1345
+ #
1346
+ # @example Add a win/loss sparkline
1347
+ # writer.add_sparkline(location: "F3", range: "A3:E3", type: :win_loss)
1348
+ #
1349
+ # @param location [String, nil] Target cell for the sparkline (e.g. "F1").
1350
+ # @param range [String, nil] Data range for the sparkline (e.g. "A1:E1").
1351
+ # @param type [Symbol, String, nil] Sparkline type (:line, :column, :win_loss / :stacked).
1352
+ # @param opts [Hash] Additional sparkline options.
1353
+ # @return [void]
1354
+ # @api public
1355
+ #: (?location: String?, ?range: String?, ?type: (String | Symbol)?, **untyped opts) -> void
1356
+ def add_sparkline(location: nil, range: nil, type: nil, **opts)
1357
+ loc = location || opts.delete(:location_ref)
1358
+ rng = range || opts.delete(:data_ref) || opts.delete(:data)
1359
+ raise ArgumentError, "Missing required location: parameter for sparkline" unless loc
1360
+ raise ArgumentError, "Missing required range: parameter for sparkline" unless rng
1361
+
1362
+ mapped_type = case type&.to_s
1363
+ when "win_loss" then "stacked"
1364
+ when nil, "" then nil
1365
+ else type.to_s
1366
+ end
1367
+ sparkline_group(sparklines: [{ data_ref: rng.to_s, location_ref: loc.to_s }], type: mapped_type, **opts)
1368
+ end
1369
+
1137
1370
  # Merges a range of cells into a single cell.
1138
1371
  #
1139
1372
  # @param range [String, Hash, nil] Cell range (e.g. "A1:B2") or hash of coordinates.
@@ -1322,6 +1555,25 @@ module Xlsxrb
1322
1555
  @current_sheet_view[name] = value
1323
1556
  end
1324
1557
 
1558
+ # Configures sheet format properties (e.g. default_row_height, default_col_width).
1559
+ #
1560
+ # @param name [Symbol, String, Hash, nil] Property name or hash of properties.
1561
+ # @param value [Object, nil] Property value when name is provided.
1562
+ # @param opts [Hash] Keyword arguments for sheet format properties.
1563
+ # @return [Hash[Symbol, untyped]]
1564
+ # @api public
1565
+ #: (?Symbol | String | Hash[Symbol, untyped] | nil name, ?untyped value, **untyped opts) -> Hash[Symbol, untyped]
1566
+ def sheet_format(name = nil, value = nil, **opts)
1567
+ sheet if @current_sheet.nil?
1568
+ if name.is_a?(Hash)
1569
+ @current_sheet_format.merge!(name)
1570
+ elsif name && value
1571
+ @current_sheet_format[name.to_sym] = value
1572
+ end
1573
+ @current_sheet_format.merge!(opts) unless opts.empty?
1574
+ @current_sheet_format
1575
+ end
1576
+
1325
1577
  # Inserts a horizontal page break before a row.
1326
1578
  #
1327
1579
  # @param row_num [Integer] 1-based row number.
@@ -1351,6 +1603,36 @@ module Xlsxrb
1351
1603
  # simplecov:enable
1352
1604
  end
1353
1605
 
1606
+ # Adds a protected range to the current sheet.
1607
+ #
1608
+ # @param name [String] Name of the protected range.
1609
+ # @param sqref [String] Cell range (e.g. "A1:B10").
1610
+ # @param password [String, nil] Optional password or hash.
1611
+ # @param opts [Hash] Additional options.
1612
+ # @return [void]
1613
+ # @api public
1614
+ #: (String name, String sqref, ?password: String?, **untyped opts) -> void
1615
+ def protected_range(name, sqref, password: nil, **opts)
1616
+ sheet if @current_sheet.nil?
1617
+ entry = { name: name.to_s, sqref: sqref.to_s }
1618
+ entry[:password] = password if password
1619
+ entry.merge!(opts) unless opts.empty?
1620
+ @current_protected_ranges << DslHelpers.normalize_protection_options(entry)
1621
+ end
1622
+ alias add_protected_range protected_range
1623
+
1624
+ # Adds a cell watch to the current sheet.
1625
+ #
1626
+ # @param ref [String] Cell reference (e.g. "A1").
1627
+ # @return [void]
1628
+ # @api public
1629
+ #: (String ref) -> void
1630
+ def cell_watch(ref)
1631
+ sheet if @current_sheet.nil?
1632
+ @current_cell_watches << ref.to_s
1633
+ end
1634
+ alias add_cell_watch cell_watch
1635
+
1354
1636
  # --- Workbook-Level Methods ---
1355
1637
 
1356
1638
  # Adds a defined name.
@@ -1398,6 +1680,11 @@ module Xlsxrb
1398
1680
  #: (?rows: String?, ?cols: String?, ?sheet: String?) -> void
1399
1681
  def print_titles(rows: nil, cols: nil, sheet: nil)
1400
1682
  sheet_name = sheet || @current_sheet || "Sheet1"
1683
+ existing = @defined_names.find { |dn| dn[:name] == "_xlnm.Print_Titles" && dn[:local_sheet_name] == sheet_name }
1684
+ if existing && (rows.nil? || cols.nil?)
1685
+ cols = Regexp.last_match(1).delete("$") if cols.nil? && existing[:value] =~ /'[^']*'!\$([A-Za-z]+:\$[A-Za-z]+)/
1686
+ rows = Regexp.last_match(1).delete("$") if rows.nil? && existing[:value] =~ /'[^']*'!\$([0-9]+:\$[0-9]+)/
1687
+ end
1401
1688
  parts = []
1402
1689
  parts << "'#{sheet_name}'!$#{cols.sub(":", ":$")}" if cols
1403
1690
  parts << "'#{sheet_name}'!$#{rows.sub(":", ":$")}" if rows
@@ -1406,6 +1693,55 @@ module Xlsxrb
1406
1693
  defined_name("_xlnm.Print_Titles", value, sheet: sheet_name)
1407
1694
  end
1408
1695
 
1696
+ # Sets repeating title rows for pagination on the current or named sheet.
1697
+ #
1698
+ # @example Repeat first row
1699
+ # stream.repeat_rows(0)
1700
+ #
1701
+ # @param first [Integer, String] First row index (0-based) or range string (e.g. "1:2").
1702
+ # @param last [Integer, String, nil] Optional last row index (0-based).
1703
+ # @param sheet [String, nil] Target sheet name.
1704
+ # @return [void]
1705
+ # @api public
1706
+ #: (Integer | String first, ?(Integer | String)? last, ?sheet: String?) -> void
1707
+ def repeat_rows(first, last = nil, sheet: nil)
1708
+ rows_str = if first.is_a?(String)
1709
+ first.include?(":") ? first : "#{first}:#{last || first}"
1710
+ else
1711
+ last ||= first
1712
+ "#{first + 1}:#{last + 1}"
1713
+ end
1714
+ print_titles(rows: rows_str, sheet: sheet)
1715
+ end
1716
+
1717
+ # Sets repeating title columns for pagination on the current or named sheet.
1718
+ #
1719
+ # @example Repeat columns A through C
1720
+ # stream.repeat_columns("A", "C")
1721
+ #
1722
+ # @param first [Integer, String] First column index (0-based) or column name/range.
1723
+ # @param last [Integer, String, nil] Optional last column index/name.
1724
+ # @param sheet [String, nil] Target sheet name.
1725
+ # @return [void]
1726
+ # @api public
1727
+ #: (Integer | String first, ?(Integer | String)? last, ?sheet: String?) -> void
1728
+ def repeat_columns(first, last = nil, sheet: nil)
1729
+ cols_str = if first.is_a?(String)
1730
+ first.include?(":") ? first : "#{first}:#{last || first}"
1731
+ else
1732
+ first_name = Utils.col_index_to_name(first)
1733
+ last_name = if last.is_a?(Integer)
1734
+ Utils.col_index_to_name(last)
1735
+ elsif last
1736
+ last.to_s
1737
+ else
1738
+ first_name
1739
+ end
1740
+ "#{first_name}:#{last_name}"
1741
+ end
1742
+ print_titles(cols: cols_str, sheet: sheet)
1743
+ end
1744
+
1409
1745
  # Sets workbook protection.
1410
1746
  #
1411
1747
  # @param opts [Hash] Protection options.
@@ -1623,9 +1959,12 @@ module Xlsxrb
1623
1959
  images: @current_images,
1624
1960
  shapes: @current_shapes,
1625
1961
  sheet_properties: @current_sheet_properties,
1962
+ sheet_format: @current_sheet_format,
1626
1963
  sheet_view: @current_sheet_view,
1627
1964
  row_breaks: @current_row_breaks,
1628
- col_breaks: @current_col_breaks
1965
+ col_breaks: @current_col_breaks,
1966
+ protected_ranges: @current_protected_ranges,
1967
+ cell_watches: @current_cell_watches
1629
1968
  }
1630
1969
  sheet_data[:cells] = @current_cells if @current_cells && !@current_cells.empty?
1631
1970
  @current_cells = nil
@@ -1636,6 +1975,7 @@ module Xlsxrb
1636
1975
  @current_row_writer.finish(
1637
1976
  drawing_rid: info[:drawing_rid],
1638
1977
  sheet_protection: @current_sheet_protection,
1978
+ protected_ranges: @current_protected_ranges.empty? ? nil : @current_protected_ranges,
1639
1979
  auto_filter: @current_auto_filter,
1640
1980
  filter_columns: @current_filter_columns.empty? ? nil : @current_filter_columns,
1641
1981
  sort_state: @current_sort_state,
@@ -1649,6 +1989,7 @@ module Xlsxrb
1649
1989
  header_footer: @current_header_footer.empty? ? nil : @current_header_footer,
1650
1990
  row_breaks: @current_row_breaks.empty? ? nil : @current_row_breaks,
1651
1991
  col_breaks: @current_col_breaks.empty? ? nil : @current_col_breaks,
1992
+ cell_watches: @current_cell_watches.empty? ? nil : @current_cell_watches,
1652
1993
  tables: @current_tables.empty? ? nil : @current_tables,
1653
1994
  table_start_rid: info[:table_start_rid],
1654
1995
  legacy_drawing_rid: info[:vml_rid],