xlsxrb 0.1.13 → 0.1.14

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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -0
  3. data/README.md +2 -0
  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 +85 -4
  13. data/lib/xlsxrb/elements/worksheet.rb +424 -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.rb +12 -14
  20. data/lib/xlsxrb/ooxml/shared_strings_parser.rb +7 -1
  21. data/lib/xlsxrb/ooxml/styles_parser.rb +39 -9
  22. data/lib/xlsxrb/ooxml/utils.rb +34 -18
  23. data/lib/xlsxrb/ooxml/workbook_parser.rb +53 -15
  24. data/lib/xlsxrb/ooxml/workbook_writer.rb +65 -14
  25. data/lib/xlsxrb/ooxml/worksheet_parser.rb +319 -79
  26. data/lib/xlsxrb/ooxml/worksheet_writer.rb +206 -61
  27. data/lib/xlsxrb/ooxml/writer/features_xml.rb +4 -2
  28. data/lib/xlsxrb/ooxml/writer.rb +14 -0
  29. data/lib/xlsxrb/ooxml/zip_reader.rb +109 -25
  30. data/lib/xlsxrb/stream_row.rb +243 -16
  31. data/lib/xlsxrb/stream_sheet.rb +414 -15
  32. data/lib/xlsxrb/stream_writer.rb +213 -17
  33. data/lib/xlsxrb/style_builder.rb +308 -22
  34. data/lib/xlsxrb/utils.rb +158 -0
  35. data/lib/xlsxrb/version.rb +1 -1
  36. data/lib/xlsxrb/workbook_builder.rb +73 -3
  37. data/lib/xlsxrb/worksheet_builder.rb +227 -16
  38. data/lib/xlsxrb.rb +578 -79
  39. data/sig/generated/xlsxrb/colors.rbs +40 -0
  40. data/sig/generated/xlsxrb/elements/cell.rbs +113 -8
  41. data/sig/generated/xlsxrb/elements/column.rbs +3 -2
  42. data/sig/generated/xlsxrb/elements/coordinate_access.rbs +18 -0
  43. data/sig/generated/xlsxrb/elements/image.rbs +82 -0
  44. data/sig/generated/xlsxrb/elements/row.rbs +83 -4
  45. data/sig/generated/xlsxrb/elements/styles.rbs +79 -0
  46. data/sig/generated/xlsxrb/elements/types.rbs +43 -0
  47. data/sig/generated/xlsxrb/elements/workbook.rbs +51 -2
  48. data/sig/generated/xlsxrb/elements/worksheet.rbs +231 -18
  49. data/sig/generated/xlsxrb/number_formatter.rbs +132 -0
  50. data/sig/generated/xlsxrb/ooxml/reader.rbs +6 -0
  51. data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +2 -0
  52. data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +5 -2
  53. data/sig/generated/xlsxrb/ooxml/utils.rbs +17 -8
  54. data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +9 -3
  55. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +28 -6
  56. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +3 -1
  57. data/sig/generated/xlsxrb/ooxml/writer.rbs +2 -0
  58. data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +10 -0
  59. data/sig/generated/xlsxrb/stream_row.rbs +100 -6
  60. data/sig/generated/xlsxrb/stream_sheet.rbs +247 -13
  61. data/sig/generated/xlsxrb/stream_writer.rbs +120 -14
  62. data/sig/generated/xlsxrb/style_builder.rbs +55 -18
  63. data/sig/generated/xlsxrb/utils.rbs +119 -0
  64. data/sig/generated/xlsxrb/workbook_builder.rbs +26 -0
  65. data/sig/generated/xlsxrb/worksheet_builder.rbs +107 -11
  66. data/sig/generated/xlsxrb.rbs +186 -27
  67. metadata +11 -1
@@ -220,15 +220,30 @@ module Xlsxrb
220
220
  # @param hidden [Boolean] Whether the row is hidden.
221
221
  # @param custom_height [Boolean] Whether to flag as custom height.
222
222
  # @param outline_level [Integer, nil] Grouping/outline level (0 - 7).
223
+ # @param collapsed [Boolean] Whether the row is collapsed.
223
224
  # @return [void]
224
225
  # @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)
