caxlsx 3.0.4 → 3.1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1add2e0fe915a49d02b64fe4df1aac62541f75ed27c97d0b418cdea137077c6
4
- data.tar.gz: d334797649d8ea1382ff2809359b7455cbde69365ecf798de4a51f0cfce903ca
3
+ metadata.gz: dfea731879b2415d4ed0ac46abfbad17483d7e9f2c27802aef87a93823589af7
4
+ data.tar.gz: 709606e3d36fe21ac41a8f9381d4017c31103d6ce0a8686eb195a935c3626538
5
5
  SHA512:
6
- metadata.gz: b164b9fe978fbc8866c3dbeea317c50d93c8b6796eeeb3a6b5214e9c11be439034543e1c1caaf9b4ddbf8a23713398a1bb4c6c207107a77b47353e183f59a226
7
- data.tar.gz: c3b2eb3c86f0498ce1979bc3898a22baff4ca2e3eb0b9efe4751062fb2cb69ba6b0cfdce72c7366830627b5c912450303abceeb7ca81179781a4f865bd4d8186
6
+ metadata.gz: cd1fd47bfe33dafa4909960d326f81433b4fa3e18d1a6f6f99e7a13617eecc98f212e2a938788d53466a47b52a0a2325a8dfcb3a6c3b52def5325b75ead9d402
7
+ data.tar.gz: bd9deebebd7cfc2222e64a8485ec9951ece3a69bb695fad5f4a790166324dab45134e835d02dc503e6c37e70b27d586db8bccf0c847448944f50ef37205e53d6
data/CHANGELOG.md CHANGED
@@ -1,7 +1,12 @@
1
1
  CHANGELOG
2
2
  ---------
3
3
 
