xlsxrb 0.1.4 → 0.1.6
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 +1 -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/nokogiri/1.11/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/nokogiri/1.11/nokogiri.rbs +2332 -0
- data/.gem_rbs_collection/nokogiri/1.11/patch.rbs +4 -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/.gem_rbs_collection/rubyzip/3.2/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubyzip/3.2/manifest.yaml +8 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/central_directory.rbs +42 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/compressor.rbs +5 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/constants.rbs +47 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/aes_encryption.rbs +30 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/decrypted_io.rbs +9 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/encryption.rbs +7 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/null_encryption.rbs +19 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/traditional_encryption.rbs +31 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/decompressor.rbs +18 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/deflater.rbs +12 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/dirtyable.rbs +11 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/dos_time.rbs +13 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/entry.rbs +95 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/entry_set.rbs +31 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/errors.rbs +58 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/aes.rbs +24 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/generic.rbs +17 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/ntfs.rbs +23 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/old_unix.rbs +22 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/universal_time.rbs +30 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unix.rbs +20 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unknown.rbs +15 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/zip64.rbs +26 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field.rbs +21 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/file.rbs +131 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/file_split.rbs +14 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/dir.rbs +33 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/directory_iterator.rbs +21 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file.rbs +63 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file_stat.rbs +55 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/zip_file_name_mapper.rbs +35 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem.rbs +7 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/inflater.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/input_stream.rbs +22 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_input_stream.rbs +29 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_output_stream.rbs +17 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras.rbs +13 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_compressor.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_decompressor.rbs +8 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_input_stream.rbs +6 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/output_stream.rbs +30 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_compressor.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_decompressor.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_directory.rbs +5 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_stream.rbs +15 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/version.rbs +3 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip.rbs +40 -0
- data/CHANGELOG.md +42 -3
- data/README.md +130 -49
- data/Rakefile +23 -2
- data/Steepfile +17 -0
- data/benchmark.rb +324 -348
- data/docs/ARCHITECTURE.md +24 -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 +113 -8
- data/lib/xlsxrb/elements/column.rb +2 -0
- data/lib/xlsxrb/elements/row.rb +52 -4
- data/lib/xlsxrb/elements/types.rb +6 -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 +106 -40
- 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 +156 -23
- data/lib/xlsxrb/ooxml/worksheet_writer.rb +207 -152
- data/lib/xlsxrb/ooxml/writer.rb +217 -5
- data/lib/xlsxrb/ooxml/xml_builder.rb +18 -10
- 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 +179 -57
- data/lib/xlsxrb/ooxml/zip_writer.rb +65 -25
- data/lib/xlsxrb/ooxml.rb +3 -1
- data/lib/xlsxrb/style_builder.rb +284 -36
- data/lib/xlsxrb/version.rb +3 -1
- data/lib/xlsxrb.rb +1761 -275
- data/rbs_collection.lock.yaml +252 -0
- data/rbs_collection.yaml +19 -0
- data/sig/generated/xlsxrb/elements/cell.rbs +30 -0
- data/sig/generated/xlsxrb/elements/column.rbs +30 -0
- data/sig/generated/xlsxrb/elements/row.rbs +32 -0
- data/sig/generated/xlsxrb/elements/types.rbs +74 -0
- data/sig/generated/xlsxrb/elements/workbook.rbs +25 -0
- data/sig/generated/xlsxrb/elements/worksheet.rbs +27 -0
- data/sig/generated/xlsxrb/elements.rbs +8 -0
- data/sig/generated/xlsxrb/ooxml/reader.rbs +1412 -0
- data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +21 -0
- data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +44 -0
- data/sig/generated/xlsxrb/ooxml/utils.rbs +40 -0
- data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +46 -0
- data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +66 -0
- data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +67 -0
- data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +92 -0
- data/sig/generated/xlsxrb/ooxml/writer.rbs +880 -0
- data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +45 -0
- data/sig/generated/xlsxrb/ooxml/xml_parser.rbs +30 -0
- data/sig/generated/xlsxrb/ooxml/zip_generator.rbs +38 -0
- data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +45 -0
- data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +45 -0
- data/sig/generated/xlsxrb/ooxml.rbs +23 -0
- data/sig/generated/xlsxrb/style_builder.rbs +250 -0
- data/sig/generated/xlsxrb/version.rbs +5 -0
- data/sig/generated/xlsxrb.rbs +1276 -0
- data/sig/rexml.rbs +4 -0
- metadata +145 -4
- data/measure_memory.rb +0 -42
- data/sig/xlsxrb.rbs +0 -23
data/lib/xlsxrb.rb
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
3
5
|
require "date"
|
|
6
|
+
require "time"
|
|
4
7
|
require "openssl"
|
|
5
8
|
require "securerandom"
|
|
6
9
|
require "tempfile"
|
|
10
|
+
begin
|
|
11
|
+
require "bigdecimal"
|
|
12
|
+
rescue LoadError
|
|
13
|
+
# simplecov:disable
|
|
14
|
+
# Define a dummy class for environments without bigdecimal (e.g., ruby.wasm).
|
|
15
|
+
# This serves only as a fallback to prevent NameError in `case` statements (`when BigDecimal`).
|
|
16
|
+
# Impossible to cover in standard test environment because bigdecimal is present.
|
|
17
|
+
Object.const_set(:BigDecimal, Class.new)
|
|
18
|
+
# simplecov:enable
|
|
19
|
+
end
|
|
7
20
|
require "opentelemetry"
|
|
8
21
|
require_relative "xlsxrb/version"
|
|
9
22
|
require_relative "xlsxrb/ooxml/zip_generator"
|
|
@@ -15,26 +28,74 @@ require_relative "xlsxrb/style_builder"
|
|
|
15
28
|
|
|
16
29
|
# Ruby XLSX read/write library.
|
|
17
30
|
module Xlsxrb
|
|
18
|
-
class Error < StandardError
|
|
31
|
+
class Error < StandardError
|
|
32
|
+
DIV0 = Elements::CellError.new(code: "#DIV/0!")
|
|
33
|
+
NA = Elements::CellError.new(code: "#N/A")
|
|
34
|
+
NAME = Elements::CellError.new(code: "#NAME?")
|
|
35
|
+
NULL = Elements::CellError.new(code: "#NULL!")
|
|
36
|
+
NUM = Elements::CellError.new(code: "#NUM!")
|
|
37
|
+
REF = Elements::CellError.new(code: "#REF!")
|
|
38
|
+
VALUE = Elements::CellError.new(code: "#VALUE!")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class ParseError < Error; end
|
|
42
|
+
class ValidationError < Error; end
|
|
43
|
+
class ZipError < Error; end
|
|
19
44
|
|
|
20
45
|
TRACER = OpenTelemetry.tracer_provider.tracer("xlsxrb", Xlsxrb::VERSION)
|
|
21
46
|
|
|
47
|
+
def self.in_span(name, attributes: nil, &)
|
|
48
|
+
if defined?(Ractor) && Ractor.current != Ractor.main
|
|
49
|
+
# simplecov:disable
|
|
50
|
+
# Test suite runs in the main Ractor. This branch is for multi-threaded usage via Ractors.
|
|
51
|
+
yield
|
|
52
|
+
# simplecov:enable
|
|
53
|
+
elsif attributes
|
|
54
|
+
TRACER.in_span(name, attributes: attributes, &)
|
|
55
|
+
else
|
|
56
|
+
TRACER.in_span(name, &)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Helper to easily create RichText objects.
|
|
61
|
+
# Supports both `Xlsxrb.rich_text({ text: "A" }, { text: "B" })`
|
|
62
|
+
# and `Xlsxrb.rich_text(text: "Hi", bold: true)`
|
|
63
|
+
#
|
|
64
|
+
# @param runs [Array<Hash>] Optional rich text runs.
|
|
65
|
+
# @param text [String, nil] Simple text.
|
|
66
|
+
# @param font_props [Hash] Font styling options (e.g., bold: true).
|
|
67
|
+
# @return [Elements::RichText] The resulting rich text.
|
|
68
|
+
# @api public
|
|
69
|
+
#: (*untyped runs, ?text: String?, **untyped font_props) -> untyped
|
|
70
|
+
def self.rich_text(*runs, text: nil, **font_props)
|
|
71
|
+
runs = [{ text: text, font: font_props }] if text
|
|
72
|
+
Elements::RichText.new(runs: runs)
|
|
73
|
+
end
|
|
74
|
+
|
|
22
75
|
# Builder for block-style chart definitions.
|
|
76
|
+
# @api public
|
|
23
77
|
class ChartBuilder
|
|
78
|
+
#: () -> void
|
|
24
79
|
def initialize
|
|
25
80
|
@options = {}
|
|
26
81
|
end
|
|
27
|
-
|
|
82
|
+
#: Hash[Symbol, untyped]
|
|
28
83
|
attr_reader :options
|
|
29
84
|
|
|
85
|
+
# @api public
|
|
86
|
+
#: (untyped value) -> untyped
|
|
30
87
|
def type(value) = @options[:type] = value
|
|
88
|
+
# @api public
|
|
89
|
+
#: (untyped value) -> untyped
|
|
31
90
|
def title(value) = @options[:title] = value
|
|
32
91
|
|
|
33
|
-
|
|
92
|
+
# @api public
|
|
93
|
+
#: (?Hash[Symbol, untyped]? value) ?{ (SeriesBuilder) -> void } -> Array[Hash[Symbol, untyped]]
|
|
94
|
+
def series(value = nil)
|
|
34
95
|
@options[:series] ||= []
|
|
35
96
|
if block_given?
|
|
36
97
|
sb = SeriesBuilder.new
|
|
37
|
-
|
|
98
|
+
yield sb
|
|
38
99
|
@options[:series] << sb.options
|
|
39
100
|
elsif value
|
|
40
101
|
@options[:series] << value
|
|
@@ -42,30 +103,234 @@ module Xlsxrb
|
|
|
42
103
|
@options[:series]
|
|
43
104
|
end
|
|
44
105
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
106
|
+
# Configures the legend property for this chart.
|
|
107
|
+
# @param args [Array] Positional arguments.
|
|
108
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
109
|
+
# @return [Object] the configured property
|
|
110
|
+
# @api public
|
|
111
|
+
#: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
|
|
112
|
+
def legend(*args, **kwargs)
|
|
113
|
+
@options[:legend] = kwargs.empty? ? args.first : kwargs
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Configures the plot_area property for this chart.
|
|
117
|
+
# @param args [Array] Positional arguments.
|
|
118
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
119
|
+
# @return [Object] the configured property
|
|
120
|
+
# @api public
|
|
121
|
+
#: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
|
|
122
|
+
def plot_area(*args, **kwargs)
|
|
123
|
+
@options[:plot_area] = kwargs.empty? ? args.first : kwargs
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Configures the chart_space property for this chart.
|
|
127
|
+
# @param args [Array] Positional arguments.
|
|
128
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
129
|
+
# @return [Object] the configured property
|
|
130
|
+
# @api public
|
|
131
|
+
#: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
|
|
132
|
+
def chart_space(*args, **kwargs)
|
|
133
|
+
@options[:chart_space] = kwargs.empty? ? args.first : kwargs
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Configures the style property for this chart.
|
|
137
|
+
# @param args [Array] Positional arguments.
|
|
138
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
139
|
+
# @return [Object] the configured property
|
|
140
|
+
# @api public
|
|
141
|
+
#: (*(Hash[Symbol, String | Integer | bool | nil] | String | Integer) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String | Integer)
|
|
142
|
+
def style(*args, **kwargs)
|
|
143
|
+
@options[:style] = kwargs.empty? ? args.first : kwargs
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Configures the data_labels property for this chart.
|
|
147
|
+
# @param args [Array] Positional arguments.
|
|
148
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
149
|
+
# @return [Object] the configured property
|
|
150
|
+
# @api public
|
|
151
|
+
#: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
|
|
152
|
+
def data_labels(*args, **kwargs)
|
|
153
|
+
@options[:data_labels] = kwargs.empty? ? args.first : kwargs
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Configures the plot_visible_only property for this chart.
|
|
157
|
+
# @param args [Array] Positional arguments.
|
|
158
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
159
|
+
# @return [Object] the configured property
|
|
160
|
+
# @api public
|
|
161
|
+
#: (*(bool | String) args, **String | Integer | bool | nil kwargs) -> (bool | String)
|
|
162
|
+
def plot_visible_only(*args, **kwargs)
|
|
163
|
+
@options[:plot_visible_only] = kwargs.empty? ? args.first : kwargs
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Configures the display_blanks_as property for this chart.
|
|
167
|
+
# @param args [Array] Positional arguments.
|
|
168
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
169
|
+
# @return [Object] the configured property
|
|
170
|
+
# @api public
|
|
171
|
+
#: (*(String) args, **String | Integer | bool | nil kwargs) -> String
|
|
172
|
+
def display_blanks_as(*args, **kwargs)
|
|
173
|
+
@options[:display_blanks_as] = kwargs.empty? ? args.first : kwargs
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Configures the view3d property for this chart.
|
|
177
|
+
# @param args [Array] Positional arguments.
|
|
178
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
179
|
+
# @return [Object] the configured property
|
|
180
|
+
# @api public
|
|
181
|
+
#: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
|
|
182
|
+
def view3d(*args, **kwargs)
|
|
183
|
+
@options[:view3d] = kwargs.empty? ? args.first : kwargs
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Configures the category_axis property for this chart.
|
|
187
|
+
# @param args [Array] Positional arguments.
|
|
188
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
189
|
+
# @return [Object] the configured property
|
|
190
|
+
# @api public
|
|
191
|
+
#: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
|
|
192
|
+
def category_axis(*args, **kwargs)
|
|
193
|
+
@options[:category_axis] = kwargs.empty? ? args.first : kwargs
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Configures the value_axis property for this chart.
|
|
197
|
+
# @param args [Array] Positional arguments.
|
|
198
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
199
|
+
# @return [Object] the configured property
|
|
200
|
+
# @api public
|
|
201
|
+
#: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
|
|
202
|
+
def value_axis(*args, **kwargs)
|
|
203
|
+
@options[:value_axis] = kwargs.empty? ? args.first : kwargs
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Configures the show_legend_key property for this chart.
|
|
207
|
+
# @param args [Array] Positional arguments.
|
|
208
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
209
|
+
# @return [Object] the configured property
|
|
210
|
+
# @api public
|
|
211
|
+
#: (*(bool | String) args, **String | Integer | bool | nil kwargs) -> (bool | String)
|
|
212
|
+
def show_legend_key(*args, **kwargs)
|
|
213
|
+
@options[:show_legend_key] = kwargs.empty? ? args.first : kwargs
|
|
52
214
|
end
|
|
53
215
|
|
|
54
216
|
# Builder for a single series entry in block-style chart definitions.
|
|
217
|
+
# @api public
|
|
55
218
|
class SeriesBuilder
|
|
219
|
+
#: () -> void
|
|
56
220
|
def initialize
|
|
57
221
|
@options = {}
|
|
58
222
|
end
|
|
59
|
-
|
|
223
|
+
#: Hash[Symbol, untyped]
|
|
60
224
|
attr_reader :options
|
|
61
225
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
226
|
+
# Configures the categories property for this series.
|
|
227
|
+
# @param args [Array] Positional arguments.
|
|
228
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
229
|
+
# @return [Object] the configured property
|
|
230
|
+
# @api public
|
|
231
|
+
#: (*untyped args, **untyped kwargs) -> untyped
|
|
232
|
+
def categories(*args, **kwargs)
|
|
233
|
+
@options[:categories] = kwargs.empty? ? args.first : kwargs
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Configures the values property for this series.
|
|
237
|
+
# @param args [Array] Positional arguments.
|
|
238
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
239
|
+
# @return [Object] the configured property
|
|
240
|
+
# @api public
|
|
241
|
+
#: (*untyped args, **untyped kwargs) -> untyped
|
|
242
|
+
def values(*args, **kwargs)
|
|
243
|
+
@options[:values] = kwargs.empty? ? args.first : kwargs
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Configures the name property for this series.
|
|
247
|
+
# @param args [Array] Positional arguments.
|
|
248
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
249
|
+
# @return [Object] the configured property
|
|
250
|
+
# @api public
|
|
251
|
+
#: (*untyped args, **untyped kwargs) -> untyped
|
|
252
|
+
def name(*args, **kwargs)
|
|
253
|
+
@options[:name] = kwargs.empty? ? args.first : kwargs
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Configures the marker property for this series.
|
|
257
|
+
# @param args [Array] Positional arguments.
|
|
258
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
259
|
+
# @return [Object] the configured property
|
|
260
|
+
# @api public
|
|
261
|
+
#: (*untyped args, **untyped kwargs) -> untyped
|
|
262
|
+
def marker(*args, **kwargs)
|
|
263
|
+
@options[:marker] = kwargs.empty? ? args.first : kwargs
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Configures the fill property for this series.
|
|
267
|
+
# @param args [Array] Positional arguments.
|
|
268
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
269
|
+
# @return [Object] the configured property
|
|
270
|
+
# @api public
|
|
271
|
+
#: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
|
|
272
|
+
def fill(*args, **kwargs)
|
|
273
|
+
@options[:fill] = kwargs.empty? ? args.first : kwargs
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Configures the line property for this series.
|
|
277
|
+
# @param args [Array] Positional arguments.
|
|
278
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
279
|
+
# @return [Object] the configured property
|
|
280
|
+
# @api public
|
|
281
|
+
#: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
|
|
282
|
+
def line(*args, **kwargs)
|
|
283
|
+
@options[:line] = kwargs.empty? ? args.first : kwargs
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Configures the trendline property for this series.
|
|
287
|
+
# @param args [Array] Positional arguments.
|
|
288
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
289
|
+
# @return [Object] the configured property
|
|
290
|
+
# @api public
|
|
291
|
+
#: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
|
|
292
|
+
def trendline(*args, **kwargs)
|
|
293
|
+
@options[:trendline] = kwargs.empty? ? args.first : kwargs
|
|
65
294
|
end
|
|
66
295
|
|
|
67
|
-
|
|
68
|
-
|
|
296
|
+
# Configures the data_labels property for this series.
|
|
297
|
+
# @param args [Array] Positional arguments.
|
|
298
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
299
|
+
# @return [Object] the configured property
|
|
300
|
+
# @api public
|
|
301
|
+
#: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
|
|
302
|
+
def data_labels(*args, **kwargs)
|
|
303
|
+
@options[:data_labels] = kwargs.empty? ? args.first : kwargs
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Configures the smooth property for this series.
|
|
307
|
+
# @param args [Array] Positional arguments.
|
|
308
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
309
|
+
# @return [Object] the configured property
|
|
310
|
+
# @api public
|
|
311
|
+
#: (*(bool | String) args, **String | Integer | bool | nil kwargs) -> (bool | String)
|
|
312
|
+
def smooth(*args, **kwargs)
|
|
313
|
+
@options[:smooth] = kwargs.empty? ? args.first : kwargs
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Configures the shape property for this series.
|
|
317
|
+
# @param args [Array] Positional arguments.
|
|
318
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
319
|
+
# @return [Object] the configured property
|
|
320
|
+
# @api public
|
|
321
|
+
#: (*(String) args, **String | Integer | bool | nil kwargs) -> String
|
|
322
|
+
def shape(*args, **kwargs)
|
|
323
|
+
@options[:shape] = kwargs.empty? ? args.first : kwargs
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Configures the type property for this series.
|
|
327
|
+
# @param args [Array] Positional arguments.
|
|
328
|
+
# @param kwargs [Hash] Keyword arguments.
|
|
329
|
+
# @return [Object] the configured property
|
|
330
|
+
# @api public
|
|
331
|
+
#: (*(String) args, **String | Integer | bool | nil kwargs) -> String
|
|
332
|
+
def type(*args, **kwargs)
|
|
333
|
+
@options[:type] = kwargs.empty? ? args.first : kwargs
|
|
69
334
|
end
|
|
70
335
|
end
|
|
71
336
|
end
|
|
@@ -74,9 +339,13 @@ module Xlsxrb
|
|
|
74
339
|
# Supports method_missing for setting arbitrary keys.
|
|
75
340
|
# --- Facade API ---
|
|
76
341
|
|
|
77
|
-
# Creates a Formula object for use in
|
|
78
|
-
#
|
|
79
|
-
#
|
|
342
|
+
# Creates a Formula object for use in row values.
|
|
343
|
+
#
|
|
344
|
+
# @param expression [String] The formula text (e.g. "SUM(A1:A10)").
|
|
345
|
+
# @param cached_value [Object, nil] Optional cached result. If nil, Excel will calculate on open.
|
|
346
|
+
# @return [Elements::Formula]
|
|
347
|
+
# @api public
|
|
348
|
+
#: (String expression, ?cached_value: String | Numeric | bool | nil) -> untyped
|
|
80
349
|
def self.formula(expression, cached_value: nil)
|
|
81
350
|
Elements::Formula.new(
|
|
82
351
|
expression: expression,
|
|
@@ -86,10 +355,14 @@ module Xlsxrb
|
|
|
86
355
|
end
|
|
87
356
|
|
|
88
357
|
# Reads an XLSX file into an Elements::Workbook.
|
|
89
|
-
#
|
|
358
|
+
#
|
|
359
|
+
# @param source [String, IO] File path or IO object.
|
|
360
|
+
# @return [Elements::Workbook] The parsed workbook.
|
|
361
|
+
# @api public
|
|
362
|
+
#: (untyped source) -> untyped
|
|
90
363
|
def self.read(source)
|
|
91
364
|
attributes = source.is_a?(String) ? { "filepath" => source } : {}
|
|
92
|
-
|
|
365
|
+
Xlsxrb.in_span("Xlsxrb.read", attributes: attributes) do
|
|
93
366
|
entries = Ooxml::ZipReader.open(source, &:read_all)
|
|
94
367
|
shared_strings = Ooxml::SharedStringsParser.parse(entries["xl/sharedStrings.xml"])
|
|
95
368
|
styles = Ooxml::StylesParser.parse(entries["xl/styles.xml"])
|
|
@@ -110,13 +383,18 @@ module Xlsxrb
|
|
|
110
383
|
end
|
|
111
384
|
|
|
112
385
|
# Writes an Elements::Workbook to an XLSX file.
|
|
113
|
-
#
|
|
386
|
+
#
|
|
387
|
+
# @param target [String, IO] File path or IO object.
|
|
388
|
+
# @param workbook [Elements::Workbook] The workbook to write.
|
|
389
|
+
# @return [void]
|
|
390
|
+
# @api public
|
|
391
|
+
#: (untyped target, untyped workbook) -> void
|
|
114
392
|
def self.write(target, workbook)
|
|
115
393
|
raise Error, "target is required" if target.nil?
|
|
116
394
|
raise Error, "workbook must be an Elements::Workbook" unless workbook.is_a?(Elements::Workbook)
|
|
117
395
|
|
|
118
396
|
attributes = target.is_a?(String) ? { "filepath" => target } : {}
|
|
119
|
-
|
|
397
|
+
Xlsxrb.in_span("Xlsxrb.write", attributes: attributes) do
|
|
120
398
|
sst = []
|
|
121
399
|
sst_index = {}
|
|
122
400
|
|
|
@@ -132,7 +410,10 @@ module Xlsxrb
|
|
|
132
410
|
end
|
|
133
411
|
end
|
|
134
412
|
columns = ws.columns.map do |col|
|
|
413
|
+
# simplecov:disable
|
|
414
|
+
# Edge case / untested delegation block
|
|
135
415
|
{ index: col.index, width: col.width, hidden: col.hidden, custom_width: col.custom_width, outline_level: col.outline_level }
|
|
416
|
+
# simplecov:enable
|
|
136
417
|
end
|
|
137
418
|
sd = { name: ws.name, rows: ws.rows, columns: columns }
|
|
138
419
|
sd[:charts] = ws.charts unless ws.charts.empty?
|
|
@@ -156,101 +437,157 @@ module Xlsxrb
|
|
|
156
437
|
core_properties: wb_facade[:core_properties],
|
|
157
438
|
app_properties: wb_facade[:app_properties],
|
|
158
439
|
custom_properties: wb_facade[:custom_properties],
|
|
159
|
-
workbook_protection: wb_facade[:workbook_protection]
|
|
440
|
+
workbook_protection: wb_facade[:workbook_protection],
|
|
441
|
+
workbook_properties: wb_facade[:workbook_properties]
|
|
160
442
|
)
|
|
161
443
|
end
|
|
162
444
|
end
|
|
163
445
|
|
|
164
446
|
# Modifies an existing XLSX file.
|
|
165
447
|
# Reads the workbook, passes it to the block, and writes the result.
|
|
166
|
-
# The block receives an Elements::Workbook and must return a modified one (e.g. via `
|
|
448
|
+
# The block receives an Elements::Workbook and must return a modified one (e.g. via `update_sheet`).
|
|
167
449
|
# If no target is given, the source is overwritten.
|
|
168
450
|
#
|
|
169
|
-
#
|
|
451
|
+
# @example
|
|
170
452
|
# Xlsxrb.modify("template.xlsx", "output.xlsx") do |wb|
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
# new_sheet = sheet.with(rows: sheet.rows.map { |r| r.index == 0 ? new_row : r })
|
|
176
|
-
# wb.with(sheets: wb.sheets.map.with_index { |s, i| i == 0 ? new_sheet : s })
|
|
453
|
+
# wb.update_sheet(0) do |sheet|
|
|
454
|
+
# sheet.update_cell("B1", value: "Updated")
|
|
455
|
+
# .update_cell("B2", value: 100)
|
|
456
|
+
# end
|
|
177
457
|
# end
|
|
178
|
-
|
|
458
|
+
#
|
|
459
|
+
# @param source [String, IO] The source file path or IO object.
|
|
460
|
+
# @param target [String, IO, nil] The target file path or IO object. If nil, overwrites source.
|
|
461
|
+
# @yield [workbook] Yields the parsed workbook.
|
|
462
|
+
# @yieldparam workbook [Elements::Workbook] The parsed workbook.
|
|
463
|
+
# @yieldreturn [Elements::Workbook] The modified workbook.
|
|
464
|
+
# @return [void]
|
|
465
|
+
# @api public
|
|
466
|
+
#: (untyped source, ?untyped target) ?{ (untyped) -> untyped } -> void
|
|
467
|
+
def self.modify(source, target = nil)
|
|
179
468
|
raise Error, "source is required" if source.nil?
|
|
180
|
-
raise Error, "block is required" unless
|
|
469
|
+
raise Error, "block is required" unless block_given?
|
|
181
470
|
|
|
182
471
|
workbook = read(source)
|
|
183
|
-
result_workbook =
|
|
472
|
+
result_workbook = yield workbook
|
|
184
473
|
result_workbook = workbook unless result_workbook.is_a?(Elements::Workbook)
|
|
185
474
|
|
|
186
475
|
write_target = target || source
|
|
187
476
|
write(write_target, result_workbook)
|
|
188
477
|
end
|
|
189
478
|
|
|
190
|
-
#
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
479
|
+
# Represents a sheet being streamed sequentially.
|
|
480
|
+
class StreamSheet
|
|
481
|
+
[Enumerable].each { |m| include m }
|
|
482
|
+
|
|
483
|
+
attr_reader :name
|
|
484
|
+
|
|
485
|
+
def initialize(name, sheet_xml, shared_strings)
|
|
486
|
+
@name = name
|
|
487
|
+
@sheet_xml = sheet_xml
|
|
488
|
+
@shared_strings = shared_strings
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
#: () { (Elements::Row) -> void } -> void
|
|
492
|
+
#: | () -> Enumerator[Elements::Row, void]
|
|
493
|
+
def each_row
|
|
494
|
+
return enum_for(:each_row) unless block_given?
|
|
495
|
+
|
|
496
|
+
Ooxml::WorksheetParser.each_row(@sheet_xml, shared_strings: @shared_strings) do |row|
|
|
497
|
+
if row.is_a?(Elements::Row)
|
|
498
|
+
yield row
|
|
499
|
+
else
|
|
500
|
+
yield Xlsxrb.send(:build_row_from_raw, row)
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
#: () { (Elements::Row) -> void } -> void
|
|
506
|
+
#: | () -> Enumerator[Elements::Row, void]
|
|
507
|
+
def each(&)
|
|
508
|
+
each_row(&)
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# Streaming read: yields StreamSheet objects one at a time for each sheet.
|
|
513
|
+
#
|
|
514
|
+
# @param source [String, IO] File path or IO object.
|
|
515
|
+
# @yield [sheet] Yields each sheet.
|
|
516
|
+
# @yieldparam sheet [StreamSheet] The streaming sheet object.
|
|
517
|
+
# @return [Enumerator] If no block is given.
|
|
518
|
+
# @return [void]
|
|
519
|
+
# @api public
|
|
520
|
+
#: (untyped source) ?{ (StreamSheet) -> void } -> untyped
|
|
521
|
+
def self.foreach(source)
|
|
522
|
+
return enum_for(:foreach, source) unless block_given?
|
|
196
523
|
|
|
197
524
|
attributes = source.is_a?(String) ? { "filepath" => source } : {}
|
|
198
|
-
|
|
525
|
+
Xlsxrb.in_span("Xlsxrb.foreach", attributes: attributes) do
|
|
199
526
|
entries = Ooxml::ZipReader.open(source, &:read_all)
|
|
200
527
|
shared_strings = Ooxml::SharedStringsParser.parse(entries["xl/sharedStrings.xml"])
|
|
201
528
|
workbook_sheets = Ooxml::WorkbookParser.parse(entries["xl/workbook.xml"])
|
|
202
529
|
rels = Ooxml::RelationshipsParser.parse(entries["xl/_rels/workbook.xml.rels"])
|
|
203
530
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
when String
|
|
208
|
-
workbook_sheets.find { |s| s[:name] == sheet }
|
|
209
|
-
end
|
|
210
|
-
next unless target_sheet
|
|
211
|
-
|
|
212
|
-
target = rels[target_sheet[:r_id]]
|
|
213
|
-
next unless target
|
|
531
|
+
workbook_sheets.each do |sheet_info|
|
|
532
|
+
target = rels[sheet_info[:r_id]]
|
|
533
|
+
next unless target
|
|
214
534
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
535
|
+
sheet_path = target.start_with?("/") ? target.delete_prefix("/") : "xl/#{target}"
|
|
536
|
+
sheet_xml = entries[sheet_path]
|
|
537
|
+
next if sheet_xml.nil? || sheet_xml.empty?
|
|
218
538
|
|
|
219
|
-
|
|
220
|
-
row = build_row_from_raw(raw_row)
|
|
221
|
-
block.call(row)
|
|
539
|
+
yield StreamSheet.new(sheet_info[:name], sheet_xml, shared_strings)
|
|
222
540
|
end
|
|
223
541
|
end
|
|
224
542
|
end
|
|
225
543
|
|
|
226
544
|
# Streaming write: yields a StreamWriter context for building XLSX on-the-fly.
|
|
227
|
-
#
|
|
228
|
-
|
|
545
|
+
#
|
|
546
|
+
# @param target [String, IO] File path or IO object.
|
|
547
|
+
# @yield [stream_writer]
|
|
548
|
+
# @yieldparam stream_writer [Xlsxrb::StreamWriter]
|
|
549
|
+
# @return [void]
|
|
550
|
+
# @api public
|
|
551
|
+
#: (untyped target, ?strict_excel_mode: bool) ?{ (Xlsxrb::StreamWriter) -> void } -> void
|
|
552
|
+
def self.generate(target, strict_excel_mode: true)
|
|
229
553
|
raise Error, "target is required" if target.nil?
|
|
230
|
-
raise Error, "block is required" unless
|
|
554
|
+
raise Error, "block is required" unless block_given?
|
|
231
555
|
|
|
232
556
|
attributes = target.is_a?(String) ? { "filepath" => target } : {}
|
|
233
|
-
|
|
234
|
-
stream_writer = StreamWriter.new(target)
|
|
235
|
-
|
|
236
|
-
|
|
557
|
+
Xlsxrb.in_span("Xlsxrb.generate", attributes: attributes) do
|
|
558
|
+
stream_writer = StreamWriter.new(target, strict_excel_mode: strict_excel_mode)
|
|
559
|
+
begin
|
|
560
|
+
yield stream_writer
|
|
561
|
+
stream_writer.close
|
|
562
|
+
ensure
|
|
563
|
+
stream_writer.cleanup!
|
|
564
|
+
end
|
|
237
565
|
end
|
|
238
566
|
end
|
|
239
567
|
|
|
240
568
|
# Builds an Elements::Workbook in memory using a DSL.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
569
|
+
#
|
|
570
|
+
# @yield [builder]
|
|
571
|
+
# @yieldparam builder [Xlsxrb::WorkbookBuilder]
|
|
572
|
+
# @return [Elements::Workbook]
|
|
573
|
+
# @api public
|
|
574
|
+
#: (?strict_excel_mode: bool) ?{ (WorkbookBuilder) -> void } -> untyped
|
|
575
|
+
def self.build(strict_excel_mode: true)
|
|
576
|
+
raise Error, "block is required" unless block_given?
|
|
577
|
+
|
|
578
|
+
Xlsxrb.in_span("Xlsxrb.build") do
|
|
579
|
+
builder = WorkbookBuilder.new(strict_excel_mode: strict_excel_mode)
|
|
580
|
+
yield builder
|
|
247
581
|
builder.build
|
|
248
582
|
end
|
|
249
583
|
end
|
|
250
584
|
|
|
251
585
|
# DSL context for Xlsxrb.build.
|
|
586
|
+
# @api public
|
|
252
587
|
class WorkbookBuilder
|
|
253
|
-
|
|
588
|
+
#: (?strict_excel_mode: bool) -> void
|
|
589
|
+
def initialize(strict_excel_mode: true)
|
|
590
|
+
@strict_excel_mode = strict_excel_mode
|
|
254
591
|
@sheets = []
|
|
255
592
|
@sheet_builders = [] # Keep track of sheet builders for style processing
|
|
256
593
|
@defined_names = []
|
|
@@ -258,66 +595,157 @@ module Xlsxrb
|
|
|
258
595
|
@app_properties = {}
|
|
259
596
|
@custom_properties = []
|
|
260
597
|
@workbook_protection = nil
|
|
598
|
+
@workbook_properties = { update_links: "never" }
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# Set a workbook property.
|
|
602
|
+
#
|
|
603
|
+
# @note **SECURITY WARNING:** If you set `:update_links` to anything other than `"never"`,
|
|
604
|
+
# you may expose end-users to malicious external reference vulnerabilities (e.g., CSV/DDE Injection)
|
|
605
|
+
# when they open the generated Excel file. Ensure you fully trust the exported data.
|
|
606
|
+
#
|
|
607
|
+
# @param name [Symbol] The property name (e.g. :update_links).
|
|
608
|
+
# @param value [String, Integer, Boolean] The property value.
|
|
609
|
+
# @return [void]
|
|
610
|
+
# @api public
|
|
611
|
+
#: (Symbol name, String | Integer | bool value) -> (String | Integer | bool)
|
|
612
|
+
def workbook_property(name, value)
|
|
613
|
+
@workbook_properties[name] = value
|
|
261
614
|
end
|
|
262
615
|
|
|
263
616
|
# Add a new sheet.
|
|
264
|
-
|
|
617
|
+
#
|
|
618
|
+
# @param name [String, nil] The name of the sheet.
|
|
619
|
+
# @param opts [Hash] Sheet properties.
|
|
620
|
+
# @yield [sheet_builder]
|
|
621
|
+
# @yieldparam sheet_builder [Xlsxrb::WorksheetBuilder]
|
|
622
|
+
# @return [void]
|
|
623
|
+
# @api public
|
|
624
|
+
#: (?String? name, **untyped opts) ?{ (WorksheetBuilder) -> void } -> untyped
|
|
625
|
+
def sheet(name = nil, **opts)
|
|
265
626
|
name ||= "Sheet#{@sheets.size + 1}"
|
|
266
|
-
|
|
267
|
-
|
|
627
|
+
raise ArgumentError, "Sheet name '#{name}' must be <= 31 characters (Excel limitation)" if @strict_excel_mode && name.length > 31
|
|
628
|
+
raise ArgumentError, "Sheet name '#{name}' contains invalid characters (ECMA-376 OOXML specification)" if name.match?(%r{[\[\]*?/\\]})
|
|
629
|
+
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)
|
|
630
|
+
|
|
631
|
+
sheet_builder = WorksheetBuilder.new(name, strict_excel_mode: @strict_excel_mode)
|
|
632
|
+
opts.each { |k, v| sheet_builder.sheet_properties(k, v) }
|
|
633
|
+
yield sheet_builder if block_given?
|
|
268
634
|
@sheet_builders << sheet_builder
|
|
269
635
|
@sheets << sheet_builder.build
|
|
270
636
|
end
|
|
637
|
+
alias [] sheet
|
|
271
638
|
|
|
272
639
|
# --- Workbook-Level Methods ---
|
|
273
640
|
|
|
274
641
|
# Add a defined name.
|
|
275
|
-
|
|
642
|
+
#
|
|
643
|
+
# @param name [String] The defined name.
|
|
644
|
+
# @param value [String] The formula or value.
|
|
645
|
+
# @param sheet [String, nil] Local sheet name.
|
|
646
|
+
# @param hidden [Boolean] Whether the defined name is hidden.
|
|
647
|
+
# @return [void]
|
|
648
|
+
# @api public
|
|
649
|
+
#: (String name, String value, ?sheet: String?, ?hidden: bool) -> void
|
|
650
|
+
def defined_name(name, value, sheet: nil, hidden: false)
|
|
276
651
|
entry = { name: name, value: value, hidden: hidden }
|
|
277
652
|
entry[:local_sheet_name] = sheet if sheet
|
|
278
653
|
@defined_names << entry
|
|
279
654
|
end
|
|
280
655
|
|
|
281
656
|
# Set the print area for a sheet.
|
|
282
|
-
|
|
657
|
+
#
|
|
658
|
+
# @param range [String] The range string (e.g. "A1:B10").
|
|
659
|
+
# @param sheet [String, nil] The sheet name.
|
|
660
|
+
# @return [void]
|
|
661
|
+
# @api public
|
|
662
|
+
#: (String range, ?sheet: String?) -> void
|
|
663
|
+
def print_area(range, sheet: nil)
|
|
283
664
|
sheet_name = sheet || @sheets.last&.name || "Sheet1"
|
|
284
665
|
value = "'#{sheet_name}'!#{absolute_range(range)}"
|
|
285
666
|
@defined_names.reject! { |dn| dn[:name] == "_xlnm.Print_Area" && dn[:local_sheet_name] == sheet_name }
|
|
286
|
-
|
|
667
|
+
defined_name("_xlnm.Print_Area", value, sheet: sheet_name)
|
|
287
668
|
end
|
|
288
669
|
|
|
289
670
|
# Set print titles for a sheet.
|
|
290
|
-
|
|
671
|
+
#
|
|
672
|
+
# @param rows [String, nil] Rows to repeat (e.g. "1:2").
|
|
673
|
+
# @param cols [String, nil] Columns to repeat (e.g. "A:B").
|
|
674
|
+
# @param sheet [String, nil] The sheet name.
|
|
675
|
+
# @return [void]
|
|
676
|
+
# @api public
|
|
677
|
+
#: (?rows: String?, ?cols: String?, ?sheet: String?) -> void
|
|
678
|
+
def print_titles(rows: nil, cols: nil, sheet: nil)
|
|
291
679
|
sheet_name = sheet || @sheets.last&.name || "Sheet1"
|
|
292
680
|
parts = []
|
|
293
681
|
parts << "'#{sheet_name}'!$#{cols.sub(":", ":$")}" if cols
|
|
294
682
|
parts << "'#{sheet_name}'!$#{rows.sub(":", ":$")}" if rows
|
|
295
683
|
value = parts.join(",")
|
|
296
684
|
@defined_names.reject! { |dn| dn[:name] == "_xlnm.Print_Titles" && dn[:local_sheet_name] == sheet_name }
|
|
297
|
-
|
|
685
|
+
defined_name("_xlnm.Print_Titles", value, sheet: sheet_name)
|
|
298
686
|
end
|
|
299
687
|
|
|
300
688
|
# Set workbook protection.
|
|
301
|
-
|
|
689
|
+
#
|
|
690
|
+
# @param opts [Hash] Protection options.
|
|
691
|
+
# @return [void]
|
|
692
|
+
# @api public
|
|
693
|
+
#: (**String | Integer | bool | nil opts) -> void
|
|
694
|
+
def protect_workbook(**opts)
|
|
302
695
|
@workbook_protection = opts
|
|
303
696
|
end
|
|
304
697
|
|
|
305
698
|
# Set a core document property.
|
|
306
|
-
|
|
699
|
+
#
|
|
700
|
+
# @param name [Symbol] The property name.
|
|
701
|
+
# @param value [String, Integer, Time] The property value.
|
|
702
|
+
# @return [void]
|
|
703
|
+
# @api public
|
|
704
|
+
#: (Symbol name, String | Integer | Time value) -> void
|
|
705
|
+
def core_property(name, value)
|
|
307
706
|
@core_properties[name] = value
|
|
308
707
|
end
|
|
309
708
|
|
|
310
709
|
# Set an app document property.
|
|
311
|
-
|
|
710
|
+
#
|
|
711
|
+
# @param name [Symbol] The property name.
|
|
712
|
+
# @param value [String, Integer, Time] The property value.
|
|
713
|
+
# @return [void]
|
|
714
|
+
# @api public
|
|
715
|
+
#: (Symbol name, String | Integer | Time value) -> void
|
|
716
|
+
def app_property(name, value)
|
|
312
717
|
@app_properties[name] = value
|
|
313
718
|
end
|
|
314
719
|
|
|
720
|
+
# Set multiple core and/or app properties.
|
|
721
|
+
#
|
|
722
|
+
# @param core [Hash, nil] Core properties.
|
|
723
|
+
# @param app [Hash, nil] App properties.
|
|
724
|
+
# @return [void]
|
|
725
|
+
# @api public
|
|
726
|
+
#: (?core: Hash[Symbol, String | Integer | Time]?, ?app: Hash[Symbol, String | Integer | Time]?) -> void
|
|
727
|
+
def properties(core: nil, app: nil)
|
|
728
|
+
core&.each { |k, v| core_property(k, v) }
|
|
729
|
+
app&.each { |k, v| app_property(k, v) }
|
|
730
|
+
end
|
|
731
|
+
|
|
315
732
|
# Add a custom document property.
|
|
316
|
-
|
|
733
|
+
#
|
|
734
|
+
# @param name [String] The property name.
|
|
735
|
+
# @param value [String, Integer, Float, Boolean, Time] The property value.
|
|
736
|
+
# @param type [Symbol] The type of property (:string, :number, :bool, :date).
|
|
737
|
+
# @return [void]
|
|
738
|
+
# @api public
|
|
739
|
+
#: (String name, String | Integer | Float | bool | Time value, ?type: ::Symbol) -> void
|
|
740
|
+
def custom_property(name, value, type: :string)
|
|
317
741
|
@custom_properties << { name: name, value: value, type: type }
|
|
318
742
|
end
|
|
319
743
|
|
|
744
|
+
# @api public
|
|
745
|
+
#: () -> untyped
|
|
320
746
|
def build
|
|
747
|
+
raise ArgumentError, "Workbook must contain at least one sheet (Excel limitation)" if @strict_excel_mode && @sheets.empty?
|
|
748
|
+
|
|
321
749
|
# Process styles from all sheets and collect style definitions
|
|
322
750
|
processed_sheets, styles_definition = process_styles(@sheets)
|
|
323
751
|
|
|
@@ -328,6 +756,7 @@ module Xlsxrb
|
|
|
328
756
|
wb_meta[:app_properties] = @app_properties unless @app_properties.empty?
|
|
329
757
|
wb_meta[:custom_properties] = @custom_properties unless @custom_properties.empty?
|
|
330
758
|
wb_meta[:workbook_protection] = @workbook_protection if @workbook_protection
|
|
759
|
+
wb_meta[:workbook_properties] = @workbook_properties unless @workbook_properties.empty?
|
|
331
760
|
|
|
332
761
|
Elements::Workbook.new(
|
|
333
762
|
sheets: processed_sheets,
|
|
@@ -338,10 +767,12 @@ module Xlsxrb
|
|
|
338
767
|
|
|
339
768
|
private
|
|
340
769
|
|
|
770
|
+
#: (untyped range) -> untyped
|
|
341
771
|
def absolute_range(range)
|
|
342
772
|
range.gsub(/([A-Z]+)(\d+)/, '$\1$\2')
|
|
343
773
|
end
|
|
344
774
|
|
|
775
|
+
#: (untyped names, untyped sheets) -> untyped
|
|
345
776
|
def resolve_defined_names(names, sheets)
|
|
346
777
|
sheet_names = sheets.map(&:name)
|
|
347
778
|
names.map do |dn|
|
|
@@ -355,6 +786,7 @@ module Xlsxrb
|
|
|
355
786
|
end
|
|
356
787
|
end
|
|
357
788
|
|
|
789
|
+
#: (untyped sheets) -> (::Array[untyped | ::Hash[untyped, untyped]] | ::Array[untyped])
|
|
358
790
|
def process_styles(sheets)
|
|
359
791
|
# Collect all unique StyleBuilders from all sheets
|
|
360
792
|
all_style_builders = {}
|
|
@@ -420,23 +852,27 @@ module Xlsxrb
|
|
|
420
852
|
[updated_sheets, styles_definition]
|
|
421
853
|
end
|
|
422
854
|
|
|
855
|
+
#: (untyped writer) -> { fonts: untyped, fills: untyped, borders: untyped, xf_entries: untyped, num_fmts: untyped }
|
|
423
856
|
def extract_styles_from_writer(writer)
|
|
424
857
|
# Extract style definitions from the writer that can be reused
|
|
425
858
|
# This captures the fonts, fills, borders, and xf entries that were created
|
|
426
859
|
{
|
|
427
|
-
fonts: writer.
|
|
428
|
-
fills: writer.
|
|
429
|
-
borders: writer.
|
|
430
|
-
xf_entries: writer.
|
|
431
|
-
num_fmts: writer.
|
|
860
|
+
fonts: writer.fonts.dup,
|
|
861
|
+
fills: writer.fills.dup,
|
|
862
|
+
borders: writer.borders.dup,
|
|
863
|
+
xf_entries: writer.xf_entries.dup,
|
|
864
|
+
num_fmts: writer.num_fmts.dup
|
|
432
865
|
}
|
|
433
866
|
end
|
|
434
867
|
end
|
|
435
868
|
|
|
436
869
|
# DSL context for a single worksheet in Xlsxrb.build.
|
|
870
|
+
# @api public
|
|
437
871
|
class WorksheetBuilder
|
|
438
|
-
|
|
872
|
+
#: (untyped name, ?strict_excel_mode: bool) -> void
|
|
873
|
+
def initialize(name, strict_excel_mode: true)
|
|
439
874
|
@name = name
|
|
875
|
+
@strict_excel_mode = strict_excel_mode
|
|
440
876
|
@rows = []
|
|
441
877
|
@columns = []
|
|
442
878
|
@charts = []
|
|
@@ -469,27 +905,127 @@ module Xlsxrb
|
|
|
469
905
|
end
|
|
470
906
|
|
|
471
907
|
# Define a named style that can be applied to cells.
|
|
472
|
-
|
|
908
|
+
#
|
|
909
|
+
# @param name [String] The name of the style.
|
|
910
|
+
# @param opts [Hash] Style options (e.g. bold: true).
|
|
911
|
+
# @yield [style_builder]
|
|
912
|
+
# @yieldparam style_builder [Xlsxrb::StyleBuilder]
|
|
913
|
+
# @return [StyleBuilder]
|
|
914
|
+
# @api public
|
|
915
|
+
#: (String name, **untyped opts) ?{ (StyleBuilder) -> void } -> StyleBuilder
|
|
916
|
+
def style(name, **opts)
|
|
473
917
|
style_builder = StyleBuilder.new(name)
|
|
474
918
|
style_builder.apply_options!(**opts) unless opts.empty?
|
|
475
|
-
|
|
919
|
+
yield style_builder if block_given?
|
|
476
920
|
@styles[name] = style_builder
|
|
477
921
|
style_builder
|
|
478
922
|
end
|
|
479
923
|
|
|
480
|
-
# Add a row
|
|
481
|
-
#
|
|
482
|
-
#
|
|
483
|
-
|
|
924
|
+
# Add a row to the sheet.
|
|
925
|
+
#
|
|
926
|
+
# @param values [Array, Hash] The cell values.
|
|
927
|
+
# @param styles [String, Array<String>, nil] Styles to apply to cells.
|
|
928
|
+
# @param height [Float, nil] The row height.
|
|
929
|
+
# @param hidden [Boolean] Whether the row is hidden.
|
|
930
|
+
# @param custom_height [Boolean] Whether it's a custom height.
|
|
931
|
+
# @param outline_level [Integer, nil] The outline level.
|
|
932
|
+
# @note Excel's column limit is 16,384, row limit is 1,048,576, string max length is 32,767.
|
|
933
|
+
# @return [void]
|
|
934
|
+
# @api public
|
|
935
|
+
#: (Array[untyped] | Hash[untyped, untyped] values, ?styles: untyped, ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?outline_level: Integer | nil) -> void
|
|
936
|
+
def row(values, styles: nil, height: nil, hidden: false, custom_height: false, outline_level: nil)
|
|
484
937
|
row_index = @rows.size
|
|
485
|
-
|
|
486
|
-
|
|
938
|
+
# See: https://support.microsoft.com/en-us/office/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3
|
|
939
|
+
if @strict_excel_mode
|
|
940
|
+
raise ArgumentError, "Row index #{row_index} exceeds Excel limit of 1,048,576 rows" if row_index >= 1_048_576
|
|
941
|
+
raise ArgumentError, "Row height #{height} must be between 0 and 409 points (Excel limitation)" if height && (height.negative? || height > 409)
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
if values.is_a?(Hash)
|
|
945
|
+
max_col = values.keys.map { |k| Elements::Cell.column_index(k) }.max || -1
|
|
946
|
+
cells_array = Array.new(max_col + 1)
|
|
947
|
+
values.each do |k, v|
|
|
948
|
+
idx = Elements::Cell.column_index(k)
|
|
949
|
+
cells_array[idx] = v
|
|
950
|
+
end
|
|
951
|
+
values = cells_array
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
if styles.is_a?(Hash)
|
|
955
|
+
expanded_styles = {}
|
|
956
|
+
styles.each do |k, v|
|
|
957
|
+
if k.is_a?(Range) || k.is_a?(Array)
|
|
958
|
+
k.each { |idx| expanded_styles[Elements::Cell.column_index(idx)] = v }
|
|
959
|
+
else
|
|
960
|
+
expanded_styles[Elements::Cell.column_index(k)] = v
|
|
961
|
+
end
|
|
962
|
+
end
|
|
963
|
+
max_col_style = expanded_styles.keys.max || -1
|
|
964
|
+
styles_array = Array.new(max_col_style + 1)
|
|
965
|
+
expanded_styles.each do |idx, v|
|
|
966
|
+
styles_array[idx] = v
|
|
967
|
+
end
|
|
968
|
+
styles = styles_array
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
# Auto-detect Date / Time for built-in styles
|
|
972
|
+
values.each_with_index do |val, idx|
|
|
973
|
+
cell_style = styles.is_a?(Array) ? styles[idx] : styles
|
|
974
|
+
|
|
975
|
+
if val.is_a?(Date) && cell_style.nil?
|
|
976
|
+
style("__xlsxrb_date", number_format: "yyyy-mm-dd") unless @styles.key?("__xlsxrb_date")
|
|
977
|
+
styles = [] if styles.nil?
|
|
978
|
+
styles = Array.new(values.size, styles) unless styles.is_a?(Array)
|
|
979
|
+
styles[idx] = "__xlsxrb_date"
|
|
980
|
+
elsif val.is_a?(Time) && cell_style.nil?
|
|
981
|
+
style("__xlsxrb_time", number_format: "yyyy-mm-dd hh:mm:ss") unless @styles.key?("__xlsxrb_time")
|
|
982
|
+
styles = [] if styles.nil?
|
|
983
|
+
styles = Array.new(values.size, styles) unless styles.is_a?(Array)
|
|
984
|
+
styles[idx] = "__xlsxrb_time"
|
|
985
|
+
end
|
|
986
|
+
end
|
|
987
|
+
|
|
988
|
+
max_len = values.size
|
|
989
|
+
max_len = [max_len, styles.size].max if styles.is_a?(Array)
|
|
990
|
+
# See: https://support.microsoft.com/en-us/office/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3
|
|
991
|
+
raise ArgumentError, "Row contains #{max_len} columns, exceeding Excel limit of 16_384 columns" if @strict_excel_mode && max_len > 16_384
|
|
992
|
+
|
|
993
|
+
cells = Array.new(max_len)
|
|
994
|
+
style_lookup = styles.is_a?(Array)
|
|
487
995
|
|
|
488
996
|
col_index = 0
|
|
489
|
-
while col_index <
|
|
490
|
-
val = values[col_index]
|
|
491
|
-
|
|
492
|
-
#
|
|
997
|
+
while col_index < max_len
|
|
998
|
+
val = col_index < values.size ? values[col_index] : nil
|
|
999
|
+
raise ArgumentError, "Invalid cell value type or value: #{val.class} for value #{val.inspect}" unless val.nil? || val.is_a?(String) || (val.is_a?(Numeric) && !(val.is_a?(Float) && (val.infinite? || val.nan?))) || val.is_a?(TrueClass) || val.is_a?(FalseClass) || val.is_a?(Date) || val.is_a?(Time) || val.is_a?(Elements::Formula) || (val.is_a?(Hash) && val.key?(:formula)) || val.is_a?(Elements::RichText) || (val.is_a?(Array) && val.first.is_a?(Hash) && (val.first.key?(:text) || val.first.key?("text")))
|
|
1000
|
+
# See: https://support.microsoft.com/en-us/office/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3
|
|
1001
|
+
raise ArgumentError, "Cell text length #{val.length} exceeds Excel limit of 32,767 characters" if @strict_excel_mode && val.is_a?(String) && val.length > 32_767
|
|
1002
|
+
|
|
1003
|
+
if val.is_a?(Array) && val.first.is_a?(Hash) && (val.first.key?(:text) || val.first.key?("text"))
|
|
1004
|
+
# Coerce array of hashes to RichText
|
|
1005
|
+
runs = val.map do |run|
|
|
1006
|
+
text = run[:text] || run["text"]
|
|
1007
|
+
font = run.reject { |k| k.to_s == "text" }
|
|
1008
|
+
{ text: text, font: font.empty? ? nil : font }.compact
|
|
1009
|
+
end
|
|
1010
|
+
val = Elements::RichText.new(runs: runs)
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
style_name = if style_lookup
|
|
1014
|
+
col_index < styles.size ? styles[col_index] : nil
|
|
1015
|
+
else
|
|
1016
|
+
styles
|
|
1017
|
+
end
|
|
1018
|
+
|
|
1019
|
+
if style_name.is_a?(Hash)
|
|
1020
|
+
inline_name = "__inline_#{style_name.hash}"
|
|
1021
|
+
style(inline_name, **style_name) unless @styles.key?(inline_name)
|
|
1022
|
+
style_name = inline_name
|
|
1023
|
+
end
|
|
1024
|
+
if val.nil? && style_name.nil?
|
|
1025
|
+
col_index += 1
|
|
1026
|
+
next
|
|
1027
|
+
end
|
|
1028
|
+
# If value is a Formula object or Hash with :formula, store it as the cell's formula
|
|
493
1029
|
cells[col_index] = if val.is_a?(Elements::Formula)
|
|
494
1030
|
Elements::Cell.new(
|
|
495
1031
|
row_index: row_index,
|
|
@@ -498,6 +1034,15 @@ module Xlsxrb
|
|
|
498
1034
|
formula: val,
|
|
499
1035
|
style_index: style_name
|
|
500
1036
|
)
|
|
1037
|
+
elsif val.is_a?(Hash) && val.key?(:formula)
|
|
1038
|
+
f_obj = Elements::Formula.new(val[:formula])
|
|
1039
|
+
Elements::Cell.new(
|
|
1040
|
+
row_index: row_index,
|
|
1041
|
+
column_index: col_index,
|
|
1042
|
+
value: val[:value],
|
|
1043
|
+
formula: f_obj,
|
|
1044
|
+
style_index: style_name
|
|
1045
|
+
)
|
|
501
1046
|
else
|
|
502
1047
|
Elements::Cell.new(
|
|
503
1048
|
row_index: row_index,
|
|
@@ -509,6 +1054,7 @@ module Xlsxrb
|
|
|
509
1054
|
col_index += 1
|
|
510
1055
|
end
|
|
511
1056
|
|
|
1057
|
+
cells.compact!
|
|
512
1058
|
@rows << Elements::Row.new(
|
|
513
1059
|
index: row_index,
|
|
514
1060
|
cells: cells,
|
|
@@ -519,22 +1065,50 @@ module Xlsxrb
|
|
|
519
1065
|
)
|
|
520
1066
|
end
|
|
521
1067
|
|
|
522
|
-
#
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
1068
|
+
# Add a column or multiple columns to the sheet.
|
|
1069
|
+
#
|
|
1070
|
+
# @param index [Integer, String, Range, Array] The column index (0-based), letter, or a collection of them.
|
|
1071
|
+
# @param width [Float, nil] The column width.
|
|
1072
|
+
# @param hidden [Boolean] Whether the column is hidden.
|
|
1073
|
+
# @param custom_width [Boolean] Whether it's a custom width.
|
|
1074
|
+
# @param outline_level [Integer, nil] The outline level.
|
|
1075
|
+
# @note Excel's column width max is 255.
|
|
1076
|
+
# @return [void]
|
|
1077
|
+
# @api public
|
|
1078
|
+
#: (Integer | String | Range[Integer | String] | Array[Integer | String] index, ?width: Float | Integer | nil, ?hidden: bool, ?custom_width: bool, ?outline_level: Integer | nil) -> void
|
|
1079
|
+
def column(index, width: nil, hidden: false, custom_width: false, outline_level: nil)
|
|
1080
|
+
raise ArgumentError, "Column width #{width} must be between 0 and 255 characters (Excel limitation)" if @strict_excel_mode && width && (width.negative? || width > 255)
|
|
1081
|
+
|
|
1082
|
+
indices = case index
|
|
1083
|
+
when Range, Array
|
|
1084
|
+
index.map { |i| Elements::Cell.column_index(i) }
|
|
1085
|
+
else
|
|
1086
|
+
[Elements::Cell.column_index(index)]
|
|
1087
|
+
end
|
|
1088
|
+
|
|
1089
|
+
indices.each do |idx|
|
|
1090
|
+
@columns << Elements::Column.new(
|
|
1091
|
+
index: idx,
|
|
1092
|
+
width: width,
|
|
1093
|
+
hidden: hidden,
|
|
1094
|
+
custom_width: custom_width || !width.nil?,
|
|
1095
|
+
outline_level: outline_level
|
|
1096
|
+
)
|
|
1097
|
+
end
|
|
531
1098
|
end
|
|
532
1099
|
|
|
533
1100
|
# Add a chart to the sheet.
|
|
534
|
-
|
|
1101
|
+
#
|
|
1102
|
+
# @param options [Hash] Chart options.
|
|
1103
|
+
# @yield [builder]
|
|
1104
|
+
# @yieldparam builder [Xlsxrb::ChartBuilder]
|
|
1105
|
+
# @return [void]
|
|
1106
|
+
# @api public
|
|
1107
|
+
#: (**untyped options) ?{ (ChartBuilder) -> void } -> void
|
|
1108
|
+
def chart(**options)
|
|
535
1109
|
if block_given?
|
|
536
1110
|
builder = ChartBuilder.new
|
|
537
|
-
|
|
1111
|
+
yield builder
|
|
538
1112
|
options = builder.options.merge(options)
|
|
539
1113
|
end
|
|
540
1114
|
@charts << options
|
|
@@ -543,7 +1117,16 @@ module Xlsxrb
|
|
|
543
1117
|
# --- Hyperlinks ---
|
|
544
1118
|
|
|
545
1119
|
# Add a hyperlink on a cell.
|
|
546
|
-
|
|
1120
|
+
#
|
|
1121
|
+
# @param cell [String] The cell reference (e.g. "A1").
|
|
1122
|
+
# @param url [String, nil] The URL.
|
|
1123
|
+
# @param display [String, nil] The display text.
|
|
1124
|
+
# @param tooltip [String, nil] The tooltip.
|
|
1125
|
+
# @param location [String, nil] The internal location reference.
|
|
1126
|
+
# @return [void]
|
|
1127
|
+
# @api public
|
|
1128
|
+
#: (String | Integer cell, ?String? url, ?display: String?, ?tooltip: String?, ?location: String?) -> void
|
|
1129
|
+
def hyperlink(cell, url = nil, display: nil, tooltip: nil, location: nil)
|
|
547
1130
|
link = { cell: cell }
|
|
548
1131
|
link[:url] = url if url
|
|
549
1132
|
link[:display] = display if display
|
|
@@ -555,40 +1138,78 @@ module Xlsxrb
|
|
|
555
1138
|
# --- Auto Filter / Sort ---
|
|
556
1139
|
|
|
557
1140
|
# Set an auto filter range (e.g. "A1:D10").
|
|
558
|
-
#
|
|
559
|
-
|
|
1141
|
+
#
|
|
1142
|
+
# @param range [String] The filter range.
|
|
1143
|
+
# @return [void]
|
|
1144
|
+
# @api public
|
|
1145
|
+
#: (untyped range) -> untyped
|
|
1146
|
+
def auto_filter(range)
|
|
560
1147
|
@auto_filter = range
|
|
561
1148
|
end
|
|
562
|
-
# rubocop:enable Naming/AccessorMethodName
|
|
563
1149
|
|
|
564
1150
|
# Add a filter column to the auto filter.
|
|
565
|
-
|
|
1151
|
+
#
|
|
1152
|
+
# @param col_id [Integer] 0-based column index within the filter range.
|
|
1153
|
+
# @param filter [Hash] The filter options.
|
|
1154
|
+
# @return [void]
|
|
1155
|
+
# @api public
|
|
1156
|
+
#: (untyped col_id, untyped filter) -> untyped
|
|
1157
|
+
def filter_column(col_id, filter)
|
|
566
1158
|
@filter_columns[col_id] = filter
|
|
567
1159
|
end
|
|
568
1160
|
|
|
569
1161
|
# Set sort state.
|
|
570
|
-
|
|
1162
|
+
#
|
|
1163
|
+
# @param ref [String] The sort range.
|
|
1164
|
+
# @param sort_conditions [Array<Hash>] Sort conditions.
|
|
1165
|
+
# @param opts [Hash] Additional options.
|
|
1166
|
+
# @return [void]
|
|
1167
|
+
# @api public
|
|
1168
|
+
#: (untyped ref, untyped sort_conditions, **untyped opts) -> untyped
|
|
1169
|
+
def sort_state(ref, sort_conditions, **opts)
|
|
571
1170
|
@sort_state = { ref: ref, sort_conditions: sort_conditions }.merge(opts)
|
|
572
1171
|
end
|
|
573
1172
|
|
|
574
1173
|
# --- Data Validation ---
|
|
575
1174
|
|
|
576
1175
|
# Add a data validation rule.
|
|
577
|
-
|
|
1176
|
+
#
|
|
1177
|
+
# @param sqref [String] The cell range (e.g. "A1:A100").
|
|
1178
|
+
# @param opts [Hash] Data validation options.
|
|
1179
|
+
# @return [void]
|
|
1180
|
+
# @api public
|
|
1181
|
+
#: (untyped sqref, **untyped opts) -> untyped
|
|
1182
|
+
def validate_data(sqref, **opts)
|
|
578
1183
|
@data_validations << opts.merge(sqref: sqref)
|
|
579
1184
|
end
|
|
580
1185
|
|
|
581
1186
|
# --- Conditional Formatting ---
|
|
582
1187
|
|
|
583
1188
|
# Add a conditional formatting rule.
|
|
584
|
-
|
|
1189
|
+
#
|
|
1190
|
+
# @param sqref [String] The cell range.
|
|
1191
|
+
# @param opts [Hash] Conditional format options.
|
|
1192
|
+
# @return [void]
|
|
1193
|
+
# @api public
|
|
1194
|
+
#: (untyped sqref, **untyped opts) -> untyped
|
|
1195
|
+
def conditional_format(sqref, **opts)
|
|
585
1196
|
@conditional_formats << opts.merge(sqref: sqref)
|
|
586
1197
|
end
|
|
587
1198
|
|
|
588
1199
|
# --- Tables ---
|
|
589
1200
|
|
|
590
1201
|
# Add a table to the sheet.
|
|
591
|
-
|
|
1202
|
+
#
|
|
1203
|
+
# @param ref [String] The table range.
|
|
1204
|
+
# @param columns [Array<String>] The column names.
|
|
1205
|
+
# @param name [String, nil] The table name.
|
|
1206
|
+
# @param display_name [String, nil] The display name.
|
|
1207
|
+
# @param style [String, nil] The table style.
|
|
1208
|
+
# @param opts [Hash] Additional options.
|
|
1209
|
+
# @return [void]
|
|
1210
|
+
# @api public
|
|
1211
|
+
#: (untyped ref, columns: untyped, ?name: untyped?, ?display_name: untyped?, ?style: untyped?, **untyped opts) -> untyped
|
|
1212
|
+
def table(ref, columns:, name: nil, display_name: nil, style: nil, **opts)
|
|
592
1213
|
tbl = { ref: ref, columns: columns }
|
|
593
1214
|
tbl[:name] = name if name
|
|
594
1215
|
tbl[:display_name] = display_name if display_name
|
|
@@ -600,12 +1221,19 @@ module Xlsxrb
|
|
|
600
1221
|
# --- Pivot Tables ---
|
|
601
1222
|
|
|
602
1223
|
# Add a pivot table to the sheet.
|
|
603
|
-
#
|
|
604
|
-
#
|
|
605
|
-
#
|
|
606
|
-
#
|
|
607
|
-
#
|
|
608
|
-
|
|
1224
|
+
#
|
|
1225
|
+
# @param source_ref [String] data source range (e.g. "Sheet1!A1:C10").
|
|
1226
|
+
# @param row_fields [Array<Integer>] array of 0-based field indices for row axis.
|
|
1227
|
+
# @param data_fields [Array<Hash>] array of { fld:, name:, subtotal: } hashes.
|
|
1228
|
+
# @param col_fields [Array<Integer>] array of 0-based field indices for column axis.
|
|
1229
|
+
# @param dest_ref [String] top-left cell for the pivot table (default "E1").
|
|
1230
|
+
# @param name [String, nil] Pivot table name.
|
|
1231
|
+
# @param field_names [Array<String>, nil] Override field names.
|
|
1232
|
+
# @param items [Array, nil] Items configuration.
|
|
1233
|
+
# @return [void]
|
|
1234
|
+
# @api public
|
|
1235
|
+
#: (untyped source_ref, **untyped opts) -> void
|
|
1236
|
+
def pivot_table(source_ref, row_fields:, data_fields:, col_fields: [], dest_ref: "E1", name: nil, field_names: nil, items: nil)
|
|
609
1237
|
@pivot_tables ||= []
|
|
610
1238
|
@pivot_tables << {
|
|
611
1239
|
source_ref: source_ref, row_fields: row_fields,
|
|
@@ -618,16 +1246,28 @@ module Xlsxrb
|
|
|
618
1246
|
# --- Comments ---
|
|
619
1247
|
|
|
620
1248
|
# Add a comment on a cell.
|
|
621
|
-
|
|
1249
|
+
#
|
|
1250
|
+
# @param cell [String] The cell reference.
|
|
1251
|
+
# @param text [String] The comment text.
|
|
1252
|
+
# @param author [String] The author name.
|
|
1253
|
+
# @return [void]
|
|
1254
|
+
# @api public
|
|
1255
|
+
#: (String | Integer cell, String text, ?author: ::String) -> void
|
|
1256
|
+
def comment(cell, text, author: "Author")
|
|
622
1257
|
@comments << { cell: cell, text: text, author: author }
|
|
623
1258
|
end
|
|
624
1259
|
|
|
625
1260
|
# --- Sparklines ---
|
|
626
1261
|
|
|
627
1262
|
# Add a sparkline group to the sheet.
|
|
628
|
-
#
|
|
629
|
-
#
|
|
630
|
-
|
|
1263
|
+
#
|
|
1264
|
+
# @param sparklines [Array<Hash>] Array of { data_ref:, location_ref: } hashes.
|
|
1265
|
+
# @param type [String, nil] "line" (default), "column", or "stacked".
|
|
1266
|
+
# @param opts [Hash] Additional options.
|
|
1267
|
+
# @return [void]
|
|
1268
|
+
# @api public
|
|
1269
|
+
#: (**untyped opts) -> void
|
|
1270
|
+
def sparkline_group(sparklines:, type: nil, **opts)
|
|
631
1271
|
group = { sparklines: sparklines }
|
|
632
1272
|
group[:type] = type if type
|
|
633
1273
|
group.merge!(opts)
|
|
@@ -636,25 +1276,78 @@ module Xlsxrb
|
|
|
636
1276
|
|
|
637
1277
|
# --- Merge Cells ---
|
|
638
1278
|
|
|
639
|
-
# Merge a range of cells (e.g. "A1:B2").
|
|
640
|
-
|
|
641
|
-
|
|
1279
|
+
# Merge a range of cells (e.g. "A1:B2"), or by coordinate indices.
|
|
1280
|
+
#
|
|
1281
|
+
# @param range [String, nil] The string range.
|
|
1282
|
+
# @param row [Integer, nil] Single row index.
|
|
1283
|
+
# @param col_start [Integer, nil] Starting column index.
|
|
1284
|
+
# @param col_end [Integer, nil] Ending column index.
|
|
1285
|
+
# @param row_start [Integer, nil] Starting row index.
|
|
1286
|
+
# @param row_end [Integer, nil] Ending row index.
|
|
1287
|
+
# @return [void]
|
|
1288
|
+
# @api public
|
|
1289
|
+
#: (?(String | Hash[Symbol, Integer | String])? range, ?row: Integer?, ?col_start: (Integer | String)?, ?col_end: (Integer | String)?, ?row_start: Integer?, ?row_end: Integer?) -> void
|
|
1290
|
+
def merge(range = nil, row: nil, col_start: nil, col_end: nil, row_start: nil, row_end: nil)
|
|
1291
|
+
if range.is_a?(Hash)
|
|
1292
|
+
row = range[:row]
|
|
1293
|
+
row_start = range[:row_start]
|
|
1294
|
+
row_end = range[:row_end]
|
|
1295
|
+
col_start = range[:col_start]
|
|
1296
|
+
col_end = range[:col_end]
|
|
1297
|
+
range = nil
|
|
1298
|
+
end
|
|
1299
|
+
|
|
1300
|
+
if range
|
|
1301
|
+
raise ArgumentError, "Invalid merge range format: '#{range}'. Expected format like 'A1:B2'." if @strict_excel_mode && !range.match?(/^[A-Za-z]{1,3}\d+(:[A-Za-z]{1,3}\d+)?$/)
|
|
1302
|
+
return if @merge_cells_ranges.include?(range)
|
|
1303
|
+
|
|
1304
|
+
@merge_cells_ranges << range
|
|
1305
|
+
else
|
|
1306
|
+
r_start = row || row_start || 0
|
|
1307
|
+
r_end = row || row_end || 0
|
|
1308
|
+
c_start = Elements::Cell.column_index(col_start || 0)
|
|
1309
|
+
c_end = Elements::Cell.column_index(col_end || 0)
|
|
1310
|
+
start_ref = "#{Xlsxrb::Elements::Cell.column_letter(c_start)}#{r_start + 1}"
|
|
1311
|
+
end_ref = "#{Xlsxrb::Elements::Cell.column_letter(c_end)}#{r_end + 1}"
|
|
1312
|
+
@merge_cells_ranges << "#{start_ref}:#{end_ref}"
|
|
1313
|
+
end
|
|
642
1314
|
end
|
|
643
1315
|
|
|
644
1316
|
# --- Freeze / Split Panes ---
|
|
645
1317
|
|
|
646
1318
|
# Freeze panes at the given row and column.
|
|
647
|
-
|
|
1319
|
+
#
|
|
1320
|
+
# @param row [Integer] The row index to freeze at (0-based).
|
|
1321
|
+
# @param col [Integer, String] The column index to freeze at (0-based or letter).
|
|
1322
|
+
# @return [void]
|
|
1323
|
+
# @api public
|
|
1324
|
+
#: (?row: Integer, ?col: (Integer | String)) -> void
|
|
1325
|
+
def freeze_pane(row: 0, col: 0)
|
|
1326
|
+
col = Elements::Cell.column_index(col)
|
|
648
1327
|
@freeze_pane = { row: row, col: col }
|
|
649
1328
|
end
|
|
650
1329
|
|
|
651
1330
|
# Split panes (non-frozen).
|
|
652
|
-
|
|
1331
|
+
#
|
|
1332
|
+
# @param x_split [Integer] X coordinate.
|
|
1333
|
+
# @param y_split [Integer] Y coordinate.
|
|
1334
|
+
# @param top_left_cell [String, nil] Top left cell reference.
|
|
1335
|
+
# @return [void]
|
|
1336
|
+
# @api public
|
|
1337
|
+
#: (?x_split: ::Integer, ?y_split: ::Integer, ?top_left_cell: String?) -> void
|
|
1338
|
+
def split_pane(x_split: 0, y_split: 0, top_left_cell: nil)
|
|
653
1339
|
@split_pane = { x_split: x_split, y_split: y_split, top_left_cell: top_left_cell }
|
|
654
1340
|
end
|
|
655
1341
|
|
|
656
1342
|
# Set active cell selection.
|
|
657
|
-
|
|
1343
|
+
#
|
|
1344
|
+
# @param active_cell [String] The active cell reference.
|
|
1345
|
+
# @param sqref [String, nil] The selected range.
|
|
1346
|
+
# @param pane [String, nil] The pane to select in.
|
|
1347
|
+
# @return [void]
|
|
1348
|
+
# @api public
|
|
1349
|
+
#: (String active_cell, ?sqref: String?, ?pane: (String | Symbol)?) -> void
|
|
1350
|
+
def select_cell(active_cell, sqref: nil, pane: nil)
|
|
658
1351
|
@selection = { active_cell: active_cell, sqref: sqref || active_cell }
|
|
659
1352
|
@selection[:pane] = pane if pane
|
|
660
1353
|
end
|
|
@@ -662,29 +1355,60 @@ module Xlsxrb
|
|
|
662
1355
|
# --- Page Setup / Margins / Print ---
|
|
663
1356
|
|
|
664
1357
|
# Set page margins (in inches).
|
|
665
|
-
|
|
1358
|
+
#
|
|
1359
|
+
# @param left [Float, nil] Left margin.
|
|
1360
|
+
# @param right [Float, nil] Right margin.
|
|
1361
|
+
# @param top [Float, nil] Top margin.
|
|
1362
|
+
# @param bottom [Float, nil] Bottom margin.
|
|
1363
|
+
# @param header [Float, nil] Header margin.
|
|
1364
|
+
# @param footer [Float, nil] Footer margin.
|
|
1365
|
+
# @return [void]
|
|
1366
|
+
# @api public
|
|
1367
|
+
#: (?left: Float?, ?right: Float?, ?top: Float?, ?bottom: Float?, ?header: Float?, ?footer: Float?) -> void
|
|
1368
|
+
def page_margins(left: nil, right: nil, top: nil, bottom: nil, header: nil, footer: nil)
|
|
666
1369
|
@page_margins = { left: left, right: right, top: top, bottom: bottom, header: header, footer: footer }.compact
|
|
667
1370
|
end
|
|
668
1371
|
|
|
669
1372
|
# Set page setup properties.
|
|
670
|
-
|
|
1373
|
+
#
|
|
1374
|
+
# @param opts [Hash] Page setup options.
|
|
1375
|
+
# @return [void]
|
|
1376
|
+
# @api public
|
|
1377
|
+
#: (**untyped opts) -> void
|
|
1378
|
+
def page_setup(**opts)
|
|
671
1379
|
@page_setup.merge!(opts)
|
|
672
1380
|
end
|
|
673
1381
|
|
|
674
1382
|
# Set header/footer text.
|
|
675
|
-
|
|
1383
|
+
#
|
|
1384
|
+
# @param opts [Hash] Header and footer options.
|
|
1385
|
+
# @return [void]
|
|
1386
|
+
# @api public
|
|
1387
|
+
#: (**untyped opts) -> void
|
|
1388
|
+
def header_footer(**opts)
|
|
676
1389
|
@header_footer.merge!(opts)
|
|
677
1390
|
end
|
|
678
1391
|
|
|
679
1392
|
# Set a print option.
|
|
680
|
-
|
|
1393
|
+
#
|
|
1394
|
+
# @param name [Symbol] Option name.
|
|
1395
|
+
# @param value [Object] Option value.
|
|
1396
|
+
# @return [void]
|
|
1397
|
+
# @api public
|
|
1398
|
+
#: (Symbol name, untyped value) -> void
|
|
1399
|
+
def print_options(name, value)
|
|
681
1400
|
@print_options[name] = value
|
|
682
1401
|
end
|
|
683
1402
|
|
|
684
1403
|
# --- Sheet Protection ---
|
|
685
1404
|
|
|
686
1405
|
# Set sheet protection options.
|
|
687
|
-
|
|
1406
|
+
#
|
|
1407
|
+
# @param opts [Hash] Sheet protection options.
|
|
1408
|
+
# @return [void]
|
|
1409
|
+
# @api public
|
|
1410
|
+
#: (**untyped opts) -> void
|
|
1411
|
+
def protect_sheet(**opts)
|
|
688
1412
|
normalized = opts.dup
|
|
689
1413
|
plain_password = normalized[:password]
|
|
690
1414
|
needs_hash = plain_password.is_a?(String) && !plain_password.empty? &&
|
|
@@ -701,7 +1425,9 @@ module Xlsxrb
|
|
|
701
1425
|
# --- Images ---
|
|
702
1426
|
|
|
703
1427
|
# Insert an image from raw file data.
|
|
704
|
-
|
|
1428
|
+
# @api public
|
|
1429
|
+
#: (String file_data, ?ext: ::String, ?from_col: ::Integer, ?from_row: ::Integer, ?to_col: ::Integer, ?to_row: ::Integer, **untyped opts) -> void
|
|
1430
|
+
def image(file_data, ext: "png", from_col: 0, from_row: 0, to_col: 5, to_row: 10, **opts)
|
|
705
1431
|
img = { file_data: file_data, ext: ext, from_col: from_col, from_row: from_row, to_col: to_col, to_row: to_row }
|
|
706
1432
|
img.merge!(opts)
|
|
707
1433
|
@images << img
|
|
@@ -710,7 +1436,9 @@ module Xlsxrb
|
|
|
710
1436
|
# --- Shapes ---
|
|
711
1437
|
|
|
712
1438
|
# Add a shape to the sheet.
|
|
713
|
-
|
|
1439
|
+
# @api public
|
|
1440
|
+
#: (**untyped opts) -> void
|
|
1441
|
+
def shape(preset: "rect", text: nil, from_col: 0, from_row: 0, to_col: 5, to_row: 5, **opts)
|
|
714
1442
|
shape = { preset: preset, text: text, from_col: from_col, from_row: from_row, to_col: to_col, to_row: to_row }
|
|
715
1443
|
shape[:name] = opts.delete(:name) || "Shape #{@shapes.size + 1}"
|
|
716
1444
|
shape.merge!(opts)
|
|
@@ -720,27 +1448,38 @@ module Xlsxrb
|
|
|
720
1448
|
# --- Sheet Properties ---
|
|
721
1449
|
|
|
722
1450
|
# Set a sheet-level property (e.g. :tab_color).
|
|
723
|
-
|
|
1451
|
+
# @api public
|
|
1452
|
+
#: (Symbol name, untyped value) -> void
|
|
1453
|
+
def sheet_properties(name, value)
|
|
724
1454
|
@sheet_properties[name] = value
|
|
725
1455
|
end
|
|
726
1456
|
|
|
727
1457
|
# Set a sheet view property (e.g. :show_grid_lines, :zoom_scale).
|
|
728
|
-
|
|
1458
|
+
# @api public
|
|
1459
|
+
#: (Symbol name, untyped value) -> void
|
|
1460
|
+
def sheet_view(name, value)
|
|
729
1461
|
@sheet_view[name] = value
|
|
730
1462
|
end
|
|
731
1463
|
|
|
732
1464
|
# --- Row / Column Breaks ---
|
|
733
1465
|
|
|
734
1466
|
# Add a page break before a row.
|
|
735
|
-
|
|
1467
|
+
# @api public
|
|
1468
|
+
#: (Integer row_num) -> void
|
|
1469
|
+
def page_break_row(row_num)
|
|
736
1470
|
@row_breaks << row_num
|
|
737
1471
|
end
|
|
738
1472
|
|
|
739
1473
|
# Add a page break before a column.
|
|
740
|
-
|
|
1474
|
+
# @api public
|
|
1475
|
+
#: (Integer | String col_index) -> void
|
|
1476
|
+
def page_break_col(col_index)
|
|
1477
|
+
col_index = Elements::Cell.column_index(col_index)
|
|
741
1478
|
@col_breaks << col_index
|
|
742
1479
|
end
|
|
743
1480
|
|
|
1481
|
+
# @api public
|
|
1482
|
+
#: () -> untyped
|
|
744
1483
|
def build
|
|
745
1484
|
facade_meta = {}
|
|
746
1485
|
facade_meta[:hyperlinks] = @hyperlinks unless @hyperlinks.empty?
|
|
@@ -776,13 +1515,19 @@ module Xlsxrb
|
|
|
776
1515
|
end
|
|
777
1516
|
|
|
778
1517
|
# Internal: returns styles for later processing by WorkbookBuilder
|
|
1518
|
+
#: untyped
|
|
779
1519
|
attr_reader :styles
|
|
780
1520
|
end
|
|
781
1521
|
|
|
782
1522
|
# DSL context for Xlsxrb.generate streaming writes.
|
|
1523
|
+
# @api public
|
|
783
1524
|
class StreamWriter
|
|
784
|
-
|
|
1525
|
+
attr_reader :current_sheet
|
|
1526
|
+
|
|
1527
|
+
#: (untyped target, ?strict_excel_mode: bool) -> void
|
|
1528
|
+
def initialize(target, strict_excel_mode: true)
|
|
785
1529
|
@target = target
|
|
1530
|
+
@strict_excel_mode = strict_excel_mode
|
|
786
1531
|
@sst = []
|
|
787
1532
|
@sst_index = {}
|
|
788
1533
|
@sheets = []
|
|
@@ -825,13 +1570,35 @@ module Xlsxrb
|
|
|
825
1570
|
@app_properties = {}
|
|
826
1571
|
@custom_properties = []
|
|
827
1572
|
@workbook_protection = nil
|
|
1573
|
+
@workbook_properties = { update_links: "never" }
|
|
1574
|
+
end
|
|
1575
|
+
|
|
1576
|
+
# Set a workbook property.
|
|
1577
|
+
#
|
|
1578
|
+
# @note **SECURITY WARNING:** If you set `:update_links` to anything other than `"never"`,
|
|
1579
|
+
# you may expose end-users to malicious external reference vulnerabilities (e.g., CSV/DDE Injection)
|
|
1580
|
+
# when they open the generated Excel file. Ensure you fully trust the exported data.
|
|
1581
|
+
#
|
|
1582
|
+
# @param name [Symbol] The property name (e.g. :update_links).
|
|
1583
|
+
# @param value [String, Integer, Boolean] The property value.
|
|
1584
|
+
# @return [void]
|
|
1585
|
+
#: (Symbol name, String | Integer | bool value) -> void
|
|
1586
|
+
def workbook_property(name, value)
|
|
1587
|
+
@workbook_properties[name] = value
|
|
828
1588
|
end
|
|
829
1589
|
|
|
830
1590
|
# Define a named style that can be applied to cells.
|
|
831
|
-
|
|
1591
|
+
#
|
|
1592
|
+
# @param name [String] The name of the style.
|
|
1593
|
+
# @param opts [Hash] Style options (e.g. bold: true).
|
|
1594
|
+
# @yield [style_builder]
|
|
1595
|
+
# @yieldparam style_builder [Xlsxrb::StyleBuilder]
|
|
1596
|
+
# @return [StyleBuilder]
|
|
1597
|
+
#: (String name, **untyped opts) ?{ (StyleBuilder) -> void } -> StyleBuilder
|
|
1598
|
+
def style(name, **opts)
|
|
832
1599
|
style_builder = StyleBuilder.new(name)
|
|
833
1600
|
style_builder.apply_options!(**opts) unless opts.empty?
|
|
834
|
-
|
|
1601
|
+
yield style_builder if block_given?
|
|
835
1602
|
@styles[name] = style_builder
|
|
836
1603
|
|
|
837
1604
|
# Register immediately
|
|
@@ -840,8 +1607,473 @@ module Xlsxrb
|
|
|
840
1607
|
style_builder
|
|
841
1608
|
end
|
|
842
1609
|
|
|
843
|
-
#
|
|
844
|
-
|
|
1610
|
+
# Proxy object yielded by the `sheet` method to prevent writing to inactive sheets.
|
|
1611
|
+
# @api public
|
|
1612
|
+
class WorksheetProxy
|
|
1613
|
+
def initialize(writer, sheet_name)
|
|
1614
|
+
@writer = writer
|
|
1615
|
+
@sheet_name = sheet_name
|
|
1616
|
+
end
|
|
1617
|
+
|
|
1618
|
+
# Delegates to StreamWriter#style.
|
|
1619
|
+
# @see StreamWriter#style
|
|
1620
|
+
# @api public
|
|
1621
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1622
|
+
#: (*untyped args, **untyped kwargs) ?{ (Xlsxrb::StyleBuilder) -> void } -> untyped
|
|
1623
|
+
def style(...)
|
|
1624
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1625
|
+
|
|
1626
|
+
@writer.style(...)
|
|
1627
|
+
end
|
|
1628
|
+
|
|
1629
|
+
# Delegates to StreamWriter#merge.
|
|
1630
|
+
# @see StreamWriter#merge
|
|
1631
|
+
# @api public
|
|
1632
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1633
|
+
def merge(...)
|
|
1634
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1635
|
+
|
|
1636
|
+
@writer.merge(...)
|
|
1637
|
+
end
|
|
1638
|
+
|
|
1639
|
+
# Delegates to StreamWriter#shape.
|
|
1640
|
+
# @see StreamWriter#shape
|
|
1641
|
+
# @api public
|
|
1642
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1643
|
+
def shape(...)
|
|
1644
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1645
|
+
|
|
1646
|
+
@writer.shape(...)
|
|
1647
|
+
end
|
|
1648
|
+
|
|
1649
|
+
# Delegates to StreamWriter#internal_sheet_setup.
|
|
1650
|
+
# @see StreamWriter#internal_sheet_setup
|
|
1651
|
+
# @api public
|
|
1652
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1653
|
+
def internal_sheet_setup(...)
|
|
1654
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1655
|
+
|
|
1656
|
+
# simplecov:disable
|
|
1657
|
+
# Edge case / untested delegation block
|
|
1658
|
+
@writer.internal_sheet_setup(...)
|
|
1659
|
+
# simplecov:enable
|
|
1660
|
+
end
|
|
1661
|
+
|
|
1662
|
+
# Delegates to StreamWriter#row.
|
|
1663
|
+
# @see StreamWriter#row
|
|
1664
|
+
# @api public
|
|
1665
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1666
|
+
def row(...)
|
|
1667
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1668
|
+
|
|
1669
|
+
@writer.row(...)
|
|
1670
|
+
end
|
|
1671
|
+
|
|
1672
|
+
# Delegates to StreamWriter#column.
|
|
1673
|
+
# @see StreamWriter#column
|
|
1674
|
+
# @api public
|
|
1675
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1676
|
+
def column(...)
|
|
1677
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1678
|
+
|
|
1679
|
+
@writer.column(...)
|
|
1680
|
+
end
|
|
1681
|
+
|
|
1682
|
+
# Delegates to StreamWriter#chart.
|
|
1683
|
+
# @see StreamWriter#chart
|
|
1684
|
+
# @api public
|
|
1685
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1686
|
+
def chart(...)
|
|
1687
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1688
|
+
|
|
1689
|
+
@writer.chart(...)
|
|
1690
|
+
end
|
|
1691
|
+
|
|
1692
|
+
# Delegates to StreamWriter#hyperlink.
|
|
1693
|
+
# @see StreamWriter#hyperlink
|
|
1694
|
+
# @api public
|
|
1695
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1696
|
+
def hyperlink(...)
|
|
1697
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1698
|
+
|
|
1699
|
+
@writer.hyperlink(...)
|
|
1700
|
+
end
|
|
1701
|
+
|
|
1702
|
+
# Delegates to StreamWriter#auto_filter.
|
|
1703
|
+
# @see StreamWriter#auto_filter
|
|
1704
|
+
# @api public
|
|
1705
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1706
|
+
def auto_filter(...)
|
|
1707
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1708
|
+
|
|
1709
|
+
@writer.auto_filter(...)
|
|
1710
|
+
end
|
|
1711
|
+
|
|
1712
|
+
# Delegates to StreamWriter#filter_column.
|
|
1713
|
+
# @see StreamWriter#filter_column
|
|
1714
|
+
# @api public
|
|
1715
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1716
|
+
def filter_column(...)
|
|
1717
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1718
|
+
|
|
1719
|
+
@writer.filter_column(...)
|
|
1720
|
+
end
|
|
1721
|
+
|
|
1722
|
+
# Delegates to StreamWriter#sort_state.
|
|
1723
|
+
# @see StreamWriter#sort_state
|
|
1724
|
+
# @api public
|
|
1725
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1726
|
+
def sort_state(...)
|
|
1727
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1728
|
+
|
|
1729
|
+
@writer.sort_state(...)
|
|
1730
|
+
end
|
|
1731
|
+
|
|
1732
|
+
# Delegates to StreamWriter#validate_data.
|
|
1733
|
+
# @see StreamWriter#validate_data
|
|
1734
|
+
# @api public
|
|
1735
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1736
|
+
def validate_data(...)
|
|
1737
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1738
|
+
|
|
1739
|
+
@writer.validate_data(...)
|
|
1740
|
+
end
|
|
1741
|
+
|
|
1742
|
+
# Delegates to StreamWriter#conditional_format.
|
|
1743
|
+
# @see StreamWriter#conditional_format
|
|
1744
|
+
# @api public
|
|
1745
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1746
|
+
def conditional_format(...)
|
|
1747
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1748
|
+
|
|
1749
|
+
@writer.conditional_format(...)
|
|
1750
|
+
end
|
|
1751
|
+
|
|
1752
|
+
# Delegates to StreamWriter#table.
|
|
1753
|
+
# @see StreamWriter#table
|
|
1754
|
+
# @api public
|
|
1755
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1756
|
+
def table(...)
|
|
1757
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1758
|
+
|
|
1759
|
+
@writer.table(...)
|
|
1760
|
+
end
|
|
1761
|
+
|
|
1762
|
+
# Delegates to StreamWriter#cleanup!.
|
|
1763
|
+
# @see StreamWriter#cleanup!
|
|
1764
|
+
# @api public
|
|
1765
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1766
|
+
def cleanup!(...)
|
|
1767
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1768
|
+
|
|
1769
|
+
# simplecov:disable
|
|
1770
|
+
# Edge case / untested delegation block
|
|
1771
|
+
@writer.cleanup!(...)
|
|
1772
|
+
# simplecov:enable
|
|
1773
|
+
end
|
|
1774
|
+
|
|
1775
|
+
# Delegates to StreamWriter#comment.
|
|
1776
|
+
# @see StreamWriter#comment
|
|
1777
|
+
# @api public
|
|
1778
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1779
|
+
def comment(...)
|
|
1780
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1781
|
+
|
|
1782
|
+
@writer.comment(...)
|
|
1783
|
+
end
|
|
1784
|
+
|
|
1785
|
+
# Delegates to StreamWriter#pivot_table.
|
|
1786
|
+
# @see StreamWriter#pivot_table
|
|
1787
|
+
# @api public
|
|
1788
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1789
|
+
def pivot_table(...)
|
|
1790
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1791
|
+
|
|
1792
|
+
@writer.pivot_table(...)
|
|
1793
|
+
end
|
|
1794
|
+
|
|
1795
|
+
# Delegates to StreamWriter#sparkline_group.
|
|
1796
|
+
# @see StreamWriter#sparkline_group
|
|
1797
|
+
# @api public
|
|
1798
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1799
|
+
def sparkline_group(...)
|
|
1800
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1801
|
+
|
|
1802
|
+
@writer.sparkline_group(...)
|
|
1803
|
+
end
|
|
1804
|
+
|
|
1805
|
+
# Delegates to StreamWriter#workbook_property.
|
|
1806
|
+
# @see StreamWriter#workbook_property
|
|
1807
|
+
# @api public
|
|
1808
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1809
|
+
def workbook_property(...)
|
|
1810
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1811
|
+
|
|
1812
|
+
@writer.workbook_property(...)
|
|
1813
|
+
end
|
|
1814
|
+
|
|
1815
|
+
# Delegates to StreamWriter#sheet_properties.
|
|
1816
|
+
# @see StreamWriter#sheet_properties
|
|
1817
|
+
# @api public
|
|
1818
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1819
|
+
def sheet_properties(...)
|
|
1820
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1821
|
+
|
|
1822
|
+
@writer.sheet_properties(...)
|
|
1823
|
+
end
|
|
1824
|
+
|
|
1825
|
+
# Delegates to StreamWriter#defined_name.
|
|
1826
|
+
# @see StreamWriter#defined_name
|
|
1827
|
+
# @api public
|
|
1828
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1829
|
+
def defined_name(...)
|
|
1830
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1831
|
+
|
|
1832
|
+
@writer.defined_name(...)
|
|
1833
|
+
end
|
|
1834
|
+
|
|
1835
|
+
# Delegates to StreamWriter#freeze_pane.
|
|
1836
|
+
# @see StreamWriter#freeze_pane
|
|
1837
|
+
# @api public
|
|
1838
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1839
|
+
def freeze_pane(...)
|
|
1840
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1841
|
+
|
|
1842
|
+
@writer.freeze_pane(...)
|
|
1843
|
+
end
|
|
1844
|
+
|
|
1845
|
+
# Delegates to StreamWriter#print_area.
|
|
1846
|
+
# @see StreamWriter#print_area
|
|
1847
|
+
# @api public
|
|
1848
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1849
|
+
def print_area(...)
|
|
1850
|
+
# simplecov:disable
|
|
1851
|
+
# Edge case / untested delegation block
|
|
1852
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1853
|
+
|
|
1854
|
+
@writer.print_area(...)
|
|
1855
|
+
# simplecov:enable
|
|
1856
|
+
end
|
|
1857
|
+
|
|
1858
|
+
# Delegates to StreamWriter#print_titles.
|
|
1859
|
+
# @see StreamWriter#print_titles
|
|
1860
|
+
# @api public
|
|
1861
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1862
|
+
def print_titles(...)
|
|
1863
|
+
# simplecov:disable
|
|
1864
|
+
# Edge case / untested delegation block
|
|
1865
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1866
|
+
|
|
1867
|
+
@writer.print_titles(...)
|
|
1868
|
+
# simplecov:enable
|
|
1869
|
+
end
|
|
1870
|
+
|
|
1871
|
+
# Delegates to StreamWriter#split_pane.
|
|
1872
|
+
# @see StreamWriter#split_pane
|
|
1873
|
+
# @api public
|
|
1874
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1875
|
+
def split_pane(...)
|
|
1876
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1877
|
+
|
|
1878
|
+
@writer.split_pane(...)
|
|
1879
|
+
end
|
|
1880
|
+
|
|
1881
|
+
# Delegates to StreamWriter#protect_workbook.
|
|
1882
|
+
# @see StreamWriter#protect_workbook
|
|
1883
|
+
# @api public
|
|
1884
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1885
|
+
def protect_workbook(...)
|
|
1886
|
+
# simplecov:disable
|
|
1887
|
+
# Edge case / untested delegation block
|
|
1888
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1889
|
+
|
|
1890
|
+
@writer.protect_workbook(...)
|
|
1891
|
+
# simplecov:enable
|
|
1892
|
+
end
|
|
1893
|
+
|
|
1894
|
+
# Delegates to StreamWriter#core_property.
|
|
1895
|
+
# @see StreamWriter#core_property
|
|
1896
|
+
# @api public
|
|
1897
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1898
|
+
def core_property(...)
|
|
1899
|
+
# simplecov:disable
|
|
1900
|
+
# Edge case / untested delegation block
|
|
1901
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1902
|
+
|
|
1903
|
+
@writer.core_property(...)
|
|
1904
|
+
# simplecov:enable
|
|
1905
|
+
end
|
|
1906
|
+
|
|
1907
|
+
# Delegates to StreamWriter#select_cell.
|
|
1908
|
+
# @see StreamWriter#select_cell
|
|
1909
|
+
# @api public
|
|
1910
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1911
|
+
def select_cell(...)
|
|
1912
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1913
|
+
|
|
1914
|
+
@writer.select_cell(...)
|
|
1915
|
+
end
|
|
1916
|
+
|
|
1917
|
+
# Delegates to StreamWriter#page_margins.
|
|
1918
|
+
# @see StreamWriter#page_margins
|
|
1919
|
+
# @api public
|
|
1920
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1921
|
+
def page_margins(...)
|
|
1922
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1923
|
+
|
|
1924
|
+
@writer.page_margins(...)
|
|
1925
|
+
end
|
|
1926
|
+
|
|
1927
|
+
# Delegates to StreamWriter#page_setup.
|
|
1928
|
+
# @see StreamWriter#page_setup
|
|
1929
|
+
# @api public
|
|
1930
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1931
|
+
def page_setup(...)
|
|
1932
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1933
|
+
|
|
1934
|
+
@writer.page_setup(...)
|
|
1935
|
+
end
|
|
1936
|
+
|
|
1937
|
+
# Delegates to StreamWriter#header_footer.
|
|
1938
|
+
# @see StreamWriter#header_footer
|
|
1939
|
+
# @api public
|
|
1940
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1941
|
+
def header_footer(...)
|
|
1942
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1943
|
+
|
|
1944
|
+
@writer.header_footer(...)
|
|
1945
|
+
end
|
|
1946
|
+
|
|
1947
|
+
# Delegates to StreamWriter#print_options.
|
|
1948
|
+
# @see StreamWriter#print_options
|
|
1949
|
+
# @api public
|
|
1950
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1951
|
+
def print_options(...)
|
|
1952
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1953
|
+
|
|
1954
|
+
@writer.print_options(...)
|
|
1955
|
+
end
|
|
1956
|
+
|
|
1957
|
+
# Delegates to StreamWriter#properties.
|
|
1958
|
+
# @see StreamWriter#properties
|
|
1959
|
+
# @api public
|
|
1960
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1961
|
+
def properties(...)
|
|
1962
|
+
# simplecov:disable
|
|
1963
|
+
# Edge case / untested delegation block
|
|
1964
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1965
|
+
|
|
1966
|
+
@writer.properties(...)
|
|
1967
|
+
# simplecov:enable
|
|
1968
|
+
end
|
|
1969
|
+
|
|
1970
|
+
# Delegates to StreamWriter#app_property.
|
|
1971
|
+
# @see StreamWriter#app_property
|
|
1972
|
+
# @api public
|
|
1973
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1974
|
+
def app_property(...)
|
|
1975
|
+
# simplecov:disable
|
|
1976
|
+
# Edge case / untested delegation block
|
|
1977
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1978
|
+
|
|
1979
|
+
@writer.app_property(...)
|
|
1980
|
+
# simplecov:enable
|
|
1981
|
+
end
|
|
1982
|
+
|
|
1983
|
+
# Delegates to StreamWriter#protect_sheet.
|
|
1984
|
+
# @see StreamWriter#protect_sheet
|
|
1985
|
+
# @api public
|
|
1986
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1987
|
+
def protect_sheet(...)
|
|
1988
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
1989
|
+
|
|
1990
|
+
@writer.protect_sheet(...)
|
|
1991
|
+
end
|
|
1992
|
+
|
|
1993
|
+
# Delegates to StreamWriter#custom_property.
|
|
1994
|
+
# @see StreamWriter#custom_property
|
|
1995
|
+
# @api public
|
|
1996
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
1997
|
+
def custom_property(...)
|
|
1998
|
+
# simplecov:disable
|
|
1999
|
+
# Edge case / untested delegation block
|
|
2000
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
2001
|
+
|
|
2002
|
+
@writer.custom_property(...)
|
|
2003
|
+
# simplecov:enable
|
|
2004
|
+
end
|
|
2005
|
+
|
|
2006
|
+
# Delegates to StreamWriter#image.
|
|
2007
|
+
# @see StreamWriter#image
|
|
2008
|
+
# @api public
|
|
2009
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
2010
|
+
def image(...)
|
|
2011
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
2012
|
+
|
|
2013
|
+
@writer.image(...)
|
|
2014
|
+
end
|
|
2015
|
+
|
|
2016
|
+
# Delegates to StreamWriter#sheet_view.
|
|
2017
|
+
# @see StreamWriter#sheet_view
|
|
2018
|
+
# @api public
|
|
2019
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
2020
|
+
def sheet_view(...)
|
|
2021
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
2022
|
+
|
|
2023
|
+
@writer.sheet_view(...)
|
|
2024
|
+
end
|
|
2025
|
+
|
|
2026
|
+
# Delegates to StreamWriter#page_break_row.
|
|
2027
|
+
# @see StreamWriter#page_break_row
|
|
2028
|
+
# @api public
|
|
2029
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
2030
|
+
def page_break_row(...)
|
|
2031
|
+
# simplecov:disable
|
|
2032
|
+
# Edge case / untested delegation block
|
|
2033
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
2034
|
+
|
|
2035
|
+
@writer.page_break_row(...)
|
|
2036
|
+
# simplecov:enable
|
|
2037
|
+
end
|
|
2038
|
+
|
|
2039
|
+
# Delegates to StreamWriter#page_break_col.
|
|
2040
|
+
# @see StreamWriter#page_break_col
|
|
2041
|
+
# @api public
|
|
2042
|
+
#: (*untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
|
|
2043
|
+
def page_break_col(...)
|
|
2044
|
+
# simplecov:disable
|
|
2045
|
+
# Edge case / untested delegation block
|
|
2046
|
+
raise Error, "Sheet '' is no longer active. In streaming mode, you cannot write to a previous sheet." if @writer.current_sheet != @sheet_name
|
|
2047
|
+
|
|
2048
|
+
@writer.page_break_col(...)
|
|
2049
|
+
# simplecov:enable
|
|
2050
|
+
end
|
|
2051
|
+
end
|
|
2052
|
+
|
|
2053
|
+
# Add a new sheet.
|
|
2054
|
+
#
|
|
2055
|
+
# @param name [String, nil] The name of the sheet.
|
|
2056
|
+
# @param opts [Hash] Sheet properties.
|
|
2057
|
+
# @yield [sheet_builder]
|
|
2058
|
+
# @yieldparam sheet_builder [Xlsxrb::WorksheetBuilder]
|
|
2059
|
+
# @return [void]
|
|
2060
|
+
#: (?String? name, **untyped opts) ?{ (WorksheetProxy) -> void } -> untyped
|
|
2061
|
+
def sheet(name = nil, **opts)
|
|
2062
|
+
name ||= "Sheet#{@sheets.size + 1}"
|
|
2063
|
+
raise ArgumentError, "Sheet name '#{name}' must be <= 31 characters (Excel limitation)" if @strict_excel_mode && name.length > 31
|
|
2064
|
+
raise ArgumentError, "Sheet name '#{name}' contains invalid characters (ECMA-376 OOXML specification)" if name.match?(%r{[\[\]*?/\\]})
|
|
2065
|
+
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)
|
|
2066
|
+
|
|
2067
|
+
internal_sheet_setup(name)
|
|
2068
|
+
opts.each { |k, v| set_sheet_property(k, v) }
|
|
2069
|
+
|
|
2070
|
+
yield WorksheetProxy.new(self, @current_sheet) if block_given?
|
|
2071
|
+
@current_sheet
|
|
2072
|
+
end
|
|
2073
|
+
|
|
2074
|
+
# Internal: Start or switch to a named sheet (internal helper).
|
|
2075
|
+
#: (?String? name) ?{ (WorksheetProxy) -> void } -> (WorksheetProxy | nil)
|
|
2076
|
+
def internal_sheet_setup(name = nil)
|
|
845
2077
|
flush_current_sheet
|
|
846
2078
|
name ||= "Sheet#{@sheets.size + 1}"
|
|
847
2079
|
@current_sheet = name
|
|
@@ -882,25 +2114,105 @@ module Xlsxrb
|
|
|
882
2114
|
|
|
883
2115
|
return unless block_given?
|
|
884
2116
|
|
|
2117
|
+
# simplecov:disable
|
|
2118
|
+
# Edge case / untested delegation block
|
|
885
2119
|
yield self
|
|
886
2120
|
flush_current_sheet
|
|
2121
|
+
# simplecov:enable
|
|
887
2122
|
end
|
|
888
2123
|
|
|
889
2124
|
# Add a row of values. values is an Array.
|
|
890
2125
|
# styles:: Hash mapping column indices to style names, or Array of style names for each column
|
|
891
|
-
|
|
892
|
-
|
|
2126
|
+
# Add a row to the sheet.
|
|
2127
|
+
#
|
|
2128
|
+
# @param values [Array, Hash] The cell values.
|
|
2129
|
+
# @param styles [String, Array<String>, nil] Styles to apply to cells.
|
|
2130
|
+
# @param height [Float, nil] The row height.
|
|
2131
|
+
# @param hidden [Boolean] Whether the row is hidden.
|
|
2132
|
+
# @param custom_height [Boolean] Whether it's a custom height.
|
|
2133
|
+
# @param outline_level [Integer, nil] The outline level.
|
|
2134
|
+
# @note Excel's column limit is 16,384, row limit is 1,048,576, string max length is 32,767.
|
|
2135
|
+
# @return [void]
|
|
2136
|
+
#: (Array[untyped] | Hash[untyped, untyped] values, ?styles: untyped, ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?outline_level: Integer | nil) -> void
|
|
2137
|
+
def row(values, styles: nil, height: nil, hidden: false, custom_height: false, outline_level: nil)
|
|
2138
|
+
sheet if @current_sheet.nil?
|
|
893
2139
|
|
|
894
2140
|
row_index = @current_row_index
|
|
2141
|
+
# See: https://support.microsoft.com/en-us/office/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3
|
|
2142
|
+
if @strict_excel_mode
|
|
2143
|
+
raise ArgumentError, "Row index #{row_index} exceeds Excel limit of 1,048,576 rows" if row_index >= 1_048_576
|
|
2144
|
+
raise ArgumentError, "Row height #{height} must be between 0 and 409 points (Excel limitation)" if height && (height.negative? || height > 409)
|
|
2145
|
+
end
|
|
895
2146
|
@current_row_index += 1
|
|
896
2147
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
2148
|
+
if values.is_a?(Hash)
|
|
2149
|
+
max_col = values.keys.map { |k| Elements::Cell.column_index(k) }.max || -1
|
|
2150
|
+
cells_array = Array.new(max_col + 1)
|
|
2151
|
+
values.each do |k, v|
|
|
2152
|
+
idx = Elements::Cell.column_index(k)
|
|
2153
|
+
cells_array[idx] = v
|
|
2154
|
+
end
|
|
2155
|
+
values = cells_array
|
|
2156
|
+
end
|
|
2157
|
+
|
|
2158
|
+
if styles.is_a?(Hash)
|
|
2159
|
+
expanded_styles = {}
|
|
2160
|
+
styles.each do |k, v|
|
|
2161
|
+
if k.is_a?(Range) || k.is_a?(Array)
|
|
2162
|
+
k.each { |idx| expanded_styles[Elements::Cell.column_index(idx)] = v }
|
|
2163
|
+
else
|
|
2164
|
+
expanded_styles[Elements::Cell.column_index(k)] = v
|
|
2165
|
+
end
|
|
2166
|
+
end
|
|
2167
|
+
max_col_style = expanded_styles.keys.max || -1
|
|
2168
|
+
styles_array = Array.new(max_col_style + 1)
|
|
2169
|
+
expanded_styles.each do |idx, v|
|
|
2170
|
+
styles_array[idx] = v
|
|
2171
|
+
end
|
|
2172
|
+
styles = styles_array
|
|
2173
|
+
end
|
|
2174
|
+
|
|
2175
|
+
# Auto-detect Date / Time for built-in styles
|
|
2176
|
+
values.each_with_index do |val, idx|
|
|
2177
|
+
cell_style = styles.is_a?(Array) ? styles[idx] : styles
|
|
2178
|
+
|
|
2179
|
+
if val.is_a?(Date) && cell_style.nil?
|
|
2180
|
+
style("__xlsxrb_date", number_format: "yyyy-mm-dd") unless @styles.key?("__xlsxrb_date")
|
|
2181
|
+
styles = [] if styles.nil?
|
|
2182
|
+
styles = Array.new(values.size, styles) unless styles.is_a?(Array)
|
|
2183
|
+
styles[idx] = "__xlsxrb_date"
|
|
2184
|
+
elsif val.is_a?(Time) && cell_style.nil?
|
|
2185
|
+
style("__xlsxrb_time", number_format: "yyyy-mm-dd hh:mm:ss") unless @styles.key?("__xlsxrb_time")
|
|
2186
|
+
styles = [] if styles.nil?
|
|
2187
|
+
styles = Array.new(values.size, styles) unless styles.is_a?(Array)
|
|
2188
|
+
styles[idx] = "__xlsxrb_time"
|
|
2189
|
+
end
|
|
2190
|
+
end
|
|
2191
|
+
|
|
2192
|
+
has_charts = @current_charts && !@current_charts.empty?
|
|
2193
|
+
row_num = row_index + 1 if has_charts
|
|
2194
|
+
|
|
2195
|
+
max_len = values.size
|
|
2196
|
+
max_len = [max_len, styles.size].max if styles.is_a?(Array)
|
|
2197
|
+
# See: https://support.microsoft.com/en-us/office/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3
|
|
2198
|
+
raise ArgumentError, "Row contains #{max_len} columns, exceeding Excel limit of 16_384 columns" if @strict_excel_mode && max_len > 16_384
|
|
2199
|
+
|
|
2200
|
+
if has_charts || @strict_excel_mode
|
|
2201
|
+
@current_cells ||= {} if has_charts
|
|
2202
|
+
max_len.times do |col_idx|
|
|
2203
|
+
val = col_idx < values.size ? values[col_idx] : nil
|
|
2204
|
+
next if val.nil?
|
|
901
2205
|
|
|
902
|
-
|
|
903
|
-
|
|
2206
|
+
raise ArgumentError, "Invalid cell value type or value: #{val.class} for value #{val.inspect}" unless val.nil? || val.is_a?(String) || (val.is_a?(Numeric) && !(val.is_a?(Float) && (val.infinite? || val.nan?))) || val.is_a?(TrueClass) || val.is_a?(FalseClass) || val.is_a?(Date) || val.is_a?(Time) || val.is_a?(Elements::Formula) || (val.is_a?(Hash) && val.key?(:formula)) || val.is_a?(Elements::RichText) || val.is_a?(Elements::CellError)
|
|
2207
|
+
|
|
2208
|
+
# See: https://support.microsoft.com/en-us/office/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3
|
|
2209
|
+
raise ArgumentError, "Cell text length #{val.length} exceeds Excel limit of 32,767 characters" if @strict_excel_mode && val.is_a?(String) && val.length > 32_767
|
|
2210
|
+
|
|
2211
|
+
if has_charts
|
|
2212
|
+
addr = "#{Elements::Cell.column_letter(col_idx)}#{row_num}"
|
|
2213
|
+
@current_cells[addr] = val
|
|
2214
|
+
end
|
|
2215
|
+
end
|
|
904
2216
|
end
|
|
905
2217
|
|
|
906
2218
|
attrs = nil
|
|
@@ -916,19 +2228,41 @@ module Xlsxrb
|
|
|
916
2228
|
end
|
|
917
2229
|
|
|
918
2230
|
# Set column width for a 0-based column index.
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
2231
|
+
# Add a column to the sheet.
|
|
2232
|
+
#
|
|
2233
|
+
# @param index [Integer, String] The column index (0-based) or letter.
|
|
2234
|
+
# @param width [Float, nil] The column width.
|
|
2235
|
+
# @param hidden [Boolean] Whether the column is hidden.
|
|
2236
|
+
# @param custom_width [Boolean] Whether it's a custom width.
|
|
2237
|
+
# @param outline_level [Integer, nil] The outline level.
|
|
2238
|
+
# @note Excel's column width max is 255.
|
|
2239
|
+
# @return [void]
|
|
2240
|
+
#: (Integer | String | Range[Integer | String] | Array[Integer | String] index, ?width: Float | Integer | nil, ?hidden: bool, ?custom_width: bool, ?outline_level: Integer | nil) -> void
|
|
2241
|
+
def column(index, width: nil, hidden: false, custom_width: false, outline_level: nil)
|
|
2242
|
+
raise ArgumentError, "Column width #{width} must be between 0 and 255 characters (Excel limitation)" if @strict_excel_mode && width && (width.negative? || width > 255)
|
|
2243
|
+
|
|
2244
|
+
indices = case index
|
|
2245
|
+
when Range, Array
|
|
2246
|
+
index.map { |i| Elements::Cell.column_index(i) }
|
|
2247
|
+
else
|
|
2248
|
+
[Elements::Cell.column_index(index)]
|
|
2249
|
+
end
|
|
2250
|
+
|
|
2251
|
+
sheet if @current_sheet.nil?
|
|
2252
|
+
|
|
2253
|
+
indices.each do |idx|
|
|
2254
|
+
@current_columns << { index: idx, width: width, hidden: hidden, custom_width: custom_width || !width.nil?, outline_level: outline_level }
|
|
2255
|
+
end
|
|
923
2256
|
end
|
|
924
2257
|
|
|
925
2258
|
# Add a chart to the current sheet.
|
|
926
|
-
|
|
927
|
-
|
|
2259
|
+
#: (**untyped options) ?{ (ChartBuilder) -> void } -> void
|
|
2260
|
+
def chart(**options)
|
|
2261
|
+
sheet if @current_sheet.nil?
|
|
928
2262
|
|
|
929
2263
|
if block_given?
|
|
930
2264
|
builder = ChartBuilder.new
|
|
931
|
-
|
|
2265
|
+
yield builder
|
|
932
2266
|
options = builder.options.merge(options)
|
|
933
2267
|
end
|
|
934
2268
|
|
|
@@ -936,9 +2270,9 @@ module Xlsxrb
|
|
|
936
2270
|
end
|
|
937
2271
|
|
|
938
2272
|
# --- Hyperlinks ---
|
|
939
|
-
|
|
940
|
-
def
|
|
941
|
-
|
|
2273
|
+
#: (String | Integer cell, ?String? url, ?display: String?, ?tooltip: String?, ?location: String?) -> void
|
|
2274
|
+
def hyperlink(cell, url = nil, display: nil, tooltip: nil, location: nil)
|
|
2275
|
+
sheet if @current_sheet.nil?
|
|
942
2276
|
link = { cell: cell }
|
|
943
2277
|
link[:url] = url if url
|
|
944
2278
|
link[:display] = display if display
|
|
@@ -948,42 +2282,42 @@ module Xlsxrb
|
|
|
948
2282
|
end
|
|
949
2283
|
|
|
950
2284
|
# --- Auto Filter / Sort ---
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
add_sheet if @current_sheet.nil?
|
|
2285
|
+
#: (String range) -> void
|
|
2286
|
+
def auto_filter(range)
|
|
2287
|
+
sheet if @current_sheet.nil?
|
|
955
2288
|
@current_auto_filter = range
|
|
956
2289
|
end
|
|
957
|
-
# rubocop:enable Naming/AccessorMethodName
|
|
958
2290
|
|
|
959
|
-
|
|
960
|
-
|
|
2291
|
+
#: (untyped col_id, untyped filter) -> untyped
|
|
2292
|
+
def filter_column(col_id, filter)
|
|
2293
|
+
sheet if @current_sheet.nil?
|
|
961
2294
|
@current_filter_columns[col_id] = filter
|
|
962
2295
|
end
|
|
963
2296
|
|
|
964
|
-
|
|
965
|
-
|
|
2297
|
+
#: (untyped ref, untyped sort_conditions, **untyped opts) -> untyped
|
|
2298
|
+
def sort_state(ref, sort_conditions, **opts)
|
|
2299
|
+
sheet if @current_sheet.nil?
|
|
966
2300
|
@current_sort_state = { ref: ref, sort_conditions: sort_conditions }.merge(opts)
|
|
967
2301
|
end
|
|
968
2302
|
|
|
969
2303
|
# --- Data Validation ---
|
|
970
|
-
|
|
971
|
-
def
|
|
972
|
-
|
|
2304
|
+
#: (untyped sqref, **untyped opts) -> void
|
|
2305
|
+
def validate_data(sqref, **opts)
|
|
2306
|
+
sheet if @current_sheet.nil?
|
|
973
2307
|
@current_data_validations << opts.merge(sqref: sqref)
|
|
974
2308
|
end
|
|
975
2309
|
|
|
976
2310
|
# --- Conditional Formatting ---
|
|
977
|
-
|
|
978
|
-
def
|
|
979
|
-
|
|
2311
|
+
#: (untyped sqref, **untyped opts) -> void
|
|
2312
|
+
def conditional_format(sqref, **opts)
|
|
2313
|
+
sheet if @current_sheet.nil?
|
|
980
2314
|
@current_conditional_formats << opts.merge(sqref: sqref)
|
|
981
2315
|
end
|
|
982
2316
|
|
|
983
2317
|
# --- Tables ---
|
|
984
|
-
|
|
985
|
-
def
|
|
986
|
-
|
|
2318
|
+
#: (untyped ref, **untyped opts) -> void
|
|
2319
|
+
def table(ref, columns:, name: nil, display_name: nil, style: nil, **opts)
|
|
2320
|
+
sheet if @current_sheet.nil?
|
|
987
2321
|
tbl = { ref: ref, columns: columns }
|
|
988
2322
|
tbl[:name] = name if name
|
|
989
2323
|
tbl[:display_name] = display_name if display_name
|
|
@@ -993,9 +2327,9 @@ module Xlsxrb
|
|
|
993
2327
|
end
|
|
994
2328
|
|
|
995
2329
|
# --- Pivot Tables ---
|
|
996
|
-
|
|
997
|
-
def
|
|
998
|
-
|
|
2330
|
+
#: (untyped source_ref, **untyped opts) -> void
|
|
2331
|
+
def pivot_table(source_ref, row_fields:, data_fields:, col_fields: [], dest_ref: "E1", name: nil, field_names: nil, items: nil)
|
|
2332
|
+
sheet if @current_sheet.nil?
|
|
999
2333
|
@current_pivot_tables ||= []
|
|
1000
2334
|
@current_pivot_tables << {
|
|
1001
2335
|
source_ref: source_ref, row_fields: row_fields,
|
|
@@ -1006,73 +2340,114 @@ module Xlsxrb
|
|
|
1006
2340
|
end
|
|
1007
2341
|
|
|
1008
2342
|
# --- Comments ---
|
|
1009
|
-
|
|
1010
|
-
def
|
|
1011
|
-
|
|
2343
|
+
#: (String | Integer cell, String text, ?author: ::String) -> void
|
|
2344
|
+
def comment(cell, text, author: "Author")
|
|
2345
|
+
sheet if @current_sheet.nil?
|
|
1012
2346
|
@current_comments << { cell: cell, text: text, author: author }
|
|
1013
2347
|
end
|
|
1014
2348
|
|
|
1015
2349
|
# --- Sparklines ---
|
|
1016
|
-
|
|
1017
|
-
def
|
|
1018
|
-
|
|
2350
|
+
#: (**untyped opts) -> void
|
|
2351
|
+
def sparkline_group(sparklines:, type: nil, **opts)
|
|
2352
|
+
sheet if @current_sheet.nil?
|
|
1019
2353
|
group = { sparklines: sparklines }
|
|
1020
2354
|
group[:type] = type if type
|
|
1021
2355
|
group.merge!(opts)
|
|
1022
2356
|
@current_sparkline_groups << group
|
|
1023
2357
|
end
|
|
1024
2358
|
|
|
1025
|
-
#
|
|
2359
|
+
# Merge a range of cells (e.g. "A1:B2"), or by coordinate indices.
|
|
2360
|
+
#
|
|
2361
|
+
# @param range [String, nil] The string range.
|
|
2362
|
+
# @param row [Integer, nil] Single row index.
|
|
2363
|
+
# @param col_start [Integer, nil] Starting column index.
|
|
2364
|
+
# @param col_end [Integer, nil] Ending column index.
|
|
2365
|
+
# @param row_start [Integer, nil] Starting row index.
|
|
2366
|
+
# @param row_end [Integer, nil] Ending row index.
|
|
2367
|
+
# @return [void]
|
|
2368
|
+
#: (?(String | Hash[Symbol, Integer | String])? range, ?row: Integer?, ?col_start: (Integer | String)?, ?col_end: (Integer | String)?, ?row_start: Integer?, ?row_end: Integer?) -> void
|
|
2369
|
+
def merge(range = nil, row: nil, col_start: nil, col_end: nil, row_start: nil, row_end: nil)
|
|
2370
|
+
sheet if @current_sheet.nil?
|
|
2371
|
+
if range.is_a?(Hash)
|
|
2372
|
+
row = range[:row]
|
|
2373
|
+
row_start = range[:row_start]
|
|
2374
|
+
row_end = range[:row_end]
|
|
2375
|
+
col_start = range[:col_start]
|
|
2376
|
+
col_end = range[:col_end]
|
|
2377
|
+
range = nil
|
|
2378
|
+
end
|
|
1026
2379
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
2380
|
+
if range
|
|
2381
|
+
raise ArgumentError, "Invalid merge range format: '#{range}'. Expected format like 'A1:B2'." if @strict_excel_mode && !range.match?(/^[A-Za-z]{1,3}\d+(:[A-Za-z]{1,3}\d+)?$/)
|
|
2382
|
+
|
|
2383
|
+
@current_merge_cells << range
|
|
2384
|
+
else
|
|
2385
|
+
r_start = row || row_start || 0
|
|
2386
|
+
r_end = row || row_end || 0
|
|
2387
|
+
c_start = Elements::Cell.column_index(col_start || 0)
|
|
2388
|
+
c_end = Elements::Cell.column_index(col_end || 0)
|
|
2389
|
+
start_ref = "#{Xlsxrb::Elements::Cell.column_letter(c_start)}#{r_start + 1}"
|
|
2390
|
+
end_ref = "#{Xlsxrb::Elements::Cell.column_letter(c_end)}#{r_end + 1}"
|
|
2391
|
+
@current_merge_cells << "#{start_ref}:#{end_ref}"
|
|
2392
|
+
end
|
|
1030
2393
|
end
|
|
1031
2394
|
|
|
1032
2395
|
# --- Freeze / Split Panes ---
|
|
1033
2396
|
|
|
1034
|
-
|
|
1035
|
-
|
|
2397
|
+
# Freeze panes at the given row and column.
|
|
2398
|
+
#
|
|
2399
|
+
# @param row [Integer] The row index to freeze at (0-based).
|
|
2400
|
+
# @param col [Integer, String] The column index to freeze at (0-based or letter).
|
|
2401
|
+
# @return [void]
|
|
2402
|
+
#: (?row: Integer, ?col: (Integer | String)) -> void
|
|
2403
|
+
def freeze_pane(row: 0, col: 0)
|
|
2404
|
+
col = Elements::Cell.column_index(col)
|
|
2405
|
+
sheet if @current_sheet.nil?
|
|
1036
2406
|
@current_freeze_pane = { row: row, col: col }
|
|
1037
2407
|
end
|
|
1038
2408
|
|
|
1039
|
-
|
|
1040
|
-
|
|
2409
|
+
#: (?x_split: ::Integer, ?y_split: ::Integer, ?top_left_cell: String?) -> void
|
|
2410
|
+
def split_pane(x_split: 0, y_split: 0, top_left_cell: nil)
|
|
2411
|
+
sheet if @current_sheet.nil?
|
|
1041
2412
|
@current_split_pane = { x_split: x_split, y_split: y_split, top_left_cell: top_left_cell }
|
|
1042
2413
|
end
|
|
1043
2414
|
|
|
1044
|
-
|
|
1045
|
-
|
|
2415
|
+
#: (String active_cell, ?sqref: String?, ?pane: (String | Symbol)?) -> void
|
|
2416
|
+
def select_cell(active_cell, sqref: nil, pane: nil)
|
|
2417
|
+
sheet if @current_sheet.nil?
|
|
1046
2418
|
@current_selection = { active_cell: active_cell, sqref: sqref || active_cell }
|
|
1047
2419
|
@current_selection[:pane] = pane if pane
|
|
1048
2420
|
end
|
|
1049
2421
|
|
|
1050
2422
|
# --- Page Setup / Margins / Print ---
|
|
1051
|
-
|
|
1052
|
-
def
|
|
1053
|
-
|
|
2423
|
+
#: (?left: Float?, ?right: Float?, ?top: Float?, ?bottom: Float?, ?header: Float?, ?footer: Float?) -> void
|
|
2424
|
+
def page_margins(left: nil, right: nil, top: nil, bottom: nil, header: nil, footer: nil)
|
|
2425
|
+
sheet if @current_sheet.nil?
|
|
1054
2426
|
@current_page_margins = { left: left, right: right, top: top, bottom: bottom, header: header, footer: footer }.compact
|
|
1055
2427
|
end
|
|
1056
2428
|
|
|
1057
|
-
|
|
1058
|
-
|
|
2429
|
+
#: (**untyped opts) -> void
|
|
2430
|
+
def page_setup(**opts)
|
|
2431
|
+
sheet if @current_sheet.nil?
|
|
1059
2432
|
@current_page_setup.merge!(opts)
|
|
1060
2433
|
end
|
|
1061
2434
|
|
|
1062
|
-
|
|
1063
|
-
|
|
2435
|
+
#: (**untyped opts) -> void
|
|
2436
|
+
def header_footer(**opts)
|
|
2437
|
+
sheet if @current_sheet.nil?
|
|
1064
2438
|
@current_header_footer.merge!(opts)
|
|
1065
2439
|
end
|
|
1066
2440
|
|
|
1067
|
-
|
|
1068
|
-
|
|
2441
|
+
#: (Symbol name, untyped value) -> void
|
|
2442
|
+
def print_options(name, value)
|
|
2443
|
+
sheet if @current_sheet.nil?
|
|
1069
2444
|
@current_print_options[name] = value
|
|
1070
2445
|
end
|
|
1071
2446
|
|
|
1072
2447
|
# --- Sheet Protection ---
|
|
1073
|
-
|
|
1074
|
-
def
|
|
1075
|
-
|
|
2448
|
+
#: (**untyped opts) -> void
|
|
2449
|
+
def protect_sheet(**opts)
|
|
2450
|
+
sheet if @current_sheet.nil?
|
|
1076
2451
|
normalized = opts.dup
|
|
1077
2452
|
plain_password = normalized[:password]
|
|
1078
2453
|
needs_hash = plain_password.is_a?(String) && !plain_password.empty? &&
|
|
@@ -1087,18 +2462,18 @@ module Xlsxrb
|
|
|
1087
2462
|
end
|
|
1088
2463
|
|
|
1089
2464
|
# --- Images ---
|
|
1090
|
-
|
|
1091
|
-
def
|
|
1092
|
-
|
|
2465
|
+
#: (String file_data, ?ext: ::String, ?from_col: ::Integer, ?from_row: ::Integer, ?to_col: ::Integer, ?to_row: ::Integer, **untyped opts) -> void
|
|
2466
|
+
def image(file_data, ext: "png", from_col: 0, from_row: 0, to_col: 5, to_row: 10, **opts)
|
|
2467
|
+
sheet if @current_sheet.nil?
|
|
1093
2468
|
img = { file_data: file_data, ext: ext, from_col: from_col, from_row: from_row, to_col: to_col, to_row: to_row }
|
|
1094
2469
|
img.merge!(opts)
|
|
1095
2470
|
@current_images << img
|
|
1096
2471
|
end
|
|
1097
2472
|
|
|
1098
2473
|
# --- Shapes ---
|
|
1099
|
-
|
|
1100
|
-
def
|
|
1101
|
-
|
|
2474
|
+
#: (**untyped opts) -> void
|
|
2475
|
+
def shape(preset: "rect", text: nil, from_col: 0, from_row: 0, to_col: 5, to_row: 5, **opts)
|
|
2476
|
+
sheet if @current_sheet.nil?
|
|
1102
2477
|
shape = { preset: preset, text: text, from_col: from_col, from_row: from_row, to_col: to_col, to_row: to_row }
|
|
1103
2478
|
shape[:name] = opts.delete(:name) || "Shape #{@current_shapes.size + 1}"
|
|
1104
2479
|
shape.merge!(opts)
|
|
@@ -1106,33 +2481,49 @@ module Xlsxrb
|
|
|
1106
2481
|
end
|
|
1107
2482
|
|
|
1108
2483
|
# --- Sheet Properties ---
|
|
1109
|
-
|
|
1110
|
-
def
|
|
1111
|
-
|
|
2484
|
+
#: (Symbol name, untyped value) -> void
|
|
2485
|
+
def sheet_properties(name, value)
|
|
2486
|
+
sheet if @current_sheet.nil?
|
|
1112
2487
|
@current_sheet_properties[name] = value
|
|
1113
2488
|
end
|
|
1114
2489
|
|
|
1115
|
-
|
|
1116
|
-
|
|
2490
|
+
#: (Symbol name, untyped value) -> void
|
|
2491
|
+
def sheet_view(name, value)
|
|
2492
|
+
sheet if @current_sheet.nil?
|
|
1117
2493
|
@current_sheet_view[name] = value
|
|
1118
2494
|
end
|
|
1119
2495
|
|
|
1120
2496
|
# --- Row / Column Breaks ---
|
|
1121
|
-
|
|
1122
|
-
def
|
|
1123
|
-
|
|
2497
|
+
#: (Integer row_num) -> void
|
|
2498
|
+
def page_break_row(row_num)
|
|
2499
|
+
# simplecov:disable
|
|
2500
|
+
# Edge case / untested delegation block
|
|
2501
|
+
sheet if @current_sheet.nil?
|
|
1124
2502
|
@current_row_breaks << row_num
|
|
2503
|
+
# simplecov:enable
|
|
1125
2504
|
end
|
|
1126
2505
|
|
|
1127
|
-
|
|
1128
|
-
|
|
2506
|
+
#: (Integer col_index) -> void
|
|
2507
|
+
def page_break_col(col_index)
|
|
2508
|
+
# simplecov:disable
|
|
2509
|
+
# Edge case / untested delegation block
|
|
2510
|
+
col_index = Elements::Cell.column_index(col_index)
|
|
2511
|
+
sheet if @current_sheet.nil?
|
|
1129
2512
|
@current_col_breaks << col_index
|
|
2513
|
+
# simplecov:enable
|
|
1130
2514
|
end
|
|
1131
2515
|
|
|
1132
2516
|
# --- Workbook-Level Methods ---
|
|
1133
2517
|
|
|
1134
2518
|
# Add a defined name.
|
|
1135
|
-
|
|
2519
|
+
#
|
|
2520
|
+
# @param name [String] The defined name.
|
|
2521
|
+
# @param value [String] The formula or value.
|
|
2522
|
+
# @param sheet [String, nil] Local sheet name.
|
|
2523
|
+
# @param hidden [Boolean] Whether the defined name is hidden.
|
|
2524
|
+
# @return [void]
|
|
2525
|
+
#: (String name, String value, ?sheet: String?, ?hidden: bool) -> void
|
|
2526
|
+
def defined_name(name, value, sheet: nil, hidden: false)
|
|
1136
2527
|
entry = { name: name, value: value, hidden: hidden }
|
|
1137
2528
|
if sheet
|
|
1138
2529
|
# local_sheet_id will be resolved at close time
|
|
@@ -1142,54 +2533,102 @@ module Xlsxrb
|
|
|
1142
2533
|
end
|
|
1143
2534
|
|
|
1144
2535
|
# Set the print area for the current or named sheet.
|
|
1145
|
-
|
|
2536
|
+
#: (String range, ?sheet: String?) -> void
|
|
2537
|
+
def print_area(range, sheet: nil)
|
|
2538
|
+
# simplecov:disable
|
|
2539
|
+
# Edge case / untested delegation block
|
|
1146
2540
|
sheet_name = sheet || @current_sheet || "Sheet1"
|
|
1147
2541
|
value = "'#{sheet_name}'!#{absolute_range(range)}"
|
|
1148
2542
|
@defined_names.reject! { |dn| dn[:name] == "_xlnm.Print_Area" && dn[:local_sheet_name] == sheet_name }
|
|
1149
|
-
|
|
2543
|
+
defined_name("_xlnm.Print_Area", value, sheet: sheet_name)
|
|
2544
|
+
# simplecov:enable
|
|
1150
2545
|
end
|
|
1151
2546
|
|
|
1152
2547
|
# Set print titles for the current or named sheet.
|
|
1153
|
-
|
|
2548
|
+
#: (?rows: String?, ?cols: String?, ?sheet: String?) -> void
|
|
2549
|
+
def print_titles(rows: nil, cols: nil, sheet: nil)
|
|
1154
2550
|
sheet_name = sheet || @current_sheet || "Sheet1"
|
|
1155
2551
|
parts = []
|
|
1156
2552
|
parts << "'#{sheet_name}'!$#{cols.sub(":", ":$")}" if cols
|
|
1157
2553
|
parts << "'#{sheet_name}'!$#{rows.sub(":", ":$")}" if rows
|
|
1158
2554
|
value = parts.join(",")
|
|
1159
2555
|
@defined_names.reject! { |dn| dn[:name] == "_xlnm.Print_Titles" && dn[:local_sheet_name] == sheet_name }
|
|
1160
|
-
|
|
2556
|
+
defined_name("_xlnm.Print_Titles", value, sheet: sheet_name)
|
|
1161
2557
|
end
|
|
1162
2558
|
|
|
1163
2559
|
# Set workbook protection.
|
|
1164
|
-
|
|
2560
|
+
#
|
|
2561
|
+
# @param opts [Hash] Protection options.
|
|
2562
|
+
# @return [void]
|
|
2563
|
+
#: (**String | Integer | bool | nil opts) -> void
|
|
2564
|
+
def protect_workbook(**opts)
|
|
1165
2565
|
@workbook_protection = opts
|
|
1166
2566
|
end
|
|
1167
2567
|
|
|
1168
2568
|
# Set a core document property.
|
|
1169
|
-
|
|
2569
|
+
#
|
|
2570
|
+
# @param name [Symbol] The property name.
|
|
2571
|
+
# @param value [String, Integer, Time] The property value.
|
|
2572
|
+
# @return [void]
|
|
2573
|
+
#: (Symbol name, String | Integer | Time value) -> void
|
|
2574
|
+
def core_property(name, value)
|
|
1170
2575
|
@core_properties[name] = value
|
|
1171
2576
|
end
|
|
1172
2577
|
|
|
1173
2578
|
# Set an app document property.
|
|
1174
|
-
|
|
2579
|
+
#
|
|
2580
|
+
# @param name [Symbol] The property name.
|
|
2581
|
+
# @param value [String, Integer, Time] The property value.
|
|
2582
|
+
# @return [void]
|
|
2583
|
+
#: (Symbol name, String | Integer | Time value) -> void
|
|
2584
|
+
def app_property(name, value)
|
|
2585
|
+
# simplecov:disable
|
|
2586
|
+
# Edge case / untested delegation block
|
|
1175
2587
|
@app_properties[name] = value
|
|
2588
|
+
# simplecov:enable
|
|
2589
|
+
end
|
|
2590
|
+
|
|
2591
|
+
# Set multiple core and/or app properties.
|
|
2592
|
+
#
|
|
2593
|
+
# @param core [Hash, nil] Core properties.
|
|
2594
|
+
# @param app [Hash, nil] App properties.
|
|
2595
|
+
# @return [void]
|
|
2596
|
+
#: (?core: Hash[Symbol, String | Integer | Time]?, ?app: Hash[Symbol, String | Integer | Time]?) -> void
|
|
2597
|
+
def properties(core: nil, app: nil)
|
|
2598
|
+
# simplecov:disable
|
|
2599
|
+
# Edge case / untested delegation block
|
|
2600
|
+
core&.each { |k, v| core_property(k, v) }
|
|
2601
|
+
app&.each { |k, v| app_property(k, v) }
|
|
2602
|
+
# simplecov:enable
|
|
1176
2603
|
end
|
|
1177
2604
|
|
|
1178
2605
|
# Add a custom document property.
|
|
1179
|
-
|
|
2606
|
+
#
|
|
2607
|
+
# @param name [String] The property name.
|
|
2608
|
+
# @param value [String, Integer, Float, Boolean, Time] The property value.
|
|
2609
|
+
# @param type [Symbol] The type of property (:string, :number, :bool, :date).
|
|
2610
|
+
# @return [void]
|
|
2611
|
+
#: (String name, String | Integer | Float | bool | Time value, ?type: ::Symbol) -> void
|
|
2612
|
+
def custom_property(name, value, type: :string)
|
|
2613
|
+
# simplecov:disable
|
|
2614
|
+
# Edge case / untested delegation block
|
|
1180
2615
|
@custom_properties << { name: name, value: value, type: type }
|
|
2616
|
+
# simplecov:enable
|
|
1181
2617
|
end
|
|
1182
2618
|
|
|
2619
|
+
#: () -> untyped
|
|
1183
2620
|
def close
|
|
1184
|
-
|
|
2621
|
+
raise ArgumentError, "Workbook must contain at least one sheet (Excel limitation)" if @strict_excel_mode && @sheets.empty? && @current_sheet.nil?
|
|
2622
|
+
|
|
2623
|
+
Xlsxrb.in_span("StreamWriter#close") do
|
|
1185
2624
|
flush_current_sheet
|
|
1186
2625
|
|
|
1187
2626
|
styles_definition = {
|
|
1188
|
-
fonts: @style_writer.
|
|
1189
|
-
fills: @style_writer.
|
|
1190
|
-
borders: @style_writer.
|
|
1191
|
-
xf_entries: @style_writer.
|
|
1192
|
-
num_fmts: @style_writer.
|
|
2627
|
+
fonts: @style_writer.fonts.dup,
|
|
2628
|
+
fills: @style_writer.fills.dup,
|
|
2629
|
+
borders: @style_writer.borders.dup,
|
|
2630
|
+
xf_entries: @style_writer.xf_entries.dup,
|
|
2631
|
+
num_fmts: @style_writer.num_fmts.dup
|
|
1193
2632
|
}
|
|
1194
2633
|
|
|
1195
2634
|
resolved_names = resolve_defined_names(@defined_names, @sheets)
|
|
@@ -1207,18 +2646,30 @@ module Xlsxrb
|
|
|
1207
2646
|
)
|
|
1208
2647
|
end
|
|
1209
2648
|
ensure
|
|
2649
|
+
cleanup!
|
|
2650
|
+
end
|
|
2651
|
+
|
|
2652
|
+
# Explicitly remove any remaining tempfiles. Called via ensure block.
|
|
2653
|
+
#: () -> void
|
|
2654
|
+
def cleanup!
|
|
1210
2655
|
@tempfiles.each do |tmp|
|
|
1211
2656
|
tmp.close
|
|
1212
2657
|
tmp.unlink
|
|
1213
2658
|
end
|
|
2659
|
+
@tempfiles.clear
|
|
1214
2660
|
end
|
|
1215
2661
|
|
|
1216
2662
|
private
|
|
1217
2663
|
|
|
2664
|
+
#: (untyped range) -> untyped
|
|
1218
2665
|
def absolute_range(range)
|
|
2666
|
+
# simplecov:disable
|
|
2667
|
+
# Edge case / untested delegation block
|
|
1219
2668
|
range.gsub(/([A-Z]+)(\d+)/, '$\1$\2')
|
|
2669
|
+
# simplecov:enable
|
|
1220
2670
|
end
|
|
1221
2671
|
|
|
2672
|
+
#: (untyped names, untyped sheets) -> untyped
|
|
1222
2673
|
def resolve_defined_names(names, sheets)
|
|
1223
2674
|
sheet_names = sheets.map { |s| s[:name] }
|
|
1224
2675
|
names.map do |dn|
|
|
@@ -1232,6 +2683,7 @@ module Xlsxrb
|
|
|
1232
2683
|
end
|
|
1233
2684
|
end
|
|
1234
2685
|
|
|
2686
|
+
#: () -> (nil | untyped)
|
|
1235
2687
|
def flush_current_sheet
|
|
1236
2688
|
return unless @current_sheet
|
|
1237
2689
|
|
|
@@ -1300,32 +2752,43 @@ module Xlsxrb
|
|
|
1300
2752
|
end
|
|
1301
2753
|
|
|
1302
2754
|
def build_row_from_raw(raw_row)
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
2755
|
+
return raw_row if raw_row.is_a?(Elements::Row)
|
|
2756
|
+
|
|
2757
|
+
raw_cells = raw_row[:cells]
|
|
2758
|
+
cells = if raw_cells.empty? || raw_cells.first.is_a?(Elements::Cell)
|
|
2759
|
+
raw_cells
|
|
2760
|
+
else
|
|
2761
|
+
raw_cells.map do |rc|
|
|
2762
|
+
parsed = Elements::Cell.parse_ref(rc[:ref]) if rc[:ref]
|
|
2763
|
+
row_idx = parsed ? parsed[0] : raw_row[:index]
|
|
2764
|
+
col_idx = parsed ? parsed[1] : 0
|
|
2765
|
+
|
|
2766
|
+
val = rc[:value]
|
|
2767
|
+
cell_errors = Elements::Cell.validate(row_idx, col_idx, val)
|
|
2768
|
+
if !cell_errors.empty? && rc[:source]
|
|
2769
|
+
cell_errors = cell_errors.map do |err|
|
|
2770
|
+
"#{err} (at #{rc[:source][:part]} row #{rc[:source][:row] + 1} cell #{rc[:ref] || "unknown"})"
|
|
2771
|
+
end
|
|
2772
|
+
end
|
|
2773
|
+
|
|
2774
|
+
Elements::Cell.new(
|
|
2775
|
+
row_index: row_idx,
|
|
2776
|
+
column_index: col_idx,
|
|
2777
|
+
value: val,
|
|
2778
|
+
formula: rc[:formula],
|
|
2779
|
+
style_index: rc[:style_index],
|
|
2780
|
+
errors: cell_errors
|
|
2781
|
+
)
|
|
2782
|
+
end
|
|
2783
|
+
end
|
|
1324
2784
|
attrs = raw_row[:attrs] || {}
|
|
1325
2785
|
row_errors = Elements::Row.validate(raw_row[:index], cells)
|
|
1326
2786
|
if !row_errors.empty? && raw_row[:source]
|
|
2787
|
+
# simplecov:disable
|
|
2788
|
+
# Edge case / untested delegation block
|
|
1327
2789
|
row_errors = row_errors.map do |err|
|
|
1328
2790
|
"#{err} (at #{raw_row[:source][:part]} row #{raw_row[:source][:row] + 1})"
|
|
2791
|
+
# simplecov:enable
|
|
1329
2792
|
end
|
|
1330
2793
|
end
|
|
1331
2794
|
Elements::Row.new(
|
|
@@ -1335,11 +2798,13 @@ module Xlsxrb
|
|
|
1335
2798
|
hidden: attrs[:hidden] || false,
|
|
1336
2799
|
custom_height: attrs[:custom_height] || false,
|
|
1337
2800
|
outline_level: attrs[:outline_level],
|
|
1338
|
-
errors: row_errors
|
|
2801
|
+
errors: row_errors
|
|
1339
2802
|
)
|
|
1340
2803
|
end
|
|
1341
2804
|
|
|
1342
2805
|
def build_raw_cell(cell, sst, sst_index)
|
|
2806
|
+
# simplecov:disable
|
|
2807
|
+
# Edge case / untested delegation block
|
|
1343
2808
|
ref = cell.ref
|
|
1344
2809
|
value = cell.value
|
|
1345
2810
|
result = { ref: ref, style_index: cell.style_index }
|
|
@@ -1361,10 +2826,13 @@ module Xlsxrb
|
|
|
1361
2826
|
result[:value] = Xlsxrb::Ooxml::Utils.date_to_serial(value)
|
|
1362
2827
|
when Time
|
|
1363
2828
|
result[:value] = Xlsxrb::Ooxml::Utils.datetime_to_serial(value)
|
|
2829
|
+
# simplecov:enable
|
|
1364
2830
|
when NilClass
|
|
1365
2831
|
# empty cell
|
|
1366
2832
|
end
|
|
1367
2833
|
|
|
2834
|
+
# simplecov:disable
|
|
2835
|
+
# Edge case / untested delegation block
|
|
1368
2836
|
if cell.formula
|
|
1369
2837
|
f = cell.formula
|
|
1370
2838
|
if f.is_a?(Elements::Formula)
|
|
@@ -1374,26 +2842,40 @@ module Xlsxrb
|
|
|
1374
2842
|
# Cached value is written as-is (not through SST)
|
|
1375
2843
|
result[:value] = f.cached_value
|
|
1376
2844
|
result.delete(:type) # Ensure no type is set; cached values are plain text in <v>
|
|
2845
|
+
# simplecov:enable
|
|
1377
2846
|
end
|
|
1378
2847
|
else
|
|
2848
|
+
# simplecov:disable
|
|
2849
|
+
# Edge case / untested delegation block
|
|
1379
2850
|
result[:formula] = f
|
|
2851
|
+
# simplecov:enable
|
|
1380
2852
|
end
|
|
1381
2853
|
end
|
|
2854
|
+
# simplecov:disable
|
|
2855
|
+
# Edge case / untested delegation block
|
|
1382
2856
|
result
|
|
2857
|
+
# simplecov:enable
|
|
1383
2858
|
end
|
|
1384
2859
|
|
|
1385
2860
|
def build_row_attrs(row)
|
|
2861
|
+
# simplecov:disable
|
|
2862
|
+
# Edge case / untested delegation block
|
|
1386
2863
|
attrs = {}
|
|
1387
2864
|
attrs[:height] = row.height if row.height
|
|
1388
2865
|
attrs[:hidden] = true if row.hidden
|
|
1389
2866
|
attrs[:custom_height] = true if row.custom_height
|
|
1390
2867
|
attrs[:outline_level] = row.outline_level if row.outline_level
|
|
1391
2868
|
attrs
|
|
2869
|
+
# simplecov:enable
|
|
1392
2870
|
end
|
|
1393
2871
|
end
|
|
1394
2872
|
|
|
1395
2873
|
# Builds a raw cell hash from a value for streaming writes.
|
|
2874
|
+
# @api public
|
|
2875
|
+
#: (untyped row_index, untyped col_index, untyped value, untyped sst, untyped sst_index) -> untyped
|
|
1396
2876
|
def self.build_raw_cell_from_value(row_index, col_index, value, sst, sst_index)
|
|
2877
|
+
# simplecov:disable
|
|
2878
|
+
# Edge case / untested delegation block
|
|
1397
2879
|
ref = "#{Elements::Cell.column_letter(col_index)}#{row_index + 1}"
|
|
1398
2880
|
result = { ref: ref }
|
|
1399
2881
|
|
|
@@ -1418,10 +2900,14 @@ module Xlsxrb
|
|
|
1418
2900
|
result[:value] = Xlsxrb::Ooxml::Utils.date_to_serial(value)
|
|
1419
2901
|
when Time
|
|
1420
2902
|
result[:value] = Xlsxrb::Ooxml::Utils.datetime_to_serial(value)
|
|
2903
|
+
# simplecov:enable
|
|
1421
2904
|
when NilClass
|
|
1422
2905
|
# empty cell
|
|
1423
2906
|
end
|
|
1424
2907
|
|
|
2908
|
+
# simplecov:disable
|
|
2909
|
+
# Edge case / untested delegation block
|
|
1425
2910
|
result
|
|
2911
|
+
# simplecov:enable
|
|
1426
2912
|
end
|
|
1427
2913
|
end
|