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 +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +59 -5
- data/examples/ages.ods +0 -0
- data/examples/report_card.rb +97 -0
- data/examples/report_cards.ods +0 -0
- data/examples/report_cards.rb +155 -0
- data/lib/calco/engines/csv_engine.rb +1 -1
- data/lib/calco/engines/default_engine.rb +1 -1
- data/lib/calco/engines/office_engine.rb +67 -146
- data/lib/calco/engines/office_file_manager.rb +232 -0
- data/lib/calco/sheet.rb +1 -1
- data/lib/calco/version.rb +1 -1
- data/lib/calco/xml_builder.rb +67 -0
- data/spec/xml_builder_spec.rb +89 -0
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 45cf19d0e01a187f98e524114e8610aa4283101d
|
4
|
+
data.tar.gz: e9ccc8b9e84b9be3e2f7c2f02334f1a123e8fbdc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a2331aaf93e90c836a73631d2f0b079fc8b73b95fa84a78f94f97732b4c408e05b658698e316d3d2871cb66b0e231ac34c122d1e3c8c52439050e35efe300f5a
|
7
|
+
data.tar.gz: b1bfb232dcc6157683ee8c62bbd219e0e6fe80758702af3c3b7c839a0d54f25018a066f832e3a3b6ebb36cf6b09b239a945e7028492e4b79bc22dba67f400d8d
|
data/Gemfile.lock
CHANGED
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 `
|
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
|
-
|
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
|
|
data/examples/ages.ods
CHANGED
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)"
|
@@ -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
|
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
|
-
|
24
|
-
result_xml_file = Tempfile.new('office-gen')
|
19
|
+
@file_manager = OfficeFileManager.new(@ods_template)
|
25
20
|
|
26
|
-
|
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
|
-
@
|
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
|
-
@
|
39
|
+
@file_manager.add_row sheet do |stream|
|
40
|
+
|
41
|
+
cells.each_index do |i|
|
62
42
|
|
63
|
-
|
43
|
+
cell = cells[i]
|
64
44
|
|
65
|
-
|
45
|
+
stream.write cell
|
66
46
|
|
67
|
-
|
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 =
|
67
|
+
column_type = 'percentage'
|
96
68
|
elsif column_type =~ /\$([A-Z]{3})/
|
97
|
-
|
98
|
-
|
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
|
-
|
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
|
-
"
|
97
|
+
"&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
|
-
|
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
|
-
|
220
|
-
|
221
|
-
|
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
|
-
|
126
|
+
column_type = 'float' unless column_type
|
228
127
|
|
229
|
-
|
128
|
+
xml << {'table:formula' => "of:=#{value}#{cell_style}"}
|
230
129
|
|
231
|
-
|
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
|
-
|
138
|
+
column_type = 'float' unless column_type
|
234
139
|
|
235
|
-
|
140
|
+
xml << {'office:value' => "#{value}#{cell_style}"} unless cell_style
|
236
141
|
|
237
|
-
|
142
|
+
elsif column.value.is_a?(Date)
|
238
143
|
|
239
|
-
|
144
|
+
column_type = 'date' unless column_type
|
240
145
|
|
241
|
-
|
146
|
+
xml << {'office:date-value' => "#{value}#{cell_style}"} unless cell_style
|
242
147
|
|
243
|
-
|
148
|
+
else
|
244
149
|
|
245
|
-
|
150
|
+
column_type = 'string' unless column_type
|
246
151
|
|
247
|
-
if
|
248
|
-
|
249
|
-
data_iterator.call(doc)
|
250
|
-
@out_stream.write $2
|
152
|
+
if cell_style
|
153
|
+
value = office_string_escape(value)
|
251
154
|
else
|
252
|
-
|
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('"', '"')
|
176
|
+
end
|
177
|
+
|
178
|
+
def office_string_escape str
|
179
|
+
'"' + str.gsub('"', '""') + '"'
|
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
|
data/lib/calco/sheet.rb
CHANGED
data/lib/calco/version.rb
CHANGED
@@ -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("'", ''')
|
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'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.
|
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
|
+
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
|