226
+ #: (Array[untyped] | Hash[untyped, untyped] values, ?styles: untyped, ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?collapsed: bool, ?outline_level: Integer | nil) -> void
227
+ def row(values, styles: nil, height: nil, hidden: false, custom_height: false, collapsed: false, outline_level: nil)
227
228
  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
229
 
229
- @writer.row(values, styles: styles, height: height, hidden: hidden, custom_height: custom_height, outline_level: outline_level)
230
+ @writer.row(values, styles: styles, height: height, hidden: hidden, custom_height: custom_height, collapsed: collapsed, outline_level: outline_level)
231
+ end
232
+ alias add_row row
233
+
234
+ # Appends a row to the worksheet and returns self for chaining.
235
+ #
236
+ # @example
237
+ # sheet << ["A", 1] << ["B", 2]
238
+ #
239
+ # @param values [Array, Hash] Row cell values.
240
+ # @return [self]
241
+ # @api public
242
+ #: (Array[untyped] | Hash[untyped, untyped] values) -> self
243
+ def <<(values)
244
+ row(values)
245
+ self
230
246
  end
231
- alias << row
232
247
 
233
248
  # Configures column width and properties.
234
249
  #
@@ -287,7 +302,7 @@ module Xlsxrb
287
302
  # @param location [String, nil] Destination location / URL (keyword alternative).
288
303
  # @return [void]
289
304
  # @api public
290
- #: (String cell, ?String? url, ?display: String?, ?tooltip: String?, ?location: String?) -> void
305
+ #: (String | Integer cell, ?(String | Elements::Hyperlink)? url, ?display: String?, ?tooltip: String?, ?location: String?) -> void
291
306
  def hyperlink(...)
292
307
  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
308
 
@@ -460,6 +475,30 @@ module Xlsxrb
460
475
  @writer.sparkline_group(...)
461
476
  end
462
477
 
478
+ # Adds a single sparkline to the sheet.
479
+ #
480
+ # @example Add a line sparkline
481
+ # s.add_sparkline(location: "F1", range: "A1:E1")
482
+ #
483
+ # @example Add a column sparkline with high/low markers
484
+ # s.add_sparkline(location: "F2", range: "A2:E2", type: :column, high: true, low: true)
485
+ #
486
+ # @example Add a win/loss sparkline
487
+ # s.add_sparkline(location: "F3", range: "A3:E3", type: :win_loss)
488
+ #
489
+ # @param location [String, nil] Target cell for the sparkline (e.g. "F1").
490
+ # @param range [String, nil] Data range for the sparkline (e.g. "A1:E1").
491
+ # @param type [Symbol, String, nil] Sparkline type (:line, :column, :win_loss / :stacked).
492
+ # @param opts [Hash] Additional sparkline options.
493
+ # @return [void]
494
+ # @api public
495
+ #: (?location: String?, ?range: String?, ?type: (String | Symbol)?, **untyped opts) -> void
496
+ def add_sparkline(...)
497
+ 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
498
+
499
+ @writer.add_sparkline(...)
500
+ end
501
+
463
502
  # Sets workbook-level properties.
464
503
  #
465
504
  # @param opts [Hash] Workbook property options.
@@ -554,6 +593,32 @@ module Xlsxrb
554
593
 
555
594
  @writer.print_titles(...)
556
595
  end
596
+
597
+ # Sets repeating title rows to appear at the top of each printed page.
598
+ #
599
+ # @param first [Integer, String] First row index (0-based) or range string (e.g. "1:2").
600
+ # @param last [Integer, String, nil] Optional last row index (0-based).
601
+ # @return [void]
602
+ # @api public
603
+ #: (Integer | String first, ?(Integer | String)? last) -> void
604
+ def repeat_rows(first, last = nil)
605
+ 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
606
+
607
+ @writer.repeat_rows(first, last, sheet: @sheet_name)
608
+ end
609
+
610
+ # Sets repeating title columns to appear at the left of each printed page.
611
+ #
612
+ # @param first [Integer, String] First column index (0-based) or column name/range.
613
+ # @param last [Integer, String, nil] Optional last column index/name.
614
+ # @return [void]
615
+ # @api public
616
+ #: (Integer | String first, ?(Integer | String)? last) -> void
617
+ def repeat_columns(first, last = nil)
618
+ 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
619
+
620
+ @writer.repeat_columns(first, last, sheet: @sheet_name)
621
+ end
557
622
  # simplecov:enable
