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
@@ -3,18 +3,22 @@
3
3
  # rbs_inline: enabled
4
4
 
5
5
  require_relative "xml_parser"
6
+ require_relative "../elements/styles"
6
7
 
7
8
  module Xlsxrb
8
9
  module Ooxml
9
10
  # SAX-based parser for xl/styles.xml.
10
- # Returns a Hash with :num_fmts, :fonts, :fills, :borders, :cell_xfs, :cell_style_xfs.
11
+ # Returns an Elements::Styles (Hash) with :num_fmts, :fonts, :fills, :borders, :cell_xfs, :cell_style_xfs.
11
12
  class StylesParser
13
+ #: (String? xml_string) -> Elements::Styles
12
14
  def self.parse(xml_string)
13
- return {} if xml_string.nil? || xml_string.empty?
15
+ return Elements::Styles.new if xml_string.nil? || xml_string.empty?
14
16
 
15
17
  listener = Listener.new
16
18
  XmlParser.parse(xml_string, listener)
17
- listener.result
19
+ styles = Elements::Styles.new(listener.result)
20
+ styles.precompute!
21
+ styles
18
22
  end
19
23
 
20
24
  # SAX listener for parsing styles.xml content.
@@ -37,6 +41,7 @@ module Xlsxrb
37
41
  @current_fill = nil
38
42
  @current_fill_pattern = nil
39
43
  @current_border = nil
44
+ @current_border_side = nil
40
45
  @current_xf = nil
41
46
  @in_cell_xfs = false
42
47
  @in_cell_style_xfs = false
@@ -70,7 +75,11 @@ module Xlsxrb
70
75
  when "fgColor", "bgColor"
71
76
  handle_fill_color(localname, attrs) if @current_fill
72
77
  when "border"
73
- @current_border = {} if parent_context?("borders")
78
+ if parent_context?("borders")
79
+ @current_border = {}
80
+ @current_border[:diagonal_up] = true if attrs["diagonalUp"] == "1"
81
+ @current_border[:diagonal_down] = true if attrs["diagonalDown"] == "1"
82
+ end
74
83
  when "left", "right", "top", "bottom", "diagonal"
75
84
  handle_border_side(localname, attrs)
76
85
  when "cellXfs"
@@ -81,6 +90,8 @@ module Xlsxrb
81
90
  handle_xf(attrs)
82
91
  when "alignment"
83
92
  handle_alignment(attrs)
93
+ when "protection"
94
+ handle_protection(attrs)
84
95
  end
85
96
  end
86
97
 
@@ -103,6 +114,8 @@ module Xlsxrb
103
114
  @result[:borders] << @current_border
104
115
  @current_border = nil
105
116
  end
117
+ when "left", "right", "top", "bottom", "diagonal"
118
+ @current_border_side = nil
106
119
  when "cellXfs"
107
120
  @in_cell_xfs = false
108
121
  when "cellStyleXfs"
@@ -124,10 +137,14 @@ module Xlsxrb
124
137
  end
125
138
 
126
139
  def handle_color(attrs)
127
- return unless @current_font
128
-
129
- color = extract_color(attrs)
130
- @current_font[:color] = color unless color.empty?
140
+ if @current_font
141
+ color = extract_color(attrs)
142
+ @current_font[:color] = color unless color.empty?
143
+ elsif @current_border && @current_border_side
144
+ color = extract_color(attrs)
145
+ @current_border[@current_border_side] ||= {}
146
+ @current_border[@current_border_side][:color] = color unless color.empty?
147
+ end
131
148
  end
132
149
 
133
150
  def handle_fill_color(localname, attrs)
@@ -141,8 +158,10 @@ module Xlsxrb
141
158
  def handle_border_side(side, attrs)
142
159
  return unless @current_border
143
160
 
161
+ @current_border_side = side.to_sym
144
162
  style = attrs["style"]
