entable 0.0.1
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.
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +110 -0
- data/Rakefile +1 -0
- data/entable.gemspec +26 -0
- data/lib/entable.rb +16 -0
- data/lib/entable/html_builder.rb +101 -0
- data/lib/entable/transformer.rb +10 -0
- data/lib/entable/version.rb +3 -0
- data/lib/entable/wrapper.rb +11 -0
- data/lib/entable/xls_export.rb +68 -0
- data/spec/entable/entable_spec.rb +124 -0
- data/spec/spec_helper.rb +64 -0
- metadata +83 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 conanite
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# Entable
|
2
|
+
|
3
|
+
LibreOffice and Microsoft Office are both able to open a HTML file and interpret the contents of the <table> element as a worksheet.
|
4
|
+
|
5
|
+
This gem generates such a HTML file, given a collection and a configuration. For each column, the configuration specifies the column header text, and how to extract the data for each cell in that column.
|
6
|
+
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
gem 'entable'
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
Or install it yourself as:
|
19
|
+
|
20
|
+
$ gem install entable
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
|
25
|
+
Basic usage:
|
26
|
+
|
27
|
+
include 'entable/builder'
|
28
|
+
|
29
|
+
def table_config
|
30
|
+
# return a Hash that you read from somewhere
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_xls items, *args
|
34
|
+
@interpreter ||= build_interpreter(table_config)
|
35
|
+
@interpreter.to_xls items, *args # returns a HTML file as text
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
A configuration for a simple one-row-per-item table should look like this:
|
40
|
+
|
41
|
+
---
|
42
|
+
columns:
|
43
|
+
- title: Last Name
|
44
|
+
content: "%{last}"
|
45
|
+
- title: First Name
|
46
|
+
content: "%{first}"
|
47
|
+
- title: Address
|
48
|
+
content: "%{address}"
|
49
|
+
|
50
|
+
|
51
|
+
A more complex configuration, where you want to filter your collection and wrap each item, producing multiple rows per item, might look like this:
|
52
|
+
|
53
|
+
---
|
54
|
+
preprocess:
|
55
|
+
wrap: contact_export
|
56
|
+
transform: sort_by_last_name
|
57
|
+
multi-row:
|
58
|
+
- - title: Last Name
|
59
|
+
content: "%{last}"
|
60
|
+
- title: First Name
|
61
|
+
content: "%{ first }"
|
62
|
+
- - title: Address
|
63
|
+
content: "%{full_address}"
|
64
|
+
attributes:
|
65
|
+
colspan: 2
|
66
|
+
|
67
|
+
In this example, there are two lines per item, and the single cell on the second line will span two columns. Before anything happens, the collection is transformed by #sort_by_last_name, and then each item is wrapped by #contact_export
|
68
|
+
|
69
|
+
A transformer allows you sort, filter, or transform your collection in any way. The collection passed here is the collection that was passed to #to_xls above. Here's how you install a transformer:
|
70
|
+
|
71
|
+
Entable.add_transformer :sort_by_full_name do |collection|
|
72
|
+
collection.sort { |a, b| a.full_name <=> b.full_name }
|
73
|
+
end
|
74
|
+
|
75
|
+
Or, similarly,
|
76
|
+
|
77
|
+
Entable.add_transformer :sort_by_full_name do |collection|
|
78
|
+
collection.order("full_name ASC")
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
Wrappers are not strictly necessary; you could apply a wrapper inside a transformer.
|
83
|
+
Separating the two frees you to apply each independently.
|
84
|
+
|
85
|
+
The most important purpose of a wrapper is to provide an isolation layer between
|
86
|
+
the table configuration and your objects. Remember, the table configuration can
|
87
|
+
invoke any ruby method on each item, so if you are allowing untrusted parties
|
88
|
+
create a configuration, you need to make sure that only safe methods are exposed.
|
89
|
+
|
90
|
+
A secondary purpose of a wrapper is to expose pre-formatted data values; you might
|
91
|
+
need to provide translations for some fields, or localised versions of numbers and
|
92
|
+
dates.
|
93
|
+
|
94
|
+
To install a wrapper, call Entable#add_wrapper
|
95
|
+
|
96
|
+
Entable.add_wrapper :contact_export do |item, *args|
|
97
|
+
ContactTable.new item, *args
|
98
|
+
end
|
99
|
+
|
100
|
+
In this example, the _item_ is the object to be wrapped, and _*args_ are the arguments passed to #to_xls earlier. This allows you pass any extra parameters to your wrapper that you might need in order to render each item as a row in a spreadsheet.
|
101
|
+
|
102
|
+
|
103
|
+
|
104
|
+
## Contributing
|
105
|
+
|
106
|
+
1. Fork it
|
107
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
108
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
109
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
110
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/entable.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- coding: utf-8; mode: ruby -*-
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
|
6
|
+
require 'entable/version'
|
7
|
+
|
8
|
+
Gem::Specification.new do |gem|
|
9
|
+
gem.name = "entable"
|
10
|
+
gem.version = Entable::VERSION
|
11
|
+
gem.authors = ["conanite"]
|
12
|
+
gem.email = ["conan@conandalton.net"]
|
13
|
+
gem.description = %q{Generate HTML tables which popular spreadsheet software packages know how to read }
|
14
|
+
gem.summary = %q{LibreOffice and Microsoft Office are both able to open a HTML file and interpret the contents of the <table> element as a worksheet.
|
15
|
+
|
16
|
+
This gem generates such a HTML file, given a collection and a configuration. For each column, the configuration specifies the column header text, and how to extract the data for each cell in that column.}
|
17
|
+
|
18
|
+
gem.homepage = "https://github.com/conanite/entable"
|
19
|
+
|
20
|
+
gem.add_development_dependency 'rspec', '~> 2.9'
|
21
|
+
|
22
|
+
gem.files = `git ls-files`.split($/)
|
23
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
24
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
25
|
+
gem.require_paths = ["lib"]
|
26
|
+
end
|
data/lib/entable.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "entable/version"
|
2
|
+
|
3
|
+
module Entable
|
4
|
+
def self.add_transformer name, &block
|
5
|
+
Entable::Transformer.add_transformer name, &block
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.add_wrapper name, &block
|
9
|
+
Entable::Wrapper.add_wrapper name, &block
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'entable/xls_export'
|
14
|
+
require 'entable/html_builder'
|
15
|
+
require 'entable/transformer'
|
16
|
+
require 'entable/wrapper'
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Entable::HtmlBuilder
|
2
|
+
class ContentDefinitionError < StandardError; end
|
3
|
+
|
4
|
+
def parse_column_content str, errors
|
5
|
+
error = false
|
6
|
+
|
7
|
+
str = str.strip.gsub(/%\{[^}]*\}/) { |match|
|
8
|
+
attr = match.gsub(/^%\{/, '').gsub(/\}$/, '').gsub(/-/, '_').strip
|
9
|
+
if attr.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)
|
10
|
+
"%{item.#{attr}}"
|
11
|
+
else
|
12
|
+
errors << "prohibited attribute #{match}"
|
13
|
+
error = true
|
14
|
+
end
|
15
|
+
}
|
16
|
+
|
17
|
+
raise ContentDefinitionError.new if error
|
18
|
+
str
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse_column_definitions conf
|
22
|
+
title_rows = []
|
23
|
+
content_rows = []
|
24
|
+
attribute_rows = []
|
25
|
+
errors = []
|
26
|
+
|
27
|
+
raise "no config: #{conf.inspect}" unless conf.is_a?(Hash)
|
28
|
+
|
29
|
+
rows = conf["multi-row"]
|
30
|
+
if rows.nil?
|
31
|
+
one_row = conf["columns"]
|
32
|
+
rows = one_row ? [one_row] : []
|
33
|
+
end
|
34
|
+
|
35
|
+
rows.each do |columns|
|
36
|
+
titles = []
|
37
|
+
contents = []
|
38
|
+
attrs = []
|
39
|
+
|
40
|
+
columns.each do |column_def|
|
41
|
+
titles << column_def["title"]
|
42
|
+
attrs << (column_def["attributes"] || { })
|
43
|
+
begin
|
44
|
+
contents << parse_column_content(column_def["content"], errors)
|
45
|
+
rescue ContentDefinitionError => e
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
title_rows << titles
|
50
|
+
content_rows << contents
|
51
|
+
attribute_rows << attrs
|
52
|
+
end
|
53
|
+
|
54
|
+
raise errors.join("\n") unless errors.empty?
|
55
|
+
|
56
|
+
preprocess = conf["preprocess"] || { }
|
57
|
+
[title_rows, content_rows, attribute_rows, preprocess["transform"], preprocess["wrap"]]
|
58
|
+
end
|
59
|
+
|
60
|
+
def build_title_rows title_rows, attribute_rows
|
61
|
+
titles = title_rows.zip(attribute_rows).map { |tr, attrs|
|
62
|
+
"<tr>" + tr.zip(attrs).map { |t, attr|
|
63
|
+
colspan_attr = attr["colspan"] ? " colspan=#{attr["colspan"]}" : ""
|
64
|
+
"<td#{colspan_attr}>#{quote_for_xls(to_utf8 t)}</td>"}.join(" ") + "</tr>"
|
65
|
+
}.join("\n")
|
66
|
+
end
|
67
|
+
|
68
|
+
def build_line_interpreter columns, attrs
|
69
|
+
columns.zip(attrs).map { |cell, attr|
|
70
|
+
colspan_attr = attr["colspan"] ? " colspan=#{attr["colspan"]}" : ""
|
71
|
+
"<td#{colspan_attr}>\#{to_utf8(#{cell.inspect})}</td>"
|
72
|
+
}.join(' ').gsub("%{", '#{')
|
73
|
+
end
|
74
|
+
|
75
|
+
def build_interpreter spreadsheet_config
|
76
|
+
title_rows, content_rows, attribute_rows, transformer, wrapper = parse_column_definitions(spreadsheet_config)
|
77
|
+
titles = build_title_rows title_rows, attribute_rows
|
78
|
+
titles = to_utf8 "\n#{titles}\n"
|
79
|
+
|
80
|
+
code = <<CODE
|
81
|
+
|
82
|
+
include Entable::XlsExport
|
83
|
+
|
84
|
+
def to_xls items, *args
|
85
|
+
#{transformer ? "items = Entable::Transformer.apply_transform(items, :#{transformer})" : ""}
|
86
|
+
#{wrapper ? "items = Entable::Wrapper.apply_wrapper :#{wrapper}, items, *args" : ""}
|
87
|
+
#{ "#{xls_html_prologue}#{titles}".inspect } + content(items) + #{xls_html_epilogue.inspect}
|
88
|
+
end
|
89
|
+
|
90
|
+
def lines item
|
91
|
+
"#{content_rows.zip(attribute_rows).map { |row, attrs| "<tr>#{build_line_interpreter row, attrs}</tr>\\n" }.join }"
|
92
|
+
end
|
93
|
+
|
94
|
+
def content(items)
|
95
|
+
items.map { |item| lines(item) }.join
|
96
|
+
end
|
97
|
+
CODE
|
98
|
+
|
99
|
+
Class.new do; class_eval code; end.new
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Entable::Transformer
|
2
|
+
def self.add_transformer name, &block
|
3
|
+
@@transformers ||= { }
|
4
|
+
@@transformers[name.to_sym] = block
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.apply_transform collection, transform_name
|
8
|
+
@@transformers[transform_name.to_sym].call collection
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Entable::Wrapper
|
2
|
+
def self.add_wrapper name, &block
|
3
|
+
@@wrappers ||= { }
|
4
|
+
@@wrappers[name.to_sym] = block
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.apply_wrapper name, items, *args
|
8
|
+
wrapper = @@wrappers[name.to_sym]
|
9
|
+
items.map { |item| wrapper.call(item, *args) }
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module Entable::XlsExport
|
4
|
+
def enc; "UTF-8"; end
|
5
|
+
|
6
|
+
def csv_date date
|
7
|
+
return I18n.l(date, :format => :csv) if date
|
8
|
+
end
|
9
|
+
|
10
|
+
def xls_html_prologue
|
11
|
+
"<html><head><meta content='application/vnd.ms-excel;charset=#{enc}' http-equiv='Content-Type'><meta content='#{enc}' http-equiv='Encoding'></head><body><table>"
|
12
|
+
end
|
13
|
+
|
14
|
+
def xls_html_epilogue
|
15
|
+
"</table></body></html>"
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_utf8 str
|
19
|
+
(str || "").encode(enc)
|
20
|
+
rescue
|
21
|
+
raise "unable to convert '#{str}' to #{enc}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def write_items_as_csv items
|
25
|
+
render :text => items.map(&:to_csv).join("\n"), :content_type => "text/csv; charset=ISO-8859-1"
|
26
|
+
end
|
27
|
+
|
28
|
+
def array_for_xls item
|
29
|
+
item.is_a?(Array) ? item : item.to_csv_array
|
30
|
+
end
|
31
|
+
|
32
|
+
def items_as_xls_string items
|
33
|
+
prologue = xls_html_prologue
|
34
|
+
epilogue = xls_html_epilogue
|
35
|
+
items = items.map { |item| array_for_xls(item) }
|
36
|
+
items = items.map { |item| yield item } if block_given?
|
37
|
+
lines = items.map { |item| "<tr><td>" + item.join("</td><td>") + "</td></tr>" }
|
38
|
+
lines = lines.map { |line| to_utf8 line }
|
39
|
+
prologue + lines.join("\n") + epilogue
|
40
|
+
end
|
41
|
+
|
42
|
+
def write_items_as_xls items, &block
|
43
|
+
write_text_as_xls items_as_xls_string(items, &block)
|
44
|
+
end
|
45
|
+
|
46
|
+
def write_text_as_xls text
|
47
|
+
render :text => text, :content_type => "application/vnd.ms-excel; charset=#{enc}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def quote_for_xls text
|
51
|
+
(text.is_a?(String) && needs_quoting?(text)) ? "=\"#{text}\"" : text
|
52
|
+
end
|
53
|
+
|
54
|
+
def needs_quoting? text
|
55
|
+
text.is_a?(String) && (text.match(/^0\d+$/) || text.match(/^\d+[^\d]+/))
|
56
|
+
end
|
57
|
+
|
58
|
+
def make_filename filename
|
59
|
+
return filename.reject { |x| x.blank? }.join(" ").strip.gsub(/ +/, '-') if filename.is_a?(Array)
|
60
|
+
filename
|
61
|
+
end
|
62
|
+
|
63
|
+
def set_xls_headers filename
|
64
|
+
headers["Content-Type"] = "application/vnd.ms-excel; charset=#{enc}"
|
65
|
+
headers["Content-disposition"] = "attachment; filename=\"#{make_filename filename}.xls\""
|
66
|
+
headers["charset"] = enc
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require File.expand_path('../../spec_helper', __FILE__)
|
4
|
+
|
5
|
+
describe Entable do
|
6
|
+
|
7
|
+
it "should generate a very simple table with one row per item" do
|
8
|
+
config = {
|
9
|
+
"columns" => [
|
10
|
+
{ "title" => "First", "content" => "%{firstname}" },
|
11
|
+
{ "title" => "Last", "content" => "%{lastname}" },
|
12
|
+
{ "title" => "Phone", "content" => "%{phone}" },
|
13
|
+
{ "title" => "Postcode", "content" => "%{postcode}" },
|
14
|
+
]
|
15
|
+
}
|
16
|
+
Exporter.new(config).to_xls(CONTACTS).should == %{<html><head><meta content='application/vnd.ms-excel;charset=UTF-8' http-equiv='Content-Type'><meta content='UTF-8' http-equiv='Encoding'></head><body><table>
|
17
|
+
<tr><td>First</td> <td>Last</td> <td>Phone</td> <td>Postcode</td></tr>
|
18
|
+
<tr><td>Conan</td> <td>Dalton</td> <td>01234567</td> <td>75020</td></tr>
|
19
|
+
<tr><td>Zed</td> <td>Zenumbra</td> <td>999999</td> <td>99999</td></tr>
|
20
|
+
<tr><td>Abraham</td> <td>Aardvark</td> <td>0000000</td> <td>0</td></tr>
|
21
|
+
<tr><td>James</td> <td>Joyce</td> <td>3647583</td> <td>75001</td></tr>
|
22
|
+
</table></body></html>}
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should generate a simple table wrapping each item first" do
|
26
|
+
config = {
|
27
|
+
"preprocess" => {
|
28
|
+
"wrap" => "uppercase"
|
29
|
+
},
|
30
|
+
"columns" => [
|
31
|
+
{ "title" => "First", "content" => "%{firstname}" },
|
32
|
+
{ "title" => "Last", "content" => "%{lastname}" },
|
33
|
+
{ "title" => "Phone", "content" => "%{phone}" },
|
34
|
+
]
|
35
|
+
}
|
36
|
+
Exporter.new(config).to_xls(CONTACTS).should == %{<html><head><meta content='application/vnd.ms-excel;charset=UTF-8' http-equiv='Content-Type'><meta content='UTF-8' http-equiv='Encoding'></head><body><table>
|
37
|
+
<tr><td>First</td> <td>Last</td> <td>Phone</td></tr>
|
38
|
+
<tr><td>CONAN</td> <td>DALTON</td> <td>01234567</td></tr>
|
39
|
+
<tr><td>ZED</td> <td>ZENUMBRA</td> <td>999999</td></tr>
|
40
|
+
<tr><td>ABRAHAM</td> <td>AARDVARK</td> <td>0000000</td></tr>
|
41
|
+
<tr><td>JAMES</td> <td>JOYCE</td> <td>3647583</td></tr>
|
42
|
+
</table></body></html>}
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should generate a simple table sorting items first" do
|
46
|
+
config = {
|
47
|
+
"preprocess" => {
|
48
|
+
"transform" => "sort_by_last_name"
|
49
|
+
},
|
50
|
+
"columns" => [
|
51
|
+
{ "title" => "First", "content" => "%{firstname}" },
|
52
|
+
{ "title" => "Last", "content" => "%{lastname}" },
|
53
|
+
{ "title" => "Phone", "content" => "%{phone}" },
|
54
|
+
]
|
55
|
+
}
|
56
|
+
Exporter.new(config).to_xls(CONTACTS).should == %{<html><head><meta content='application/vnd.ms-excel;charset=UTF-8' http-equiv='Content-Type'><meta content='UTF-8' http-equiv='Encoding'></head><body><table>
|
57
|
+
<tr><td>First</td> <td>Last</td> <td>Phone</td></tr>
|
58
|
+
<tr><td>Abraham</td> <td>Aardvark</td> <td>0000000</td></tr>
|
59
|
+
<tr><td>Conan</td> <td>Dalton</td> <td>01234567</td></tr>
|
60
|
+
<tr><td>James</td> <td>Joyce</td> <td>3647583</td></tr>
|
61
|
+
<tr><td>Zed</td> <td>Zenumbra</td> <td>999999</td></tr>
|
62
|
+
</table></body></html>}
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
it "should generate a table with two rows per item, applying #colspan attribute " do
|
67
|
+
config = {
|
68
|
+
"multi-row" => [
|
69
|
+
[
|
70
|
+
{ "title" => "First", "content" => "%{firstname}" },
|
71
|
+
{ "title" => "Last", "content" => "%{lastname}" },
|
72
|
+
],
|
73
|
+
[
|
74
|
+
{ "title" => "Postcode", "content" => "%{postcode}", "attributes" => { "colspan" => 2 } },
|
75
|
+
]
|
76
|
+
]
|
77
|
+
}
|
78
|
+
Exporter.new(config).to_xls(CONTACTS).should == %{<html><head><meta content='application/vnd.ms-excel;charset=UTF-8' http-equiv='Content-Type'><meta content='UTF-8' http-equiv='Encoding'></head><body><table>
|
79
|
+
<tr><td>First</td> <td>Last</td></tr>
|
80
|
+
<tr><td colspan=2>Postcode</td></tr>
|
81
|
+
<tr><td>Conan</td> <td>Dalton</td></tr>
|
82
|
+
<tr><td colspan=2>75020</td></tr>
|
83
|
+
<tr><td>Zed</td> <td>Zenumbra</td></tr>
|
84
|
+
<tr><td colspan=2>99999</td></tr>
|
85
|
+
<tr><td>Abraham</td> <td>Aardvark</td></tr>
|
86
|
+
<tr><td colspan=2>0</td></tr>
|
87
|
+
<tr><td>James</td> <td>Joyce</td></tr>
|
88
|
+
<tr><td colspan=2>75001</td></tr>
|
89
|
+
</table></body></html>}
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
it "should generate a table with two rows per item, applying #colspan attribute, sorting by last name, uppercasing names " do
|
94
|
+
config = {
|
95
|
+
"preprocess" => {
|
96
|
+
"transform" => "sort_by_last_name",
|
97
|
+
"wrap" => "uppercase"
|
98
|
+
},
|
99
|
+
"multi-row" => [
|
100
|
+
[
|
101
|
+
{ "title" => "First", "content" => "%{firstname}" },
|
102
|
+
{ "title" => "Last", "content" => "%{lastname}" },
|
103
|
+
],
|
104
|
+
[
|
105
|
+
{ "title" => "Postcode", "content" => "%{postcode}", "attributes" => { "colspan" => 2 } },
|
106
|
+
]
|
107
|
+
]
|
108
|
+
}
|
109
|
+
Exporter.new(config).to_xls(CONTACTS).should == %{<html><head><meta content='application/vnd.ms-excel;charset=UTF-8' http-equiv='Content-Type'><meta content='UTF-8' http-equiv='Encoding'></head><body><table>
|
110
|
+
<tr><td>First</td> <td>Last</td></tr>
|
111
|
+
<tr><td colspan=2>Postcode</td></tr>
|
112
|
+
<tr><td>ABRAHAM</td> <td>AARDVARK</td></tr>
|
113
|
+
<tr><td colspan=2>0</td></tr>
|
114
|
+
<tr><td>CONAN</td> <td>DALTON</td></tr>
|
115
|
+
<tr><td colspan=2>75020</td></tr>
|
116
|
+
<tr><td>JAMES</td> <td>JOYCE</td></tr>
|
117
|
+
<tr><td colspan=2>75001</td></tr>
|
118
|
+
<tr><td>ZED</td> <td>ZENUMBRA</td></tr>
|
119
|
+
<tr><td colspan=2>99999</td></tr>
|
120
|
+
</table></body></html>}
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'entable'
|
2
|
+
|
3
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
4
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
5
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
6
|
+
# loaded once.
|
7
|
+
#
|
8
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
9
|
+
RSpec.configure do |config|
|
10
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
11
|
+
config.run_all_when_everything_filtered = true
|
12
|
+
config.filter_run :focus
|
13
|
+
|
14
|
+
# Run specs in random order to surface order dependencies. If you find an
|
15
|
+
# order dependency and want to debug it, you can fix the order by providing
|
16
|
+
# the seed, which is printed after each run.
|
17
|
+
# --seed 1234
|
18
|
+
config.order = 'random'
|
19
|
+
end
|
20
|
+
|
21
|
+
Contact = Struct.new :firstname, :lastname, :phone, :postcode
|
22
|
+
|
23
|
+
CONTACTS = []
|
24
|
+
CONTACTS << Contact.new("Conan", "Dalton", "01234567", "75020")
|
25
|
+
CONTACTS << Contact.new("Zed", "Zenumbra", "999999", "99999")
|
26
|
+
CONTACTS << Contact.new("Abraham", "Aardvark", "0000000", "0")
|
27
|
+
CONTACTS << Contact.new("James", "Joyce", "3647583", "75001")
|
28
|
+
|
29
|
+
class ContactUpper
|
30
|
+
def firstname; contact.firstname.upcase; end
|
31
|
+
def lastname; contact.lastname.upcase; end
|
32
|
+
def phone; contact.phone; end
|
33
|
+
def postcode; contact.postcode; end
|
34
|
+
|
35
|
+
attr_accessor :contact
|
36
|
+
|
37
|
+
def initialize contact
|
38
|
+
self.contact = contact
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
Entable.add_transformer :sort_by_last_name do |collection|
|
43
|
+
collection.sort_by &:lastname
|
44
|
+
end
|
45
|
+
|
46
|
+
Entable.add_wrapper :uppercase do |item|
|
47
|
+
ContactUpper.new item
|
48
|
+
end
|
49
|
+
|
50
|
+
class Exporter
|
51
|
+
include Entable::XlsExport
|
52
|
+
include Entable::HtmlBuilder
|
53
|
+
|
54
|
+
attr_accessor :config
|
55
|
+
|
56
|
+
def initialize config
|
57
|
+
self.config = config
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_xls items
|
61
|
+
build_interpreter(config).to_xls items
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: entable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- conanite
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-04-11 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '2.9'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '2.9'
|
30
|
+
description: ! 'Generate HTML tables which popular spreadsheet software packages know
|
31
|
+
how to read '
|
32
|
+
email:
|
33
|
+
- conan@conandalton.net
|
34
|
+
executables: []
|
35
|
+
extensions: []
|
36
|
+
extra_rdoc_files: []
|
37
|
+
files:
|
38
|
+
- .gitignore
|
39
|
+
- .rspec
|
40
|
+
- Gemfile
|
41
|
+
- LICENSE.txt
|
42
|
+
- README.md
|
43
|
+
- Rakefile
|
44
|
+
- entable.gemspec
|
45
|
+
- lib/entable.rb
|
46
|
+
- lib/entable/html_builder.rb
|
47
|
+
- lib/entable/transformer.rb
|
48
|
+
- lib/entable/version.rb
|
49
|
+
- lib/entable/wrapper.rb
|
50
|
+
- lib/entable/xls_export.rb
|
51
|
+
- spec/entable/entable_spec.rb
|
52
|
+
- spec/spec_helper.rb
|
53
|
+
homepage: https://github.com/conanite/entable
|
54
|
+
licenses: []
|
55
|
+
post_install_message:
|
56
|
+
rdoc_options: []
|
57
|
+
require_paths:
|
58
|
+
- lib
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ! '>='
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ! '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
requirements: []
|
72
|
+
rubyforge_project:
|
73
|
+
rubygems_version: 1.8.24
|
74
|
+
signing_key:
|
75
|
+
specification_version: 3
|
76
|
+
summary: LibreOffice and Microsoft Office are both able to open a HTML file and interpret
|
77
|
+
the contents of the <table> element as a worksheet. This gem generates such a HTML
|
78
|
+
file, given a collection and a configuration. For each column, the configuration
|
79
|
+
specifies the column header text, and how to extract the data for each cell in that
|
80
|
+
column.
|
81
|
+
test_files:
|
82
|
+
- spec/entable/entable_spec.rb
|
83
|
+
- spec/spec_helper.rb
|