558
623
 
559
624
  # Splits sheet view into panes.
@@ -829,7 +894,8 @@ module Xlsxrb
829
894
  @sheet_entry_started = false
830
895
  @current_row_buffer = String.new(capacity: 65_536)
831
896
  @current_sheet_io = StringIO.new(@current_row_buffer)
832
- @current_row_writer = Ooxml::WorksheetWriter.new(@current_sheet_io)
897
+ date1904 = @workbook_properties&.[](:date1904) ? true : false
898
+ @current_row_writer = Ooxml::WorksheetWriter.new(@current_sheet_io, date1904: date1904)
833
899
  @current_row_writer.instance_variable_set(:@started, true)
834
900
 
835
901
  @current_columns = []
@@ -883,7 +949,18 @@ module Xlsxrb
883
949
  row_buf.clear
884
950
  end
885
951
 
886
- @current_row_writer = Ooxml::WorksheetWriter.new(zip_io)
952
+ if @current_columns && !@current_columns.empty? && @style_name_to_id
953
+ @current_columns.each do |col|
954
+ s = col[:style] || col[:style_index]
955
+ if s.is_a?(Symbol) || s.is_a?(String)
956
+ resolved = @style_name_to_id[s] || @style_name_to_id[s.to_s]
957
+ col[:style] = resolved if resolved
958
+ end
959
+ end
960
+ end
961
+
962
+ date1904 = @workbook_properties&.[](:date1904) ? true : false
963
+ @current_row_writer = Ooxml::WorksheetWriter.new(zip_io, date1904: date1904)
887
964
  @current_row_writer.start(
888
965
  columns: @current_columns,
889
966
  sheet_properties: @current_sheet_properties.empty? ? nil : @current_sheet_properties,
@@ -906,8 +983,8 @@ module Xlsxrb
906
983
  # @param outline_level [Integer, nil] Grouping/outline level (0 - 7).
907
984
  # @return [void]
908
985
  # @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)
986
+ #: (Array[untyped] | Hash[untyped, untyped] values, ?styles: untyped, ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?collapsed: bool, ?outline_level: Integer | nil) -> void
987
+ def row(values, styles: nil, height: nil, hidden: false, custom_height: false, collapsed: false, outline_level: nil)
911
988
  sheet if @current_sheet.nil?
912
989
 
913
990
  row_index = @current_row_index
@@ -931,36 +1008,60 @@ module Xlsxrb
931
1008
  raise ArgumentError, "Row contains #{values.length} columns, exceeding Excel limit of 16_384 columns" if @strict_excel_mode && values.length > 16_384
932
1009
 
933
1010
  attrs = nil
934
- if height || hidden || outline_level
1011
+ if height || hidden || outline_level || collapsed
935
1012
  attrs = {}
936
1013
  attrs[:height] = height if height
937
1014
  attrs[:hidden] = true if hidden
938
1015
  attrs[:custom_height] = custom_height || !height.nil?
1016
+ attrs[:collapsed] = true if collapsed
939
1017
  attrs[:outline_level] = outline_level if outline_level
940
1018
  end
941
1019
 
942
1020
  @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
1021
  start_sheet_entry if !@sheet_entry_started && @current_row_buffer.bytesize >= 65_536
944
1022
  end
945
- alias << row
1023
+ alias add_row row
1024
+
1025
+ # Appends a row to the worksheet and returns self for chaining.
1026
+ #
1027
+ # @example
1028
+ # writer << ["A", 1] << ["B", 2]
1029
+ #
1030
+ # @param values [Array, Hash] Row cell values.
1031
+ # @return [self]
1032
+ # @api public
1033
+ #: (Array[untyped] | Hash[untyped, untyped] values) -> self
1034
+ def <<(values)
1035
+ row(values)
1036
+ self
1037
+ end
946
1038
 
947
1039
  # Sets column formatting and properties for one or multiple columns.
948
1040
  #
949
1041
  # @param index [Integer, String, Range, Array] Column index (0-based) or letter ("A".."D").
950
1042
  # @param width [Float, Integer, nil] Column width in character units (0 - 255).
951
1043
  # @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.
1044
+ # @param style [Integer, Symbol, String, nil] Column style format or index.
954
1045
  # @return [void]
955
1046
  # @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)
1047
+ #: (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
1048
+ def column(index, width: nil, hidden: false, custom_width: false, outline_level: nil, style: nil)
958
1049
  raise ArgumentError, "Column width #{width} must be between 0 and 255 characters (Excel limitation)" if @strict_excel_mode && width && (width.negative? || width > 255)
959
1050
 
960
1051
  sheet if @current_sheet.nil?
961
1052
 
1053
+ style_id = if @style_name_to_id && style
1054
+ @style_name_to_id[style] || @style_name_to_id[style.to_s] || style
1055
+ else
1056
+ style
1057
+ end
962
1058
  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 }
