xlsxrb 0.1.15 → 0.1.17

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +125 -0
  3. data/README.md +24 -5
  4. data/benchmark/micro_ips.rb +70 -0
  5. data/lib/xlsxrb/chart_builder.rb +41 -2
  6. data/lib/xlsxrb/dsl_helpers.rb +522 -6
  7. data/lib/xlsxrb/elements/cell.rb +34 -9
  8. data/lib/xlsxrb/elements/column.rb +2 -2
  9. data/lib/xlsxrb/elements/styles.rb +427 -15
  10. data/lib/xlsxrb/elements/types.rb +1 -0
  11. data/lib/xlsxrb/elements/workbook.rb +79 -4
  12. data/lib/xlsxrb/elements/worksheet.rb +173 -11
  13. data/lib/xlsxrb/ooxml/cfb.rb +10 -0
  14. data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +77 -38
  15. data/lib/xlsxrb/ooxml/reader/listeners/drawing_listeners.rb +187 -24
  16. data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +115 -2
  17. data/lib/xlsxrb/ooxml/reader/listeners/style_listeners.rb +28 -18
  18. data/lib/xlsxrb/ooxml/reader.rb +253 -136
  19. data/lib/xlsxrb/ooxml/shared_strings_parser.rb +83 -28
  20. data/lib/xlsxrb/ooxml/styles_parser.rb +76 -25
  21. data/lib/xlsxrb/ooxml/utils.rb +59 -2
  22. data/lib/xlsxrb/ooxml/workbook_parser.rb +57 -3
  23. data/lib/xlsxrb/ooxml/workbook_writer.rb +471 -86
  24. data/lib/xlsxrb/ooxml/worksheet_parser.rb +210 -127
  25. data/lib/xlsxrb/ooxml/worksheet_writer.rb +515 -184
  26. data/lib/xlsxrb/ooxml/writer/drawing_xml.rb +595 -453
  27. data/lib/xlsxrb/ooxml/writer/features_xml.rb +26 -9
  28. data/lib/xlsxrb/ooxml/writer/styles_xml.rb +87 -24
  29. data/lib/xlsxrb/ooxml/writer.rb +299 -59
  30. data/lib/xlsxrb/ooxml/xml_builder.rb +12 -4
  31. data/lib/xlsxrb/ooxml/xml_parser.rb +45 -0
  32. data/lib/xlsxrb/ooxml/zip_reader.rb +48 -29
  33. data/lib/xlsxrb/ooxml/zip_writer.rb +15 -5
  34. data/lib/xlsxrb/stream_row.rb +4 -3
  35. data/lib/xlsxrb/stream_sheet.rb +182 -8
  36. data/lib/xlsxrb/stream_writer.rb +977 -84
  37. data/lib/xlsxrb/style_builder.rb +59 -6
  38. data/lib/xlsxrb/utils.rb +80 -0
  39. data/lib/xlsxrb/version.rb +1 -1
  40. data/lib/xlsxrb/workbook_builder.rb +307 -24
  41. data/lib/xlsxrb/worksheet_builder.rb +1150 -49
  42. data/lib/xlsxrb.rb +705 -142
  43. data/sig/generated/xlsxrb/chart_builder.rbs +31 -3
  44. data/sig/generated/xlsxrb/dsl_helpers.rbs +119 -4
  45. data/sig/generated/xlsxrb/elements/cell.rbs +20 -3
  46. data/sig/generated/xlsxrb/elements/column.rbs +3 -3
  47. data/sig/generated/xlsxrb/elements/styles.rbs +271 -5
  48. data/sig/generated/xlsxrb/elements/types.rbs +2 -0
  49. data/sig/generated/xlsxrb/elements/workbook.rbs +63 -3
  50. data/sig/generated/xlsxrb/elements/worksheet.rbs +92 -2
  51. data/sig/generated/xlsxrb/ooxml/reader/listeners/core_listeners.rbs +2 -0
  52. data/sig/generated/xlsxrb/ooxml/reader/listeners/drawing_listeners.rbs +74 -2
  53. data/sig/generated/xlsxrb/ooxml/reader/listeners/feature_listeners.rbs +36 -0
  54. data/sig/generated/xlsxrb/ooxml/reader/listeners/style_listeners.rbs +5 -0
  55. data/sig/generated/xlsxrb/ooxml/reader.rbs +29 -0
  56. data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +3 -1
  57. data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +5 -0
  58. data/sig/generated/xlsxrb/ooxml/utils.rbs +22 -0
  59. data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +15 -0
  60. data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +20 -4
  61. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +19 -3
  62. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +15 -2
  63. data/sig/generated/xlsxrb/ooxml/writer/drawing_xml.rbs +16 -0
  64. data/sig/generated/xlsxrb/ooxml/writer/styles_xml.rbs +5 -2
  65. data/sig/generated/xlsxrb/ooxml/writer.rbs +42 -8
  66. data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +4 -1
  67. data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +6 -6
  68. data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +8 -2
  69. data/sig/generated/xlsxrb/stream_row.rbs +3 -2
  70. data/sig/generated/xlsxrb/stream_sheet.rbs +122 -6
  71. data/sig/generated/xlsxrb/stream_writer.rbs +606 -30
  72. data/sig/generated/xlsxrb/style_builder.rbs +24 -6
  73. data/sig/generated/xlsxrb/utils.rbs +33 -0
  74. data/sig/generated/xlsxrb/workbook_builder.rbs +87 -5
  75. data/sig/generated/xlsxrb/worksheet_builder.rbs +557 -12
  76. data/sig/generated/xlsxrb.rbs +118 -20
  77. metadata +2 -1
