xlsxrb 0.1.14 → 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.
@@ -47,7 +47,7 @@ module Xlsxrb
47
47
  # split_pane: { x_split:, y_split:, top_left_cell: }
48
48
  # selection: { active_cell:, sqref:, pane: }
49
49
  # sheet_view: Hash of sheet view properties
50
- def start(columns: [], sheet_properties: nil, freeze_pane: nil, split_pane: nil, selection: nil, sheet_view: nil)
50
+ def start(columns: [], sheet_properties: nil, freeze_pane: nil, split_pane: nil, selection: nil, sheet_view: nil, sheet_format: nil)
51
51
  return if @started
52
52
 
53
53
  @started = true
@@ -75,6 +75,7 @@ module Xlsxrb
75
75
 
76
76
  write_sheet_properties(sheet_properties) if sheet_properties && !sheet_properties.empty?
77
77
  write_sheet_views(freeze_pane: freeze_pane, split_pane: split_pane, selection: selection, sheet_view: sheet_view) if freeze_pane || split_pane || selection || (sheet_view && !sheet_view.empty?)
78
+ write_sheet_format_pr(sheet_format) if sheet_format && !sheet_format.empty?
78
79
  write_columns(columns) unless columns.empty?
79
80
 
80
81
  @builder.open_tag("sheetData")
@@ -88,7 +89,7 @@ module Xlsxrb
88
89
  row_num_str = row_num.to_s
89
90
  buf = @row_buffer ||= String.new(capacity: 65_536)
90
91
  buf << '<row r="' << row_num_str << '"'
91
- buf << ' s="' << attrs[:style_index].to_s << '" customFormat="1"' if attrs[:style_index]
92
+ buf << ' s="' << attrs[:style_index].to_s << '" customFormat="1"' if attrs[:style_index].is_a?(Integer)
92
93
  if attrs[:height]
93
94
  buf << ' ht="' << attrs[:height].to_s << '" customHeight="1"'
94
95
  elsif attrs[:custom_height]
@@ -128,6 +129,7 @@ module Xlsxrb
128
129
 
129
130
  col_style = @column_styles && col_idx ? @column_styles[col_idx] : nil
130
131
  style_id ||= attrs[:style_index] || col_style
132
+ style_id = nil unless style_id.is_a?(Integer)
131
133
 
132
134
  # Fast path for common unstyled cells
133
135
  if !formula && !style_id && !cell_type_val
@@ -425,8 +427,7 @@ module Xlsxrb
425
427
  end
426
428
 
427
429
  is_styles_collection = styles && (styles.is_a?(Array) || styles.is_a?(Hash))
428
- single_style_id = nil
429
- single_style_id = style_map[styles] if styles && style_map && !is_styles_collection
430
+ single_style_id = resolve_style_id(styles, style_map) unless is_styles_collection
430
431
 
431
432
  buf << '<row r="' << row_num_str << '"'
432
433
  if attrs
@@ -444,7 +445,7 @@ module Xlsxrb
444
445
  end
445
446
 
446
447
  row_style = attrs ? attrs[:style_index] : nil
447
- row_style_id = row_style && style_map ? (style_map[row_style] || style_map[row_style.to_s] || row_style) : row_style
448
+ row_style_id = resolve_style_id(row_style, style_map)
448
449
 
449
450
  col_index = 0
450
451
  while col_index < max_len
@@ -456,20 +457,12 @@ module Xlsxrb
456
457
  else
457
458
  styles[col_index]
458
459
  end
459
- style_id = if style_map && style_name
460
- style_map[style_name] || style_map[style_name.to_s] || style_name
461
- else
462
- style_name
463
- end
460
+ style_id = resolve_style_id(style_name, style_map)
464
461
  end
465
462
 
466
463
  if style_id.nil?
467
464
  col_style = @column_styles ? @column_styles[col_index] : nil
