xlsxrb 0.1.4 → 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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +1 -1
  3. data/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +9 -0
  4. data/.gem_rbs_collection/ast/2.4/ast.rbs +73 -0
  5. data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +9 -0
  6. data/.gem_rbs_collection/concurrent-ruby/1.1/array.rbs +4 -0
  7. data/.gem_rbs_collection/concurrent-ruby/1.1/atomic_reference.rbs +16 -0
  8. data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +96 -0
  9. data/.gem_rbs_collection/concurrent-ruby/1.1/hash.rbs +4 -0
  10. data/.gem_rbs_collection/concurrent-ruby/1.1/map.rbs +68 -0
  11. data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +249 -0
  12. data/.gem_rbs_collection/concurrent-ruby/1.1/set.rbs +4 -0
  13. data/.gem_rbs_collection/concurrent-ruby/1.1/timer_task.rbs +47 -0
  14. data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +5 -0
  15. data/.gem_rbs_collection/csv/3.3/.rbs_meta.yaml +9 -0
  16. data/.gem_rbs_collection/csv/3.3/csv.rbs +3871 -0
  17. data/.gem_rbs_collection/csv/3.3/manifest.yaml +3 -0
  18. data/.gem_rbs_collection/lint_roller/1.1/.rbs_meta.yaml +9 -0
  19. data/.gem_rbs_collection/lint_roller/1.1/lint_roller.rbs +48 -0
  20. data/.gem_rbs_collection/listen/3.9/.rbs_meta.yaml +9 -0
  21. data/.gem_rbs_collection/listen/3.9/listen.rbs +25 -0
  22. data/.gem_rbs_collection/listen/3.9/listener.rbs +24 -0
  23. data/.gem_rbs_collection/logger/1.7/.rbs_meta.yaml +9 -0
  24. data/.gem_rbs_collection/logger/1.7/formatter.rbs +45 -0
  25. data/.gem_rbs_collection/logger/1.7/log_device.rbs +100 -0
  26. data/.gem_rbs_collection/logger/1.7/logger.rbs +796 -0
  27. data/.gem_rbs_collection/logger/1.7/manifest.yaml +2 -0
  28. data/.gem_rbs_collection/logger/1.7/period.rbs +17 -0
  29. data/.gem_rbs_collection/logger/1.7/severity.rbs +34 -0
  30. data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +9 -0
  31. data/.gem_rbs_collection/parallel/1.20/parallel.rbs +86 -0
  32. data/.gem_rbs_collection/parser/3.2/.rbs_meta.yaml +9 -0
  33. data/.gem_rbs_collection/parser/3.2/manifest.yaml +7 -0
  34. data/.gem_rbs_collection/parser/3.2/parser.rbs +194 -0
  35. data/.gem_rbs_collection/parser/3.2/polyfill.rbs +4 -0
  36. data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +9 -0
  37. data/.gem_rbs_collection/rainbow/3.0/global.rbs +7 -0
  38. data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +209 -0
  39. data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +5 -0
  40. data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +9 -0
  41. data/.gem_rbs_collection/rake/13.0/manifest.yaml +2 -0
  42. data/.gem_rbs_collection/rake/13.0/rake.rbs +39 -0
  43. data/.gem_rbs_collection/regexp_parser/2.8/.rbs_meta.yaml +9 -0
  44. data/.gem_rbs_collection/regexp_parser/2.8/regexp_parser.rbs +17 -0
  45. data/.gem_rbs_collection/rubocop/1.57/.rbs_meta.yaml +9 -0
  46. data/.gem_rbs_collection/rubocop/1.57/rubocop.rbs +208 -0
  47. data/.gem_rbs_collection/rubocop-ast/1.46/.rbs_meta.yaml +9 -0
  48. data/.gem_rbs_collection/rubocop-ast/1.46/rubocop-ast.rbs +903 -0
  49. data/CHANGELOG.md +24 -3
  50. data/README.md +91 -17
  51. data/Rakefile +22 -2
  52. data/Steepfile +19 -0
  53. data/docs/ARCHITECTURE.md +17 -0
  54. data/docs/DEVELOPMENT.md +58 -0
  55. data/docs/QUALITY_ASSURANCE.md +26 -0
  56. data/docs/visual/VisualGallery.md +893 -938
  57. data/docs/wasm/ruby.wasm +0 -0
  58. data/lib/xlsxrb/elements/cell.rb +74 -1
  59. data/lib/xlsxrb/elements/column.rb +2 -0
  60. data/lib/xlsxrb/elements/row.rb +37 -2
  61. data/lib/xlsxrb/elements/types.rb +2 -0
  62. data/lib/xlsxrb/elements/workbook.rb +29 -0
  63. data/lib/xlsxrb/elements/worksheet.rb +98 -1
  64. data/lib/xlsxrb/elements.rb +2 -0
  65. data/lib/xlsxrb/ooxml/reader.rb +8 -6
  66. data/lib/xlsxrb/ooxml/shared_strings_parser.rb +2 -0
  67. data/lib/xlsxrb/ooxml/styles_parser.rb +2 -0
  68. data/lib/xlsxrb/ooxml/utils.rb +2 -0
  69. data/lib/xlsxrb/ooxml/workbook_parser.rb +6 -1
  70. data/lib/xlsxrb/ooxml/workbook_writer.rb +15 -4
  71. data/lib/xlsxrb/ooxml/worksheet_parser.rb +12 -0
  72. data/lib/xlsxrb/ooxml/worksheet_writer.rb +49 -9
  73. data/lib/xlsxrb/ooxml/writer.rb +217 -5
  74. data/lib/xlsxrb/ooxml/xml_builder.rb +2 -0
  75. data/lib/xlsxrb/ooxml/xml_parser.rb +2 -0
  76. data/lib/xlsxrb/ooxml/zip_generator.rb +5 -8
  77. data/lib/xlsxrb/ooxml/zip_reader.rb +88 -52
  78. data/lib/xlsxrb/ooxml/zip_writer.rb +2 -0
  79. data/lib/xlsxrb/ooxml.rb +3 -1
  80. data/lib/xlsxrb/style_builder.rb +156 -36
  81. data/lib/xlsxrb/version.rb +3 -1
  82. data/lib/xlsxrb.rb +1510 -231
  83. data/rbs_collection.lock.yaml +224 -0
  84. data/rbs_collection.yaml +19 -0
  85. data/sig/generated/xlsxrb/elements.rbs +8 -0
  86. data/sig/generated/xlsxrb/ooxml.rbs +23 -0
  87. data/sig/generated/xlsxrb/style_builder.rbs +88 -0
  88. data/sig/generated/xlsxrb/version.rbs +5 -0
  89. metadata +71 -5
  90. data/benchmark.rb +0 -400
  91. data/measure_memory.rb +0 -42
  92. 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
  module Xlsxrb