145
- @current_border[side.to_sym] = { style: style } if style
163
+ @current_border[@current_border_side] ||= {}
164
+ @current_border[@current_border_side][:style] = style if style
146
165
  end
147
166
 
148
167
  def handle_xf(attrs)
@@ -168,9 +187,20 @@ module Xlsxrb
168
187
  alignment[:vertical] = attrs["vertical"] if attrs["vertical"]
169
188
  alignment[:wrap_text] = true if attrs["wrapText"] == "1"
170
189
  alignment[:text_rotation] = attrs["textRotation"]&.to_i if attrs["textRotation"]
190
+ alignment[:indent] = attrs["indent"]&.to_i if attrs["indent"]
191
+ alignment[:shrink_to_fit] = true if attrs["shrinkToFit"] == "1"
171
192
  @current_xf[:alignment] = alignment unless alignment.empty?
172
193
  end
173
194
 
195
+ def handle_protection(attrs)
196
+ return unless @current_xf
197
+
198
+ protection = {}
199
+ protection[:locked] = attrs["locked"] == "1" unless attrs["locked"].nil?
200
+ protection[:hidden] = attrs["hidden"] == "1" unless attrs["hidden"].nil?
201
+ @current_xf[:protection] = protection unless protection.empty?
202
+ end
203
+
174
204
  def finalize_xf
175
205
  return unless @current_xf
176
206
 
@@ -14,6 +14,10 @@ module Xlsxrb
14
14
  EPOCH_1900 = Date.new(1899, 12, 31) # serial 1 = Jan 1, 1900
15
15
  EPOCH_1900_JD = EPOCH_1900.jd
16
16
 
17
+ # Excel 1904 date system epoch.
18
+ EPOCH_1904 = Date.new(1904, 1, 1) # serial 0 = Jan 1, 1904
19
+ EPOCH_1904_JD = EPOCH_1904.jd
20
+
17
21
  # Built-in number format codes defined by SpreadsheetML.
