xlsxrb 0.1.3 → 0.1.5
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/.devcontainer/Dockerfile +2 -1
- data/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/ast/2.4/ast.rbs +73 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/array.rbs +4 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/atomic_reference.rbs +16 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +96 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/hash.rbs +4 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/map.rbs +68 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +249 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/set.rbs +4 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/timer_task.rbs +47 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +5 -0
- data/.gem_rbs_collection/csv/3.3/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/csv/3.3/csv.rbs +3871 -0
- data/.gem_rbs_collection/csv/3.3/manifest.yaml +3 -0
- data/.gem_rbs_collection/lint_roller/1.1/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/lint_roller/1.1/lint_roller.rbs +48 -0
- data/.gem_rbs_collection/listen/3.9/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/listen/3.9/listen.rbs +25 -0
- data/.gem_rbs_collection/listen/3.9/listener.rbs +24 -0
- data/.gem_rbs_collection/logger/1.7/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/logger/1.7/formatter.rbs +45 -0
- data/.gem_rbs_collection/logger/1.7/log_device.rbs +100 -0
- data/.gem_rbs_collection/logger/1.7/logger.rbs +796 -0
- data/.gem_rbs_collection/logger/1.7/manifest.yaml +2 -0
- data/.gem_rbs_collection/logger/1.7/period.rbs +17 -0
- data/.gem_rbs_collection/logger/1.7/severity.rbs +34 -0
- data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/parallel/1.20/parallel.rbs +86 -0
- data/.gem_rbs_collection/parser/3.2/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/parser/3.2/manifest.yaml +7 -0
- data/.gem_rbs_collection/parser/3.2/parser.rbs +194 -0
- data/.gem_rbs_collection/parser/3.2/polyfill.rbs +4 -0
- data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rainbow/3.0/global.rbs +7 -0
- data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +209 -0
- data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +5 -0
- data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rake/13.0/manifest.yaml +2 -0
- data/.gem_rbs_collection/rake/13.0/rake.rbs +39 -0
- data/.gem_rbs_collection/regexp_parser/2.8/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/regexp_parser/2.8/regexp_parser.rbs +17 -0
- data/.gem_rbs_collection/rubocop/1.57/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubocop/1.57/rubocop.rbs +208 -0
- data/.gem_rbs_collection/rubocop-ast/1.46/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubocop-ast/1.46/rubocop-ast.rbs +903 -0
- data/CHANGELOG.md +24 -3
- data/README.md +91 -17
- data/Rakefile +124 -89
- data/Steepfile +19 -0
- data/docs/ARCHITECTURE.md +17 -0
- data/docs/DEVELOPMENT.md +58 -0
- data/docs/QUALITY_ASSURANCE.md +26 -0
- data/docs/visual/VisualGallery.md +893 -938
- data/docs/wasm/ruby.wasm +0 -0
- data/lib/xlsxrb/elements/cell.rb +74 -1
- data/lib/xlsxrb/elements/column.rb +2 -0
- data/lib/xlsxrb/elements/row.rb +37 -2
- data/lib/xlsxrb/elements/types.rb +2 -0
- data/lib/xlsxrb/elements/workbook.rb +29 -0
- data/lib/xlsxrb/elements/worksheet.rb +98 -1
- data/lib/xlsxrb/elements.rb +2 -0
- data/lib/xlsxrb/ooxml/reader.rb +8 -6
- data/lib/xlsxrb/ooxml/shared_strings_parser.rb +2 -0
- data/lib/xlsxrb/ooxml/styles_parser.rb +2 -0
- data/lib/xlsxrb/ooxml/utils.rb +2 -0
- data/lib/xlsxrb/ooxml/workbook_parser.rb +6 -1
- data/lib/xlsxrb/ooxml/workbook_writer.rb +15 -4
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +12 -0
- data/lib/xlsxrb/ooxml/worksheet_writer.rb +49 -9
- data/lib/xlsxrb/ooxml/writer.rb +217 -5
- data/lib/xlsxrb/ooxml/xml_builder.rb +2 -0
- data/lib/xlsxrb/ooxml/xml_parser.rb +2 -0
- data/lib/xlsxrb/ooxml/zip_generator.rb +5 -8
- data/lib/xlsxrb/ooxml/zip_reader.rb +88 -52
- data/lib/xlsxrb/ooxml/zip_writer.rb +2 -0
- data/lib/xlsxrb/ooxml.rb +3 -1
- data/lib/xlsxrb/style_builder.rb +156 -36
- data/lib/xlsxrb/version.rb +3 -1
- data/lib/xlsxrb.rb +1510 -231
- data/rbs_collection.lock.yaml +224 -0
- data/rbs_collection.yaml +19 -0
- data/sig/generated/xlsxrb/elements.rbs +8 -0
- data/sig/generated/xlsxrb/ooxml.rbs +23 -0
- data/sig/generated/xlsxrb/style_builder.rbs +88 -0
- data/sig/generated/xlsxrb/version.rbs +5 -0
- metadata +67 -29
- data/benchmark.rb +0 -399
- data/measure_memory.rb +0 -42
- data/sig/xlsxrb.rbs +0 -23
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
3
5
|
require "zlib"
|
|
4
6
|
|
|
5
7
|
module Xlsxrb
|
|
@@ -142,20 +144,15 @@ module Xlsxrb
|
|
|
142
144
|
end
|
|
143
145
|
|
|
144
146
|
def le16(value)
|
|
145
|
-
[
|
|
147
|
+
[value].pack("v").bytes
|
|
146
148
|
end
|
|
147
149
|
|
|
148
150
|
def le32(value)
|
|
149
|
-
[
|
|
151
|
+
[value].pack("V").bytes
|
|
150
152
|
end
|
|
151
153
|
|
|
152
154
|
def calculate_central_dir_size
|
|
153
|
-
|
|
154
|
-
@entries.each do |entry|
|
|
155
|
-
filename = entry[:path]
|
|
156
|
-
size += 46 + filename.bytesize
|
|
157
|
-
end
|
|
158
|
-
size
|
|
155
|
+
@entries.sum { |entry| 46 + entry[:path].bytesize }
|
|
159
156
|
end
|
|
160
157
|
end
|
|
161
158
|
end
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
3
5
|
require "zlib"
|
|
4
6
|
require "stringio"
|
|
7
|
+
require "tempfile"
|
|
5
8
|
|
|
6
9
|
module Xlsxrb
|
|
7
10
|
module Ooxml
|
|
8
11
|
# Reads ZIP archives using only stdlib (zlib).
|
|
9
12
|
# Scans local file headers sequentially — works with non-seekable IO.
|
|
10
13
|
class ZipReader
|
|
11
|
-
LOCAL_HEADER_SIG =
|
|
14
|
+
LOCAL_HEADER_SIG = "PK\x03\x04".b
|
|
15
|
+
MAX_UNCOMPRESSED_SIZE = 500 * 1024 * 1024 # 500MB per file limit
|
|
12
16
|
|
|
13
17
|
# Opens a ZIP from a file path or IO and yields the reader.
|
|
14
18
|
def self.open(source)
|
|
@@ -57,85 +61,94 @@ module Xlsxrb
|
|
|
57
61
|
|
|
58
62
|
def parse_entries
|
|
59
63
|
result = {}
|
|
60
|
-
data = @io.is_a?(StringIO) ? @io.string : @io.read
|
|
61
|
-
data = data.b
|
|
62
|
-
pos = 0
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
io = if @io.is_a?(StringIO) || (@io.respond_to?(:read) && @io.respond_to?(:seek) && @io.respond_to?(:pos))
|
|
66
|
+
@io
|
|
67
|
+
elsif @io.respond_to?(:each) || @io.is_a?(Enumerator)
|
|
68
|
+
tf = Tempfile.new("xlsxrb_zip")
|
|
69
|
+
tf.binmode
|
|
70
|
+
@io.each { |chunk| tf.write(chunk) }
|
|
71
|
+
tf.rewind
|
|
72
|
+
tf
|
|
73
|
+
else
|
|
74
|
+
StringIO.new(@io.read.b)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
io.binmode if io.respond_to?(:binmode)
|
|
78
|
+
|
|
79
|
+
first_sig = io.read(4)
|
|
80
|
+
raise ArgumentError, "Invalid magic number: Expected a valid ZIP/XLSX file format (PK\\x03\\x04)" unless first_sig == LOCAL_HEADER_SIG
|
|
81
|
+
|
|
82
|
+
io.seek(-4, IO::SEEK_CUR) if io.respond_to?(:seek)
|
|
83
|
+
io = StringIO.new(first_sig + io.read.b) unless io.respond_to?(:seek)
|
|
84
|
+
|
|
85
|
+
loop do
|
|
86
|
+
sig = io.read(4)
|
|
66
87
|
break unless sig == LOCAL_HEADER_SIG
|
|
67
88
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
break if pos + 30 > data.bytesize
|
|
89
|
+
header = io.read(26)
|
|
90
|
+
break unless header && header.bytesize == 26
|
|
71
91
|
|
|
72
|
-
gp_flag
|
|
73
|
-
method
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
name_len = data[pos + 26, 2].unpack1("v")
|
|
78
|
-
extra_len = data[pos + 28, 2].unpack1("v")
|
|
92
|
+
gp_flag = header[2, 2].unpack1("v")
|
|
93
|
+
method = header[4, 2].unpack1("v")
|
|
94
|
+
compressed_size = header[14, 4].unpack1("V")
|
|
95
|
+
name_len = header[22, 2].unpack1("v")
|
|
96
|
+
extra_len = header[24, 2].unpack1("v")
|
|
79
97
|
|
|
80
|
-
entry_name =
|
|
81
|
-
|
|
98
|
+
entry_name = io.read(name_len).force_encoding("UTF-8")
|
|
99
|
+
io.read(extra_len) # skip extra
|
|
82
100
|
|
|
83
|
-
# Data descriptor present if bit 3 of gp_flag is set
|
|
84
101
|
has_data_descriptor = gp_flag.anybits?(0x08)
|
|
85
102
|
|
|
86
103
|
if has_data_descriptor && compressed_size.zero?
|
|
87
|
-
|
|
88
|
-
raw, comp_sz, = find_data_descriptor(data, file_data_offset, method)
|
|
89
|
-
entry_data = decompress(raw, method)
|
|
90
|
-
result[entry_name] = entry_data unless entry_name.end_with?("/")
|
|
91
|
-
# Skip past data + descriptor (12 or 16 bytes)
|
|
92
|
-
desc_offset = file_data_offset + comp_sz
|
|
93
|
-
# Check for optional signature
|
|
94
|
-
pos = if desc_offset + 4 <= data.bytesize && data[desc_offset, 4] == [0x50, 0x4B, 0x07, 0x08].pack("C4")
|
|
95
|
-
desc_offset + 16
|
|
96
|
-
else
|
|
97
|
-
desc_offset + 12
|
|
98
|
-
end
|
|
104
|
+
raw, = find_data_descriptor_stream(io, method)
|
|
99
105
|
else
|
|
100
|
-
raw =
|
|
101
|
-
entry_data = decompress(raw, method)
|
|
102
|
-
result[entry_name] = entry_data unless entry_name.end_with?("/")
|
|
103
|
-
pos = file_data_offset + compressed_size
|
|
106
|
+
raw = io.read(compressed_size)
|
|
104
107
|
end
|
|
108
|
+
entry_data = decompress(raw, method)
|
|
109
|
+
result[entry_name] = entry_data unless entry_name.end_with?("/")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if io.is_a?(Tempfile)
|
|
113
|
+
io.close
|
|
114
|
+
io.unlink
|
|
105
115
|
end
|
|
106
116
|
|
|
107
117
|
result
|
|
108
118
|
end
|
|
109
119
|
|
|
110
|
-
def
|
|
111
|
-
# For deflated data, we inflate to find the end
|
|
120
|
+
def find_data_descriptor_stream(io, method)
|
|
112
121
|
if method == 8
|
|
113
122
|
inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
|
114
123
|
result = +""
|
|
115
124
|
consumed = 0
|
|
116
125
|
chunk_size = 4096
|
|
117
|
-
pos = offset
|
|
118
126
|
begin
|
|
119
|
-
while
|
|
120
|
-
|
|
121
|
-
break if chunk.nil? || chunk.empty?
|
|
127
|
+
while (chunk = io.read(chunk_size))
|
|
128
|
+
break if chunk.empty?
|
|
122
129
|
|
|
123
|
-
|
|
130
|
+
inflated = inflater.inflate(chunk)
|
|
131
|
+
raise ArgumentError, "ZIP bomb detected: Uncompressed size exceeds #{MAX_UNCOMPRESSED_SIZE} bytes" if result.bytesize + inflated.bytesize > MAX_UNCOMPRESSED_SIZE
|
|
132
|
+
|
|
133
|
+
result << inflated
|
|
124
134
|
consumed += chunk.bytesize
|
|
125
|
-
pos += chunk.bytesize
|
|
126
135
|
end
|
|
127
136
|
rescue Zlib::BufError, Zlib::DataError
|
|
128
|
-
# Inflation ended
|
|
137
|
+
# Inflation ended
|
|
129
138
|
ensure
|
|
130
|
-
# Calculate actual compressed bytes consumed
|
|
131
139
|
consumed -= inflater.avail_in
|
|
132
140
|
inflater.close
|
|
133
141
|
end
|
|
134
|
-
|
|
142
|
+
|
|
143
|
+
io.seek(-inflater.avail_in, IO::SEEK_CUR) if io.respond_to?(:seek)
|
|
144
|
+
|
|
145
|
+
desc_sig = io.read(4)
|
|
146
|
+
io.seek(-4, IO::SEEK_CUR) unless desc_sig == [0x50, 0x4B, 0x07, 0x08].pack("C4")
|
|
147
|
+
io.read(12)
|
|
148
|
+
|
|
149
|
+
[result, consumed]
|
|
135
150
|
else
|
|
136
|
-
|
|
137
|
-
# This is a fallback; stored + data descriptor is rare
|
|
138
|
-
[data[offset, 0], 0, 0]
|
|
151
|
+
[io.read(0), 0]
|
|
139
152
|
end
|
|
140
153
|
end
|
|
141
154
|
|
|
@@ -143,15 +156,38 @@ module Xlsxrb
|
|
|
143
156
|
return raw&.dup&.force_encoding("UTF-8") || "" if method.zero? # stored
|
|
144
157
|
|
|
145
158
|
# Deflated
|
|
146
|
-
|
|
159
|
+
safe_inflate(raw || "", -Zlib::MAX_WBITS)
|
|
147
160
|
rescue Zlib::DataError
|
|
148
161
|
# Try with raw deflate (no header)
|
|
149
|
-
|
|
162
|
+
safe_inflate(raw || "", -Zlib::MAX_WBITS)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def safe_inflate(raw, wbits)
|
|
166
|
+
inflater = Zlib::Inflate.new(wbits)
|
|
167
|
+
result = +""
|
|
168
|
+
chunk_size = 32_768
|
|
169
|
+
offset = 0
|
|
170
|
+
raw_len = raw.bytesize
|
|
171
|
+
|
|
150
172
|
begin
|
|
151
|
-
|
|
173
|
+
while offset < raw_len
|
|
174
|
+
chunk = raw.byteslice(offset, chunk_size)
|
|
175
|
+
inflated = inflater.inflate(chunk)
|
|
176
|
+
raise ArgumentError, "ZIP bomb detected: Uncompressed size exceeds #{MAX_UNCOMPRESSED_SIZE} bytes" if result.bytesize + inflated.bytesize > MAX_UNCOMPRESSED_SIZE
|
|
177
|
+
|
|
178
|
+
result << inflated
|
|
179
|
+
offset += chunk_size
|
|
180
|
+
end
|
|
181
|
+
# Finish inflation
|
|
182
|
+
inflated = inflater.finish
|
|
183
|
+
raise ArgumentError, "ZIP bomb detected: Uncompressed size exceeds #{MAX_UNCOMPRESSED_SIZE} bytes" if result.bytesize + inflated.bytesize > MAX_UNCOMPRESSED_SIZE
|
|
184
|
+
|
|
185
|
+
result << inflated
|
|
152
186
|
ensure
|
|
153
187
|
inflater.close
|
|
154
188
|
end
|
|
189
|
+
|
|
190
|
+
result.force_encoding("UTF-8")
|
|
155
191
|
end
|
|
156
192
|
end
|
|
157
193
|
end
|
data/lib/xlsxrb/ooxml.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
3
5
|
require_relative "ooxml/utils"
|
|
4
6
|
require_relative "ooxml/zip_reader"
|
|
5
7
|
require_relative "ooxml/zip_writer"
|
|
@@ -17,6 +19,6 @@ module Xlsxrb
|
|
|
17
19
|
# Handles ZIP extraction, SAX XML parsing, and XML generation
|
|
18
20
|
# in strict accordance with ECMA-376.
|
|
19
21
|
module Ooxml
|
|
20
|
-
Event =
|
|
22
|
+
Event = Data.define(:type, :args, :source)
|
|
21
23
|
end
|
|
22
24
|
end
|
data/lib/xlsxrb/style_builder.rb
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
3
5
|
module Xlsxrb
|
|
4
6
|
# Helper class for building cell styles with a fluent DSL.
|
|
5
7
|
# Encapsulates font, fill, border, alignment, and number format properties.
|
|
6
8
|
class StyleBuilder
|
|
9
|
+
COLORS = {
|
|
10
|
+
black: "FF000000",
|
|
11
|
+
white: "FFFFFFFF",
|
|
12
|
+
red: "FFFF0000",
|
|
13
|
+
green: "FF00FF00",
|
|
14
|
+
blue: "FF0000FF",
|
|
15
|
+
yellow: "FFFFFF00",
|
|
16
|
+
cyan: "FF00FFFF",
|
|
17
|
+
magenta: "FFFF00FF",
|
|
18
|
+
gray: "FF808080",
|
|
19
|
+
grey: "FF808080"
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
def resolve_color(color)
|
|
23
|
+
return nil unless color
|
|
24
|
+
|
|
25
|
+
if color.is_a?(Symbol) || (color.is_a?(String) && color.start_with?(":") && color.length > 1)
|
|
26
|
+
key = color.to_s.sub(/^:/, "").to_sym
|
|
27
|
+
return COLORS[key] || color.to_s
|
|
28
|
+
end
|
|
29
|
+
color.to_s
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# : (?String? name) -> void
|
|
7
33
|
def initialize(name = nil)
|
|
8
34
|
@name = name
|
|
9
35
|
@font_props = {}
|
|
@@ -17,83 +43,149 @@ module Xlsxrb
|
|
|
17
43
|
|
|
18
44
|
# Applies option-style definitions so callers can use add_style(name, **opts)
|
|
19
45
|
# as an alternative to block-based fluent chaining.
|
|
46
|
+
# : (**untyped opts) -> self
|
|
20
47
|
def apply_options!(**opts)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
48
|
+
if opts.key?(:font)
|
|
49
|
+
font_opts = opts[:font] || {}
|
|
50
|
+
bold(font_opts[:bold]) if font_opts.key?(:bold)
|
|
51
|
+
italic(font_opts[:italic]) if font_opts.key?(:italic)
|
|
52
|
+
size(font_opts[:size]) if font_opts.key?(:size)
|
|
53
|
+
font_name(font_opts[:name]) if font_opts.key?(:name)
|
|
54
|
+
font_color(font_opts[:color]) if font_opts.key?(:color)
|
|
55
|
+
underline(font_opts[:underline]) if font_opts.key?(:underline)
|
|
56
|
+
strike(font_opts[:strike]) if font_opts.key?(:strike)
|
|
57
|
+
vert_align(font_opts[:vert_align]) if font_opts.key?(:vert_align)
|
|
58
|
+
else
|
|
59
|
+
bold(opts[:bold]) if opts.key?(:bold)
|
|
60
|
+
italic(opts[:italic]) if opts.key?(:italic)
|
|
61
|
+
size(opts[:size]) if opts.key?(:size)
|
|
62
|
+
font_name(opts[:font_name]) if opts.key?(:font_name)
|
|
63
|
+
font_color(opts[:font_color]) if opts.key?(:font_color)
|
|
64
|
+
underline(opts[:underline]) if opts.key?(:underline)
|
|
65
|
+
strike(opts[:strike]) if opts.key?(:strike)
|
|
66
|
+
vert_align(opts[:vert_align]) if opts.key?(:vert_align)
|
|
67
|
+
end
|
|
29
68
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
69
|
+
if opts.key?(:fill)
|
|
70
|
+
fill_opts = opts[:fill] || {}
|
|
71
|
+
if fill_opts.key?(:color)
|
|
72
|
+
fill_color(fill_opts[:color])
|
|
73
|
+
else
|
|
74
|
+
fill_pattern(
|
|
75
|
+
fill_opts[:pattern] || "solid",
|
|
76
|
+
fg_color: fill_opts[:fg_color],
|
|
77
|
+
bg_color: fill_opts[:bg_color]
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
else
|
|
81
|
+
fill_color(opts[:fill_color]) if opts.key?(:fill_color)
|
|
82
|
+
if opts.key?(:fill_pattern)
|
|
83
|
+
pattern = opts[:fill_pattern] || {}
|
|
84
|
+
fill_pattern(
|
|
85
|
+
pattern[:pattern] || "solid",
|
|
86
|
+
fg_color: pattern[:fg_color],
|
|
87
|
+
bg_color: pattern[:bg_color]
|
|
88
|
+
)
|
|
89
|
+
end
|
|
38
90
|
end
|
|
39
91
|
fill_gradient(**opts[:fill_gradient]) if opts.key?(:fill_gradient) && opts[:fill_gradient]
|
|
40
92
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
93
|
+
if opts.key?(:border)
|
|
94
|
+
border_opts = opts[:border] || {}
|
|
95
|
+
border_all(**border_opts[:all]) if border_opts.key?(:all) && border_opts[:all]
|
|
96
|
+
border_left(**border_opts[:left]) if border_opts.key?(:left) && border_opts[:left]
|
|
97
|
+
border_right(**border_opts[:right]) if border_opts.key?(:right) && border_opts[:right]
|
|
98
|
+
border_top(**border_opts[:top]) if border_opts.key?(:top) && border_opts[:top]
|
|
99
|
+
border_bottom(**border_opts[:bottom]) if border_opts.key?(:bottom) && border_opts[:bottom]
|
|
100
|
+
else
|
|
101
|
+
border_all(**opts[:border_all]) if opts.key?(:border_all) && opts[:border_all]
|
|
102
|
+
border_left(**opts[:border_left]) if opts.key?(:border_left) && opts[:border_left]
|
|
103
|
+
border_right(**opts[:border_right]) if opts.key?(:border_right) && opts[:border_right]
|
|
104
|
+
border_top(**opts[:border_top]) if opts.key?(:border_top) && opts[:border_top]
|
|
105
|
+
border_bottom(**opts[:border_bottom]) if opts.key?(:border_bottom) && opts[:border_bottom]
|
|
106
|
+
end
|
|
46
107
|
|
|
47
108
|
number_format(opts[:number_format]) if opts.key?(:number_format)
|
|
48
109
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
110
|
+
if opts.key?(:alignment)
|
|
111
|
+
align_opts = opts[:alignment] || {}
|
|
112
|
+
align_horizontal(align_opts[:horizontal]) if align_opts.key?(:horizontal)
|
|
113
|
+
align_vertical(align_opts[:vertical]) if align_opts.key?(:vertical)
|
|
114
|
+
wrap_text(align_opts[:wrap_text]) if align_opts.key?(:wrap_text)
|
|
115
|
+
text_rotation(align_opts[:text_rotation]) if align_opts.key?(:text_rotation)
|
|
116
|
+
indent(align_opts[:indent]) if align_opts.key?(:indent)
|
|
117
|
+
shrink_to_fit(align_opts[:shrink_to_fit]) if align_opts.key?(:shrink_to_fit)
|
|
118
|
+
else
|
|
119
|
+
align_horizontal(opts[:align_horizontal]) if opts.key?(:align_horizontal)
|
|
120
|
+
align_vertical(opts[:align_vertical]) if opts.key?(:align_vertical)
|
|
121
|
+
wrap_text(opts[:wrap_text]) if opts.key?(:wrap_text)
|
|
122
|
+
text_rotation(opts[:text_rotation]) if opts.key?(:text_rotation)
|
|
123
|
+
indent(opts[:indent]) if opts.key?(:indent)
|
|
124
|
+
shrink_to_fit(opts[:shrink_to_fit]) if opts.key?(:shrink_to_fit)
|
|
125
|
+
end
|
|
55
126
|
|
|
56
127
|
self
|
|
57
128
|
end
|
|
58
129
|
|
|
59
130
|
# --- Font Properties ---
|
|
60
131
|
|
|
132
|
+
# : (**untyped opts) -> self
|
|
133
|
+
def font(**opts)
|
|
134
|
+
bold(opts[:bold]) if opts.key?(:bold)
|
|
135
|
+
italic(opts[:italic]) if opts.key?(:italic)
|
|
136
|
+
size(opts[:size]) if opts.key?(:size)
|
|
137
|
+
font_name(opts[:name]) if opts.key?(:name)
|
|
138
|
+
font_color(opts[:color]) if opts.key?(:color)
|
|
139
|
+
underline(opts[:underline]) if opts.key?(:underline)
|
|
140
|
+
strike(opts[:strike]) if opts.key?(:strike)
|
|
141
|
+
vert_align(opts[:vert_align]) if opts.key?(:vert_align)
|
|
142
|
+
self
|
|
143
|
+
end
|
|
144
|
+
|
|
61
145
|
# rubocop:disable Style/OptionalBooleanParameter
|
|
146
|
+
# : (?bool value) -> self
|
|
62
147
|
def bold(value = true)
|
|
63
148
|
@font_props[:bold] = value
|
|
64
149
|
self
|
|
65
150
|
end
|
|
66
151
|
|
|
152
|
+
# : (?bool value) -> self
|
|
67
153
|
def italic(value = true)
|
|
68
154
|
@font_props[:italic] = value
|
|
69
155
|
self
|
|
70
156
|
end
|
|
71
157
|
|
|
158
|
+
# : (Numeric size_value) -> self
|
|
72
159
|
def size(size_value)
|
|
73
160
|
@font_props[:sz] = size_value.to_i
|
|
74
161
|
self
|
|
75
162
|
end
|
|
76
163
|
|
|
164
|
+
# : (String name) -> self
|
|
77
165
|
def font_name(name)
|
|
78
166
|
@font_props[:name] = name
|
|
79
167
|
self
|
|
80
168
|
end
|
|
81
169
|
|
|
170
|
+
# : (String | Symbol color) -> self
|
|
82
171
|
def font_color(color)
|
|
83
|
-
@font_props[:color] = color
|
|
172
|
+
@font_props[:color] = resolve_color(color)
|
|
84
173
|
self
|
|
85
174
|
end
|
|
86
175
|
|
|
176
|
+
# : (?String val) -> self
|
|
87
177
|
def underline(val = "single")
|
|
88
178
|
@font_props[:underline] = val
|
|
89
179
|
self
|
|
90
180
|
end
|
|
91
181
|
|
|
182
|
+
# : (?bool value) -> self
|
|
92
183
|
def strike(value = true)
|
|
93
184
|
@font_props[:strike] = value
|
|
94
185
|
self
|
|
95
186
|
end
|
|
96
187
|
|
|
188
|
+
# : (String value) -> self
|
|
97
189
|
def vert_align(value)
|
|
98
190
|
@font_props[:vert_align] = value
|
|
99
191
|
self
|
|
@@ -102,6 +194,7 @@ module Xlsxrb
|
|
|
102
194
|
|
|
103
195
|
# --- Fill Properties ---
|
|
104
196
|
|
|
197
|
+
# : (String | Symbol pattern, ?fg_color: String | Symbol | nil, ?bg_color: String | Symbol | nil) -> self
|
|
105
198
|
def fill_pattern(pattern, fg_color: nil, bg_color: nil)
|
|
106
199
|
@fill_props[:pattern] = pattern
|
|
107
200
|
@fill_props[:fg_color] = fg_color if fg_color
|
|
@@ -109,16 +202,19 @@ module Xlsxrb
|
|
|
109
202
|
self
|
|
110
203
|
end
|
|
111
204
|
|
|
205
|
+
# : (String | Symbol color) -> self
|
|
112
206
|
def fill_color(color)
|
|
113
207
|
@fill_props[:pattern] = "solid"
|
|
114
|
-
@fill_props[:fg_color] = color
|
|
208
|
+
@fill_props[:fg_color] = resolve_color(color)
|
|
115
209
|
self
|
|
116
210
|
end
|
|
117
211
|
|
|
212
|
+
# : (?pattern: String | Symbol, ?fg_color: String | Symbol | nil, ?bg_color: String | Symbol | nil) -> self
|
|
118
213
|
def fill(pattern: "solid", fg_color: nil, bg_color: nil)
|
|
119
|
-
fill_pattern(pattern, fg_color: fg_color, bg_color: bg_color)
|
|
214
|
+
fill_pattern(pattern, fg_color: resolve_color(fg_color), bg_color: resolve_color(bg_color))
|
|
120
215
|
end
|
|
121
216
|
|
|
217
|
+
# : (type: String, ?degree: Numeric | nil, ?stops: Array[untyped]) -> self
|
|
122
218
|
def fill_gradient(type:, degree: nil, stops: [])
|
|
123
219
|
@fill_props[:gradient] = {
|
|
124
220
|
type: type,
|
|
@@ -130,8 +226,19 @@ module Xlsxrb
|
|
|
130
226
|
|
|
131
227
|
# --- Border Properties ---
|
|
132
228
|
|
|
229
|
+
# : (**untyped opts) -> self
|
|
230
|
+
def border(**opts)
|
|
231
|
+
border_all(**opts[:all]) if opts.key?(:all) && opts[:all]
|
|
232
|
+
border_left(**opts[:left]) if opts.key?(:left) && opts[:left]
|
|
233
|
+
border_right(**opts[:right]) if opts.key?(:right) && opts[:right]
|
|
234
|
+
border_top(**opts[:top]) if opts.key?(:top) && opts[:top]
|
|
235
|
+
border_bottom(**opts[:bottom]) if opts.key?(:bottom) && opts[:bottom]
|
|
236
|
+
self
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# : (?style: String | Symbol, ?color: String | Symbol | nil) -> self
|
|
133
240
|
def border_all(style: "thin", color: nil)
|
|
134
|
-
color_opt = color ? { color: color } : {}
|
|
241
|
+
color_opt = color ? { color: resolve_color(color) } : {}
|
|
135
242
|
@border_props[:left] = { style: style, **color_opt }
|
|
136
243
|
@border_props[:right] = { style: style, **color_opt }
|
|
137
244
|
@border_props[:top] = { style: style, **color_opt }
|
|
@@ -139,29 +246,34 @@ module Xlsxrb
|
|
|
139
246
|
self
|
|
140
247
|
end
|
|
141
248
|
|
|
249
|
+
# : (?style: String | Symbol, ?color: String | Symbol | nil) -> self
|
|
142
250
|
def border_left(style: "thin", color: nil)
|
|
143
|
-
@border_props[:left] = { style: style, color: color }.compact
|
|
251
|
+
@border_props[:left] = { style: style, color: resolve_color(color) }.compact
|
|
144
252
|
self
|
|
145
253
|
end
|
|
146
254
|
|
|
255
|
+
# : (?style: String | Symbol, ?color: String | Symbol | nil) -> self
|
|
147
256
|
def border_right(style: "thin", color: nil)
|
|
148
|
-
@border_props[:right] = { style: style, color: color }.compact
|
|
257
|
+
@border_props[:right] = { style: style, color: resolve_color(color) }.compact
|
|
149
258
|
self
|
|
150
259
|
end
|
|
151
260
|
|
|
261
|
+
# : (?style: String | Symbol, ?color: String | Symbol | nil) -> self
|
|
152
262
|
def border_top(style: "thin", color: nil)
|
|
153
|
-
@border_props[:top] = { style: style, color: color }.compact
|
|
263
|
+
@border_props[:top] = { style: style, color: resolve_color(color) }.compact
|
|
154
264
|
self
|
|
155
265
|
end
|
|
156
266
|
|
|
267
|
+
# : (?style: String | Symbol, ?color: String | Symbol | nil) -> self
|
|
157
268
|
def border_bottom(style: "thin", color: nil)
|
|
158
|
-
@border_props[:bottom] = { style: style, color: color }.compact
|
|
269
|
+
@border_props[:bottom] = { style: style, color: resolve_color(color) }.compact
|
|
159
270
|
self
|
|
160
271
|
end
|
|
161
272
|
|
|
162
273
|
# rubocop:disable Naming/MethodParameterName
|
|
274
|
+
# : (?style: String | Symbol, ?color: String | Symbol | nil, ?up: bool, ?down: bool) -> self
|
|
163
275
|
def border_diagonal(style: "thin", color: nil, up: false, down: false)
|
|
164
|
-
@border_props[:diagonal] = { style: style, color: color }.compact
|
|
276
|
+
@border_props[:diagonal] = { style: style, color: resolve_color(color) }.compact
|
|
165
277
|
@border_props[:diagonal_up] = true if up
|
|
166
278
|
@border_props[:diagonal_down] = true if down
|
|
167
279
|
self
|
|
@@ -170,33 +282,39 @@ module Xlsxrb
|
|
|
170
282
|
|
|
171
283
|
# --- Alignment Properties ---
|
|
172
284
|
|
|
285
|
+
# : (String | Symbol value) -> self
|
|
173
286
|
def align_horizontal(value)
|
|
174
287
|
@alignment[:horizontal] = value
|
|
175
288
|
self
|
|
176
289
|
end
|
|
177
290
|
|
|
291
|
+
# : (String | Symbol value) -> self
|
|
178
292
|
def align_vertical(value)
|
|
179
293
|
@alignment[:vertical] = value
|
|
180
294
|
self
|
|
181
295
|
end
|
|
182
296
|
|
|
183
297
|
# rubocop:disable Style/OptionalBooleanParameter
|
|
298
|
+
# : (?bool value) -> self
|
|
184
299
|
def wrap_text(value = true)
|
|
185
300
|
@alignment[:wrap_text] = value
|
|
186
301
|
self
|
|
187
302
|
end
|
|
188
303
|
|
|
304
|
+
# : (?bool value) -> self
|
|
189
305
|
def shrink_to_fit(value = true)
|
|
190
306
|
@alignment[:shrink_to_fit] = value
|
|
191
307
|
self
|
|
192
308
|
end
|
|
193
309
|
# rubocop:enable Style/OptionalBooleanParameter
|
|
194
310
|
|
|
311
|
+
# : (Numeric value) -> self
|
|
195
312
|
def text_rotation(value)
|
|
196
313
|
@alignment[:text_rotation] = value
|
|
197
314
|
self
|
|
198
315
|
end
|
|
199
316
|
|
|
317
|
+
# : (Numeric value) -> self
|
|
200
318
|
def indent(value)
|
|
201
319
|
@alignment[:indent] = value.to_i
|
|
202
320
|
self
|
|
@@ -204,6 +322,7 @@ module Xlsxrb
|
|
|
204
322
|
|
|
205
323
|
# --- Number Format ---
|
|
206
324
|
|
|
325
|
+
# : (String | Integer num_fmt_id) -> self
|
|
207
326
|
def number_format(num_fmt_id)
|
|
208
327
|
@num_fmt_id = num_fmt_id
|
|
209
328
|
self
|
|
@@ -212,6 +331,7 @@ module Xlsxrb
|
|
|
212
331
|
|
|
213
332
|
# Register this style with the given Writer, returning the style_id.
|
|
214
333
|
# writer:: Xlsxrb::Ooxml::Writer instance
|
|
334
|
+
# : (untyped writer) -> Integer
|
|
215
335
|
def register_with(writer)
|
|
216
336
|
font_id = 0
|
|
217
337
|
fill_id = 0
|