4
- - **Unreleased**
4
+ - **March.27.21**: 3.1.0
5
+ - [PR #95](https://github.com/caxlsx/caxlsx/pull/95) - Replace mimemagic with marcel
6
+ - [PR #87](https://github.com/caxlsx/caxlsx/pull/87) - Implement :offset option for worksheet#add_row
7
+ - [PR #79](https://github.com/caxlsx/caxlsx/pull/79) - Add support for format in pivot tables
8
+ - [PR #77](https://github.com/caxlsx/caxlsx/pull/77) - Fix special characters in table header
9
+ - [PR #57](https://github.com/caxlsx/caxlsx/pull/57) - Deprecate using #serialize with boolean argument: Calls like `Package#serialize("name.xlsx", false)` should be replaced with `Package#serialize("name.xlsx", confirm_valid: false)`.
5
10
 
6
11
  - **January.5.21**: 3.0.4
7
12
  - [PR #72](https://github.com/caxlsx/caxlsx/pull/72) - Relax Ruby dependency to allow for Ruby 3. This required Travis to be upgraded from Ubuntu Trusty to Ubuntu Bionic. rbx-3 was dropped.
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
  [![Build Status](https://travis-ci.com/caxlsx/caxlsx.svg?branch=master)](https://travis-ci.com/caxlsx/caxlsx)
3
3
  [![Gem
4
4
  Version](https://badge.fury.io/rb/caxlsx.svg)](http://badge.fury.io/rb/caxlsx)
5
+ ![Total downloads](http://ruby-gem-downloads-badge.herokuapp.com/caxlsx?type=total)
6
+ ![Downloads for 3.0.4 (latest)](http://ruby-gem-downloads-badge.herokuapp.com/caxlsx/3.0.4?label=downloads%203.0.4)
5
7
 
6
8
  ## Notice: Community Axlsx Organization
7
9
 
data/lib/axlsx.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # encoding: UTF-8
2
2
  require 'htmlentities'
3
3
  require 'axlsx/version.rb'
4
- require 'mimemagic'
4
+ require 'marcel'
5
5
 
6
6
  require 'axlsx/util/simple_typed_list.rb'
7
7
  require 'axlsx/util/constants.rb'
data/lib/axlsx/package.rb CHANGED
@@ -74,13 +74,14 @@ module Axlsx
74
74
  # Serialize your workbook to disk as an xlsx document.
75
75
  #
76
76
  # @param [String] output The name of the file you want to serialize your package to
77
- # @param [Boolean] confirm_valid Validate the package prior to serialization.
78
- # @param [String, nil] zip_command When `nil`, `#serialize` with RubyZip to
77
+ # @param [Hash] options
78
+ # @option options [Boolean] :confirm_valid Validate the package prior to serialization.
79
+ # @option options [String] :zip_command When `nil`, `#serialize` with RubyZip to
79
80
  # zip the XLSX file contents. When a String, the provided zip command (e.g.,
80
81
  # "zip") is used to zip the file contents (may be faster for large files)
81
82
  # @return [Boolean] False if confirm_valid and validation errors exist. True if the package was serialized
82
83
  # @note A tremendous amount of effort has gone into ensuring that you cannot create invalid xlsx documents.
83
- # confirm_valid should be used in the rare case that you cannot open the serialized file.
84
+ # options[:confirm_valid] should be used in the rare case that you cannot open the serialized file.
84
85
  # @see Package#validate
85
86
  # @example
86
87
  # # This is how easy it is to create a valid xlsx file. Of course you might want to add a sheet or two, and maybe some data, styles and charts.
@@ -92,14 +93,15 @@ module Axlsx
92
93
  # p.serialize("example.xlsx")
93
94
  #
94
95
  # # Serialize to a file, using a system zip binary
95
- # p.serialize("example.xlsx", false, zip_command: "zip")
96
- # p.serialize("example.xlsx", false, zip_command: "/path/to/zip")
97
- # p.serialize("example.xlsx", false, zip_command: "zip -1")
96
+ # p.serialize("example.xlsx", zip_command: "zip", confirm_valid: false)
97
+ # p.serialize("example.xlsx", zip_command: "/path/to/zip")
98
+ # p.serialize("example.xlsx", zip_command: "zip -1")
98
99
  #
99
100
  # # Serialize to a stream
100
101
  # s = p.to_stream()
101
102
  # File.open('example_streamed.xlsx', 'w') { |f| f.write(s.read) }
102
- def serialize(output, confirm_valid=false, zip_command: nil)
103
+ def serialize(output, options = {}, secondary_options = nil)
104
+ confirm_valid, zip_command = parse_serialize_options(options, secondary_options)
103
105
  return false unless !confirm_valid || self.validate.empty?
104
106
  zip_provider = if zip_command
105
107
  ZipCommand.new(zip_command)
@@ -359,5 +361,28 @@ module Axlsx
359
361
  rels.lock
360
362
  rels
361
363
  end
364
+
365
+ # Parse the arguments of `#serialize`
366
+ # @return [Boolean, (String or nil)] Returns an array where the first value is
367
+ # `confirm_valid` and the second is the `zip_command`.
368
+ # @private
369
+ def parse_serialize_options(options, secondary_options)
370
+ if secondary_options
371
+ warn "[DEPRECATION] Axlsx::Package#serialize with 3 arguments is deprecated. " +
372
+ "Use keyword args instead e.g., package.serialize(output, confirm_valid: false, zip_command: 'zip')"
373
+ end
374
+ if options.is_a?(Hash)
375
+ options.merge!(secondary_options || {})
376
+ invalid_keys = options.keys - [:confirm_valid, :zip_command]
377
+ if invalid_keys.any?
378
+ raise ArgumentError.new("Invalid keyword arguments: #{invalid_keys}")
379
+ end
380
+ [options.fetch(:confirm_valid, false), options.fetch(:zip_command, nil)]
381
+ else
382
+ warn "[DEPRECATION] Axlsx::Package#serialize with confirm_valid as a boolean is deprecated. " +
383
+ "Use keyword args instead e.g., package.serialize(output, confirm_valid: false)"
384
+ parse_serialize_options((secondary_options || {}).merge(confirm_valid: options), nil)
385
+ end
386
+ end
362
387
  end
363
388
  end
@@ -5,7 +5,7 @@ module Axlsx
5
5
  # @param [String] v File path
6
6
  # @return [String] File mime type
7
7
  def self.get_mime_type(v)
8
- MimeMagic.by_magic(File.open(v)).to_s
8
+ Marcel::MimeType.for(Pathname.new(v))
9
9
  end
10
10
  end
11
11
  end
data/lib/axlsx/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module Axlsx
2
2
 
3
3
  # The current version
4
- VERSION = "3.0.4"
4
+ VERSION = "3.1.0"
5
5
  end
@@ -111,8 +111,12 @@ module Axlsx
111
111
  if data_field.is_a? String
112
112
  data_field = {:ref => data_field}
113
113
  end
114
- data_field.values.each do |value|
115
- DataTypeValidator.validate "#{self.class}.data[]", [String], value
114
+ data_field.each do |key, value|
115
+ if key == :num_fmt
116
+ DataTypeValidator.validate "#{self.class}.data[]", [Integer], value
117
+ else
118
+ DataTypeValidator.validate "#{self.class}.data[]", [String], value
119
+ end
116
120
  end
117
121
  @data << data_field
118
122
  end
@@ -212,6 +216,7 @@ module Axlsx
212
216
  data.each do |datum_value|
213
217
  # The correct name prefix in ["Sum","Average", etc...]
214
218
  str << "<dataField name='#{(datum_value[:subtotal]||'')} of #{datum_value[:ref]}' fld='#{header_index_of(datum_value[:ref])}' baseField='0' baseItem='0'"
219
+ str << " numFmtId='#{datum_value[:num_fmt]}'" if datum_value[:num_fmt]
215
220
  str << " subtotal='#{datum_value[:subtotal]}' " if datum_value[:subtotal]
216
221
  str << "/>"
217
222
  end
@@ -53,7 +53,7 @@ module Axlsx
53
53
  str << '</cacheSource>'
54
54
  str << ( '<cacheFields count="' << pivot_table.header_cells_count.to_s << '">')
55
55
  pivot_table.header_cells.each do |cell|
56
- str << ( '<cacheField name="' << cell.value << '" numFmtId="0">')
56
+ str << ( '<cacheField name="' << cell.clean_value << '" numFmtId="0">')
57
57
  str << '<sharedItems count="0">'
58
58
  str << '</sharedItems>'
59
59
  str << '</cacheField>'
@@ -25,11 +25,12 @@ module Axlsx
25
25
  # @option options [Array, Symbol] types
26
26
  # @option options [Array, Integer] style
27
27
  # @option options [Float] height the row's height (in points)
28
+ # @option options [Integer] offset - add empty columns before values
28
29
  # @see Row#array_to_cells
29
30
  # @see Cell
30
31
  def initialize(worksheet, values=[], options={})
31
32
  self.worksheet = worksheet
32
- super(Cell, nil, values.size)
33
+ super(Cell, nil, values.size + options[:offset].to_i)
33
34
  self.height = options.delete(:height)
34
35
  worksheet.rows << self
35
36
  array_to_cells(values, options)
@@ -147,14 +148,15 @@ module Axlsx
147
148
  # @option options [Array, Integer] style
148
149
  def array_to_cells(values, options={})
149
150
  DataTypeValidator.validate :array_to_cells, Array, values
150
- types, style, formula_values, escape_formulas = options.delete(:types), options.delete(:style), options.delete(:formula_values), options.delete(:escape_formulas)
151
+ types, style, formula_values, escape_formulas, offset = options.delete(:types), options.delete(:style), options.delete(:formula_values), options.delete(:escape_formulas), options.delete(:offset)
152
+ offset.to_i.times { |index| self[index] = Cell.new(self) } if offset
151
153
  values.each_with_index do |value, index|
152
154
  options[:style] = style.is_a?(Array) ? style[index] : style if style
153
155
  options[:type] = types.is_a?(Array) ? types[index] : types if types
154
156
  options[:escape_formulas] = escape_formulas.is_a?(Array) ? escape_formulas[index] : escape_formulas if escape_formulas
155
157
  options[:formula_value] = formula_values[index] if formula_values.is_a?(Array)
156
158
 
157
- self[index] = Cell.new(self, value, options)
159
+ self[index + offset.to_i] = Cell.new(self, value, options)
158
160
  end
159
161
  end
160
162
  end
@@ -80,7 +80,7 @@ module Axlsx
80
80
  str << ('<autoFilter ref="' << @ref << '"/>')
81
81
  str << ('<tableColumns count="' << header_cells.length.to_s << '">')
82
82
  header_cells.each_with_index do |cell,index|
83
- str << ('<tableColumn id ="' << (index+1).to_s << '" name="' << cell.value << '"/>')
83
+ str << ('<tableColumn id ="' << (index+1).to_s << '" name="' << cell.clean_value << '"/>')
84
84
  end
85
85
  str << '</tableColumns>'
86
86
  table_style_info.to_xml_string(str)
@@ -393,6 +393,9 @@ module Axlsx
393
393
  # @example - specify whether a certain cells in a row should escape formulas or not
394
394
  # ws.add_row ['=IF(2+2=4,4,5)', '=IF(13+13=4,4,5)'], :escape_formulas=>[true, false]
395
395
  #
396
+ # @example - add a column offset when adding a row (inserts 'n' blank, unstyled columns before data)
397
+ # ws.add_row ['I wish', 'for a fish', 'on my fish wish dish'], offset: 3
398
+ #
396
399
  # @see Worksheet#column_widths
397
400
  # @return [Row]
398
401
  # @option options [Array] values
@@ -400,6 +403,7 @@ module Axlsx
400
403
  # @option options [Array, Integer] style
401
404
  # @option options [Array] widths each member of the widths array will affect how auto_fit behavies.
402
405
  # @option options [Float] height the row's height (in points)
406
+ # @option options [Integer] offset - add empty columns before values
403
407
  # @option options [Array, Boolean] escape_formulas - Whether to treat a value starting with an equal
404
408
  # sign as formula (default) or as simple string.
405
409
  # Allowing user generated data to be interpreted as formulas can be dangerous
data/test/tc_helper.rb CHANGED
@@ -8,5 +8,3 @@ end
8
8
  require 'test/unit'
9
9
  require "timecop"
10
10
  require "axlsx.rb"
11
- # MIME detection for Microsoft Office 2007+ formats
12
- require 'mimemagic/overlay'
data/test/tc_package.rb CHANGED
@@ -129,25 +129,28 @@ class TestPackage < Test::Unit::TestCase
129
129
  def test_serialization
130
130
  @package.serialize(@fname)
131
131
  assert_zip_file_matches_package(@fname, @package)
132
+ assert_created_with_rubyzip(@fname, @package)
132
133
  File.delete(@fname)
133
134
  end
134
135
 
135
136
  def test_serialization_with_zip_command
136
- @package.serialize(@fname, false, zip_command: "zip")
137
+ @package.serialize(@fname, zip_command: "zip")
137
138
  assert_zip_file_matches_package(@fname, @package)
139
+ assert_created_with_zip_command(@fname, @package)
138
140
  File.delete(@fname)
139
141
  end
140
142
 
141
143
  def test_serialization_with_zip_command_and_absolute_path
142
144
  fname = "#{Dir.tmpdir}/#{@fname}"
143
- @package.serialize(fname, false, zip_command: "zip")
145
+ @package.serialize(fname, zip_command: "zip")
144
146
  assert_zip_file_matches_package(fname, @package)
147
+ assert_created_with_zip_command(fname, @package)
145
148
  File.delete(fname)
146
149
  end
147
150
 
148
151
  def test_serialization_with_invalid_zip_command
149
152
  assert_raises Axlsx::ZipCommand::ZipError do
150
- @package.serialize(@fname, false, zip_command: "invalid_zip")
153
+ @package.serialize(@fname, zip_command: "invalid_zip")
151
154
  end
152
155
  end
153
156
 
@@ -156,6 +159,53 @@ class TestPackage < Test::Unit::TestCase
156
159
  package.send(:parts).each{ |part| zf.get_entry(part[:entry]) }
157
160
  end
158
161
 
162
+ def assert_created_with_rubyzip(fname, package)
163
+ assert_equal 2098, get_mtime(fname, package).year, "XLSX files created with RubyZip have 2098 as the file mtime"
164
+ end
165
+
166
+ def assert_created_with_zip_command(fname, package)
167
+ assert_equal Time.now.utc.year, get_mtime(fname, package).year, "XLSX files created with a zip command have the current year as the file mtime"
168
+ end
169
+
170
+ def get_mtime(fname, package)
171
+ zf = Zip::File.open(fname)
172
+ part = package.send(:parts).first
173
+ entry = zf.get_entry(part[:entry])
174
+ entry.mtime.utc
175
+ end
176
+
177
+ def test_serialization_with_deprecated_argument
178
+ warnings = capture_warnings do
179
+ @package.serialize(@fname, false)
180
+ end
181
+ assert_equal 1, warnings.size
182
+ assert_includes warnings.first, "confirm_valid as a boolean is deprecated"
183
+ File.delete(@fname)
184
+ end
185
+
186
+ def test_serialization_with_deprecated_three_arguments
187
+ warnings = capture_warnings do
188
+ @package.serialize(@fname, true, zip_command: "zip")
189
+ end
190
+ assert_zip_file_matches_package(@fname, @package)
191
+ assert_created_with_zip_command(@fname, @package)
192
+ assert_equal 2, warnings.size
193
+ assert_includes warnings.first, "with 3 arguments is deprecated"
194
+ File.delete(@fname)
195
+ end
196
+
197
+ def capture_warnings(&block)
198
+ original_warn = Kernel.instance_method(:warn)
199
+ warnings = []
200
+ Kernel.send(:define_method, :warn) { |string| warnings << string }
201
+ block.call
202
+ original_verbose = $VERBOSE
203
+ $VERBOSE = nil
204
+ Kernel.send(:define_method, :warn, original_warn)
205
+ $VERBOSE = original_verbose
206
+ warnings
207
+ end
208
+
159
209
  # See comment for Package#zip_entry_for_part
160
210
  def test_serialization_creates_identical_files_at_any_time_if_created_at_is_set
161
211
  @package.core.created = Time.now
@@ -178,7 +228,7 @@ class TestPackage < Test::Unit::TestCase
178
228
  end
179
229
 
180
230
  def test_serialization_creates_files_with_excel_mime_type
181
- assert_equal(MimeMagic.by_magic(@package.to_stream).type,
231
+ assert_equal(Marcel::MimeType.for(@package.to_stream),
182
232
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
183
233
  end
184
234
 
@@ -132,4 +132,12 @@ class TestPivotTable < Test::Unit::TestCase
132
132
  end
133
133
  shared_test_pivot_table_xml_validity(pivot_table)
134
134
  end
135
+
136
+ def test_add_pivot_table_with_format_options_on_data_field
137
+ pivot_table = @ws.add_pivot_table('G5:G6', 'A1:E5') do |pt|
138
+ pt.data = [{:ref=>"Sales", :subtotal => 'sum', num_fmt: 4}]
139
+ end
140
+ doc = Nokogiri::XML(pivot_table.to_xml_string)
141
+ assert_equal('4', doc.at_css('dataFields dataField')['numFmtId'], 'adding format options to pivot_table')
142
+ end
135
143
  end
@@ -51,4 +51,12 @@ class TestPivotTableCacheDefinition < Test::Unit::TestCase
51
51
  assert(errors.empty?, "error free validation")
52
52
  end
53
53
 
54
+ def test_to_xml_string_for_special_characters
55
+ cell = @ws.rows.first.cells.first
56
+ cell.value = "&><'\""
57
+
58
+ doc = Nokogiri::XML(@cache_definition.to_xml_string)
59
+ errors = doc.errors
60
+ assert(errors.empty?, "invalid xml: #{errors.map(&:to_s).join(', ')}")
61
+ end
54
62
  end
@@ -136,4 +136,25 @@ class TestRow < Test::Unit::TestCase
136
136
  assert_equal(r_s_xml.xpath(".//row[@r=1][@ht=20][@customHeight=1]").size, 1)
137
137
  end
138
138
 
139
+ def test_offsets
140
+ offset = 3
141
+ values = [1,2,3,4,5]
142
+ r = @ws.add_row(values, offset: offset, style: 1)
143
+ r.cells.each_with_index do |c, index|
144
+ assert_equal(c.style, index < offset ? 0 : 1)
145
+ assert_equal(c.value, index < offset ? nil : values[index - offset])
146
+ end
147
+ end
148
+
149
+ def test_offsets_with_styles
150
+ offset = 3
151
+ values = [1,2,3,4,5]
152
+ styles = (1..5).map{ @ws.workbook.styles.add_style }
153
+ r = @ws.add_row(values, offset: offset, style: styles)
154
+ r.cells.each_with_index do |c, index|
155
+ assert_equal(c.style, index < offset ? 0 : styles[index-offset])
156
+ assert_equal(c.value, index < offset ? nil : values[index - offset])
157
+ end
158
+ end
159
+
139
160
  end
@@ -64,4 +64,14 @@ class TestTable < Test::Unit::TestCase
64
64
  end
65
65
  assert(errors.empty?, "error free validation")
66
66
  end
67
+
68
+ def test_to_xml_string_for_special_characters
69
+ cell = @ws.rows.first.cells.first
70
+ cell.value = "&><'\""
71
+
72
+ table = @ws.add_table("A1:D5")
73
+ doc = Nokogiri::XML(table.to_xml_string)
74
+ errors = doc.errors
75
+ assert(errors.empty?, "invalid xml: #{errors.map(&:to_s).join(', ')}")
76
+ end
67
77
  end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: caxlsx
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.4
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Randy Morgan
8
8
  - Jurriaan Pruis
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-01-05 00:00:00.000000000 Z
12
+ date: 2021-03-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: nokogiri
@@ -72,19 +72,19 @@ dependencies:
72
72
  - !ruby/object:Gem::Version
73
73
  version: 4.3.4
74
74
  - !ruby/object:Gem::Dependency
75
- name: mimemagic
75
+ name: marcel
76
76
  requirement: !ruby/object:Gem::Requirement
77
77
  requirements:
78
78
  - - "~>"
79
79
  - !ruby/object:Gem::Version
80
- version: '0.3'
80
+ version: '1.0'
81
81
  type: :runtime
82
82
  prerelease: false
83
83
  version_requirements: !ruby/object:Gem::Requirement
84
84
  requirements:
85
85
  - - "~>"
86
86
  - !ruby/object:Gem::Version
87
- version: '0.3'
87
+ version: '1.0'
88
88
  - !ruby/object:Gem::Dependency
89
89
  name: yard
90
90
  requirement: !ruby/object:Gem::Requirement
@@ -452,7 +452,7 @@ homepage: https://github.com/caxlsx/caxlsx
452
452
  licenses:
453
453
  - MIT
454
454
  metadata: {}
455
- post_install_message:
455
+ post_install_message:
456
456
  rdoc_options: []
457
457
  require_paths:
458
458
  - lib
@@ -468,7 +468,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
468
468
  version: '0'
469
469
  requirements: []
470
470
  rubygems_version: 3.0.3
471
- signing_key:
471
+ signing_key:
472
472
  specification_version: 4
473
473
  summary: Excel OOXML (xlsx) with charts, styles, images and autowidth columns.
474
474
  test_files: