roo 1.13.2 → 2.0.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.
Files changed (172) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +7 -0
  3. data/.simplecov +4 -0
  4. data/.travis.yml +13 -0
  5. data/CHANGELOG.md +500 -0
  6. data/Gemfile +16 -10
  7. data/Guardfile +24 -0
  8. data/LICENSE +3 -1
  9. data/README.md +254 -0
  10. data/Rakefile +23 -23
  11. data/examples/roo_soap_client.rb +28 -31
  12. data/examples/roo_soap_server.rb +4 -6
  13. data/examples/write_me.rb +9 -10
  14. data/lib/roo/base.rb +303 -388
  15. data/lib/roo/csv.rb +120 -113
  16. data/lib/roo/excelx/comments.rb +24 -0
  17. data/lib/roo/excelx/extractor.rb +20 -0
  18. data/lib/roo/excelx/relationships.rb +26 -0
  19. data/lib/roo/excelx/shared_strings.rb +40 -0
  20. data/lib/roo/excelx/sheet_doc.rb +202 -0
  21. data/lib/roo/excelx/styles.rb +62 -0
  22. data/lib/roo/excelx/workbook.rb +59 -0
  23. data/lib/roo/excelx.rb +452 -484
  24. data/lib/roo/font.rb +17 -0
  25. data/lib/roo/libre_office.rb +5 -0
  26. data/lib/roo/link.rb +15 -0
  27. data/lib/roo/{openoffice.rb → open_office.rb} +678 -496
  28. data/lib/roo/spreadsheet.rb +20 -23
  29. data/lib/roo/utils.rb +78 -0
  30. data/lib/roo/version.rb +3 -0
  31. data/lib/roo.rb +18 -24
  32. data/roo.gemspec +20 -204
  33. data/spec/lib/roo/base_spec.rb +1 -4
  34. data/spec/lib/roo/csv_spec.rb +21 -13
  35. data/spec/lib/roo/excelx/format_spec.rb +7 -6
  36. data/spec/lib/roo/excelx_spec.rb +388 -11
  37. data/spec/lib/roo/libreoffice_spec.rb +16 -6
  38. data/spec/lib/roo/openoffice_spec.rb +2 -8
  39. data/spec/lib/roo/spreadsheet_spec.rb +40 -12
  40. data/spec/lib/roo/utils_spec.rb +106 -0
  41. data/spec/spec_helper.rb +2 -1
  42. data/test/test_generic_spreadsheet.rb +19 -67
  43. data/test/test_helper.rb +9 -56
  44. data/test/test_roo.rb +252 -477
  45. metadata +63 -302
  46. data/CHANGELOG +0 -417
  47. data/Gemfile.lock +0 -78
  48. data/README.markdown +0 -126
  49. data/VERSION +0 -1
  50. data/lib/roo/excel.rb +0 -355
  51. data/lib/roo/excel2003xml.rb +0 -300
  52. data/lib/roo/google.rb +0 -292
  53. data/lib/roo/roo_rails_helper.rb +0 -83
  54. data/lib/roo/worksheet.rb +0 -18
  55. data/spec/lib/roo/excel2003xml_spec.rb +0 -15
  56. data/spec/lib/roo/excel_spec.rb +0 -17
  57. data/spec/lib/roo/google_spec.rb +0 -64
  58. data/test/files/1900_base.xls +0 -0
  59. data/test/files/1900_base.xlsx +0 -0
  60. data/test/files/1904_base.xls +0 -0
  61. data/test/files/1904_base.xlsx +0 -0
  62. data/test/files/Bibelbund.csv +0 -3741
  63. data/test/files/Bibelbund.ods +0 -0
  64. data/test/files/Bibelbund.xls +0 -0
  65. data/test/files/Bibelbund.xlsx +0 -0
  66. data/test/files/Bibelbund.xml +0 -62518
  67. data/test/files/Bibelbund1.ods +0 -0
  68. data/test/files/Pfand_from_windows_phone.xlsx +0 -0
  69. data/test/files/bad_excel_date.xls +0 -0
  70. data/test/files/bbu.ods +0 -0
  71. data/test/files/bbu.xls +0 -0
  72. data/test/files/bbu.xlsx +0 -0
  73. data/test/files/bbu.xml +0 -152
  74. data/test/files/bode-v1.ods.zip +0 -0
  75. data/test/files/bode-v1.xls.zip +0 -0
  76. data/test/files/boolean.csv +0 -2
  77. data/test/files/boolean.ods +0 -0
  78. data/test/files/boolean.xls +0 -0
  79. data/test/files/boolean.xlsx +0 -0
  80. data/test/files/boolean.xml +0 -112
  81. data/test/files/borders.ods +0 -0
  82. data/test/files/borders.xls +0 -0
  83. data/test/files/borders.xlsx +0 -0
  84. data/test/files/borders.xml +0 -144
  85. data/test/files/bug-numbered-sheet-names.xlsx +0 -0
  86. data/test/files/bug-row-column-fixnum-float.xls +0 -0
  87. data/test/files/bug-row-column-fixnum-float.xml +0 -127
  88. data/test/files/comments.ods +0 -0
  89. data/test/files/comments.xls +0 -0
  90. data/test/files/comments.xlsx +0 -0
  91. data/test/files/csvtypes.csv +0 -1
  92. data/test/files/datetime.ods +0 -0
  93. data/test/files/datetime.xls +0 -0
  94. data/test/files/datetime.xlsx +0 -0
  95. data/test/files/datetime.xml +0 -142
  96. data/test/files/datetime_floatconv.xls +0 -0
  97. data/test/files/datetime_floatconv.xml +0 -148
  98. data/test/files/dreimalvier.ods +0 -0
  99. data/test/files/emptysheets.ods +0 -0
  100. data/test/files/emptysheets.xls +0 -0
  101. data/test/files/emptysheets.xlsx +0 -0
  102. data/test/files/emptysheets.xml +0 -105
  103. data/test/files/excel2003.xml +0 -21140
  104. data/test/files/false_encoding.xls +0 -0
  105. data/test/files/false_encoding.xml +0 -132
  106. data/test/files/file_item_error.xlsx +0 -0
  107. data/test/files/formula.ods +0 -0
  108. data/test/files/formula.xls +0 -0
  109. data/test/files/formula.xlsx +0 -0
  110. data/test/files/formula.xml +0 -134
  111. data/test/files/formula_parse_error.xls +0 -0
  112. data/test/files/formula_parse_error.xml +0 -1833
  113. data/test/files/formula_string_error.xlsx +0 -0
  114. data/test/files/html-escape.ods +0 -0
  115. data/test/files/link.xls +0 -0
  116. data/test/files/link.xlsx +0 -0
  117. data/test/files/matrix.ods +0 -0
  118. data/test/files/matrix.xls +0 -0
  119. data/test/files/named_cells.ods +0 -0
  120. data/test/files/named_cells.xls +0 -0
  121. data/test/files/named_cells.xlsx +0 -0
  122. data/test/files/no_spreadsheet_file.txt +0 -1
  123. data/test/files/numbers1.csv +0 -18
  124. data/test/files/numbers1.ods +0 -0
  125. data/test/files/numbers1.xls +0 -0
  126. data/test/files/numbers1.xlsx +0 -0
  127. data/test/files/numbers1.xml +0 -312
  128. data/test/files/numeric-link.xlsx +0 -0
  129. data/test/files/only_one_sheet.ods +0 -0
  130. data/test/files/only_one_sheet.xls +0 -0
  131. data/test/files/only_one_sheet.xlsx +0 -0
  132. data/test/files/only_one_sheet.xml +0 -67
  133. data/test/files/paragraph.ods +0 -0
  134. data/test/files/paragraph.xls +0 -0
  135. data/test/files/paragraph.xlsx +0 -0
  136. data/test/files/paragraph.xml +0 -127
  137. data/test/files/prova.xls +0 -0
  138. data/test/files/ric.ods +0 -0
  139. data/test/files/simple_spreadsheet.ods +0 -0
  140. data/test/files/simple_spreadsheet.xls +0 -0
  141. data/test/files/simple_spreadsheet.xlsx +0 -0
  142. data/test/files/simple_spreadsheet.xml +0 -225
  143. data/test/files/simple_spreadsheet_from_italo.ods +0 -0
  144. data/test/files/simple_spreadsheet_from_italo.xls +0 -0
  145. data/test/files/simple_spreadsheet_from_italo.xml +0 -242
  146. data/test/files/so_datetime.csv +0 -7
  147. data/test/files/style.ods +0 -0
  148. data/test/files/style.xls +0 -0
  149. data/test/files/style.xlsx +0 -0
  150. data/test/files/style.xml +0 -154
  151. data/test/files/time-test.csv +0 -2
  152. data/test/files/time-test.ods +0 -0
  153. data/test/files/time-test.xls +0 -0
  154. data/test/files/time-test.xlsx +0 -0
  155. data/test/files/time-test.xml +0 -131
  156. data/test/files/type_excel.ods +0 -0
  157. data/test/files/type_excel.xlsx +0 -0
  158. data/test/files/type_excelx.ods +0 -0
  159. data/test/files/type_excelx.xls +0 -0
  160. data/test/files/type_openoffice.xls +0 -0
  161. data/test/files/type_openoffice.xlsx +0 -0
  162. data/test/files/whitespace.ods +0 -0
  163. data/test/files/whitespace.xls +0 -0
  164. data/test/files/whitespace.xlsx +0 -0
  165. data/test/files/whitespace.xml +0 -184
  166. data/test/rm_sub_test.rb +0 -12
  167. data/test/rm_test.rb +0 -7
  168. data/website/index.html +0 -385
  169. data/website/index.txt +0 -423
  170. data/website/javascripts/rounded_corners_lite.inc.js +0 -285
  171. data/website/stylesheets/screen.css +0 -130
  172. data/website/template.rhtml +0 -48
