calco 0.1.1 → 0.2.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 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