4
6
  module Ooxml
5
7
  # Streams well-formed XML to a writable IO without building a DOM.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rbs_inline: enabled
4
+
3
5
  require "rexml/parsers/sax2parser"
4
6
  require "rexml/sax2listener"
5
7
 
@@ -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
- [(value & 0xFF), ((value >> 8) & 0xFF)]
147
+ [value].pack("v").bytes
146
148
  end
147
149
 
148
150
  def le32(value)
149
- [(value & 0xFF), ((value >> 8) & 0xFF), ((value >> 16) & 0xFF), ((value >> 24) & 0xFF)]
151
+ [value].pack("V").bytes
150
152
  end
151
153
 
152
154
  def calculate_central_dir_size
153
- size = 0
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 = [0x50, 0x4B, 0x03, 0x04].pack("C4")
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
- while pos + 4 <= data.bytesize
65
- sig = data[pos, 4]
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
- # Local file header: 4 sig + 2 version + 2 flags + 2 method + 2 time + 2 date
69
- # + 4 crc + 4 compressed + 4 uncompressed + 2 name_len + 2 extra_len = 30 bytes
70
- break if pos + 30 > data.bytesize
89
+ header = io.read(26)
90
+ break unless header && header.bytesize == 26
71
91
 
72
- gp_flag = data[pos + 6, 2].unpack1("v")
73
- method = data[pos + 8, 2].unpack1("v")
74
- data[pos + 14, 4].unpack1("V")
75
- compressed_size = data[pos + 18, 4].unpack1("V")
76
- data[pos + 22, 4].unpack1("V")
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 = data[pos + 30, name_len].force_encoding("UTF-8")
81
- file_data_offset = pos + 30 + name_len + extra_len
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
- # Need to find the data descriptor to know sizes
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 = data[file_data_offset, compressed_size]
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 find_data_descriptor(data, offset, method)
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 pos < data.bytesize
120
- chunk = data[pos, [chunk_size, data.bytesize - pos].min]
121
- break if chunk.nil? || chunk.empty?
127
+ while (chunk = io.read(chunk_size))
128
+ break if chunk.empty?
122
129
 
