calco 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a5707d4a21a4635524c7c6f979f289244416e63d
4
- data.tar.gz: 08b02c592036444641c85f22c323b18eccb4bfcb
3
+ metadata.gz: 45cf19d0e01a187f98e524114e8610aa4283101d
4
+ data.tar.gz: e9ccc8b9e84b9be3e2f7c2f02334f1a123e8fbdc
5
5
  SHA512:
6
- metadata.gz: 8aaab8c7481340e72160a003b50c9c5e784f88835b8fb9d46ee7edec95ae187f2eea0f076297e907411cb1d6890b8bdcac0f078dce1982b8e2e37c0cbf5238f1
7
- data.tar.gz: 16ee8d319c4931296165f878c623b988c41179f67a34a906322d85dab425fab8e4605de3ec2cdd2db32b0b725164e41067b83d54580065c3128d7578fc54e068
6
+ metadata.gz: a2331aaf93e90c836a73631d2f0b079fc8b73b95fa84a78f94f97732b4c408e05b658698e316d3d2871cb66b0e231ac34c122d1e3c8c52439050e35efe300f5a
7
+ data.tar.gz: b1bfb232dcc6157683ee8c62bbd219e0e6fe80758702af3c3b7c839a0d54f25018a066f832e3a3b6ebb36cf6b09b239a945e7028492e4b79bc22dba67f400d8d
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- calco (0.1.0)
4
+ calco (0.1.3)
5
5
  rubyzip (~> 1.1.0)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -234,6 +234,60 @@ See specifications
234
234
  * [spec/csv_engine_spec.rb](spec/csv_engine_spec.rb)
235
235
  * [spec/calculator_engine_spec.rb](spec/calculator_engine_spec.rb)
236
236
 
237
+ ## LibreOffice engine
238
+
239
+ The office engine uses a template file when it writes the output file. It
240
+ searches for each sheet a template sheet in the template file after its name.
241
+
242
+ If the engine finds a template sheet, it removes the content and inserts the
243
+ generated rows. If the template sheet contains the header, the engine does
244
+ not remove it (using the `has_header` directive).
245
+
246
+ ```ruby
247
+ doc.save($stdout) do
248
+
249
+ sheet = doc.current
250
+
251
+ sheet[:some_date] = Date.new(1934, 10, 3)
252
+ sheet.write_row 3
253
+
254
+ sheet[:some_date] = Date.new(2004, 6, 19)
255
+ sheet.write_row 5
256
+
257
+ end
258
+ ```
259
+
260
+ If the engine does not find a template sheet, it appends a new sheet.
261
+
262
+ Here is an example:
263
+
264
+ ```ruby
265
+ engine = Calco::OfficeEngine.new('names.ods')
266
+
267
+ doc = spreadsheet(engine) do
268
+
269
+ definitions do
270
+
271
+ set name: ''
272
+
273
+ end
274
+
275
+ sheet('Main') do
276
+
277
+ has_titles true
278
+
279
+ column value_of(:name)
280
+
281
+ end
282
+
283
+ end
284
+ ```
285
+
286
+ The code creates an office engine and sets a template files named `names.ods`.
287
+
288
+ The spreadsheet definition defines a sheet named _Main_ and says that the
289
+ template sheet contains the header row (see `has_titles true`).
290
+
237
291
  ## Tips
238
292
 
239
293
  ### Titles row...
@@ -246,7 +300,7 @@ returns headers or empty if no header is set (see
246
300
  [spec/header_row_spec.rb](spec/header_row_spec.rb)).
247
301
 
248
302
  A sheet is marked as having a header row (a first row with titles) by using
249
- the `:title` option or the `has_title` method.
303
+ the `:title` option or the `has_titles` method.
250
304
 
251
305
  The use of the header row depends on the engine (the office engine does not
252
306
  write the column titles).
@@ -288,16 +342,14 @@ The time values are real time values, not strings. The formulas are computed.
288
342
 
289
343
  ## Todo
290
344
 
291
- 0. Mutliple sheets and cross-sheet references
292
- 0. office engine
293
- * problems with UTF-8
294
- 0. change styles when generating...
345
+ 1. cross-sheet references
295
346
  2. specs for office engine
296
347
  * currencies
297
348
  * percentages
298
349
  * time
299
350
  * date, now
300
351
  * absolute $A$5
352
+ * styles (both formulas and values)
301
353
  3. CSV engine (using the calculator)
302
354
 
303
355
  ## Done
@@ -351,6 +403,8 @@ create `date_functions.rb`, etc.
351
403
  23. added `empty_row` (in all engines)
352
404
  24. explained examples in top of files
353
405
  25. wrote a gem description, reused examples
