write_xlsx 1.12.3 → 1.15.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 +4 -4
- data/.rubocop.yml +22 -0
- data/Changes +23 -0
- data/README.md +1 -1
- data/lib/write_xlsx/chart/area.rb +2 -2
- data/lib/write_xlsx/chart/axis.rb +55 -32
- data/lib/write_xlsx/chart/axis_writer.rb +528 -0
- data/lib/write_xlsx/chart/bar.rb +2 -2
- data/lib/write_xlsx/chart/caption.rb +16 -9
- data/lib/write_xlsx/chart/chart_area.rb +121 -0
- data/lib/write_xlsx/chart/column.rb +2 -2
- data/lib/write_xlsx/chart/d_pt_point_writer.rb +14 -0
- data/lib/write_xlsx/chart/doughnut.rb +0 -3
- data/lib/write_xlsx/chart/formatting_writer.rb +652 -0
- data/lib/write_xlsx/chart/initialization.rb +100 -0
- data/lib/write_xlsx/chart/line.rb +4 -3
- data/lib/write_xlsx/chart/pie.rb +6 -2
- data/lib/write_xlsx/chart/radar.rb +2 -2
- data/lib/write_xlsx/chart/scatter.rb +4 -3
- data/lib/write_xlsx/chart/series.rb +35 -15
- data/lib/write_xlsx/chart/series_data.rb +132 -0
- data/lib/write_xlsx/chart/series_writer.rb +318 -0
- data/lib/write_xlsx/chart/settings.rb +226 -0
- data/lib/write_xlsx/chart/stock.rb +2 -2
- data/lib/write_xlsx/chart/table.rb +50 -0
- data/lib/write_xlsx/chart/xml_writer.rb +305 -0
- data/lib/write_xlsx/chart.rb +286 -2477
- data/lib/write_xlsx/chartsheet.rb +35 -83
- data/lib/write_xlsx/constants.rb +11 -0
- data/lib/write_xlsx/drawing.rb +5 -3
- data/lib/write_xlsx/format/alignment_state.rb +39 -0
- data/lib/write_xlsx/format/alignment_style.rb +92 -0
- data/lib/write_xlsx/format/border_state.rb +47 -0
- data/lib/write_xlsx/format/border_style.rb +116 -0
- data/lib/write_xlsx/format/fill_state.rb +26 -0
- data/lib/write_xlsx/format/fill_style.rb +52 -0
- data/lib/write_xlsx/format/font_state.rb +74 -0
- data/lib/write_xlsx/format/font_style.rb +172 -0
- data/lib/write_xlsx/format/format_state.rb +65 -0
- data/lib/write_xlsx/format/number_format_state.rb +20 -0
- data/lib/write_xlsx/format/number_format_style.rb +28 -0
- data/lib/write_xlsx/format/protection_state.rb +20 -0
- data/lib/write_xlsx/format/protection_style.rb +28 -0
- data/lib/write_xlsx/format.rb +1093 -426
- data/lib/write_xlsx/formats.rb +0 -2
- data/lib/write_xlsx/image_property.rb +4 -1
- data/lib/write_xlsx/inserted_chart.rb +1 -1
- data/lib/write_xlsx/object_positioning.rb +203 -0
- data/lib/write_xlsx/package/app.rb +3 -3
- data/lib/write_xlsx/package/button.rb +6 -2
- data/lib/write_xlsx/package/comments.rb +11 -3
- data/lib/write_xlsx/package/conditional_format.rb +7 -3
- data/lib/write_xlsx/package/content_types.rb +2 -2
- data/lib/write_xlsx/package/core.rb +2 -2
- data/lib/write_xlsx/package/custom.rb +3 -2
- data/lib/write_xlsx/package/metadata.rb +2 -2
- data/lib/write_xlsx/package/packager.rb +0 -3
- data/lib/write_xlsx/package/relationships.rb +2 -2
- data/lib/write_xlsx/package/rich_value.rb +4 -2
- data/lib/write_xlsx/package/rich_value_rel.rb +2 -2
- data/lib/write_xlsx/package/rich_value_structure.rb +2 -2
- data/lib/write_xlsx/package/rich_value_types.rb +3 -3
- data/lib/write_xlsx/package/shared_strings.rb +2 -2
- data/lib/write_xlsx/package/styles.rb +13 -9
- data/lib/write_xlsx/package/table.rb +8 -2
- data/lib/write_xlsx/package/theme.rb +0 -3
- data/lib/write_xlsx/package/vml.rb +2 -2
- data/lib/write_xlsx/page_setup.rb +192 -0
- data/lib/write_xlsx/shape.rb +97 -100
- data/lib/write_xlsx/sheets.rb +9 -4
- data/lib/write_xlsx/sparkline.rb +2 -2
- data/lib/write_xlsx/utility/cell_reference.rb +124 -0
- data/lib/write_xlsx/utility/chart_formatting.rb +262 -0
- data/lib/write_xlsx/utility/common.rb +44 -0
- data/lib/write_xlsx/utility/date_time.rb +113 -0
- data/lib/write_xlsx/utility/dimensions.rb +40 -0
- data/lib/write_xlsx/utility/drawing.rb +136 -0
- data/lib/write_xlsx/utility/rich_text.rb +184 -0
- data/lib/write_xlsx/utility/sheetname_quoting.rb +73 -0
- data/lib/write_xlsx/utility/string_width.rb +45 -0
- data/lib/write_xlsx/utility/url.rb +27 -0
- data/lib/write_xlsx/utility/xml_primitives.rb +32 -0
- data/lib/write_xlsx/version.rb +1 -1
- data/lib/write_xlsx/workbook/chart_data.rb +188 -0
- data/lib/write_xlsx/workbook/format_preparation.rb +199 -0
- data/lib/write_xlsx/workbook/initialization.rb +223 -0
- data/lib/write_xlsx/workbook/package_preparation.rb +231 -0
- data/lib/write_xlsx/workbook/workbook_writer.rb +164 -0
- data/lib/write_xlsx/workbook.rb +143 -981
- data/lib/write_xlsx/worksheet/asset_manager.rb +60 -0
- data/lib/write_xlsx/worksheet/autofilter.rb +390 -0
- data/lib/write_xlsx/worksheet/cell_data.rb +13 -6
- data/lib/write_xlsx/worksheet/cell_data_manager.rb +47 -0
- data/lib/write_xlsx/worksheet/cell_data_store.rb +61 -0
- data/lib/write_xlsx/worksheet/columns.rb +204 -0
- data/lib/write_xlsx/worksheet/comments_support.rb +61 -0
- data/lib/write_xlsx/worksheet/conditional_formats.rb +30 -0
- data/lib/write_xlsx/worksheet/data_validation.rb +9 -1
- data/lib/write_xlsx/worksheet/data_writing.rb +1017 -0
- data/lib/write_xlsx/worksheet/drawing_methods.rb +308 -0
- data/lib/write_xlsx/worksheet/drawing_preparation.rb +290 -0
- data/lib/write_xlsx/worksheet/drawing_relations.rb +76 -0
- data/lib/write_xlsx/worksheet/drawing_xml_writer.rb +50 -0
- data/lib/write_xlsx/worksheet/formatting.rb +418 -0
- data/lib/write_xlsx/worksheet/hyperlink.rb +9 -1
- data/lib/write_xlsx/worksheet/initialization.rb +146 -0
- data/lib/write_xlsx/worksheet/panes.rb +64 -0
- data/lib/write_xlsx/worksheet/print_options.rb +72 -0
- data/lib/write_xlsx/worksheet/protection.rb +65 -0
- data/lib/write_xlsx/worksheet/rich_text_helpers.rb +78 -0
- data/lib/write_xlsx/worksheet/row_col_sizing.rb +69 -0
- data/lib/write_xlsx/worksheet/rows.rb +84 -0
- data/lib/write_xlsx/worksheet/selection.rb +41 -0
- data/lib/write_xlsx/worksheet/xml_writer.rb +1246 -0
- data/lib/write_xlsx/worksheet.rb +376 -4530
- metadata +66 -4
- data/lib/write_xlsx/utility.rb +0 -986
- data/lib/write_xlsx/worksheet/page_setup.rb +0 -192
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Writexlsx
|
|
5
|
+
module Utility
|
|
6
|
+
module RichText
|
|
7
|
+
def underline_attributes(underline)
|
|
8
|
+
case underline
|
|
9
|
+
when 2
|
|
10
|
+
[%w[val double]]
|
|
11
|
+
when 33
|
|
12
|
+
[%w[val singleAccounting]]
|
|
13
|
+
when 34
|
|
14
|
+
[%w[val doubleAccounting]]
|
|
15
|
+
else
|
|
16
|
+
[] # Default to single underline.
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
#
|
|
21
|
+
# Convert user defined font values into private hash values.
|
|
22
|
+
#
|
|
23
|
+
def convert_font_args(params)
|
|
24
|
+
return unless params
|
|
25
|
+
|
|
26
|
+
font = params_to_font(params)
|
|
27
|
+
|
|
28
|
+
# Convert font size units.
|
|
29
|
+
font[:_size] *= 100 if font[:_size] && font[:_size] != 0
|
|
30
|
+
|
|
31
|
+
# Convert rotation into 60,000ths of a degree.
|
|
32
|
+
font[:_rotation] = 60_000 * font[:_rotation].to_i if ptrue?(font[:_rotation])
|
|
33
|
+
|
|
34
|
+
font
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def params_to_font(params)
|
|
38
|
+
{
|
|
39
|
+
_name: params[:name],
|
|
40
|
+
_color: params[:color],
|
|
41
|
+
_size: params[:size],
|
|
42
|
+
_bold: params[:bold],
|
|
43
|
+
_italic: params[:italic],
|
|
44
|
+
_underline: params[:underline],
|
|
45
|
+
_pitch_family: params[:pitch_family],
|
|
46
|
+
_charset: params[:charset],
|
|
47
|
+
_baseline: params[:baseline] || 0,
|
|
48
|
+
_rotation: params[:rotation]
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
#
|
|
53
|
+
# Get the font style attributes from a font hash.
|
|
54
|
+
#
|
|
55
|
+
def get_font_style_attributes(font)
|
|
56
|
+
return [] unless font
|
|
57
|
+
return [] unless font.respond_to?(:[])
|
|
58
|
+
|
|
59
|
+
attributes = []
|
|
60
|
+
attributes << ['sz', font[:_size]] if ptrue?(font[:_size])
|
|
61
|
+
attributes << ['b', font[:_bold]] if font[:_bold]
|
|
62
|
+
attributes << ['i', font[:_italic]] if font[:_italic]
|
|
63
|
+
attributes << %w[u sng] if font[:_underline]
|
|
64
|
+
|
|
65
|
+
# Turn off baseline when testing fonts that don't have it.
|
|
66
|
+
attributes << ['baseline', font[:_baseline]] if font[:_baseline] != -1
|
|
67
|
+
attributes
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
#
|
|
71
|
+
# Get the font latin attributes from a font hash.
|
|
72
|
+
#
|
|
73
|
+
def get_font_latin_attributes(font)
|
|
74
|
+
return [] unless font
|
|
75
|
+
return [] unless font.respond_to?(:[])
|
|
76
|
+
|
|
77
|
+
attributes = []
|
|
78
|
+
attributes << ['typeface', font[:_name]] if ptrue?(font[:_name])
|
|
79
|
+
attributes << ['pitchFamily', font[:_pitch_family]] if font[:_pitch_family]
|
|
80
|
+
attributes << ['charset', font[:_charset]] if font[:_charset]
|
|
81
|
+
|
|
82
|
+
attributes
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
#
|
|
86
|
+
# Write the <c:txPr> element.
|
|
87
|
+
#
|
|
88
|
+
def write_tx_pr(font, is_y_axis = nil) # :nodoc:
|
|
89
|
+
rotation = nil
|
|
90
|
+
rotation = font[:_rotation] if font && font.respond_to?(:[]) && font[:_rotation]
|
|
91
|
+
@writer.tag_elements('c:txPr') do
|
|
92
|
+
# Write the a:bodyPr element.
|
|
93
|
+
write_a_body_pr(rotation, is_y_axis)
|
|
94
|
+
# Write the a:lstStyle element.
|
|
95
|
+
write_a_lst_style
|
|
96
|
+
# Write the a:p element.
|
|
97
|
+
write_a_p_formula(font)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
#
|
|
102
|
+
# Write the <a:bodyPr> element.
|
|
103
|
+
#
|
|
104
|
+
def write_a_body_pr(rot, is_y_axis = nil) # :nodoc:
|
|
105
|
+
rot = -5400000 if !rot && ptrue?(is_y_axis)
|
|
106
|
+
attributes = []
|
|
107
|
+
if rot
|
|
108
|
+
if rot == 16_200_000
|
|
109
|
+
# 270 deg/stacked angle.
|
|
110
|
+
attributes << ['rot', 0]
|
|
111
|
+
attributes << %w[vert wordArtVert]
|
|
112
|
+
elsif rot == 16_260_000
|
|
113
|
+
# 271 deg/stacked angle.
|
|
114
|
+
attributes << ['rot', 0]
|
|
115
|
+
attributes << %w[vert eaVert]
|
|
116
|
+
else
|
|
117
|
+
attributes << ['rot', rot]
|
|
118
|
+
attributes << %w[vert horz]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
@writer.empty_tag('a:bodyPr', attributes)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
#
|
|
126
|
+
# Write the <a:lstStyle> element.
|
|
127
|
+
#
|
|
128
|
+
def write_a_lst_style # :nodoc:
|
|
129
|
+
@writer.empty_tag('a:lstStyle')
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
#
|
|
133
|
+
# Write the <a:p> element for formula titles.
|
|
134
|
+
#
|
|
135
|
+
def write_a_p_formula(font = nil) # :nodoc:
|
|
136
|
+
@writer.tag_elements('a:p') do
|
|
137
|
+
# Write the a:pPr element.
|
|
138
|
+
write_a_p_pr_formula(font)
|
|
139
|
+
# Write the a:endParaRPr element.
|
|
140
|
+
write_a_end_para_rpr
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
#
|
|
145
|
+
# Write the <a:pPr> element for formula titles.
|
|
146
|
+
#
|
|
147
|
+
def write_a_p_pr_formula(font) # :nodoc:
|
|
148
|
+
@writer.tag_elements('a:pPr') { write_a_def_rpr(font) }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
#
|
|
152
|
+
# Write the <a:defRPr> element.
|
|
153
|
+
#
|
|
154
|
+
def write_a_def_rpr(font = nil) # :nodoc:
|
|
155
|
+
write_def_rpr_r_pr_common(
|
|
156
|
+
font,
|
|
157
|
+
get_font_style_attributes(font),
|
|
158
|
+
'a:defRPr'
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def write_def_rpr_r_pr_common(font, style_attributes, tag) # :nodoc:
|
|
163
|
+
latin_attributes = get_font_latin_attributes(font)
|
|
164
|
+
has_color = ptrue?(font) && ptrue?(font[:_color])
|
|
165
|
+
|
|
166
|
+
if !latin_attributes.empty? || has_color
|
|
167
|
+
@writer.tag_elements(tag, style_attributes) do
|
|
168
|
+
write_a_solid_fill(color: font[:_color]) if has_color
|
|
169
|
+
write_a_latin(latin_attributes) unless latin_attributes.empty?
|
|
170
|
+
end
|
|
171
|
+
else
|
|
172
|
+
@writer.empty_tag(tag, style_attributes)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
#
|
|
177
|
+
# Write the <a:endParaRPr> element.
|
|
178
|
+
#
|
|
179
|
+
def write_a_end_para_rpr # :nodoc:
|
|
180
|
+
@writer.empty_tag('a:endParaRPr', [%w[lang en-US]])
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Writexlsx
|
|
5
|
+
module Utility
|
|
6
|
+
module SheetnameQuoting
|
|
7
|
+
#
|
|
8
|
+
# Sheetnames used in references should be quoted if they contain any spaces,
|
|
9
|
+
# special characters or if the look like something that isn't a sheet name.
|
|
10
|
+
# TODO. We need to handle more special cases.
|
|
11
|
+
#
|
|
12
|
+
def quote_sheetname(sheetname) # :nodoc:
|
|
13
|
+
name = sheetname.dup
|
|
14
|
+
return name if already_quoted_sheetname?(name)
|
|
15
|
+
return name unless sheetname_needs_quoting?(name)
|
|
16
|
+
|
|
17
|
+
"'#{escape_sheetname(name)}'"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def already_quoted_sheetname?(name)
|
|
23
|
+
name.start_with?("'")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def escape_sheetname(name)
|
|
27
|
+
name.gsub("'", "''")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def sheetname_needs_quoting?(name)
|
|
31
|
+
contains_non_identifier_chars?(name) ||
|
|
32
|
+
starts_with_digit_or_dot?(name) ||
|
|
33
|
+
valid_a1_reference_name?(name) ||
|
|
34
|
+
starts_with_rc_reference?(name) ||
|
|
35
|
+
single_rc_reference?(name)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def contains_non_identifier_chars?(name)
|
|
39
|
+
name.match?(/[^\p{L}\p{N}_.]/)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def starts_with_digit_or_dot?(name)
|
|
43
|
+
name.match?(/^[\p{N}.]/)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def valid_a1_reference_name?(name)
|
|
47
|
+
upcased = name.upcase
|
|
48
|
+
return false unless upcased.match?(/^[A-Z]{1,3}\d+$/)
|
|
49
|
+
|
|
50
|
+
row, col = xl_cell_to_rowcol(upcased)
|
|
51
|
+
row.between?(0, 1_048_575) && col.between?(0, 16_383)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def starts_with_rc_reference?(name)
|
|
55
|
+
upcased = name.upcase
|
|
56
|
+
|
|
57
|
+
if (match = upcased.match(/^R(\d+)/))
|
|
58
|
+
return match[1].to_i.between?(1, 1_048_576)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if (match = upcased.match(/^R?C(\d+)/))
|
|
62
|
+
return match[1].to_i.between?(1, 16_384)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def single_rc_reference?(name)
|
|
69
|
+
%w[R C RC].include?(name.upcase)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Writexlsx
|
|
5
|
+
module Utility
|
|
6
|
+
module StringWidth
|
|
7
|
+
MAX_DIGIT_WIDTH = 7 # For Calabri 11. # :nodoc:
|
|
8
|
+
PADDING = 5 # :nodoc:
|
|
9
|
+
DEFAULT_COL_PIXELS = 64
|
|
10
|
+
CHAR_WIDTHS = {
|
|
11
|
+
' ' => 3, '!' => 5, '"' => 6, '#' => 7, '$' => 7, '%' => 11,
|
|
12
|
+
'&' => 10, "'" => 3, '(' => 5, ')' => 5, '*' => 7, '+' => 7,
|
|
13
|
+
',' => 4, '-' => 5, '.' => 4, '/' => 6, '0' => 7, '1' => 7,
|
|
14
|
+
'2' => 7, '3' => 7, '4' => 7, '5' => 7, '6' => 7, '7' => 7,
|
|
15
|
+
'8' => 7, '9' => 7, ':' => 4, ';' => 4, '<' => 7, '=' => 7,
|
|
16
|
+
'>' => 7, '?' => 7, '@' => 13, 'A' => 9, 'B' => 8, 'C' => 8,
|
|
17
|
+
'D' => 9, 'E' => 7, 'F' => 7, 'G' => 9, 'H' => 9, 'I' => 4,
|
|
18
|
+
'J' => 5, 'K' => 8, 'L' => 6, 'M' => 12, 'N' => 10, 'O' => 10,
|
|
19
|
+
'P' => 8, 'Q' => 10, 'R' => 8, 'S' => 7, 'T' => 7, 'U' => 9,
|
|
20
|
+
'V' => 9, 'W' => 13, 'X' => 8, 'Y' => 7, 'Z' => 7, '[' => 5,
|
|
21
|
+
'\\' => 6, ']' => 5, '^' => 7, '_' => 7, '`' => 4, 'a' => 7,
|
|
22
|
+
'b' => 8, 'c' => 6, 'd' => 8, 'e' => 8, 'f' => 5, 'g' => 7,
|
|
23
|
+
'h' => 8, 'i' => 4, 'j' => 4, 'k' => 7, 'l' => 4, 'm' => 12,
|
|
24
|
+
'n' => 8, 'o' => 8, 'p' => 8, 'q' => 8, 'r' => 5, 's' => 6,
|
|
25
|
+
't' => 5, 'u' => 8, 'v' => 7, 'w' => 11, 'x' => 7, 'y' => 7,
|
|
26
|
+
'z' => 6, '{' => 5, '|' => 7, '}' => 5, '~' => 7
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
#
|
|
30
|
+
# xl_string_pixel_width($string)
|
|
31
|
+
#
|
|
32
|
+
# Get the pixel width of a string based on individual character widths taken
|
|
33
|
+
# from Excel. UTF8 characters are given a default width of 8.
|
|
34
|
+
#
|
|
35
|
+
# Note, Excel adds an additional 7 pixels padding to a cell.
|
|
36
|
+
#
|
|
37
|
+
def xl_string_pixel_width(string)
|
|
38
|
+
length = 0
|
|
39
|
+
string.to_s.chars.each { |char| length += CHAR_WIDTHS[char] || 8 }
|
|
40
|
+
|
|
41
|
+
length
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Writexlsx
|
|
5
|
+
module Utility
|
|
6
|
+
module Url
|
|
7
|
+
def escape_url(url)
|
|
8
|
+
unless url =~ /%[0-9a-fA-F]{2}/
|
|
9
|
+
# Escape the URL escape symbol.
|
|
10
|
+
url = url.gsub("%", "%25")
|
|
11
|
+
|
|
12
|
+
# Escape whitespae in URL.
|
|
13
|
+
url = url.gsub(/[\s\x00]/, '%20')
|
|
14
|
+
|
|
15
|
+
# Escape other special characters in URL.
|
|
16
|
+
re = /(["<>\[\]`^{}])/
|
|
17
|
+
while re =~ url
|
|
18
|
+
match = $LAST_MATCH_INFO[1]
|
|
19
|
+
url = url.sub(re, sprintf("%%%x", match.ord))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
url
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Writexlsx
|
|
5
|
+
module Utility
|
|
6
|
+
module XmlPrimitives
|
|
7
|
+
#
|
|
8
|
+
# Write the <color> element.
|
|
9
|
+
#
|
|
10
|
+
def write_color(name, value, writer = @writer) # :nodoc:
|
|
11
|
+
attributes = [[name, value]]
|
|
12
|
+
|
|
13
|
+
writer.empty_tag('color', attributes)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def write_xml_declaration
|
|
17
|
+
@writer.xml_decl
|
|
18
|
+
yield
|
|
19
|
+
@writer.crlf
|
|
20
|
+
@writer.close
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def r_id_attributes(id)
|
|
24
|
+
['r:id', "rId#{id}"]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def xml_str
|
|
28
|
+
@writer.string
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/write_xlsx/version.rb
CHANGED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Writexlsx
|
|
5
|
+
class Workbook
|
|
6
|
+
module ChartData
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
#
|
|
10
|
+
# Convert a range formula such as Sheet1!$B$1:$B$5 into a sheet name and cell
|
|
11
|
+
# range such as ( 'Sheet1', 0, 1, 4, 1 ).
|
|
12
|
+
#
|
|
13
|
+
def get_chart_range(range) # :nodoc:
|
|
14
|
+
# Split the range formula into sheetname and cells at the last '!'.
|
|
15
|
+
pos = range.rindex('!')
|
|
16
|
+
return nil unless pos
|
|
17
|
+
|
|
18
|
+
if pos > 0
|
|
19
|
+
sheetname = range[0, pos]
|
|
20
|
+
cells = range[(pos + 1)..-1]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Split the cell range into 2 cells or else use single cell for both.
|
|
24
|
+
if cells =~ /:/
|
|
25
|
+
cell_1, cell_2 = cells.split(":")
|
|
26
|
+
else
|
|
27
|
+
cell_1 = cells
|
|
28
|
+
cell_2 = cells
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Remove leading/trailing apostrophes and convert escaped quotes to single.
|
|
32
|
+
sheetname.sub!(/^'/, '')
|
|
33
|
+
sheetname.sub!(/'$/, '')
|
|
34
|
+
sheetname.gsub!("''", "'")
|
|
35
|
+
|
|
36
|
+
row_start, col_start = xl_cell_to_rowcol(cell_1)
|
|
37
|
+
row_end, col_end = xl_cell_to_rowcol(cell_2)
|
|
38
|
+
|
|
39
|
+
# Check that we have a 1D range only.
|
|
40
|
+
return nil if row_start != row_end && col_start != col_end
|
|
41
|
+
|
|
42
|
+
[sheetname, row_start, col_start, row_end, col_end]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
#
|
|
46
|
+
# Add "cached" data to charts to provide the numCache and strCache data for
|
|
47
|
+
# series and title/axis ranges.
|
|
48
|
+
#
|
|
49
|
+
def add_chart_data # :nodoc:
|
|
50
|
+
worksheets = {}
|
|
51
|
+
seen_ranges = {}
|
|
52
|
+
|
|
53
|
+
# Map worksheet names to worksheet objects.
|
|
54
|
+
@worksheets.each { |worksheet| worksheets[worksheet.name] = worksheet }
|
|
55
|
+
|
|
56
|
+
# Build an array of the worksheet charts including any combined charts.
|
|
57
|
+
@charts.collect { |chart| [chart, chart.combined] }.flatten.compact
|
|
58
|
+
.each do |chart|
|
|
59
|
+
chart.formula_ids.each do |range, id|
|
|
60
|
+
# Skip if the series has user defined data.
|
|
61
|
+
if chart.formula_data[id]
|
|
62
|
+
seen_ranges[range] = chart.formula_data[id] unless seen_ranges[range]
|
|
63
|
+
next
|
|
64
|
+
# Check to see if the data is already cached locally.
|
|
65
|
+
elsif seen_ranges[range]
|
|
66
|
+
chart.formula_data[id] = seen_ranges[range]
|
|
67
|
+
next
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Convert the range formula to a sheet name and cell range.
|
|
71
|
+
sheetname, *cells = get_chart_range(range)
|
|
72
|
+
|
|
73
|
+
# Skip if we couldn't parse the formula.
|
|
74
|
+
next unless sheetname
|
|
75
|
+
|
|
76
|
+
# Handle non-contiguous ranges: (Sheet1!$A$1:$A$2,Sheet1!$A$4:$A$5).
|
|
77
|
+
# We don't try to parse the ranges. We just return an empty list.
|
|
78
|
+
if sheetname =~ /^\([^,]+,/
|
|
79
|
+
chart.formula_data[id] = []
|
|
80
|
+
seen_ranges[range] = []
|
|
81
|
+
next
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Raise if the name is unknown since it indicates a user error in
|
|
85
|
+
# a chart series formula.
|
|
86
|
+
raise "Unknown worksheet reference '#{sheetname}' in range '#{range}' passed to add_series()\n" unless worksheets[sheetname]
|
|
87
|
+
|
|
88
|
+
# Add the data to the chart.
|
|
89
|
+
# And store range data locally to avoid lookup if seen agein.
|
|
90
|
+
chart.formula_data[id] =
|
|
91
|
+
seen_ranges[range] = chart_data(worksheets[sheetname], cells)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def chart_data(worksheet, cells)
|
|
97
|
+
# Get the data from the worksheet table.
|
|
98
|
+
data = worksheet.get_range_data(*cells)
|
|
99
|
+
|
|
100
|
+
# Convert shared string indexes to strings.
|
|
101
|
+
data.collect do |token|
|
|
102
|
+
if token.is_a?(Hash)
|
|
103
|
+
string = @shared_strings.string(token[:sst_id])
|
|
104
|
+
|
|
105
|
+
# Ignore rich strings for now. Deparse later if necessary.
|
|
106
|
+
if string =~ /^<r>/ && string =~ %r{</r>$}
|
|
107
|
+
''
|
|
108
|
+
else
|
|
109
|
+
string
|
|
110
|
+
end
|
|
111
|
+
else
|
|
112
|
+
token
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
#
|
|
118
|
+
# Sort internal and user defined names in the same order as used by Excel.
|
|
119
|
+
# This may not be strictly necessary but unsorted elements caused a lot of
|
|
120
|
+
# issues in the the Spreadsheet::WriteExcel binary version. Also makes
|
|
121
|
+
# comparison testing easier.
|
|
122
|
+
#
|
|
123
|
+
def sort_defined_names(names) # :nodoc:
|
|
124
|
+
names.sort do |a, b|
|
|
125
|
+
name_a = normalise_defined_name(a[0])
|
|
126
|
+
name_b = normalise_defined_name(b[0])
|
|
127
|
+
sheet_a = normalise_sheet_name(a[2])
|
|
128
|
+
sheet_b = normalise_sheet_name(b[2])
|
|
129
|
+
# Primary sort based on the defined name.
|
|
130
|
+
if name_a > name_b
|
|
131
|
+
1
|
|
132
|
+
elsif name_a < name_b
|
|
133
|
+
-1
|
|
134
|
+
elsif sheet_a >= sheet_b # name_a == name_b
|
|
135
|
+
# Secondary sort based on the sheet name.
|
|
136
|
+
1
|
|
137
|
+
else
|
|
138
|
+
-1
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Used in the above sort routine to normalise the defined names. Removes any
|
|
144
|
+
# leading '_xmln.' from internal names and lowercases the strings.
|
|
145
|
+
def normalise_defined_name(name) # :nodoc:
|
|
146
|
+
name.sub(/^_xlnm./, '').downcase
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Used in the above sort routine to normalise the worksheet names for the
|
|
150
|
+
# secondary sort. Removes leading quote and lowercases the strings.
|
|
151
|
+
def normalise_sheet_name(name) # :nodoc:
|
|
152
|
+
name.sub(/^'/, '').downcase
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
#
|
|
156
|
+
# Extract the named ranges from the sorted list of defined names. These are
|
|
157
|
+
# used in the App.xml file.
|
|
158
|
+
#
|
|
159
|
+
def extract_named_ranges(defined_names) # :nodoc:
|
|
160
|
+
named_ranges = []
|
|
161
|
+
|
|
162
|
+
defined_names.each do |defined_name|
|
|
163
|
+
name, index, range = defined_name
|
|
164
|
+
|
|
165
|
+
# Skip autoFilter ranges.
|
|
166
|
+
next if name == '_xlnm._FilterDatabase'
|
|
167
|
+
|
|
168
|
+
# We are only interested in defined names with ranges.
|
|
169
|
+
next unless range =~ /^([^!]+)!/
|
|
170
|
+
|
|
171
|
+
sheet_name = ::Regexp.last_match(1)
|
|
172
|
+
|
|
173
|
+
# Match Print_Area and Print_Titles xlnm types.
|
|
174
|
+
if name =~ /^_xlnm\.(.*)$/
|
|
175
|
+
xlnm_type = ::Regexp.last_match(1)
|
|
176
|
+
name = "#{sheet_name}!#{xlnm_type}"
|
|
177
|
+
elsif index != -1
|
|
178
|
+
name = "#{sheet_name}!#{name}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
named_ranges << name
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
named_ranges
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|