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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +57 -0
- data/README.md +2 -0
- 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 +85 -4
- data/lib/xlsxrb/elements/worksheet.rb +424 -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.rb +12 -14
- 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 +53 -15
- data/lib/xlsxrb/ooxml/workbook_writer.rb +65 -14
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +319 -79
- data/lib/xlsxrb/ooxml/worksheet_writer.rb +206 -61
- data/lib/xlsxrb/ooxml/writer/features_xml.rb +4 -2
- data/lib/xlsxrb/ooxml/writer.rb +14 -0
- data/lib/xlsxrb/ooxml/zip_reader.rb +109 -25
- data/lib/xlsxrb/stream_row.rb +243 -16
- data/lib/xlsxrb/stream_sheet.rb +414 -15
- data/lib/xlsxrb/stream_writer.rb +213 -17
- data/lib/xlsxrb/style_builder.rb +308 -22
- data/lib/xlsxrb/utils.rb +158 -0
- data/lib/xlsxrb/version.rb +1 -1
- data/lib/xlsxrb/workbook_builder.rb +73 -3
- data/lib/xlsxrb/worksheet_builder.rb +227 -16
- data/lib/xlsxrb.rb +578 -79
- 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 +51 -2
- data/sig/generated/xlsxrb/elements/worksheet.rbs +231 -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/worksheet_parser.rbs +28 -6
- data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +3 -1
- data/sig/generated/xlsxrb/ooxml/writer.rbs +2 -0
- data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +10 -0
- data/sig/generated/xlsxrb/stream_row.rbs +100 -6
- data/sig/generated/xlsxrb/stream_sheet.rbs +247 -13
- data/sig/generated/xlsxrb/stream_writer.rbs +120 -14
- data/sig/generated/xlsxrb/style_builder.rbs +55 -18
- data/sig/generated/xlsxrb/utils.rbs +119 -0
- data/sig/generated/xlsxrb/workbook_builder.rbs +26 -0
- data/sig/generated/xlsxrb/worksheet_builder.rbs +107 -11
- data/sig/generated/xlsxrb.rbs +186 -27
- metadata +11 -1
data/lib/xlsxrb/style_builder.rb
CHANGED
|
@@ -20,6 +20,281 @@ module Xlsxrb
|
|
|
20
20
|
grey: "FF808080"
|
|
21
21
|
}.freeze
|
|
22
22
|
|
|
23
|
+
CSS_COLORS = {
|
|
24
|
+
aliceblue: "F0F8FF",
|
|
25
|
+
antiquewhite: "FAEBD7",
|
|
26
|
+
aqua: "00FFFF",
|
|
27
|
+
aquamarine: "7FFFD4",
|
|
28
|
+
azure: "F0FFFF",
|
|
29
|
+
beige: "F5F5DC",
|
|
30
|
+
bisque: "FFE4C4",
|
|
31
|
+
black: "000000",
|
|
32
|
+
blanchedalmond: "FFEBCD",
|
|
33
|
+
blue: "0000FF",
|
|
34
|
+
blueviolet: "8A2BE2",
|
|
35
|
+
brown: "A52A2A",
|
|
36
|
+
burlywood: "DEB887",
|
|
37
|
+
cadetblue: "5F9EA0",
|
|
38
|
+
chartreuse: "7FFF00",
|
|
39
|
+
chocolate: "D2691E",
|
|
40
|
+
coral: "FF7F50",
|
|
41
|
+
cornflowerblue: "6495ED",
|
|
42
|
+
cornsilk: "FFF8DC",
|
|
43
|
+
crimson: "DC143C",
|
|
44
|
+
cyan: "00FFFF",
|
|
45
|
+
darkblue: "00008B",
|
|
46
|
+
darkcyan: "008B8B",
|
|
47
|
+
darkgoldenrod: "B8860B",
|
|
48
|
+
darkgray: "A9A9A9",
|
|
49
|
+
darkgrey: "A9A9A9",
|
|
50
|
+
darkgreen: "006400",
|
|
51
|
+
darkkhaki: "BDB76B",
|
|
52
|
+
darkmagenta: "8B008B",
|
|
53
|
+
darkolivegreen: "556B2F",
|
|
54
|
+
darkorange: "FF8C00",
|
|
55
|
+
darkorchid: "9932CC",
|
|
56
|
+
darkred: "8B0000",
|
|
57
|
+
darksalmon: "E9967A",
|
|
58
|
+
darkseagreen: "8FBC8F",
|
|
59
|
+
darkslateblue: "483D8B",
|
|
60
|
+
darkslategray: "2F4F4F",
|
|
61
|
+
darkslategrey: "2F4F4F",
|
|
62
|
+
darkturquoise: "00CED1",
|
|
63
|
+
darkviolet: "9400D3",
|
|
64
|
+
deeppink: "FF1493",
|
|
65
|
+
deepskyblue: "00BFFF",
|
|
66
|
+
dimgray: "696969",
|
|
67
|
+
dimgrey: "696969",
|
|
68
|
+
dodgerblue: "1E90FF",
|
|
69
|
+
firebrick: "B22222",
|
|
70
|
+
floralwhite: "FFFAF0",
|
|
71
|
+
forestgreen: "228B22",
|
|
72
|
+
fuchsia: "FF00FF",
|
|
73
|
+
gainsboro: "DCDCDC",
|
|
74
|
+
ghostwhite: "F8F8FF",
|
|
75
|
+
gold: "FFD700",
|
|
76
|
+
goldenrod: "DAA520",
|
|
77
|
+
gray: "808080",
|
|
78
|
+
grey: "808080",
|
|
79
|
+
green: "00FF00",
|
|
80
|
+
greenyellow: "ADFF2F",
|
|
81
|
+
honeydew: "F0FFF0",
|
|
82
|
+
hotpink: "FF69B4",
|
|
83
|
+
indianred: "CD5C5C",
|
|
84
|
+
indigo: "4B0082",
|
|
85
|
+
ivory: "FFFFF0",
|
|
86
|
+
khaki: "F0E68C",
|
|
87
|
+
lavender: "E6E6FA",
|
|
88
|
+
lavenderblush: "FFF0F5",
|
|
89
|
+
lawngreen: "7CFC00",
|
|
90
|
+
lemonchiffon: "FFFACD",
|
|
91
|
+
lightblue: "ADD8E6",
|
|
92
|
+
lightcoral: "F08080",
|
|
93
|
+
lightcyan: "E0FFFF",
|
|
94
|
+
lightgoldenrodyellow: "FAFAD2",
|
|
95
|
+
lightgray: "D3D3D3",
|
|
96
|
+
lightgrey: "D3D3D3",
|
|
97
|
+
lightgreen: "90EE90",
|
|
98
|
+
lightpink: "FFB6C1",
|
|
99
|
+
lightsalmon: "FFA07A",
|
|
100
|
+
lightseagreen: "20B2AA",
|
|
101
|
+
lightskyblue: "87CEFA",
|
|
102
|
+
lightslategray: "778899",
|
|
103
|
+
lightslategrey: "778899",
|
|
104
|
+
lightsteelblue: "B0C4DE",
|
|
105
|
+
lightyellow: "FFFFE0",
|
|
106
|
+
lime: "00FF00",
|
|
107
|
+
limegreen: "32CD32",
|
|
108
|
+
linen: "FAF0E6",
|
|
109
|
+
magenta: "FF00FF",
|
|
110
|
+
maroon: "800000",
|
|
111
|
+
mediumaquamarine: "66CDAA",
|
|
112
|
+
mediumblue: "0000CD",
|
|
113
|
+
mediumorchid: "BA55D3",
|
|
114
|
+
mediumpurple: "9370DB",
|
|
115
|
+
mediumseagreen: "3CB371",
|
|
116
|
+
mediumslateblue: "7B68EE",
|
|
117
|
+
mediumspringgreen: "00FA9A",
|
|
118
|
+
mediumturquoise: "48D1CC",
|
|
119
|
+
mediumvioletred: "C71585",
|
|
120
|
+
midnightblue: "191970",
|
|
121
|
+
mintcream: "F5FFFA",
|
|
122
|
+
mistyrose: "FFE4E1",
|
|
123
|
+
moccasin: "FFE4B5",
|
|
124
|
+
navajowhite: "FFDEAD",
|
|
125
|
+
navy: "000080",
|
|
126
|
+
oldlace: "FDF5E6",
|
|
127
|
+
olive: "808000",
|
|
128
|
+
olivedrab: "6B8E23",
|
|
129
|
+
orange: "FFA500",
|
|
130
|
+
orangered: "FF4500",
|
|
131
|
+
orchid: "DA70D6",
|
|
132
|
+
palegoldenrod: "EEE8AA",
|
|
133
|
+
palegreen: "98FB98",
|
|
134
|
+
paleturquoise: "AFEEEE",
|
|
135
|
+
palevioletred: "DB7093",
|
|
136
|
+
papayawhip: "FFEFD5",
|
|
137
|
+
peachpuff: "FFDAB9",
|
|
138
|
+
peru: "CD853F",
|
|
139
|
+
pink: "FFC0CB",
|
|
140
|
+
plum: "DDA0DD",
|
|
141
|
+
powderblue: "B0E0E6",
|
|
142
|
+
purple: "800080",
|
|
143
|
+
rebeccapurple: "663399",
|
|
144
|
+
red: "FF0000",
|
|
145
|
+
rosybrown: "BC8F8F",
|
|
146
|
+
royalblue: "4169E1",
|
|
147
|
+
saddlebrown: "8B4513",
|
|
148
|
+
salmon: "FA8072",
|
|
149
|
+
sandybrown: "F4A460",
|
|
150
|
+
seagreen: "2E8B57",
|
|
151
|
+
seashell: "FFF5EE",
|
|
152
|
+
sienna: "A0522D",
|
|
153
|
+
silver: "C0C0C0",
|
|
154
|
+
skyblue: "87CEEB",
|
|
155
|
+
slateblue: "6A5ACD",
|
|
156
|
+
slategray: "708090",
|
|
157
|
+
slategrey: "708090",
|
|
158
|
+
snow: "FFFAFA",
|
|
159
|
+
springgreen: "00FF7F",
|
|
160
|
+
steelblue: "4682B4",
|
|
161
|
+
tan: "D2B48C",
|
|
162
|
+
teal: "008080",
|
|
163
|
+
thistle: "D8BFD8",
|
|
164
|
+
tomato: "FF6347",
|
|
165
|
+
turquoise: "40E0D0",
|
|
166
|
+
violet: "EE82EE",
|
|
167
|
+
wheat: "F5DEB3",
|
|
168
|
+
white: "FFFFFF",
|
|
169
|
+
whitesmoke: "F5F5F5",
|
|
170
|
+
yellow: "FFFF00",
|
|
171
|
+
yellowgreen: "9ACD32"
|
|
172
|
+
}.freeze
|
|
173
|
+
|
|
174
|
+
BORDER_STYLES = {
|
|
175
|
+
none: :none,
|
|
176
|
+
thin: :thin,
|
|
177
|
+
medium: :medium,
|
|
178
|
+
dashed: :dashed,
|
|
179
|
+
dotted: :dotted,
|
|
180
|
+
thick: :thick,
|
|
181
|
+
double: :double,
|
|
182
|
+
hair: :hair,
|
|
183
|
+
medium_dashed: :mediumDashed,
|
|
184
|
+
mediumdashed: :mediumDashed,
|
|
185
|
+
mediumDashed: :mediumDashed,
|
|
186
|
+
dash_dot: :dashDot,
|
|
187
|
+
dashdot: :dashDot,
|
|
188
|
+
dashDot: :dashDot,
|
|
189
|
+
medium_dash_dot: :mediumDashDot,
|
|
190
|
+
mediumdashdot: :mediumDashDot,
|
|
191
|
+
mediumDashDot: :mediumDashDot,
|
|
192
|
+
dash_dot_dot: :dashDotDot,
|
|
193
|
+
dashdotdot: :dashDotDot,
|
|
194
|
+
dashDotDot: :dashDotDot,
|
|
195
|
+
medium_dash_dot_dot: :mediumDashDotDot,
|
|
196
|
+
mediumdashdotdot: :mediumDashDotDot,
|
|
197
|
+
mediumDashDotDot: :mediumDashDotDot,
|
|
198
|
+
slant_dash_dot: :slantDashDot,
|
|
199
|
+
slantdashdot: :slantDashDot,
|
|
200
|
+
slantDashDot: :slantDashDot
|
|
201
|
+
}.freeze
|
|
202
|
+
|
|
203
|
+
BORDER_INDEX_MAP = {
|
|
204
|
+
0 => :none,
|
|
205
|
+
1 => :thin,
|
|
206
|
+
2 => :medium,
|
|
207
|
+
3 => :dashed,
|
|
208
|
+
4 => :dotted,
|
|
209
|
+
5 => :thick,
|
|
210
|
+
6 => :double,
|
|
211
|
+
7 => :hair,
|
|
212
|
+
8 => :mediumDashed,
|
|
213
|
+
9 => :dashDot,
|
|
214
|
+
10 => :mediumDashDot,
|
|
215
|
+
11 => :dashDotDot,
|
|
216
|
+
12 => :mediumDashDotDot,
|
|
217
|
+
13 => :slantDashDot
|
|
218
|
+
}.freeze
|
|
219
|
+
|
|
220
|
+
# Normalizes a color definition (CSS color name, hex code, or RGB integer) to an OpenXML hex color string.
|
|
221
|
+
#
|
|
222
|
+
# @example
|
|
223
|
+
# StyleBuilder.normalize_color(:navy) #=> "FF000080"
|
|
224
|
+
# StyleBuilder.normalize_color("#FF0000") #=> "FFFF0000"
|
|
225
|
+
# StyleBuilder.normalize_color("#F00") #=> "FFFF0000"
|
|
226
|
+
# StyleBuilder.normalize_color("FF0000", alpha: false) #=> "FF0000"
|
|
227
|
+
# StyleBuilder.normalize_color(0xFF0000) #=> "FFFF0000"
|
|
228
|
+
#
|
|
229
|
+
# @param color [String, Symbol, Integer, nil]
|
|
230
|
+
# @param alpha [Boolean] Whether to include the alpha channel ("AARRGGBB" vs "RRGGBB").
|
|
231
|
+
# @return [String, nil]
|
|
232
|
+
# @api public
|
|
233
|
+
#: (String | Symbol | Integer | nil color, ?alpha: bool) -> String?
|
|
234
|
+
def self.normalize_color(color, alpha: true)
|
|
235
|
+
return nil if color.nil?
|
|
236
|
+
|
|
237
|
+
hex = case color
|
|
238
|
+
when Symbol, defined?(String) && String
|
|
239
|
+
str = color.to_s.strip
|
|
240
|
+
return nil if str.empty?
|
|
241
|
+
|
|
242
|
+
# Strip leading colon (e.g. ":red")
|
|
243
|
+
str = str.sub(/^:/, "")
|
|
244
|
+
return nil if str.empty?
|
|
245
|
+
|
|
246
|
+
sym_key = str.downcase.to_sym
|
|
247
|
+
if CSS_COLORS.key?(sym_key)
|
|
248
|
+
CSS_COLORS[sym_key]
|
|
249
|
+
else
|
|
250
|
+
# Hex string parsing
|
|
251
|
+
clean_hex = str.sub(/^#/, "")
|
|
252
|
+
case clean_hex.length
|
|
253
|
+
when 3
|
|
254
|
+
if clean_hex.match?(/\A[0-9a-fA-F]{3}\z/)
|
|
255
|
+
r, g, b = clean_hex.chars
|
|
256
|
+
"#{r}#{r}#{g}#{g}#{b}#{b}".upcase
|
|
257
|
+
end
|
|
258
|
+
when 6
|
|
259
|
+
clean_hex.upcase if clean_hex.match?(/\A[0-9a-fA-F]{6}\z/)
|
|
260
|
+
when 8
|
|
261
|
+
if clean_hex.match?(/\A[0-9a-fA-F]{8}\z/)
|
|
262
|
+
return alpha ? clean_hex.upcase : clean_hex[2..7].upcase
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
when Integer
|
|
267
|
+
format("%06X", color & 0xFFFFFF)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
return nil unless hex
|
|
271
|
+
|
|
272
|
+
alpha ? "FF#{hex}" : hex
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Normalizes a border style symbol, string, or integer index to an OOXML ST_BorderStyle symbol.
|
|
276
|
+
#
|
|
277
|
+
# @example
|
|
278
|
+
# StyleBuilder.normalize_border_style(:border_thin) #=> :thin
|
|
279
|
+
# StyleBuilder.normalize_border_style(1) #=> :thin
|
|
280
|
+
# StyleBuilder.normalize_border_style(:medium_dashed) #=> :mediumDashed
|
|
281
|
+
# StyleBuilder.normalize_border_style("double") #=> :double
|
|
282
|
+
# StyleBuilder.normalize_border_style(0) #=> :none
|
|
283
|
+
#
|
|
284
|
+
# @param style [Symbol, String, Integer, nil]
|
|
285
|
+
# @return [Symbol, nil]
|
|
286
|
+
# @api public
|
|
287
|
+
#: (Symbol | String | Integer | nil style) -> Symbol?
|
|
288
|
+
def self.normalize_border_style(style)
|
|
289
|
+
return nil if style.nil?
|
|
290
|
+
|
|
291
|
+
return BORDER_INDEX_MAP[style] if style.is_a?(Integer)
|
|
292
|
+
|
|
293
|
+
str = style.to_s.sub(/^border_/, "")
|
|
294
|
+
sym = str.to_sym
|
|
295
|
+
BORDER_STYLES[sym] || BORDER_STYLES[str.downcase.to_sym]
|
|
296
|
+
end
|
|
297
|
+
|
|
23
298
|
# Resolves a color symbol, string, or prefix to an aRGB hex string.
|
|
24
299
|
#
|
|
25
300
|
# @param color [Symbol, String, Object, nil]
|
|
@@ -28,10 +303,15 @@ module Xlsxrb
|
|
|
28
303
|
def self.resolve_color(color)
|
|
29
304
|
return nil unless color
|
|
30
305
|
|
|
306
|
+
return Colors.palette_color(color, alpha: true) if color.is_a?(Integer) && defined?(Colors) && color >= 0 && color < Colors::INDEXED_COLORS.size
|
|
307
|
+
|
|
31
308
|
if color.is_a?(Symbol) || (color.is_a?(String) && color.start_with?(":") && color.length > 1)
|
|
32
309
|
key = color.to_s.sub(/^:/, "").to_sym
|
|
33
|
-
return COLORS[key] || color.to_s
|
|
310
|
+
return COLORS[key] || (CSS_COLORS[key] ? "FF#{CSS_COLORS[key]}" : nil) || color.to_s
|
|
34
311
|
end
|
|
312
|
+
|
|
313
|
+
return normalize_color(color, alpha: true) || color.to_s if color.is_a?(String) && color.start_with?("#")
|
|
314
|
+
|
|
35
315
|
color.to_s
|
|
36
316
|
end
|
|
37
317
|
|
|
@@ -314,75 +594,81 @@ module Xlsxrb
|
|
|
314
594
|
end
|
|
315
595
|
|
|
316
596
|
# Sets all borders.
|
|
317
|
-
# @param style [String, Symbol] The border style.
|
|
597
|
+
# @param style [String, Symbol, Integer] The border style.
|
|
318
598
|
# @param color [String, Symbol, nil] The color.
|
|
319
599
|
# @return [self]
|
|
320
600
|
# @api public
|
|
321
|
-
#: (?style: String | Symbol, ?color: String | Symbol | nil) -> self
|
|
601
|
+
#: (?style: String | Symbol | Integer, ?color: String | Symbol | nil) -> self
|
|
322
602
|
def border_all(style: "thin", color: nil)
|
|
603
|
+
norm_style = self.class.normalize_border_style(style)&.to_s || style.to_s
|
|
323
604
|
color_opt = color ? { color: resolve_color(color) } : {}
|
|
324
|
-
@border_props[:left] = { style:
|
|
325
|
-
@border_props[:right] = { style:
|
|
326
|
-
@border_props[:top] = { style:
|
|
327
|
-
@border_props[:bottom] = { style:
|
|
605
|
+
@border_props[:left] = { style: norm_style, **color_opt }
|
|
606
|
+
@border_props[:right] = { style: norm_style, **color_opt }
|
|
607
|
+
@border_props[:top] = { style: norm_style, **color_opt }
|
|
608
|
+
@border_props[:bottom] = { style: norm_style, **color_opt }
|
|
328
609
|
self
|
|
329
610
|
end
|
|
330
611
|
|
|
331
612
|
# Sets the left border.
|
|
332
|
-
# @param style [String, Symbol] The border style.
|
|
613
|
+
# @param style [String, Symbol, Integer] The border style.
|
|
333
614
|
# @param color [String, Symbol, nil] The color.
|
|
334
615
|
# @return [self]
|
|
335
616
|
# @api public
|
|
336
|
-
#: (?style: String | Symbol, ?color: String | Symbol | nil) -> self
|
|
617
|
+
#: (?style: String | Symbol | Integer, ?color: String | Symbol | nil) -> self
|
|
337
618
|
def border_left(style: "thin", color: nil)
|
|
338
|
-
|
|
619
|
+
norm_style = self.class.normalize_border_style(style)&.to_s || style.to_s
|
|
620
|
+
@border_props[:left] = { style: norm_style, color: resolve_color(color) }.compact
|
|
339
621
|
self
|
|
340
622
|
end
|
|
341
623
|
|
|
342
624
|
# Sets the right border.
|
|
343
|
-
# @param style [String, Symbol] The border style.
|
|
625
|
+
# @param style [String, Symbol, Integer] The border style.
|
|
344
626
|
# @param color [String, Symbol, nil] The color.
|
|
345
627
|
# @return [self]
|
|
346
628
|
# @api public
|
|
347
|
-
#: (?style: String | Symbol, ?color: String | Symbol | nil) -> self
|
|
629
|
+
#: (?style: String | Symbol | Integer, ?color: String | Symbol | nil) -> self
|
|
348
630
|
def border_right(style: "thin", color: nil)
|
|
349
|
-
|
|
631
|
+
norm_style = self.class.normalize_border_style(style)&.to_s || style.to_s
|
|
632
|
+
@border_props[:right] = { style: norm_style, color: resolve_color(color) }.compact
|
|
350
633
|
self
|
|
351
634
|
end
|
|
352
635
|
|
|
353
636
|
# Sets the top border.
|
|
354
|
-
# @param style [String, Symbol] The border style.
|
|
637
|
+
# @param style [String, Symbol, Integer] The border style.
|
|
355
638
|
# @param color [String, Symbol, nil] The color.
|
|
356
639
|
# @return [self]
|
|
357
640
|
# @api public
|
|
358
|
-
#: (?style: String | Symbol, ?color: String | Symbol | nil) -> self
|
|
641
|
+
#: (?style: String | Symbol | Integer, ?color: String | Symbol | nil) -> self
|
|
359
642
|
def border_top(style: "thin", color: nil)
|
|
360
|
-
|
|
643
|
+
norm_style = self.class.normalize_border_style(style)&.to_s || style.to_s
|
|
644
|
+
@border_props[:top] = { style: norm_style, color: resolve_color(color) }.compact
|
|
361
645
|
self
|
|
362
646
|
end
|
|
363
647
|
|
|
364
648
|
# Sets the bottom border.
|
|
365
|
-
# @param style [String, Symbol] The border style.
|
|
649
|
+
# @param style [String, Symbol, Integer] The border style.
|
|
366
650
|
# @param color [String, Symbol, nil] The color.
|
|
367
651
|
# @return [self]
|
|
368
652
|
# @api public
|
|
369
|
-
#: (?style: String | Symbol, ?color: String | Symbol | nil) -> self
|
|
653
|
+
#: (?style: String | Symbol | Integer, ?color: String | Symbol | nil) -> self
|
|
370
654
|
def border_bottom(style: "thin", color: nil)
|
|
371
|
-
|
|
655
|
+
norm_style = self.class.normalize_border_style(style)&.to_s || style.to_s
|
|
656
|
+
@border_props[:bottom] = { style: norm_style, color: resolve_color(color) }.compact
|
|
372
657
|
self
|
|
373
658
|
end
|
|
374
659
|
|
|
375
660
|
# rubocop:disable Naming/MethodParameterName
|
|
376
661
|
# Sets diagonal borders.
|
|
377
|
-
# @param style [String, Symbol] The border style.
|
|
662
|
+
# @param style [String, Symbol, Integer] The border style.
|
|
378
663
|
# @param color [String, Symbol, nil] The color.
|
|
379
664
|
# @param up [Boolean] Diagonal up.
|
|
380
665
|
# @param down [Boolean] Diagonal down.
|
|
381
666
|
# @return [self]
|
|
382
667
|
# @api public
|
|
383
|
-
#: (?style: String | Symbol, ?color: String | Symbol | nil, ?up: bool, ?down: bool) -> self
|
|
668
|
+
#: (?style: String | Symbol | Integer, ?color: String | Symbol | nil, ?up: bool, ?down: bool) -> self
|
|
384
669
|
def border_diagonal(style: "thin", color: nil, up: false, down: false)
|
|
385
|
-
|
|
670
|
+
norm_style = self.class.normalize_border_style(style)&.to_s || style.to_s
|
|
671
|
+
@border_props[:diagonal] = { style: norm_style, color: resolve_color(color) }.compact
|
|
386
672
|
@border_props[:diagonal_up] = true if up
|
|
387
673
|
@border_props[:diagonal_down] = true if down
|
|
388
674
|
self
|
data/lib/xlsxrb/utils.rb
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
require_relative "elements/cell"
|
|
6
|
+
require_relative "ooxml/utils"
|
|
7
|
+
|
|
8
|
+
module Xlsxrb
|
|
9
|
+
# Public coordinate conversion and reference parsing utilities.
|
|
10
|
+
#
|
|
11
|
+
# @api public
|
|
12
|
+
module Utils
|
|
13
|
+
# Converts a cell reference string (e.g. "A1", "BC100") to 0-based [row, col] coordinates.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# Xlsxrb::Utils.ref_to_row_col("A1") #=> [0, 0]
|
|
17
|
+
# Xlsxrb::Utils.ref_to_row_col("BC100") #=> [99, 54]
|
|
18
|
+
#
|
|
19
|
+
# @param cell_ref [String, nil] Excel cell reference string.
|
|
20
|
+
# @return [Array(Integer, Integer), nil] 0-based [row_idx, col_idx] tuple, or nil if invalid.
|
|
21
|
+
# @api public
|
|
22
|
+
#: (String? cell_ref) -> [Integer, Integer]?
|
|
23
|
+
def self.ref_to_row_col(cell_ref)
|
|
24
|
+
Elements::Cell.parse_ref(cell_ref)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Converts 0-based [row_idx, col_idx] coordinates to an A1-style reference string.
|
|
28
|
+
#
|
|
29
|
+
# @example
|
|
30
|
+
# Xlsxrb::Utils.row_col_to_ref(0, 0) #=> "A1"
|
|
31
|
+
# Xlsxrb::Utils.row_col_to_ref(99, 54) #=> "BC100"
|
|
32
|
+
#
|
|
33
|
+
# @param row_idx [Integer] 0-based row index.
|
|
34
|
+
# @param col_idx [Integer] 0-based column index.
|
|
35
|
+
# @return [String] A1-style reference string.
|
|
36
|
+
# @api public
|
|
37
|
+
#: (Integer row_idx, Integer col_idx) -> String
|
|
38
|
+
def self.row_col_to_ref(row_idx, col_idx)
|
|
39
|
+
"#{Elements::Cell.column_letter(col_idx)}#{row_idx + 1}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Converts a column letter name (e.g. "A", "BC") to a 0-based integer index.
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# Xlsxrb::Utils.col_name_to_index("A") #=> 0
|
|
46
|
+
# Xlsxrb::Utils.col_name_to_index("BC") #=> 54
|
|
47
|
+
#
|
|
48
|
+
# @param col_name [String, Symbol, Integer] Column letter or index.
|
|
49
|
+
# @return [Integer] 0-based column index.
|
|
50
|
+
# @api public
|
|
51
|
+
#: (String | Symbol | Integer col_name) -> Integer
|
|
52
|
+
def self.col_name_to_index(col_name)
|
|
53
|
+
Elements::Cell.column_index(col_name)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Converts a 0-based column integer index to a column letter name (e.g. 0 -> "A", 54 -> "BC").
|
|
57
|
+
#
|
|
58
|
+
# @example
|
|
59
|
+
# Xlsxrb::Utils.col_index_to_name(0) #=> "A"
|
|
60
|
+
# Xlsxrb::Utils.col_index_to_name(54) #=> "BC"
|
|
61
|
+
#
|
|
62
|
+
# @param col_idx [Integer] 0-based column index.
|
|
63
|
+
# @return [String] Column letter name.
|
|
64
|
+
# @api public
|
|
65
|
+
#: (Integer col_idx) -> String
|
|
66
|
+
def self.col_index_to_name(col_idx)
|
|
67
|
+
Elements::Cell.column_letter(col_idx)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Splits an A1-style cell reference into its column name and 1-based row number.
|
|
71
|
+
#
|
|
72
|
+
# @example
|
|
73
|
+
# Xlsxrb::Utils.split_coordinate("A1") #=> ["A", 1]
|
|
74
|
+
# Xlsxrb::Utils.split_coordinate("BC100") #=> ["BC", 100]
|
|
75
|
+
#
|
|
76
|
+
# @param cell_ref [String, nil] Excel cell reference string.
|
|
77
|
+
# @return [Array(String, Integer), nil] [col_name, row_number] tuple (1-based row), or nil if invalid.
|
|
78
|
+
# @api public
|
|
79
|
+
#: (String? cell_ref) -> [String, Integer]?
|
|
80
|
+
def self.split_coordinate(cell_ref)
|
|
81
|
+
return nil unless cell_ref
|
|
82
|
+
|
|
83
|
+
coords = Elements::Cell.parse_ref(cell_ref)
|
|
84
|
+
return nil unless coords
|
|
85
|
+
|
|
86
|
+
row_idx, col_idx = coords
|
|
87
|
+
[Elements::Cell.column_letter(col_idx), row_idx + 1]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Converts a Date or Time to an Excel numeric serial number (1900 or 1904 system).
|
|
91
|
+
#
|
|
92
|
+
# @example
|
|
93
|
+
# Xlsxrb::Utils.date_to_serial(Date.new(2026, 1, 1)) #=> 46023
|
|
94
|
+
#
|
|
95
|
+
# @param date_or_time [Date, Time] Date or Time instance.
|
|
96
|
+
# @param date1904 [Boolean] Whether to use the 1904 date system.
|
|
97
|
+
# @return [Integer, Float] Serial number.
|
|
98
|
+
# @api public
|
|
99
|
+
#: (Date | Time date_or_time, ?date1904: bool) -> (Integer | Float)
|
|
100
|
+
def self.date_to_serial(date_or_time, date1904: false)
|
|
101
|
+
if date_or_time.is_a?(Time)
|
|
102
|
+
Ooxml::Utils.datetime_to_serial(date_or_time, date1904: date1904)
|
|
103
|
+
else
|
|
104
|
+
Ooxml::Utils.date_to_serial(date_or_time, date1904: date1904)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Converts an Excel serial number to a Date.
|
|
109
|
+
#
|
|
110
|
+
# @example
|
|
111
|
+
# Xlsxrb::Utils.serial_to_date(46023) #=> #<Date: 2026-01-01>
|
|
112
|
+
#
|
|
113
|
+
# @param serial [Numeric] Excel serial number.
|
|
114
|
+
# @param date1904 [Boolean] Whether to use the 1904 date system.
|
|
115
|
+
# @return [Date]
|
|
116
|
+
# @api public
|
|
117
|
+
#: (Numeric serial, ?date1904: bool) -> Date
|
|
118
|
+
def self.serial_to_date(serial, date1904: false)
|
|
119
|
+
Ooxml::Utils.serial_to_date(serial, date1904: date1904)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Converts a Time or Date to a fractional Excel serial number.
|
|
123
|
+
#
|
|
124
|
+
# @example
|
|
125
|
+
# Xlsxrb::Utils.datetime_to_serial(Time.utc(2026, 1, 1, 12, 0, 0)) #=> 46023.5
|
|
126
|
+
#
|
|
127
|
+
# @param time [Time, Date] Time or Date instance.
|
|
128
|
+
# @param date1904 [Boolean] Whether to use the 1904 date system.
|
|
129
|
+
# @return [Float] Fractional serial number.
|
|
130
|
+
# @api public
|
|
131
|
+
#: (Time | Date time, ?date1904: bool) -> Float
|
|
132
|
+
def self.datetime_to_serial(time, date1904: false)
|
|
133
|
+
if time.is_a?(Time)
|
|
134
|
+
Ooxml::Utils.datetime_to_serial(time, date1904: date1904)
|
|
135
|
+
else
|
|
136
|
+
Ooxml::Utils.date_to_serial(time, date1904: date1904).to_f
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Converts an Excel serial number to a Time (UTC).
|
|
141
|
+
#
|
|
142
|
+
# @example
|
|
143
|
+
# Xlsxrb::Utils.serial_to_time(46023.5) #=> 2026-01-01 12:00:00 UTC
|
|
144
|
+
#
|
|
145
|
+
# @param serial [Numeric] Excel serial number.
|
|
146
|
+
# @param date1904 [Boolean] Whether to use the 1904 date system.
|
|
147
|
+
# @return [Time]
|
|
148
|
+
# @api public
|
|
149
|
+
#: (Numeric serial, ?date1904: bool) -> Time
|
|
150
|
+
def self.serial_to_time(serial, date1904: false)
|
|
151
|
+
Ooxml::Utils.serial_to_datetime(serial, date1904: date1904)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
class << self
|
|
155
|
+
alias serial_to_datetime serial_to_time
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
data/lib/xlsxrb/version.rb
CHANGED
|
@@ -63,7 +63,12 @@ module Xlsxrb
|
|
|
63
63
|
raise ArgumentError, "Sheet name '#{name}' contains invalid characters (ECMA-376 OOXML specification)" if name.match?(%r{[\[\]*?/\\]})
|
|
64
64
|
raise ArgumentError, "Sheet name '#{name}' is already used. Excel requires unique sheet names." if @strict_excel_mode && @sheets.map { |s| s.respond_to?(:name) ? s.name.downcase : s.to_s.downcase }.include?(name.downcase)
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
state = opts.delete(:state)
|
|
67
|
+
hidden = opts.delete(:hidden)
|
|
68
|
+
state ||= :hidden if hidden
|
|
69
|
+
state ||= :visible
|
|
70
|
+
|
|
71
|
+
sheet_builder = WorksheetBuilder.new(name, strict_excel_mode: @strict_excel_mode, state: state)
|
|
67
72
|
opts.each { |k, v| sheet_builder.sheet_properties(k, v) }
|
|
68
73
|
yield sheet_builder if block_given?
|
|
69
74
|
@sheet_builders << sheet_builder
|
|
@@ -112,6 +117,11 @@ module Xlsxrb
|
|
|
112
117
|
#: (?rows: String?, ?cols: String?, ?sheet: String?) -> void
|
|
113
118
|
def print_titles(rows: nil, cols: nil, sheet: nil)
|
|
114
119
|
sheet_name = sheet || @sheets.last&.name || "Sheet1"
|
|
120
|
+
existing = @defined_names.find { |dn| dn[:name] == "_xlnm.Print_Titles" && dn[:local_sheet_name] == sheet_name }
|
|
121
|
+
if existing && (rows.nil? || cols.nil?)
|
|
122
|
+
cols = Regexp.last_match(1).delete("$") if cols.nil? && existing[:value] =~ /'[^']*'!\$([A-Za-z]+:\$[A-Za-z]+)/
|
|
123
|
+
rows = Regexp.last_match(1).delete("$") if rows.nil? && existing[:value] =~ /'[^']*'!\$([0-9]+:\$[0-9]+)/
|
|
124
|
+
end
|
|
115
125
|
parts = []
|
|
116
126
|
parts << "'#{sheet_name}'!$#{cols.sub(":", ":$")}" if cols
|
|
117
127
|
parts << "'#{sheet_name}'!$#{rows.sub(":", ":$")}" if rows
|
|
@@ -120,6 +130,55 @@ module Xlsxrb
|
|
|
120
130
|
defined_name("_xlnm.Print_Titles", value, sheet: sheet_name)
|
|
121
131
|
end
|
|
122
132
|
|
|
133
|
+
# Sets repeating title rows for pagination on the target or active sheet.
|
|
134
|
+
#
|
|
135
|
+
# @example Repeat first row
|
|
136
|
+
# wb.repeat_rows(0)
|
|
137
|
+
#
|
|
138
|
+
# @param first [Integer, String] First row index (0-based) or range string (e.g. "1:2").
|
|
139
|
+
# @param last [Integer, String, nil] Optional last row index (0-based).
|
|
140
|
+
# @param sheet [String, nil] Target sheet name.
|
|
141
|
+
# @return [void]
|
|
142
|
+
# @api public
|
|
143
|
+
#: (Integer | String first, ?(Integer | String)? last, ?sheet: String?) -> void
|
|
144
|
+
def repeat_rows(first, last = nil, sheet: nil)
|
|
145
|
+
rows_str = if first.is_a?(String)
|
|
146
|
+
first.include?(":") ? first : "#{first}:#{last || first}"
|
|
147
|
+
else
|
|
148
|
+
last ||= first
|
|
149
|
+
"#{first + 1}:#{last + 1}"
|
|
150
|
+
end
|
|
151
|
+
print_titles(rows: rows_str, sheet: sheet)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Sets repeating title columns for pagination on the target or active sheet.
|
|
155
|
+
#
|
|
156
|
+
# @example Repeat columns A through C
|
|
157
|
+
# wb.repeat_columns("A", "C")
|
|
158
|
+
#
|
|
159
|
+
# @param first [Integer, String] First column index (0-based) or column name/range.
|
|
160
|
+
# @param last [Integer, String, nil] Optional last column index/name.
|
|
161
|
+
# @param sheet [String, nil] Target sheet name.
|
|
162
|
+
# @return [void]
|
|
163
|
+
# @api public
|
|
164
|
+
#: (Integer | String first, ?(Integer | String)? last, ?sheet: String?) -> void
|
|
165
|
+
def repeat_columns(first, last = nil, sheet: nil)
|
|
166
|
+
cols_str = if first.is_a?(String)
|
|
167
|
+
first.include?(":") ? first : "#{first}:#{last || first}"
|
|
168
|
+
else
|
|
169
|
+
first_name = Utils.col_index_to_name(first)
|
|
170
|
+
last_name = if last.is_a?(Integer)
|
|
171
|
+
Utils.col_index_to_name(last)
|
|
172
|
+
elsif last
|
|
173
|
+
last.to_s
|
|
174
|
+
else
|
|
175
|
+
first_name
|
|
176
|
+
end
|
|
177
|
+
"#{first_name}:#{last_name}"
|
|
178
|
+
end
|
|
179
|
+
print_titles(cols: cols_str, sheet: sheet)
|
|
180
|
+
end
|
|
181
|
+
|
|
123
182
|
# Sets workbook structure and window protection.
|
|
124
183
|
#
|
|
125
184
|
# @param opts [Hash] Protection options (e.g. lock_structure: true, password: "secret").
|
|
@@ -190,9 +249,16 @@ module Xlsxrb
|
|
|
190
249
|
# Process styles from all sheets and collect style definitions
|
|
191
250
|
processed_sheets, styles_definition = process_styles(@sheets)
|
|
192
251
|
|
|
252
|
+
# Auto-register print_area and print_titles defined names from sheet builders if not explicitly defined
|
|
253
|
+
@sheet_builders.each do |sb|
|
|
254
|
+
print_area(sb.print_area, sheet: sb.name) if sb.print_area && @defined_names.none? { |dn| dn[:name] == "_xlnm.Print_Area" && dn[:local_sheet_name] == sb.name }
|
|
255
|
+
print_titles(rows: sb.print_titles[:rows], cols: sb.print_titles[:cols], sheet: sb.name) if sb.print_titles && @defined_names.none? { |dn| dn[:name] == "_xlnm.Print_Titles" && dn[:local_sheet_name] == sb.name }
|
|
256
|
+
end
|
|
257
|
+
|
|
193
258
|
# Store workbook-level metadata in unmapped_data
|
|
194
259
|
wb_meta = {}
|
|
195
|
-
|
|
260
|
+
dns = resolve_defined_names(@defined_names, processed_sheets)
|
|
261
|
+
wb_meta[:defined_names] = dns unless dns.empty?
|
|
196
262
|
wb_meta[:core_properties] = @core_properties unless @core_properties.empty?
|
|
197
263
|
wb_meta[:app_properties] = @app_properties unless @app_properties.empty?
|
|
198
264
|
wb_meta[:custom_properties] = @custom_properties unless @custom_properties.empty?
|
|
@@ -202,7 +268,8 @@ module Xlsxrb
|
|
|
202
268
|
Elements::Workbook.new(
|
|
203
269
|
sheets: processed_sheets,
|
|
204
270
|
styles: styles_definition,
|
|
205
|
-
unmapped_data: wb_meta.empty? ? {} : { facade: wb_meta }
|
|
271
|
+
unmapped_data: wb_meta.empty? ? {} : { facade: wb_meta },
|
|
272
|
+
defined_names: dns
|
|
206
273
|
)
|
|
207
274
|
end
|
|
208
275
|
|
|
@@ -275,6 +342,7 @@ module Xlsxrb
|
|
|
275
342
|
height: row.height,
|
|
276
343
|
hidden: row.hidden,
|
|
277
344
|
custom_height: row.custom_height,
|
|
345
|
+
collapsed: row.collapsed,
|
|
278
346
|
outline_level: row.outline_level,
|
|
279
347
|
unmapped_data: row.unmapped_data,
|
|
280
348
|
errors: row.errors
|
|
@@ -285,6 +353,8 @@ module Xlsxrb
|
|
|
285
353
|
rows: new_rows,
|
|
286
354
|
columns: sheet.columns,
|
|
287
355
|
charts: sheet.charts,
|
|
356
|
+
conditional_formatting: sheet.conditional_formatting,
|
|
357
|
+
data_validations: sheet.data_validations,
|
|
288
358
|
unmapped_data: sheet.unmapped_data,
|
|
289
359
|
errors: sheet.errors
|
|
290
360
|
)
|