@@ -1,496 +1,678 @@
1
- require 'date'
2
- require 'nokogiri'
3
- require 'cgi'
4
-
5
- class Roo::OpenOffice < Roo::Base
6
- class << self
7
- def extract_content(tmpdir, filename)
8
- Roo::ZipFile.open(filename) do |zip|
9
- process_zipfile(tmpdir, zip)
10
- end
11
- end
12
-
13
- def process_zipfile(tmpdir, zip, path='')
14
- if zip.file.file? path
15
- if path == "content.xml"
16
- open(File.join(tmpdir, 'roo_content.xml'),'wb') {|f|
17
- f << zip.read(path)
18
- }
19
- end
20
- else
21
- unless path.empty?
22
- path += '/'
23
- end
24
- zip.dir.foreach(path) do |filename|
25
- process_zipfile(tmpdir, zip, path+filename)
26
- end
27
- end
28
- end
29
- end
30
-
31
- # initialization and opening of a spreadsheet file
32
- # values for packed: :zip
33
- def initialize(filename, options={}, deprecated_file_warning=:error, deprecated_tmpdir_root=nil)
34
- if Hash === options
35
- packed = options[:packed]
36
- file_warning = options[:file_warning] || :error
37
- tmpdir_root = options[:tmpdir_root]
38
- else
39
- warn 'Supplying `packed`, `file_warning`, or `tmpdir_root` as separate arguments to `Roo::OpenOffice.new` is deprecated. Use an options hash instead.'
40
- packed = options
41
- file_warning = deprecated_file_warning
42
- tmpdir_root = deprecated_tmpdir_root
43
- end
44
-
45
- file_type_check(filename,'.ods','an Roo::OpenOffice', file_warning, packed)
46
- make_tmpdir(tmpdir_root) do |tmpdir|
47
- filename = download_uri(filename, tmpdir) if uri?(filename)
48
- filename = unzip(filename, tmpdir) if packed == :zip
49
- #TODO: @cells_read[:default] = false
50
- @filename = filename
51
- unless File.file?(@filename)
52
- raise IOError, "file #{@filename} does not exist"
53
- end
54
- self.class.extract_content(tmpdir, @filename)
55
- @doc = load_xml(File.join(tmpdir, "roo_content.xml"))
56
- end
57
- super(filename, options)
58
- @formula = Hash.new
59
- @style = Hash.new
60
- @style_defaults = Hash.new { |h,k| h[k] = [] }
61
- @style_definitions = Hash.new
62
- @comment = Hash.new
63
- @comments_read = Hash.new
64
- end
65
-
66
- def method_missing(m,*args)
67
- read_labels
68
- # is method name a label name
69
- if @label.has_key?(m.to_s)
70
- row,col = label(m.to_s)
71
- cell(row,col)
72
- else
73
- # call super for methods like #a1
74
- super
75
- end
76
- end
77
-
78
- # Returns the content of a spreadsheet-cell.
79
- # (1,1) is the upper left corner.
80
- # (1,1), (1,'A'), ('A',1), ('a',1) all refers to the
81
- # cell at the first line and first row.
82
- def cell(row, col, sheet=nil)
83
- sheet ||= @default_sheet
84
- read_cells(sheet)
85
- row,col = normalize(row,col)
86
- if celltype(row,col,sheet) == :date
87
- yyyy,mm,dd = @cell[sheet][[row,col]].to_s.split('-')
88
- return Date.new(yyyy.to_i,mm.to_i,dd.to_i)
89
- end
90
- @cell[sheet][[row,col]]
91
- end
92
-
93
- # Returns the formula at (row,col).
94
- # Returns nil if there is no formula.
95
- # The method #formula? checks if there is a formula.
96
- def formula(row,col,sheet=nil)
97
- sheet ||= @default_sheet
98
- read_cells(sheet)
99
- row,col = normalize(row,col)
100
- @formula[sheet][[row,col]]
101
- end
102
- alias_method :formula?, :formula
103
-
104
- # returns each formula in the selected sheet as an array of elements
105
- # [row, col, formula]
106
- def formulas(sheet=nil)
107
- sheet ||= @default_sheet
108
- read_cells(sheet)
109
- if @formula[sheet]
110
- @formula[sheet].each.collect do |elem|
111
- [elem[0][0], elem[0][1], elem[1]]
112
- end
113
- else
114
- []
115
- end
116
- end
117
-
118
- class Font
119
- attr_accessor :bold, :italic, :underline
120
-
121
- def bold?
122
- @bold == 'bold'
123
- end
124
-
125
- def italic?
126
- @italic == 'italic'
127
- end
128
-
129
- def underline?
130
- @underline != nil
131
- end
132
- end
133
-
134
- # Given a cell, return the cell's style
135
- def font(row, col, sheet=nil)
136
- sheet ||= @default_sheet
137
- read_cells(sheet)
138
- row,col = normalize(row,col)
139
- style_name = @style[sheet][[row,col]] || @style_defaults[sheet][col - 1] || 'Default'
140
- @style_definitions[style_name]
141
- end
142
-
143
- # returns the type of a cell:
144
- # * :float
145
- # * :string
146
- # * :date
147
- # * :percentage
148
- # * :formula
149
- # * :time
150
- # * :datetime
151
- def celltype(row,col,sheet=nil)
152
- sheet ||= @default_sheet
153
- read_cells(sheet)
154
- row,col = normalize(row,col)
155
- if @formula[sheet][[row,col]]
156
- return :formula
157
- else
158
- @cell_type[sheet][[row,col]]
159
- end
160
- end
161
-
162
- def sheets
163
- @doc.xpath("//*[local-name()='table']").map do |sheet|
164
- sheet.attributes["name"].value
165
- end
166
- end
167
-
168
- # version of the Roo::OpenOffice document
169
- # at 2007 this is always "1.0"
170
- def officeversion
171
- oo_version
172
- @officeversion
173
- end
174
-
175
- # shows the internal representation of all cells
176
- # mainly for debugging purposes
177
- def to_s(sheet=nil)
178
- sheet ||= @default_sheet
179
- read_cells(sheet)
180
- @cell[sheet].inspect
181
- end
182
-
183
- # returns the row,col values of the labelled cell
184
- # (nil,nil) if label is not defined
185
- def label(labelname)
186
- read_labels
187
- unless @label.size > 0
188
- return nil,nil,nil
189
- end
190
- if @label.has_key? labelname
191
- return @label[labelname][1].to_i,
192
- Roo::Base.letter_to_number(@label[labelname][2]),
193
- @label[labelname][0]
194
- else
195
- return nil,nil,nil
196
- end
197
- end
198
-
199
- # Returns an array which all labels. Each element is an array with
200
- # [labelname, [row,col,sheetname]]
201
- def labels(sheet=nil)
202
- read_labels
203
- @label.map do |label|
204
- [ label[0], # name
205
- [ label[1][1].to_i, # row
206
- Roo::Base.letter_to_number(label[1][2]), # column
207
- label[1][0], # sheet
208
- ] ]
209
- end
210
- end
211
-
212
- # returns the comment at (row/col)
213
- # nil if there is no comment
214
- def comment(row,col,sheet=nil)
215
- sheet ||= @default_sheet
216
- read_cells(sheet)
217
- row,col = normalize(row,col)
218
- return nil unless @comment[sheet]
219
- @comment[sheet][[row,col]]
220
- end
221
-
222
- # true, if there is a comment
223
- def comment?(row,col,sheet=nil)
224
- sheet ||= @default_sheet
225
- read_cells(sheet)
226
- row,col = normalize(row,col)
227
- comment(row,col) != nil
228
- end
229
-
230
-
231
- # returns each comment in the selected sheet as an array of elements
232
- # [row, col, comment]
233
- def comments(sheet=nil)
234
- sheet ||= @default_sheet
235
- read_comments(sheet) unless @comments_read[sheet]
236
- if @comment[sheet]
237
- @comment[sheet].each.collect do |elem|
238
- [elem[0][0],elem[0][1],elem[1]]
239
- end
240
- else
241
- []
242
- end
243
- end
244
-
245
- private
246
-
247
- # read the version of the OO-Version
248
- def oo_version
249
- @doc.xpath("//*[local-name()='document-content']").each do |office|
250
- @officeversion = attr(office,'version')
251
- end
252
- end
253
-
254
- # helper function to set the internal representation of cells
255
- def set_cell_values(sheet,x,y,i,v,value_type,formula,table_cell,str_v,style_name)
256
- key = [y,x+i]
257
- @cell_type[sheet] = {} unless @cell_type[sheet]
258
- @cell_type[sheet][key] = Roo::OpenOffice.oo_type_2_roo_type(value_type)
259
- @formula[sheet] = {} unless @formula[sheet]
260
- if formula
261
- ['of:', 'oooc:'].each do |prefix|
262
- if formula[0,prefix.length] == prefix
263
- formula = formula[prefix.length..-1]
264
- end
265
- end
266
- @formula[sheet][key] = formula
267
- end
268
- @cell[sheet] = {} unless @cell[sheet]
269
- @style[sheet] = {} unless @style[sheet]
270
- @style[sheet][key] = style_name
271
- case @cell_type[sheet][key]
272
- when :float
273
- @cell[sheet][key] = v.to_f
274
- when :string
275
- @cell[sheet][key] = str_v
276
- when :date
277
- #TODO: if table_cell.attributes['date-value'].size != "XXXX-XX-XX".size
278
- if attr(table_cell,'date-value').size != "XXXX-XX-XX".size
279
- #-- dann ist noch eine Uhrzeit vorhanden
280
- #-- "1961-11-21T12:17:18"
281
- @cell[sheet][key] = DateTime.parse(attr(table_cell,'date-value').to_s)
282
- @cell_type[sheet][key] = :datetime
283
- else
284
- @cell[sheet][key] = table_cell.attributes['date-value']
285
- end
286
- when :percentage
287
- @cell[sheet][key] = v.to_f
288
- when :time
289
- hms = v.split(':')
290
- @cell[sheet][key] = hms[0].to_i*3600 + hms[1].to_i*60 + hms[2].to_i
291
- else
292
- @cell[sheet][key] = v
293
- end
294
- end
295
-
296
- # read all cells in the selected sheet
297
- #--
298
- # the following construct means '4 blanks'
299
- # some content <text:s text:c="3"/>
300
- #++
301
- def read_cells(sheet=nil)
302
- sheet ||= @default_sheet
303
- validate_sheet!(sheet)
304
- return if @cells_read[sheet]
305
-
306
- sheet_found = false
307
- @doc.xpath("//*[local-name()='table']").each do |ws|
308
- if sheet == attr(ws,'name')
309
- sheet_found = true
310
- col = 1
311
- row = 1
312
- ws.children.each do |table_element|
313
- case table_element.name
314
- when 'table-column'
315
- @style_defaults[sheet] << table_element.attributes['default-cell-style-name']
316
- when 'table-row'
317
- if table_element.attributes['number-rows-repeated']
318
- skip_row = attr(table_element,'number-rows-repeated').to_s.to_i
319
- row = row + skip_row - 1
320
- end
321
- table_element.children.each do |cell|
322
- skip_col = attr(cell, 'number-columns-repeated')
323
- formula = attr(cell,'formula')
324
- value_type = attr(cell,'value-type')
325
- v = attr(cell,'value')
326
- style_name = attr(cell,'style-name')
327
- case value_type
328
- when 'string'
329
- str_v = ''
330
- # insert \n if there is more than one paragraph
331
- para_count = 0
332
- cell.children.each do |str|
333
- # begin comments
334
- =begin
335
- - <table:table-cell office:value-type="string">
336
- - <office:annotation office:display="true" draw:style-name="gr1" draw:text-style-name="P1" svg:width="1.1413in" svg:height="0.3902in" svg:x="2.0142in" svg:y="0in" draw:caption-point-x="-0.2402in" draw:caption-point-y="0.5661in">
337
- <dc:date>2011-09-20T00:00:00</dc:date>
338
- <text:p text:style-name="P1">Kommentar fuer B4</text:p>
339
- </office:annotation>
340
- <text:p>B4 (mit Kommentar)</text:p>
341
- </table:table-cell>
342
- =end
343
- if str.name == 'annotation'
344
- str.children.each do |annotation|
345
- if annotation.name == 'p'
346
- # @comment ist ein Hash mit Sheet als Key (wie bei @cell)
347
- # innerhalb eines Elements besteht ein Eintrag aus einem
348
- # weiteren Hash mit Key [row,col] und dem eigentlichen
349
- # Kommentartext als Inhalt
350
- @comment[sheet] = Hash.new unless @comment[sheet]
351
- key = [row,col]
352
- @comment[sheet][key] = annotation.text
353
- end
354
- end
355
- end
356
- # end comments
357
- if str.name == 'p'
358
- v = str.content
359
- str_v += "\n" if para_count > 0
360
- para_count += 1
361
- if str.children.size > 1
362
- str_v += children_to_string(str.children)
363
- else
364
- str.children.each do |child|
365
- str_v += child.content #.text
366
- end
367
- end
368
- str_v.gsub!(/&apos;/,"'") # special case not supported by unescapeHTML
369
- str_v = CGI.unescapeHTML(str_v)
370
- end # == 'p'
371
- end
372
- when 'time'
373
- cell.children.each do |str|
374
- if str.name == 'p'
375
- v = str.content
376
- end
377
- end
378
- when '', nil
379
- #
380
- when 'date'
381
- #
382
- when 'percentage'
383
- #
384
- when 'float'
385
- #
386
- when 'boolean'
387
- v = attr(cell,'boolean-value').to_s
388
- else
389
- # raise "unknown type #{value_type}"
390
- end
391
- if skip_col
392
- if v != nil or cell.attributes['date-value']
393
- 0.upto(skip_col.to_i-1) do |i|
394
- set_cell_values(sheet,col,row,i,v,value_type,formula,cell,str_v,style_name)
395
- end
396
- end
397
- col += (skip_col.to_i - 1)
398
- end # if skip
399
- set_cell_values(sheet,col,row,0,v,value_type,formula,cell,str_v,style_name)
400
- col += 1
401
- end
402
- row += 1
403
- col = 1
404
- end
405
- end
406
- end
407
- end
408
- @doc.xpath("//*[local-name()='automatic-styles']").each do |style|
409
- read_styles(style)
410
- end
411
- if !sheet_found
412
- raise RangeError
413
- end
414
- @cells_read[sheet] = true
415
- @comments_read[sheet] = true
416
- end
417
-
418
- # Only calls read_cells because Roo::Base calls read_comments
419
- # whereas the reading of comments is done in read_cells for Roo::OpenOffice-objects
420
- def read_comments(sheet=nil)
421
- read_cells(sheet)
422
- end
423
-
424
- def read_labels
425
- @label ||= Hash[@doc.xpath("//table:named-range").map do |ne|
426
- #-
427
- # $Sheet1.$C$5
428
- #+
429
- name = attr(ne,'name').to_s
430
- sheetname,coords = attr(ne,'cell-range-address').to_s.split('.$')
431
- col, row = coords.split('$')
432
- sheetname = sheetname[1..-1] if sheetname[0,1] == '$'
433
- [name, [sheetname,row,col]]
434
- end]
435
- end
436
-
437
- def read_styles(style_elements)
438
- @style_definitions['Default'] = Roo::OpenOffice::Font.new
439
- style_elements.each do |style|
440
- next unless style.name == 'style'
441
- style_name = attr(style,'name')
442
- style.each do |properties|
443
- font = Roo::OpenOffice::Font.new
444
- font.bold = attr(properties,'font-weight')
445
- font.italic = attr(properties,'font-style')
446
- font.underline = attr(properties,'text-underline-style')
447
- @style_definitions[style_name] = font
448
- end
449
- end
450
- end
451
-
452
- A_ROO_TYPE = {
453
- "float" => :float,
454
- "string" => :string,
455
- "date" => :date,
456
- "percentage" => :percentage,
457
- "time" => :time,
458
- }
459
-
460
- def self.oo_type_2_roo_type(ootype)
461
- return A_ROO_TYPE[ootype]
462
- end
463
-
464
- # helper method to convert compressed spaces and other elements within
465
- # an text into a string
466
- def children_to_string(children)
467
- result = ''
468
- children.each {|child|
469
- if child.text?
470
- result = result + child.content
471
- else
472
- if child.name == 's'
473
- compressed_spaces = child.attributes['c'].to_s.to_i
474
- # no explicit number means a count of 1:
475
- if compressed_spaces == 0
476
- compressed_spaces = 1
477
- end
478
- result = result + " "*compressed_spaces
479
- else
480
- result = result + child.content
481
- end
482
- end
483
- }
484
- result
485
- end
486
-
487
- def attr(node, attr_name)
488
- if node.attributes[attr_name]
489
- node.attributes[attr_name].value
490
- end
491
- end
492
- end # class
493
-
494
- # LibreOffice is just an alias for Roo::OpenOffice class
495
- class Roo::LibreOffice < Roo::OpenOffice
496
- end
1
+ require 'date'
2
+ require 'nokogiri'
3
+ require 'cgi'
4
+ require 'zip/filesystem'
5
+ require 'roo/font'
6
+
7
+ class Roo::OpenOffice < Roo::Base
8
+ # initialization and opening of a spreadsheet file
9
+ # values for packed: :zip
10
+ def initialize(filename, options={})
11
+ packed = options[:packed]
12
+ file_warning = options[:file_warning] || :error
13
+
14
+ @only_visible_sheets = options[:only_visible_sheets]
15
+ file_type_check(filename,'.ods','an Roo::OpenOffice', file_warning, packed)
16
+ @tmpdir = make_tmpdir(filename.split('/').last, options[:tmpdir_root])
17
+ @filename = local_filename(filename, @tmpdir, packed)
18
+ #TODO: @cells_read[:default] = false
19
+ Zip::File.open(@filename) do |zip_file|
20
+ if content_entry = zip_file.glob("content.xml").first
21
+ roo_content_xml_path = File.join(@tmpdir, 'roo_content.xml')
22
+ content_entry.extract(roo_content_xml_path)
23
+ decrypt_if_necessary(
24
+ zip_file,
25
+ content_entry,
26
+ roo_content_xml_path,
27
+ options
28
+ )
29
+ else
30
+ raise ArgumentError, 'file missing required content.xml'
31
+ end
32
+ end
33
+ super(filename, options)
34
+ @formula = Hash.new
35
+ @style = Hash.new
36
+ @style_defaults = Hash.new { |h,k| h[k] = [] }
37
+ @table_display = Hash.new { |h,k| h[k] = true }
38
+ @font_style_definitions = Hash.new
39
+ @comment = Hash.new
40
+ @comments_read = Hash.new
41
+ end
42
+
43
+ def method_missing(m,*args)
44
+ read_labels
45
+ # is method name a label name
46
+ if @label.has_key?(m.to_s)
47
+ row,col = label(m.to_s)
48
+ cell(row,col)
49
+ else
50
+ # call super for methods like #a1
51
+ super
52
+ end
53
+ end
54
+
55
+ # Returns the content of a spreadsheet-cell.
56
+ # (1,1) is the upper left corner.
57
+ # (1,1), (1,'A'), ('A',1), ('a',1) all refers to the
58
+ # cell at the first line and first row.
59
+ def cell(row, col, sheet=nil)
60
+ sheet ||= default_sheet
61
+ read_cells(sheet)
62
+ row,col = normalize(row,col)
63
+ if celltype(row,col,sheet) == :date
64
+ yyyy,mm,dd = @cell[sheet][[row,col]].to_s.split('-')
65
+ return Date.new(yyyy.to_i,mm.to_i,dd.to_i)
66
+ end
67
+ @cell[sheet][[row,col]]
68
+ end
69
+
70
+ # Returns the formula at (row,col).
71
+ # Returns nil if there is no formula.
72
+ # The method #formula? checks if there is a formula.
73
+ def formula(row,col,sheet=nil)
74
+ sheet ||= default_sheet
75
+ read_cells(sheet)
76
+ row,col = normalize(row,col)
77
+ @formula[sheet][[row,col]]
78
+ end
79
+
80
+ # Predicate methods really should return a boolean
81
+ # value. Hopefully no one was relying on the fact that this
82
+ # previously returned either nil/formula
83
+ def formula?(*args)
84
+ !!formula(*args)
85
+ end
86
+
87
+ # returns each formula in the selected sheet as an array of elements
88
+ # [row, col, formula]
89
+ def formulas(sheet=nil)
90
+ sheet ||= default_sheet
91
+ read_cells(sheet)
92
+ if @formula[sheet]
93
+ @formula[sheet].each.collect do |elem|
94
+ [elem[0][0], elem[0][1], elem[1]]
95
+ end
96
+ else
97
+ []
98
+ end
99
+ end
100
+
101
+ # Given a cell, return the cell's style
102
+ def font(row, col, sheet=nil)
103
+ sheet ||= default_sheet
104
+ read_cells(sheet)
105
+ row,col = normalize(row,col)
106
+ style_name = @style[sheet][[row,col]] || @style_defaults[sheet][col - 1] || 'Default'
107
+ @font_style_definitions[style_name]
108
+ end
109
+
110
+ # returns the type of a cell:
111
+ # * :float
112
+ # * :string
113
+ # * :date
114
+ # * :percentage
115
+ # * :formula
116
+ # * :time
117
+ # * :datetime
118
+ def celltype(row,col,sheet=nil)
119
+ sheet ||= default_sheet
120
+ read_cells(sheet)
121
+ row,col = normalize(row,col)
122
+ if @formula[sheet][[row,col]]
123
+ return :formula
124
+ else
125
+ @cell_type[sheet][[row,col]]
126
+ end
127
+ end
128
+
129
+ def sheets
130
+ unless @table_display.any?
131
+ doc.xpath("//*[local-name()='automatic-styles']").each do |style|
132
+ read_table_styles(style)
133
+ end
134
+ end
135
+ doc.xpath("//*[local-name()='table']").map do |sheet|
136
+ if !@only_visible_sheets || @table_display[attr(sheet,'style-name')]
137
+ sheet.attributes["name"].value
138
+ end
139
+ end.compact
140
+ end
141
+
142
+ # version of the Roo::OpenOffice document
143
+ # at 2007 this is always "1.0"
144
+ def officeversion
145
+ oo_version
146
+ @officeversion
147
+ end
148
+
149
+ # shows the internal representation of all cells
150
+ # mainly for debugging purposes
151
+ def to_s(sheet=nil)
152
+ sheet ||= default_sheet
153
+ read_cells(sheet)
154
+ @cell[sheet].inspect
155
+ end
156
+
157
+ # returns the row,col values of the labelled cell
158
+ # (nil,nil) if label is not defined
159
+ def label(labelname)
160
+ read_labels
161
+ unless @label.size > 0
162
+ return nil,nil,nil
163
+ end
164
+ if @label.has_key? labelname
165
+ return @label[labelname][1].to_i,
166
+ ::Roo::Utils.letter_to_number(@label[labelname][2]),
167
+ @label[labelname][0]
168
+ else
169
+ return nil,nil,nil
170
+ end
171
+ end
172
+
173
+ # Returns an array which all labels. Each element is an array with
174
+ # [labelname, [row,col,sheetname]]
175
+ def labels(sheet=nil)
176
+ read_labels
177
+ @label.map do |label|
178
+ [ label[0], # name
179
+ [ label[1][1].to_i, # row
180
+ ::Roo::Utils.letter_to_number(label[1][2]), # column
181
+ label[1][0], # sheet
182
+ ] ]
183
+ end
184
+ end
185
+
186
+ # returns the comment at (row/col)
187
+ # nil if there is no comment
188
+ def comment(row,col,sheet=nil)
189
+ sheet ||= default_sheet
190
+ read_cells(sheet)
191
+ row,col = normalize(row,col)
192
+ return nil unless @comment[sheet]
193
+ @comment[sheet][[row,col]]
194
+ end
195
+
196
+ # returns each comment in the selected sheet as an array of elements
197
+ # [row, col, comment]
198
+ def comments(sheet=nil)
199
+ sheet ||= default_sheet
200
+ read_comments(sheet) unless @comments_read[sheet]
201
+ if @comment[sheet]
202
+ @comment[sheet].each.collect do |elem|
203
+ [elem[0][0],elem[0][1],elem[1]]
204
+ end
205
+ else
206
+ []
207
+ end
208
+ end
209
+
210
+ private
211
+
212
+ # If the ODS file has an encryption-data element, then try to decrypt.
213
+ # If successful, the temporary content.xml will be overwritten with
214
+ # decrypted contents.
215
+ def decrypt_if_necessary(
216
+ zip_file,
217
+ content_entry,
218
+ roo_content_xml_path, options
219
+ )
220
+ # Check if content.xml is encrypted by extracting manifest.xml
221
+ # and searching for a manifest:encryption-data element
222
+
223
+ if manifest_entry = zip_file.glob("META-INF/manifest.xml").first
224
+ roo_manifest_xml_path = File.join(@tmpdir, "roo_manifest.xml")
225
+ manifest_entry.extract(roo_manifest_xml_path)
226
+ manifest = ::Roo::Utils.load_xml(roo_manifest_xml_path)
227
+
228
+ # XPath search for manifest:encryption-data only for the content.xml
229
+ # file
230
+
231
+ encryption_data = manifest.xpath(
232
+ "//manifest:file-entry[@manifest:full-path='content.xml']"\
233
+ "/manifest:encryption-data"
234
+ ).first
235
+
236
+ # If XPath returns a node, then we know content.xml is encrypted
237
+
238
+ if !encryption_data.nil?
239
+
240
+ # Since we know it's encrypted, we check for the password option
241
+ # and if it doesn't exist, raise an argument error
242
+
243
+ password = options[:password]
244
+ if !password.nil?
245
+ perform_decryption(
246
+ encryption_data,
247
+ password,
248
+ content_entry,
249
+ roo_content_xml_path
250
+ )
251
+ else
252
+ raise ArgumentError,
253
+ 'file is encrypted but password was not supplied'
254
+ end
255
+ end
256
+ else
257
+ raise ArgumentError, 'file missing required META-INF/manifest.xml'
258
+ end
259
+ end
260
+
261
+ # Process the ODS encryption manifest and perform the decryption
262
+ def perform_decryption(
263
+ encryption_data,
264
+ password,
265
+ content_entry,
266
+ roo_content_xml_path
267
+ )
268
+ # Extract various expected attributes from the manifest that
269
+ # describe the encryption
270
+
271
+ algorithm_node = encryption_data.xpath("manifest:algorithm").first
272
+ key_derivation_node =
273
+ encryption_data.xpath("manifest:key-derivation").first
274
+ start_key_generation_node =
275
+ encryption_data.xpath("manifest:start-key-generation").first
276
+
277
+ # If we have all the expected elements, then we can perform
278
+ # the decryption.
279
+
280
+ if !algorithm_node.nil? && !key_derivation_node.nil? &&
281
+ !start_key_generation_node.nil?
282
+
283
+ # The algorithm is a URI describing the algorithm used
284
+ algorithm = algorithm_node['manifest:algorithm-name']
285
+
286
+ # The initialization vector is base-64 encoded
287
+ iv = Base64.decode64(
288
+ algorithm_node['manifest:initialisation-vector']
289
+ )
290
+ key_derivation_name =
291
+ key_derivation_node['manifest:key-derivation-name']
292
+ key_size = key_derivation_node['manifest:key-size'].to_i
293
+ iteration_count =
294
+ key_derivation_node['manifest:iteration-count'].to_i
295
+ salt = Base64.decode64(key_derivation_node['manifest:salt'])
296
+
297
+ # The key is hashed with an algorithm represented by this URI
298
+ key_generation_name =
299
+ start_key_generation_node[
300
+ 'manifest:start-key-generation-name'
301
+ ]
302
+ key_generation_size =
303
+ start_key_generation_node['manifest:key-size'].to_i
304
+
305
+ hashed_password = password
306
+ key = nil
307
+
308
+ if key_generation_name.eql?(
309
+ "http://www.w3.org/2000/09/xmldsig#sha256"
310
+ )
311
+ hashed_password = Digest::SHA256.digest(password)
312
+ else
313
+ raise ArgumentError, 'Unknown key generation algorithm ' +
314
+ key_generation_name
315
+ end
316
+
317
+ cipher = find_cipher(
318
+ algorithm,
319
+ key_derivation_name,
320
+ hashed_password,
321
+ salt,
322
+ iteration_count,
323
+ iv
324
+ )
325
+
326
+ begin
327
+ decrypted = decrypt(content_entry, cipher)
328
+
329
+ # Finally, inflate the decrypted stream and overwrite
330
+ # content.xml
331
+ IO.binwrite(
332
+ roo_content_xml_path,
333
+ Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(decrypted)
334
+ )
335
+ rescue StandardError => error
336
+ raise ArgumentError,
337
+ 'Invalid password or other data error: ' + error.to_s
338
+ end
339
+ else
340
+ raise ArgumentError,
341
+ 'manifest.xml missing encryption-data elements'
342
+ end
343
+ end
344
+
345
+ # Create a cipher based on an ODS algorithm URI from manifest.xml
346
+ def find_cipher(
347
+ algorithm,
348
+ key_derivation_name,
349
+ hashed_password,
350
+ salt,
351
+ iteration_count,
352
+ iv
353
+ )
354
+ cipher = nil
355
+ if algorithm.eql? "http://www.w3.org/2001/04/xmlenc#aes256-cbc"
356
+ cipher = OpenSSL::Cipher.new('AES-256-CBC')
357
+ cipher.decrypt
358
+ cipher.padding = 0
359
+ cipher.key = find_cipher_key(
360
+ cipher,
361
+ key_derivation_name,
362
+ hashed_password,
363
+ salt,
364
+ iteration_count
365
+ )
366
+ cipher.iv = iv
367
+ else
368
+ raise ArgumentError, 'Unknown algorithm ' + algorithm
369
+ end
370
+ cipher
371
+ end
372
+
373
+ # Create a cipher key based on an ODS algorithm string from manifest.xml
374
+ def find_cipher_key(
375
+ cipher,
376
+ key_derivation_name,
377
+ hashed_password,
378
+ salt,
379
+ iteration_count
380
+ )
381
+ if key_derivation_name.eql? "PBKDF2"
382
+ key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(
383
+ hashed_password,
384
+ salt,
385
+ iteration_count,
386
+ cipher.key_len
387
+ )
388
+ else
389
+ raise ArgumentError, 'Unknown key derivation name ' +
390
+ key_derivation_name
391
+ end
392
+ key
393
+ end
394
+
395
+ # Block decrypt raw bytes from the zip file based on the cipher
396
+ def decrypt(content_entry, cipher)
397
+ # Zip::Entry.extract writes a 0-length file when trying
398
+ # to extract an encrypted stream, so we read the
399
+ # raw bytes based on the offset and lengths
400
+ decrypted = ""
401
+ File.open(@filename, "rb") do |zipfile|
402
+ zipfile.seek(
403
+ content_entry.local_header_offset +
404
+ content_entry.calculate_local_header_size
405
+ )
406
+ total_to_read = content_entry.compressed_size
407
+
408
+ block_size = 4096
409
+ block_size = total_to_read if block_size > total_to_read
410
+
411
+ while buffer = zipfile.read(block_size)
412
+ decrypted += cipher.update(buffer)
413
+ total_to_read -= buffer.length
414
+
415
+ break if total_to_read == 0
416
+
417
+ block_size = total_to_read if block_size > total_to_read
418
+ end
419
+ end
420
+
421
+ decrypted + cipher.final
422
+ end
423
+
424
+ def doc
425
+ @doc ||= ::Roo::Utils.load_xml(File.join(@tmpdir, "roo_content.xml"))
426
+ end
427
+
428
+ # read the version of the OO-Version
429
+ def oo_version
430
+ doc.xpath("//*[local-name()='document-content']").each do |office|
431
+ @officeversion = attr(office,'version')
432
+ end
433
+ end
434
+
435
+ # helper function to set the internal representation of cells
436
+ def set_cell_values(sheet,x,y,i,v,value_type,formula,table_cell,str_v,style_name)
437
+ key = [y,x+i]
438
+ @cell_type[sheet] = {} unless @cell_type[sheet]
439
+ @cell_type[sheet][key] = Roo::OpenOffice.oo_type_2_roo_type(value_type)
440
+ @formula[sheet] = {} unless @formula[sheet]
441
+ if formula
442
+ ['of:', 'oooc:'].each do |prefix|
443
+ if formula[0,prefix.length] == prefix
444
+ formula = formula[prefix.length..-1]
445
+ end
446
+ end
447
+ @formula[sheet][key] = formula
448
+ end
449
+ @cell[sheet] = {} unless @cell[sheet]
450
+ @style[sheet] = {} unless @style[sheet]
451
+ @style[sheet][key] = style_name
452
+ case @cell_type[sheet][key]
453
+ when :float
454
+ @cell[sheet][key] = v.to_f
455
+ when :string
456
+ @cell[sheet][key] = str_v
457
+ when :date
458
+ #TODO: if table_cell.attributes['date-value'].size != "XXXX-XX-XX".size
459
+ if attr(table_cell,'date-value').size != "XXXX-XX-XX".size
460
+ #-- dann ist noch eine Uhrzeit vorhanden
461
+ #-- "1961-11-21T12:17:18"
462
+ @cell[sheet][key] = DateTime.parse(attr(table_cell,'date-value').to_s)
463
+ @cell_type[sheet][key] = :datetime
464
+ else
465
+ @cell[sheet][key] = table_cell.attributes['date-value']
466
+ end
467
+ when :percentage
468
+ @cell[sheet][key] = v.to_f
469
+ when :time
470
+ hms = v.split(':')
471
+ @cell[sheet][key] = hms[0].to_i*3600 + hms[1].to_i*60 + hms[2].to_i
472
+ else
473
+ @cell[sheet][key] = v
474
+ end
475
+ end
476
+
477
+ # read all cells in the selected sheet
478
+ #--
479
+ # the following construct means '4 blanks'
480
+ # some content <text:s text:c="3"/>
481
+ #++
482
+ def read_cells(sheet = default_sheet)
483
+ validate_sheet!(sheet)
484
+ return if @cells_read[sheet]
485
+
486
+ sheet_found = false
487
+ doc.xpath("//*[local-name()='table']").each do |ws|
488
+ if sheet == attr(ws,'name')
489
+ sheet_found = true
490
+ col = 1
491
+ row = 1
492
+ ws.children.each do |table_element|
493
+ case table_element.name
494
+ when 'table-column'
495
+ @style_defaults[sheet] << table_element.attributes['default-cell-style-name']
496
+ when 'table-row'
497
+ if table_element.attributes['number-rows-repeated']
498
+ skip_row = attr(table_element,'number-rows-repeated').to_s.to_i
499
+ row = row + skip_row - 1
500
+ end
501
+ table_element.children.each do |cell|
502
+ skip_col = attr(cell, 'number-columns-repeated')
503
+ formula = attr(cell,'formula')
504
+ value_type = attr(cell,'value-type')
505
+ v = attr(cell,'value')
506
+ style_name = attr(cell,'style-name')
507
+ case value_type
508
+ when 'string'
509
+ str_v = ''
510
+ # insert \n if there is more than one paragraph
511
+ para_count = 0
512
+ cell.children.each do |str|
513
+ # begin comments
514
+ =begin
515
+ - <table:table-cell office:value-type="string">
516
+ - <office:annotation office:display="true" draw:style-name="gr1" draw:text-style-name="P1" svg:width="1.1413in" svg:height="0.3902in" svg:x="2.0142in" svg:y="0in" draw:caption-point-x="-0.2402in" draw:caption-point-y="0.5661in">
517
+ <dc:date>2011-09-20T00:00:00</dc:date>
518
+ <text:p text:style-name="P1">Kommentar fuer B4</text:p>
519
+ </office:annotation>
520
+ <text:p>B4 (mit Kommentar)</text:p>
521
+ </table:table-cell>
522
+ =end
523
+ if str.name == 'annotation'
524
+ str.children.each do |annotation|
525
+ if annotation.name == 'p'
526
+ # @comment ist ein Hash mit Sheet als Key (wie bei @cell)
527
+ # innerhalb eines Elements besteht ein Eintrag aus einem
528
+ # weiteren Hash mit Key [row,col] und dem eigentlichen
529
+ # Kommentartext als Inhalt
530
+ @comment[sheet] = Hash.new unless @comment[sheet]
531
+ key = [row,col]
532
+ @comment[sheet][key] = annotation.text
533
+ end
534
+ end
535
+ end
536
+ # end comments
537
+ if str.name == 'p'
538
+ v = str.content
539
+ str_v += "\n" if para_count > 0
540
+ para_count += 1
541
+ if str.children.size > 1
542
+ str_v += children_to_string(str.children)
543
+ else
544
+ str.children.each do |child|
545
+ str_v += child.content #.text
546
+ end
547
+ end
548
+ str_v.gsub!(/&apos;/,"'") # special case not supported by unescapeHTML
549
+ str_v = CGI.unescapeHTML(str_v)
550
+ end # == 'p'
551
+ end
552
+ when 'time'
553
+ cell.children.each do |str|
554
+ if str.name == 'p'
555
+ v = str.content
556
+ end
557
+ end
558
+ when '', nil, 'date', 'percentage', 'float'
559
+ #
560
+ when 'boolean'
561
+ v = attr(cell,'boolean-value').to_s
562
+ else
563
+ # raise "unknown type #{value_type}"
564
+ end
565
+ if skip_col
566
+ if v != nil or cell.attributes['date-value']
567
+ 0.upto(skip_col.to_i-1) do |i|
568
+ set_cell_values(sheet,col,row,i,v,value_type,formula,cell,str_v,style_name)
569
+ end
570
+ end
571
+ col += (skip_col.to_i - 1)
572
+ end # if skip
573
+ set_cell_values(sheet,col,row,0,v,value_type,formula,cell,str_v,style_name)
574
+ col += 1
575
+ end
576
+ row += 1
577
+ col = 1
578
+ end
579
+ end
580
+ end
581
+ end
582
+ doc.xpath("//*[local-name()='automatic-styles']").each do |style|
583
+ read_styles(style)
584
+ end
585
+ if !sheet_found
586
+ raise RangeError
587
+ end
588
+ @cells_read[sheet] = true
589
+ @comments_read[sheet] = true
590
+ end
591
+
592
+ # Only calls read_cells because Roo::Base calls read_comments
593
+ # whereas the reading of comments is done in read_cells for Roo::OpenOffice-objects
594
+ def read_comments(sheet=nil)
595
+ read_cells(sheet)
596
+ end
597
+
598
+ def read_labels
599
+ @label ||= Hash[doc.xpath("//table:named-range").map do |ne|
600
+ #-
601
+ # $Sheet1.$C$5
602
+ #+
603
+ name = attr(ne,'name').to_s
604
+ sheetname,coords = attr(ne,'cell-range-address').to_s.split('.$')
605
+ col, row = coords.split('$')
606
+ sheetname = sheetname[1..-1] if sheetname[0,1] == '$'
607
+ [name, [sheetname,row,col]]
608
+ end]
609
+ end
610
+
611
+ def read_styles(style_elements)
612
+ @font_style_definitions['Default'] = Roo::Font.new
613
+ style_elements.each do |style|
614
+ next unless style.name == 'style'
615
+ style_name = attr(style,'name')
616
+ style.each do |properties|
617
+ font = Roo::OpenOffice::Font.new
618
+ font.bold = attr(properties,'font-weight')
619
+ font.italic = attr(properties,'font-style')
620
+ font.underline = attr(properties,'text-underline-style')
621
+ @font_style_definitions[style_name] = font
622
+ end
623
+ end
624
+ end
625
+
626
+ def read_table_styles(styles)
627
+ styles.children.each do |style|
628
+ next unless style.name == 'style'
629
+ style_name = attr(style,'name')
630
+ style.children.each do |properties|
631
+ display = attr(properties,'display')
632
+ next unless display
633
+ @table_display[style_name] = (display == 'true')
634
+ end
635
+ end
636
+ end
637
+
638
+ A_ROO_TYPE = {
639
+ "float" => :float,
640
+ "string" => :string,
641
+ "date" => :date,
642
+ "percentage" => :percentage,
643
+ "time" => :time,
644
+ }
645
+
646
+ def self.oo_type_2_roo_type(ootype)
647
+ return A_ROO_TYPE[ootype]
648
+ end
649
+
650
+ # helper method to convert compressed spaces and other elements within
651
+ # an text into a string
652
+ def children_to_string(children)
653
+ result = ''
654
+ children.each {|child|
655
+ if child.text?
656
+ result = result + child.content
657
+ else
658
+ if child.name == 's'
659
+ compressed_spaces = child.attributes['c'].to_s.to_i
660
+ # no explicit number means a count of 1:
661
+ if compressed_spaces == 0
662
+ compressed_spaces = 1
663
+ end
664
+ result = result + " "*compressed_spaces
665
+ else
666
+ result = result + child.content
667
+ end
668
+ end
669
+ }
670
+ result
671
+ end
672
+
673
+ def attr(node, attr_name)
674
+ if node.attributes[attr_name]
675
+ node.attributes[attr_name].value
676
+ end
677
+ end
678
+ end