1059
+ entry = { index: idx, width: width, hidden: hidden, custom_width: custom_width || !width.nil?, outline_level: outline_level }
1060
+ if style_id
1061
+ entry[:style] = style_id
1062
+ @current_row_writer&.set_column_style(idx, style_id)
1063
+ end
1064
+ @current_columns << entry
964
1065
  end
965
1066
  end
966
1067
 
@@ -993,9 +1094,18 @@ module Xlsxrb
993
1094
  # @param location [String, nil] Internal location.
994
1095
  # @return [void]
995
1096
  # @api public
996
- #: (String | Integer cell, ?String? url, ?display: String?, ?tooltip: String?, ?location: String?) -> void
1097
+ #: (String | Integer cell, ?(String | Elements::Hyperlink)? url, ?display: String?, ?tooltip: String?, ?location: String?) -> void
997
1098
  def hyperlink(cell, url = nil, display: nil, tooltip: nil, location: nil)
998
1099
  sheet if @current_sheet.nil?
1100
+ if url.is_a?(Elements::Hyperlink)
1101
+ link = { cell: cell }
1102
+ link[:url] = url.url if url.url
1103
+ link[:display] = url.display || display if url.display || display
1104
+ link[:tooltip] = url.tooltip || tooltip if url.tooltip || tooltip
1105
+ link[:location] = url.location || location if url.location || location
1106
+ @current_hyperlinks << link
1107
+ return
1108
+ end
999
1109
  link = { cell: cell }
1000
1110
  link[:url] = url if url
1001
1111
  link[:display] = display if display
@@ -1134,6 +1244,38 @@ module Xlsxrb
1134
1244
  @current_sparkline_groups << group
1135
1245
  end
1136
1246
 
1247
+ # Adds a single sparkline to the sheet.
1248
+ #
1249
+ # @example Add a line sparkline
1250
+ # writer.add_sparkline(location: "F1", range: "A1:E1")
1251
+ #
1252
+ # @example Add a column sparkline with high/low markers
1253
+ # writer.add_sparkline(location: "F2", range: "A2:E2", type: :column, high: true, low: true)
1254
+ #
1255
+ # @example Add a win/loss sparkline
1256
+ # writer.add_sparkline(location: "F3", range: "A3:E3", type: :win_loss)
1257
+ #
1258
+ # @param location [String, nil] Target cell for the sparkline (e.g. "F1").
1259
+ # @param range [String, nil] Data range for the sparkline (e.g. "A1:E1").
1260
+ # @param type [Symbol, String, nil] Sparkline type (:line, :column, :win_loss / :stacked).
1261
+ # @param opts [Hash] Additional sparkline options.
1262
+ # @return [void]
1263
+ # @api public
1264
+ #: (?location: String?, ?range: String?, ?type: (String | Symbol)?, **untyped opts) -> void
1265
+ def add_sparkline(location: nil, range: nil, type: nil, **opts)
1266
+ loc = location || opts.delete(:location_ref)
1267
+ rng = range || opts.delete(:data_ref) || opts.delete(:data)
1268
+ raise ArgumentError, "Missing required location: parameter for sparkline" unless loc
1269
+ raise ArgumentError, "Missing required range: parameter for sparkline" unless rng
1270
+
1271
+ mapped_type = case type&.to_s
1272
+ when "win_loss" then "stacked"
1273
+ when nil, "" then nil
1274
+ else type.to_s
1275
+ end
1276
+ sparkline_group(sparklines: [{ data_ref: rng.to_s, location_ref: loc.to_s }], type: mapped_type, **opts)
1277
+ end
1278
+
1137
1279
  # Merges a range of cells into a single cell.
1138
1280
  #
1139
1281
  # @param range [String, Hash, nil] Cell range (e.g. "A1:B2") or hash of coordinates.
@@ -1398,6 +1540,11 @@ module Xlsxrb
1398
1540
  #: (?rows: String?, ?cols: String?, ?sheet: String?) -> void
1399
1541
  def print_titles(rows: nil, cols: nil, sheet: nil)
1400
1542
  sheet_name = sheet || @current_sheet || "Sheet1"
1543
+ existing = @defined_names.find { |dn| dn[:name] == "_xlnm.Print_Titles" && dn[:local_sheet_name] == sheet_name }
1544
+ if existing && (rows.nil? || cols.nil?)
1545
+ cols = Regexp.last_match(1).delete("$") if cols.nil? && existing[:value] =~ /'[^']*'!\$([A-Za-z]+:\$[A-Za-z]+)/
1546
+ rows = Regexp.last_match(1).delete("$") if rows.nil? && existing[:value] =~ /'[^']*'!\$([0-9]+:\$[0-9]+)/
1547
+ end
1401
1548
  parts = []
1402
1549
  parts << "'#{sheet_name}'!$#{cols.sub(":", ":$")}" if cols
1403
1550
  parts << "'#{sheet_name}'!$#{rows.sub(":", ":$")}" if rows
@@ -1406,6 +1553,55 @@ module Xlsxrb
1406
1553
  defined_name("_xlnm.Print_Titles", value, sheet: sheet_name)
1407
1554
  end
1408
1555
 
1556
+ # Sets repeating title rows for pagination on the current or named sheet.
1557
+ #
1558
+ # @example Repeat first row
1559
+ # stream.repeat_rows(0)
1560
+ #
1561
+ # @param first [Integer, String] First row index (0-based) or range string (e.g. "1:2").
1562
+ # @param last [Integer, String, nil] Optional last row index (0-based).
1563
+ # @param sheet [String, nil] Target sheet name.
1564
+ # @return [void]
1565
+ # @api public
1566
+ #: (Integer | String first, ?(Integer | String)? last, ?sheet: String?) -> void
1567
+ def repeat_rows(first, last = nil, sheet: nil)
1568
+ rows_str = if first.is_a?(String)
1569
+ first.include?(":") ? first : "#{first}:#{last || first}"
1570
+ else
1571
+ last ||= first
1572
+ "#{first + 1}:#{last + 1}"
1573
+ end
1574
+ print_titles(rows: rows_str, sheet: sheet)
1575
+ end
1576
+
1577
+ # Sets repeating title columns for pagination on the current or named sheet.
1578
+ #
1579
+ # @example Repeat columns A through C
1580
+ # stream.repeat_columns("A", "C")
1581
+ #
1582
+ # @param first [Integer, String] First column index (0-based) or column name/range.
1583
+ # @param last [Integer, String, nil] Optional last column index/name.
1584
+ # @param sheet [String, nil] Target sheet name.
1585
+ # @return [void]
1586
+ # @api public
1587
+ #: (Integer | String first, ?(Integer | String)? last, ?sheet: String?) -> void
1588
+ def repeat_columns(first, last = nil, sheet: nil)
1589
+ cols_str = if first.is_a?(String)
1590
+ first.include?(":") ? first : "#{first}:#{last || first}"
1591
+ else
1592
+ first_name = Utils.col_index_to_name(first)
1593
+ last_name = if last.is_a?(Integer)
1594
+ Utils.col_index_to_name(last)
1595
+ elsif last
1596
+ last.to_s
1597
+ else
1598
+ first_name
1599
+ end
1600
+ "#{first_name}:#{last_name}"
1601
+ end
1602
+ print_titles(cols: cols_str, sheet: sheet)
1603
+ end
1604
+
1409
1605
  # Sets workbook protection.
1410
1606
  #
1411
1607
  # @param opts [Hash] Protection options.