406
+ 26. use formula when applying dynamic styles to values
407
+ 27. mutliple sheets
354
408
 
355
409
  ## Contributing
356
410
 
Binary file
@@ -0,0 +1,97 @@
1
+ require 'date'
2
+ require 'tmpdir'
3
+
4
+ #
5
+ # Next example uses the office engine.
6
+ #
7
+ # It saves an office file, in the temporary directory, named "res.ods" and uses
8
+ # a template file named "report_cards.ods"
9
+ #
10
+ # Shows applying styles
11
+ #
12
+ # The example fills an academic report card, such a report contains students'
13
+ # grades for different disciplines. Each sheet cover a different period.
14
+ #
15
+ # Here are the grades:
16
+ # A -> Excellent
17
+ # B -> Very Good
18
+ # C -> Good
19
+ # D -> Acceptable
20
+ # F -> Fail
21
+ #
22
+
23
+ require 'calco/engines/office_engine'
24
+
25
+ output_file = File.join(Dir.tmpdir, "res.ods")
26
+
27
+ def relative_path file
28
+ File.join(File.dirname(__FILE__), file)
29
+ end
30
+
31
+ engine = Calco::OfficeEngine.new(relative_path('report_cards.ods'))
32
+
33
+ doc = spreadsheet(engine) do
34
+
35
+ definitions do
36
+
37
+ set name: ''
38
+
39
+ set art: ''
40
+ set english: ''
41
+ set geography: ''
42
+ set history: ''
43
+ set math: ''
44
+ set music: ''
45
+ set physics: ''
46
+
47
+ end
48
+
49
+ sheet('Period 1') do
50
+
51
+ has_titles true
52
+
53
+ column value_of(:name)
54
+
55
+ highlight_f = _if(current == '"F"', '"fail"', '"default"')
56
+
57
+ column value_of(:art), style: highlight_f
58
+ column value_of(:english), style: highlight_f
59
+ column value_of(:geography), style: highlight_f
60
+ column value_of(:history), style: highlight_f
61
+ column value_of(:history), style: highlight_f
62
+ column value_of(:math), style: highlight_f
63
+ column value_of(:music), style: highlight_f
64
+ column value_of(:physics), style: highlight_f
65
+
66
+ end
67
+
68
+ end
69
+
70
+ Strudent = Struct.new(:name, :art, :english, :geography, :history, :math, :music, :physics)
71
+
72
+ period_1 = [
73
+ Strudent.new('Alan', 'A', 'B', 'C', 'D', 'B', 'B', 'B'),
74
+ Strudent.new('Greg', 'A', 'B', 'C', 'C', 'F', 'A', 'F'),
75
+ Strudent.new('Iñes', 'A', 'A', 'A', 'B', 'A', 'A', 'B'),
76
+ Strudent.new('Jack', 'C', 'C', 'F', 'C', 'C', 'C', 'C'),
77
+ Strudent.new( 'Jim', 'C', 'D', 'D', 'F', 'C', 'B', 'D'),
78
+ Strudent.new('Luis', 'A', 'A', 'D', 'A', 'A', 'B', 'B'),
79
+ Strudent.new('Phil', 'B', 'A', 'F', 'D', 'D', 'B', 'F'),
80
+ Strudent.new( 'Tom', 'C', 'F', 'C', 'B', 'B', 'D', 'C'),
81
+ ]
82
+
83
+ doc.save(output_file) do |spreadsheet|
84
+
85
+ sheet = doc.sheet["Period 1"]
86
+
87
+ period_1.each_with_index do |student, i|
88
+
89
+ sheet.record_assign student.to_h
90
+
91
+ sheet.write_row i + 1
92
+
93
+ end
94
+
95
+ end
96
+
97
+ puts "Wrote #{output_file} (1st period)"
Binary file
@@ -0,0 +1,155 @@
1
+ require 'date'
2
+ require 'tmpdir'
3
+
4
+ #
5
+ # Next example uses the office engine.
6
+ #
7
+ # It saves an office file, in the temporary directory, named "res.ods" and uses
8
+ # a template file named "report_cards.ods"
9
+ #
10
+ # Shows writing several sheets + applying styles + appending a new sheet (named
11
+ # "Info")
12
+ #
13
+ # The example fills academic report cards, such reports contain students'
14
+ # grades for different disciplines. Each sheet cover a different period.
15
+ #
16
+ # Here are the grades:
17
+ # A -> Excellent
18
+ # B -> Very Good
19
+ # C -> Good
20
+ # D -> Acceptable
21
+ # F -> Fail
22
+ #
23
+
24
+ require 'calco/engines/office_engine'
25
+
26
+ output_file = File.join(Dir.tmpdir, "res.ods")
27
+
28
+ def relative_path file
29
+ File.join(File.dirname(__FILE__), file)
30
+ end
31
+
32
+ engine = Calco::OfficeEngine.new(relative_path('report_cards.ods'))
33
+
34
+ doc = spreadsheet(engine) do
35
+
36
+ definitions do
37
+
38
+ set name: ''
39
+
40
+ set art: ''
41
+ set english: ''
42
+ set geography: ''
43
+ set history: ''
44
+ set math: ''
45
+ set music: ''
46
+ set physics: ''
47
+
48
+ set author: 'Author'
49
+ set updated_at: 'Last update'
50
+
51
+ end
52
+
53
+ sheet('Period 1') do
54
+
55
+ has_titles true
56
+
57
+ column value_of(:name)
58
+
59
+ highlight_f = _if(current == '"F"', '"fail"', '"default"')
60
+
61
+ column value_of(:art), style: highlight_f
62
+ column value_of(:english), style: highlight_f
63
+ column value_of(:geography), style: highlight_f
64
+ column value_of(:history), style: highlight_f
65
+ column value_of(:history), style: highlight_f
66
+ column value_of(:math), style: highlight_f
67
+ column value_of(:music), style: highlight_f
68
+ column value_of(:physics), style: highlight_f
69
+
70
+ end
71
+
72
+ sheet('Period 2') do
73
+
74
+ has_titles true
75
+
76
+ column value_of(:name)
77
+
78
+ highlight_f = _if(current == '"F"', '"fail"', '"default"')
79
+
80
+ column value_of(:art), style: highlight_f
81
+ column value_of(:english), style: highlight_f
82
+ column value_of(:geography), style: highlight_f
83
+ column value_of(:history), style: highlight_f
84
+ column value_of(:history), style: highlight_f
85
+ column value_of(:math), style: highlight_f
86
+ column value_of(:music), style: highlight_f
87
+ column value_of(:physics), style: highlight_f
88
+
89
+ end
90
+
91
+ sheet('Info') do
92
+
93
+ column value_of(:author)
94
+ column value_of(:updated_at)
95
+
96
+ end
97
+
98
+ end
99
+
100
+ Strudent = Struct.new(:name, :art, :english, :geography, :history, :math, :music, :physics)
101
+
102
+ period_1 = [
103
+ Strudent.new('Alan', 'A', 'B', 'C', 'D', 'B', 'B', 'B'),
104
+ Strudent.new('Greg', 'A', 'B', 'C', 'C', 'F', 'A', 'F'),
105
+ Strudent.new('Iñes', 'A', 'A', 'A', 'B', 'A', 'A', 'B'),
106
+ Strudent.new('Jack', 'C', 'C', 'F', 'C', 'C', 'C', 'C'),
107
+ Strudent.new( 'Jim', 'C', 'D', 'D', 'F', 'C', 'B', 'D'),
108
+ Strudent.new('Luis', 'A', 'A', 'D', 'A', 'A', 'B', 'B'),
109
+ Strudent.new('Phil', 'B', 'A', 'F', 'D', 'D', 'B', 'F'),
110
+ Strudent.new( 'Tom', 'C', 'F', 'C', 'B', 'B', 'D', 'C'),
111
+ ]
112
+ period_2 = [
113
+ Strudent.new('Alan', 'B', 'B', 'B', 'C', 'A', 'B', 'B'),
114
+ Strudent.new('Greg', 'B', 'B', 'C', 'C', 'D', 'A', 'D'),
115
+ Strudent.new('Iñes', 'A', 'A', 'A', 'B', 'A', 'A', 'B'),
116
+ Strudent.new('Jack', 'D', 'B', 'B', 'C', 'B', 'C', 'D'),
117
+ Strudent.new( 'Jim', 'B', 'F', 'D', 'D', 'B', 'B', 'C'),
118
+ Strudent.new('Luis', 'A', 'A', 'B', 'B', 'A', 'B', 'C'),
119
+ Strudent.new('Phil', 'A', 'A', 'D', 'B', 'D', 'B', 'F'),
120
+ Strudent.new( 'Tom', 'C', 'D', 'D', 'D', 'D', 'F', 'B'),
121
+ ]
122
+
123
+ doc.save(output_file) do |spreadsheet|
124
+
125
+ sheet = doc.sheet["Period 1"]
126
+
127
+ period_1.each_with_index do |student, i|
128
+
129
+ sheet.record_assign student.to_h
130
+
131
+ sheet.write_row i + 1
132
+
133
+ end
134
+
135
+ sheet = doc.sheet["Period 2"]
136
+
137
+ period_2.each_with_index do |student, i|
138
+
139
+ sheet.record_assign student.to_h
140
+
141
+ sheet.write_row i + 1
142
+
143
+ end
144
+
145
+ sheet = doc.sheet["Info"]
146
+
147
+ sheet.write_row 0
148
+
149
+ sheet[:author] = 'Jean Lazarou'
150
+ sheet[:updated_at] = Date.today.to_s
151
+ sheet.write_row 1
152
+
153
+ end
154
+
155
+ puts "Wrote #{output_file} (3 sheets)"
@@ -9,7 +9,7 @@ module Calco
9
9
  @col_sep, @quote_char = col_sep, quote_char