468
- col_style_id = if col_style && style_map
469
- style_map[col_style] || style_map[col_style.to_s] || col_style
470
- else
471
- col_style
472
- end
465
+ col_style_id = resolve_style_id(col_style, style_map)
473
466
  style_id = row_style_id || col_style_id
474
467
  end
475
468
 
@@ -638,7 +631,7 @@ module Xlsxrb
638
631
  end
639
632
  end
640
633
  f_attrs << ' ca="1"' if formula_ca
641
- buf << "<f#{f_attrs}>" << escape_xml(formula_expr) << "</f>"
634
+ buf << "><f#{f_attrs}>" << escape_xml(formula_expr) << "</f>"
642
635
  if xml_val
643
636
  buf << "<v>" << xml_val.to_s << "</v></c>"
644
637
  else
@@ -662,12 +655,12 @@ module Xlsxrb
662
655
 
663
656
  # Write the worksheet footer. Call once after all rows.
664
657
  # Options for post-sheetData elements (in OOXML order):
665
- def finish(drawing_rid: nil, sheet_protection: nil, auto_filter: nil,
658
+ def finish(drawing_rid: nil, sheet_protection: nil, protected_ranges: nil, auto_filter: nil,
666
659
  filter_columns: nil, sort_state: nil, merge_cells: nil,
667
660
  conditional_formats: nil, data_validations: nil,
668
661
  hyperlinks: nil, print_options: nil, page_margins: nil,
669
662
  page_setup: nil, header_footer: nil, row_breaks: nil,
670
- col_breaks: nil, tables: nil, table_start_rid: nil,
663
+ col_breaks: nil, cell_watches: nil, tables: nil, table_start_rid: nil,
671
664
  legacy_drawing_rid: nil, sparkline_groups: nil)
672
665
  return if @finished
673
666
 
@@ -681,6 +674,7 @@ module Xlsxrb
681
674
 
682
675
  # Elements must appear in OOXML specification order after sheetData
683
676
  write_sheet_protection(sheet_protection) if sheet_protection
677
+ write_protected_ranges(protected_ranges) if protected_ranges && !protected_ranges.empty?
684
678
  write_auto_filter(auto_filter, filter_columns, sort_state) if auto_filter
685
679
  write_merge_cells(merge_cells) if merge_cells && !merge_cells.empty?
686
680
  write_conditional_formatting(conditional_formats) if conditional_formats && !conditional_formats.empty?
@@ -692,6 +686,7 @@ module Xlsxrb
692
686
  write_header_footer(header_footer) if header_footer && !header_footer.empty?
693
687
  write_row_breaks(row_breaks) if row_breaks && !row_breaks.empty?
694
688
  write_col_breaks(col_breaks) if col_breaks && !col_breaks.empty?
689
+ write_cell_watches(cell_watches) if cell_watches && !cell_watches.empty?
695
690
  @builder.empty_tag("drawing", { "r:id": drawing_rid }) if drawing_rid
696
691
  @builder.empty_tag("legacyDrawing", { "r:id": legacy_drawing_rid }) if legacy_drawing_rid
697
692
  write_table_parts(tables, table_start_rid) if tables && !tables.empty?
@@ -701,6 +696,17 @@ module Xlsxrb
701
696
 
702
697
  private
703
698
 
699
+ def resolve_style_id(style, style_map)
700
+ return nil if style.nil?
701
+
702
+ if style_map
703
+ mapped = style_map[style] || style_map[style.to_s]
704
+ return mapped if mapped
705
+ end
706
+
707
+ style.is_a?(Integer) ? style : nil
708
+ end
709
+
704
710
  # --- Pre-sheetData elements ---
705
711
 
706
712
  def write_sheet_properties(props)
@@ -770,6 +776,22 @@ module Xlsxrb
770
776
  @builder.close_tag("sheetViews")
771
777
  end
772
778
 
779
+ def write_sheet_format_pr(fmt)
780
+ return if fmt.nil? || fmt.empty?
781
+
782
+ attrs = {}
783
+ attrs[:defaultRowHeight] = fmt[:default_row_height].to_s if fmt[:default_row_height]
784
+ attrs[:defaultColWidth] = fmt[:default_col_width].to_s if fmt[:default_col_width]
785
+ attrs[:baseColWidth] = fmt[:base_col_width].to_s if fmt[:base_col_width]
786
+ attrs[:outlineLevelRow] = fmt[:outline_level_row].to_s if fmt[:outline_level_row]
787
+ attrs[:outlineLevelCol] = fmt[:outline_level_col].to_s if fmt[:outline_level_col]
788
+ attrs[:customHeight] = "1" if fmt[:custom_height]
789
+ attrs[:zeroHeight] = "1" if fmt[:zero_height]
790
+ attrs[:thickTop] = "1" if fmt[:thick_top]
791
+ attrs[:thickBottom] = "1" if fmt[:thick_bottom]
792
+ @builder.empty_tag("sheetFormatPr", attrs) unless attrs.empty?
793
+ end
794
+
773
795
  # --- Post-sheetData elements (in OOXML order) ---
774
796
 
775
797
  def write_sheet_protection(opts)
@@ -798,6 +820,28 @@ module Xlsxrb
798
820
  @builder.empty_tag("sheetProtection", attrs)
799
821
  end
800
822
 
823
+ def write_protected_ranges(ranges)
824
+ @builder.open_tag("protectedRanges")
825
+ ranges.each do |pr|
826
+ attrs = { sqref: pr[:sqref].to_s, name: pr[:name].to_s }
827
+ attrs[:password] = pr[:password].to_s if pr[:password]
828
+ attrs[:algorithmName] = pr[:algorithm_name].to_s if pr[:algorithm_name]
829
+ attrs[:hashValue] = pr[:hash_value].to_s if pr[:hash_value]
830
+ attrs[:saltValue] = pr[:salt_value].to_s if pr[:salt_value]
831
+ attrs[:spinCount] = pr[:spin_count].to_s if pr[:spin_count]
832
+ attrs[:securityDescriptor] = pr[:security_descriptor].to_s if pr[:security_descriptor]
833
+ sds = pr[:security_descriptors] || []
834
+ if sds.empty?
835
+ @builder.empty_tag("protectedRange", attrs)
836
+ else
837
+ @builder.open_tag("protectedRange", attrs)
838
+ sds.each { |sd| @builder.tag("securityDescriptor") { @builder.text(sd.to_s) } }
839
+ @builder.close_tag("protectedRange")
840
+ end
841
+ end
842
+ @builder.close_tag("protectedRanges")
843
+ end
844
+
801
845
  def write_auto_filter(range, filter_columns, sort_state)
802
846
  if (filter_columns && !filter_columns.empty?) || sort_state
803
847
  @builder.open_tag("autoFilter", { ref: range })
@@ -1133,6 +1177,14 @@ module Xlsxrb
1133
1177
  @builder.close_tag("colBreaks")
1134
1178
  end
1135
1179
 
1180
+ def write_cell_watches(watches)
1181
+ @builder.open_tag("cellWatches")
1182
+ watches.each do |w|
1183
+ @builder.empty_tag("cellWatch", { r: w.to_s })
1184
+ end
1185
+ @builder.close_tag("cellWatches")
1186
+ end
1187
+
1136
1188
  def write_table_parts(tables, start_rid)
1137
1189
  @builder.open_tag("tableParts", { count: tables.size.to_s })
1138
1190
  tables.each_with_index do |_tbl, idx|
@@ -1243,7 +1295,7 @@ module Xlsxrb
1243
1295
  col_idx = cell[:column_index]
1244
1296
  col_style = @column_styles && col_idx ? @column_styles[col_idx] : nil
1245
1297
  style_id = cell[:style_index] || col_style
1246
- attrs[:s] = style_id.to_s if style_id
1298
+ attrs[:s] = style_id.to_s if style_id.is_a?(Integer)
1247
1299
 
1248
1300
  if value.nil? && formula.nil?
1249
1301
  @builder.empty_tag("c", attrs)
@@ -715,7 +715,7 @@ module Xlsxrb
715
715
  "<vt:i4>#{prop[:value]}</vt:i4>"
716
716
  when :float, :r8
717
717
  "<vt:r8>#{prop[:value]}</vt:r8>"
718
- when :bool
718
+ when :bool, :boolean
719
719
  "<vt:bool>#{prop[:value] ? "true" : "false"}</vt:bool>"
720
720
  when :date, :filetime
721
721
  "<vt:filetime>#{prop[:value]}</vt:filetime>"
@@ -118,14 +118,26 @@ module Xlsxrb
118
118
  str.match?(ESCAPE_RE) ? str.gsub(ESCAPE_RE, ESCAPE_MAP) : str
119
119
  end
120
120
 
121
- # Unescapes predefined XML entities in a string in a single pass.
121
+ NUM_ENTITY_RE = /&#(?:(\d+)|x([0-9a-fA-F]+));/
122
+ private_constant :NUM_ENTITY_RE
123
+
124
+ # Unescapes predefined and numeric/hex XML entities in a string.
122
125
  #
123
126
  # @param value [Object]
124
127
  # @return [String]
125
128
  #: (untyped value) -> String
126
129
  def self.unescape(value)
127
130
  str = value.to_s
128
- str.include?("&") ? str.gsub(UNESCAPE_RE, UNESCAPE_MAP) : str
131
+ return str unless str.include?("&")
132
+
133
+ res = str.gsub(UNESCAPE_RE, UNESCAPE_MAP)
134
+ res.gsub(NUM_ENTITY_RE) do |match|
135
+ dec = Regexp.last_match(1)
136
+ codepoint = dec ? Integer(dec) : Integer(Regexp.last_match(2), 16)
137
+ codepoint.chr(Encoding::UTF_8)
138
+ rescue RangeError
139
+ match
140
+ end
129
141
  end
130
142
 
131
143
  private
@@ -397,7 +397,7 @@ module Xlsxrb
397
397
  cd_catalog = parse_catalog_from_central_directory(io)
398
398
  return cd_catalog if cd_catalog && !cd_catalog.empty?
399
399
 
400
- io.seek(-4, IO::SEEK_CUR) if io.respond_to?(:seek)
400
+ io.seek(0, IO::SEEK_SET) if io.respond_to?(:seek)
401
401
  parse_catalog_from_local_headers(io)
402
402
  end
403
403
 
@@ -437,7 +437,10 @@ module Xlsxrb
437
437
 
438
438
  _v_made, _v_need, _gp, method, _time, _date, crc, csize, usize, nlen, elen, clen, _dnum, _iattr, _eattr, offset = cd_header.unpack("vvvvvvVVVvvvvvVV")
439
439
  entry_name = io.read(nlen).force_encoding("UTF-8")
440
- io.read(elen + clen)
440
+ extra_data = elen.positive? ? io.read(elen) : nil
441
+ io.read(clen) if clen.positive?
442
+
443
+ usize, csize, offset = parse_zip64_extra(extra_data, usize, csize, offset) if extra_data && (usize == 0xFFFF_FFFF || csize == 0xFFFF_FFFF || offset == 0xFFFF_FFFF)
441
444
 
442
445
  norm_name = normalize_entry_name(entry_name)
443
446
  next if norm_name.end_with?("/")
@@ -496,7 +499,8 @@ module Xlsxrb
496
499
  extra_len = header[24, 2].unpack1("v")
497
500
 
498
501
  entry_name = io.read(name_len).force_encoding("UTF-8")
499
- io.read(extra_len)
502
+ extra_data = extra_len.positive? ? io.read(extra_len) : nil
503
+ uncompressed_size, compressed_size, = parse_zip64_extra(extra_data, uncompressed_size, compressed_size, 0) if extra_data && (uncompressed_size == 0xFFFF_FFFF || compressed_size == 0xFFFF_FFFF)
500
504
  data_offset = io.pos
501
505
 
502
506
  norm_name = normalize_entry_name(entry_name)
@@ -645,6 +649,34 @@ module Xlsxrb
645
649
  data.force_encoding(Encoding::UTF_8)
646
650
  end
647
651
  end
652
+
653
+ #: (String extra_data, Integer usize, Integer csize, Integer offset) -> [Integer, Integer, Integer]
654
+ def parse_zip64_extra(extra_data, usize, csize, offset)
655
+ pos = 0
656
+ len = extra_data.bytesize
657
+ while pos + 4 <= len
658
+ header_id, data_size = extra_data.byteslice(pos, 4).unpack("vv")
659
+ pos += 4
660
+ if header_id == 0x0001
661
+ field_data = extra_data.byteslice(pos, data_size)
662
+ if field_data
663
+ fpos = 0
664
+ if usize == 0xFFFF_FFFF && fpos + 8 <= field_data.bytesize
665
+ usize = field_data.byteslice(fpos, 8).unpack1("Q<")
666
+ fpos += 8
667
+ end
668
+ if csize == 0xFFFF_FFFF && fpos + 8 <= field_data.bytesize
669
+ csize = field_data.byteslice(fpos, 8).unpack1("Q<")
670
+ fpos += 8
671
+ end
672
+ offset = field_data.byteslice(fpos, 8).unpack1("Q<") if offset == 0xFFFF_FFFF && fpos + 8 <= field_data.bytesize
673
+ end
674
+ break
675
+ end
676
+ pos += data_size
677
+ end
678
+ [usize, csize, offset]
679
+ end
648
680
  end
649
681
  end
650
682
  end
@@ -314,7 +314,41 @@ module Xlsxrb
314
314
  # @api public
315
315
  #: () -> Elements::Worksheet
316
316
  def load
317
- Xlsxrb.send(:build_worksheet, @name, raw_sheet_xml, @shared_strings, @styles, state: @state, zip_reader: @zip_reader, entry_name: @entry_name, hyperlinks: hyperlinks, comments: comments, images: images, date1904: @date1904, dimension: dimension, trim_empty_rows: @trim_empty_rows, pad_empty_rows: @pad_empty_rows, pad_empty_cells: @pad_empty_cells)
317
+ Xlsxrb.send(
318
+ :build_worksheet,
319
+ @name,
320
+ raw_sheet_xml,
321
+ @shared_strings,
322
+ @styles,
323
+ state: @state,
324
+ zip_reader: @zip_reader,
325
+ entry_name: @entry_name,
326
+ hyperlinks: hyperlinks,
327
+ comments: comments,
328
+ images: images,
329
+ date1904: @date1904,
330
+ dimension: dimension,
331
+ trim_empty_rows: @trim_empty_rows,
332
+ pad_empty_rows: @pad_empty_rows,
333
+ pad_empty_cells: @pad_empty_cells,
334
+ merged_cells: merged_cells,
335
+ auto_filter: auto_filter,
336
+ tables: tables,
337
+ shapes: shapes,
338
+ freeze_pane: freeze_pane,
339
+ page_setup: page_setup,
340
+ page_margins: page_margins,
341
+ header_footer: header_footer,
342
+ print_options: print_options,
343
+ sheet_protection: sheet_protection,
344
+ sheet_view: sheet_view,
345
+ sheet_properties: sheet_properties,
346
+ sheet_format: sheet_format,
347
+ row_breaks: row_breaks,
348
+ col_breaks: col_breaks,
349
+ protected_ranges: protected_ranges,
350
+ cell_watches: cell_watches
351
+ )
318
352
  end
319
353
  alias to_worksheet load
320
354
 
@@ -511,6 +545,169 @@ module Xlsxrb
511
545
  end
512
546
  end
513
547
 
548
+ # Returns tables defined in this worksheet.
549
+ #
550
+ # @return [Array<Hash[Symbol, untyped]>]
551
+ # @api public
552
+ #: () -> Array[Hash[Symbol, untyped]]
553
+ def tables
554
+ @tables ||= Xlsxrb.send(:resolve_tables, zip_reader: @zip_reader, entry_name: @entry_name)
555
+ end
556
+
557
+ # Returns shapes defined in this worksheet.
558
+ #
559
+ # @return [Array<Hash[Symbol, untyped]>]
560
+ # @api public
561
+ #: () -> Array[Hash[Symbol, untyped]]
562
+ def shapes
563
+ @shapes ||= Xlsxrb.send(:resolve_shapes, zip_reader: @zip_reader, entry_name: @entry_name)
564
+ end
565
+
566
+ # Returns freeze pane settings for this worksheet, or nil if none.
567
+ #
568
+ # @return [Hash[Symbol, untyped], nil]
569
+ # @api public
570
+ #: () -> Hash[Symbol, untyped]?
571
+ def freeze_pane
572
+ sheet_view_data[:pane]
573
+ end
574
+
575
+ # Returns sheet view properties for this worksheet.
576
+ #
577
+ # @return [Hash[Symbol, untyped]]
578
+ # @api public
579
+ #: () -> Hash[Symbol, untyped]
580
+ def sheet_view
581
+ sheet_view_data[:view] || {}
582
+ end
583
+
584
+ # Returns sheet protection settings as a hash, or nil if unprotected.
585
+ #
586
+ # @return [Hash[Symbol, untyped], nil]
587
+ # @api public
588
+ #: () -> Hash[Symbol, untyped]?
589
+ def sheet_protection
590
+ @sheet_protection ||= if raw_sheet_xml.include?("<sheetProtection")
591
+ listener = Ooxml::Reader::SheetProtectionListener.new
592
+ Ooxml::XmlParser.parse(raw_sheet_xml, listener)
593
+ listener.protection
594
+ end
595
+ end
596
+
597
+ # Returns sheet-level properties (tabColor, outlinePr) for this worksheet.
598
+ #
599
+ # @return [Hash[Symbol, untyped]]
600
+ # @api public
601
+ #: () -> Hash[Symbol, untyped]
602
+ def sheet_properties
603
+ @sheet_properties ||= if raw_sheet_xml.include?("<sheetPr")
604
+ listener = Ooxml::Reader::SheetPropertiesListener.new
605
+ Ooxml::XmlParser.parse(raw_sheet_xml, listener)
606
+ listener.properties
607
+ else
608
+ {}
609
+ end
610
+ end
611
+
612
+ # Returns sheet format properties (defaultRowHeight, defaultColWidth, baseColWidth) for this worksheet.
613
+ #
614
+ # @return [Hash[Symbol, untyped]]
615
+ # @api public
616
+ #: () -> Hash[Symbol, untyped]
617
+ def sheet_format
618
+ @sheet_format ||= if raw_sheet_xml.include?("<sheetFormatPr")
619
+ listener = Ooxml::Reader::SheetFormatListener.new
620
+ Ooxml::XmlParser.parse(raw_sheet_xml, listener)
621
+ listener.properties
622
+ else
623
+ {}
624
+ end
625
+ end
626
+
627
+ # Returns page setup configurations for this worksheet.
628
+ #
629
+ # @return [Hash[Symbol, untyped]]
630
+ # @api public
631
+ #: () -> Hash[Symbol, untyped]
632
+ def page_setup
633
+ print_page_data[:page_setup] || {}
634
+ end
635
+
636
+ # Returns page margins for this worksheet, or nil if none.
637
+ #
638
+ # @return [Hash[Symbol, untyped], nil]
639
+ # @api public
640
+ #: () -> Hash[Symbol, untyped]?
641
+ def page_margins
642
+ print_page_data[:page_margins]
643
+ end
644
+
645
+ # Returns header/footer configurations for this worksheet.
646
+ #
647
+ # @return [Hash[Symbol, untyped]]
648
+ # @api public
649
+ #: () -> Hash[Symbol, untyped]
650
+ def header_footer
651
+ print_page_data[:header_footer] || {}
652
+ end
653
+
654
+ # Returns print options for this worksheet.
655
+ #
656
+ # @return [Hash[Symbol, untyped]]
657
+ # @api public
658
+ #: () -> Hash[Symbol, untyped]
659
+ def print_options
660
+ print_page_data[:print_options] || {}
661
+ end
662
+
663
+ # Returns explicit row page breaks for this worksheet.
664
+ #
665
+ # @return [Array<untyped>]
666
+ # @api public
667
+ #: () -> Array[untyped]
668
+ def row_breaks
669
+ print_page_data[:row_breaks] || []
670
+ end
671
+
672
+ # Returns explicit column page breaks for this worksheet.
673
+ #
674
+ # @return [Array<untyped>]
675
+ # @api public
676
+ #: () -> Array[untyped]
677
+ def col_breaks
678
+ print_page_data[:col_breaks] || []
679
+ end
680
+
681
+ # Returns protected ranges defined for this worksheet.
682
+ #
683
+ # @return [Array<Hash[Symbol, untyped]>]
684
+ # @api public
685
+ #: () -> Array[Hash[Symbol, untyped]]
686
+ def protected_ranges
687
+ @protected_ranges ||= if raw_sheet_xml.include?("<protectedRanges")
688
+ listener = Ooxml::Reader::ProtectedRangesListener.new
689
+ Ooxml::XmlParser.parse(raw_sheet_xml, listener)
690
+ listener.ranges
691
+ else
692
+ []
693
+ end
694
+ end
695
+
696
+ # Returns cell watch cell references defined for this worksheet.
697
+ #
698
+ # @return [Array<String>]
699
+ # @api public
700
+ #: () -> Array[String]
701
+ def cell_watches
702
+ @cell_watches ||= if raw_sheet_xml.include?("<cellWatches")
703
+ listener = Ooxml::Reader::CellWatchesListener.new
704
+ Ooxml::XmlParser.parse(raw_sheet_xml, listener)
705
+ listener.watches
706
+ else
707
+ []
708
+ end
709
+ end
710
+
514
711
  # Closes any underlying reader resources.
515
712
  #: () -> void
516
713
  def close
@@ -520,7 +717,42 @@ module Xlsxrb
520
717
 
521
718
  private
522
719
 
523
- # Returns the full raw XML string for this worksheet, loaded on demand.
720
+ #: () -> Hash[Symbol, untyped]
721
+ def sheet_view_data
722
+ @sheet_view_data ||= if raw_sheet_xml.include?("<sheetView")
723
+ listener = Ooxml::Reader::SheetViewListener.new
724
+ Ooxml::XmlParser.parse(raw_sheet_xml, listener)
725
+ { view: listener.view, pane: listener.pane, selection: listener.selection }
726
+ else
727
+ { view: {}, pane: nil, selection: nil }
728
+ end
729
+ end
730
+
731
+ #: () -> Hash[Symbol, untyped]
732
+ def print_page_data
733
+ @print_page_data ||= if raw_sheet_xml.match?(/<(?:pageSetup|pageMargins|headerFooter|printOptions|rowBreaks|colBreaks)\b/)
734
+ listener = Ooxml::Reader::PrintPageListener.new
735
+ Ooxml::XmlParser.parse(raw_sheet_xml, listener)
736
+ {
737
+ page_margins: listener.page_margins,
738
+ page_setup: listener.page_setup,
739
+ header_footer: listener.header_footer,
740
+ print_options: listener.print_options,
741
+ row_breaks: listener.row_breaks,
742
+ col_breaks: listener.col_breaks
743
+ }
744
+ else
745
+ {
746
+ page_margins: nil,
747
+ page_setup: {},
748
+ header_footer: {},
749
+ print_options: {},
750
+ row_breaks: [],
751
+ col_breaks: []
752
+ }
753
+ end
754
+ end
755
+
524
756
  #: () -> String
525
757
  def raw_sheet_xml
526
758
  @raw_sheet_xml ||= if @sheet_source.is_a?(String)