18
22
  BUILTIN_NUM_FMT_CODES = {
19
23
  0 => "General",
@@ -56,35 +60,47 @@ module Xlsxrb
56
60
  DEFAULT_DATETIME_FORMAT = "yyyy\\-mm\\-dd\\ hh:mm:ss"
57
61
 
58
62
  class << self
59
- # Converts a Date to an Excel serial number (1900 system).
60
- def date_to_serial(date)
61
- serial = date.jd - EPOCH_1900_JD
62
- # Lotus 1-2-3 bug: serial 60 = Feb 29, 1900 (doesn't exist).
63
- # Dates on or after Mar 1, 1900 (raw serial >= 60) need +1.
64
- serial += 1 if serial >= 60
65
- serial
63
+ # Converts a Date to an Excel serial number (1900 or 1904 system).
64
+ #: (Date date, ?date1904: bool) -> untyped
65
+ def date_to_serial(date, date1904: false)
66
+ if date1904
67
+ date.jd - EPOCH_1904_JD
68
+ else
69
+ serial = date.jd - EPOCH_1900_JD
70
+ # Lotus 1-2-3 bug: serial 60 = Feb 29, 1900 (doesn't exist).
71
+ # Dates on or after Mar 1, 1900 (raw serial >= 60) need +1.
72
+ serial += 1 if serial >= 60
73
+ serial
74
+ end
66
75
  end
67
76
 
68
- # Converts an Excel serial number (1900 system) to a Date.
69
- def serial_to_date(serial)
70
- # Adjust for Lotus 1-2-3 bug.
71
- serial -= 1 if serial > 60
72
- EPOCH_1900 + serial
77
+ # Converts an Excel serial number (1900 or 1904 system) to a Date.
78
+ #: (Numeric serial, ?date1904: bool) -> Date
79
+ def serial_to_date(serial, date1904: false)
80
+ if date1904
81
+ EPOCH_1904 + serial
82
+ else
83
+ # Adjust for Lotus 1-2-3 bug.
84
+ serial -= 1 if serial > 60
85
+ EPOCH_1900 + serial
86
+ end
73
87
  end
74
88
 
75
- # Converts a Time to a fractional Excel serial number (1900 system).
76
- def datetime_to_serial(time)
89
+ # Converts a Time to a fractional Excel serial number (1900 or 1904 system).
90
+ #: (Time time, ?date1904: bool) -> Float
91
+ def datetime_to_serial(time, date1904: false)
77
92
  date = time.to_date
78
- day_serial = date_to_serial(date)
93
+ day_serial = date_to_serial(date, date1904: date1904)
79
94
  # Fractional part: seconds since midnight / seconds per day
80
95
  seconds_since_midnight = (time.hour * 3600) + (time.min * 60) + time.sec
81
96
  day_serial + (seconds_since_midnight.to_f / 86_400)
82
97
  end
83
98
 
84
- # Converts a fractional Excel serial number to a Time (1900 system, UTC).
85
- def serial_to_datetime(serial)
99
+ # Converts a fractional Excel serial number to a Time (1900 or 1904 system, UTC).
100
+ #: (Numeric serial, ?date1904: bool) -> Time
101
+ def serial_to_datetime(serial, date1904: false)
86
102
  int_part, frac = serial.divmod(1)
87
- date = serial_to_date(int_part)
103
+ date = serial_to_date(int_part, date1904: date1904)
88
104
  total_seconds = (frac * 86_400).round
89
105
  hours, remaining = total_seconds.divmod(3600)
90
106
  minutes, seconds = remaining.divmod(60)
@@ -10,39 +10,77 @@ module Xlsxrb
10
10
  # Returns sheet list: [{ name:, sheet_id:, r_id: }, ...].
11
11
  class WorkbookParser
12
12
  def self.parse(xml_string)
13
- return [] if xml_string.nil? || xml_string.empty?
13
+ parse_with_properties(xml_string)[:sheets]
14
+ end
15
+
16
+ def self.parse_with_properties(xml_string)
17
+ return { sheets: [], date1904: false, defined_names: [] } if xml_string.nil? || xml_string.empty?
14
18
 
15
19
  listener = Listener.new
16
20
  XmlParser.parse(xml_string, listener)
17
- listener.sheets
21
+ { sheets: listener.sheets, date1904: listener.date1904, defined_names: listener.defined_names }
18
22
  end
19
23
 
20
- # SAX listener for workbook.xml sheets.
24
+ # SAX listener for workbook.xml sheets, properties, and defined names.
21
25
  class Listener
22
26
  include REXML::SAX2Listener
23
27
 
24
- attr_reader :sheets
28
+ attr_reader :sheets, :date1904, :defined_names
25
29
 
26
30
  def initialize
27
31
  @sheets = []
32
+ @date1904 = false
33
+ @defined_names = []
34
+ @in_defined_name = false
35
+ @current_dn = nil
36
+ @dn_text = +""
28
37
  end
29
38
 
30
39
  def start_element(_uri, localname, _qname, attrs)
31
- return unless localname == "sheet"
32
-
33
- name_attr = attrs["name"]
34
- name_attr = REXML::Text.unnormalize(name_attr) if name_attr
40
+ case localname
41
+ when "sheet"
42
+ name_attr = attrs["name"]
43
+ name_attr = REXML::Text.unnormalize(name_attr) if name_attr
44
+ raw_state = attrs["state"]
45
+ state_sym = case raw_state
46
+ when "hidden" then :hidden
47
+ when "veryHidden" then :very_hidden
48
+ else :visible
49
+ end
50
+
51
+ @sheets << {
52
+ name: name_attr,
53
+ sheet_id: attrs["sheetId"]&.to_i,
54
+ r_id: attrs["r:id"] || attrs["id"] || attrs.find { |k, _| k.end_with?(":id") }&.last,
55
+ state: state_sym
56
+ }
57
+ when "workbookPr"
58
+ d1904 = attrs["date1904"]
59
+ @date1904 = %w[1 true].include?(d1904) unless d1904.nil?
60
+ when "definedName"
61
+ @in_defined_name = true
62
+ @current_dn = {
63
+ name: attrs["name"],
64
+ hidden: %w[1 true].include?(attrs["hidden"])
65
+ }
66
+ @current_dn[:local_sheet_id] = attrs["localSheetId"].to_i if attrs["localSheetId"]
67
+ @current_dn[:comment] = attrs["comment"] if attrs["comment"]
68
+ @dn_text = +""
69
+ end
70
+ end
35
71
 
36
- @sheets << {
37
- name: name_attr,
38
- sheet_id: attrs["sheetId"]&.to_i,
39
- r_id: attrs["r:id"] || attrs["id"] || attrs.find { |k, _| k.end_with?(":id") }&.last
40
- }
72
+ def characters(text)
73
+ @dn_text << text if @in_defined_name
41
74
  end
42
75
 
43
- def end_element(_uri, _localname, _qname); end
76
+ def end_element(_uri, localname, _qname)
77
+ return unless localname == "definedName" && @in_defined_name
44
78
 
45
- def characters(_text); end
79
+ @current_dn[:value] = @dn_text.strip
80
+ @defined_names << @current_dn
81
+ @in_defined_name = false
82
+ @current_dn = nil
83
+ end
46
84
  end
47
85
  end
48
86
 
@@ -259,7 +259,7 @@ module Xlsxrb
259
259
  comment_idx += 1
260
260
  comment_writer = Xlsxrb::Ooxml::Writer.new
261
261
  sheet_comments.each do |c|
262
- comment_writer.add_comment(c[:cell], c[:text], author: c[:author] || "Author")
262
+ comment_writer.add_comment(c[:cell] || c[:ref], c[:text], author: c[:author] || "Author")
263
263
  end
264
264
  zip.add_entry("xl/comments#{comment_idx}.xml", comment_writer.send(:generate_comments_xml, comment_writer.comments))
265
265
  zip.add_entry("xl/drawings/vmlDrawing#{comment_idx}.vml", comment_writer.send(:generate_vml_drawing_xml, comment_writer.comments))
@@ -495,11 +495,17 @@ module Xlsxrb
495
495
 
496
496
  b.open_tag("sheets")
497
497
  @sheets.each_with_index do |sheet, idx|
498
- b.empty_tag("sheet", {
499
- name: sheet[:name],
500
- sheetId: (idx + 1).to_s,
501
- "r:id": "rId#{idx + 1}"
502
- })
498
+ s_name = sheet.respond_to?(:name) ? sheet.name : sheet[:name]
499
+ sheet_attrs = {
500
+ name: s_name,
501
+ sheetId: (idx + 1).to_s,
502
+ "r:id": "rId#{idx + 1}"
503
+ }
504
+ state = sheet.respond_to?(:state) ? sheet.state : sheet[:state]
505
+ if state && state.to_sym != :visible
506
+ sheet_attrs[:state] = state.to_sym == :very_hidden ? "veryHidden" : "hidden"
507
+ end
508
+ b.empty_tag("sheet", sheet_attrs)
503
509
  end
504
510
  b.close_tag("sheets")
505
511
 
@@ -508,9 +514,16 @@ module Xlsxrb
508
514
  b.open_tag("definedNames")
509
515
  @defined_names.each do |dn|
510
516
  dn_attrs = { name: dn[:name] }
511
- dn_attrs[:localSheetId] = dn[:local_sheet_id].to_s if dn[:local_sheet_id]
517
+ local_id = dn[:local_sheet_id]
518
+ if !local_id && dn[:sheet]
519
+ target_name = dn[:sheet].to_s
520
+ matched_idx = @sheets.find_index { |s| (s.respond_to?(:name) ? s.name : s[:name]) == target_name }
521
+ local_id = matched_idx if matched_idx
522
+ end
523
+ dn_attrs[:localSheetId] = local_id.to_s if local_id
512
524
  dn_attrs[:hidden] = "1" if dn[:hidden]
513
- b.tag("definedName", dn_attrs) { |_| b.text(dn[:value]) }
525
+ val = dn[:value] || (dn[:sheet] && dn[:range] ? "'#{dn[:sheet]}'!#{dn[:range]}" : dn[:range])
526
+ b.tag("definedName", dn_attrs) { |_| b.text(val) }
514
527
  end
515
528
  b.close_tag("definedNames")
516
529
  end
@@ -581,11 +594,26 @@ module Xlsxrb
581
594
  b.open_tag("styleSheet", { xmlns: SSML_NS })
582
595
 
583
596
  # Number formats
584
- num_fmts = @styles&.dig(:num_fmts) || []
597
+ raw_num_fmts = @styles&.dig(:num_fmts) || []
598
+ num_fmts = if raw_num_fmts.is_a?(Hash)
599
+ raw_num_fmts.map { |k, v| { num_fmt_id: k.to_i, format_code: v.to_s } }
600
+ elsif raw_num_fmts.is_a?(Array)
601
+ raw_num_fmts.map do |nf|
602
+ if nf.is_a?(Hash)
603
+ id = (nf[:num_fmt_id] || nf[:id]).to_i
604
+ code = (nf[:format_code] || nf[:code] || nf[:format]).to_s
605
+ { num_fmt_id: id, format_code: code }
606
+ elsif nf.is_a?(Array) && nf.size == 2
607
+ { num_fmt_id: nf[0].to_i, format_code: nf[1].to_s }
608
+ end
609
+ end.compact
610
+ else
611
+ []
612
+ end
585
613
  unless num_fmts.empty?
586
614
  b.tag("numFmts", { count: num_fmts.size.to_s }) do |_|
587
615
  num_fmts.each do |nf|
588
- b.empty_tag("numFmt", { numFmtId: nf[:num_fmt_id].to_s, formatCode: nf[:format_code] }) if nf.is_a?(Hash)
616
+ b.empty_tag("numFmt", { numFmtId: nf[:num_fmt_id].to_s, formatCode: nf[:format_code] })
589
617
  end
590
618
  end
591
619
  end
@@ -596,13 +624,13 @@ module Xlsxrb
596
624
  b.tag("fonts", { count: fonts.size.to_s }) do |_|
597
625
  fonts.each do |font_props|
598
626
  b.tag("font") do |_|
599
- b.empty_tag("sz", { val: (font_props[:sz] || 11).to_s }) if font_props[:sz] || fonts == [{}]
600
627
  b.empty_tag("b") if font_props[:bold]
601
628
  b.empty_tag("i") if font_props[:italic]
602
629
  b.empty_tag("strike") if font_props[:strike]
603
630
  if font_props[:underline]
604
631
  b.empty_tag("u", font_props[:underline] == "single" ? {} : { val: font_props[:underline] })
605
632
  end
633
+ b.empty_tag("sz", { val: (font_props[:sz] || 11).to_s }) if font_props[:sz] || fonts == [{}]
606
634
  b.empty_tag("color", { rgb: font_props[:color] }) if font_props[:color]
607
635
  b.empty_tag("name", { val: font_props[:name] || "Calibri" })
608
636
  end
@@ -646,7 +674,16 @@ module Xlsxrb
646
674
  side_data = border_props[side]
647
675
  if side_data
648
676
  b.tag(side.to_s, { style: side_data[:style] }) do |_|
649
- b.empty_tag("color", { rgb: side_data[:color] }) if side_data[:color]
677
+ c = side_data[:color]
678
+ if c.is_a?(Hash)
679
+ c_attrs = {}
680
+ c_attrs[:rgb] = c[:rgb] if c[:rgb]
681
+ c_attrs[:theme] = c[:theme].to_s if c[:theme]
682
+ c_attrs[:tint] = c[:tint].to_s if c[:tint]
683
+ b.empty_tag("color", c_attrs) unless c_attrs.empty?
684
+ elsif c
685
+ b.empty_tag("color", { rgb: c })
686
+ end
650
687
  end
651
688
  else
652
689
  b.empty_tag(side.to_s)
@@ -662,7 +699,7 @@ module Xlsxrb
662
699
  end
663
700
 
664
701
  # Cell XFs (formatting)
665
- xf_entries = @styles&.dig(:xf_entries) || []
702
+ xf_entries = @styles&.dig(:cell_xfs) || @styles&.dig(:xf_entries) || []
666
703
  xf_count = [xf_entries.size, 1].max
667
704
  b.tag("cellXfs", { count: xf_count.to_s }) do |_|
668
705
  xf_entries.each do |xf|
@@ -889,7 +926,8 @@ module Xlsxrb
889
926
  sheet_view: nil, sheet_properties: nil,
890
927
  tables: nil, table_start_rid: nil,
891
928
  legacy_drawing_rid: nil, sparkline_groups: nil)