10
10
  end
11
11
 
12
- def empty_row
12
+ def empty_row sheet
13
13
  @out_stream.write CSV.generate_line([])
14
14
  end
15
15
 
@@ -25,7 +25,7 @@ module Calco
25
25
 
26
26
  end
27
27
 
28
- def empty_row
28
+ def empty_row sheet
29
29
  end
30
30
 
31
31
  def write_row sheet, row_id
@@ -1,53 +1,31 @@
1
1
  require 'date'
2
2
 
3
- require 'zip'
4
- require 'tmpdir'
5
- require 'pathname'
6
- require 'tempfile'
7
- require 'rexml/document'
8
-
9
3
  require 'calco'
4
+ require 'calco/xml_builder'
5
+
6
+ require_relative 'office_file_manager'
10
7
 
11
8
  module Calco
12
9
 
13
10
  class OfficeEngine < DefaultEngine
14
11
 
15
- def initialize ods_template, first_row_is_header = true
12
+ def initialize ods_template
16
13
  @ods_template = ods_template
17
- @first_row_is_header = first_row_is_header
18
14
  end
19
15
 
20
16
  # output is a String (as a file name)
21
17
  def save doc, to_filename, &data_iterator
22
18
 
23
- content_xml_file = Tempfile.new('office-gen')
24
- result_xml_file = Tempfile.new('office-gen')
19
+ @file_manager = OfficeFileManager.new(@ods_template)
25
20
 