@@ -17,9 +17,13 @@ module Xlsxrb
17
17
  class Worksheet
18
18
  [Enumerable, CoordinateAccess].each { |m| include m }
19
19
 
20
- attr_reader :name, :rows, :columns, :charts, :conditional_formatting, :data_validations, :unmapped_data, :errors, :state, :hyperlinks, :comments, :images, :styles, :date1904, :print_area, :print_titles, :sparkline_groups, :merged_cells, :auto_filter, :tables, :shapes, :freeze_pane, :page_setup, :page_margins, :header_footer, :print_options, :sheet_protection, :sheet_view, :sheet_properties, :sheet_format, :row_breaks, :col_breaks, :protected_ranges, :cell_watches
20
+ attr_reader :name, :rows, :columns, :charts, :conditional_formatting, :data_validations, :unmapped_data, :errors, :state, :hyperlinks, :comments, :images, :styles, :date1904, :print_area, :print_titles, :sparkline_groups, :merged_cells, :auto_filter, :tables, :shapes, :freeze_pane, :page_setup, :page_margins, :header_footer, :print_options, :sheet_protection, :sheet_view, :sheet_properties, :sheet_format, :row_breaks, :col_breaks, :protected_ranges, :cell_watches, :ignored_errors, :phonetic_properties, :selection, :sheet_calc_pr, :background_image, :threaded_comments
21
21
  attr_writer :dimension
22
22
  alias conditional_formats conditional_formatting
23
+ alias phonetic_pr phonetic_properties
24
+ alias calc_pr sheet_calc_pr
25
+ alias background_picture background_image
26
+ alias picture background_image
23
27
 
24
28
  # @param name [String] The worksheet name (max 31 characters).
25
29
  # @param rows [Array<Elements::Row>] Rows in the sheet.
@@ -59,8 +63,14 @@ module Xlsxrb
59
63
  # @param col_breaks [Array<Integer | Hash>] Explicit page column breaks.
60
64
  # @param protected_ranges [Array<Hash>] Protected range definitions.
61
65
  # @param cell_watches [Array<String>] Watched cell references.