123
- result << inflater.inflate(chunk)
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 — find actual consumed size
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
- [result, consumed, result.bytesize]
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
- # Stored — scan for data descriptor signature or central directory
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
- Zlib::Inflate.inflate(-raw || "")
159
+ safe_inflate(raw || "", -Zlib::MAX_WBITS)
147
160
  rescue Zlib::DataError
148
161
  # Try with raw deflate (no header)
149
- inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
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
- inflater.inflate(raw || "")
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
@@ -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
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 = Struct.new(:type, :args, :source)
22
+ Event = Data.define(:type, :args, :source)
21
23
  end
22
24
  end
@@ -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
- bold(opts[:bold]) if opts.key?(:bold)
22
- italic(opts[:italic]) if opts.key?(:italic)
23
- size(opts[:size]) if opts.key?(:size)
24
- font_name(opts[:font_name]) if opts.key?(:font_name)
25
- font_color(opts[:font_color]) if opts.key?(:font_color)
26
- underline(opts[:underline]) if opts.key?(:underline)
27
- strike(opts[:strike]) if opts.key?(:strike)
28
- vert_align(opts[:vert_align]) if opts.key?(:vert_align)
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
- fill_color(opts[:fill_color]) if opts.key?(:fill_color)
31
- if opts.key?(:fill_pattern)
32
- pattern = opts[:fill_pattern] || {}
33
- fill_pattern(
34
- pattern[:pattern] || "solid",
35
- fg_color: pattern[:fg_color],
36
- bg_color: pattern[:bg_color]
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
- border_all(**opts[:border_all]) if opts.key?(:border_all) && opts[:border_all]
42
- border_left(**opts[:border_left]) if opts.key?(:border_left) && opts[:border_left]
43
- border_right(**opts[:border_right]) if opts.key?(:border_right) && opts[:border_right]
44
- border_top(**opts[:border_top]) if opts.key?(:border_top) && opts[:border_top]
45
- border_bottom(**opts[:border_bottom]) if opts.key?(:border_bottom) && opts[:border_bottom]
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
- align_horizontal(opts[:align_horizontal]) if opts.key?(:align_horizontal)
50
- align_vertical(opts[:align_vertical]) if opts.key?(:align_vertical)
51
- wrap_text(opts[:wrap_text]) if opts.key?(:wrap_text)
52
- text_rotation(opts[:text_rotation]) if opts.key?(:text_rotation)
53
- indent(opts[:indent]) if opts.key?(:indent)
54
- shrink_to_fit(opts[:shrink_to_fit]) if opts.key?(:shrink_to_fit)
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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rbs_inline: enabled
4
+
3
5
  module Xlsxrb
4
- VERSION = "0.1.4"
6
+ VERSION = "0.1.5"
5
7
  end