26
- Zip::File.open(@ods_template) do |zipfile|
27
- content = zipfile.read("content.xml")
28
- open(content_xml_file, "w") {|out| out.write content}
29
- end
30
-
31
- write_result_content doc, content_xml_file, result_xml_file, @first_row_is_header, &data_iterator
32
-
33
- FileUtils.cp(@ods_template, to_filename)
34
-
35
- Zip::File.open(to_filename) do |zipfile|
36
-
37
- zipfile.get_output_stream("content.xml") do |os|
38
-
39
- File.open(result_xml_file).each_line do |line|
40
- os.puts line
41
- end
42
-
43
- end
44
-
45
- end
21
+ data_iterator.call(doc)
46
22
 
23
+ @file_manager.save doc, to_filename
24
+
47
25
  end
48
26
 
49
- def empty_row
50
- @out_stream.write '<table:table-row/>'
27
+ def empty_row sheet
28
+ @file_manager.add_empty_row sheet
51
29
  end
52
30
 
53
31
  def write_row sheet, row_id
@@ -58,18 +36,18 @@ module Calco
58
36
 
59
37
  cells = sheet.row(row_id)
60
38
 
61
- @out_stream.write '<table:table-row>'
39
+ @file_manager.add_row sheet do |stream|
40
+
41
+ cells.each_index do |i|
62
42
 
63
- cells.each_index do |i|
43
+ cell = cells[i]
64
44
 
65
- cell = cells[i]
45
+ stream.write cell
66
46
 
67
- @out_stream.write cell
47
+ end
68
48
 
69
49
  end
70
-
71
- @out_stream.write '</table:table-row>'
72
-
50
+
73
51
  end
74
52
 
75
53
  def generate_cell row_number, column, cell_style, column_style, column_type
@@ -77,67 +55,24 @@ module Calco
77
55
  return '<table:table-cell/>' unless column
78
56
  return '<table:table-cell/>' if column.absolute_row && column.absolute_row != row_number
79
57
 
58
+ currency = nil
59
+
80
60
  cell = column.generate(row_number)
81
61
 
82
- if cell_style
83
- cell = cell.to_s + cell_style.generate(row_number)
84
- end
85
-
86
- if column_style
87
- column_style = %[table:style-name="#{column_style}"]
88
- else
89
- column_style = ''
90
- end
62
+ cell_style = cell_style.generate(row_number) if cell_style
91
63
 
92
64
  if column_type
93
65
 
94
66
  if column_type == '%'
95
- column_type = %[office:value-type="percentage"]
67
+ column_type = 'percentage'
96
68
  elsif column_type =~ /\$([A-Z]{3})/
97
- column_type = %[office:value-type="currency" office:currency="#{$1}"]
98
- else
99
- column_type = %[office:value-type="#{column_type}"]
69
+ currency = "#{$1}"
70
+ column_type = 'currency'
100
71
  end
