xlsxrb 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +104 -0
- data/README.md +17 -14
- data/lib/xlsxrb/colors.rb +118 -0
- data/lib/xlsxrb/elements/cell.rb +205 -16
- data/lib/xlsxrb/elements/column.rb +6 -4
- data/lib/xlsxrb/elements/coordinate_access.rb +32 -0
- data/lib/xlsxrb/elements/image.rb +128 -0
- data/lib/xlsxrb/elements/row.rb +206 -9
- data/lib/xlsxrb/elements/styles.rb +157 -0
- data/lib/xlsxrb/elements/types.rb +95 -1
- data/lib/xlsxrb/elements/workbook.rb +121 -4
- data/lib/xlsxrb/elements/worksheet.rb +519 -29
- data/lib/xlsxrb/elements.rb +2 -0
- data/lib/xlsxrb/number_formatter.rb +429 -0
- data/lib/xlsxrb/ooxml/cfb.rb +1 -1
- data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +7 -5
- data/lib/xlsxrb/ooxml/reader/listeners/drawing_listeners.rb +11 -4
- data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +7 -1
- data/lib/xlsxrb/ooxml/reader.rb +14 -17
- data/lib/xlsxrb/ooxml/shared_strings_parser.rb +7 -1
- data/lib/xlsxrb/ooxml/styles_parser.rb +39 -9
- data/lib/xlsxrb/ooxml/utils.rb +34 -18
- data/lib/xlsxrb/ooxml/workbook_parser.rb +74 -16
- data/lib/xlsxrb/ooxml/workbook_writer.rb +82 -23
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +319 -79
- data/lib/xlsxrb/ooxml/worksheet_writer.rb +263 -66
- data/lib/xlsxrb/ooxml/writer/features_xml.rb +5 -3
- data/lib/xlsxrb/ooxml/writer.rb +14 -0
- data/lib/xlsxrb/ooxml/xml_builder.rb +14 -2
- data/lib/xlsxrb/ooxml/zip_reader.rb +144 -28
- data/lib/xlsxrb/stream_row.rb +243 -16
- data/lib/xlsxrb/stream_sheet.rb +647 -16
- data/lib/xlsxrb/stream_writer.rb +375 -34
- data/lib/xlsxrb/style_builder.rb +308 -22
- data/lib/xlsxrb/utils.rb +211 -0
- data/lib/xlsxrb/version.rb +1 -1
- data/lib/xlsxrb/workbook_builder.rb +72 -11
- data/lib/xlsxrb/worksheet_builder.rb +738 -24
- data/lib/xlsxrb.rb +1073 -83
- data/sig/generated/xlsxrb/colors.rbs +40 -0
- data/sig/generated/xlsxrb/elements/cell.rbs +113 -8
- data/sig/generated/xlsxrb/elements/column.rbs +3 -2
- data/sig/generated/xlsxrb/elements/coordinate_access.rbs +18 -0
- data/sig/generated/xlsxrb/elements/image.rbs +82 -0
- data/sig/generated/xlsxrb/elements/row.rbs +83 -4
- data/sig/generated/xlsxrb/elements/styles.rbs +79 -0
- data/sig/generated/xlsxrb/elements/types.rbs +43 -0
- data/sig/generated/xlsxrb/elements/workbook.rbs +79 -2
- data/sig/generated/xlsxrb/elements/worksheet.rbs +282 -18
- data/sig/generated/xlsxrb/number_formatter.rbs +132 -0
- data/sig/generated/xlsxrb/ooxml/reader.rbs +6 -0
- data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +2 -0
- data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +5 -2
- data/sig/generated/xlsxrb/ooxml/utils.rbs +17 -8
- data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +9 -3
- data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +1 -1
- data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +28 -6
- data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +13 -3
- data/sig/generated/xlsxrb/ooxml/writer.rbs +2 -0
- data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +3 -1
- data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +13 -0
- data/sig/generated/xlsxrb/stream_row.rbs +100 -6
- data/sig/generated/xlsxrb/stream_sheet.rbs +358 -14
- data/sig/generated/xlsxrb/stream_writer.rbs +220 -30
- data/sig/generated/xlsxrb/style_builder.rbs +55 -18
- data/sig/generated/xlsxrb/utils.rbs +151 -0
- data/sig/generated/xlsxrb/workbook_builder.rbs +26 -0
- data/sig/generated/xlsxrb/worksheet_builder.rbs +356 -18
- data/sig/generated/xlsxrb.rbs +223 -27
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
@
|
|
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[
|
|
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
|
|
data/lib/xlsxrb/ooxml/utils.rb
CHANGED
|
@@ -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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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,
|
|
76
|
+
def end_element(_uri, localname, _qname)
|
|
77
|
+
return unless localname == "definedName" && @in_defined_name
|
|
44
78
|
|
|
45
|
-
|
|
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
|
|
|
@@ -51,8 +89,28 @@ module Xlsxrb
|
|
|
51
89
|
def self.parse(xml_string)
|
|
52
90
|
return {} if xml_string.nil? || xml_string.empty?
|
|
53
91
|
|
|
92
|
+
sanitized = xml_string.lstrip
|
|
93
|
+
if sanitized.start_with?("<?xml")
|
|
94
|
+
first_decl_end = sanitized.index("?>")
|
|
95
|
+
if first_decl_end
|
|
96
|
+
decl = sanitized[0..(first_decl_end + 1)]
|
|
97
|
+
rest = sanitized[(first_decl_end + 2)..].gsub(/<\?xml\b[^>]*\?>/i, "")
|
|
98
|
+
sanitized = "#{decl}#{rest}".lstrip
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
54
102
|
listener = Listener.new
|
|
55
|
-
|
|
103
|
+
begin
|
|
104
|
+
XmlParser.parse(sanitized, listener)
|
|
105
|
+
rescue StandardError
|
|
106
|
+
# Fallback to regex scanning if SAX parser fails on malformed XML
|
|
107
|
+
sanitized.scan(%r{<(?:[a-zA-Z0-9_]+:)?Relationship\b([^>]*)/?>}i) do |match|
|
|
108
|
+
attrs_str = match[0]
|
|
109
|
+
id = attrs_str[/\bId=["']([^"']+)["']/i, 1]
|
|
110
|
+
target = attrs_str[/\bTarget=["']([^"']+)["']/i, 1]
|
|
111
|
+
listener.relationships[id] = target if id && target
|
|
112
|
+
end
|
|
113
|
+
end
|
|
56
114
|
listener.relationships
|
|
57
115
|
end
|
|
58
116
|
|
|
@@ -93,6 +93,9 @@ module Xlsxrb
|
|
|
93
93
|
selection: sheet[:selection],
|
|
94
94
|
sheet_view: sheet[:sheet_view],
|
|
95
95
|
sheet_properties: sheet[:sheet_properties],
|
|
96
|
+
sheet_format: sheet[:sheet_format],
|
|
97
|
+
protected_ranges: sheet[:protected_ranges],
|
|
98
|
+
cell_watches: sheet[:cell_watches],
|
|
96
99
|
tables: sheet_tables,
|
|
97
100
|
table_start_rid: info[:table_start_rid],
|
|
98
101
|
legacy_drawing_rid: info[:vml_rid],
|
|
@@ -259,7 +262,7 @@ module Xlsxrb
|
|
|
259
262
|
comment_idx += 1
|
|
260
263
|
comment_writer = Xlsxrb::Ooxml::Writer.new
|
|
261
264
|
sheet_comments.each do |c|
|
|
262
|
-
comment_writer.add_comment(c[:cell], c[:text], author: c[:author] || "Author")
|
|
265
|
+
comment_writer.add_comment(c[:cell] || c[:ref], c[:text], author: c[:author] || "Author")
|
|
263
266
|
end
|
|
264
267
|
zip.add_entry("xl/comments#{comment_idx}.xml", comment_writer.send(:generate_comments_xml, comment_writer.comments))
|
|
265
268
|
zip.add_entry("xl/drawings/vmlDrawing#{comment_idx}.vml", comment_writer.send(:generate_vml_drawing_xml, comment_writer.comments))
|
|
@@ -495,11 +498,17 @@ module Xlsxrb
|
|
|
495
498
|
|
|
496
499
|
b.open_tag("sheets")
|
|
497
500
|
@sheets.each_with_index do |sheet, idx|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
501
|
+
s_name = sheet.respond_to?(:name) ? sheet.name : sheet[:name]
|
|
502
|
+
sheet_attrs = {
|
|
503
|
+
name: s_name,
|
|
504
|
+
sheetId: (idx + 1).to_s,
|
|
505
|
+
"r:id": "rId#{idx + 1}"
|
|
506
|
+
}
|
|
507
|
+
state = sheet.respond_to?(:state) ? sheet.state : sheet[:state]
|
|
508
|
+
if state && state.to_sym != :visible
|
|
509
|
+
sheet_attrs[:state] = state.to_sym == :very_hidden ? "veryHidden" : "hidden"
|
|
510
|
+
end
|
|
511
|
+
b.empty_tag("sheet", sheet_attrs)
|
|
503
512
|
end
|
|
504
513
|
b.close_tag("sheets")
|
|
505
514
|
|
|
@@ -508,9 +517,16 @@ module Xlsxrb
|
|
|
508
517
|
b.open_tag("definedNames")
|
|
509
518
|
@defined_names.each do |dn|
|
|
510
519
|
dn_attrs = { name: dn[:name] }
|
|
511
|
-
|
|
520
|
+
local_id = dn[:local_sheet_id]
|
|
521
|
+
if !local_id && dn[:sheet]
|
|
522
|
+
target_name = dn[:sheet].to_s
|
|
523
|
+
matched_idx = @sheets.find_index { |s| (s.respond_to?(:name) ? s.name : s[:name]) == target_name }
|
|
524
|
+
local_id = matched_idx if matched_idx
|
|
525
|
+
end
|
|
526
|
+
dn_attrs[:localSheetId] = local_id.to_s if local_id
|
|
512
527
|
dn_attrs[:hidden] = "1" if dn[:hidden]
|
|
513
|
-
|
|
528
|
+
val = dn[:value] || (dn[:sheet] && dn[:range] ? "'#{dn[:sheet]}'!#{dn[:range]}" : dn[:range])
|
|
529
|
+
b.tag("definedName", dn_attrs) { |_| b.text(val) }
|
|
514
530
|
end
|
|
515
531
|
b.close_tag("definedNames")
|
|
516
532
|
end
|
|
@@ -581,11 +597,26 @@ module Xlsxrb
|
|
|
581
597
|
b.open_tag("styleSheet", { xmlns: SSML_NS })
|
|
582
598
|
|
|
583
599
|
# Number formats
|
|
584
|
-
|
|
600
|
+
raw_num_fmts = @styles&.dig(:num_fmts) || []
|
|
601
|
+
num_fmts = if raw_num_fmts.is_a?(Hash)
|
|
602
|
+
raw_num_fmts.map { |k, v| { num_fmt_id: k.to_i, format_code: v.to_s } }
|
|
603
|
+
elsif raw_num_fmts.is_a?(Array)
|
|
604
|
+
raw_num_fmts.map do |nf|
|
|
605
|
+
if nf.is_a?(Hash)
|
|
606
|
+
id = (nf[:num_fmt_id] || nf[:id]).to_i
|
|
607
|
+
code = (nf[:format_code] || nf[:code] || nf[:format]).to_s
|
|
608
|
+
{ num_fmt_id: id, format_code: code }
|
|
609
|
+
elsif nf.is_a?(Array) && nf.size == 2
|
|
610
|
+
{ num_fmt_id: nf[0].to_i, format_code: nf[1].to_s }
|
|
611
|
+
end
|
|
612
|
+
end.compact
|
|
613
|
+
else
|
|
614
|
+
[]
|
|
615
|
+
end
|
|
585
616
|
unless num_fmts.empty?
|
|
586
617
|
b.tag("numFmts", { count: num_fmts.size.to_s }) do |_|
|
|
587
618
|
num_fmts.each do |nf|
|
|
588
|
-
b.empty_tag("numFmt", { numFmtId: nf[:num_fmt_id].to_s, formatCode: nf[:format_code] })
|
|
619
|
+
b.empty_tag("numFmt", { numFmtId: nf[:num_fmt_id].to_s, formatCode: nf[:format_code] })
|
|
589
620
|
end
|
|
590
621
|
end
|
|
591
622
|
end
|
|
@@ -596,13 +627,13 @@ module Xlsxrb
|
|
|
596
627
|
b.tag("fonts", { count: fonts.size.to_s }) do |_|
|
|
597
628
|
fonts.each do |font_props|
|
|
598
629
|
b.tag("font") do |_|
|
|
599
|
-
b.empty_tag("sz", { val: (font_props[:sz] || 11).to_s }) if font_props[:sz] || fonts == [{}]
|
|
600
630
|
b.empty_tag("b") if font_props[:bold]
|
|
601
631
|
b.empty_tag("i") if font_props[:italic]
|
|
602
632
|
b.empty_tag("strike") if font_props[:strike]
|
|
603
633
|
if font_props[:underline]
|
|
604
634
|
b.empty_tag("u", font_props[:underline] == "single" ? {} : { val: font_props[:underline] })
|
|
605
635
|
end
|
|
636
|
+
b.empty_tag("sz", { val: (font_props[:sz] || 11).to_s }) if font_props[:sz] || fonts == [{}]
|
|
606
637
|
b.empty_tag("color", { rgb: font_props[:color] }) if font_props[:color]
|
|
607
638
|
b.empty_tag("name", { val: font_props[:name] || "Calibri" })
|
|
608
639
|
end
|
|
@@ -646,7 +677,16 @@ module Xlsxrb
|
|
|
646
677
|
side_data = border_props[side]
|
|
647
678
|
if side_data
|
|
648
679
|
b.tag(side.to_s, { style: side_data[:style] }) do |_|
|
|
649
|
-
|
|
680
|
+
c = side_data[:color]
|
|
681
|
+
if c.is_a?(Hash)
|
|
682
|
+
c_attrs = {}
|
|
683
|
+
c_attrs[:rgb] = c[:rgb] if c[:rgb]
|
|
684
|
+
c_attrs[:theme] = c[:theme].to_s if c[:theme]
|
|
685
|
+
c_attrs[:tint] = c[:tint].to_s if c[:tint]
|
|
686
|
+
b.empty_tag("color", c_attrs) unless c_attrs.empty?
|
|
687
|
+
elsif c
|
|
688
|
+
b.empty_tag("color", { rgb: c })
|
|
689
|
+
end
|
|
650
690
|
end
|
|
651
691
|
else
|
|
652
692
|
b.empty_tag(side.to_s)
|
|
@@ -662,7 +702,7 @@ module Xlsxrb
|
|
|
662
702
|
end
|
|
663
703
|
|
|
664
704
|
# Cell XFs (formatting)
|
|
665
|
-
xf_entries = @styles&.dig(:xf_entries) || []
|
|
705
|
+
xf_entries = @styles&.dig(:cell_xfs) || @styles&.dig(:xf_entries) || []
|
|
666
706
|
xf_count = [xf_entries.size, 1].max
|
|
667
707
|
b.tag("cellXfs", { count: xf_count.to_s }) do |_|
|
|
668
708
|
xf_entries.each do |xf|
|
|
@@ -880,23 +920,25 @@ module Xlsxrb
|
|
|
880
920
|
end
|
|
881
921
|
|
|
882
922
|
def write_worksheet_xml(io, sheet, drawing_rid: nil, sheet_protection: nil,
|
|
883
|
-
|
|
884
|
-
merge_cells: nil, conditional_formats: nil,
|
|
923
|
+
protected_ranges: nil, auto_filter: nil, filter_columns: nil,
|
|
924
|
+
sort_state: nil, merge_cells: nil, conditional_formats: nil,
|
|
885
925
|
data_validations: nil, hyperlinks: nil,
|
|
886
926
|
print_options: nil, page_margins: nil, page_setup: nil,
|
|
887
927
|
header_footer: nil, row_breaks: nil, col_breaks: nil,
|
|
888
|
-
|
|
889
|
-
sheet_view: nil, sheet_properties: nil,
|
|
890
|
-
tables: nil, table_start_rid: nil,
|
|
928
|
+
cell_watches: nil, freeze_pane: nil, split_pane: nil,
|
|
929
|
+
selection: nil, sheet_view: nil, sheet_properties: nil,
|
|
930
|
+
sheet_format: nil, tables: nil, table_start_rid: nil,
|
|
891
931
|
legacy_drawing_rid: nil, sparkline_groups: nil)
|
|
892
|
-
|
|
932
|
+
date1904 = @workbook_properties&.[](:date1904) ? true : false
|
|
933
|
+
ws = WorksheetWriter.new(io, date1904: date1904)
|
|
893
934
|
ws.start(
|
|
894
935
|
columns: sheet[:columns] || [],
|
|
895
936
|
sheet_properties: sheet_properties,
|
|
896
937
|
freeze_pane: freeze_pane,
|
|
897
938
|
split_pane: split_pane,
|
|
898
939
|
selection: selection,
|
|
899
|
-
sheet_view: sheet_view
|
|
940
|
+
sheet_view: sheet_view,
|
|
941
|
+
sheet_format: sheet_format
|
|
900
942
|
)
|
|
901
943
|
if sheet[:rows_tmp_path]
|
|
902
944
|
File.open(sheet[:rows_tmp_path], "rb") do |f|
|
|
@@ -914,6 +956,8 @@ module Xlsxrb
|
|
|
914
956
|
attrs[:hidden] = true if row.hidden
|
|
915
957
|
attrs[:custom_height] = true if row.custom_height
|
|
916
958
|
attrs[:outline_level] = row.outline_level if row.outline_level
|
|
959
|
+
style_idx = row.unmapped_data&.[](:style_index) || (row.respond_to?(:style_index) && row.style_index)
|
|
960
|
+
attrs[:style_index] = style_idx if style_idx
|
|
917
961
|
ws.write_row(row.index, row.cells, attrs: attrs, unmapped: row.unmapped_data || [], sst_index: @shared_strings_index)
|
|
918
962
|
end
|
|
919
963
|
end
|
|
@@ -921,6 +965,7 @@ module Xlsxrb
|
|
|
921
965
|
ws.finish(
|
|
922
966
|
drawing_rid: drawing_rid,
|
|
923
967
|
sheet_protection: sheet_protection,
|
|
968
|
+
protected_ranges: protected_ranges,
|
|
924
969
|
auto_filter: auto_filter,
|
|
925
970
|
filter_columns: filter_columns,
|
|
926
971
|
sort_state: sort_state,
|
|
@@ -934,6 +979,7 @@ module Xlsxrb
|
|
|
934
979
|
header_footer: header_footer,
|
|
935
980
|
row_breaks: row_breaks,
|
|
936
981
|
col_breaks: col_breaks,
|
|
982
|
+
cell_watches: cell_watches,
|
|
937
983
|
tables: tables,
|
|
938
984
|
table_start_rid: table_start_rid,
|
|
939
985
|
legacy_drawing_rid: legacy_drawing_rid,
|
|
@@ -1020,7 +1066,18 @@ module Xlsxrb
|
|
|
1020
1066
|
b.tag("cp:keywords") { |_| b.text(@core_properties[:keywords]) } if @core_properties[:keywords]
|
|
1021
1067
|
b.tag("dc:description") { |_| b.text(@core_properties[:description]) } if @core_properties[:description]
|
|
1022
1068
|
b.tag("cp:lastModifiedBy") { |_| b.text(@core_properties[:last_modified_by]) } if @core_properties[:last_modified_by]
|
|
1069
|
+
b.tag("cp:revision") { |_| b.text(@core_properties[:revision]) } if @core_properties[:revision]
|
|
1070
|
+
if @core_properties[:created]
|
|
1071
|
+
created_val = @core_properties[:created].respond_to?(:iso8601) ? @core_properties[:created].iso8601 : @core_properties[:created]
|
|
1072
|
+
b.tag("dcterms:created", { "xsi:type": "dcterms:W3CDTF" }) { |_| b.text(created_val) }
|
|
1073
|
+
end
|
|
1074
|
+
if @core_properties[:modified]
|
|
1075
|
+
modified_val = @core_properties[:modified].respond_to?(:iso8601) ? @core_properties[:modified].iso8601 : @core_properties[:modified]
|
|
1076
|
+
b.tag("dcterms:modified", { "xsi:type": "dcterms:W3CDTF" }) { |_| b.text(modified_val) }
|
|
1077
|
+
end
|
|
1023
1078
|
b.tag("cp:category") { |_| b.text(@core_properties[:category]) } if @core_properties[:category]
|
|
1079
|
+
b.tag("cp:contentStatus") { |_| b.text(@core_properties[:content_status]) } if @core_properties[:content_status]
|
|
1080
|
+
b.tag("dc:language") { |_| b.text(@core_properties[:language]) } if @core_properties[:language]
|
|
1024
1081
|
b.close_tag("cp:coreProperties")
|
|
1025
1082
|
io.string
|
|
1026
1083
|
end
|
|
@@ -1051,11 +1108,13 @@ module Xlsxrb
|
|
|
1051
1108
|
@custom_properties.each_with_index do |prop, idx|
|
|
1052
1109
|
b.open_tag("property", { fmtid: "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}", pid: (idx + 2).to_s, name: prop[:name] })
|
|
1053
1110
|
case prop[:type]
|
|
1054
|
-
when :number
|
|
1111
|
+
when :number, :int, :i4
|
|
1055
1112
|
b.tag("vt:i4") { |_| b.text(prop[:value].to_s) }
|
|
1056
|
-
when :
|
|
1113
|
+
when :float, :r8
|
|
1114
|
+
b.tag("vt:r8") { |_| b.text(prop[:value].to_s) }
|
|
1115
|
+
when :bool, :boolean
|
|
1057
1116
|
b.tag("vt:bool") { |_| b.text(prop[:value] ? "true" : "false") }
|
|
1058
|
-
when :date
|
|
1117
|
+
when :date, :filetime
|
|
1059
1118
|
b.tag("vt:filetime") { |_| b.text(prop[:value].to_s) }
|
|
1060
1119
|
else
|
|
1061
1120
|
b.tag("vt:lpwstr") { |_| b.text(prop[:value].to_s) }
|