summaryrefslogtreecommitdiffhomepage
path: root/lib/axlsx.rb
blob: 814900fdb35721df6c5b9540fe0614679ec45f8d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# frozen_string_literal: true

require 'htmlentities'
require 'axlsx/version'
require 'marcel'

require 'axlsx/util/simple_typed_list'
require 'axlsx/util/constants'
require 'axlsx/util/validators'
require 'axlsx/util/accessors'
require 'axlsx/util/serialized_attributes'
require 'axlsx/util/options_parser'
require 'axlsx/util/mime_type_utils'
require 'axlsx/util/buffered_zip_output_stream'
require 'axlsx/util/zip_command'

require 'axlsx/stylesheet/styles'

require 'axlsx/doc_props/app'
require 'axlsx/doc_props/core'
require 'axlsx/content_type/content_type'
require 'axlsx/rels/relationships'

require 'axlsx/drawing/drawing'
require 'axlsx/workbook/workbook'
require 'axlsx/package'
# required gems
require 'nokogiri'
require 'zip'

# core dependencies
require 'bigdecimal'
require 'set'
require 'time'

if Gem.loaded_specs.key?("axlsx_styler")
  raise StandardError, "Please remove `axlsx_styler` from your Gemfile, the associated functionality is now built-in to `caxlsx` directly."
end

# xlsx generation with charts, images, automated column width, customizable styles
# and full schema validation. Axlsx excels at helping you generate beautiful
# Office Open XML Spreadsheet documents without having to understand the entire
# ECMA specification. Check out the README for some examples of how easy it is.
# Best of all, you can validate your xlsx file before serialization so you know
# for sure that anything generated is going to load on your client's machine.
module Axlsx
  # I am a very big fan of activesupports instance_values method, but do not want to require nor include the entire
  # library just for this one method.
  #
  # Defining as a class method on Axlsx to refrain from monkeypatching Object for all users of this gem.
  def self.instance_values_for(object)
    object.instance_variables.to_h { |name| [name.to_s[1..], object.instance_variable_get(name)] }
  end

  # determines the cell range for the items provided
  def self.cell_range(cells, absolute = true)
    return "" unless cells.first.is_a? Cell

    first_cell, last_cell = cells.minmax_by(&:pos)
    reference = "#{first_cell.reference(absolute)}:#{last_cell.reference(absolute)}"
    if absolute
      escaped_name = first_cell.row.worksheet.name.gsub ''', "''"
      "'#{escaped_name}'!#{reference}"
    else
      reference
    end
  end

  # sorts the array of cells provided to start from the minimum x,y to
  # the maximum x.y#
  # @param [Array] cells
  # @return [Array]
  def self.sort_cells(cells)
    cells.sort_by(&:pos)
  end

  # global reference html entity encoding
  # @return [HtmlEntities]
  def self.coder
    @@coder ||= ::HTMLEntities.new
  end

  # returns the x, y position of a cell
  def self.name_to_indices(name)
    raise ArgumentError, 'invalid cell name' unless name.size > 1

    letters_str = name[/[A-Z]+/]

    # capitalization?!?
    v = letters_str.reverse.chars.reduce({ base: 1, i: 0 }) do |val, c|
      val[:i] += ((c.bytes.first - 64) * val[:base])

      val[:base] *= 26

      next val
    end

    col_index = (v[:i] - 1)

    numbers_str = name[/[1-9][0-9]*/]

    row_index = (numbers_str.to_i - 1)

    [col_index, row_index]
  end

  # converts the column index into alphabetical values.
  # @note This follows the standard spreadsheet convention of naming columns A to Z, followed by AA to AZ etc.
  # @return [String]
  def self.col_ref(index)
    # Every row will call this for each column / cell and so we can cache result and avoid lots of small object
    # allocations.
    @col_ref ||= {}
    @col_ref[index] ||= begin
      i = index
      chars = +''
      while i >= 26
        i, char = i.divmod(26)
        chars.prepend((char + 65).chr)
        i -= 1
      end
      chars.prepend((i + 65).chr)
      chars.freeze
    end
  end

  # converts the row index into string values.
  # @note The spreadsheet rows are 1-based and the passed in index is 0-based, so we add 1.
  # @return [String]
  def self.row_ref(index)
    @row_ref ||= {}
    @row_ref[index] ||= (index + 1).to_s.freeze
  end

  # @return [String] The alpha(column)numeric(row) reference for this sell.
  # @example Relative Cell Reference
  #   ws.rows.first.cells.first.r #=> "A1"
  def self.cell_r(c_index, r_index)
    col_ref(c_index) + row_ref(r_index)
  end

  # Creates an array of individual cell references based on an Excel reference range.
  # @param [String] range A cell range, for example A1:D5
  # @return [Array]
  def self.range_to_a(range)
    range =~ /^(\w+?\d+):(\w+?\d+)$/
    start_col, start_row = name_to_indices(::Regexp.last_match(1))
    end_col,   end_row   = name_to_indices(::Regexp.last_match(2))
    (start_row..end_row).to_a.map do |row_num|
      (start_col..end_col).to_a.map do |col_num|
        cell_r(col_num, row_num)
      end
    end
  end

  # performs the increadible feat of changing snake_case to CamelCase
  # @param [String] s The snake case string to camelize
  # @return [String]
  def self.camel(s = "", all_caps = true)
    s = s.to_s
    s = s.capitalize if all_caps
    s.gsub(/_(.)/) { ::Regexp.last_match(1).upcase }
  end

  # returns the provided string with all invalid control charaters
  # removed.
  # @param [String] str The string to process
  # @return [String]
  def self.sanitize(str)
    if str.frozen?
      str.delete(CONTROL_CHARS)
    else
      str.delete!(CONTROL_CHARS)
      str
    end
  end

  # If value is boolean return 1 or 0
  # else return the value
  # @param [Object] value The value to process
  # @return [Object]
  def self.booleanize(value)
    if BOOLEAN_VALUES.include?(value)
      value ? '1' : '0'
    else
      value
    end
  end

  # utility method for performing a deep merge on a Hash
  # @param [Hash] first_hash Hash to merge into
  # @param [Hash] second_hash Hash to be added
  def self.hash_deep_merge(first_hash, second_hash)
    first_hash.merge(second_hash) do |_key, this_val, other_val|
      if this_val.is_a?(Hash) && other_val.is_a?(Hash)
        Axlsx.hash_deep_merge(this_val, other_val)
      else
        other_val
      end
    end
  end

  # Instructs the serializer to not try to escape cell value input.
  # This will give you a huge speed bonus, but if you content has <, > or other xml character data
  # the workbook will be invalid and Excel will complain.
  def self.trust_input
    @trust_input ||= false
  end

  # @param[Boolean] trust_me A boolean value indicating if the cell value content is to be trusted
  # @return [Boolean]
  # @see Axlsx::trust_input
  def self.trust_input=(trust_me)
    @trust_input = trust_me
  end

  # Whether to treat values starting with an equals sign as formulas or as literal strings.
  # Allowing user-generated data to be interpreted as formulas is a security risk.
  # See https://www.owasp.org/index.php/CSV_Injection for details.
  # @return [Boolean]
  def self.escape_formulas
    !defined?(@escape_formulas) || @escape_formulas.nil? ? false : @escape_formulas
  end

  # Sets whether to treat values starting with an equals sign as formulas or as literal strings.
  # @param [Boolean] value The value to set.
  def self.escape_formulas=(value)
    Axlsx.validate_boolean(value)
    @escape_formulas = value
  end
end