101
72
 
102
73
  end
103
74
 
104
- if column.is_a?(Formula)
105
-
106
- column_type = 'office:value-type="float"' unless column_type
107
-
108
- %[<table:table-cell #{column_style} #{column_type} table:formula="of:=#{cell}" />]
109
-
110
- elsif column.respond_to?(:value)
111
-
112
- if column.value.nil?
113
-
114
- "<table:table-cell/>"
115
-
116
- elsif column.value.is_a?(Numeric)
117
-
118
- column_type = 'office:value-type="float"' unless column_type
119
-
120
- %[<table:table-cell #{column_style} #{column_type} office:value="#{cell}"/>]
121
-
122
- elsif column.value.is_a?(Date)
123
-
124
- column_type = 'office:value-type="date"' unless column_type
125
-
126
- %[<table:table-cell #{column_style} #{column_type} office:date-value="#{cell.to_s}"/>]
127
-
128
- else
129
-
130
- column_type = 'office:value-type="string"' unless column_type
131
-
132
- %[
133
- <table:table-cell #{column_style} #{column_type}>
134
- <text:p><![CDATA[#{cell}]]></text:p>
135
- </table:table-cell>
136
- ]
137
-
138
- end
139
-
140
- end
75
+ office_cell column, column_style, column_type, currency, cell.to_s, cell_style
141
76
 
142
77
  end
143
78
 
@@ -159,7 +94,7 @@ module Calco
159
94
  end
160
95
 
161
96
  def style statement, row
162
- "+ORG.OPENOFFICE.STYLE(#{statement.generate(row)})"
97
+ "&amp;T(ORG.OPENOFFICE.STYLE(#{statement.generate(row)}))"
163
98
  end
164
99
 
165
100
  def operator op
@@ -182,80 +117,66 @@ module Calco
182
117
 
183
118
  private
184
119
 
185
- # returns the parent table and removes template/example rows, also returns
186
- # the first template/example row (to find cell styles for instance)
187
- def retrieve_template_row doc, first_row_is_header
188
-
189
- root = doc.root
190
-
191
- count = 0
192
- template_row = nil
193
-
194
- table = root.elements['//table:table']
195
- table.each_element('table:table-row') do |row|
196
-
197
- if first_row_is_header && count == 0
198
- # keep the header row
199
- else
200
-
201
- table.delete_element(row)
202
-
203
- template_row = row unless template_row
204
-
205
- end
206
-
207
- count += 1
208
-
209
- end
210
-
211
- raise "Cannot find template row in #{@ods_template}" unless template_row
212
-
213
- return table, template_row
214
-
215
- end
216
-
217
- def create_temporary xml, to_filename
120
+ def office_cell column, column_style, column_type, currency, value, cell_style
218
121
 
219
- to = Pathname.new(to_filename)
220
-
221
- temp_file = Tempfile.new('office-gen', to.dirname.to_s)
222
-
223
- File.open(temp_file, 'w') { |stream| stream.puts xml }
224
-
225
- temp_file
122
+ xml = XMLBuilder.new('table:table-cell')
123
+
124
+ if column.is_a?(Formula)
226
125
 
227
- end
126
+ column_type = 'float' unless column_type
228
127
 
229
- def write_result_content doc, content_xml_file, result_xml_file, first_row_is_header, &data_iterator
128
+ xml << {'table:formula' => "of:=#{value}#{cell_style}"}
230
129
 
231
- file = File.new(content_xml_file)
130
+ elsif column.respond_to?(:value)
131
+
132
+ if column.value.nil?
133
+
134
+ return xml.build
135
+
136
+ elsif column.value.is_a?(Numeric)
232
137
 
233
- xml = REXML::Document.new(file)
138
+ column_type = 'float' unless column_type
234
139
 
235
- table, template_row = retrieve_template_row(xml, first_row_is_header)
140
+ xml << {'office:value' => "#{value}#{cell_style}"} unless cell_style
236
141
 
237
- table.add_text "%%%Insert data here%%%\n"
142
+ elsif column.value.is_a?(Date)
238
143
 
239
- temp_file = create_temporary(xml, result_xml_file)
144
+ column_type = 'date' unless column_type
240
145
 
241
- File.open(result_xml_file, 'w') do |stream|
146
+ xml << {'office:date-value' => "#{value}#{cell_style}"} unless cell_style
242
147
 
243
- @out_stream = stream
148
+ else
244
149
 
245
- File.open(temp_file, 'r').each do |line|
150
+ column_type = 'string' unless column_type
246
151
 
247
- if line =~ /(.*)%%%Insert data here%%%(.*)/
248
- @out_stream.write $1
249
- data_iterator.call(doc)
250
- @out_stream.write $2
152
+ if cell_style
153
+ value = office_string_escape(value)
251
154
  else
252
- @out_stream.write line
155
+ xml.add_child 'text:p', office_string_value(value)
253
156
  end
254
-
157
+
255
158
  end
256
159
 
160
+ if cell_style
161
+ xml << {'table:formula' => "of:=#{value}#{cell_style}"}
162
+ end
163
+
257
164
  end
258
165
 
166
+ xml << {'table:style-name' => column_style} if column_style
167
+ xml << {'office:currency' => currency} if currency
168
+ xml << {'office:value-type' => column_type}
169
+
170
+ xml.build
171
+
172
+ end
173
+
174
+ def office_string_value str
175
+ str.gsub('&quot;', '"')
176
+ end
177
+
178
+ def office_string_escape str
179
+ '"' + str.gsub('&quot;', '""') + '"'
259
180
  end
260
181
 
261
182
  end
@@ -0,0 +1,232 @@
1
+ require 'zip'
2
+ require 'tmpdir'
3
+ require 'pathname'
4
+ require 'stringio'
5
+ require 'tempfile'
6
+ require 'rexml/document'
7
+
8
+ module Calco
9
+
10
+ class OfficeFileManager
11
+
12
+ Descriptor = Struct.new(:stream, :file, :new_sheet, :header)
13
+
14
+ def initialize ods_template
15
+
16
+ @ods_template = ods_template
17
+
18
+ @sheets = Hash.new do |h, k|
19
+
20
+ temp_file = Tempfile.new("office-gen-sheet-")
21
+
22
+ stream = open(temp_file, 'w:utf-8')
23
+
24
+ h[k] = Descriptor.new(stream, temp_file, false)
25
+
26
+ end
27
+
28
+ end
29
+
30
+ def save definitions, to_filename
31
+
32
+ @sheets.each do |name, descriptor|
33
+
34
+ descriptor.stream.close
35
+
36
+ end
37
+
38
+ flush_content definitions, to_filename
39
+
40
+ end
41
+
42
+ def add_empty_row sheet
43
+ @sheets[sheet.sheet_name].stream.write '<table:table-row/>'
44
+ end
45
+
46
+ def add_row sheet
47
+
48
+ stream = @sheets[sheet.sheet_name].stream
49
+
50
+ stream.write '<table:table-row>'
51
+
52
+ yield stream
53
+
54
+ stream.write '</table:table-row>'
55
+
56
+ end
57
+
58
+ private
59
+
60
+ def flush_content definitions, to_filename
61
+
62
+ content_xml_file = Tempfile.new('office-gen')
63
+ result_xml_file = Tempfile.new('office-gen')
64
+
65
+ extract_template_content content_xml_file
66
+
67
+ prepare_content_file definitions, content_xml_file, result_xml_file
68
+
69
+ create_file result_xml_file, to_filename
70
+
71
+ end
72
+
73
+ def extract_template_content content_xml_file
74
+
75
+ Zip::File.open(@ods_template) do |zipfile|
76
+ content = zipfile.read("content.xml")
77
+ open(content_xml_file, "w") {|out| out.write content}
78
+ end
79
+
80
+ end
81
+
82
+ def prepare_content_file definitions, content_xml_file, result_xml_file
83
+
84
+ file = File.new(content_xml_file)
85
+
86
+ xml = REXML::Document.new(file)
87
+
88
+ prepare_xml(xml)
89
+
90
+ temp_file = create_temporary(xml, result_xml_file)
91
+
92
+ File.open(result_xml_file, 'w') do |stream|
93
+
94
+ @out_stream = stream
95
+
96
+ File.open(temp_file, 'r').each do |line|
97
+
98
+ if line =~ /\A%%%Insert data here \[(.*)\]%%%\Z/
99
+
100
+ write_sheet $1, definitions
101
+
102
+ elsif line =~ /(.*)(<\/office:spreadsheet>)(.*)/
103
+
104
+ @out_stream.write $1
105
+
106
+ write_new_sheets
107
+
108
+ @out_stream.write $2
109
+ @out_stream.write $3
110
+
111
+ else
112
+
113
+ @out_stream.write line
114
+
115
+ end
116
+
117
+ end
118
+
119
+ end
120
+
121
+ end
122
+
123
+ def create_file result_xml_file, to_filename
124
+
125
+ FileUtils.cp(@ods_template, to_filename)
126
+
127
+ Zip::File.open(to_filename) do |zipfile|
128
+
129
+ zipfile.get_output_stream("content.xml") do |os|
130
+
131
+ File.open(result_xml_file).each_line do |line|
132
+ os.puts line
133
+ end
134
+
135
+ end
136
+
137
+ end
138
+
139
+ end
140
+
141
+ def prepare_xml xml_doc
142
+
143
+ root = xml_doc.root
144
+
145
+ @sheets.each_key do |name|
146
+
147
+ table = root.elements["//table:table[@table:name='#{name}']"]
148
+
149
+ unless table
150
+ @sheets[name].new_sheet = true
151
+ next
152
+ end
153
+
154
+ state = :waiting_header
155
+
156
+ table.each_element('table:table-row') do |row|
157
+
158
+ @sheets[name].header = stringify(row) if state == :waiting_header
159
+
160
+ state = :header_consumed
161
+
162
+ table.delete_element(row)
163
+
164
+ end
165
+
166
+ table.add_text marker(name)
167
+
168
+ end
169
+
170
+ end
171
+
172
+ def write_sheet name, definitions
173
+
174
+ sheet = @sheets[name]
175
+
176
+ if definitions[name].has_titles?
177
+
178
+ if sheet.header
179
+ @out_stream.write sheet.header
180
+ else
181
+ $sdterr.puts "Cannot find template row in #{@ods_template} for #{name}"
182
+ end
183
+
184
+ end
185
+
186
+ @out_stream.write sheet.file.read
187
+
188
+ end
189
+
190
+ def write_new_sheets
191
+
192
+ @sheets.each do |name, descriptor|
193
+
194
+ next unless descriptor.new_sheet
195
+
196
+ @out_stream.write "<table:table table:name='#{name}'>"
197
+ @out_stream.write descriptor.file.read
198
+ @out_stream.write '</table:table>'
199
+
200
+ end
201
+
202
+ end
203
+
204
+ def create_temporary xml, to_filename
205
+
206
+ to = Pathname.new(to_filename)
207
+
208
+ temp_file = Tempfile.new('office-gen', to.dirname.to_s)
209
+
210
+ File.open(temp_file, 'w') { |stream| stream.puts xml }
211
+
212
+ temp_file
213
+
214
+ end
215
+
216
+ def marker(name)
217
+ "\n%%%Insert data here [#{name}]%%%\n"
218
+ end
219
+
220
+ def stringify(row)
221
+
222
+ buffer = StringIO.new
223
+
224
+ REXML::Formatters::Default.new.write(row, buffer)
225
+
226
+ buffer.string
227
+
228
+ end
229
+
230
+ end
231
+
232
+ end
@@ -257,7 +257,7 @@ module Calco
257
257
 
258
258
  # set "next row" as empty, delegated to current engine
259
259
  def empty_row
260
- @engine.empty_row
260
+ @engine.empty_row self
261
261
  end
262
262
 
263
263
  # Calls the passed block for every Element, the cell definition,
@@ -1,3 +1,3 @@
1
1
  module Calco
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -0,0 +1,67 @@
1
+ module Calco
2
+
3
+ class XMLBuilder
4
+
5
+ def initialize tag
6
+ @tag = tag
7
+ @kids = {}
8
+ @attributes = {}
9
+ end
10
+
11
+ # +attributes+ is a hash with key/value pairs, keys are the attribute names
12
+ def << attributes
13
+
14
+ attributes.each do |attribute, value|
15
+ @attributes[attribute] = escape(value)
16
+ end
17
+
18
+ end
19
+
20
+ def attribute name, value
21
+ @attributes[name] = escape(value)
22
+ end
23
+
24
+ # add a child text-node with the given +tag+/+value+
25
+ def add_child tag, value
26
+ @kids[tag] = value
27
+ end
28
+
29
+ def build
30
+
31
+ buffer = "<#{@tag} "
32
+
33
+ return buffer + '/>' if @kids.empty? && @attributes.empty?
34
+
35
+ @attributes.each do |attribute, value|
36
+ buffer << "#{attribute}='#{value}' "
37
+ end
38
+
39
+ if @kids.empty?
40
+ buffer << '/>'
41
+ else
42
+
43
+ buffer << '>'
44
+
45
+ @kids.each do |text_tag, value|
46
+ buffer << "<#{text_tag}><![CDATA[#{value}]]></#{text_tag}>"
47
+ end
48
+
49
+ buffer << "</#{@tag}>"
50
+
51
+ end
52
+
53
+ buffer
54
+
55
+ end
56
+
57
+ def escape str
58
+
59
+ str = str.to_s
60
+
61
+ str.gsub("'", '&apos;')
62
+
63
+ end
64
+
65
+ end
66
+
67
+ end
@@ -0,0 +1,89 @@
1
+ require 'calco/xml_builder'
2
+
3
+ module Calco
4
+
5
+ describe XMLBuilder do
6
+
7
+ it "generates empty tag" do
8
+
9
+ builder = XMLBuilder.new('hello')
10
+
11
+ expect(builder.build).to eq('<hello />')
12
+
13
+ end
14
+
15
+ it "generates tag + one attribute" do
16
+
17
+ builder = XMLBuilder.new('hello')
18
+
19
+ builder << {:name => 'Max'}
20
+
21
+ expect(builder.build).to eq("<hello name='Max' />")
22
+
23
+ end
24
+
25
+ it "generates tag + many attributes" do
26
+
27
+ builder = XMLBuilder.new('hello')
28
+
29
+ builder << {name: 'Max', greeting: 'Bonjour', today: Date.new(2014, 3, 16)}
30
+
31
+ expect(builder.build).to eq("<hello name='Max' greeting='Bonjour' today='2014-03-16' />")
32
+
33
+ end
34
+
35
+ it "generates tag + many attributes (alternate notation)" do
36
+
37
+ builder = XMLBuilder.new('hello')
38
+
39
+ builder.attribute :name, 'Max'
40
+ builder.attribute :greeting, 'Bonjour'
41
+ builder.attribute "today", Date.new(2014, 3, 16)
42
+
43
+ expect(builder.build).to eq("<hello name='Max' greeting='Bonjour' today='2014-03-16' />")
44
+
45
+ end
46
+
47
+ it "overwrites attributes" do
48
+
49
+ builder = XMLBuilder.new('hello')
50
+
51
+ builder.attribute :name, 'Max'
52
+
53
+ builder << {:name => 'Joe'}
54
+
55
+ expect(builder.build).to eq("<hello name='Joe' />")
56
+
57
+ end
58
+
59
+ it "generates tag + attributes + text child nodes" do
60
+
61
+ builder = XMLBuilder.new('hello')
62
+
63
+ builder << {name: 'Max', town: 'London'}
64
+
65
+ builder.add_child "comment", "He's Joe's best friend"
66
+ builder.add_child "info", "He lives somewhere"
67
+
68
+ expect(builder.build).to eq(
69
+ "<hello name='Max' town='London' >" +
70
+ "<comment><![CDATA[He's Joe's best friend]]></comment>" +
71
+ '<info><![CDATA[He lives somewhere]]></info>' +
72
+ '</hello>'
73
+ )
74
+
75
+ end
76
+
77
+ it "escapes attibute values" do
78
+
79
+ builder = XMLBuilder.new('hello')
80
+
81
+ builder << {name: 'Max', last_name: "O'Neil"}
82
+
83
+ expect(builder.build).to eq("<hello name='Max' last_name='O&apos;Neil' />")
84
+
85
+ end
86
+
87
+ end
88
+
89
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: calco
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean Lazarou
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-11-11 00:00:00.000000000 Z
11
+ date: 2014-11-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -61,6 +61,9 @@ files:
61
61
  - examples/multiplication_tables.ods
62
62
  - examples/multiplication_tables.rb
63
63
  - examples/register_function.rb
64
+ - examples/report_card.rb
65
+ - examples/report_cards.ods
66
+ - examples/report_cards.rb
64
67
  - examples/using_date_functions.rb
65
68
  - examples/write_csv.rb
66
69
  - examples/write_ods.rb
@@ -88,6 +91,7 @@ files:
88
91
  - lib/calco/engines/csv_engine.rb
89
92
  - lib/calco/engines/default_engine.rb
90
93
  - lib/calco/engines/office_engine.rb
94
+ - lib/calco/engines/office_file_manager.rb
91
95
  - lib/calco/engines/simple_calculator_engine.rb
92
96
  - lib/calco/math_functions.rb
93
97
  - lib/calco/sheet.rb
@@ -96,6 +100,7 @@ files:
96
100
  - lib/calco/style.rb
97
101
  - lib/calco/time_functions.rb
98
102
  - lib/calco/version.rb
103
+ - lib/calco/xml_builder.rb
99
104
  - spec/absolute_references_spec.rb
100
105
  - spec/builtin_functions_spec.rb
101
106
  - spec/calculator_engine_spec.rb
@@ -114,6 +119,7 @@ files:
114
119
  - spec/spreadsheet_spec.rb
115
120
  - spec/styles_spec.rb
116
121
  - spec/variables_spec.rb
122
+ - spec/xml_builder_spec.rb
117
123
  homepage: https://github.com/jeanlazarou/calco
118
124
  licenses: []
119
125
  metadata: {}
@@ -156,3 +162,4 @@ test_files:
156
162
  - spec/spreadsheet_spec.rb
157
163
  - spec/styles_spec.rb
158
164
  - spec/variables_spec.rb
165
+ - spec/xml_builder_spec.rb