62
- #: (name: String?, ?rows: Array[Elements::Row], ?columns: Array[Elements::Column], ?charts: Array[Hash[Symbol, untyped]], ?conditional_formatting: Array[Hash[Symbol, untyped]]?, ?data_validations: Array[Hash[Symbol, untyped]], ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?, ?conditional_formats: Array[Hash[Symbol, untyped]]?, ?state: Symbol, ?hyperlinks: Hash[String, untyped], ?comments: Array[Hash[Symbol, untyped]], ?images: Array[Elements::Image], ?styles: Hash[untyped, untyped]?, ?date1904: bool, ?dimension: String?, ?trim_empty_rows: bool, ?pad_empty_rows: bool, ?pad_empty_cells: bool, ?print_area: String?, ?print_titles: Hash[Symbol, untyped]?, ?sparkline_groups: Array[Hash[Symbol, untyped]], ?merged_cells: Array[String], ?auto_filter: String?, ?tables: Array[Hash[Symbol, untyped]], ?shapes: Array[Hash[Symbol, untyped]], ?freeze_pane: Hash[Symbol, untyped]?, ?page_setup: Hash[Symbol, untyped], ?page_margins: Hash[Symbol, untyped]?, ?header_footer: Hash[Symbol, untyped], ?print_options: Hash[Symbol, untyped], ?sheet_protection: Hash[Symbol, untyped]?, ?sheet_view: Hash[Symbol, untyped], ?sheet_properties: Hash[Symbol, untyped], ?sheet_format: Hash[Symbol, untyped], ?row_breaks: Array[untyped], ?col_breaks: Array[untyped], ?protected_ranges: Array[Hash[Symbol, untyped]], ?cell_watches: Array[String]) -> void
63
- def initialize(name:, rows: [], columns: [], charts: [], conditional_formatting: nil, data_validations: [], unmapped_data: {}, errors: nil, conditional_formats: nil, state: :visible, hyperlinks: {}, comments: [], images: [], styles: nil, date1904: false, dimension: nil, trim_empty_rows: false, pad_empty_rows: false, pad_empty_cells: false, print_area: nil, print_titles: nil, sparkline_groups: [], merged_cells: [], auto_filter: nil, tables: [], shapes: [], freeze_pane: nil, page_setup: {}, page_margins: nil, header_footer: {}, print_options: {}, sheet_protection: nil, sheet_view: {}, sheet_properties: {}, sheet_format: {}, row_breaks: [], col_breaks: [], protected_ranges: [], cell_watches: [])
66
+ # @param ignored_errors [Array<Hash>] Ignored error rules.
67
+ # @param phonetic_properties [Hash, nil] Phonetic text properties.
68
+ # @param selection [Hash, Array<Hash>, nil] Active cell and selection settings.
69
+ # @param sheet_calc_pr [Hash, nil] Worksheet calculation properties.
70
+ # @param background_image [Hash, nil] Worksheet background image specification.
71
+ # @param threaded_comments [Array<Hash>] Threaded comments in the sheet.
72
+ #: (name: String?, ?rows: Array[Elements::Row], ?columns: Array[Elements::Column], ?charts: Array[Hash[Symbol, untyped]], ?conditional_formatting: Array[Hash[Symbol, untyped]]?, ?data_validations: Array[Hash[Symbol, untyped]], ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?, ?conditional_formats: Array[Hash[Symbol, untyped]]?, ?state: Symbol, ?hyperlinks: Hash[String, untyped], ?comments: Array[Hash[Symbol, untyped]], ?images: Array[Elements::Image], ?styles: (Elements::Styles | Hash[untyped, untyped])?, ?date1904: bool, ?dimension: String?, ?trim_empty_rows: bool, ?pad_empty_rows: bool, ?pad_empty_cells: bool, ?print_area: String?, ?print_titles: Hash[Symbol, untyped]?, ?sparkline_groups: Array[Hash[Symbol, untyped]], ?merged_cells: Array[String], ?auto_filter: String?, ?tables: Array[Hash[Symbol, untyped]], ?shapes: Array[Hash[Symbol, untyped]], ?freeze_pane: Hash[Symbol, untyped]?, ?page_setup: Hash[Symbol, untyped], ?page_margins: Hash[Symbol, untyped]?, ?header_footer: Hash[Symbol, untyped], ?print_options: Hash[Symbol, untyped], ?sheet_protection: Hash[Symbol, untyped]?, ?sheet_view: Hash[Symbol, untyped], ?sheet_properties: Hash[Symbol, untyped], ?sheet_format: Hash[Symbol, untyped], ?row_breaks: Array[untyped], ?col_breaks: Array[untyped], ?protected_ranges: Array[Hash[Symbol, untyped]], ?cell_watches: Array[String], ?ignored_errors: Array[Hash[Symbol, untyped]], ?phonetic_properties: Hash[Symbol, untyped]?, ?selection: (Hash[Symbol, untyped] | Array[Hash[Symbol, untyped]])?, ?sheet_calc_pr: Hash[Symbol, untyped]?, ?background_image: Hash[Symbol, untyped]?, ?threaded_comments: Array[Hash[Symbol, untyped]]) -> void
73
+ def initialize(name:, rows: [], columns: [], charts: [], conditional_formatting: nil, data_validations: [], unmapped_data: {}, errors: nil, conditional_formats: nil, state: :visible, hyperlinks: {}, comments: [], images: [], styles: nil, date1904: false, dimension: nil, trim_empty_rows: false, pad_empty_rows: false, pad_empty_cells: false, print_area: nil, print_titles: nil, sparkline_groups: [], merged_cells: [], auto_filter: nil, tables: [], shapes: [], freeze_pane: nil, page_setup: {}, page_margins: nil, header_footer: {}, print_options: {}, sheet_protection: nil, sheet_view: {}, sheet_properties: {}, sheet_format: {}, row_breaks: [], col_breaks: [], protected_ranges: [], cell_watches: [], ignored_errors: [], phonetic_properties: nil, selection: nil, sheet_calc_pr: nil, background_image: nil, threaded_comments: [])
64
74
  @name = name
65
75
  @rows = (rows || []).freeze
66
76
  @columns = (columns || []).freeze
@@ -84,6 +94,7 @@ module Xlsxrb
84
94
  @trim_empty_rows = trim_empty_rows ? true : false
85
95
  @pad_empty_rows = pad_empty_rows ? true : false
86
96
  @pad_empty_cells = pad_empty_cells ? true : false
97
+ @threaded_comments = (threaded_comments || []).freeze
87
98
  @merged_cells = (merged_cells || []).freeze
88
99
  @auto_filter = auto_filter
89
100
  @tables = (tables || []).freeze
@@ -101,6 +112,11 @@ module Xlsxrb
101
112
  @col_breaks = (col_breaks || []).freeze
102
113
  @protected_ranges = (protected_ranges || unmapped_data[:protected_ranges] || unmapped_data.dig(:facade, :protected_ranges) || []).freeze
103
114
  @cell_watches = (cell_watches || unmapped_data[:cell_watches] || unmapped_data.dig(:facade, :cell_watches) || []).freeze
115
+ @ignored_errors = (ignored_errors || unmapped_data[:ignored_errors] || unmapped_data.dig(:facade, :ignored_errors) || []).freeze
116
+ @phonetic_properties = (phonetic_properties || unmapped_data[:phonetic_properties] || unmapped_data.dig(:facade, :phonetic_properties))&.freeze
117
+ @selection = selection&.freeze
118
+ @sheet_calc_pr = (sheet_calc_pr || unmapped_data[:sheet_calc_pr] || unmapped_data.dig(:facade, :sheet_calc_pr) || (@sheet_properties[:full_calc_on_load] ? { full_calc_on_load: true } : nil))&.freeze
119
+ @background_image = (background_image || unmapped_data[:background_image] || unmapped_data.dig(:facade, :background_image))&.freeze
104
120
  end
105
121
 
106
122
  # Returns whether trailing empty rows are omitted during iteration.
@@ -152,6 +168,68 @@ module Xlsxrb
152
168
  @dimension ||= compute_dimension
153
169
  end
154
170
 
171
+ # Returns the worksheet tab color specification, or nil if not set.
172
+ #
173
+ # @return [String, Hash, nil] RGB hex string (e.g. "FFFF0000") or color hash (e.g. { theme: 3, tint: -0.5 }).
174
+ # @api public
175
+ #: () -> (String | Hash[Symbol, untyped])?
176
+ def tab_color
177
+ props = sheet_properties
178
+ return props[:tab_color] if props[:tab_color]
179
+ return { theme: props[:tab_color_theme], tint: props[:tab_color_tint] } if props[:tab_color_theme]
180
+ return { indexed: props[:tab_color_indexed] } if props[:tab_color_indexed]
181
+ return { auto: true } if props[:tab_color_auto]
182
+
183
+ nil
184
+ end
185
+
186
+ # Returns the internal VBA or application code name of the worksheet, or nil if not specified.
187
+ #
188
+ # @return [String, nil]
189
+ # @api public
190
+ #: () -> String?
191
+ def code_name
192
+ sheet_properties[:code_name]
193
+ end
194
+
195
+ # Returns whether fit-to-page is enabled for page setup on this worksheet.
196
+ #
197
+ # @return [Boolean]
198
+ # @api public
199
+ #: () -> bool
200
+ def fit_to_page?
201
+ sheet_properties[:fit_to_page] ? true : false
202
+ end
203
+ alias fit_to_page fit_to_page?
204
+
205
+ # Returns whether full calculation on load is enabled on this worksheet.
206
+ #
207
+ # @return [Boolean]
208
+ # @api public
209
+ #: () -> bool
210
+ def full_calc_on_load?
211
+ (@sheet_calc_pr && @sheet_calc_pr[:full_calc_on_load]) || sheet_properties[:full_calc_on_load] ? true : false
212
+ end
213
+ alias full_calc_on_load full_calc_on_load?
214
+
215
+ # Returns the outline and grouping properties of the worksheet, or an empty hash if none configured.
216
+ #
217
+ # @return [Hash]
218
+ # @api public
219
+ #: () -> Hash[Symbol, untyped]
220
+ def outline_properties
221
+ props = sheet_properties
222
+ return props[:outline_properties] if props[:outline_properties]
223
+
224
+ res = {}
225
+ res[:summary_below] = props[:summary_below] unless props[:summary_below].nil?
226
+ res[:summary_right] = props[:summary_right] unless props[:summary_right].nil?
227
+ res[:show_outline_symbols] = props[:show_outline_symbols] unless props[:show_outline_symbols].nil?
228
+ res[:apply_styles] = props[:apply_styles] unless props[:apply_styles].nil?
229
+ res
230
+ end
231
+ alias outline_pr outline_properties
232
+
155
233
  # Iterate over rows in the worksheet.
156
234
  #
157
235
  # @example
@@ -362,6 +440,15 @@ module Xlsxrb
362
440
  @state == :visible
363
441
  end
364
442
 
443
+ # Returns whether the worksheet is very hidden (:very_hidden).
444
+ #
445
+ # @return [Boolean]
446
+ # @api public
447
+ #: () -> bool
448
+ def very_hidden?
449
+ @state == :very_hidden
450
+ end
451
+
365
452
  # Returns hyperlink metadata for the given cell reference, or nil.
366
453
  #
367
454
  # @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
@@ -406,6 +493,34 @@ module Xlsxrb
406
493
  end
407
494
  end
408
495
 
496
+ # Returns threaded comments for the given cell reference, or empty array.
497
+ #
498
+ # @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
499
+ # @param col [Integer, nil] Optional 0-based column index.
500
+ # @return [Array<Hash[Symbol, untyped]>]
501
+ # @api public
502
+ #: (String | Symbol | Integer ref_or_row, ?Integer? col) -> Array[Hash[Symbol, untyped]]
503
+ def threaded_comment(ref_or_row, col = nil)
504
+ ref = if col
505
+ "#{Cell.column_letter(col)}#{ref_or_row.to_i + 1}"
506
+ else
507
+ ref_or_row.to_s.upcase
508
+ end
509
+ threaded_comments_by_ref[ref] || []
510
+ end
511
+
512
+ # Returns threaded comments indexed by cell reference.
513
+ #
514
+ # @return [Hash<String, Array<Hash[Symbol, untyped]>>]
515
+ # @api public
516
+ #: () -> Hash[String, Array[Hash[Symbol, untyped]]]
517
+ def threaded_comments_by_ref
518
+ @threaded_comments_by_ref ||= @threaded_comments.each_with_object({}) do |c, acc|
519
+ r = c[:ref] || c[:cell]
520
+ (acc[r.to_s.upcase] ||= []) << c if r
521
+ end
522
+ end
523
+
409
524
  # Access a cell by reference or 0-based coordinates.
410
525
  #
411
526
  # @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
@@ -539,6 +654,26 @@ module Xlsxrb
539
654
  new_col_breaks = changes.key?(:col_breaks) ? changes[:col_breaks] : col_breaks
540
655
  new_protected_ranges = changes.key?(:protected_ranges) ? changes[:protected_ranges] : protected_ranges
541
656
  new_cell_watches = changes.key?(:cell_watches) ? changes[:cell_watches] : cell_watches
657
+ new_ignored_errors = changes.key?(:ignored_errors) ? changes[:ignored_errors] : ignored_errors
658
+ new_phonetic_properties = if changes.key?(:phonetic_properties)
659
+ changes[:phonetic_properties]
660
+ elsif changes.key?(:phonetic_pr)
661
+ changes[:phonetic_pr]
662
+ else
663
+ phonetic_properties
664
+ end
665
+ new_selection = changes.key?(:selection) ? changes[:selection] : selection
666
+ new_sheet_calc_pr = changes.key?(:sheet_calc_pr) ? changes[:sheet_calc_pr] : sheet_calc_pr
667
+ new_background_image = if changes.key?(:background_image)
668
+ changes[:background_image]
669
+ elsif changes.key?(:background_picture)
670
+ changes[:background_picture]
671
+ elsif changes.key?(:picture)
672
+ changes[:picture]
673
+ else
674
+ background_image
675
+ end
676
+ new_threaded_comments = changes.key?(:threaded_comments) ? changes[:threaded_comments] : threaded_comments
542
677
 
543
678
  self.class.new(
544
679
  name: new_name,
@@ -575,7 +710,13 @@ module Xlsxrb
575
710
  row_breaks: new_row_breaks,
576
711
  col_breaks: new_col_breaks,
577
712
  protected_ranges: new_protected_ranges,
578
- cell_watches: new_cell_watches
713
+ cell_watches: new_cell_watches,
714
+ ignored_errors: new_ignored_errors,
715
+ phonetic_properties: new_phonetic_properties,
716
+ selection: new_selection,
717
+ sheet_calc_pr: new_sheet_calc_pr,
718
+ background_image: new_background_image,
719
+ threaded_comments: new_threaded_comments
579
720
  )
580
721
  end
581
722
 
@@ -595,6 +736,7 @@ module Xlsxrb
595
736
  state: state,
596
737
  hyperlinks: hyperlinks,
597
738
  comments: comments,
739
+ threaded_comments: threaded_comments,
598
740
  styles: styles,
599
741
  date1904: date1904,
600
742
  dimension: dimension,
@@ -616,7 +758,19 @@ module Xlsxrb
616
758
  sheet_properties: sheet_properties,
617
759
  sheet_format: sheet_format,
618
760
  row_breaks: row_breaks,
619
- col_breaks: col_breaks
761
+ col_breaks: col_breaks,
762
+ protected_ranges: protected_ranges,
763
+ cell_watches: cell_watches,
764
+ ignored_errors: ignored_errors,
765
+ phonetic_properties: phonetic_properties,
766
+ phonetic_pr: phonetic_properties,
767
+ selection: selection,
768
+ sheet_calc_pr: sheet_calc_pr,
769
+ calc_pr: sheet_calc_pr,
770
+ code_name: code_name,
771
+ background_image: background_image,
772
+ background_picture: background_image,
773
+ picture: background_image
620
774
  }
621
775
  end
622
776
 
@@ -628,13 +782,18 @@ module Xlsxrb
628
782
  name == other.name && rows == other.rows && columns == other.columns && charts == other.charts &&
629
783
  conditional_formatting == other.conditional_formatting && data_validations == other.data_validations &&
630
784
  state == other.state && hyperlinks == other.hyperlinks && comments == other.comments &&
785
+ threaded_comments == other.threaded_comments &&
631
786
  merged_cells == other.merged_cells && auto_filter == other.auto_filter &&
632
787
  tables == other.tables && shapes == other.shapes && freeze_pane == other.freeze_pane &&
633
788
  page_setup == other.page_setup && page_margins == other.page_margins &&
634
789
  header_footer == other.header_footer && print_options == other.print_options &&
