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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +125 -0
- data/README.md +24 -5
- data/benchmark/micro_ips.rb +70 -0
- data/lib/xlsxrb/chart_builder.rb +41 -2
- data/lib/xlsxrb/dsl_helpers.rb +522 -6
- data/lib/xlsxrb/elements/cell.rb +34 -9
- data/lib/xlsxrb/elements/column.rb +2 -2
- data/lib/xlsxrb/elements/styles.rb +427 -15
- data/lib/xlsxrb/elements/types.rb +1 -0
- data/lib/xlsxrb/elements/workbook.rb +79 -4
- data/lib/xlsxrb/elements/worksheet.rb +173 -11
- data/lib/xlsxrb/ooxml/cfb.rb +10 -0
- data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +77 -38
- data/lib/xlsxrb/ooxml/reader/listeners/drawing_listeners.rb +187 -24
- data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +115 -2
- data/lib/xlsxrb/ooxml/reader/listeners/style_listeners.rb +28 -18
- data/lib/xlsxrb/ooxml/reader.rb +253 -136
- data/lib/xlsxrb/ooxml/shared_strings_parser.rb +83 -28
- data/lib/xlsxrb/ooxml/styles_parser.rb +76 -25
- data/lib/xlsxrb/ooxml/utils.rb +59 -2
- data/lib/xlsxrb/ooxml/workbook_parser.rb +57 -3
- data/lib/xlsxrb/ooxml/workbook_writer.rb +471 -86
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +210 -127
- data/lib/xlsxrb/ooxml/worksheet_writer.rb +515 -184
- data/lib/xlsxrb/ooxml/writer/drawing_xml.rb +595 -453
- data/lib/xlsxrb/ooxml/writer/features_xml.rb +26 -9
- data/lib/xlsxrb/ooxml/writer/styles_xml.rb +87 -24
- data/lib/xlsxrb/ooxml/writer.rb +299 -59
- data/lib/xlsxrb/ooxml/xml_builder.rb +12 -4
- data/lib/xlsxrb/ooxml/xml_parser.rb +45 -0
- data/lib/xlsxrb/ooxml/zip_reader.rb +48 -29
- data/lib/xlsxrb/ooxml/zip_writer.rb +15 -5
- data/lib/xlsxrb/stream_row.rb +4 -3
- data/lib/xlsxrb/stream_sheet.rb +182 -8
- data/lib/xlsxrb/stream_writer.rb +977 -84
- data/lib/xlsxrb/style_builder.rb +59 -6
- data/lib/xlsxrb/utils.rb +80 -0
- data/lib/xlsxrb/version.rb +1 -1
- data/lib/xlsxrb/workbook_builder.rb +307 -24
- data/lib/xlsxrb/worksheet_builder.rb +1150 -49
- data/lib/xlsxrb.rb +705 -142
- data/sig/generated/xlsxrb/chart_builder.rbs +31 -3
- data/sig/generated/xlsxrb/dsl_helpers.rbs +119 -4
- data/sig/generated/xlsxrb/elements/cell.rbs +20 -3
- data/sig/generated/xlsxrb/elements/column.rbs +3 -3
- data/sig/generated/xlsxrb/elements/styles.rbs +271 -5
- data/sig/generated/xlsxrb/elements/types.rbs +2 -0
- data/sig/generated/xlsxrb/elements/workbook.rbs +63 -3
- data/sig/generated/xlsxrb/elements/worksheet.rbs +92 -2
- data/sig/generated/xlsxrb/ooxml/reader/listeners/core_listeners.rbs +2 -0
- data/sig/generated/xlsxrb/ooxml/reader/listeners/drawing_listeners.rbs +74 -2
- data/sig/generated/xlsxrb/ooxml/reader/listeners/feature_listeners.rbs +36 -0
- data/sig/generated/xlsxrb/ooxml/reader/listeners/style_listeners.rbs +5 -0
- data/sig/generated/xlsxrb/ooxml/reader.rbs +29 -0
- data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +3 -1
- data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +5 -0
- data/sig/generated/xlsxrb/ooxml/utils.rbs +22 -0
- data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +15 -0
- data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +20 -4
- data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +19 -3
- data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +15 -2
- data/sig/generated/xlsxrb/ooxml/writer/drawing_xml.rbs +16 -0
- data/sig/generated/xlsxrb/ooxml/writer/styles_xml.rbs +5 -2
- data/sig/generated/xlsxrb/ooxml/writer.rbs +42 -8
- data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +4 -1
- data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +6 -6
- data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +8 -2
- data/sig/generated/xlsxrb/stream_row.rbs +3 -2
- data/sig/generated/xlsxrb/stream_sheet.rbs +122 -6
- data/sig/generated/xlsxrb/stream_writer.rbs +606 -30
- data/sig/generated/xlsxrb/style_builder.rbs +24 -6
- data/sig/generated/xlsxrb/utils.rbs +33 -0
- data/sig/generated/xlsxrb/workbook_builder.rbs +87 -5
- data/sig/generated/xlsxrb/worksheet_builder.rbs +557 -12
- data/sig/generated/xlsxrb.rbs +118 -20
- metadata +2 -1
|
@@ -32,19 +32,22 @@ module Xlsxrb
|
|
|
32
32
|
|
|
33
33
|
def self.write(target, sheets:, shared_strings: [], shared_strings_index: nil, styles: nil,
|
|
34
34
|
defined_names: nil, core_properties: nil, app_properties: nil,
|
|
35
|
-
custom_properties: nil, workbook_protection: nil, workbook_properties: nil
|
|
35
|
+
custom_properties: nil, workbook_protection: nil, workbook_properties: nil,
|
|
36
|
+
compression_level: Zlib::DEFAULT_COMPRESSION)
|
|
36
37
|
writer = new(sheets: sheets, shared_strings: shared_strings, shared_strings_index: shared_strings_index, styles: styles,
|
|
37
38
|
defined_names: defined_names, core_properties: core_properties,
|
|
38
39
|
app_properties: app_properties, custom_properties: custom_properties,
|
|
39
|
-
workbook_protection: workbook_protection, workbook_properties: workbook_properties
|
|
40
|
+
workbook_protection: workbook_protection, workbook_properties: workbook_properties,
|
|
41
|
+
compression_level: compression_level)
|
|
40
42
|
writer.write_to(target)
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
def initialize(sheets:, shared_strings: [], shared_strings_index: nil, styles: nil,
|
|
44
46
|
defined_names: nil, core_properties: nil, app_properties: nil,
|
|
45
|
-
custom_properties: nil, workbook_protection: nil, workbook_properties: nil
|
|
47
|
+
custom_properties: nil, workbook_protection: nil, workbook_properties: nil,
|
|
48
|
+
compression_level: Zlib::DEFAULT_COMPRESSION)
|
|
46
49
|
@sheets = sheets
|
|
47
|
-
@shared_strings = shared_strings
|
|
50
|
+
@shared_strings = shared_strings || []
|
|
48
51
|
@shared_strings_index = shared_strings_index
|
|
49
52
|
@styles = styles || {}
|
|
50
53
|
@defined_names = defined_names || []
|
|
@@ -53,27 +56,42 @@ module Xlsxrb
|
|
|
53
56
|
@custom_properties = custom_properties || []
|
|
54
57
|
@workbook_protection = workbook_protection
|
|
55
58
|
@workbook_properties = workbook_properties || { update_links: "never" }
|
|
59
|
+
@compression_level = compression_level || Zlib::DEFAULT_COMPRESSION
|
|
56
60
|
@drawing_count = 0
|
|
57
61
|
@chart_count = 0
|
|
58
62
|
@comment_count = 0
|
|
59
63
|
@table_count = 0
|
|
60
64
|
@pivot_cache_count = 0
|
|
61
65
|
@pivot_table_count = 0
|
|
66
|
+
@threaded_comment_count = 0
|
|
67
|
+
@persons = []
|
|
62
68
|
end
|
|
63
69
|
|
|
64
70
|
def write_to(target)
|
|
65
71
|
preprocess_conditional_formats!
|
|
66
72
|
|
|
67
|
-
|
|
73
|
+
active_tab_idx = resolve_active_tab_index
|
|
74
|
+
effective_active_idx = active_tab_idx || 0
|
|
75
|
+
|
|
76
|
+
ZipWriter.open(target, compression_level: @compression_level) do |zip|
|
|
68
77
|
@sheets.each_with_index do |sheet, idx|
|
|
69
78
|
info = prepare_sheet_auxiliary(sheet, idx)
|
|
70
79
|
sheet_tables = sheet[:tables] || []
|
|
71
80
|
|
|
81
|
+
sv = sheet[:sheet_view]
|
|
82
|
+
if idx == effective_active_idx
|
|
83
|
+
sv = (sv || {}).merge(tab_selected: true)
|
|
84
|
+
elsif sv&.key?(:tab_selected)
|
|
85
|
+
sv = sv.dup
|
|
86
|
+
sv.delete(:tab_selected)
|
|
87
|
+
end
|
|
88
|
+
|
|
72
89
|
zip.start_entry("xl/worksheets/sheet#{idx + 1}.xml")
|
|
73
90
|
write_worksheet_xml(
|
|
74
91
|
ZipEntryIO.new(zip),
|
|
75
92
|
sheet,
|
|
76
93
|
drawing_rid: info[:drawing_rid],
|
|
94
|
+
picture_rid: info[:picture_rid],
|
|
77
95
|
sheet_protection: sheet[:sheet_protection],
|
|
78
96
|
auto_filter: sheet[:auto_filter],
|
|
79
97
|
filter_columns: sheet[:filter_columns],
|
|
@@ -91,11 +109,14 @@ module Xlsxrb
|
|
|
91
109
|
freeze_pane: sheet[:freeze_pane],
|
|
92
110
|
split_pane: sheet[:split_pane],
|
|
93
111
|
selection: sheet[:selection],
|
|
94
|
-
sheet_view:
|
|
112
|
+
sheet_view: sv,
|
|
95
113
|
sheet_properties: sheet[:sheet_properties],
|
|
96
114
|
sheet_format: sheet[:sheet_format],
|
|
97
115
|
protected_ranges: sheet[:protected_ranges],
|
|
98
116
|
cell_watches: sheet[:cell_watches],
|
|
117
|
+
ignored_errors: sheet[:ignored_errors],
|
|
118
|
+
phonetic_pr: sheet[:phonetic_properties] || sheet[:phonetic_pr],
|
|
119
|
+
sheet_calc_pr: sheet[:sheet_calc_pr],
|
|
99
120
|
tables: sheet_tables,
|
|
100
121
|
table_start_rid: info[:table_start_rid],
|
|
101
122
|
legacy_drawing_rid: info[:vml_rid],
|
|
@@ -108,20 +129,23 @@ module Xlsxrb
|
|
|
108
129
|
end
|
|
109
130
|
end
|
|
110
131
|
|
|
111
|
-
def prepare_sheet_auxiliary(sheet,
|
|
132
|
+
def prepare_sheet_auxiliary(sheet, idx)
|
|
112
133
|
sheet_images = sheet[:images] || []
|
|
113
134
|
sheet_charts = sheet[:charts] || []
|
|
114
135
|
sheet_shapes = sheet[:shapes] || []
|
|
115
136
|
sheet_comments = sheet[:comments] || []
|
|
137
|
+
sheet_threaded_comments = sheet[:threaded_comments] || []
|
|
116
138
|
sheet_tables = sheet[:tables] || []
|
|
117
139
|
sheet_hyperlinks = sheet[:hyperlinks] || []
|
|
118
140
|
has_drawing = sheet_images.any? || sheet_charts.any? || sheet_shapes.any?
|
|
119
141
|
has_comments = sheet_comments.any?
|
|
142
|
+
has_threaded_comments = sheet_threaded_comments.any?
|
|
120
143
|
|
|
121
144
|
sheet_rels = []
|
|
122
145
|
drawing_rid = nil
|
|
123
146
|
vml_rid = nil
|
|
124
147
|
table_start_rid = nil
|
|
148
|
+
picture_rid = nil
|
|
125
149
|
hyperlink_rels = []
|
|
126
150
|
|
|
127
151
|
if has_drawing
|
|
@@ -140,6 +164,12 @@ module Xlsxrb
|
|
|
140
164
|
vml_rid = "rId#{vml_rid_num}"
|
|
141
165
|
end
|
|
142
166
|
|
|
167
|
+
if has_threaded_comments
|
|
168
|
+
@threaded_comment_count += 1
|
|
169
|
+
tc_rid_num = sheet_rels.size + 1
|
|
170
|
+
sheet_rels << { id: "rId#{tc_rid_num}", type: "http://schemas.microsoft.com/office/2017/10/relationships/threadedComment", target: "../threadedComments/threadedComment#{@threaded_comment_count}.xml" }
|
|
171
|
+
end
|
|
172
|
+
|
|
143
173
|
sheet_hyperlinks.each do |link|
|
|
144
174
|
next unless link[:url]
|
|
145
175
|
|
|
@@ -178,6 +208,33 @@ module Xlsxrb
|
|
|
178
208
|
end
|
|
179
209
|
end
|
|
180
210
|
|
|
211
|
+
bg_img = sheet[:background_image] || sheet[:background_picture] || sheet[:picture]
|
|
212
|
+
if bg_img
|
|
213
|
+
bg_data = if bg_img.is_a?(Hash)
|
|
214
|
+
bg_img[:file_data] || bg_img[:data]
|
|
215
|
+
else
|
|
216
|
+
(bg_img.respond_to?(:data) ? bg_img.data : bg_img)
|
|
217
|
+
end
|
|
218
|
+
if bg_data.is_a?(String) && !bg_data.include?("\x00") && File.file?(bg_data)
|
|
219
|
+
bg_ext = File.extname(bg_data).delete_prefix(".").downcase
|
|
220
|
+
bg_data = File.binread(bg_data)
|
|
221
|
+
else
|
|
222
|
+
bg_ext = if bg_img.is_a?(Hash)
|
|
223
|
+
(bg_img[:ext] || (bg_img[:filename] && File.extname(bg_img[:filename]).delete_prefix(".")) || "png").to_s.downcase
|
|
224
|
+
elsif bg_img.respond_to?(:filename)
|
|
225
|
+
File.extname(bg_img.filename).delete_prefix(".").downcase
|
|
226
|
+
else
|
|
227
|
+
"png"
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
if bg_data && !bg_data.empty?
|
|
231
|
+
p_rid_num = sheet_rels.size + 1
|
|
232
|
+
picture_rid = "rId#{p_rid_num}"
|
|
233
|
+
sheet_rels << { id: picture_rid, type: "#{DOC_REL}/image", target: "../media/sheet#{idx + 1}_bg.#{bg_ext}" }
|
|
234
|
+
sheet[:_bg_media] = { path: "xl/media/sheet#{idx + 1}_bg.#{bg_ext}", data: bg_data.b, ext: bg_ext }
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
181
238
|
sheet[:_prepared_rels] = sheet_rels
|
|
182
239
|
sheet[:_enriched_hyperlinks] = enriched_hyperlinks
|
|
183
240
|
|
|
@@ -185,6 +242,7 @@ module Xlsxrb
|
|
|
185
242
|
drawing_rid: drawing_rid,
|
|
186
243
|
vml_rid: vml_rid,
|
|
187
244
|
table_start_rid: table_start_rid,
|
|
245
|
+
picture_rid: picture_rid,
|
|
188
246
|
enriched_hyperlinks: enriched_hyperlinks,
|
|
189
247
|
sheet_rels: sheet_rels
|
|
190
248
|
}
|
|
@@ -193,6 +251,7 @@ module Xlsxrb
|
|
|
193
251
|
def write_auxiliary_parts(zip)
|
|
194
252
|
drawing_idx = 0
|
|
195
253
|
comment_idx = 0
|
|
254
|
+
tc_idx = 0
|
|
196
255
|
table_idx = 0
|
|
197
256
|
pivot_cache_idx = 0
|
|
198
257
|
pivot_table_idx = 0
|
|
@@ -268,6 +327,29 @@ module Xlsxrb
|
|
|
268
327
|
zip.add_entry("xl/drawings/vmlDrawing#{comment_idx}.vml", comment_writer.send(:generate_vml_drawing_xml, comment_writer.comments))
|
|
269
328
|
end
|
|
270
329
|
|
|
330
|
+
sheet_threaded_comments = sheet[:threaded_comments] || []
|
|
331
|
+
if sheet_threaded_comments.any?
|
|
332
|
+
tc_idx += 1
|
|
333
|
+
tc_writer = Xlsxrb::Ooxml::Writer.new
|
|
334
|
+
sheet_threaded_comments.each do |c|
|
|
335
|
+
tc_writer.add_threaded_comment(
|
|
336
|
+
c[:cell] || c[:ref],
|
|
337
|
+
c[:text],
|
|
338
|
+
author: c[:author] || "Author",
|
|
339
|
+
timestamp: c[:timestamp] || c[:dt],
|
|
340
|
+
id: c[:id],
|
|
341
|
+
parent_id: c[:parent_id],
|
|
342
|
+
done: c[:done],
|
|
343
|
+
user_id: c[:user_id],
|
|
344
|
+
provider_id: c[:provider_id]
|
|
345
|
+
)
|
|
346
|
+
end
|
|
347
|
+
tc_writer.persons.each do |p|
|
|
348
|
+
@persons << p unless @persons.any? { |ep| ep[:id] == p[:id] || ep[:display_name] == p[:display_name] }
|
|
349
|
+
end
|
|
350
|
+
zip.add_entry("xl/threadedComments/threadedComment#{tc_idx}.xml", tc_writer.send(:generate_threaded_comments_xml, tc_writer.threaded_comments))
|
|
351
|
+
end
|
|
352
|
+
|
|
271
353
|
sheet_tables.each do |tbl|
|
|
272
354
|
table_idx += 1
|
|
273
355
|
zip.add_entry("xl/tables/table#{table_idx}.xml", build_table_xml(tbl, table_idx))
|
|
@@ -308,8 +390,15 @@ module Xlsxrb
|
|
|
308
390
|
end
|
|
309
391
|
end
|
|
310
392
|
|
|
393
|
+
zip.add_binary_entry(sheet[:_bg_media][:path], sheet[:_bg_media][:data]) if sheet[:_bg_media]
|
|
394
|
+
|
|
311
395
|
zip.add_entry("xl/worksheets/_rels/sheet#{idx + 1}.xml.rels", build_sheet_rels_from_list(sheet_rels)) unless sheet_rels.empty?
|
|
312
396
|
end
|
|
397
|
+
|
|
398
|
+
return unless @persons.any?
|
|
399
|
+
|
|
400
|
+
p_writer = Xlsxrb::Ooxml::Writer.new
|
|
401
|
+
zip.add_entry("xl/persons/person.xml", p_writer.send(:generate_persons_xml, @persons))
|
|
313
402
|
end
|
|
314
403
|
|
|
315
404
|
def write_package_metadata(zip)
|
|
@@ -407,6 +496,23 @@ module Xlsxrb
|
|
|
407
496
|
end
|
|
408
497
|
end
|
|
409
498
|
|
|
499
|
+
if sheet[:_bg_media]
|
|
500
|
+
bg_ext = sheet[:_bg_media][:ext]
|
|
501
|
+
unless image_exts[bg_ext]
|
|
502
|
+
mime = case bg_ext
|
|
503
|
+
when "png" then "image/png"
|
|
504
|
+
when "jpg", "jpeg" then "image/jpeg"
|
|
505
|
+
when "gif" then "image/gif"
|
|
506
|
+
when "bmp" then "image/bmp"
|
|
507
|
+
when "emf" then "image/x-emf"
|
|
508
|
+
when "wmf" then "image/x-wmf"
|
|
509
|
+
else "application/octet-stream"
|
|
510
|
+
end
|
|
511
|
+
b.empty_tag("Default", { Extension: bg_ext, ContentType: mime })
|
|
512
|
+
image_exts[bg_ext] = true
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
|
|
410
516
|
if sheet_comments.any?
|
|
411
517
|
comment_count += 1
|
|
412
518
|
b.empty_tag("Override", { PartName: "/xl/comments#{comment_count}.xml", ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml" })
|
|
@@ -427,6 +533,11 @@ module Xlsxrb
|
|
|
427
533
|
end
|
|
428
534
|
end
|
|
429
535
|
|
|
536
|
+
b.empty_tag("Override", { PartName: "/xl/persons/person.xml", ContentType: "application/vnd.ms-excel.person+xml" }) if @persons.any?
|
|
537
|
+
(1..@threaded_comment_count).each do |tc|
|
|
538
|
+
b.empty_tag("Override", { PartName: "/xl/threadedComments/threadedComment#{tc}.xml", ContentType: "application/vnd.ms-excel.threadedcomments+xml" })
|
|
539
|
+
end
|
|
540
|
+
|
|
430
541
|
b.close_tag("Types")
|
|
431
542
|
io.string
|
|
432
543
|
end
|
|
@@ -484,6 +595,7 @@ module Xlsxrb
|
|
|
484
595
|
attrs = {}
|
|
485
596
|
attrs[:date1904] = "1" if @workbook_properties[:date1904]
|
|
486
597
|
attrs[:updateLinks] = @workbook_properties[:update_links] if @workbook_properties[:update_links]
|
|
598
|
+
attrs[:codeName] = @workbook_properties[:code_name] if @workbook_properties[:code_name]
|
|
487
599
|
b.empty_tag("workbookPr", attrs) unless attrs.empty?
|
|
488
600
|
end
|
|
489
601
|
|
|
@@ -496,6 +608,26 @@ module Xlsxrb
|
|
|
496
608
|
b.empty_tag("workbookProtection", wp_attrs) unless wp_attrs.empty?
|
|
497
609
|
end
|
|
498
610
|
|
|
611
|
+
# Workbook view: activeTab, firstSheet, showSheetTabs, tabRatio
|
|
612
|
+
active_tab_idx = resolve_active_tab_index
|
|
613
|
+
first_sheet_idx = resolve_first_sheet_index
|
|
614
|
+
show_sheet_tabs = @workbook_properties&.[](:show_sheet_tabs)
|
|
615
|
+
tab_ratio = @workbook_properties&.[](:tab_ratio)
|
|
616
|
+
|
|
617
|
+
emit_active_tab = !@workbook_properties&.[](:active_tab).nil? || active_tab_idx&.positive?
|
|
618
|
+
|
|
619
|
+
has_book_views = emit_active_tab || first_sheet_idx || !show_sheet_tabs.nil? || tab_ratio
|
|
620
|
+
if has_book_views
|
|
621
|
+
bv_attrs = {}
|
|
622
|
+
bv_attrs[:activeTab] = active_tab_idx.to_s if emit_active_tab
|
|
623
|
+
bv_attrs[:firstSheet] = first_sheet_idx.to_s if first_sheet_idx
|
|
624
|
+
bv_attrs[:showSheetTabs] = show_sheet_tabs ? "1" : "0" unless show_sheet_tabs.nil?
|
|
625
|
+
bv_attrs[:tabRatio] = tab_ratio.to_s if tab_ratio
|
|
626
|
+
b.open_tag("bookViews")
|
|
627
|
+
b.empty_tag("workbookView", bv_attrs)
|
|
628
|
+
b.close_tag("bookViews")
|
|
629
|
+
end
|
|
630
|
+
|
|
499
631
|
b.open_tag("sheets")
|
|
500
632
|
@sheets.each_with_index do |sheet, idx|
|
|
501
633
|
s_name = sheet.respond_to?(:name) ? sheet.name : sheet[:name]
|
|
@@ -531,6 +663,9 @@ module Xlsxrb
|
|
|
531
663
|
b.close_tag("definedNames")
|
|
532
664
|
end
|
|
533
665
|
|
|
666
|
+
# Calculation properties: ask Excel to recalculate every formula when the file is opened
|
|
667
|
+
b.empty_tag("calcPr", { fullCalcOnLoad: "1" }) if @workbook_properties&.[](:full_calc_on_load)
|
|
668
|
+
|
|
534
669
|
# Pivot caches
|
|
535
670
|
total_pivots = @sheets.sum { |s| (s[:pivot_tables] || []).size }
|
|
536
671
|
if total_pivots.positive?
|
|
@@ -586,6 +721,15 @@ module Xlsxrb
|
|
|
586
721
|
})
|
|
587
722
|
end
|
|
588
723
|
|
|
724
|
+
if @persons.any?
|
|
725
|
+
rid += 1
|
|
726
|
+
b.empty_tag("Relationship", {
|
|
727
|
+
Id: "rId#{rid}",
|
|
728
|
+
Type: "http://schemas.microsoft.com/office/2017/10/relationships/person",
|
|
729
|
+
Target: "persons/person.xml"
|
|
730
|
+
})
|
|
731
|
+
end
|
|
732
|
+
|
|
589
733
|
b.close_tag("Relationships")
|
|
590
734
|
io.string
|
|
591
735
|
end
|
|
@@ -597,7 +741,11 @@ module Xlsxrb
|
|
|
597
741
|
b.open_tag("styleSheet", { xmlns: SSML_NS })
|
|
598
742
|
|
|
599
743
|
# Number formats
|
|
600
|
-
raw_num_fmts = @styles
|
|
744
|
+
raw_num_fmts = if @styles.respond_to?(:num_fmts)
|
|
745
|
+
@styles.num_fmts
|
|
746
|
+
else
|
|
747
|
+
@styles&.dig(:num_fmts) || []
|
|
748
|
+
end
|
|
601
749
|
num_fmts = if raw_num_fmts.is_a?(Hash)
|
|
602
750
|
raw_num_fmts.map { |k, v| { num_fmt_id: k.to_i, format_code: v.to_s } }
|
|
603
751
|
elsif raw_num_fmts.is_a?(Array)
|
|
@@ -622,7 +770,11 @@ module Xlsxrb
|
|
|
622
770
|
end
|
|
623
771
|
|
|
624
772
|
# Fonts
|
|
625
|
-
fonts = @styles
|
|
773
|
+
fonts = if @styles.respond_to?(:fonts)
|
|
774
|
+
@styles.fonts
|
|
775
|
+
else
|
|
776
|
+
@styles&.dig(:fonts) || []
|
|
777
|
+
end
|
|
626
778
|
fonts = [{}] if fonts.empty? # At least one default font
|
|
627
779
|
b.tag("fonts", { count: fonts.size.to_s }) do |_|
|
|
628
780
|
fonts.each do |font_props|
|
|
@@ -631,17 +783,38 @@ module Xlsxrb
|
|
|
631
783
|
b.empty_tag("i") if font_props[:italic]
|
|
632
784
|
b.empty_tag("strike") if font_props[:strike]
|
|
633
785
|
if font_props[:underline]
|
|
634
|
-
|
|
786
|
+
u_val = font_props[:underline]
|
|
787
|
+
if u_val == true || u_val.to_s == "single"
|
|
788
|
+
b.empty_tag("u")
|
|
789
|
+
else
|
|
790
|
+
mapped_u = StyleBuilder::UNDERLINE_MAP[u_val.to_sym] || u_val.to_s
|
|
791
|
+
b.empty_tag("u", { val: mapped_u })
|
|
792
|
+
end
|
|
793
|
+
end
|
|
794
|
+
va_val = font_props[:vert_align] || font_props[:vertical_align]
|
|
795
|
+
va_val ||= "superscript" if font_props[:superscript]
|
|
796
|
+
va_val ||= "subscript" if font_props[:subscript]
|
|
797
|
+
if va_val
|
|
798
|
+
mapped_va = StyleBuilder::VERT_ALIGN_MAP[va_val.to_sym] || va_val.to_s
|
|
799
|
+
b.empty_tag("vertAlign", { val: mapped_va })
|
|
635
800
|
end
|
|
636
801
|
b.empty_tag("sz", { val: (font_props[:sz] || 11).to_s }) if font_props[:sz] || fonts == [{}]
|
|
637
|
-
|
|
802
|
+
color_attrs = color_tag_attrs(font_props[:color] || (font_props.key?(:rgb) || font_props.key?(:theme) || font_props.key?(:indexed) ? font_props : nil))
|
|
803
|
+
b.empty_tag("color", color_attrs) if color_attrs
|
|
638
804
|
b.empty_tag("name", { val: font_props[:name] || "Calibri" })
|
|
805
|
+
b.empty_tag("family", { val: font_props[:family].to_s }) if font_props[:family]
|
|
806
|
+
b.empty_tag("charset", { val: font_props[:charset].to_s }) if font_props[:charset]
|
|
807
|
+
b.empty_tag("scheme", { val: font_props[:scheme].to_s }) if font_props[:scheme]
|
|
639
808
|
end
|
|
640
809
|
end
|
|
641
810
|
end
|
|
642
811
|
|
|
643
812
|
# Fills
|
|
644
|
-
fills = @styles
|
|
813
|
+
fills = if @styles.respond_to?(:fills)
|
|
814
|
+
@styles.fills
|
|
815
|
+
else
|
|
816
|
+
@styles&.dig(:fills) || []
|
|
817
|
+
end
|
|
645
818
|
fills = [{ pattern: "none" }, { pattern: "gray125" }] if fills.empty?
|
|
646
819
|
b.tag("fills", { count: fills.size.to_s }) do |_|
|
|
647
820
|
fills.each do |fill_props|
|
|
@@ -650,14 +823,18 @@ module Xlsxrb
|
|
|
650
823
|
b.open_tag("gradientFill", { type: fill_props[:gradient][:type], degree: fill_props[:gradient][:degree]&.to_s }.compact)
|
|
651
824
|
fill_props[:gradient][:stops].each do |stop|
|
|
652
825
|
b.tag("stop", { position: stop[:position].to_s }) do |_|
|
|
653
|
-
|
|
826
|
+
c_attrs = color_tag_attrs(stop[:color] || stop)
|
|
827
|
+
b.empty_tag("color", c_attrs) if c_attrs
|
|
654
828
|
end
|
|
655
829
|
end
|
|
656
830
|
b.close_tag("gradientFill")
|
|
657
831
|
else
|
|
658
832
|
b.tag("patternFill", { patternType: fill_props[:pattern] || "none" }) do |_|
|
|
659
|
-
|
|
660
|
-
b.empty_tag("
|
|
833
|
+
fg_attrs = color_tag_attrs(fill_props[:fg_color] || (fill_props[:fg_color_theme] ? { theme: fill_props[:fg_color_theme], tint: fill_props[:fg_color_tint] } : nil) || (fill_props[:fg_color_indexed] ? { indexed: fill_props[:fg_color_indexed] } : nil))
|
|
834
|
+
b.empty_tag("fgColor", fg_attrs) if fg_attrs
|
|
835
|
+
|
|
836
|
+
bg_attrs = color_tag_attrs(fill_props[:bg_color] || (fill_props[:bg_color_theme] ? { theme: fill_props[:bg_color_theme], tint: fill_props[:bg_color_tint] } : nil) || (fill_props[:bg_color_indexed] ? { indexed: fill_props[:bg_color_indexed] } : nil))
|
|
837
|
+
b.empty_tag("bgColor", bg_attrs) if bg_attrs
|
|
661
838
|
end
|
|
662
839
|
end
|
|
663
840
|
end
|
|
@@ -665,7 +842,11 @@ module Xlsxrb
|
|
|
665
842
|
end
|
|
666
843
|
|
|
667
844
|
# Borders
|
|
668
|
-
borders = @styles
|
|
845
|
+
borders = if @styles.respond_to?(:borders)
|
|
846
|
+
@styles.borders
|
|
847
|
+
else
|
|
848
|
+
@styles&.dig(:borders) || []
|
|
849
|
+
end
|
|
669
850
|
borders = [{}] if borders.empty?
|
|
670
851
|
b.tag("borders", { count: borders.size.to_s }) do |_|
|
|
671
852
|
borders.each do |border_props|
|
|
@@ -677,16 +858,8 @@ module Xlsxrb
|
|
|
677
858
|
side_data = border_props[side]
|
|
678
859
|
if side_data
|
|
679
860
|
b.tag(side.to_s, { style: side_data[:style] }) do |_|
|
|
680
|
-
|
|
681
|
-
|
|
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
|
|
861
|
+
c_attrs = color_tag_attrs(side_data[:color])
|
|
862
|
+
b.empty_tag("color", c_attrs) if c_attrs
|
|
690
863
|
end
|
|
691
864
|
else
|
|
692
865
|
b.empty_tag(side.to_s)
|
|
@@ -702,7 +875,11 @@ module Xlsxrb
|
|
|
702
875
|
end
|
|
703
876
|
|
|
704
877
|
# Cell XFs (formatting)
|
|
705
|
-
xf_entries = @styles
|
|
878
|
+
xf_entries = if @styles.respond_to?(:cell_xfs)
|
|
879
|
+
@styles.cell_xfs
|
|
880
|
+
else
|
|
881
|
+
@styles&.dig(:cell_xfs) || @styles&.dig(:xf_entries) || []
|
|
882
|
+
end
|
|
706
883
|
xf_count = [xf_entries.size, 1].max
|
|
707
884
|
b.tag("cellXfs", { count: xf_count.to_s }) do |_|
|
|
708
885
|
xf_entries.each do |xf|
|
|
@@ -751,7 +928,11 @@ module Xlsxrb
|
|
|
751
928
|
end
|
|
752
929
|
|
|
753
930
|
# Differential formats (for conditional formatting)
|
|
754
|
-
dxfs = @styles
|
|
931
|
+
dxfs = if @styles.respond_to?(:dxfs)
|
|
932
|
+
@styles.dxfs
|
|
933
|
+
else
|
|
934
|
+
@styles&.dig(:dxfs) || []
|
|
935
|
+
end
|
|
755
936
|
unless dxfs.empty?
|
|
756
937
|
b.tag("dxfs", { count: dxfs.size.to_s }) do |_|
|
|
757
938
|
dxfs.each do |dxf|
|
|
@@ -762,6 +943,9 @@ module Xlsxrb
|
|
|
762
943
|
b.empty_tag("numFmt", { numFmtId: nf[:num_fmt_id].to_s, formatCode: nf[:format_code].to_s })
|
|
763
944
|
end
|
|
764
945
|
write_dxf_fill(b, dxf[:fill]) if dxf[:fill]
|
|
946
|
+
write_dxf_alignment(b, dxf[:alignment]) if dxf[:alignment]
|
|
947
|
+
write_dxf_border(b, dxf[:border]) if dxf[:border]
|
|
948
|
+
write_dxf_protection(b, dxf[:protection]) if dxf[:protection]
|
|
765
949
|
end
|
|
766
950
|
end
|
|
767
951
|
end
|
|
@@ -772,8 +956,6 @@ module Xlsxrb
|
|
|
772
956
|
end
|
|
773
957
|
|
|
774
958
|
def preprocess_conditional_formats!
|
|
775
|
-
@styles[:dxfs] ||= []
|
|
776
|
-
|
|
777
959
|
@sheets.each do |sheet|
|
|
778
960
|
rules = sheet[:conditional_formats]
|
|
779
961
|
next if rules.nil? || rules.empty?
|
|
@@ -785,11 +967,15 @@ module Xlsxrb
|
|
|
785
967
|
dxf = normalized.delete(:dxf) || extract_dxf_from_rule!(normalized)
|
|
786
968
|
next normalized unless dxf
|
|
787
969
|
|
|
788
|
-
dxf_id = @styles
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
970
|
+
dxf_id = if @styles.respond_to?(:add_dxf)
|
|
971
|
+
@styles.dxfs.index(dxf) || @styles.add_dxf(dxf)
|
|
972
|
+
else
|
|
973
|
+
@styles[:dxfs] ||= []
|
|
974
|
+
@styles[:dxfs].index(dxf) || begin
|
|
975
|
+
@styles[:dxfs] << dxf
|
|
976
|
+
@styles[:dxfs].size - 1
|
|
977
|
+
end
|
|
978
|
+
end
|
|
793
979
|
normalized[:format_id] = dxf_id
|
|
794
980
|
normalized
|
|
795
981
|
end
|
|
@@ -797,48 +983,162 @@ module Xlsxrb
|
|
|
797
983
|
end
|
|
798
984
|
|
|
799
985
|
def extract_dxf_from_rule!(rule)
|
|
800
|
-
|
|
801
|
-
font[:color] = rule.delete(:font_color) if rule.key?(:font_color)
|
|
802
|
-
font[:bold] = rule.delete(:bold) if rule.key?(:bold)
|
|
803
|
-
font[:italic] = rule.delete(:italic) if rule.key?(:italic)
|
|
804
|
-
font[:underline] = rule.delete(:underline) if rule.key?(:underline)
|
|
805
|
-
|
|
806
|
-
fill = {}
|
|
807
|
-
if rule.key?(:fill_color)
|
|
808
|
-
fill[:pattern] = "solid"
|
|
809
|
-
fill[:fg_color] = rule.delete(:fill_color)
|
|
810
|
-
end
|
|
811
|
-
|
|
812
|
-
dxf = {}
|
|
813
|
-
dxf[:font] = font unless font.empty?
|
|
814
|
-
dxf[:fill] = fill unless fill.empty?
|
|
815
|
-
dxf.empty? ? nil : dxf
|
|
986
|
+
DslHelpers.extract_dxf_from_rule!(rule)
|
|
816
987
|
end
|
|
817
988
|
|
|
818
989
|
def write_dxf_font(builder, font)
|
|
819
990
|
builder.tag("font") do |_|
|
|
820
|
-
|
|
821
|
-
|
|
991
|
+
if font.key?(:bold)
|
|
992
|
+
font[:bold] ? builder.empty_tag("b") : builder.empty_tag("b", { val: "0" })
|
|
993
|
+
end
|
|
994
|
+
if font.key?(:italic)
|
|
995
|
+
font[:italic] ? builder.empty_tag("i") : builder.empty_tag("i", { val: "0" })
|
|
996
|
+
end
|
|
997
|
+
if font.key?(:strike)
|
|
998
|
+
font[:strike] ? builder.empty_tag("strike") : builder.empty_tag("strike", { val: "0" })
|
|
999
|
+
end
|
|
822
1000
|
if font[:underline]
|
|
823
|
-
if font[:underline]
|
|
1001
|
+
if [true, "single"].include?(font[:underline])
|
|
824
1002
|
builder.empty_tag("u")
|
|
825
1003
|
else
|
|
826
|
-
|
|
1004
|
+
mapped_u = StyleBuilder::UNDERLINE_MAP[font[:underline].to_sym] || font[:underline]
|
|
1005
|
+
builder.empty_tag("u", { val: mapped_u })
|
|
827
1006
|
end
|
|
828
1007
|
end
|
|
829
|
-
|
|
1008
|
+
va_val = font[:vert_align] || font[:vertical_align]
|
|
1009
|
+
va_val ||= "superscript" if font[:superscript]
|
|
1010
|
+
va_val ||= "subscript" if font[:subscript]
|
|
1011
|
+
if va_val
|
|
1012
|
+
mapped_va = StyleBuilder::VERT_ALIGN_MAP[va_val.to_sym] || va_val.to_s
|
|
1013
|
+
builder.empty_tag("vertAlign", { val: mapped_va })
|
|
1014
|
+
end
|
|
1015
|
+
color_attrs = color_tag_attrs(font[:color] || font)
|
|
1016
|
+
builder.empty_tag("color", color_attrs) if color_attrs
|
|
830
1017
|
builder.empty_tag("name", { val: font[:name] }) if font[:name]
|
|
831
|
-
|
|
1018
|
+
sz_val = font[:sz] || font[:size]
|
|
1019
|
+
builder.empty_tag("sz", { val: sz_val.to_s }) if sz_val
|
|
832
1020
|
end
|
|
833
1021
|
end
|
|
834
1022
|
|
|
835
1023
|
def write_dxf_fill(builder, fill)
|
|
836
1024
|
builder.tag("fill") do |_|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1025
|
+
pattern_attrs = {}
|
|
1026
|
+
pattern = fill[:pattern] || fill[:pattern_type]
|
|
1027
|
+
pattern ||= "solid" if (fill[:color] || fill[:fill_color] || fill[:fg_color]) && !fill[:bg_color]
|
|
1028
|
+
pattern_attrs[:patternType] = pattern if pattern
|
|
1029
|
+
builder.tag("patternFill", pattern_attrs) do |_|
|
|
1030
|
+
fg = fill[:fg_color] || fill[:color] || fill[:fill_color]
|
|
1031
|
+
fg_attrs = color_tag_attrs(fg)
|
|
1032
|
+
builder.empty_tag("fgColor", fg_attrs) if fg_attrs
|
|
1033
|
+
bg_attrs = color_tag_attrs(fill[:bg_color])
|
|
1034
|
+
builder.empty_tag("bgColor", bg_attrs) if bg_attrs
|
|
1035
|
+
end
|
|
1036
|
+
end
|
|
1037
|
+
end
|
|
1038
|
+
|
|
1039
|
+
def write_dxf_alignment(builder, alignment)
|
|
1040
|
+
return if alignment.nil? || alignment.empty?
|
|
1041
|
+
|
|
1042
|
+
align_attrs = {}
|
|
1043
|
+
align_attrs[:horizontal] = alignment[:horizontal] if alignment[:horizontal]
|
|
1044
|
+
align_attrs[:vertical] = alignment[:vertical] if alignment[:vertical]
|
|
1045
|
+
align_attrs[:wrapText] = "1" if alignment[:wrap_text]
|
|
1046
|
+
align_attrs[:textRotation] = alignment[:text_rotation].to_s if alignment[:text_rotation]
|
|
1047
|
+
align_attrs[:indent] = alignment[:indent].to_s if alignment[:indent]
|
|
1048
|
+
align_attrs[:relativeIndent] = alignment[:relative_indent].to_s if alignment[:relative_indent]
|
|
1049
|
+
align_attrs[:shrinkToFit] = "1" if alignment[:shrink_to_fit]
|
|
1050
|
+
align_attrs[:readingOrder] = alignment[:reading_order].to_s if alignment[:reading_order]
|
|
1051
|
+
align_attrs[:justifyLastLine] = "1" if alignment[:justify_last_line]
|
|
1052
|
+
builder.empty_tag("alignment", align_attrs) unless align_attrs.empty?
|
|
1053
|
+
end
|
|
1054
|
+
|
|
1055
|
+
def write_dxf_border(builder, border)
|
|
1056
|
+
return if border.nil? || border.empty?
|
|
1057
|
+
|
|
1058
|
+
border_attrs = {}
|
|
1059
|
+
border_attrs[:diagonalUp] = "1" if border[:diagonal_up]
|
|
1060
|
+
border_attrs[:diagonalDown] = "1" if border[:diagonal_down]
|
|
1061
|
+
|
|
1062
|
+
builder.tag("border", border_attrs) do |_|
|
|
1063
|
+
%i[left right top bottom diagonal].each do |side|
|
|
1064
|
+
side_data = border[side]
|
|
1065
|
+
next unless side_data
|
|
1066
|
+
|
|
1067
|
+
tag_attrs = {}
|
|
1068
|
+
tag_attrs[:style] = side_data[:style] if side_data[:style]
|
|
1069
|
+
c_attrs = color_tag_attrs(side_data[:color])
|
|
1070
|
+
if c_attrs
|
|
1071
|
+
builder.tag(side.to_s, tag_attrs) do |_|
|
|
1072
|
+
builder.empty_tag("color", c_attrs)
|
|
1073
|
+
end
|
|
1074
|
+
elsif !tag_attrs.empty?
|
|
1075
|
+
builder.empty_tag(side.to_s, tag_attrs)
|
|
1076
|
+
else
|
|
1077
|
+
builder.empty_tag(side.to_s)
|
|
1078
|
+
end
|
|
1079
|
+
end
|
|
1080
|
+
end
|
|
1081
|
+
end
|
|
1082
|
+
|
|
1083
|
+
def write_dxf_protection(builder, protection)
|
|
1084
|
+
return if protection.nil? || protection.empty?
|
|
1085
|
+
|
|
1086
|
+
prot_attrs = {}
|
|
1087
|
+
prot_attrs[:locked] = protection[:locked] ? "1" : "0" unless protection[:locked].nil?
|
|
1088
|
+
prot_attrs[:hidden] = protection[:hidden] ? "1" : "0" unless protection[:hidden].nil?
|
|
1089
|
+
builder.empty_tag("protection", prot_attrs) unless prot_attrs.empty?
|
|
1090
|
+
end
|
|
1091
|
+
|
|
1092
|
+
# : (untyped rgb) -> ::String?
|
|
1093
|
+
def format_rgb_hex(rgb)
|
|
1094
|
+
return nil unless rgb
|
|
1095
|
+
|
|
1096
|
+
str = rgb.to_s.sub(/^#/, "").strip
|
|
1097
|
+
return nil if str.empty?
|
|
1098
|
+
|
|
1099
|
+
case str
|
|
1100
|
+
when /\A[0-9a-fA-F]{8}\z/
|
|
1101
|
+
str.upcase
|
|
1102
|
+
when /\A[0-9a-fA-F]{6}\z/
|
|
1103
|
+
"FF#{str.upcase}"
|
|
1104
|
+
when /\A[0-9a-fA-F]{3}\z/
|
|
1105
|
+
r, g, b = str.chars
|
|
1106
|
+
"FF#{r}#{r}#{g}#{g}#{b}#{b}".upcase
|
|
1107
|
+
else
|
|
1108
|
+
resolved = StyleBuilder.resolve_color(rgb)
|
|
1109
|
+
case resolved
|
|
1110
|
+
when /\A[0-9a-fA-F]{8}\z/
|
|
1111
|
+
resolved.upcase
|
|
1112
|
+
when /\A[0-9a-fA-F]{6}\z/
|
|
1113
|
+
"FF#{resolved.upcase}"
|
|
1114
|
+
end
|
|
1115
|
+
end
|
|
1116
|
+
end
|
|
1117
|
+
|
|
1118
|
+
# : (untyped spec) -> ::Hash[Symbol, ::String]?
|
|
1119
|
+
def color_tag_attrs(spec)
|
|
1120
|
+
return nil unless spec
|
|
1121
|
+
|
|
1122
|
+
spec = spec[:color] if spec.is_a?(Hash) && spec[:color].is_a?(Hash)
|
|
1123
|
+
return nil unless spec
|
|
1124
|
+
|
|
1125
|
+
if spec.is_a?(Hash)
|
|
1126
|
+
if spec[:auto]
|
|
1127
|
+
{ auto: "1" }
|
|
1128
|
+
elsif spec[:rgb] || (spec[:color] && !spec[:color].is_a?(Hash))
|
|
1129
|
+
raw_rgb = spec[:rgb] || spec[:color]
|
|
1130
|
+
fmt = format_rgb_hex(raw_rgb)
|
|
1131
|
+
fmt ? { rgb: fmt } : nil
|
|
1132
|
+
elsif spec[:theme]
|
|
1133
|
+
attrs = { theme: spec[:theme].to_s }
|
|
1134
|
+
attrs[:tint] = spec[:tint].to_s if spec[:tint]
|
|
1135
|
+
attrs
|
|
1136
|
+
elsif spec[:indexed]
|
|
1137
|
+
{ indexed: spec[:indexed].to_s }
|
|
841
1138
|
end
|
|
1139
|
+
elsif spec.is_a?(String) || spec.is_a?(Symbol)
|
|
1140
|
+
fmt = format_rgb_hex(spec)
|
|
1141
|
+
fmt ? { rgb: fmt } : nil
|
|
842
1142
|
end
|
|
843
1143
|
end
|
|
844
1144
|
|
|
@@ -849,6 +1149,7 @@ module Xlsxrb
|
|
|
849
1149
|
|
|
850
1150
|
def escape_xml(value)
|
|
851
1151
|
str = value.to_s
|
|
1152
|
+
str = str.dup.force_encoding(Encoding::UTF_8) if str.encoding == Encoding::BINARY
|
|
852
1153
|
str = str.gsub(INVALID_XML_CHARS_RE, "") if str.match?(INVALID_XML_CHARS_RE)
|
|
853
1154
|
str.match?(ESCAPE_RE) ? str.gsub(ESCAPE_RE, ESCAPE_MAP) : str
|
|
854
1155
|
end
|
|
@@ -873,19 +1174,26 @@ module Xlsxrb
|
|
|
873
1174
|
buf << "<i/>" if font[:italic]
|
|
874
1175
|
buf << "<strike/>" if font[:strike]
|
|
875
1176
|
if font[:underline]
|
|
876
|
-
if font[:underline] == true
|
|
1177
|
+
if font[:underline] == true || font[:underline].to_s == "single"
|
|
877
1178
|
buf << "<u/>"
|
|
878
1179
|
else
|
|
879
|
-
|
|
1180
|
+
mapped_u = StyleBuilder::UNDERLINE_MAP[font[:underline].to_sym] || font[:underline]
|
|
1181
|
+
buf << '<u val="' << mapped_u.to_s << '"/>'
|
|
880
1182
|
end
|
|
881
1183
|
end
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
if font[:
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1184
|
+
va_val = font[:vert_align] || font[:vertical_align]
|
|
1185
|
+
va_val ||= "superscript" if font[:superscript]
|
|
1186
|
+
va_val ||= "subscript" if font[:subscript]
|
|
1187
|
+
if va_val
|
|
1188
|
+
mapped_va = StyleBuilder::VERT_ALIGN_MAP[va_val.to_sym] || va_val.to_s
|
|
1189
|
+
buf << '<vertAlign val="' << mapped_va << '"/>'
|
|
1190
|
+
end
|
|
1191
|
+
font_sz = font[:sz] || font[:size]
|
|
1192
|
+
buf << '<sz val="' << font_sz.to_s << '"/>' if font_sz
|
|
1193
|
+
color_attrs = color_tag_attrs(font[:color] || font)
|
|
1194
|
+
if color_attrs
|
|
1195
|
+
attrs_str = color_attrs.map { |k, v| %(#{k}="#{v}") }.join(" ")
|
|
1196
|
+
buf << "<color #{attrs_str}/>"
|
|
889
1197
|
end
|
|
890
1198
|
buf << '<rFont val="' << escape_xml(font[:name]) << '"/>' if font[:name]
|
|
891
1199
|
buf << '<family val="' << font[:family].to_s << '"/>' if font[:family]
|
|
@@ -894,17 +1202,11 @@ module Xlsxrb
|
|
|
894
1202
|
else
|
|
895
1203
|
buf << "<r><t>"
|
|
896
1204
|
end
|
|
897
|
-
buf << escape_xml(run[:text]) << "</t></r>"
|
|
1205
|
+
buf << escape_xml(run[:text].to_s) << "</t></r>"
|
|
898
1206
|
end
|
|
899
1207
|
buf << "</si>"
|
|
900
1208
|
else
|
|
901
|
-
|
|
902
|
-
needs_space = s.start_with?(" ", "\t", "\n") || s.end_with?(" ", "\t", "\n")
|
|
903
|
-
if needs_space
|
|
904
|
-
buf << '<si><t xml:space="preserve">' << escape_xml(s) << "</t></si>"
|
|
905
|
-
else
|
|
906
|
-
buf << "<si><t>" << escape_xml(s) << "</t></si>"
|
|
907
|
-
end
|
|
1209
|
+
buf << "<si><t>" << escape_xml(str.to_s) << "</t></si>"
|
|
908
1210
|
end
|
|
909
1211
|
|
|
910
1212
|
if buf.bytesize >= 32_768
|
|
@@ -919,16 +1221,17 @@ module Xlsxrb
|
|
|
919
1221
|
zip.finish_entry
|
|
920
1222
|
end
|
|
921
1223
|
|
|
922
|
-
def write_worksheet_xml(io, sheet, drawing_rid: nil, sheet_protection: nil,
|
|
1224
|
+
def write_worksheet_xml(io, sheet, drawing_rid: nil, picture_rid: nil, sheet_protection: nil,
|
|
923
1225
|
protected_ranges: nil, auto_filter: nil, filter_columns: nil,
|
|
924
1226
|
sort_state: nil, merge_cells: nil, conditional_formats: nil,
|
|
925
1227
|
data_validations: nil, hyperlinks: nil,
|
|
926
1228
|
print_options: nil, page_margins: nil, page_setup: nil,
|
|
927
1229
|
header_footer: nil, row_breaks: nil, col_breaks: nil,
|
|
928
|
-
cell_watches: nil, freeze_pane: nil, split_pane: nil,
|
|
1230
|
+
cell_watches: nil, ignored_errors: nil, freeze_pane: nil, split_pane: nil,
|
|
929
1231
|
selection: nil, sheet_view: nil, sheet_properties: nil,
|
|
930
1232
|
sheet_format: nil, tables: nil, table_start_rid: nil,
|
|
931
|
-
legacy_drawing_rid: nil, sparkline_groups: nil
|
|
1233
|
+
legacy_drawing_rid: nil, sparkline_groups: nil, phonetic_pr: nil,
|
|
1234
|
+
sheet_calc_pr: nil)
|
|
932
1235
|
date1904 = @workbook_properties&.[](:date1904) ? true : false
|
|
933
1236
|
ws = WorksheetWriter.new(io, date1904: date1904)
|
|
934
1237
|
ws.start(
|
|
@@ -964,6 +1267,7 @@ module Xlsxrb
|
|
|
964
1267
|
end
|
|
965
1268
|
ws.finish(
|
|
966
1269
|
drawing_rid: drawing_rid,
|
|
1270
|
+
picture_rid: picture_rid,
|
|
967
1271
|
sheet_protection: sheet_protection,
|
|
968
1272
|
protected_ranges: protected_ranges,
|
|
969
1273
|
auto_filter: auto_filter,
|
|
@@ -980,10 +1284,13 @@ module Xlsxrb
|
|
|
980
1284
|
row_breaks: row_breaks,
|
|
981
1285
|
col_breaks: col_breaks,
|
|
982
1286
|
cell_watches: cell_watches,
|
|
1287
|
+
ignored_errors: ignored_errors,
|
|
983
1288
|
tables: tables,
|
|
984
1289
|
table_start_rid: table_start_rid,
|
|
985
1290
|
legacy_drawing_rid: legacy_drawing_rid,
|
|
986
|
-
sparkline_groups: sparkline_groups
|
|
1291
|
+
sparkline_groups: sparkline_groups,
|
|
1292
|
+
phonetic_pr: phonetic_pr,
|
|
1293
|
+
sheet_calc_pr: sheet_calc_pr
|
|
987
1294
|
)
|
|
988
1295
|
end
|
|
989
1296
|
|
|
@@ -1002,14 +1309,36 @@ module Xlsxrb
|
|
|
1002
1309
|
t_attrs[:totalsRowCount] = tbl[:totals_row_count].to_s if tbl[:totals_row_count]&.positive?
|
|
1003
1310
|
b.open_tag("table", t_attrs)
|
|
1004
1311
|
|
|
1005
|
-
# Auto filter for the table range
|
|
1006
|
-
b.empty_tag("autoFilter", { ref: tbl[:ref] })
|
|
1312
|
+
# Auto filter for the table range unless auto_filter: false
|
|
1313
|
+
b.empty_tag("autoFilter", { ref: tbl[:ref] }) unless tbl[:auto_filter] == false
|
|
1007
1314
|
|
|
1008
1315
|
# Table columns
|
|
1009
1316
|
columns = tbl[:columns] || []
|
|
1010
1317
|
b.open_tag("tableColumns", { count: columns.size.to_s })
|
|
1011
|
-
columns.each_with_index do |
|
|
1012
|
-
|
|
1318
|
+
columns.each_with_index do |col, ci|
|
|
1319
|
+
col_name = col.is_a?(Hash) ? col[:name] : col
|
|
1320
|
+
col_attrs = { id: (ci + 1).to_s, name: col_name.to_s }
|
|
1321
|
+
calc_formula = nil
|
|
1322
|
+
tot_formula = nil
|
|
1323
|
+
if col.is_a?(Hash)
|
|
1324
|
+
calc_formula = col[:calculated_column_formula] || col[:data_formula] || col[:formula]
|
|
1325
|
+
tot_formula = col[:totals_row_formula] || col[:total_row_formula] || col[:total_formula]
|
|
1326
|
+
trf = col[:totals_row_function] || ("custom" if tot_formula)
|
|
1327
|
+
col_attrs[:totalsRowFunction] = trf.to_s if trf
|
|
1328
|
+
col_attrs[:totalsRowLabel] = col[:totals_row_label].to_s if col[:totals_row_label]
|
|
1329
|
+
col_attrs[:dataDxfId] = col[:data_dxf_id].to_s if col[:data_dxf_id]
|
|
1330
|
+
col_attrs[:totalsRowDxfId] = col[:totals_row_dxf_id].to_s if col[:totals_row_dxf_id]
|
|
1331
|
+
col_attrs[:headerRowDxfId] = col[:header_row_dxf_id].to_s if col[:header_row_dxf_id]
|
|
1332
|
+
col_attrs[:dataCellStyle] = col[:data_cell_style].to_s if col[:data_cell_style]
|
|
1333
|
+
end
|
|
1334
|
+
if calc_formula || tot_formula
|
|
1335
|
+
b.open_tag("tableColumn", col_attrs)
|
|
1336
|
+
b.tag("calculatedColumnFormula") { |cb| cb.text(calc_formula.to_s) } if calc_formula
|
|
1337
|
+
b.tag("totalsRowFormula") { |tb| tb.text(tot_formula.to_s) } if tot_formula
|
|
1338
|
+
b.close_tag("tableColumn")
|
|
1339
|
+
else
|
|
1340
|
+
b.empty_tag("tableColumn", col_attrs)
|
|
1341
|
+
end
|
|
1013
1342
|
end
|
|
1014
1343
|
b.close_tag("tableColumns")
|
|
1015
1344
|
|
|
@@ -1124,6 +1453,62 @@ module Xlsxrb
|
|
|
1124
1453
|
b.close_tag("Properties")
|
|
1125
1454
|
io.string
|
|
1126
1455
|
end
|
|
1456
|
+
|
|
1457
|
+
def resolve_active_tab_index
|
|
1458
|
+
active_tab = @workbook_properties&.[](:active_tab)
|
|
1459
|
+
if active_tab.nil?
|
|
1460
|
+
first_vis_idx = @sheets.find_index do |s|
|
|
1461
|
+
state = s.respond_to?(:state) ? s.state : s[:state]
|
|
1462
|
+
state.nil? || state.to_sym == :visible
|
|
1463
|
+
end
|
|
1464
|
+
active_tab = first_vis_idx if first_vis_idx&.positive?
|
|
1465
|
+
end
|
|
1466
|
+
|
|
1467
|
+
if active_tab.is_a?(String) || active_tab.is_a?(Symbol)
|
|
1468
|
+
target_name = active_tab.to_s
|
|
1469
|
+
idx = @sheets.find_index do |s|
|
|
1470
|
+
(s.respond_to?(:name) ? s.name : s[:name]) == target_name
|
|
1471
|
+
end
|
|
1472
|
+
raise ArgumentError, "Active sheet '#{target_name}' not found" if idx.nil?
|
|
1473
|
+
|
|
1474
|
+
active_tab = idx
|
|
1475
|
+
end
|
|
1476
|
+
|
|
1477
|
+
if active_tab
|
|
1478
|
+
target_sheet = @sheets[active_tab]
|
|
1479
|
+
raise ArgumentError, "Active sheet index #{active_tab} out of bounds" if target_sheet.nil?
|
|
1480
|
+
|
|
1481
|
+
state = target_sheet.respond_to?(:state) ? target_sheet.state : target_sheet[:state]
|
|
1482
|
+
if state && state.to_sym != :visible
|
|
1483
|
+
name = target_sheet.respond_to?(:name) ? target_sheet.name : target_sheet[:name]
|
|
1484
|
+
raise ArgumentError, "Cannot set hidden sheet '#{name}' as active sheet"
|
|
1485
|
+
end
|
|
1486
|
+
end
|
|
1487
|
+
|
|
1488
|
+
active_tab
|
|
1489
|
+
end
|
|
1490
|
+
|
|
1491
|
+
def resolve_first_sheet_index
|
|
1492
|
+
first_sheet = @workbook_properties&.[](:first_sheet)
|
|
1493
|
+
return nil if first_sheet.nil?
|
|
1494
|
+
|
|
1495
|
+
if first_sheet.is_a?(String) || first_sheet.is_a?(Symbol)
|
|
1496
|
+
target_name = first_sheet.to_s
|
|
1497
|
+
idx = @sheets.find_index do |s|
|
|
1498
|
+
(s.respond_to?(:name) ? s.name : s[:name]) == target_name
|
|
1499
|
+
end
|
|
1500
|
+
raise ArgumentError, "First sheet '#{target_name}' not found" if idx.nil?
|
|
1501
|
+
|
|
1502
|
+
first_sheet = idx
|
|
1503
|
+
end
|
|
1504
|
+
|
|
1505
|
+
if first_sheet
|
|
1506
|
+
target_sheet = @sheets[first_sheet]
|
|
1507
|
+
raise ArgumentError, "First sheet index #{first_sheet} out of bounds" if target_sheet.nil?
|
|
1508
|
+
end
|
|
1509
|
+
|
|
1510
|
+
first_sheet
|
|
1511
|
+
end
|
|
1127
1512
|
end
|
|
1128
1513
|
end
|
|
1129
1514
|
end
|