892
- ws = WorksheetWriter.new(io)
929
+ date1904 = @workbook_properties&.[](:date1904) ? true : false
930
+ ws = WorksheetWriter.new(io, date1904: date1904)
893
931
  ws.start(
894
932
  columns: sheet[:columns] || [],
895
933
  sheet_properties: sheet_properties,
@@ -914,6 +952,8 @@ module Xlsxrb
914
952
  attrs[:hidden] = true if row.hidden
915
953
  attrs[:custom_height] = true if row.custom_height
916
954
  attrs[:outline_level] = row.outline_level if row.outline_level
955
+ style_idx = row.unmapped_data&.[](:style_index) || (row.respond_to?(:style_index) && row.style_index)
956
+ attrs[:style_index] = style_idx if style_idx
917
957
  ws.write_row(row.index, row.cells, attrs: attrs, unmapped: row.unmapped_data || [], sst_index: @shared_strings_index)
918
958
  end
919
959
  end
@@ -1020,7 +1060,18 @@ module Xlsxrb
1020
1060
  b.tag("cp:keywords") { |_| b.text(@core_properties[:keywords]) } if @core_properties[:keywords]
1021
1061
  b.tag("dc:description") { |_| b.text(@core_properties[:description]) } if @core_properties[:description]
1022
1062
  b.tag("cp:lastModifiedBy") { |_| b.text(@core_properties[:last_modified_by]) } if @core_properties[:last_modified_by]
1063
+ b.tag("cp:revision") { |_| b.text(@core_properties[:revision]) } if @core_properties[:revision]
1064
+ if @core_properties[:created]
1065
+ created_val = @core_properties[:created].respond_to?(:iso8601) ? @core_properties[:created].iso8601 : @core_properties[:created]
1066
+ b.tag("dcterms:created", { "xsi:type": "dcterms:W3CDTF" }) { |_| b.text(created_val) }
1067
+ end
1068
+ if @core_properties[:modified]
1069
+ modified_val = @core_properties[:modified].respond_to?(:iso8601) ? @core_properties[:modified].iso8601 : @core_properties[:modified]
1070
+ b.tag("dcterms:modified", { "xsi:type": "dcterms:W3CDTF" }) { |_| b.text(modified_val) }
1071
+ end
1023
1072
  b.tag("cp:category") { |_| b.text(@core_properties[:category]) } if @core_properties[:category]
1073
+ b.tag("cp:contentStatus") { |_| b.text(@core_properties[:content_status]) } if @core_properties[:content_status]
1074
+ b.tag("dc:language") { |_| b.text(@core_properties[:language]) } if @core_properties[:language]
1024
1075
  b.close_tag("cp:coreProperties")
1025
1076
  io.string
1026
1077
  end