axlsx 1.1.8 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +5 -2
- data/CHANGELOG.md +39 -0
- data/README.md +48 -46
- data/Rakefile +3 -3
- data/examples/basic_charts.rb +8 -0
- data/examples/example.rb +7 -1
- data/examples/example.xlsx +0 -0
- data/examples/example_streamed.xlsx +0 -0
- data/examples/no-use_autowidth.xlsx +0 -0
- data/examples/scraping_html.rb +91 -0
- data/examples/shared_strings_example.xlsx +0 -0
- data/lib/axlsx.rb +14 -8
- data/lib/axlsx/drawing/bar_3D_chart.rb +2 -8
- data/lib/axlsx/drawing/chart.rb +29 -25
- data/lib/axlsx/drawing/d_lbls.rb +100 -0
- data/lib/axlsx/drawing/drawing.rb +2 -0
- data/lib/axlsx/drawing/line_3D_chart.rb +2 -9
- data/lib/axlsx/drawing/pie_3D_chart.rb +3 -0
- data/lib/axlsx/drawing/scatter_chart.rb +2 -8
- data/lib/axlsx/drawing/two_cell_anchor.rb +38 -1
- data/lib/axlsx/util/simple_typed_list.rb +13 -6
- data/lib/axlsx/version.rb +2 -7
- data/lib/axlsx/workbook/defined_name.rb +174 -0
- data/lib/axlsx/workbook/defined_names.rb +21 -0
- data/lib/axlsx/workbook/workbook.rb +39 -13
- data/lib/axlsx/workbook/worksheet/auto_filter.rb +34 -0
- data/lib/axlsx/workbook/worksheet/cell.rb +24 -1
- data/lib/axlsx/workbook/worksheet/col.rb +15 -0
- data/lib/axlsx/workbook/worksheet/cols.rb +20 -0
- data/lib/axlsx/workbook/worksheet/comments.rb +8 -0
- data/lib/axlsx/workbook/worksheet/conditional_formattings.rb +25 -0
- data/lib/axlsx/workbook/worksheet/data_validations.rb +28 -0
- data/lib/axlsx/workbook/worksheet/dimension.rb +65 -0
- data/lib/axlsx/workbook/worksheet/merged_cells.rb +35 -0
- data/lib/axlsx/workbook/worksheet/protected_ranges.rb +34 -0
- data/lib/axlsx/workbook/worksheet/row.rb +1 -1
- data/lib/axlsx/workbook/worksheet/sheet_data.rb +25 -0
- data/lib/axlsx/workbook/worksheet/sheet_pr.rb +24 -0
- data/lib/axlsx/workbook/worksheet/tables.rb +31 -0
- data/lib/axlsx/workbook/worksheet/worksheet.rb +263 -380
- data/lib/axlsx/workbook/worksheet/worksheet_comments.rb +57 -0
- data/lib/axlsx/workbook/worksheet/worksheet_drawing.rb +64 -0
- data/test/drawing/tc_bar_series.rb +1 -1
- data/test/drawing/tc_chart.rb +7 -1
- data/test/drawing/tc_d_lbls.rb +47 -0
- data/test/drawing/tc_drawing.rb +5 -4
- data/test/drawing/tc_line_series.rb +1 -1
- data/test/drawing/tc_pie_3D_chart.rb +1 -1
- data/test/drawing/tc_pie_series.rb +1 -1
- data/test/drawing/tc_scatter_series.rb +1 -1
- data/test/drawing/tc_series.rb +1 -1
- data/test/tc_package.rb +16 -1
- data/test/workbook/tc_defined_name.rb +41 -0
- data/test/workbook/tc_workbook.rb +5 -3
- data/test/workbook/worksheet/table/tc_table.rb +0 -8
- data/test/workbook/worksheet/tc_cell.rb +2 -4
- data/test/workbook/worksheet/tc_protected_range.rb +0 -1
- data/test/workbook/worksheet/tc_row.rb +2 -2
- data/test/workbook/worksheet/tc_worksheet.rb +19 -21
- metadata +48 -7
@@ -0,0 +1,21 @@
|
|
1
|
+
module Axlsx
|
2
|
+
# a simple types list of DefinedName objects
|
3
|
+
class DefinedNames < SimpleTypedList
|
4
|
+
|
5
|
+
# creates the DefinedNames object
|
6
|
+
def initialize
|
7
|
+
super DefinedName
|
8
|
+
end
|
9
|
+
|
10
|
+
# Serialize to xml
|
11
|
+
# @param [String] str
|
12
|
+
# @return [String]
|
13
|
+
def to_xml_string(str = '')
|
14
|
+
return if @list.empty?
|
15
|
+
str << "<definedNames>"
|
16
|
+
each { |defined_name| defined_name.to_xml_string(str) }
|
17
|
+
str << '</definedNames>'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
@@ -1,8 +1,9 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
module Axlsx
|
3
|
-
|
3
|
+
require 'axlsx/workbook/worksheet/auto_filter.rb'
|
4
4
|
require 'axlsx/workbook/worksheet/date_time_converter.rb'
|
5
5
|
require 'axlsx/workbook/worksheet/protected_range.rb'
|
6
|
+
require 'axlsx/workbook/worksheet/protected_ranges.rb'
|
6
7
|
require 'axlsx/workbook/worksheet/cell.rb'
|
7
8
|
require 'axlsx/workbook/worksheet/page_margins.rb'
|
8
9
|
require 'axlsx/workbook/worksheet/page_setup.rb'
|
@@ -13,15 +14,27 @@ require 'axlsx/workbook/worksheet/data_bar.rb'
|
|
13
14
|
require 'axlsx/workbook/worksheet/icon_set.rb'
|
14
15
|
require 'axlsx/workbook/worksheet/conditional_formatting.rb'
|
15
16
|
require 'axlsx/workbook/worksheet/conditional_formatting_rule.rb'
|
17
|
+
require 'axlsx/workbook/worksheet/conditional_formattings.rb'
|
16
18
|
require 'axlsx/workbook/worksheet/row.rb'
|
17
19
|
require 'axlsx/workbook/worksheet/col.rb'
|
20
|
+
require 'axlsx/workbook/worksheet/cols.rb'
|
18
21
|
require 'axlsx/workbook/worksheet/comments.rb'
|
19
22
|
require 'axlsx/workbook/worksheet/comment.rb'
|
23
|
+
require 'axlsx/workbook/worksheet/merged_cells.rb'
|
20
24
|
require 'axlsx/workbook/worksheet/sheet_protection.rb'
|
25
|
+
require 'axlsx/workbook/worksheet/sheet_pr.rb'
|
26
|
+
require 'axlsx/workbook/worksheet/dimension.rb'
|
27
|
+
require 'axlsx/workbook/worksheet/sheet_data.rb'
|
28
|
+
require 'axlsx/workbook/worksheet/worksheet_drawing.rb'
|
29
|
+
require 'axlsx/workbook/worksheet/worksheet_comments.rb'
|
21
30
|
require 'axlsx/workbook/worksheet/worksheet.rb'
|
22
31
|
require 'axlsx/workbook/shared_strings_table.rb'
|
32
|
+
require 'axlsx/workbook/defined_name.rb'
|
33
|
+
require 'axlsx/workbook/defined_names.rb'
|
23
34
|
require 'axlsx/workbook/worksheet/table.rb'
|
35
|
+
require 'axlsx/workbook/worksheet/tables.rb'
|
24
36
|
require 'axlsx/workbook/worksheet/data_validation.rb'
|
37
|
+
require 'axlsx/workbook/worksheet/data_validations.rb'
|
25
38
|
require 'axlsx/workbook/worksheet/sheet_view.rb'
|
26
39
|
require 'axlsx/workbook/worksheet/pane.rb'
|
27
40
|
require 'axlsx/workbook/worksheet/selection.rb'
|
@@ -101,13 +114,22 @@ require 'axlsx/workbook/worksheet/selection.rb'
|
|
101
114
|
# @return [SimpleTypedList]
|
102
115
|
attr_reader :tables
|
103
116
|
|
104
|
-
|
105
|
-
#
|
117
|
+
|
118
|
+
# A collection of defined names for this workbook
|
119
|
+
# @note The recommended way to manage defined names is Workbook#add_defined_name
|
120
|
+
# @see DefinedName
|
121
|
+
# @return [DefinedNames]
|
122
|
+
def defined_names
|
123
|
+
@defined_names ||= DefinedNames.new
|
124
|
+
end
|
125
|
+
|
126
|
+
# A collection of comments associated with this workbook
|
127
|
+
# @note The recommended way to manage comments is WOrksheet#add_comment
|
106
128
|
# @see Worksheet#add_comment
|
107
129
|
# @see Comment
|
108
130
|
# @return [Comments]
|
109
131
|
def comments
|
110
|
-
|
132
|
+
worksheets.map { |sheet| sheet.comments }.compact
|
111
133
|
end
|
112
134
|
|
113
135
|
# The styles associated with this workbook
|
@@ -190,6 +212,14 @@ require 'axlsx/workbook/worksheet/selection.rb'
|
|
190
212
|
worksheet
|
191
213
|
end
|
192
214
|
|
215
|
+
# Adds a defined name to this workbook
|
216
|
+
# @return [DefinedName]
|
217
|
+
# @param [String] formula @see DefinedName
|
218
|
+
# @param [Hash] options @see DefinedName
|
219
|
+
def add_defined_name(formula, options)
|
220
|
+
defined_names << DefinedName.new(formula, options)
|
221
|
+
end
|
222
|
+
|
193
223
|
# The workbook relationships. This is managed automatically by the workbook
|
194
224
|
# @return [Relationships]
|
195
225
|
def relationships
|
@@ -231,17 +261,13 @@ require 'axlsx/workbook/worksheet/selection.rb'
|
|
231
261
|
str << '<workbookPr date1904="' << @@date1904.to_s << '"/>'
|
232
262
|
str << '<sheets>'
|
233
263
|
@worksheets.each_with_index do |sheet, index|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
str << '<definedNames>'
|
238
|
-
@worksheets.each_with_index do |sheet, index|
|
239
|
-
if sheet.auto_filter
|
240
|
-
str << '<definedName name="_xlnm._FilterDatabase" localSheetId="' << index.to_s << '" hidden="1">'
|
241
|
-
str << sheet.abs_auto_filter << '</definedName>'
|
264
|
+
str << '<sheet name="' << sheet.name << '" sheetId="' << (index+1).to_s << '" r:id="' << sheet.rId << '"/>'
|
265
|
+
if defined_name = sheet.auto_filter.defined_name
|
266
|
+
add_defined_name defined_name, :name => '_xlnm._FilterDatabase', :local_sheet_id => index, :hidden => 1
|
242
267
|
end
|
243
268
|
end
|
244
|
-
str << '</
|
269
|
+
str << '</sheets>'
|
270
|
+
defined_names.to_xml_string(str)
|
245
271
|
str << '</workbook>'
|
246
272
|
end
|
247
273
|
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Axlsx
|
2
|
+
|
3
|
+
#This class represents an auto filter range in a worksheet
|
4
|
+
class AutoFilter
|
5
|
+
|
6
|
+
# creates a new Autofilter object
|
7
|
+
# @param [Worksheet] worksheet
|
8
|
+
def initialize(worksheet)
|
9
|
+
raise ArgumentError, 'you must provide a worksheet' unless worksheet.is_a?(Worksheet)
|
10
|
+
@worksheet = worksheet
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :worksheet
|
14
|
+
|
15
|
+
# The range the autofilter should be applied to.
|
16
|
+
# This should be a string like 'A1:B8'
|
17
|
+
# @return [String]
|
18
|
+
attr_accessor :range
|
19
|
+
|
20
|
+
# the formula for the defined name required for this auto filter
|
21
|
+
# @return [String]
|
22
|
+
def defined_name
|
23
|
+
return unless range
|
24
|
+
Axlsx.cell_range(range.split(':').collect { |name| worksheet.name_to_cell(name)})
|
25
|
+
end
|
26
|
+
|
27
|
+
# serialize the object
|
28
|
+
# @return [String]
|
29
|
+
def to_xml_string(str='')
|
30
|
+
str << "<autoFilter ref='#{range}'></autoFilter>"
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -318,11 +318,34 @@ module Axlsx
|
|
318
318
|
end
|
319
319
|
|
320
320
|
def is_formula?
|
321
|
-
@type == :string && @value.start_with?('=')
|
321
|
+
@type == :string && @value.to_s.start_with?('=')
|
322
322
|
end
|
323
323
|
|
324
|
+
# This is still not perfect...
|
325
|
+
# - scaling is not linear as font sizes increst
|
326
|
+
# - different fonts have different mdw and char widths
|
327
|
+
def autowidth
|
328
|
+
return if is_formula? || value == nil
|
329
|
+
mdw = 1.78 #This is the widest width of 0..9 in arial@10px)
|
330
|
+
font_scale = (font_size/10.0).to_f
|
331
|
+
((value.to_s.count(Worksheet.thin_chars) * mdw + 5) / mdw * 256) / 256.0 * font_scale
|
332
|
+
end
|
333
|
+
|
334
|
+
# returns the absolute or relative string style reference for
|
335
|
+
# this cell.
|
336
|
+
# @param [Boolean] absolute -when false a relative reference will be
|
337
|
+
# returned.
|
338
|
+
# @return [String]
|
339
|
+
def reference(absolute=true)
|
340
|
+
absolute ? r_abs : r
|
341
|
+
end
|
342
|
+
|
324
343
|
private
|
325
344
|
|
345
|
+
def font_size
|
346
|
+
sz || @styles.fonts[@styles.cellXfs[style].fontId].sz
|
347
|
+
end
|
348
|
+
|
326
349
|
# Utility method for setting inline style attributes
|
327
350
|
def set_run_style( validator, attr, value)
|
328
351
|
return unless INLINE_STYLES.include?(attr.to_s)
|
@@ -102,6 +102,21 @@ module Axlsx
|
|
102
102
|
self.send("#{o[0]}=", o[1]) if self.respond_to? "#{o[0]}="
|
103
103
|
end
|
104
104
|
end
|
105
|
+
|
106
|
+
# updates the width for this col based on the cells autowidth and
|
107
|
+
# an optionally specified fixed width
|
108
|
+
# @param [Cell] cell The cell to use in updating this col's width
|
109
|
+
# @param [Integer] fixed_width If this is specified the width is set
|
110
|
+
# to this value and the cell's attributes are ignored.
|
111
|
+
# @param [Boolean] use_autowidth If this is false, the cell's
|
112
|
+
# autowidth value will be ignored.
|
113
|
+
def update_width(cell, fixed_width=nil, use_autowidth=true)
|
114
|
+
if fixed_width.is_a? Numeric
|
115
|
+
self.width = fixed_width
|
116
|
+
elsif use_autowidth
|
117
|
+
self.width = [width || 0, cell.autowidth || 0].max
|
118
|
+
end
|
119
|
+
end
|
105
120
|
|
106
121
|
# Serialize this columns data to an xml string
|
107
122
|
# @param [String] str
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Axlsx
|
2
|
+
|
3
|
+
# The cols class manages the col object used to manage column widths.
|
4
|
+
# This is where the magic happens with autowidth
|
5
|
+
class Cols < SimpleTypedList
|
6
|
+
|
7
|
+
def initialize(worksheet)
|
8
|
+
raise ArgumentError, "you must provide a worksheet" unless worksheet.is_a?(Worksheet)
|
9
|
+
super Col
|
10
|
+
@worksheet = worksheet
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_xml_string(str = '')
|
14
|
+
return if empty?
|
15
|
+
str << '<cols>'
|
16
|
+
each { |item| item.to_xml_string(str) }
|
17
|
+
str << '</cols>'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -53,6 +53,14 @@ module Axlsx
|
|
53
53
|
@list.map { |comment| comment.author.to_s }.uniq.sort
|
54
54
|
end
|
55
55
|
|
56
|
+
# The relationships required by this object
|
57
|
+
# @return [Array]
|
58
|
+
def relationships
|
59
|
+
[Relationship.new(VML_DRAWING_R, "../#{vml_drawing.pn}"),
|
60
|
+
Relationship.new(COMMENT_R, "../#{pn}"),
|
61
|
+
Relationship.new(COMMENT_R_NULL, "NULL")]
|
62
|
+
end
|
63
|
+
|
56
64
|
# serialize the object
|
57
65
|
# @param [String] str
|
58
66
|
# @return [String]
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Axlsx
|
2
|
+
|
3
|
+
# A simple, self serializing class for storing conditional formattings
|
4
|
+
class ConditionalFormattings < SimpleTypedList
|
5
|
+
|
6
|
+
# creates a new Tables object
|
7
|
+
def initialize(worksheet)
|
8
|
+
raise ArgumentError, "you must provide a worksheet" unless worksheet.is_a?(Worksheet)
|
9
|
+
super ConditionalFormatting
|
10
|
+
@worksheet = worksheet
|
11
|
+
end
|
12
|
+
|
13
|
+
# The worksheet that owns this collection of tables
|
14
|
+
# @return [Worksheet]
|
15
|
+
attr_reader :worksheet
|
16
|
+
|
17
|
+
# serialize the conditional formattings
|
18
|
+
def to_xml_string(str = "")
|
19
|
+
return if empty?
|
20
|
+
each { |item| item.to_xml_string(str) }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Axlsx
|
2
|
+
|
3
|
+
# A simple, self serializing class for storing conditional formattings
|
4
|
+
class DataValidations < SimpleTypedList
|
5
|
+
|
6
|
+
# creates a new Tables object
|
7
|
+
def initialize(worksheet)
|
8
|
+
raise ArgumentError, "you must provide a worksheet" unless worksheet.is_a?(Worksheet)
|
9
|
+
super DataValidation
|
10
|
+
@worksheet = worksheet
|
11
|
+
end
|
12
|
+
|
13
|
+
# The worksheet that owns this collection of tables
|
14
|
+
# @return [Worksheet]
|
15
|
+
attr_reader :worksheet
|
16
|
+
|
17
|
+
# serialize the conditional formattings
|
18
|
+
def to_xml_string(str = "")
|
19
|
+
return if empty?
|
20
|
+
str << "<dataValidations count='#{size}'>"
|
21
|
+
each { |item| item.to_xml_string(str) }
|
22
|
+
str << '</dataValidations>'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Axlsx
|
2
|
+
|
3
|
+
# This class manages the dimensions for a worksheet.
|
4
|
+
# While this node is optional in the specification some readers like
|
5
|
+
# LibraOffice require this node to render the sheet
|
6
|
+
class Dimension
|
7
|
+
|
8
|
+
# the default value for the first cell in the dimension
|
9
|
+
# @return [String]
|
10
|
+
def self.default_first
|
11
|
+
@@default_first ||= 'A1'
|
12
|
+
end
|
13
|
+
|
14
|
+
# the default value for the last cell in the dimension
|
15
|
+
# @return [String]
|
16
|
+
def self.default_last
|
17
|
+
@@default_last ||= 'AA200'
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
# Creates a new dimension object
|
22
|
+
# @param[Worksheet] worksheet - the worksheet this dimension applies
|
23
|
+
# to.
|
24
|
+
def initialize(worksheet)
|
25
|
+
raise ArgumentError, "you must provide a worksheet" unless worksheet.is_a?(Worksheet)
|
26
|
+
@worksheet = worksheet
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_reader :worksheet
|
30
|
+
|
31
|
+
# the full refernece for this dimension
|
32
|
+
# @return [String]
|
33
|
+
def sqref
|
34
|
+
"#{first_cell_reference}:#{last_cell_reference}"
|
35
|
+
end
|
36
|
+
|
37
|
+
# serialize the object
|
38
|
+
# @return [String]
|
39
|
+
def to_xml_string(str = '')
|
40
|
+
return if worksheet.rows.empty?
|
41
|
+
str << "<dimension ref=\"%s\"></dimension>" % sqref
|
42
|
+
end
|
43
|
+
|
44
|
+
# The first cell in the dimension
|
45
|
+
# @return [String]
|
46
|
+
def first_cell_reference
|
47
|
+
dimension_reference(worksheet.rows.first.cells.first, Dimension.default_first)
|
48
|
+
end
|
49
|
+
|
50
|
+
# the last cell in the dimension
|
51
|
+
# @return [String]
|
52
|
+
def last_cell_reference
|
53
|
+
dimension_reference(worksheet.rows.last.cells.last, Dimension.default_last)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# Returns the reference of a cell or the default specified
|
59
|
+
# @return [String]
|
60
|
+
def dimension_reference(cell, default)
|
61
|
+
return default unless cell.respond_to?(:r)
|
62
|
+
cell.r
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Axlsx
|
2
|
+
|
3
|
+
# A simple list of merged cells
|
4
|
+
class MergedCells < SimpleTypedList
|
5
|
+
|
6
|
+
# creates a new MergedCells object
|
7
|
+
# @param [Worksheet] worksheet
|
8
|
+
def initialize(worksheet)
|
9
|
+
raise ArgumentError, 'you must provide a worksheet' unless worksheet.is_a?(Worksheet)
|
10
|
+
super String
|
11
|
+
end
|
12
|
+
|
13
|
+
# adds cells to the merged cells collection
|
14
|
+
# @param [Array||String] cells The cells to add to the merged cells
|
15
|
+
# collection. This can be an array of actual cells or a string style
|
16
|
+
# range like 'A1:C1'
|
17
|
+
def add(cells)
|
18
|
+
@list << if cells.is_a?(String)
|
19
|
+
cells
|
20
|
+
elsif cells.is_a?(Array)
|
21
|
+
Axlsx::cell_range(cells, false)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# serialize the object
|
26
|
+
# @param [String] str
|
27
|
+
# @return [String]
|
28
|
+
def to_xml_string(str = '')
|
29
|
+
return if @list.empty?
|
30
|
+
str << "<mergeCells count='#{size}'>"
|
31
|
+
each { |merged_cell| str << "<mergeCell ref='#{merged_cell}'></mergeCell>" }
|
32
|
+
str << '</mergeCells>'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Axlsx
|
2
|
+
|
3
|
+
# A self serializing collection of ranges that should be protected in
|
4
|
+
# the worksheet
|
5
|
+
class ProtectedRanges < SimpleTypedList
|
6
|
+
|
7
|
+
attr_reader :worksheet
|
8
|
+
|
9
|
+
def initialize(worksheet)
|
10
|
+
raise ArgumentError, 'You must provide a worksheet' unless worksheet.is_a?(Worksheet)
|
11
|
+
super ProtectedRange
|
12
|
+
@worksheet = worksheet
|
13
|
+
end
|
14
|
+
|
15
|
+
# Adds a protected range
|
16
|
+
# @param [Array|String] cells A string range reference or array of cells that will be protected
|
17
|
+
def add_range(cells)
|
18
|
+
sqref = if cells.is_a?(String)
|
19
|
+
cells
|
20
|
+
elsif cells.is_a?(SimpleTypedList) || cells.is_a?(Array)
|
21
|
+
Axlsx::cell_range(cells, false)
|
22
|
+
end
|
23
|
+
@list << ProtectedRange.new(:sqref => sqref, :name => "Range#{size}")
|
24
|
+
last
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_xml_string(str = '')
|
28
|
+
return if empty?
|
29
|
+
str << '<protectedRanges>'
|
30
|
+
each { |range| range.to_xml_string(str) }
|
31
|
+
str << '</protectedRanges>'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|