635
790
  sheet_protection == other.sheet_protection && sheet_view == other.sheet_view &&
636
791
  sheet_properties == other.sheet_properties && sheet_format == other.sheet_format &&
637
- row_breaks == other.row_breaks && col_breaks == other.col_breaks
792
+ row_breaks == other.row_breaks && col_breaks == other.col_breaks &&
793
+ protected_ranges == other.protected_ranges && cell_watches == other.cell_watches &&
794
+ ignored_errors == other.ignored_errors && phonetic_properties == other.phonetic_properties &&
795
+ selection == other.selection && sheet_calc_pr == other.sheet_calc_pr &&
796
+ background_image == other.background_image
638
797
  end
639
798
  alias eql? ==
640
799
 
@@ -642,9 +801,10 @@ module Xlsxrb
642
801
  def hash
643
802
  [
644
803
  self.class, name, rows, columns, charts, conditional_formatting, data_validations,
645
- state, hyperlinks, comments, merged_cells, auto_filter, tables, shapes, freeze_pane,
804
+ state, hyperlinks, comments, threaded_comments, merged_cells, auto_filter, tables, shapes, freeze_pane,
646
805
  page_setup, page_margins, header_footer, print_options, sheet_protection, sheet_view,
647
- sheet_properties, sheet_format, row_breaks, col_breaks
806
+ sheet_properties, sheet_format, row_breaks, col_breaks, protected_ranges,
807
+ cell_watches, ignored_errors, phonetic_properties, selection, sheet_calc_pr, background_image
648
808
  ].hash
649
809
  end
650
810
 
@@ -658,7 +818,7 @@ module Xlsxrb
658
818
  end
659
819
  alias to_worksheet load
660
820
 
661
- FORBIDDEN_NAME_CHARS = %r{[\\/?*\[\]]}
821
+ FORBIDDEN_NAME_CHARS = %r{[\\/?*\[\]:]|[\x00-\x08\x0B\x0C\x0E-\x1F]}
662
822
 
663
823
  # Returns whether the worksheet name is valid according to OOXML specifications (1..31 chars, no forbidden chars).
664
824
  #
@@ -668,7 +828,7 @@ module Xlsxrb
668
828
  def self.valid_name?(name)
669
829
  case name
670
830
  when String
671
- !name.empty? && name.size <= 31 && !name.match?(FORBIDDEN_NAME_CHARS)
831
+ !name.empty? && name.size <= 31 && !name.match?(FORBIDDEN_NAME_CHARS) && !name.start_with?("'") && !name.end_with?("'") && !name.casecmp?("History")
672
832
  else
673
833
  false
674
834
  end
@@ -686,7 +846,9 @@ module Xlsxrb
686
846
  errs << "worksheet name must be a non-empty String (got #{name.inspect})"
687
847
  else
688
848
  errs << "worksheet name cannot exceed 31 characters (got #{name.size})" if name.size > 31
689
- errs << "worksheet name cannot contain \\, /, ?, *, [, or ]" if name.match?(FORBIDDEN_NAME_CHARS)
849
+ errs << "worksheet name cannot contain \\, /, ?, *, [, ], or :" if name.match?(FORBIDDEN_NAME_CHARS)
850
+ errs << "worksheet name cannot start or end with an apostrophe" if name.start_with?("'") || name.end_with?("'")
851
+ errs << "worksheet name 'History' is reserved by Excel" if name.casecmp?("History")
690
852
  end
691
853
  errs << "rows must be an Array (got #{rows.class})" unless rows.is_a?(Array)
692
854
  if rows.is_a?(Array)
@@ -100,9 +100,16 @@ module Xlsxrb
100
100
  raise Xlsxrb::Error, "CFB file is too small" if @data.bytesize < 512
101
101
 
102
102
  _, major_ver = @data[0x18, 4].unpack("v2")
103
+ raise Xlsxrb::Error, "Unsupported CFB major version: #{major_ver}" unless [3, 4].include?(major_ver)
104
+
103
105
  @major_version = major_ver
104
106
  @sector_shift = @data[0x1E, 2].unpack1("v")
105
107
  @mini_sector_shift = @data[0x20, 2].unpack1("v")
108
+
109
+ expected_shift = @major_version == 4 ? 12 : 9
110
+ raise Xlsxrb::Error, "Invalid CFB sector shift in header: #{@sector_shift}" unless @sector_shift == expected_shift
111
+ raise Xlsxrb::Error, "Invalid CFB mini sector shift in header: #{@mini_sector_shift}" unless @mini_sector_shift == 6
112
+
106
113
  @sector_size = 1 << @sector_shift
107
114
  @mini_sector_size = 1 << @mini_sector_shift
108
115
 
@@ -123,6 +130,9 @@ module Xlsxrb
123
130
  end
124
131
 
125
132
  def read_sector(sector_id)
133
+ max_sectors = (@data.bytesize - 512) / @sector_size
134
+ return "".b if sector_id.negative? || sector_id >= max_sectors
135
+
126
136
  offset = sector_offset(sector_id)
127
137
  @data[offset, @sector_size] || "".b
128
138
  end
@@ -24,12 +24,15 @@ module Xlsxrb
24
24
  @runs = []
25
25
  @current_font = {}
26
26
  @has_runs = false
27
+ @inside_rph = false
27
28
  end
28
29
 
29
30
  def start_element(_uri, local_name, qname, attributes)
30
31
  name = element_name(local_name, qname)
31
32
 
32
33
  case name
34
+ when "rPh"
35
+ @inside_rph = true
33
36
  when "si"
34
37
  @inside_si = true
35
38
  @text_buffer = +""
@@ -74,7 +77,7 @@ module Xlsxrb
74
77
  when "scheme"
75
78
  @current_font[:scheme] = attributes["val"] if @inside_rpr
76
79
  when "t"
77
- @inside_t = @inside_si
80
+ @inside_t = @inside_si && !@inside_rph
78
81
  @text_buffer = +"" if @inside_r
79
82
  end
80
83
  end
@@ -87,12 +90,16 @@ module Xlsxrb
87
90
  name = element_name(local_name, qname)
88
91
 
89
92
  case name
93
+ when "rPh"
94
+ @inside_rph = false
90
95
  when "t"
91
96
  @inside_t = false
92
97
  when "rPr"
93
98
  @inside_rpr = false
94
99
  when "r"
95
- run = { text: @text_buffer.dup }
100
+ decoded_text = XmlBuilder.unescape(@text_buffer)
101
+ decoded_text = decoded_text.gsub("\r\n", "\n").tr("\r", "\n") if decoded_text.include?("\r")
102
+ run = { text: decoded_text }
96
103
  run[:font] = @current_font.dup unless @current_font.empty?
97
104
  @runs << run
98
105
  @inside_r = false
@@ -100,7 +107,9 @@ module Xlsxrb
100
107
  @strings << if @has_runs
101
108
  Xlsxrb::Elements::RichText.new(runs: @runs)
102
109
  else
103
- @text_buffer.dup
110
+ decoded_text = XmlBuilder.unescape(@text_buffer)
111
+ decoded_text = decoded_text.gsub("\r\n", "\n").tr("\r", "\n") if decoded_text.include?("\r")
112
+ decoded_text
104
113
  end
105
114
  @inside_si = false
106
115
  @text_buffer = +""
@@ -141,22 +150,28 @@ module Xlsxrb
141
150
  @value_buffer = +""
142
151
  @inline_text_buffer = +""
143
152
  @formula_buffer = +""
153
+ @shared_formula_masters = {}
144
154
  @is_runs = []
145
155
  @is_has_runs = false
146
156
  @is_current_font = {}
147
157
  @is_run_text = +""
158
+ @inside_rph = false
159
+ @has_inline_text = false
148
160
  end
149
161
 
150
162
  def start_element(_uri, local_name, qname, attributes)
151
163
  name = element_name(local_name, qname)
152
164
 
153
165
  case name
166
+ when "rPh"
167
+ @inside_rph = true
154
168
  when "row"
155
169
  parse_row_attributes(attributes)
156
170
  when "c"
157
171
  @current_cell_ref = attributes["r"]
158
172
  @current_cell_type = attributes["t"]
159
173
  @current_cell_ph = %w[1 true].include?(attributes["ph"])
174
+ @has_inline_text = false
160
175
  @value_buffer = +""
161
176
  @inline_text_buffer = +""
162
177
  @formula_buffer = +""
@@ -188,6 +203,7 @@ module Xlsxrb
188
203
  @formula_r1 = attributes["r1"] if attributes["r1"]
189
204
  @formula_r2 = attributes["r2"] if attributes["r2"]
190
205
  when "is"
206
+ @has_inline_text = true
191
207
  @inside_is = true if @current_cell_type == "inlineStr"
192
208
  when "r"
193
209
  if @inside_is
@@ -231,11 +247,15 @@ module Xlsxrb
231
247
  when "scheme"
232
248
  @is_current_font[:scheme] = attributes["val"] if @inside_is_rpr
233
249
  when "t"
234
- if @inside_is_r
235
- @is_run_text = +""
236
- @inside_inline_text = true
237
- elsif @inside_is || (@current_cell_type == "inlineStr" && !@current_cell_ref.nil?)
238
- @inside_inline_text = true
250
+ unless @inside_rph
251
+ if @inside_is_r
252
+ @is_run_text = +""
253
+ @inside_inline_text = true
254
+ @has_inline_text = true
255
+ elsif @inside_is || (@current_cell_type == "inlineStr" && !@current_cell_ref.nil?)
256
+ @inside_inline_text = true
257
+ @has_inline_text = true
258
+ end
239
259
  end
240
260
  end
241
261
  end
@@ -256,6 +276,8 @@ module Xlsxrb
256
276
  name = element_name(local_name, qname)
257
277
 
258
278
  case name
279
+ when "rPh"
280
+ @inside_rph = false
259
281
  when "v"
260
282
  @inside_value = false
261
283
  when "f"
@@ -281,6 +303,8 @@ module Xlsxrb
281
303
  @value_buffer = +""
282
304
  @inline_text_buffer = +""
283
305
  @formula_buffer = +""
306
+ when "worksheet"
307
+ expand_shared_formulas!
284
308
  end
285
309
  end
286
310
 
@@ -309,10 +333,25 @@ module Xlsxrb
309
333
  return if @current_cell_ref.nil?
310
334
 
311
335
  unless @formula_buffer.empty? && @formula_type.nil?
312
- cached = @value_buffer.empty? ? nil : @value_buffer.dup
336
+ cached = if @value_buffer.empty?
337
+ nil
338
+ elsif @current_cell_type == "b"
339
+ v = @value_buffer.strip
340
+ v == "1" || v.casecmp?("true")
341
+ else
342
+ @value_buffer.dup
343
+ end
313
344
  f_type = { "shared" => :shared, "array" => :array, "dataTable" => :data_table }[@formula_type]
345
+ expr = @formula_buffer.dup
346
+ if f_type == :shared && @formula_si
347
+ if !expr.empty?
348
+ @shared_formula_masters[@formula_si] = { cell: @current_cell_ref, expression: expr }
349
+ elsif (master = @shared_formula_masters[@formula_si])
350
+ expr = Xlsxrb::Utils.expand_formula(master[:expression], from_ref: master[:cell], to_ref: @current_cell_ref)
351
+ end
352
+ end
314
353
  @cells[@current_cell_ref] = Xlsxrb::Elements::Formula.new(
315
- expression: @formula_buffer.dup,
354
+ expression: expr,
316
355
  cached_value: cached,
317
356
  type: f_type,
318
357
  ref: @formula_ref,
@@ -328,39 +367,39 @@ module Xlsxrb
328
367
  return
329
368
  end
330
369
 
331
- case @current_cell_type
332
- when "inlineStr"
333
- @cells[@current_cell_ref] = if @is_has_runs
334
- Xlsxrb::Elements::RichText.new(runs: @is_runs.map(&:dup))
335
- else
336
- @inline_text_buffer.dup
337
- end
338
- when "s"
339
- index = @value_buffer.to_i
340
- @cells[@current_cell_ref] = @shared_strings[index] || ""
341
- when "e"
342
- code = @value_buffer.dup
343
- @cells[@current_cell_ref] = if Xlsxrb::Elements::VALID_ERROR_CODES.include?(code)
344
- Xlsxrb::Elements::CellError.new(code:)
345
- else
346
- code
347
- end
348
- when "b"
349
- @cells[@current_cell_ref] = @value_buffer.strip == "1"
350
- when nil, "", "n"
351
- return if @value_buffer.empty?
370
+ if @current_cell_type == "inlineStr" && @is_has_runs
371
+ @cells[@current_cell_ref] = Xlsxrb::Elements::RichText.new(runs: @is_runs.map(&:dup))
372
+ return
373
+ end
352
374
 
353
- raw = @value_buffer.dup
354
- @cells[@current_cell_ref] = numeric_value(raw)
375
+ v = if @current_cell_type == "inlineStr"
376
+ @has_inline_text ? @inline_text_buffer : nil
377
+ else
378
+ @value_buffer.empty? ? nil : @value_buffer
379
+ end
380
+
381
+ val = Xlsxrb::Ooxml::WorksheetParser.resolve_cell_value(@current_cell_type, v, nil, @shared_strings)
382
+ return if val.nil? && (@current_cell_type.nil? || @current_cell_type == "" || @current_cell_type == "n" || @current_cell_type == "s" || @current_cell_type == "inlineStr")
383
+
384
+ @cells[@current_cell_ref] = if @current_cell_type == "e" && val && Xlsxrb::Elements::VALID_ERROR_CODES.include?(val)
385
+ Xlsxrb::Elements::CellError.new(code: val)
386
+ else
387
+ val
388
+ end
389
+ end
390
+
391
+ def expand_shared_formulas!
392
+ @cells.each do |ref, val|
393
+ next unless val.is_a?(Xlsxrb::Elements::Formula) && val.type == :shared && (val.expression.nil? || val.expression.empty?)
394
+ next unless val.shared_index && (master = @shared_formula_masters[val.shared_index])
395
+
396
+ expanded = Xlsxrb::Utils.expand_formula(master[:expression], from_ref: master[:cell], to_ref: ref)
397
+ @cells[ref] = val.with(expression: expanded)
355
398
  end
356
399
  end
357
400
 
358
401
  def numeric_value(raw)
359
- if raw.include?(".")
360
- raw.to_f
361
- else
362
- raw.to_i
363
- end
402
+ Xlsxrb::Ooxml::WorksheetParser.numeric_cell_value(raw)
364
403
  end
365
404
 
366
405
  def element_name(local_name, qname)